From b7a0e093d8996408bb0f31ee8422dc7273b570c7 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 9 Oct 2026 00:10:37 +0700 Subject: [PATCH] perf: Skip numeric literal and keyword constant collection when no file-analysis rule is active Signed-off-by: Abdul Malik Ikhsan --- src/Analyser/AnalysisNodeCollector.php | 18 ++++++++++-- src/Analyser/AnalysisNodeExtractor.php | 10 +++---- tests/Analyser/AnalysisNodeCollectorTest.php | 29 ++++++++++++++++++-- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/Analyser/AnalysisNodeCollector.php b/src/Analyser/AnalysisNodeCollector.php index 3bd78816..1c7179dd 100644 --- a/src/Analyser/AnalysisNodeCollector.php +++ b/src/Analyser/AnalysisNodeCollector.php @@ -437,8 +437,15 @@ final class AnalysisNodeCollector extends NodeVisitorAbstract */ private array $fileFunctionLikeAnalyses = []; + /** + * @param bool $withFileAnalysis Whether to record the facts only file-analysis + * rules read ({@see getNonCanonicalKeywordConstants()}, + * {@see getNumericLiterals()}); an architecture-only + * run never reads them + */ public function __construct( - private readonly LayerResolverInterface $layerResolver + private readonly LayerResolverInterface $layerResolver, + private readonly bool $withFileAnalysis = true, ) { $this->constExprEvaluator = new ConstExprEvaluator(function (Expr $expr): string { if ( @@ -1116,6 +1123,10 @@ private function finishFunctionLikeAnalysis(): void private function collectNodeAnalysis(Node $node): void { if ($node instanceof Int_ || $node instanceof Float_) { + if (! $this->withFileAnalysis) { + return; + } + $rawValue = $node->getAttribute('rawValue'); // Parser-created scalar nodes always carry rawValue. Programmatic @@ -1178,7 +1189,10 @@ private function collectNodeAnalysis(Node $node): void if ($node instanceof ConstFetch) { $name = $node->name; $name->setAttribute(self::NON_CLASS_NAME_ATTRIBUTE, true); - $this->collectKeywordConstant($name); + + if ($this->withFileAnalysis) { + $this->collectKeywordConstant($name); + } if ($this->activeClassLikeAnalyses !== [] && ! isset(self::KEYWORD_CONSTANTS[$name->toLowerString()])) { // An unqualified fetch in a namespace is not a FullyQualified diff --git a/src/Analyser/AnalysisNodeExtractor.php b/src/Analyser/AnalysisNodeExtractor.php index 36012ade..9a53929e 100644 --- a/src/Analyser/AnalysisNodeExtractor.php +++ b/src/Analyser/AnalysisNodeExtractor.php @@ -52,7 +52,7 @@ public function extract( $progressHandler?->start(count($filesToParse)); - $analysisNodeCollector = new AnalysisNodeCollector($this->layerResolver); + $analysisNodeCollector = new AnalysisNodeCollector($this->layerResolver, $withFileAnalysis); $nodeTraverser = new NodeTraverser(new NameResolver(), $analysisNodeCollector); $fileAnalyses = []; @@ -67,13 +67,13 @@ public function extract( $analysisNodeCollector->setCurrentFile($fileToParse, $this->fileAnalysisProvider->tokens()); $resolvedAst = $nodeTraverser->traverse($ast); - $nonCanonicalKeywordConstants = $analysisNodeCollector->getNonCanonicalKeywordConstants(); - $numericLiterals = $analysisNodeCollector->getNumericLiterals(); - $fileFunctions = $analysisNodeCollector->getFileFunctions(); - // The same traversal ran NameResolver, so the file analysis // reuses its AST instead of resolving names in a second walk. if ($withFileAnalysis) { + $nonCanonicalKeywordConstants = $analysisNodeCollector->getNonCanonicalKeywordConstants(); + $numericLiterals = $analysisNodeCollector->getNumericLiterals(); + $fileFunctions = $analysisNodeCollector->getFileFunctions(); + $this->fileAnalysisProvider->replaceResolvedAst($fileToParse, $resolvedAst); } } diff --git a/tests/Analyser/AnalysisNodeCollectorTest.php b/tests/Analyser/AnalysisNodeCollectorTest.php index 027d6da2..1abaacf0 100644 --- a/tests/Analyser/AnalysisNodeCollectorTest.php +++ b/tests/Analyser/AnalysisNodeCollectorTest.php @@ -123,6 +123,31 @@ public function testCollectsNumericLiteralSpellingsAndValues(): void $this->assertSame([], $analysisNodeCollector->getNumericLiterals()); } + public function testSkipsFileAnalysisFactsWhenDisabledButStillCollectsClassDependencies(): void + { + $analysisNodeCollector = $this->makeCollector(<<<'PHP' +assertSame([], $analysisNodeCollector->getNonCanonicalKeywordConstants()); + $this->assertSame([], $analysisNodeCollector->getNumericLiterals()); + + $classNodes = $analysisNodeCollector->getClassNodes(); + + $this->assertCount(1, $classNodes); + $this->assertTrue($classNodes[0]->usesConstant('M_PI')); + } + public function testExposesUnconditionallyDeclaredFunctionsOfTheFileTraversedLast(): void { $analysisNodeCollector = $this->makeCollector(<<<'PHP' @@ -175,10 +200,10 @@ private function collectAnonymousClassNodes(string $code): array return $this->makeCollector($code)->getAnonymousClassNodes(); } - private function makeCollector(string $code): AnalysisNodeCollector + private function makeCollector(string $code, bool $withFileAnalysis = true): AnalysisNodeCollector { $namespaceLayerResolver = new NamespaceLayerResolver(['Domain' => 'src/Domain/'], self::BASE_PATH); - $analysisNodeCollector = new AnalysisNodeCollector($namespaceLayerResolver); + $analysisNodeCollector = new AnalysisNodeCollector($namespaceLayerResolver, $withFileAnalysis); $parser = (new ParserFactory())->createForNewestSupportedVersion(); $ast = $parser->parse($code);