Skip to content

Fix phpstan/phpstan#13440: Seemingly same type reports as invalid/incompatible.#5073

Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-v6x5b0k
Closed

Fix phpstan/phpstan#13440: Seemingly same type reports as invalid/incompatible.#5073
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-v6x5b0k

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

Fixes a false positive where PHPStan reports "Parameter #2 $cb of class Box constructor expects Closure(Foo): TNewReturn, Closure(Foo): TNewReturn given" — the expected and given types are identical but flagged as incompatible due to an invariance check failure. This only occurs at level 9+ (checkExplicitMixed: true).

Changes

  • Modified src/Type/Generic/TemplateTypeVariance.php — in the isValidVariance() method's invariant branch, added a fallback check: if equals() fails but both types are TemplateType instances with the same scope and name, treat them as equal
  • Added tests/PHPStan/Rules/Classes/data/bug-13440.php — regression test data file
  • Modified tests/PHPStan/Rules/Classes/InstantiationRuleTest.php — added checkExplicitMixed property and testBug13440() test method

Root cause

At level 9+, RuleLevelHelper::transformCommonType() converts TemplateMixedType to TemplateStrictMixedType (via toStrictMixedType()) to enforce stricter mixed type checking. This transformation preserves the template identity (name, scope, variance) but changes the concrete class.

The accepting type (constructor parameter) gets fully transformed, so TVal inside Closure(Foo<TVal>): TNewReturn becomes a TemplateStrictMixedType. However, transformAcceptedType() deliberately does NOT transform closure parameter types (for sound contravariance reasons), so the given type keeps TVal as TemplateMixedType.

When the invariance check in isValidVariance() compares these two template types using equals(), it fails because TemplateTypeTrait::equals() checks $type instanceof self — and TemplateMixedType is not an instance of TemplateStrictMixedType (or vice versa). The fix adds a secondary check: if both sides are TemplateType instances with the same scope and name, they represent the same template parameter and should pass the invariance check.

Test

Added a regression test that reproduces the issue: a generic class Box<TVal, TReturn> with a method that calls new self($this->val, $cb) where $cb is a closure with a generic type parameter Foo<TVal>. With checkExplicitMixed: true, this previously produced a false positive argument.type error.

Fixes phpstan/phpstan#13440

…tity

- TemplateMixedType and TemplateStrictMixedType with same scope+name represent
  the same template parameter but fail equals() due to different concrete classes
- At level 9+ transformCommonType converts TemplateMixedType to
  TemplateStrictMixedType in accepting type but not in accepted closure params
- Added fallback check in isValidVariance() for invariant templates: if both
  sides are TemplateType with matching scope and name, treat as equal
- New regression test in tests/PHPStan/Rules/Classes/data/bug-13440.php

Closes phpstan/phpstan#13440
@VincentLanglet
Copy link
Contributor

When dumping $a and $b the issue was kinda clear

string(44) "TVal of mixed (class Bug13440\Box, argument)"
string(44) "TVal of mixed (class Bug13440\Box, argument)"
string(38) "PHPStan\Type\Generic\TemplateMixedType"
string(44) "PHPStan\Type\Generic\TemplateStrictMixedType"

The second one was transformed into a strict type by the RuleLevelHelper but not the first one.
Callable/Closure was traversed by the transformAcceptedType but not the transformCommonType.

@VincentLanglet VincentLanglet self-assigned this Feb 28, 2026
@VincentLanglet VincentLanglet force-pushed the create-pull-request/patch-v6x5b0k branch from a846da0 to 63ad626 Compare February 28, 2026 11:23
Copy link
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense. I am not generics expert though.

@staabm staabm changed the title Fix #13440: Seemingly same type reports as invalid/incompatible. Fix phpstan/phpstan#13440: Seemingly same type reports as invalid/incompatible. Feb 28, 2026
@VincentLanglet
Copy link
Contributor

I don't think that's the right fix ; I'll open a new PR

@staabm staabm deleted the create-pull-request/patch-v6x5b0k branch February 28, 2026 12:08
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.

3 participants