Skip to content

Fix phpstan/phpstan#9444: return statement is missing#5172

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

Fix phpstan/phpstan#9444: return statement is missing#5172
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-dnpm1zh

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

PHPStan falsely reported "return statement is missing" for a method containing a for loop with try { return ... } catch { match(...) { ..., default => throw $e } }. The method always either returns or throws, but PHPStan couldn't prove it.

Changes

  • src/Analyser/ExprHandler/MatchHandler.php: When merging arm body scopes after an exhaustive match expression, exclude arms whose body type is NeverType (e.g., default => throw $e). This correctly narrows the match subject variable to only the values from non-throwing arms in the post-match scope.
  • src/Analyser/NodeScopeResolver.php: After the real processing of a for loop body, re-evaluate the loop condition against the loop scope (body result + loop expressions). If the condition is always true, upgrade $alwaysIterates to YES. This allows the narrowing from within the loop body to influence whether the loop is recognized as always iterating.
  • tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php: Added testBug9444 test method.
  • tests/PHPStan/Rules/Missing/data/bug-9444.php: Regression test reproducing the issue.

Root cause

Two issues combined to cause the false positive:

  1. Match scope pollution: The match handler merged ALL arm body scopes (including always-terminating arms like default => throw $e) into the post-match scope. This prevented the match subject variable ($i) from being properly narrowed. After the fix, only non-throwing arms contribute to the post-match scope, so $i is correctly narrowed to 0|1|2|3|4.

  2. For loop condition evaluation timing: The for loop's $alwaysIterates flag was computed using the warmup scope (which generalizes types across iterations), making it maybe instead of yes. The fix adds a secondary check after the real body processing: if the loop scope (after body + increment) shows the condition is always true, $alwaysIterates is upgraded to yes. With the match narrowing fix, $i after the body is {0,1,2,3,4}, and after $i++ is {1,2,3,4,5}, making $i <= 5 always true — so the loop is recognized as always iterating (and thus always terminating since there's no break).

Test

Added a regression test in tests/PHPStan/Rules/Missing/data/bug-9444.php that reproduces the exact code from the issue: a for loop with try { return ... } catch { match with default => throw }. The test expects no errors.

Fixes phpstan/phpstan#9444

- Exclude always-terminating (NeverType) match arm scopes from post-match
  scope merge in MatchHandler, so variables are correctly narrowed after
  a match with `default => throw`
- Re-evaluate for loop condition against the real processing's loop scope
  to detect when body narrowing makes the condition always true
- New regression test in tests/PHPStan/Rules/Missing/data/bug-9444.php

Closes phpstan/phpstan#9444
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.

2 participants