From b6073f07bf43ede9999e2063d5b09496dfb76468 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:11:46 +0000 Subject: [PATCH] Fix throw points not properly matched to catch clauses - When a method has @throws with a supertype of the caught exception (e.g. @throws RuntimeException with catch PDOException), implicit throw points from other method calls were incorrectly excluded from the catch scope - Phase 3 (implicit throw point matching) was skipped when explicit @throws matched even as "maybe", now it only skips when there's a definitive "yes" match - Added regression test in tests/PHPStan/Rules/Variables/data/bug-9349.php Closes https://github.com/phpstan/phpstan/issues/9349 --- src/Analyser/NodeScopeResolver.php | 6 +- .../Variables/DefinedVariableRuleTest.php | 15 +++++ .../PHPStan/Rules/Variables/data/bug-9349.php | 58 +++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Variables/data/bug-9349.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 79b05cd0f8..ce853bd980 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1878,6 +1878,7 @@ public function processStmtNode( // explicit only $onlyExplicitIsThrow = true; + $hasDirectExplicitNonThrowMatch = false; if (count($matchingThrowPoints) === 0) { foreach ($throwPoints as $throwPointIndex => $throwPoint) { foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) { @@ -1895,6 +1896,9 @@ public function processStmtNode( && !($throwNode instanceof Node\Stmt\Expression && $throwNode->expr instanceof Expr\Throw_) ) { $onlyExplicitIsThrow = false; + if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->yes()) { + $hasDirectExplicitNonThrowMatch = true; + } } $matchingThrowPoints[$throwPointIndex] = $throwPoint; } @@ -1902,7 +1906,7 @@ public function processStmtNode( } // implicit only - if (count($matchingThrowPoints) === 0 || $onlyExplicitIsThrow) { + if (count($matchingThrowPoints) === 0 || $onlyExplicitIsThrow || !$hasDirectExplicitNonThrowMatch) { foreach ($throwPoints as $throwPointIndex => $throwPoint) { if ($throwPoint->isExplicit()) { continue; diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index add6c66beb..066817e7c5 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -1403,6 +1403,21 @@ public function testBug14019(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14019.php'], []); } + public function testBug9349(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->polluteScopeWithLoopInitialAssignments = false; + $this->checkMaybeUndefinedVariables = true; + $this->polluteScopeWithAlwaysIterableForeach = true; + + $this->analyse([__DIR__ . '/data/bug-9349.php'], [ + [ + 'Variable $sql might not be defined.', + 19, + ], + ]); + } + #[RequiresPhp('>= 8.0')] public function testBug14274(): void { diff --git a/tests/PHPStan/Rules/Variables/data/bug-9349.php b/tests/PHPStan/Rules/Variables/data/bug-9349.php new file mode 100644 index 0000000000..42c2fae5fa --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-9349.php @@ -0,0 +1,58 @@ +maybeThrows(); + $sql = "SELECT * FROM foo"; + $rs = $pdo->query($sql); + if ($result = $rs->fetch(\PDO::FETCH_ASSOC)) { + // do something + } + } catch (\PDOException $e) { + var_dump($sql); + } + } + + /** + * @throws \RuntimeException + */ + public function maybeThrows(): void + { + if (random_int(0, 1) === 1) { + throw new \RuntimeException(); + } + } + + public function test2(): void + { + global $pdo; + + try { + $this->maybeThrows2(); + $sql = "SELECT * FROM foo"; + $rs = $pdo->query($sql); + if ($result = $rs->fetch(\PDO::FETCH_ASSOC)) { + // do something + } + } catch (\PDOException $e) { + var_dump($sql); + } + } + + /** + * @throws \LogicException + */ + public function maybeThrows2(): void + { + if (random_int(0, 1) === 1) { + throw new \LogicException(); + } + } +}