From b18f4314b13ba1a85c0ce52cfb3526c431ceb48e Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Fri, 9 Oct 2026 23:47:57 +0200 Subject: [PATCH 1/2] Print the rules on CollectedDataNode in --debug output Co-Authored-By: Claude Opus 5.5 --- src/Analyser/AnalyserResultFinalizer.php | 21 ++- src/Command/AnalyseApplication.php | 47 ++++++ .../Analyser/AnalyserResultFinalizerTest.php | 157 ++++++++++++++++++ 3 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/AnalyserResultFinalizerTest.php diff --git a/src/Analyser/AnalyserResultFinalizer.php b/src/Analyser/AnalyserResultFinalizer.php index c40bc1066bb..82c91667e7d 100644 --- a/src/Analyser/AnalyserResultFinalizer.php +++ b/src/Analyser/AnalyserResultFinalizer.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser; +use Closure; use PHPStan\AnalysedCodeException; use PHPStan\BetterReflection\NodeCompiler\Exception\UnableToCompileNode; use PHPStan\BetterReflection\Reflection\Exception\CircularReference; @@ -38,7 +39,17 @@ public function __construct( { } - public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $debug): FinalizerResult + /** + * @param Closure(string $ruleClass): void|null $preRuleCallback called before each rule on CollectedDataNode runs + * @param Closure(): void|null $postRuleCallback called after each of those rules, also when it throws + */ + public function finalize( + AnalyserResult $analyserResult, + bool $onlyFiles, + bool $debug, + ?Closure $preRuleCallback = null, + ?Closure $postRuleCallback = null, + ): FinalizerResult { if (count($analyserResult->getCollectedData()) === 0) { return $this->addUnmatchedIgnoredErrors($this->mergeFilteredPhpErrors($analyserResult), [], []); @@ -57,6 +68,10 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ $tempCollectorErrors = []; $internalErrors = $analyserResult->getInternalErrors(); foreach ($this->ruleRegistry->getRules($nodeType) as $rule) { + if ($preRuleCallback !== null) { + $preRuleCallback(get_class($rule)); + } + try { $ruleErrors = $rule->processNode($node, $scope); } catch (AnalysedCodeException $e) { @@ -96,6 +111,10 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ shouldReportBug: true, ); continue; + } finally { + if ($postRuleCallback !== null) { + $postRuleCallback(); + } } foreach ($ruleErrors as $ruleError) { diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index f5868057130..76e8633d003 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -2,6 +2,7 @@ namespace PHPStan\Command; +use Closure; use PHPStan\Analyser\AnalyserResult; use PHPStan\Analyser\AnalyserResultFinalizer; use PHPStan\Analyser\Error; @@ -135,10 +136,13 @@ public function analyse( $processedFiles = $intermediateAnalyserResult->getProcessedFiles(); $resultCacheResult = $resultCacheManager->process($intermediateAnalyserResult, $resultCache, $errorOutput, $onlyFiles, true); + [$preRuleCallback, $postRuleCallback] = $this->createRuleCallbacks($debug, $stdOutput); $analyserResult = $this->analyserResultFinalizer->finalize( $this->switchTmpFileInAnalyserResult($resultCacheResult->getAnalyserResult(), $insteadOfFile, $tmpFile), $onlyFiles, $debug, + $preRuleCallback, + $postRuleCallback, )->getAnalyserResult(); $internalErrors = $analyserResult->getInternalErrors(); $errors = array_merge( @@ -291,6 +295,49 @@ private function runAnalyser( return $analyserResult; } + /** + * Prints the rules on CollectedDataNode the way --debug prints the analysed files. They run after + * all files and can take longer than the analysis itself. + * + * @return array{(Closure(string $ruleClass): void)|null, (Closure(): void)|null} + */ + private function createRuleCallbacks(bool $debug, Output $stdOutput): array + { + if (!$debug) { + return [ + null, + null, + ]; + } + + $startTime = null; + $preRuleCallback = static function (string $ruleClass) use ($stdOutput, &$startTime): void { + $stdOutput->writeLineFormatted(sprintf('Running %s on the collected data', $ruleClass)); + $startTime = microtime(true); + }; + if (!$stdOutput->isDebug()) { + return [ + $preRuleCallback, + null, + ]; + } + + $previousMemory = memory_get_peak_usage(true); + $postRuleCallback = static function () use ($stdOutput, &$previousMemory, &$startTime): void { + if ($startTime === null) { + throw new ShouldNotHappenException(); + } + $currentTotalMemory = memory_get_peak_usage(true); + $stdOutput->writeLineFormatted(sprintf('--- consumed %s, total %s, took %.2f s', BytesHelper::bytes($currentTotalMemory - $previousMemory), BytesHelper::bytes($currentTotalMemory), microtime(true) - $startTime)); + $previousMemory = $currentTotalMemory; + }; + + return [ + $preRuleCallback, + $postRuleCallback, + ]; + } + private function switchTmpFileInAnalyserResult( AnalyserResult $analyserResult, ?string $insteadOfFile, diff --git a/tests/PHPStan/Analyser/AnalyserResultFinalizerTest.php b/tests/PHPStan/Analyser/AnalyserResultFinalizerTest.php new file mode 100644 index 00000000000..c34fca50257 --- /dev/null +++ b/tests/PHPStan/Analyser/AnalyserResultFinalizerTest.php @@ -0,0 +1,157 @@ +createReportingRule(); + $throwingRule = $this->createThrowingRule(); + + $events = []; + $result = $this->createFinalizer([$reportingRule, $throwingRule])->finalize( + $this->createAnalyserResult(), + false, + false, + static function (string $ruleClass) use (&$events): void { + $events[] = 'pre ' . $ruleClass; + }, + static function () use (&$events): void { + $events[] = 'post'; + }, + )->getAnalyserResult(); + + $this->assertSame([ + 'pre ' . get_class($reportingRule), + 'post', + 'pre ' . get_class($throwingRule), + 'post', + ], $events); + $this->assertCount(1, $result->getErrors()); + $this->assertSame('Reported.', $result->getErrors()[0]->getMessage()); + $this->assertCount(1, $result->getInternalErrors()); + $this->assertSame('Failed.', $result->getInternalErrors()[0]->getMessage()); + } + + public function testPostRuleCallbackRunsWhenDebugRethrows(): void + { + $throwingRule = $this->createThrowingRule(); + + $events = []; + try { + $this->createFinalizer([$throwingRule])->finalize( + $this->createAnalyserResult(), + false, + true, + static function (string $ruleClass) use (&$events): void { + $events[] = 'pre ' . $ruleClass; + }, + static function () use (&$events): void { + $events[] = 'post'; + }, + ); + $this->fail('The exception should have been rethrown in debug mode.'); + } catch (RuntimeException $e) { + $this->assertSame('Failed.', $e->getMessage()); + } + + $this->assertSame([ + 'pre ' . get_class($throwingRule), + 'post', + ], $events); + } + + /** + * @param list> $rules + */ + private function createFinalizer(array $rules): AnalyserResultFinalizer + { + return new AnalyserResultFinalizer( + new DirectRuleRegistry($rules), + new DirectExtensionsCollection([]), + self::getContainer()->getByType(RuleErrorTransformer::class), + self::createScopeFactory( + self::createReflectionProvider(), + self::getContainer()->getService('typeSpecifier'), + ), + new LocalIgnoresProcessor(), + false, + ); + } + + private function createAnalyserResult(): AnalyserResult + { + return new AnalyserResult( + unorderedErrors: [], + filteredPhpErrors: [], + allPhpErrors: [], + locallyIgnoredErrors: [], + linesToIgnore: [], + unmatchedLineIgnores: [], + internalErrors: [], + collectedData: [__FILE__ => [TraitDeclarationCollector::class => ['data']]], + dependencies: null, + usedTraitDependencies: null, + valueDependencies: null, + packageDependencies: null, + exportedNodes: [], + reachedInternalErrorsCountLimit: false, + peakMemoryUsageBytes: 0, + processedFiles: [], + ); + } + + /** + * @return Rule + */ + private function createReportingRule(): Rule + { + return new /** @implements Rule */class implements Rule { + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + return [RuleErrorBuilder::message('Reported.')->identifier('tests.reported')->file(__FILE__)->line(1)->build()]; + } + + }; + } + + /** + * @return Rule + */ + private function createThrowingRule(): Rule + { + return new /** @implements Rule */class implements Rule { + + public function getNodeType(): string + { + return CollectedDataNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + throw new RuntimeException('Failed.'); + } + + }; + } + +} From 7dadacff123112b29acb66fca6611c34c3e3cb08 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Sat, 10 Oct 2026 01:05:39 +0200 Subject: [PATCH 2/2] Say what the time of a rule on CollectedDataNode covers Co-Authored-By: Claude Opus 5.5 --- src/Analyser/AnalyserResultFinalizer.php | 4 ++-- src/Command/AnalyseApplication.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Analyser/AnalyserResultFinalizer.php b/src/Analyser/AnalyserResultFinalizer.php index 82c91667e7d..1f2be4721f1 100644 --- a/src/Analyser/AnalyserResultFinalizer.php +++ b/src/Analyser/AnalyserResultFinalizer.php @@ -40,8 +40,8 @@ public function __construct( } /** - * @param Closure(string $ruleClass): void|null $preRuleCallback called before each rule on CollectedDataNode runs - * @param Closure(): void|null $postRuleCallback called after each of those rules, also when it throws + * @param Closure(string $ruleClass): void|null $preRuleCallback + * @param Closure(): void|null $postRuleCallback called when the rule's processNode() has returned or thrown, before its errors are processed */ public function finalize( AnalyserResult $analyserResult, diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 76e8633d003..9057fba03b0 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -299,7 +299,7 @@ private function runAnalyser( * Prints the rules on CollectedDataNode the way --debug prints the analysed files. They run after * all files and can take longer than the analysis itself. * - * @return array{(Closure(string $ruleClass): void)|null, (Closure(): void)|null} + * @return array{Closure(string $ruleClass): void|null, Closure(): void|null} */ private function createRuleCallbacks(bool $debug, Output $stdOutput): array {