Fix phpstan/phpstan#11430: Two unbounded generics in conditional return are assumed to be always the same#5336
Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Conversation
…rn assumed same - Don't subtract TemplateType from another TemplateType's bound in tryRemove - This prevented `T of mixed~S` from being created, which couldn't match the plain `T` in invariant generic argument checks - New regression test in tests/PHPStan/Rules/TooWideTypehints/data/bug-11430.php
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 a method had two unbounded template types (e.g.
TandS) used in a conditional return type like(T is S ? None : Some<T>), PHPStan incorrectly reported "Method never returns Some so it can be removed from the return type." This was a false positive — the method can clearly return either branch depending on the arguments.Changes
TemplateTypeTrait::tryRemove()insrc/Type/Generic/TemplateTypeTrait.phpto returnnullwhen asked to remove aTemplateType, preventing meaningless template bound narrowingtests/PHPStan/Rules/TooWideTypehints/data/bug-11430.phptestBug11430intests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.phpRoot cause
When resolving the conditional return type
(T is S ? None : Some<T>),ConditionalType::getNormalizedElse()replaces the subjectTin the else branch withTypeCombinator::remove(T, S). This calledTemplateTypeTrait::tryRemove(S), which in turn calledTypeCombinator::remove(mixed, S). Since plainMixedType::tryRemoveconsidersmixeda supertype of any template type, it succeeded and returnedmixed~S(mixed minus S). This created a new template typeT of mixed~S.The problem: when
narrowTypelater compared the declared return typeNone|Some<T of mixed~S>against the actual return typeNone|Some<T>, the invariant generic variance check usedequals()to compareT of mixed~SwithT. Since these aren't equal (different bounds),isValidVariancereturnedNo, causingSome<T of mixed~S>to be reported as never returned.The fix prevents subtracting a TemplateType from another TemplateType's bound, since template types represent unknown types and their subtraction doesn't produce useful narrowing information.
Test
Added a regression test reproducing the exact scenario from the issue: a final class with a static method using two unbounded generics in a conditional return type. The test expects no errors (the false positive is eliminated).
Fixes phpstan/phpstan#11430