From 4afa331e96d94cedfa8792aa02afde6ecf4e634e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:25:33 +0000 Subject: [PATCH] Fix anonymous class constructor throw points leaking inner scope - Throw points from anonymous class constructor body carried inner method scopes instead of the outer scope, causing variables defined before try blocks to be reported as "might not be defined" in finally blocks - Added replaceScope() method to InternalThrowPoint to allow scope replacement - New regression test in tests/PHPStan/Rules/Variables/data/bug-13920.php Closes https://github.com/phpstan/phpstan/issues/13920 --- src/Analyser/ExprHandler/NewHandler.php | 2 +- src/Analyser/InternalThrowPoint.php | 5 ++++ .../Variables/DefinedVariableRuleTest.php | 10 +++++++ .../Rules/Variables/data/bug-13920.php | 26 +++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-13920.php diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 9e83f7c474..3190cf4832 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -190,7 +190,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $constructorResult = $node; }, StatementContext::createTopLevel()); if ($constructorResult !== null) { - $throwPoints = array_map(static fn (ThrowPoint $point) => InternalThrowPoint::createFromPublic($point), $constructorResult->getStatementResult()->getThrowPoints()); + $throwPoints = array_map(static fn (ThrowPoint $point): InternalThrowPoint => InternalThrowPoint::createFromPublic($point)->replaceScope($scope), $constructorResult->getStatementResult()->getThrowPoints()); $impurePoints = $constructorResult->getImpurePoints(); } } else { diff --git a/src/Analyser/InternalThrowPoint.php b/src/Analyser/InternalThrowPoint.php index 92291bd5ac..aaac06c8df 100644 --- a/src/Analyser/InternalThrowPoint.php +++ b/src/Analyser/InternalThrowPoint.php @@ -88,6 +88,11 @@ public function canContainAnyThrowable(): bool return $this->canContainAnyThrowable; } + public function replaceScope(MutatingScope $scope): self + { + return new self($scope, $this->type, $this->node, $this->explicit, $this->canContainAnyThrowable); + } + public function subtractCatchType(Type $catchType): self { return new self($this->scope, TypeCombinator::remove($this->type, $catchType), $this->node, $this->explicit, $this->canContainAnyThrowable); diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index add6c66beb..076dfc65f9 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1425,6 +1425,16 @@ public function testBug12373(): void $this->analyse([__DIR__ . '/data/bug-12373.php'], []); } + public function testBug13920(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = true; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-13920.php'], []); + } + public function testBug14117(): void { $this->cliArgumentsVariablesRegistered = true; diff --git a/tests/PHPStan/Rules/Variables/data/bug-13920.php b/tests/PHPStan/Rules/Variables/data/bug-13920.php new file mode 100644 index 0000000000..e21d129248 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-13920.php @@ -0,0 +1,26 @@ +