Skip to content

Fix #9349: Throw point are not properly recognized#5068

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-imch3v6
Open

Fix #9349: Throw point are not properly recognized#5068
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-imch3v6

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When a try block contains a method with @throws RuntimeException followed by variable assignments and another method call that can implicitly throw, the catch block for PDOException (which extends RuntimeException) incorrectly reported variables as "Undefined" instead of "might not be defined".

The root cause was that the try/catch throw point matching algorithm had a flag ($onlyExplicitIsThrow) that prevented implicit throw points from being matched to a catch clause whenever an explicit non-throw-statement match already existed. This meant that when @throws RuntimeException on a method call matched catch(PDOException) (because PDOException extends RuntimeException), implicit throw points from later code — where variables were already assigned — were excluded from the catch scope entirely.

Changes

  • Removed the $onlyExplicitIsThrow flag and made implicit throw point matching unconditional in src/Analyser/NodeScopeResolver.php (lines ~1917-1957)
  • Removed the now-unused InternalThrowPoint::getNode() method from src/Analyser/InternalThrowPoint.php
  • Updated test expectations in tests/PHPStan/Analyser/nsrt/bug-4821.php$method certainty changed from No to Maybe (correct: $method->invoke() can throw after $method is assigned)
  • Updated test expectations in tests/PHPStan/Analyser/nsrt/explicit-throws.php$a certainty changed from Yes to Maybe (correct: unknown function doFoo() can throw InvalidArgumentException before $a is assigned)
  • Added regression test tests/PHPStan/Analyser/nsrt/throw-points/bug-9349.php

Root cause

The try/catch matching algorithm in NodeScopeResolver processes throw points in three phases:

  1. Throwable matches all — if catch type is Throwable, match everything
  2. Explicit only — match explicit throw points (@throws annotations and throw statements)
  3. Implicit only — match implicit throw points (from method calls without @throws)

Phase 3 was guarded by a condition: only run if no explicit matches exist, OR all explicit matches are literal throw statements ($onlyExplicitIsThrow). When an explicit match came from a method call's @throws annotation (not a throw statement), the flag was set to false, causing phase 3 to be skipped entirely.

This was incorrect because implicit throw points from different method calls (later in the try block) provide scopes where additional variables are defined. Skipping them meant the catch scope only reflected the state at the first matching throw point, missing variables assigned afterwards.

The fix removes this guard, making implicit throw point matching unconditional. This is sound because implicit throw points represent real possibilities — any method call without @throws void can throw any Throwable.

Test

Added tests/PHPStan/Analyser/nsrt/throw-points/bug-9349.php with two test cases:

  1. @throws RuntimeException + implicit throw + catch(PDOException) — verifies $sql is Maybe (not No)
  2. @throws LogicException + implicit throw + catch(PDOException) — verifies $sql is Yes (LogicException is unrelated to PDOException, so only implicit throws match)

Fixes phpstan/phpstan#9349

- Always include implicit throw points when matching to catch clauses,
  regardless of whether explicit non-throw-statement matches exist
- Previously, when a method with @throws matched a catch clause, implicit
  throw points from other method calls were excluded from the catch scope
- This caused variables assigned between the two calls to be incorrectly
  reported as "Undefined variable" instead of "might not be defined"
- Removed unused $onlyExplicitIsThrow flag and InternalThrowPoint::getNode()
- Updated test expectations in bug-4821.php and explicit-throws.php to
  reflect the corrected (more sound) behavior

Closes phpstan/phpstan#9349
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant