Skip to content

Use lower bound types for contravariant template positions in GenericObjectType::inferTemplateTypes#5460

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-4ttcowz
Open

Use lower bound types for contravariant template positions in GenericObjectType::inferTemplateTypes#5460
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-4ttcowz

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When a function has a parameter like Contravariant<T> (where Contravariant declares @template-contravariant T) and the argument is an Invariant<X> type (where Invariant extends Contravariant with an invariant template), PHPStan incorrectly inferred the function's template T by treating types from the contravariant position as upper bounds (unioned). This caused the inferred type to widen to the broadest argument type, leading to false positive argument.type errors.

Changes

  • Modified GenericObjectType::inferTemplateTypes() in src/Type/Generic/GenericObjectType.php to convert inferred template types to lower bounds when the template position is contravariant
  • The variance detection logic mirrors the existing pattern in getReferencedTemplateTypes(): checks explicit call-site variance first, then falls back to the declared template variance from the class reflection
  • Added regression test tests/PHPStan/Analyser/nsrt/bug-12444.php for the reported issue (contravariant templates with direct and inherited types)
  • Added expanded test tests/PHPStan/Analyser/nsrt/bug-12444b.php covering analogous cases:
    • Non-variadic parameters (already correct, confirmed)
    • Mixed variance templates (covariant Out + contravariant In, confirmed correct)
    • Instance methods and static methods (already correct via same code path, confirmed)

Root cause

GenericObjectType::inferTemplateTypes() always stored inferred types as upper bounds in TemplateTypeMap, regardless of the template's declared variance. When multiple arguments contribute to inferring the same template parameter:

  • Upper bounds are combined via TypeCombinator::union() — this widens the type
  • Lower bounds are combined via TypeCombinator::intersect() — this narrows the type

For contravariant positions, the correct behavior is to use lower bounds (intersection), matching how ClosureType::inferTemplateTypesOnParametersAcceptor() already handles callable parameter types. Without this, when variadic args like Invariant<Throwable> and Invariant<Exception> can't be simplified by TypeCombinator::union() (because invariant templates require exact match), the template inference produced T = Throwable instead of T = RuntimeException.

Test

  • bug-12444.php: Tests the exact reproduction from the issue — contravariant template inference with both direct Contravariant<T> and inherited Invariant<T> extends Contravariant<T> types
  • bug-12444b.php: Tests analogous cases — non-variadic params, mixed variance (@template-covariant + @template-contravariant), and methods/static methods

Fixes phpstan/phpstan#12444

…cObjectType::inferTemplateTypes`

- In `GenericObjectType::inferTemplateTypes()`, inferred types from
  contravariant template positions are now converted to lower bound
  types via `convertToLowerBoundTypes()`. This mirrors how
  `ClosureType` handles parameter types (contravariant positions).
- The effective variance is determined using the same logic as
  `getReferencedTemplateTypes()`: explicit call-site variance takes
  precedence, falling back to the declared template variance.
- This fixes template inference when a child interface with an
  invariant template extends a parent with a contravariant template.
  Previously, types from contravariant positions were treated as upper
  bounds (unioned), causing the inferred type to widen incorrectly.
  Now they are treated as lower bounds (intersected), preserving the
  narrower type from covariant positions.
- Also fixes the same issue for direct use of contravariant generic
  types in non-variadic parameters, methods, and static methods.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants