Skip to content

Fix #9601: Failed Determine type in match operator#5066

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

Fix #9601: Failed Determine type in match operator#5066
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-eca06eb

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

After a match(true) expression where all non-throwing arms use instanceof checks and the default arm throws, PHPStan failed to narrow the matched variable's type. The variable remained mixed instead of being narrowed to the union of matched types.

Changes

  • src/Analyser/NodeScopeResolver.php: Fixed Expr\Throw_ handling to set $isAlwaysTerminating = true (was incorrectly inheriting from the inner expression result; Exit_ already had this correct)
  • src/Analyser/NodeScopeResolver.php: Added isAlwaysTerminating() checks before adding match arm body scopes to the merge list, in all three code paths: default arms, non-default arms (general path), and non-default arms (enum fast path)
  • tests/PHPStan/Analyser/nsrt/bug-9601.php: New regression test
  • tests/PHPStan/Rules/ScopeFunctionCallStackRuleTest.php: Updated expectations — code after a statement with throw as a function argument is now correctly identified as unreachable
  • tests/PHPStan/Rules/ScopeFunctionCallStackWithParametersRuleTest.php: Same update

Root cause

Two issues combined to cause this bug:

  1. Expr\Throw_ in processExprNode() set $isAlwaysTerminating = $result->isAlwaysTerminating() (inheriting from the inner expression like new LogicException()), which returned false. This should have been true, matching the behavior of Exit_.

  2. Match expression processing unconditionally added all arm body scopes to $armBodyScopes for the post-match scope merge. When the default arm throws, its scope (containing the un-narrowed variable type) was merged with the narrowed scopes from other arms, diluting HelloWorld|HelloWorld2 back to mixed. The fix follows the same pattern used by the ternary operator (lines 4133-4142) and if/else branches (lines 1168/1202), which already excluded always-terminating branches from scope merges.

Test

The regression test creates a match(true) with two instanceof conditions and a throwing default arm, then asserts the variable is narrowed to the union of the matched types after the match expression.

Fixes phpstan/phpstan#9601

- Set isAlwaysTerminating = true for Expr\Throw_ (was incorrectly
  inheriting from inner expression, unlike Exit_ which was already correct)
- Skip always-terminating match arm body scopes when merging post-match
  scope, so throwing arms don't dilute narrowed types
- New regression test in tests/PHPStan/Analyser/nsrt/bug-9601.php
- Updated ScopeFunctionCallStack test expectations: code after a
  statement containing throw-as-argument is now correctly unreachable

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