Skip to content

Commit 6fdd5b3

Browse files
SanderMullerclaude
authored andcommitted
Print the rules on CollectedDataNode in --debug output
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c539adc commit 6fdd5b3

3 files changed

Lines changed: 224 additions & 1 deletion

File tree

‎src/Analyser/AnalyserResultFinalizer.php‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Analyser;
44

5+
use Closure;
56
use PHPStan\AnalysedCodeException;
67
use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode;
78
use PHPStan\BetterReflection\Reflection\Exception\CircularReference;
@@ -38,7 +39,17 @@ public function __construct(
3839
{
3940
}
4041

41-
public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $debug): FinalizerResult
42+
/**
43+
* @param Closure(string $ruleClass): void|null $preRuleCallback called before each rule on CollectedDataNode runs
44+
* @param Closure(): void|null $postRuleCallback called after each of those rules, also when it throws
45+
*/
46+
public function finalize(
47+
AnalyserResult $analyserResult,
48+
bool $onlyFiles,
49+
bool $debug,
50+
?Closure $preRuleCallback = null,
51+
?Closure $postRuleCallback = null,
52+
): FinalizerResult
4253
{
4354
if (count($analyserResult->getCollectedData()) === 0) {
4455
return $this->addUnmatchedIgnoredErrors($this->mergeFilteredPhpErrors($analyserResult), [], []);
@@ -57,6 +68,10 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
5768
$tempCollectorErrors = [];
5869
$internalErrors = $analyserResult->getInternalErrors();
5970
foreach ($this->ruleRegistry->getRules($nodeType) as $rule) {
71+
if ($preRuleCallback !== null) {
72+
$preRuleCallback(get_class($rule));
73+
}
74+
6075
try {
6176
$ruleErrors = $rule->processNode($node, $scope);
6277
} catch (AnalysedCodeException $e) {
@@ -96,6 +111,10 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
96111
shouldReportBug: true,
97112
);
98113
continue;
114+
} finally {
115+
if ($postRuleCallback !== null) {
116+
$postRuleCallback();
117+
}
99118
}
100119

101120
foreach ($ruleErrors as $ruleError) {

‎src/Command/AnalyseApplication.php‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Command;
44

5+
use Closure;
56
use PHPStan\Analyser\AnalyserResult;
67
use PHPStan\Analyser\AnalyserResultFinalizer;
78
use PHPStan\Analyser\Error;
@@ -135,10 +136,13 @@ public function analyse(
135136
$processedFiles = $intermediateAnalyserResult->getProcessedFiles();
136137

137138
$resultCacheResult = $resultCacheManager->process($intermediateAnalyserResult, $resultCache, $errorOutput, $onlyFiles, true);
139+
[$preRuleCallback, $postRuleCallback] = $this->createRuleCallbacks($debug, $stdOutput);
138140
$analyserResult = $this->analyserResultFinalizer->finalize(
139141
$this->switchTmpFileInAnalyserResult($resultCacheResult->getAnalyserResult(), $insteadOfFile, $tmpFile),
140142
$onlyFiles,
141143
$debug,
144+
$preRuleCallback,
145+
$postRuleCallback,
142146
)->getAnalyserResult();
143147
$internalErrors = $analyserResult->getInternalErrors();
144148
$errors = array_merge(
@@ -291,6 +295,49 @@ private function runAnalyser(
291295
return $analyserResult;
292296
}
293297

298+
/**
299+
* Prints the rules on CollectedDataNode the way --debug prints the analysed files. They run after
300+
* all files and can take longer than the analysis itself.
301+
*
302+
* @return array{(Closure(string $ruleClass): void)|null, (Closure(): void)|null}
303+
*/
304+
private function createRuleCallbacks(bool $debug, Output $stdOutput): array
305+
{
306+
if (!$debug) {
307+
return [
308+
null,
309+
null,
310+
];
311+
}
312+
313+
$startTime = null;
314+
$preRuleCallback = static function (string $ruleClass) use ($stdOutput, &$startTime): void {
315+
$stdOutput->writeLineFormatted(sprintf('Running %s on the collected data', $ruleClass));
316+
$startTime = microtime(true);
317+
};
318+
if (!$stdOutput->isDebug()) {
319+
return [
320+
$preRuleCallback,
321+
null,
322+
];
323+
}
324+
325+
$previousMemory = memory_get_peak_usage(true);
326+
$postRuleCallback = static function () use ($stdOutput, &$previousMemory, &$startTime): void {
327+
if ($startTime === null) {
328+
throw new ShouldNotHappenException();
329+
}
330+
$currentTotalMemory = memory_get_peak_usage(true);
331+
$stdOutput->writeLineFormatted(sprintf('--- consumed %s, total %s, took %.2f s', BytesHelper::bytes($currentTotalMemory - $previousMemory), BytesHelper::bytes($currentTotalMemory), microtime(true) - $startTime));
332+
$previousMemory = $currentTotalMemory;
333+
};
334+
335+
return [
336+
$preRuleCallback,
337+
$postRuleCallback,
338+
];
339+
}
340+
294341
private function switchTmpFileInAnalyserResult(
295342
AnalyserResult $analyserResult,
296343
?string $insteadOfFile,
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PhpParser\Node;
6+
use PHPStan\DependencyInjection\DirectExtensionsCollection;
7+
use PHPStan\Node\CollectedDataNode;
8+
use PHPStan\Rules\DirectRegistry as DirectRuleRegistry;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use PHPStan\Rules\Traits\TraitDeclarationCollector;
12+
use PHPStan\Testing\PHPStanTestCase;
13+
use RuntimeException;
14+
use function get_class;
15+
16+
class AnalyserResultFinalizerTest extends PHPStanTestCase
17+
{
18+
19+
public function testRuleCallbacksRunAroundEveryCollectedDataRule(): void
20+
{
21+
$reportingRule = $this->createReportingRule();
22+
$throwingRule = $this->createThrowingRule();
23+
24+
$events = [];
25+
$result = $this->createFinalizer([$reportingRule, $throwingRule])->finalize(
26+
$this->createAnalyserResult(),
27+
false,
28+
false,
29+
static function (string $ruleClass) use (&$events): void {
30+
$events[] = 'pre ' . $ruleClass;
31+
},
32+
static function () use (&$events): void {
33+
$events[] = 'post';
34+
},
35+
)->getAnalyserResult();
36+
37+
$this->assertSame([
38+
'pre ' . get_class($reportingRule),
39+
'post',
40+
'pre ' . get_class($throwingRule),
41+
'post',
42+
], $events);
43+
$this->assertCount(1, $result->getErrors());
44+
$this->assertSame('Reported.', $result->getErrors()[0]->getMessage());
45+
$this->assertCount(1, $result->getInternalErrors());
46+
$this->assertSame('Failed.', $result->getInternalErrors()[0]->getMessage());
47+
}
48+
49+
public function testPostRuleCallbackRunsWhenDebugRethrows(): void
50+
{
51+
$throwingRule = $this->createThrowingRule();
52+
53+
$events = [];
54+
try {
55+
$this->createFinalizer([$throwingRule])->finalize(
56+
$this->createAnalyserResult(),
57+
false,
58+
true,
59+
static function (string $ruleClass) use (&$events): void {
60+
$events[] = 'pre ' . $ruleClass;
61+
},
62+
static function () use (&$events): void {
63+
$events[] = 'post';
64+
},
65+
);
66+
$this->fail('The exception should have been rethrown in debug mode.');
67+
} catch (RuntimeException $e) {
68+
$this->assertSame('Failed.', $e->getMessage());
69+
}
70+
71+
$this->assertSame([
72+
'pre ' . get_class($throwingRule),
73+
'post',
74+
], $events);
75+
}
76+
77+
/**
78+
* @param list<Rule<CollectedDataNode>> $rules
79+
*/
80+
private function createFinalizer(array $rules): AnalyserResultFinalizer
81+
{
82+
return new AnalyserResultFinalizer(
83+
new DirectRuleRegistry($rules),
84+
new DirectExtensionsCollection([]),
85+
self::getContainer()->getByType(RuleErrorTransformer::class),
86+
self::createScopeFactory(
87+
self::createReflectionProvider(),
88+
self::getContainer()->getService('typeSpecifier'),
89+
),
90+
new LocalIgnoresProcessor(),
91+
false,
92+
);
93+
}
94+
95+
private function createAnalyserResult(): AnalyserResult
96+
{
97+
return new AnalyserResult(
98+
unorderedErrors: [],
99+
filteredPhpErrors: [],
100+
allPhpErrors: [],
101+
locallyIgnoredErrors: [],
102+
linesToIgnore: [],
103+
unmatchedLineIgnores: [],
104+
internalErrors: [],
105+
collectedData: [__FILE__ => [TraitDeclarationCollector::class => ['data']]],
106+
dependencies: null,
107+
usedTraitDependencies: null,
108+
valueDependencies: null,
109+
packageDependencies: null,
110+
exportedNodes: [],
111+
reachedInternalErrorsCountLimit: false,
112+
peakMemoryUsageBytes: 0,
113+
processedFiles: [],
114+
);
115+
}
116+
117+
/**
118+
* @return Rule<CollectedDataNode>
119+
*/
120+
private function createReportingRule(): Rule
121+
{
122+
return new /** @implements Rule<CollectedDataNode> */class implements Rule {
123+
124+
public function getNodeType(): string
125+
{
126+
return CollectedDataNode::class;
127+
}
128+
129+
public function processNode(Node $node, Scope $scope): array
130+
{
131+
return [RuleErrorBuilder::message('Reported.')->identifier('tests.reported')->file(__FILE__)->line(1)->build()];
132+
}
133+
134+
};
135+
}
136+
137+
/**
138+
* @return Rule<CollectedDataNode>
139+
*/
140+
private function createThrowingRule(): Rule
141+
{
142+
return new /** @implements Rule<CollectedDataNode> */class implements Rule {
143+
144+
public function getNodeType(): string
145+
{
146+
return CollectedDataNode::class;
147+
}
148+
149+
public function processNode(Node $node, Scope $scope): array
150+
{
151+
throw new RuntimeException('Failed.');
152+
}
153+
154+
};
155+
}
156+
157+
}

0 commit comments

Comments
 (0)