From b8b3585e031a0669a98ac4086374d63be23230ae Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 30 Jul 2026 16:40:53 +0200 Subject: [PATCH] Reference the original chain in ExistingArrayDimFetch, delete DeepNodeCloner The unset() handling deep-cloned the already-processed offset chain and ran a NodeTraverser over the clone to wrap its levels in ExistingArrayDimFetch nodes. The wrappers now reference the original sub-expressions directly - the unset statement's own walk already processed them, and the assign target preparation prices the chain through the scope without walking it, so nothing re-processes the shared nodes. DeepNodeCloner had no other consumer. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7 --- src/Analyser/NodeScopeResolver.php | 34 +++++++------------ src/Node/DeepNodeCloner.php | 29 ---------------- src/Testing/RuleTestCase.php | 2 -- src/Testing/TypeInferenceTestCase.php | 2 -- tests/PHPStan/Analyser/AnalyserTest.php | 2 -- .../Fiber/FiberNodeScopeResolverRuleTest.php | 2 -- .../Fiber/FiberNodeScopeResolverTest.php | 2 -- 7 files changed, 13 insertions(+), 60 deletions(-) delete mode 100644 src/Node/DeepNodeCloner.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index ebb599d056a..275ebd2acdf 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5,7 +5,6 @@ use ArrayAccess; use Closure; use IteratorAggregate; -use Override; use PhpParser\Comment\Doc; use PhpParser\Modifiers; use PhpParser\Node; @@ -49,7 +48,6 @@ use PhpParser\Node\Stmt\While_; use PhpParser\NodeFinder; use PhpParser\NodeTraverser; -use PhpParser\NodeVisitorAbstract; use PHPStan\Analyser\ExprHandler\AssignHandler; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper; @@ -73,7 +71,6 @@ use PHPStan\Node\ClassPropertyNode; use PHPStan\Node\ClassStatementsGatherer; use PHPStan\Node\ClosureReturnStatementsNode; -use PHPStan\Node\DeepNodeCloner; use PHPStan\Node\DoWhileLoopConditionNode; use PHPStan\Node\ExecutionEndNode; use PHPStan\Node\Expr\ExistingArrayDimFetch; @@ -259,7 +256,6 @@ public function __construct( #[AutowiredExtensions(of: StaticMethodParameterClosureTypeExtension::class)] private readonly ExtensionsCollection $staticMethodParameterClosureTypeExtensions, private readonly ScopeFactory $scopeFactory, - private readonly DeepNodeCloner $deepNodeCloner, #[AutowiredParameter] private readonly bool $polluteScopeWithLoopInitialAssignments, #[AutowiredParameter] @@ -2406,25 +2402,21 @@ public function processStmtNode( )); } - $clonedVar = $this->deepNodeCloner->cloneNode($var->var); - $traverser = new NodeTraverser(); - $traverser->addVisitor(new class () extends NodeVisitorAbstract { - - #[Override] - public function leaveNode(Node $node): ?ExistingArrayDimFetch - { - if (!$node instanceof ArrayDimFetch || $node->dim === null) { - return null; - } - - return new ExistingArrayDimFetch($node->var, $node->dim); + // wrap the already-processed chain in ExistingArrayDimFetch nodes + // referencing the original sub-expressions - the unset statement's + // own walk already processed them, and the assign target + // preparation prices the chain without re-walking it + $buildExistingChain = static function (Expr $node) use (&$buildExistingChain): Expr { + if (!$node instanceof ArrayDimFetch || $node->dim === null) { + return $node; } - }); - - /** @var Expr $clonedVar */ - [$clonedVar] = $traverser->traverse([$clonedVar]); - $scope = $this->processVirtualAssign($scope, $storage, $stmt, $clonedVar, new UnsetOffsetExpr($var->var, $var->dim), $nodeCallback)->getScope(); + return new ExistingArrayDimFetch( + $buildExistingChain($node->var), + $node->dim, + ); + }; + $scope = $this->processVirtualAssign($scope, $storage, $stmt, $buildExistingChain($var->var), new UnsetOffsetExpr($var->var, $var->dim), $nodeCallback)->getScope(); } elseif ($var instanceof PropertyFetch) { $scope = $scope->invalidateExpression($var); $impurePoints[] = new ImpurePoint( diff --git a/src/Node/DeepNodeCloner.php b/src/Node/DeepNodeCloner.php deleted file mode 100644 index 8372bfd0183..00000000000 --- a/src/Node/DeepNodeCloner.php +++ /dev/null @@ -1,29 +0,0 @@ -traverse([$node]); - - /** @var T */ - return $clonedNode; - } - -} diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index 04f550f51ba..d070726072c 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -24,7 +24,6 @@ use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\Fixable\Patcher; -use PHPStan\Node\DeepNodeCloner; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\Reflection\ClassReflectionFactory; use PHPStan\Reflection\InitializerExprTypeResolver; @@ -122,7 +121,6 @@ protected function createNodeScopeResolver(): NodeScopeResolver self::getContainer()->getExtensionsCollection(MethodParameterClosureTypeExtension::class), self::getContainer()->getExtensionsCollection(StaticMethodParameterClosureTypeExtension::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), - self::getContainer()->getByType(DeepNodeCloner::class), $this->shouldPolluteScopeWithLoopInitialAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), self::getContainer()->getParameter('polluteScopeWithBlock'), diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 13e9a907dab..f19adc7dbe2 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -15,7 +15,6 @@ use PHPStan\Analyser\ScopeContext; use PHPStan\File\FileHelper; use PHPStan\File\SystemAgnosticSimpleRelativePathHelper; -use PHPStan\Node\DeepNodeCloner; use PHPStan\Node\InClassNode; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\TypeStringResolver; @@ -97,7 +96,6 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getExtensionsCollection(MethodParameterClosureTypeExtension::class), $container->getExtensionsCollection(StaticMethodParameterClosureTypeExtension::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), - self::getContainer()->getByType(DeepNodeCloner::class), $container->getParameter('polluteScopeWithLoopInitialAssignments'), $container->getParameter('polluteScopeWithAlwaysIterableForeach'), $container->getParameter('polluteScopeWithBlock'), diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 04417b7ab9f..586aa791332 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -14,7 +14,6 @@ use PHPStan\Dependency\ExportedNodeResolver; use PHPStan\Dependency\PackageDependencyResolver; use PHPStan\DependencyInjection\DirectExtensionsCollection; -use PHPStan\Node\DeepNodeCloner; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Node\Printer\Printer; use PHPStan\Parser\RichParser; @@ -838,7 +837,6 @@ private function createAnalyser(): Analyser $container->getExtensionsCollection(MethodParameterClosureTypeExtension::class), $container->getExtensionsCollection(StaticMethodParameterClosureTypeExtension::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), - $container->getByType(DeepNodeCloner::class), false, true, true, diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php index c9732b873d2..88d4aae93e2 100644 --- a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php +++ b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverRuleTest.php @@ -9,7 +9,6 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\DirectExtensionsCollection; use PHPStan\File\FileHelper; -use PHPStan\Node\DeepNodeCloner; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\Reflection\ClassReflectionFactory; use PHPStan\Reflection\InitializerExprTypeResolver; @@ -142,7 +141,6 @@ protected function createNodeScopeResolver(): NodeScopeResolver self::getContainer()->getExtensionsCollection(MethodParameterClosureTypeExtension::class), self::getContainer()->getExtensionsCollection(StaticMethodParameterClosureTypeExtension::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), - self::getContainer()->getByType(DeepNodeCloner::class), $this->shouldPolluteScopeWithLoopInitialAssignments(), $this->shouldPolluteScopeWithAlwaysIterableForeach(), self::getContainer()->getParameter('polluteScopeWithBlock'), diff --git a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php index 00038bbc18e..bfaac47349f 100644 --- a/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/Fiber/FiberNodeScopeResolverTest.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\File\FileHelper; -use PHPStan\Node\DeepNodeCloner; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\Reflection\ClassReflectionFactory; use PHPStan\Reflection\InitializerExprTypeResolver; @@ -75,7 +74,6 @@ protected static function createNodeScopeResolver(): NodeScopeResolver $container->getExtensionsCollection(MethodParameterClosureTypeExtension::class), $container->getExtensionsCollection(StaticMethodParameterClosureTypeExtension::class), self::createScopeFactory($reflectionProvider, $typeSpecifier), - $container->getByType(DeepNodeCloner::class), $container->getParameter('polluteScopeWithLoopInitialAssignments'), $container->getParameter('polluteScopeWithAlwaysIterableForeach'), $container->getParameter('polluteScopeWithBlock'),