Anchor template types inferred from invariant generic parameters instead of widening them into a union#5941
Open
phpstan-bot wants to merge 1 commit into
Open
Conversation
…ead of widening them into a union
- GenericParametersAcceptorResolver::resolve() now runs an "anchor" pass before
the main inference loop: template types that occur in an invariant position
(e.g. T inside an invariant Column<T>) are inferred first and substituted into
the other parameters. The remaining occurrences of the same template type are
then validated against the anchored type instead of being unioned into it.
- As a result, `where(Column<T> $column, T $value)` called with an `IntColumn`
(`Column<int>`) and a string now reports the mismatch on `$value`
("expects int, string given") rather than on `$column`
("expects Column<'...'|int>, IntColumn given").
- The anchored type is kept out of the parameter that anchored it, so dependent
template types inferred through a template's bound (e.g.
`@template TModel of ModelInterface<TChild>`) are still resolved.
- Covariant positions (e.g. `iterable<T>`, `@template-covariant`) are unaffected
and keep the existing union-widening behavior.
- Applies uniformly to function, method and static-method calls (shared resolver).
- Added GenericParametersAcceptorResolver::getInvariantTemplateTypeNames() to
detect invariant template occurrences via getReferencedTemplateTypes().
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 generic method/function used a template type
Tin both an invariantgeneric parameter (
Column<T>) and a plain parameter (T $value), PHPStaninferred
Tas the union of every occurrence. So callingproduced the confusing error
i.e. it blamed the (correct)
Column<int>argument and reported a widenedexpected type, instead of pointing at the actually incompatible value argument.
The fix makes inference anchor
Tto the type determined by the invariantgeneric argument (
T = int), so the diagnostic is now reported on$value:This matches what Psalm and Mago report for the same code.
Changes
src/Reflection/GenericParametersAcceptorResolver.phpresolve(): template types occurring in an invariantposition are inferred first and substituted into the other parameters before
the main inference loop, so other occurrences are validated against the
anchored type rather than widening it into a union.
parameter that anchored it, so template types inferred through a template's
bound (e.g.
@template TModel of ModelInterface<TChild>) keep resolving.getInvariantTemplateTypeNames(), which usesType::getReferencedTemplateTypes()(covariant base position) to findtemplate types that appear in an invariant position.
tests/PHPStan/Analyser/nsrt/generic-object-lower-bound.phpandtests/PHPStan/Rules/Methods/CallMethodsRuleTest::testGenericObjectLowerBoundencoded the old union-widening behavior; updated to the corrected behavior
(the mismatch is now reported on the
T $dargument).Root cause
GenericParametersAcceptorResolver::resolve()combined every per-parametertemplate inference with
TemplateTypeMap::union(), which unions the inferredtypes regardless of the variance of the position they came from. An invariant
generic argument like
Column<int>fixesTexactly, but its inference wasunioned with the covariant
T $valueinference, producingint|string. Theresolved
Column<int|string>parameter then failed against the originalColumn<int>argument (invariance), so the error landed on the wrong parameter.The fix recognizes that an invariant position determines the template type
exactly and treats it as an anchor: it is inferred first and the other
occurrences are checked against it. Covariant positions (
iterable<T>,@template-covariant) are deliberately excluded and keep the union behavior,which is verified by a dedicated regression assertion.
Test
tests/PHPStan/Rules/Methods/data/bug-14872.php+CallMethodsRuleTest::testBug14872()— the reported reproducer: the twomismatching calls now report on
$value(expects int, string given/expects string, int given), and matching calls report nothing. Verified thetest fails before the fix (error reported on
$columnwithColumn<'test_string_value'|int>).tests/PHPStan/Analyser/nsrt/bug-14872.php— asserts the inferred return typeis anchored (
int) for the invariant case, and still widened (1|2|'x') for acovariant
iterable<T>position, guarding against over-reach.Fixes phpstan/phpstan#14872