Skip to content

Commit c96f229

Browse files
SanderMullerclaude
andcommitted
Reuse the errors of rules on CollectedDataNode from the result cache
CollectedDataNode records which collectors a rule reads and whether it reads isOnlyFilesAnalysis(). The result cache stores the errors of each rule with a hash of that input. On the next run, a rule whose input is the same returns its stored errors and does not run again. A rule can declare what else it reads with the DependencyTracker methods, for example trackFileDependency() or trackDirectoryDependency(). These values are stored with its errors, and the rule runs again when one of them changes. The ignore extensions and the local ignores still run on every error, also on reused errors. This is behind the reuseCollectedDataRuleErrors feature toggle, which is on in bleeding edge only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent a778146 commit c96f229

46 files changed

Lines changed: 1352 additions & 36 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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,57 @@ jobs:
215215
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan -vvv")
216216
echo "$OUTPUT"
217217
../bashunit assert contains 'Result cache restored.' "$OUTPUT"
218+
- script: |
219+
cd e2e/result-cache-collected-data-rules
220+
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
221+
echo "$OUTPUT"
222+
../bashunit assert contains 'Caller.php:10:Call to function ResultCacheCollectedDataRules\pure() on a separate line has no effect.' "$OUTPUT"
223+
# Nothing changed, so every rule on CollectedDataNode reuses its errors from the result cache.
224+
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
225+
echo "$OUTPUT"
226+
../bashunit assert contains 'Caller.php:10:Call to function ResultCacheCollectedDataRules\pure() on a separate line has no effect.' "$OUTPUT"
227+
echo "$OUTPUT" | grep -E 'Result cache reused the errors of ([0-9]+) of \1 rules on collected data\.'
228+
../bashunit assert contains 'Result cache was not rewritten because it is unchanged.' "$OUTPUT"
229+
# src/Other.php is analysed again, but what it collects stays the same.
230+
echo '// changed' >> src/Other.php
231+
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
232+
echo "$OUTPUT"
233+
../bashunit assert contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT"
234+
../bashunit assert contains 'Caller.php:10:Call to function ResultCacheCollectedDataRules\pure() on a separate line has no effect.' "$OUTPUT"
235+
echo "$OUTPUT" | grep -E 'Result cache reused the errors of ([0-9]+) of \1 rules on collected data\.'
236+
# Removing the call changes what src/Caller.php collects, so the rule runs again and the error goes away.
237+
sed -i 's/^\t\tpure();$//' src/Caller.php
238+
OUTPUT=$(../bashunit assert exit_code "0" "../../bin/phpstan analyse -vv --error-format raw")
239+
echo "$OUTPUT"
240+
../bashunit assert not_contains 'on a separate line has no effect' "$OUTPUT"
241+
- script: |
242+
cd e2e/result-cache-collected-data-rules-tracking
243+
composer install
244+
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
245+
echo "$OUTPUT"
246+
../bashunit assert contains 'Controller.php:15:View about not found.' "$OUTPUT"
247+
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
248+
echo "$OUTPUT"
249+
echo "$OUTPUT" | grep -E 'Result cache reused the errors of ([0-9]+) of \1 rules on collected data\.'
250+
# The rule tracks the views directory, so a new view runs it again.
251+
printf 'about\n' > views/about.txt
252+
OUTPUT=$(../bashunit assert exit_code "0" "../../bin/phpstan analyse -vv --error-format raw")
253+
echo "$OUTPUT"
254+
../bashunit assert not_contains 'View about not found.' "$OUTPUT"
255+
rm views/about.txt
256+
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
257+
echo "$OUTPUT"
258+
../bashunit assert contains 'Controller.php:15:View about not found.' "$OUTPUT"
259+
# The rule tracks the file of ignored views too.
260+
printf 'about\n' > ignored-views.txt
261+
OUTPUT=$(../bashunit assert exit_code "0" "../../bin/phpstan analyse -vv --error-format raw")
262+
echo "$OUTPUT"
263+
../bashunit assert not_contains 'View about not found.' "$OUTPUT"
264+
# And the class of extra views, which is not analysed.
265+
sed -i "s/'contact'/'nothing'/" lib/ExtraViews.php
266+
OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
267+
echo "$OUTPUT"
268+
../bashunit assert contains 'Controller.php:20:View contact not found.' "$OUTPUT"
218269
- script: |
219270
cd e2e/bug-14514
220271
composer install

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ e2e/bashunit
2222
/e2e/result-cache-moved-tmpdir/tmp-a
2323
/e2e/result-cache-moved-tmpdir/tmp-b
2424
/e2e/result-cache-stale-objects/tmp
25+
/e2e/result-cache-collected-data-rules/tmp
26+
/e2e/result-cache-collected-data-rules-tracking/tmp
2527
/e2e/result-cache-truncated/tmp
2628
/e2e/result-cache-worker-count/tmp
2729
e2e/restart-manager/probe.log

‎conf/bleedingEdge.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ parameters:
3333
backedEnumFromParameterType: true
3434
unusedVariable: true
3535
unusedParameters: true
36+
reuseCollectedDataRuleErrors: true

‎conf/config.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ parameters:
6565
backedEnumFromParameterType: false
6666
unusedVariable: false
6767
unusedParameters: false
68+
reuseCollectedDataRuleErrors: false
6869
fileExtensions:
6970
- php
7071
checkAdvancedIsset: false

‎conf/parametersSchema.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ parametersSchema:
6262
backedEnumFromParameterType: bool()
6363
unusedVariable: bool()
6464
unusedParameters: bool()
65+
reuseCollectedDataRuleErrors: bool()
6566
])
6667
fileExtensions: listOf(string())
6768
checkAdvancedIsset: bool()
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-collected-data-rules-tracking/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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheCollectedDataRulesTracking;
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\Scope;
10+
use PHPStan\Collectors\Collector;
11+
12+
/**
13+
* @implements Collector<FuncCall, array{string, int}>
14+
*/
15+
final class ViewCollector implements Collector
16+
{
17+
18+
public function getNodeType(): string
19+
{
20+
return FuncCall::class;
21+
}
22+
23+
public function processNode(Node $node, Scope $scope)
24+
{
25+
if (!$node->name instanceof Name || $node->name->toLowerString() !== 'view') {
26+
return null;
27+
}
28+
29+
$args = $node->getArgs();
30+
if (!isset($args[0]) || !$args[0]->value instanceof String_) {
31+
return null;
32+
}
33+
34+
return [$args[0]->value->value, $node->getStartLine()];
35+
}
36+
37+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ResultCacheCollectedDataRulesTracking;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\DependencyTracker;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\CollectedDataNode;
9+
use PHPStan\Reflection\ReflectionProvider;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use function file;
13+
use function in_array;
14+
use function is_file;
15+
use function sprintf;
16+
17+
/**
18+
* Reports a view without a file in the views directory, unless the view is listed in a file of
19+
* ignored views or is a constant of ExtraViews. None of them is analysed, so the rule declares
20+
* all three.
21+
*
22+
* @implements Rule<CollectedDataNode>
23+
*/
24+
final class ViewRule implements Rule
25+
{
26+
27+
public function __construct(
28+
private ReflectionProvider $reflectionProvider,
29+
private string $viewsDirectory,
30+
private string $ignoredViewsFile,
31+
)
32+
{
33+
}
34+
35+
public function getNodeType(): string
36+
{
37+
return CollectedDataNode::class;
38+
}
39+
40+
/**
41+
* @param Scope&DependencyTracker $scope
42+
*/
43+
public function processNode(Node $node, Scope $scope): array
44+
{
45+
$scope->trackDirectoryDependency($this->viewsDirectory, '*.txt');
46+
$scope->trackFileDependency($this->ignoredViewsFile);
47+
$scope->trackClassDependency(ExtraViews::class);
48+
49+
$ignoredViews = file($this->ignoredViewsFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
50+
$extraViews = $this->reflectionProvider->getClass(ExtraViews::class)->getNativeReflection()->getConstants();
51+
$errors = [];
52+
foreach ($node->get(ViewCollector::class) as $file => $views) {
53+
foreach ($views as [$view, $line]) {
54+
if (is_file($this->viewsDirectory . '/' . $view . '.txt') || in_array($view, $ignoredViews, true) || in_array($view, $extraViews, true)) {
55+
continue;
56+
}
57+
58+
$errors[] = RuleErrorBuilder::message(sprintf('View %s not found.', $view))
59+
->file($file)
60+
->line($line)
61+
->identifier('view.notFound')
62+
->build();
63+
}
64+
}
65+
66+
return $errors;
67+
}
68+
69+
}

0 commit comments

Comments
 (0)