Skip to content

Commit dc1410c

Browse files
ondrejmirtesclaude
andcommitted
Result cache: dependencies on values declared through $scope->valueDependency()
A rule or a dynamic return type extension that reads something the result cache does not know about - a DI container, a config file - declares it with $scope->valueDependency(SomeValueExtension::class, $key). The extension, a ResultCacheValueExtension, returns the value for the key. The value is stored in the result cache, and when it's different on the next run, the files that asked for it are re-analysed. The same value declared more times, by one or several rules and extensions, is recorded once. When it's declared outside the walk of the analysed file, like when a private property type is inferred from the constructor, it is the file of the scope that depends on it, together with the files depending on that file. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdwtgiEHwkF5BTz73jgHQQ
1 parent c51e3d6 commit dc1410c

58 files changed

Lines changed: 1290 additions & 22 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/e2e-tests.yml‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,71 @@ jobs:
442442
echo "$INFO"
443443
../bashunit -a equals 'true' "$(echo "$INFO" | jq --raw-output '.resultCacheUsed')"
444444
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
445+
- script: |
446+
cd e2e/result-cache-value-dependency
447+
composer install
448+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
449+
echo "$OUTPUT"
450+
../bashunit -a contains 'UsesCache.php:7:Service cache does not exist.' "$OUTPUT"
451+
../bashunit -a contains "UsesLocale.php:10:Function ResultCacheE2EValueDependency\usesLocale() should return 'none' but returns 'cs'." "$OUTPUT"
452+
../bashunit -a contains "UsesLocaleToo.php:10:Function ResultCacheE2EValueDependency\usesLocaleToo() should return 'none' but returns 'cs'." "$OUTPUT"
453+
../bashunit -a contains 'Holder.php:17:Method ResultCacheE2EValueDependency\Holder::isDebug() should return int but returns string.' "$OUTPUT"
454+
../bashunit -a contains 'UsesHolder.php:9:Function ResultCacheE2EValueDependency\usesHolder() should return int but returns string.' "$OUTPUT"
455+
../bashunit -a not_contains 'UsesMailer.php' "$OUTPUT"
456+
# The extensions read container.json, which the result cache knows nothing about. A rule and a
457+
# dynamic return type extension both declare the service they ask about - it is recorded once.
458+
# A change of its value re-analyses only the file asking about it.
459+
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["services"]["mailer"] = "ResultCacheE2EValueDependency\\Logger"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
460+
INFO=$(../../bin/phpstan result-cache-info --json)
461+
echo "$INFO"
462+
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
463+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
464+
echo "$OUTPUT"
465+
../bashunit -a contains 'UsesMailer.php:7:Call to an undefined method ResultCacheE2EValueDependency\Logger::send().' "$OUTPUT"
466+
# Values nobody asked about do not re-analyse anything.
467+
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["parameters"]["timezone"] = "UTC"; $c["services"]["router"] = "ResultCacheE2EValueDependency\\Logger"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
468+
INFO=$(../../bin/phpstan result-cache-info --json)
469+
echo "$INFO"
470+
../bashunit -a equals '0' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
471+
# A value that was missing is watched too.
472+
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["services"]["cache"] = "ResultCacheE2EValueDependency\\Logger"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
473+
INFO=$(../../bin/phpstan result-cache-info --json)
474+
echo "$INFO"
475+
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
476+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
477+
echo "$OUTPUT"
478+
../bashunit -a not_contains 'Service cache does not exist.' "$OUTPUT"
479+
# Every file asking about the same value is re-analysed.
480+
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["parameters"]["locale"] = "en"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
481+
INFO=$(../../bin/phpstan result-cache-info --json)
482+
echo "$INFO"
483+
../bashunit -a equals '2' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
484+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
485+
echo "$OUTPUT"
486+
../bashunit -a contains "UsesLocale.php:10:Function ResultCacheE2EValueDependency\usesLocale() should return 'none' but returns 'en'." "$OUTPUT"
487+
../bashunit -a contains "UsesLocaleToo.php:10:Function ResultCacheE2EValueDependency\usesLocaleToo() should return 'none' but returns 'en'." "$OUTPUT"
488+
# Scope::getFile() is not the file to re-analyse when the extension runs outside the walk of the
489+
# analysed file: the type of Holder::$debug is inferred from the constructor on a scope of
490+
# src/Holder.php, whenever the property is first needed, and it is cached. So it is what
491+
# src/Holder.php declares that depends on the value: a change of it re-analyses src/Holder.php and
492+
# the files depending on it.
493+
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["parameters"]["debug"] = "1"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
494+
INFO=$(../../bin/phpstan result-cache-info --json)
495+
echo "$INFO"
496+
../bashunit -a equals '2' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
497+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
498+
echo "$OUTPUT"
499+
../bashunit -a not_contains 'Holder.php' "$OUTPUT"
500+
# Inside a trait, the value is asked about in the analysis of the class using it - that's the
501+
# file re-analysed, and the error is reported in the trait.
502+
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["services"]["logger"] = "ResultCacheE2EValueDependency\\Mailer"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
503+
INFO=$(../../bin/phpstan result-cache-info --json)
504+
echo "$INFO"
505+
../bashunit -a equals '2' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
506+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
507+
echo "$OUTPUT"
508+
../bashunit -a contains 'UsesLogger.php:7:Call to an undefined method ResultCacheE2EValueDependency\Mailer::log().' "$OUTPUT"
509+
../bashunit -a contains 'LoggerTrait.php (in context of class ResultCacheE2EValueDependency\UsesLoggerTrait):10:Call to an undefined method ResultCacheE2EValueDependency\Mailer::log().' "$OUTPUT"
445510
- script: |
446511
cd e2e/result-cache-scanned-trait
447512
../../bin/phpstan analyse
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/vendor
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"autoload-dev": {
3+
"classmap": ["extension/"]
4+
}
5+
}

