Skip to content

Fix phpstan/phpstan#14396: UnhandledMatchError since 2.1.41#5333

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

Fix phpstan/phpstan#14396: UnhandledMatchError since 2.1.41#5333
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-lq0733l

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Fixes a false positive where an exhaustive match expression inside a foreach loop was incorrectly reported as throwing UnhandledMatchError when treatPhpDocTypesAsCertain was set to false.

Changes

  • Fixed src/Analyser/TypeSpecifier.php in resolveNormalizedIdentical(): when an identity comparison is always true/false (constant boolean) and involves an AlwaysRememberedExpr, the type specification is now created for both the AlwaysRememberedExpr and its unwrapped inner expression, not just the unwrapped one
  • Added regression test tests/PHPStan/Rules/Exceptions/Bug14396Test.php with config tests/PHPStan/Rules/Exceptions/bug-14396.neon and test data tests/PHPStan/Rules/Exceptions/data/bug-14396.php

Root cause

When treatPhpDocTypesAsCertain is false, the MatchHandler uses native types to determine if an arm condition is always true. For match subjects with native type mixed (e.g., from iterating a PHPDoc-typed list<Item> over a natively-typed array), no arm condition evaluates as always-true natively, so $hasAlwaysTrueCond stays false.

The handler then falls back to checking if the remaining PHPDoc type is never. During per-arm type narrowing via filterByFalseyValue, when the match subject type has been narrowed to exactly match the last arm condition (e.g., both are null), the TypeSpecifier takes a fast path for constant boolean identity comparisons. In this fast path, it only specified the type for the unwrapped expression (e.g., $item->status) but NOT for the AlwaysRememberedExpr wrapper that the match handler tracks. This left the AlwaysRememberedExpr type as null instead of narrowing it to never, making the match appear non-exhaustive.

The fix ensures both the wrapper and the inner expression are specified, consistent with how other code paths in the same function handle AlwaysRememberedExpr.

Test

The regression test uses MissingCheckedExceptionInFunctionThrowsRule with treatPhpDocTypesAsCertain: false configured via NEON. It verifies that an exhaustive match expression covering all enum cases plus null inside a foreach loop does not produce a false UnhandledMatchError exception warning.

Fixes phpstan/phpstan#14396

…treatPhpDocTypesAsCertain=false

- Fixed TypeSpecifier to specify types for both AlwaysRememberedExpr and its
  unwrapped inner expression when handling always-true/false identity comparisons
- Previously, only the unwrapped expression was specified, leaving the
  AlwaysRememberedExpr type un-narrowed in the match scope
- New regression test in tests/PHPStan/Rules/Exceptions/Bug14396Test.php
@staabm staabm deleted the create-pull-request/patch-lq0733l branch March 29, 2026 18:46
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