Resolve ConditionalType when subject-target relationship is deterministic despite containing template types#5631
Conversation
…nistic despite containing template types - `ConditionalType::isResolvable()` previously returned `false` whenever the subject or target contained a template type, even when the type relationship was deterministic (e.g. `T of string` is always a subtype of `string`). - This caused false positive "contains unresolvable type" errors when calling functions/methods with conditional return types like `(T is string ? string : T)` and passing a narrowed template type argument. - The fix checks `target->isSuperTypeOf(subject)`: if the result is `yes()` or `no()`, the conditional is resolvable regardless of template types in subject/target. - Added regression tests for function calls, method calls, and static method calls, plus NSRT tests for type inference correctness.
VincentLanglet
left a comment
There was a problem hiding this comment.
Exclude nsrt test from PHP 7.4 with the // lint comment
VincentLanglet
left a comment
There was a problem hiding this comment.
Add non regression test for phpstan/phpstan#8048
The test uses `mixed` type hint which requires PHP 8.0+. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ensures template type T with class-string parameter is correctly resolved and does not produce false "Unable to resolve the template type T" errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Both review comments addressed and pushed:
Both |
The previous test didn't match the playground reproducer — it used @return T|null instead of the conditional return type that triggers the bug. Replace with the actual reproducer: a conditional return type ($responseType is class-string<T> ? T : null) called with null. Add NSRT test verifying that request(null) resolves to null and request(CustomResponse::class) resolves to CustomResponse. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Pushed. Here's what I did:
|
|
|
||
| $isSuperType = $this->target->isSuperTypeOf($this->subject); | ||
|
|
||
| return $isSuperType->yes() || $isSuperType->no(); |
There was a problem hiding this comment.
mutation testing suggests we miss tests for this line when $isSuperType is maybe
There was a problem hiding this comment.
Pushed. Here's what I did to address staabm's review comment:
-
Added NSRT test for
maybepath (tests/PHPStan/Analyser/nsrt/bug-11894.php): A multi-template class method with@return (TArray is non-empty-array ? non-empty-list<TKey> : list<TKey>)called with anon-empty-array<int, int>argument. This verifies the correct typenon-empty-list<int>is inferred. IfisResolvable()incorrectly returnedtruefor themaybecase, the conditional would resolve prematurely to the union of both branches (list<int>) — this test catches that mutation. -
Added rule test data for
maybecases: AddedtestMaybeRelation(functions) andtestMaybeMethod/testMaybeStaticMethod(methods) that call the conditional return functions withT of string|int. These verify no false "unresolvable type" errors are reported for themaybepath, sinceTypeCombinator::remove(T of string|int, string)producesint(notnever).
…able() Mutation testing showed the `$isSuperType->no()` branch was not specifically covered. Added: - NSRT test with a multi-template conditional return type where premature resolution (incorrectly treating `maybe` as resolvable) would collapse `non-empty-list` to `list` - Rule test data for `maybe` cases (T of string|int with target string) verifying no false "unresolvable type" errors are reported Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
When a function with a conditional return type like
@return (T is string ? string : T)is called with a narrowed template type argument (e.g.,T of stringafter anis_string()check), PHPStan incorrectly reports "Return type of call to function ... contains unresolvable type." This is a false positive — the conditional return type is fully deterministic in this case.Changes
ConditionalType::isResolvable()insrc/Type/ConditionalType.phpto check the actual subject-target type relationship when template types are present. Iftarget->isSuperTypeOf(subject)returns a definitiveyes()orno(), the conditional is considered resolvable.tests/PHPStan/Rules/Functions/data/bug-11894.php(function calls with conditional return types, including negated conditionals)tests/PHPStan/Rules/Methods/data/bug-11894.php(method calls and static method calls — same pattern, different rule)tests/PHPStan/Analyser/nsrt/bug-11894.phpto verify type inference produces correct results for narrowed, non-matching, and partially-narrowable template typesCallToFunctionParametersRuleTest,CallMethodsRuleTest, andCallStaticMethodsRuleTestRoot cause
ConditionalType::isResolvable()unconditionally returnedfalsewhen the subject or target contained a template type. This prevented resolution of the conditional type duringresolveLateResolvableTypes(false)inResolvedFunctionVariantWithOriginal::getReturnType(). The unresolvedConditionalTypethen had its branches traversed byUnresolvableTypeHelper, which found an implicitneverin the normalized else branch — a natural result ofTypeCombinator::remove(T of string, string)producingnever— and incorrectly flagged it as an unresolvable type.The fix makes
isResolvable()smarter: even with template types present, if the subject-target supertype relationship is deterministic (yesorno), the conditional can be resolved. For example,string->isSuperTypeOf(T of string)is alwaysyes(the template's bound guarantees it), so the conditional resolves to theifbranch (string) and the problematicneverin the else branch is never reached.Analogous cases probed
CallMethodsRule— tested and confirmed fixed by the sameConditionalType::isResolvable()changeCallStaticMethodsRule— tested and confirmed fixedT is not string ? T : string): tested, works correctlyT of intpassed toT is string ? ...): confirmed the conditional correctly resolves to the else branch (no()path)T of string|intwith targetstring): confirmed these correctly remain unresolvable (mayberesult)generic-return-type-never.php): confirmed still passing — the fix does not suppress real unresolvable type errorsTest
tests/PHPStan/Rules/Functions/data/bug-11894.php+CallToFunctionParametersRuleTest::testBug11894— function calls with narrowed template types and conditional return types (including negated variants)tests/PHPStan/Rules/Methods/data/bug-11894.php+CallMethodsRuleTest::testBug11894+CallStaticMethodsRuleTest::testBug11894— method and static method equivalentstests/PHPStan/Analyser/nsrt/bug-11894.php— type inference verification for narrowed, non-matching, and partially-narrowable template type argumentsFixes phpstan/phpstan#11894
Fixes phpstan/phpstan#8048