Skip to content

Commit 68c299c

Browse files
ondrejmirtesclaude
andcommitted
DeclarationDependencyTracker for extensions describing a class
Class reflection extensions and other extensions describing a class get no Scope, and what they declare is remembered and reused by every file analysed after it without asking the extension again. So it's the class that depends on what the extension read, not the file being analysed. DeclarationDependencyTracker, injected into such an extension, records the value, file, directory or class dependency for the class. The values are kept for the whole process, and every file depending on the class - or on a class extending or implementing it - gets them as its own dependencies when its analysis finishes. When one changes, all those files are analysed again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdwtgiEHwkF5BTz73jgHQQ
1 parent 4abe56f commit 68c299c

15 files changed

Lines changed: 372 additions & 4 deletions

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,16 @@ jobs:
579579
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
580580
echo "$OUTPUT"
581581
../bashunit -a contains 'UsesMake.php:7:Function ResultCacheE2EValueDependency\usesMake() should return int but returns string.' "$OUTPUT"
582+
# A class reflection extension tracking on the class: every file using the class is analysed again,
583+
# also one that got the magic method from the reflection cache without asking the extension.
584+
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["parameters"]["greeting"] = "1"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
585+
INFO=$(../../bin/phpstan result-cache-info --json)
586+
echo "$INFO"
587+
../bashunit -a equals '3' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
588+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
589+
echo "$OUTPUT"
590+
../bashunit -a contains 'UsesMagic.php:7:Function ResultCacheE2EValueDependency\usesMagic() should return string but returns int.' "$OUTPUT"
591+
../bashunit -a contains 'UsesMagicToo.php:7:Function ResultCacheE2EValueDependency\usesMagicToo() should return string but returns int.' "$OUTPUT"
582592
- script: |
583593
cd e2e/result-cache-file-dependency
584594
composer install

‎e2e/result-cache-value-dependency/container.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"throwing": "no",
1111
"region": "eu",
1212
"closureThis": "ResultCacheE2EValueDependency\\Mailer",
13-
"allowedClass": "ResultCacheE2EValueDependency\\Mailer"
13+
"allowedClass": "ResultCacheE2EValueDependency\\Mailer",
14+
"greeting": "hello"
1415
}
1516
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
use PHPStan\Reflection\ClassMemberReflection;
6+
use PHPStan\Reflection\ClassReflection;
7+
use PHPStan\Reflection\FunctionVariant;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\TrinaryLogic;
10+
use PHPStan\Type\Generic\TemplateTypeMap;
11+
use PHPStan\Type\Type;
12+
13+
final class MagicMethodReflection implements MethodReflection
14+
{
15+
16+
public function __construct(private ClassReflection $declaringClass, private string $name, private Type $returnType)
17+
{
18+
}
19+
20+
public function getDeclaringClass(): ClassReflection
21+
{
22+
return $this->declaringClass;
23+
}
24+
25+
public function isStatic(): bool
26+
{
27+
return false;
28+
}
29+
30+
public function isPrivate(): bool
31+
{
32+
return false;
33+
}
34+
35+
public function isPublic(): bool
36+
{
37+
return true;
38+
}
39+
40+
public function getDocComment(): ?string
41+
{
42+
return null;
43+
}
44+
45+
public function getName(): string
46+
{
47+
return $this->name;
48+
}
49+
50+
public function getPrototype(): ClassMemberReflection
51+
{
52+
return $this;
53+
}
54+
55+
public function getVariants(): array
56+
{
57+
return [new FunctionVariant(TemplateTypeMap::createEmpty(), null, [], false, $this->returnType)];
58+
}
59+
60+
public function isDeprecated(): TrinaryLogic
61+
{
62+
return TrinaryLogic::createNo();
63+
}
64+
65+
public function getDeprecatedDescription(): ?string
66+
{
67+
return null;
68+
}
69+
70+
public function isFinal(): TrinaryLogic
71+
{
72+
return TrinaryLogic::createNo();
73+
}
74+
75+
public function isInternal(): TrinaryLogic
76+
{
77+
return TrinaryLogic::createNo();
78+
}
79+
80+
public function getThrowType(): ?Type
81+
{
82+
return null;
83+
}
84+
85+
public function hasSideEffects(): TrinaryLogic
86+
{
87+
return TrinaryLogic::createNo();
88+
}
89+
90+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
use PHPStan\Analyser\DeclarationDependencyTracker;
6+
use PHPStan\Reflection\ClassReflection;
7+
use PHPStan\Reflection\MethodReflection;
8+
use PHPStan\Reflection\MethodsClassReflectionExtension;
9+
use PHPStan\Type\Constant\ConstantIntegerType;
10+
use PHPStan\Type\Constant\ConstantStringType;
11+
12+
/**
13+
* Magic::greet() returns the "greeting" parameter - an int when it's numeric. A class reflection
14+
* extension gets no Scope, and what it declares is reused by every file using the class, so it
15+
* tracks the parameter on the class.
16+
*/
17+
final class MagicMethodsExtension implements MethodsClassReflectionExtension
18+
{
19+
20+
public function __construct(private DeclarationDependencyTracker $declarationDependencyTracker)
21+
{
22+
}
23+
24+
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
25+
{
26+
return $classReflection->getName() === Magic::class && $methodName === 'greet';
27+
}
28+
29+
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
30+
{
31+
$this->declarationDependencyTracker->trackValueDependency($classReflection, ParameterValueExtension::class, 'greeting');
32+
$greeting = Container::getParameter('greeting') ?? '';
33+
34+
return new MagicMethodReflection(
35+
$classReflection,
36+
$methodName,
37+
is_numeric($greeting) ? new ConstantIntegerType((int) $greeting) : new ConstantStringType($greeting),
38+
);
39+
}
40+
41+
}

‎e2e/result-cache-value-dependency/phpstan.neon‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,7 @@ services:
5454
class: ResultCacheE2EValueDependency\MakeReturnTypeExtension
5555
tags:
5656
- phpstan.broker.dynamicFunctionReturnTypeExtension
57+
-
58+
class: ResultCacheE2EValueDependency\MagicMethodsExtension
59+
tags:
60+
- phpstan.broker.methodsClassReflectionExtension
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
/**
6+
* greet() comes from MagicMethodsExtension.
7+
*/
8+
class Magic
9+
{
10+
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
function usesMagic(Magic $magic): string
6+
{
7+
return $magic->greet();
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
function usesMagicToo(Magic $magic): string
6+
{
7+
return $magic->greet();
8+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\Analyser\ResultCache\ResultCacheValueExtension;
6+
use PHPStan\Reflection\ClassReflection;
7+
8+
/**
9+
* DependencyTracker for extensions that get no Scope because they describe a class rather than
10+
* analyse code - class reflection extensions adding magic methods and properties, for example. Inject
11+
* it into the extension's constructor.
12+
*
13+
* What PHPStan learns about a class this way is remembered and reused by every file analysed after
14+
* it, so it's the class that depends on what the extension read, not the file being analysed: each
15+
* method declares the dependency of $classReflection. When it changes, every file depending on the
16+
* class - referencing it, calling its methods, reading its properties - is analysed again.
17+
*
18+
* @api
19+
*/
20+
interface DeclarationDependencyTracker
21+
{
22+
23+
/**
24+
* What $classReflection declares depends on the value $extensionClass gives for $key - see
25+
* DependencyTracker::trackValueDependency().
26+
*
27+
* @param class-string<ResultCacheValueExtension> $extensionClass
28+
*/
29+
public function trackValueDependency(ClassReflection $classReflection, string $extensionClass, string $key): void;
30+
31+
/**
32+
* What $classReflection declares depends on the contents of $file - see
33+
* DependencyTracker::trackFileDependency().
34+
*/
35+
public function trackFileDependency(ClassReflection $classReflection, string $file): void;
36+
37+
/**
38+
* What $classReflection declares depends on which files there are in $directory - see
39+
* DependencyTracker::trackDirectoryDependency().
40+
*/
41+
public function trackDirectoryDependency(ClassReflection $classReflection, string $directory, string $pattern = '*'): void;
42+
43+
/**
44+
* What $classReflection declares depends on what the class $className declares - see
45+
* DependencyTracker::trackClassDependency().
46+
*/
47+
public function trackClassDependency(ClassReflection $classReflection, string $className): void;
48+
49+
}

‎src/Analyser/DependencyTracker.php‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
* It tracks what the analysis of the current file depends on besides the analysed code, so that
2525
* the result cache re-analyses the file when that changes - see ResultCacheValueExtension.
2626
*
27+
* Extensions that get no Scope because they describe a class - class reflection extensions - use
28+
* DeclarationDependencyTracker instead.
29+
*
2730
* @api
2831
*/
2932
interface DependencyTracker

0 commit comments

Comments
 (0)