Skip to content

Commit ed978d4

Browse files
committed
Merge branch 'dependency-tracker-scenarios' into 2.3.x
2 parents 6984f7c + 2c7c9cf commit ed978d4

44 files changed

Lines changed: 1122 additions & 15 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: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,8 @@ jobs:
454454
../bashunit -a contains 'UsesHolder.php:9:Function ResultCacheE2EValueDependency\usesHolder() should return int but returns string.' "$OUTPUT"
455455
../bashunit -a contains 'UsesMayThrow.php:9:Dead catch - RuntimeException is never thrown in the try block.' "$OUTPUT"
456456
../bashunit -a contains 'UsesRegion.php:7:Region eu is used.' "$OUTPUT"
457+
../bashunit -a contains 'UsesView.php:7:View dashboard does not exist.' "$OUTPUT"
458+
../bashunit -a contains 'UsesMake.php:7:Call to an undefined method object::run().' "$OUTPUT"
457459
../bashunit -a not_contains 'UsesMailer.php' "$OUTPUT"
458460
# The extensions read container.json, which the result cache knows nothing about. A rule and a
459461
# dynamic return type extension both declare the service they ask about - it is recorded once.
@@ -526,6 +528,73 @@ jobs:
526528
echo "$OUTPUT"
527529
../bashunit -a contains 'UsesRegion.php:7:Region us is used.' "$OUTPUT"
528530
../bashunit -a not_contains 'Region eu is used.' "$OUTPUT"
531+
# So does a parameter closure this extension: $this in the closure follows the parameter.
532+
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["parameters"]["closureThis"] = "ResultCacheE2EValueDependency\\Logger"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
533+
INFO=$(../../bin/phpstan result-cache-info --json)
534+
echo "$INFO"
535+
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
536+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
537+
echo "$OUTPUT"
538+
../bashunit -a contains 'UsesWithThis.php:8:Call to an undefined method ResultCacheE2EValueDependency\Logger::send().' "$OUTPUT"
539+
# So does a type-specifying extension: the narrowed type follows the parameter.
540+
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["parameters"]["allowedClass"] = "ResultCacheE2EValueDependency\\Logger"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
541+
INFO=$(../../bin/phpstan result-cache-info --json)
542+
echo "$INFO"
543+
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
544+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
545+
echo "$OUTPUT"
546+
../bashunit -a contains 'UsesIsAllowed.php:8:Call to an undefined method ResultCacheE2EValueDependency\Logger::send().' "$OUTPUT"
547+
# A rule tracking a directory: a file the pattern doesn't match changes nothing...
548+
echo 'notes' > views/notes.txt
549+
INFO=$(../../bin/phpstan result-cache-info --json)
550+
echo "$INFO"
551+
../bashunit -a equals '0' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
552+
# ...a matching file appearing re-analyses the file that looked for it.
553+
echo '<p>dashboard</p>' > views/dashboard.html
554+
INFO=$(../../bin/phpstan result-cache-info --json)
555+
echo "$INFO"
556+
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
557+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
558+
echo "$OUTPUT"
559+
../bashunit -a not_contains 'View dashboard does not exist.' "$OUTPUT"
560+
# ...and so does a matching file whose contents change.
561+
echo '<p>dashboard, changed</p>' > views/dashboard.html
562+
INFO=$(../../bin/phpstan result-cache-info --json)
563+
echo "$INFO"
564+
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
565+
../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw"
566+
# A dynamic return type extension tracking a class named in a string: the class appearing - outside
567+
# the analysed paths, so no fallback for new files kicks in - re-analyses the file naming it...
568+
printf '<?php declare(strict_types = 1);\n\nnamespace ResultCacheE2EValueDependency\\Lib;\n\nclass Gadget extends Base\n{\n}\n' > lib/Gadget.php
569+
INFO=$(../../bin/phpstan result-cache-info --json)
570+
echo "$INFO"
571+
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
572+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
573+
echo "$OUTPUT"
574+
../bashunit -a not_contains 'UsesMake.php' "$OUTPUT"
575+
# ...a method body edited in its parent class does not...
576+
sed -i.bak 's/return 1;/return 2;/' lib/Base.php && rm lib/Base.php.bak
577+
INFO=$(../../bin/phpstan result-cache-info --json)
578+
echo "$INFO"
579+
../bashunit -a equals '0' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
580+
# ...and a signature changed there does.
581+
printf '<?php declare(strict_types = 1);\n\nnamespace ResultCacheE2EValueDependency\\Lib;\n\nclass Base\n{\n\n\tpublic function run(): string\n\t{\n\t\treturn \x27x\x27;\n\t}\n\n}\n' > lib/Base.php
582+
INFO=$(../../bin/phpstan result-cache-info --json)
583+
echo "$INFO"
584+
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
585+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
586+
echo "$OUTPUT"
587+
../bashunit -a contains 'UsesMake.php:7:Function ResultCacheE2EValueDependency\usesMake() should return int but returns string.' "$OUTPUT"
588+
# A class reflection extension tracking on the class: every file using the class is analysed again,
589+
# also one that got the magic method from the reflection cache without asking the extension.
590+
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));'
591+
INFO=$(../../bin/phpstan result-cache-info --json)
592+
echo "$INFO"
593+
../bashunit -a equals '3' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
594+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
595+
echo "$OUTPUT"
596+
../bashunit -a contains 'UsesMagic.php:7:Function ResultCacheE2EValueDependency\usesMagic() should return string but returns int.' "$OUTPUT"
597+
../bashunit -a contains 'UsesMagicToo.php:7:Function ResultCacheE2EValueDependency\usesMagicToo() should return string but returns int.' "$OUTPUT"
529598
- script: |
530599
cd e2e/result-cache-file-dependency
531600
composer install
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
{
22
"autoload-dev": {
3-
"classmap": ["extension/"]
3+
"classmap": [
4+
"extension/"
5+
]
6+
},
7+
"autoload": {
8+
"psr-4": {
9+
"ResultCacheE2EValueDependency\\Lib\\": "lib/"
10+
}
411
}
512
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"timezone": "Europe/Prague",
99
"debug": "yes",
1010
"throwing": "no",
11-
"region": "eu"
11+
"region": "eu",
12+
"closureThis": "ResultCacheE2EValueDependency\\Mailer",
13+
"allowedClass": "ResultCacheE2EValueDependency\\Mailer",
14+
"greeting": "hello"
1215
}
1316
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\DependencyTracker;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Analyser\SpecifiedTypes;
9+
use PHPStan\Analyser\TypeSpecifier;
10+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
11+
use PHPStan\Analyser\TypeSpecifierContext;
12+
use PHPStan\Reflection\FunctionReflection;
13+
use PHPStan\Type\FunctionTypeSpecifyingExtension;
14+
use PHPStan\Type\ObjectType;
15+
16+
/**
17+
* isAllowed($object) narrows $object to the class in the "allowedClass" parameter.
18+
*/
19+
final class IsAllowedTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
20+
{
21+
22+
private TypeSpecifier $typeSpecifier;
23+
24+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
25+
{
26+
$this->typeSpecifier = $typeSpecifier;
27+
}
28+
29+
public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
30+
{
31+
return $functionReflection->getName() === 'ResultCacheE2EValueDependency\isAllowed' && $context->true() && isset($node->getArgs()[0]);
32+
}
33+
34+
/**
35+
* @param Scope&DependencyTracker $scope
36+
*/
37+
public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
38+
{
39+
$scope->trackValueDependency(ParameterValueExtension::class, 'allowedClass');
40+
$class = Container::getParameter('allowedClass');
41+
if ($class === null) {
42+
return new SpecifiedTypes();
43+
}
44+
45+
return $this->typeSpecifier->create($node->getArgs()[0]->value, new ObjectType($class), $context, $scope);
46+
}
47+
48+
}
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+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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\DependencyTracker;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\FunctionReflection;
10+
use PHPStan\Reflection\ReflectionProvider;
11+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12+
use PHPStan\Type\ObjectType;
13+
use PHPStan\Type\Type;
14+
15+
/**
16+
* make('ClassName') returns an instance of the class named in the string.
17+
*/
18+
final class MakeReturnTypeExtension implements DynamicFunctionReturnTypeExtension
19+
{
20+
21+
public function __construct(private ReflectionProvider $reflectionProvider)
22+
{
23+
}
24+
25+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
26+
{
27+
return $functionReflection->getName() === 'ResultCacheE2EValueDependency\make';
28+
}
29+
30+
/**
31+
* @param Scope&DependencyTracker $scope
32+
*/
33+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
34+
{
35+
$arg = $functionCall->getArgs()[0]->value ?? null;
36+
if (!$arg instanceof String_) {
37+
return null;
38+
}
39+
40+
$scope->trackClassDependency($arg->value);
41+
if (!$this->reflectionProvider->hasClass($arg->value)) {
42+
return null;
43+
}
44+
45+
return new ObjectType($arg->value);
46+
}
47+
48+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PhpParser\Node\Name;
8+
use PhpParser\Node\Scalar\String_;
9+
use PHPStan\Analyser\DependencyTracker;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
14+
/**
15+
* Reports view('name') when there's no views/name.html - whether there is depends on the views/
16+
* directory, not on a single file.
17+
*
18+
* @implements Rule<FuncCall>
19+
*/
20+
final class ViewRule implements Rule
21+
{
22+
23+
public function getNodeType(): string
24+
{
25+
return FuncCall::class;
26+
}
27+
28+
/**
29+
* @param Scope&DependencyTracker $scope
30+
*/
31+
public function processNode(Node $node, Scope $scope): array
32+
{
33+
if (!$node->name instanceof Name || $node->name->getLast() !== 'view' || !isset($node->getArgs()[0]) || !$node->getArgs()[0]->value instanceof String_) {
34+
return [];
35+
}
36+
37+
$directory = dirname(__DIR__) . '/views';
38+
$scope->trackDirectoryDependency($directory, '*.html');
39+
40+
$name = $node->getArgs()[0]->value->value;
41+
if (is_file($directory . '/' . $name . '.html')) {
42+
return [];
43+
}
44+
45+
return [
46+
RuleErrorBuilder::message(sprintf('View %s does not exist.', $name))->identifier('resultCacheE2E.view')->build(),
47+
];
48+
}
49+
50+
}

0 commit comments

Comments
 (0)