Skip to content

Preserve template bound for enum-case, integer-range, constant-float and constant-bool subtypes in TemplateTypeFactory::create()#5905

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

Preserve template bound for enum-case, integer-range, constant-float and constant-bool subtypes in TemplateTypeFactory::create()#5905
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-m8a4z3z

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

When a template-typed variable was narrowed by a comparison (e.g. Foo::Abc === $foo where $foo is TFoo of Foo::*), PHPStan lost the generic type information: the narrowed value became TFoo of mixed instead of TFoo of Foo::Abc, which then caused spurious argument.type errors such as "expects Foo::Abc|Foo::Bcd, TFoo given".

The root cause is in TemplateTypeFactory::create(), which builds the Template* wrapper for a narrowed/computed bound. It dispatched on the exact bound class via get_class(), so any bound that is a subtype of a handled base type but lacks its own dedicated Template* class fell through to the final TemplateMixedType catch-all — silently widening the bound to mixed.

Changes

  • src/Type/Generic/TemplateTypeFactory.php:
    • Reordered the GenericObjectType branch before the ObjectType branch and broadened the latter to accept any ObjectType subtype (notably EnumCaseObjectType), mapping it to TemplateObjectType with the precise bound preserved.
    • Routed IntegerRangeType (subtype of IntegerType) to TemplateIntegerType.
    • Routed ConstantFloatType (subtype of FloatType) to TemplateFloatType.
    • Routed constant booleans (detected via isTrue()/isFalse()) to TemplateBooleanType.
  • tests/PHPStan/Type/TypeCombinatorTest.php: updated data set Fixed broken travis config #67, which previously asserted the buggy TemplateMixedType / 'T (class Foo, parameter)' result and carried // should be ... comments; it now expects TemplateBooleanType / 'T of true (class Foo, parameter)'.

Root cause

TemplateTypeFactory::create() is a dispatch table that maps a bound Type to the matching Template* type. Each branch guarded with $boundClass === SomeType::class to avoid catching subtypes that need their own wrapper. But several concrete subtypes have no dedicated wrapper:

  • EnumCaseObjectType (← ObjectType)
  • IntegerRangeType (← IntegerType)
  • ConstantFloatType (← FloatType)
  • ConstantBooleanType (← BooleanType)

For these, the strict checks skipped every branch and the function fell through to return new TemplateMixedType(...), discarding the bound. The fix routes each of these subtypes to the appropriate base Template* class (which stores the precise bound), so narrowing a template variable keeps its generic identity and bound.

Test

  • tests/PHPStan/Analyser/nsrt/bug-10083.php — a new regression test that mirrors the issue and also covers the analogous families:
    • enum-case narrowing of TFoo of Foo::* in both branches of an if and through a ternary (the reported case),
    • integer-range narrowing of TInt of int (>= 0 && <= 5TInt of int<0, 5>),
    • float narrowing of TFloat of 1.0|2.0 (=== 1.0TFloat of 1.0).
      All assertions fail before the fix (the bound shows as mixed) and pass after.
  • The boolean case is exercised by TypeCombinatorTest::testRemove data set Fixed broken travis config #67 (removing false from a T of bool template now yields T of true).

Fixes phpstan/phpstan#10083

…and constant-bool subtypes in `TemplateTypeFactory::create()`

- `TemplateTypeFactory::create()` matched bounds with strict `get_class()` checks, so any bound that was a subtype of a handled base type (without its own dedicated `Template*` class) fell through to the final `TemplateMixedType` catch-all, silently widening the bound to `mixed`.
- This lost the generic type after narrowing a template variable, e.g. `TFoo of Foo::*` narrowed via `=== Foo::Abc` became `TFoo of mixed`, producing bogus `argument.type` errors.
- Reorder the `GenericObjectType` check before the `ObjectType` check and let any remaining `ObjectType` subtype (e.g. `EnumCaseObjectType`) map to `TemplateObjectType`, keeping the precise bound.
- Route `IntegerRangeType` to `TemplateIntegerType`, `ConstantFloatType` to `TemplateFloatType`, and constant booleans (`isTrue()`/`isFalse()`) to `TemplateBooleanType`.
- Update `TypeCombinatorTest` data set phpstan#67 which was documenting the boolean case as a known bug (`should be T of true`).
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.

Generic type information lost after comparison operation

2 participants