Skip to content

Accept bidirectionally equivalent types in invariant template variance check#5709

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-g6z39k6
Open

Accept bidirectionally equivalent types in invariant template variance check#5709
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-g6z39k6

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

For final classes, static(Foo) and Foo are semantically identical since the class cannot be extended. However, PHPStan's invariant template variance check used equals() (a structural comparison) which returned false because StaticType and ObjectType are different PHP classes. This caused false positives like:

Method Value::childCollect() should return Collection<Value> but returns Collection<static(Value)>.

Changes

  • Modified src/Type/Generic/TemplateTypeVariance.php: In the invariant branch of isValidVariance(), after the equals() check fails, added a bidirectional isSuperTypeOf() check. If $a->isSuperTypeOf($b)->yes() AND $b->isSuperTypeOf($a)->yes(), the types are semantically equivalent and accepted. The existing isSuperTypeOf call for the "not covariant" hint is reused (stored in a local variable) to avoid redundant computation.

Root cause

The invariant template variance check in TemplateTypeVariance::isValidVariance() used $a->equals($b) to determine type equality. equals() is a structural comparison — ObjectType::equals() checks get_class($type) !== static::class, so it rejects StaticType even when the types are semantically identical.

For a final class Foo, StaticType(Foo) and ObjectType(Foo) represent the same set of values (since no subclass of Foo exists). Both isSuperTypeOf directions correctly return yes for this case. The fix uses this bidirectional supertype relationship as the semantic equality check.

Analogous cases probed

  • ThisType in final classes: $this(FinalFoo) vs FinalFoo in invariant generics. This requires changes to ThisType::isSuperTypeOf (currently returns maybe for ObjectType even for final classes). A broader ThisType change was tested but reverted because it affected intersection type simplification for enums (which are inherently final). Left for a separate fix.
  • Covariant/contravariant templates: Already handled correctly — these use isSuperTypeOf directly, not equals().
  • Parameter types: Already resolved by TransformStaticTypeTraverser which resolves static to the concrete type for final classes in declared method signatures.
  • Static method return types: Tested and working with the fix.

Test

  • Added rule test tests/PHPStan/Rules/Methods/ReturnTypeRuleTest::testBug14647 with test data reproducing the original issue: a final class with @return Collection<static> returning new Collection([new static()]).
  • Added NSRT test tests/PHPStan/Analyser/nsrt/bug-14647.php verifying inferred types for collect(), childCollect(), boxed(), and staticBoxed() on final classes.
  • Both tests cover the original static case and a static method variant with Box<static>.

Fixes phpstan/phpstan#14647

…e check

- In `TemplateTypeVariance::isValidVariance()`, the invariant check used
  `equals()` which is a structural comparison. `StaticType` and `ObjectType`
  are structurally different PHP classes, so `equals()` returned false even
  when both represent the same type (e.g. `static(FinalClass)` vs `FinalClass`).
- After `equals()` fails, now check bidirectional `isSuperTypeOf()`: if both
  `$a->isSuperTypeOf($b)->yes()` and `$b->isSuperTypeOf($a)->yes()`, the
  types are semantically equivalent and accepted for invariant templates.
- This fixes false positives like `Collection<static(Foo)>` not matching
  `Collection<Foo>` when `Foo` is a final class and the template is invariant.
- Probed analogous cases: `ThisType` in final classes (requires broader
  `ThisType::isSuperTypeOf` changes that affect intersection simplification
  for enums — left for a separate fix), covariant/contravariant templates
  (already handled by `isSuperTypeOf` directly), parameter types (already
  resolved by `transformStaticType`).
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.

1 participant