Skip to content

Commit 86c58fc

Browse files
ondrejmirtesclaude
andcommitted
Dynamic throw type extensions and collectors can track dependencies
Their scope is Scope&DependencyTracker, the same as for dynamic return type extensions and rules. A collector reading something PHPStan does not know about can track it, and the file is analysed again and its data collected again when it changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdwtgiEHwkF5BTz73jgHQQ
1 parent 9ccc2e4 commit 86c58fc

14 files changed

Lines changed: 185 additions & 8 deletions

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,8 @@ jobs:
452452
../bashunit -a contains "UsesLocaleToo.php:10:Function ResultCacheE2EValueDependency\usesLocaleToo() should return 'none' but returns 'cs'." "$OUTPUT"
453453
../bashunit -a contains 'Holder.php:17:Method ResultCacheE2EValueDependency\Holder::isDebug() should return int but returns string.' "$OUTPUT"
454454
../bashunit -a contains 'UsesHolder.php:9:Function ResultCacheE2EValueDependency\usesHolder() should return int but returns string.' "$OUTPUT"
455+
../bashunit -a contains 'UsesMayThrow.php:9:Dead catch - RuntimeException is never thrown in the try block.' "$OUTPUT"
456+
../bashunit -a contains 'UsesRegion.php:7:Region eu is used.' "$OUTPUT"
455457
../bashunit -a not_contains 'UsesMailer.php' "$OUTPUT"
456458
# The extensions read container.json, which the result cache knows nothing about. A rule and a
457459
# dynamic return type extension both declare the service they ask about - it is recorded once.
@@ -507,6 +509,23 @@ jobs:
507509
echo "$OUTPUT"
508510
../bashunit -a contains 'UsesLogger.php:7:Call to an undefined method ResultCacheE2EValueDependency\Mailer::log().' "$OUTPUT"
509511
../bashunit -a contains 'LoggerTrait.php (in context of class ResultCacheE2EValueDependency\UsesLoggerTrait):10:Call to an undefined method ResultCacheE2EValueDependency\Mailer::log().' "$OUTPUT"
512+
# A dynamic throw type extension tracks the values it reads too.
513+
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["parameters"]["throwing"] = "yes"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
514+
INFO=$(../../bin/phpstan result-cache-info --json)
515+
echo "$INFO"
516+
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
517+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
518+
echo "$OUTPUT"
519+
../bashunit -a not_contains 'Dead catch' "$OUTPUT"
520+
# So does a collector: the data it collected is collected again in the analysis of the file.
521+
php -r '$c = json_decode(file_get_contents("container.json"), true); $c["parameters"]["region"] = "us"; file_put_contents("container.json", json_encode($c, JSON_PRETTY_PRINT));'
522+
INFO=$(../../bin/phpstan result-cache-info --json)
523+
echo "$INFO"
524+
../bashunit -a equals '1' "$(echo "$INFO" | jq --raw-output '.filesToAnalyseCount')"
525+
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
526+
echo "$OUTPUT"
527+
../bashunit -a contains 'UsesRegion.php:7:Region us is used.' "$OUTPUT"
528+
../bashunit -a not_contains 'Region eu is used.' "$OUTPUT"
510529
- script: |
511530
cd e2e/result-cache-file-dependency
512531
composer install

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
"parameters": {
77
"locale": "cs",
88
"timezone": "Europe/Prague",
9-
"debug": "yes"
9+
"debug": "yes",
10+
"throwing": "no",
11+
"region": "eu"
1012
}
1113
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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\Reflection\FunctionReflection;
9+
use PHPStan\Type\DynamicFunctionThrowTypeExtension;
10+
use PHPStan\Type\ObjectType;
11+
use PHPStan\Type\Type;
12+
13+
/**
14+
* mayThrow() throws when the "throwing" parameter says so.
15+
*/
16+
final class MayThrowThrowTypeExtension implements DynamicFunctionThrowTypeExtension
17+
{
18+
19+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
20+
{
21+
return $functionReflection->getName() === 'ResultCacheE2EValueDependency\mayThrow';
22+
}
23+
24+
/**
25+
* @param Scope&DependencyTracker $scope
26+
*/
27+
public function getThrowTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, Scope $scope): ?Type
28+
{
29+
$scope->trackValueDependency(ParameterValueExtension::class, 'throwing');
30+
31+
return Container::getParameter('throwing') === 'yes' ? new ObjectType(\RuntimeException::class) : null;
32+
}
33+
34+
}
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;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PhpParser\Node\Name;
8+
use PHPStan\Analyser\DependencyTracker;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Collectors\Collector;
11+
12+
/**
13+
* Collects the "region" parameter at the calls to region().
14+
*
15+
* @implements Collector<FuncCall, string>
16+
*/
17+
final class RegionCollector implements Collector
18+
{
19+
20+
public function getNodeType(): string
21+
{
22+
return FuncCall::class;
23+
}
24+
25+
/**
26+
* @param Scope&DependencyTracker $scope
27+
*/
28+
public function processNode(Node $node, Scope $scope)
29+
{
30+
if (!$node->name instanceof Name || $node->name->getLast() !== 'region') {
31+
return null;
32+
}
33+
34+
$scope->trackValueDependency(ParameterValueExtension::class, 'region');
35+
36+
return Container::getParameter('region') ?? 'missing';
37+
}
38+
39+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheE2EValueDependency;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Node\CollectedDataNode;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
11+
/**
12+
* Reports what RegionCollector collected.
13+
*
14+
* @implements Rule<CollectedDataNode>
15+
*/
16+
final class RegionRule implements Rule
17+
{
18+
19+
public function getNodeType(): string
20+
{
21+
return CollectedDataNode::class;
22+
}
23+
24+
public function processNode(Node $node, Scope $scope): array
25+
{
26+
$errors = [];
27+
foreach ($node->get(RegionCollector::class) as $file => $regions) {
28+
foreach ($regions as $region) {
29+
$errors[] = RuleErrorBuilder::message(sprintf('Region %s is used.', $region))->identifier('resultCacheE2E.region')->file($file)->line(7)->build();
30+
}
31+
}
32+
33+
return $errors;
34+
}
35+
36+
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,15 @@ services:
2626
class: ResultCacheE2EValueDependency\ParameterReturnTypeExtension
2727
tags:
2828
- phpstan.broker.dynamicFunctionReturnTypeExtension
29+
-
30+
class: ResultCacheE2EValueDependency\MayThrowThrowTypeExtension
31+
tags:
32+
- phpstan.dynamicFunctionThrowTypeExtension
33+
-
34+
class: ResultCacheE2EValueDependency\RegionCollector
35+
tags:
36+
- phpstan.collector
37+
-
38+
class: ResultCacheE2EValueDependency\RegionRule
39+
tags:
40+
- phpstan.rules.rule
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+
function usesMayThrow(): void
6+
{
7+
try {
8+
mayThrow();
9+
} catch (\RuntimeException $e) {
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 usesRegion(): string
6+
{
7+
return region();
8+
}

‎e2e/result-cache-value-dependency/src/functions.php‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,15 @@ function parameter(string $name)
1414
{
1515
return $name === '' ? 0 : '';
1616
}
17+
18+
/**
19+
* @throws void
20+
*/
21+
function mayThrow(): void
22+
{
23+
}
24+
25+
function region(): string
26+
{
27+
return '';
28+
}

‎src/Analyser/DependencyTracker.php‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
use PHPStan\Analyser\ResultCache\ResultCacheValueExtension;
66

77
/**
8-
* The interface DependencyTracker can be typehinted in 2nd parameter of Rule::processNode(),
9-
* and in the Scope parameter of dynamic return type extensions, expression type resolver
10-
* extensions and parameter out type extensions:
8+
* The interface DependencyTracker can be typehinted in 2nd parameter of Rule::processNode() and
9+
* Collector::processNode(), and in the Scope parameter of dynamic return type extensions, dynamic
10+
* throw type extensions, expression type resolver extensions and parameter out type extensions:
1111
*
1212
* ```php
1313
* /**

0 commit comments

Comments
 (0)