From 0d6fcc976420c77680102337793a5625710b7f44 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 9 Jul 2026 23:12:54 +0200 Subject: [PATCH] Replace NodeFinder with direct recursion in shouldInvalidateExpression() The findFirst() call allocated a NodeTraverser + FirstFindingVisitor and paid the full visitor protocol (~4 calls per visited node) for every (stored expression, invalidated expression) pair that passes the substring pre-filter - about 2M visited nodes per analysed slice. A hand-rolled pre-order search with identical match semantics does one method call per node. --- src/Analyser/MutatingScope.php | 74 +++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 60c3326ae3..5831d4d0b7 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3186,30 +3186,7 @@ private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr return false; } - $nodeFinder = new NodeFinder(); - $expressionToInvalidateClass = get_class($exprToInvalidate); - $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($expressionToInvalidateClass, $exprStringToInvalidate): bool { - if ( - $exprStringToInvalidate === '$this' - && $node instanceof Name - && ( - in_array($node->toLowerString(), ['self', 'static', 'parent'], true) - || ($this->getClassReflection() !== null && $this->getClassReflection()->is($this->resolveName($node))) - ) - ) { - return true; - } - - if (!$node instanceof $expressionToInvalidateClass) { - return false; - } - - $nodeString = $this->getNodeKey($node); - - return $nodeString === $exprStringToInvalidate; - }); - - if ($found === null) { + if (!$this->containsExpressionToInvalidate($expr, get_class($exprToInvalidate), $exprStringToInvalidate)) { return false; } @@ -3232,6 +3209,55 @@ private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr return true; } + /** + * Depth-first pre-order search for the invalidated expression, replacing a + * NodeFinder::findFirst() call - this runs for every (stored expression, + * invalidated expression) pair whose keys pass the substring pre-filter, + * so the traverser/visitor machinery overhead was significant. + * + * @param class-string $expressionToInvalidateClass + */ + private function containsExpressionToInvalidate(Node $node, string $expressionToInvalidateClass, string $exprStringToInvalidate): bool + { + if ( + $exprStringToInvalidate === '$this' + && $node instanceof Name + && ( + in_array($node->toLowerString(), ['self', 'static', 'parent'], true) + || ($this->getClassReflection() !== null && $this->getClassReflection()->is($this->resolveName($node))) + ) + ) { + return true; + } + + if ( + $node instanceof $expressionToInvalidateClass + && $this->getNodeKey($node) === $exprStringToInvalidate + ) { + return true; + } + + foreach ($node->getSubNodeNames() as $subNodeName) { + $subNode = $node->$subNodeName; + if ($subNode instanceof Node) { + if ($this->containsExpressionToInvalidate($subNode, $expressionToInvalidateClass, $exprStringToInvalidate)) { + return true; + } + } elseif (is_array($subNode)) { + foreach ($subNode as $subNodeItem) { + if ( + $subNodeItem instanceof Node + && $this->containsExpressionToInvalidate($subNodeItem, $expressionToInvalidateClass, $exprStringToInvalidate) + ) { + return true; + } + } + } + } + + return false; + } + private function isPrivatePropertyOfDifferentClass(Expr $expr, ClassReflection $invalidatingClass): bool { if ($expr instanceof Expr\StaticPropertyFetch || $expr instanceof PropertyFetch) {