Skip to content

Fix phpstan/phpstan#14413: Narrowing generic union via ::class comparison discards type parameters#5370

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

Fix phpstan/phpstan#14413: Narrowing generic union via ::class comparison discards type parameters#5370
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-hz0um3g

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When narrowing a generic union type like Cat<string>|Dog<string> via $a::class === Cat::class, PHPStan was discarding the generic type parameters. The narrowed type became Cat&Cat<string> (a redundant intersection) instead of Cat<string>.

Changes

  • Modified src/Analyser/TypeSpecifier.php:
    • Added determineExactClassType() helper method that intersects the expression's current scope type with a plain ObjectType to extract generic parameters, then returns the narrowed type if it's more specific, or falls back to ObjectType with asFinal()
    • Updated 3 call sites: get_class($a) === 'Foo', $a::class === 'Foo', and 'Foo' === $a::class to use the new helper
  • Added tests/PHPStan/Analyser/nsrt/bug-14413.php regression test

Root cause

Commit 772f297 introduced asFinal() on the ObjectType created for ::class/get_class() narrowing to prevent subclass matches. However, ObjectType('Cat', asFinal).isSuperTypeOf(GenericObjectType('Cat', [StringType])) returns Maybe (because hasFinalByKeywordOverride is true on ObjectType but false on GenericObjectType). This prevented TypeCombinator::intersect from simplifying the intersection, leaving both Cat and Cat<string> in the result as Cat&Cat<string>.

The fix pre-narrows the expression type by intersecting with a plain ObjectType (which correctly simplifies generics), then checks if the result is more specific. If it has generic parameters, it uses the narrowed type directly. For non-generic cases, it falls back to the asFinal() behavior.

Test

Added tests/PHPStan/Analyser/nsrt/bug-14413.php covering:

  • match expression with $a::class preserving generic parameters
  • if/else with $a::class === preserving generic parameters
  • Mirror case Cat::class === $a::class
  • match with method call using generic return type
  • Non-matching class narrowing to *NEVER*

Fixes phpstan/phpstan#14413

…owing via ::class comparison

- Added determineExactClassType() helper in TypeSpecifier that intersects the
  current scope type with a plain ObjectType to extract generic parameters
- When generics are found (narrowed type is more specific than plain ObjectType),
  uses the narrowed type; otherwise falls back to ObjectType with asFinal()
- Updated all 3 ::class/get_class narrowing sites to use the new helper
- New regression test in tests/PHPStan/Analyser/nsrt/bug-14413.php
@staabm staabm deleted the create-pull-request/patch-hz0um3g branch March 31, 2026 17:49
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