Fix #9534: Match expression does not handle remaining value: with enumerated class-string#4894
Merged
ondrejmirtes merged 2 commits into2.1.xfrom Feb 12, 2026
Merged
Conversation
- GenericClassStringType::tryRemove() only handled class-string with a single generic type parameter, failing for unions like class-string<A|B|C> - Added handling for union generic types: when removing a constant class-string of a final class, remove the corresponding ObjectType from the inner union - New regression tests in both rule test and nsrt for bug #9534
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When using a
class-string<A|B|C>parameter type with final classes and matching against all possible::classconstants in a match expression, PHPStan incorrectly reported "Match expression does not handle remaining value" even though the match was exhaustive.The root cause was that
GenericClassStringType::tryRemove()only handled the case where the generic type parameter was a single class (e.g.,class-string<Foo>), but not when it was a union of classes (e.g.,class-string<Foo|Bar|Baz>).Changes
src/Type/Generic/GenericClassStringType.phpto handle union generic type parameters intryRemove()class-string<A|B|C>, the correspondingObjectTypeis now removed from the inner union typehasClass()guard before callinggetClass()for robustnesstests/PHPStan/Rules/Comparison/data/bug-9534.phpcovering:tests/PHPStan/Analyser/nsrt/bug-9534.phpverifying type narrowing works correctly through if-chainstestBug9534intests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.phpRoot cause
GenericClassStringType::tryRemove()checkedcount($genericObjectClassNames) === 1and only handled the single-class case. Forclass-string<Car|Bike|Boat>,getObjectClassNames()returns 3 names, so the condition failed and the method fell through to the parent'stryRemove()which doesn't handle this case either. The type was never narrowed, so the match condition type never reachedNeverType, and the match was reported as non-exhaustive.The fix adds an
elseif (count($genericObjectClassNames) > 1)branch that, for final classes, usesTypeCombinator::remove()to remove the matchingObjectTypefrom the inner union, then wraps the result in a newGenericClassStringType.Test
The regression test covers three scenarios:
class-string<Car|Bike|Boat>): Still correctly reports "unhandled" because subclasses could existclass-string<FinalCar|FinalBike|FinalBoat>): No longer reports false positive when all cases are matchedFixes phpstan/phpstan#9534