Resolve decided ConditionalType and ConditionalTypeForParameter when inheriting PHPDoc from a parent - #6298
Open
phpstan-bot wants to merge 1 commit into
Open
Conversation
…hen inheriting PHPDoc from a parent * `ResolvedPhpDocBlock::resolveTemplateTypeInTag()` now runs a new `simplifyConditionalTypes()` pass after `TemplateTypeHelper::resolveTemplateTypes()`, so a conditional type that became decided by substituting the parent's `@template` types with the `@extends`/`@implements`/`@use` arguments is replaced by its result. * `ConditionalType` is only resolved when `isResolvable()` and its subject contains no `StaticType`/`ThisType` — the late static bound type is known only at the call site, so resolving it eagerly would lose narrowing in grandchild classes. * `ConditionalTypeForParameter` is resolved against the merged `@param` type, but only when the target type is decidedly a supertype (or decidedly not) of the parameter type; an undecided condition is left alone so it can still be resolved against the actual argument type at the call site. * The merged `@param` tags are threaded into the merging of `@return`, `@param-out`, `@param-closure-this` and `@phpstan-assert`, and inherited `@param` tags get a second pass once all parameter types are known. * Same fix therefore applies to all inherited tags resolved through `resolveTemplateTypeInTag()`: `@var` (properties and class constants), `@param`, `@param-out`, `@param-closure-this`, `@return` and `@phpstan-assert`, and to inheritance through parent classes, interfaces and abstract trait methods. * Probed and found already correct: `@method` tags with conditional return types on generic classes, and call-site resolution of `ConditionalTypeForParameter` when the parameter type leaves the condition undecided.
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
A conditional type declared on a generic parent (
@return (T is int ? string : int)) was inherited verbatim by a subclass that fixes the template argument (@extends A<string>). The subject was substituted tostring, but the type stayed aConditionalType, soMethodConditionalReturnTypeRulereportedConditional return type uses subject type string which is not part of PHPDoc @template tags.on a method that has no PHPDoc of its own.The fix simplifies conditional types while the PHPDoc is being inherited: once the parent's template types are substituted, a condition that has become decided is replaced by its branch (recursively, so nested conditionals collapse too).
Changes
src/PhpDoc/ResolvedPhpDocBlock.phpsimplifyConditionalTypes()pass, run fromresolveTemplateTypeInTag()right afterTemplateTypeHelper::resolveTemplateTypes(). BecauseresolveTemplateTypeInTag()is used only for tags taken over from a parent, PHPDoc written on the method/property itself is untouched and still reported on as before.containsStaticType()helper guarding the simplification.@paramtags are passed down tomergeReturnTags(),mergeParamOutTags(),mergeParamClosureThisTags()andmergeAssertTags()soConditionalTypeForParametercan be resolved against the inherited parameter type. Inherited@paramtags themselves are simplified in a second pass, once every parameter type is known.Cases fixed (each has a failing-before test):
@return (T is int ? string : int)inherited from a generic parent class — the reported bug.@implements).@use).Leaf extends Mid extends Base<string>).@return ($val is int ? string : int)—ConditionalTypeForParameter, which reportedCondition "string is int" in conditional return type is always false.instead.(T is not int ? ...)).list<(T is int ? string : int)>).@param-outand in@param.Probed and found already correct, so no change and no test kept:
@methodtags with a conditional return type on a generic class — those are not checked by the rule and were already resolved correctly.@self-out— not template-resolved during inheritance at all today, and not reachable from the conditional-return-type rules.Root cause
TemplateTypeHelper::resolveTemplateTypes()traverses intoConditionalType::traverse(), which rebuilds the conditional type with the substituted subject but never asks whether the condition has become decidable. The type therefore stays aConditionalTypewhose subject is a plainstring, which is exactly the shapeConditionalReturnTypeRuleHelperflags as "subject is not a template type". The user-visible error was attributed to the subclass even though the PHPDoc lives on the parent.The same pattern applies to
ConditionalTypeForParameter: substituting@param T $valto@param string $valdecides($val is int ? ...), and the rule then reports an always-false condition on the subclass.Two things must not be simplified, and both are guarded:
static/$this, because the late static bound type is only known at the call site — resolving it in the subclass would lose narrowing for a grandchild class that starts satisfying the condition;ConditionalTypeForParameterwhose condition is undecided by the declared parameter type, because it still has to be resolved against the actual argument type at the call site.Test
tests/PHPStan/Analyser/nsrt/bug-15128.php— the playground reproducer plus subclasses that keep the condition undecided (@extends A<int|string>) or stay generic (@extends A<U>); asserts the inferred types are unchanged, including per-call-site resolution ofConditionalTypeForParameter.tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest::testBug15128()— analyses the reproducer with the rule and expects no errors. Fails without the fix with the reportedconditionalType.subjectNotFounderror.tests/PHPStan/Rules/PhpDoc/data/conditional-return-type-inheritance.php+testConditionalReturnTypeInheritance()— covers the interface, trait, multi-level-chain, negated, nested,@param-out,@paramandConditionalTypeForParametervariants. Fails without the fix with 8 errors.tests/PHPStan/Analyser/nsrt/conditional-type-inherited-static-subject.php— locks in that astaticsubject keeps being resolved per call site; removing thecontainsStaticType()guard makes it fail.Fixes phpstan/phpstan#15128