‎e2e/result-cache-value-dependency/composer.lock‎

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"services": {
3+
"mailer": "ResultCacheE2EValueDependency\\Mailer",
4+
"logger": "ResultCacheE2EValueDependency\\Logger"
5+
},
6+
"parameters": {
7+
"locale": "cs",
8+
"timezone": "Europe/Prague",
9+
"debug": "yes"
10+
}
11+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
/**
6+
* Stands for a compiled DI container the analysed code gets its services and parameters from.
7+
*/
8+
final class Container
9+
{
10+
11+
public static function getService(string $id): ?string
12+
{
13+
return self::read()['services'][$id] ?? null;
14+
}
15+
16+
public static function getParameter(string $name): ?string
17+
{
18+
return self::read()['parameters'][$name] ?? null;
19+
}
20+
21+
/**
22+
* @return array{services: array<string, string>, parameters: array<string, string>}
23+
*/
24+
private static function read(): array
25+
{
26+
$contents = file_get_contents(dirname(__DIR__) . '/container.json');
27+
/** @var array{services: array<string, string>, parameters: array<string, string>} $container */
28+
$container = json_decode($contents === false ? '{}' : $contents, true);
29+
30+
return $container;
31+
}
32+
33+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
use PHPStan\Analyser\ResultCache\ResultCacheValueExtension;
6+
7+
final class HasServiceValueExtension implements ResultCacheValueExtension
8+
{
9+
10+
public function getValue(string $key): string
11+
{
12+
return Container::getService($key) ?? 'missing';
13+
}
14+
15+
public function keyToResultCache(string $key): string
16+
{
17+
return $key;
18+
}
19+
20+
public function keyFromResultCache(string $storedKey): string
21+
{
22+
return $storedKey;
23+
}
24+
25+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PhpParser\Node\Scalar\String_;
7+
use PHPStan\Analyser\DependencyEmitter;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\FunctionReflection;
10+
use PHPStan\Type\Constant\ConstantIntegerType;
11+
use PHPStan\Type\Constant\ConstantStringType;
12+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
13+
use PHPStan\Type\Type;
14+
15+
final class ParameterReturnTypeExtension implements DynamicFunctionReturnTypeExtension
16+
{
17+
18+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
19+
{
20+
return $functionReflection->getName() === 'ResultCacheE2EValueDependency\parameter';
21+
}
22+
23+
/**
24+
* @param Scope&DependencyEmitter $scope
25+
*/
26+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
27+
{
28+
$arg = $functionCall->getArgs()[0]->value ?? null;
29+
if (!$arg instanceof String_) {
30+
return null;
31+
}
32+
33+
$scope->valueDependency(ParameterValueExtension::class, $arg->value);
34+
$value = Container::getParameter($arg->value);
35+
if ($value === null) {
36+
return null;
37+
}
38+
39+
return is_numeric($value) ? new ConstantIntegerType((int) $value) : new ConstantStringType($value);
40+
}
41+
42+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
use PHPStan\Analyser\ResultCache\ResultCacheValueExtension;
6+
7+
final class ParameterValueExtension implements ResultCacheValueExtension
8+
{
9+
10+
public function getValue(string $key): string
11+
{
12+
return Container::getParameter($key) ?? 'missing';
13+
}
14+
15+
public function keyToResultCache(string $key): string
16+
{
17+
return $key;
18+
}
19+
20+
public function keyFromResultCache(string $storedKey): string
21+
{
22+
return $storedKey;
23+
}
24+
25+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PhpParser\Node\Scalar\String_;
7+
use PHPStan\Analyser\DependencyEmitter;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\FunctionReflection;
10+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
11+
use PHPStan\Type\ObjectType;
12+
use PHPStan\Type\Type;
13+
14+
final class ServiceReturnTypeExtension implements DynamicFunctionReturnTypeExtension
15+
{
16+
17+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
18+
{
19+
return $functionReflection->getName() === 'ResultCacheE2EValueDependency\service';
20+
}
21+
22+
/**
23+
* @param Scope&DependencyEmitter $scope
24+
*/
25+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
26+
{
27+
$arg = $functionCall->getArgs()[0]->value ?? null;
28+
if (!$arg instanceof String_) {
29+
return null;
30+
}
31+
32+
// the same value ServiceRule declares - recorded once
33+
$scope->valueDependency(HasServiceValueExtension::class, $arg->value);
34+
$class = Container::getService($arg->value);
35+
36+
return $class !== null ? new ObjectType($class) : null;
37+
}
38+
39+
}

0 commit comments

Comments
 (0)