Skip to content

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

Open
mhert wants to merge 1 commit intophpstan:2.1.xfrom
mhert:fix-class-string-generic-preservation
Open

Fix phpstan/phpstan#14413: Narrowing generic union via ::class comparison discards type parameters#5366
mhert wants to merge 1 commit intophpstan:2.1.xfrom
mhert:fix-class-string-generic-preservation

Conversation

@mhert
Copy link
Copy Markdown
Contributor

@mhert mhert commented Mar 31, 2026

Summary

Narrowing a generic union like Cat<string>|Dog<string> via $a::class === Cat::class discarded the generic type parameters — the inferred type became plain Cat instead of Cat<string>. This caused downstream type errors when accessing generic methods on the narrowed variable.

Motivation

When using ::class comparisons to narrow generic union types, PHPStan's TypeSpecifier::resolveNormalizedIdentical() constructed a plain ObjectType without consulting the variable's known type for generic information. This meant that code like:

/** @param Cat<string>|Dog<string> $a */
function foo(Animal $a): void {
    if ($a::class === Cat::class) {
        $a->value(); // was inferred as mixed instead of string
    }
}

lost all generic type information after narrowing.

Changes

  • src/Analyser/TypeSpecifier.php — Extracted the duplicated $a::class === 'Foo' / 'Foo' === $a::class handling into a single resolveClassStringComparison() helper. Added generic-aware narrowing that infers template type parameters from the variable's current type via getAncestorWithClassName() + inferTemplateTypes().

Test Cases

  • tests/PHPStan/Analyser/nsrt/class-string-generic-narrowing.php — Multiple scenarios:
    • Union match preserves generics (Cat<string>|Dog<string>Cat<string> in match arm)
    • If/else ::class comparison preserves generics
    • Mirror case (Cat::class === $a::class) preserves generics
    • Match with method call on narrowed generic type
    • Non-matching class comparison yields *NEVER*

Fixes phpstan/phpstan#14413

Co-Authored-By: Claude Code

… ::class comparison

Narrowing a generic union like Cat<string>|Dog<string> via $a::class === Cat::class
discarded the generic type parameters — the type became plain Cat instead of Cat<string>.
This happened because resolveNormalizedIdentical() constructed a plain ObjectType without
consulting the variable's known type for generic information.

Extracts the duplicated $a::class === 'Foo' / 'Foo' === $a::class handling into a single
resolveClassStringComparison() helper and adds generic-aware narrowing: it first tries
TypeCombinator::intersect() with the current variable type (which preserves generics from
unions), and falls back to template inference via getAncestorWithClassName() +
inferTemplateTypes() for single generic parents.
@ondrejmirtes
Copy link
Copy Markdown
Member

Please open an issue with a reproducing code example first.

@phpstan-bot
Copy link
Copy Markdown
Collaborator

This pull request has been marked as ready for review.

@mhert mhert changed the title fix: preserve generic type parameters when narrowing via ::class comparison Fix phpstan/phpstan#14413: Narrowing generic union via ::class comparison discards type parameters Mar 31, 2026
@mhert
Copy link
Copy Markdown
Contributor Author

mhert commented Apr 2, 2026

Friendly ping for review when you get a chance 😊

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