Skip to content

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

Merged
staabm merged 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-m8a4z3z
Jun 26, 2026
Merged

Preserve template bound for enum-case, integer-range and constant-bool subtypes in TemplateTypeFactory::create()#5905
staabm merged 2 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-m8a4z3z

Conversation

@phpstan-bot

@phpstan-bot phpstan-bot commented Jun 21, 2026

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 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)
  • 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>),
  • 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`).
@staabm staabm force-pushed the create-pull-request/patch-m8a4z3z branch from 9c06248 to 1c8dcd9 Compare June 24, 2026 15:31

@staabm staabm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this makes sense to me.

@staabm staabm requested a review from VincentLanglet June 24, 2026 15:45
Comment on lines 87 to 88
if ($bound instanceof FloatType && ($boundClass === FloatType::class || $bound instanceof ConstantFloatType || $bound instanceof TemplateType)) {
return new TemplateFloatType($scope, $strategy, $variance, $name, $bound, $default);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

drop const-float support for now. I think thats not really useful atm

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've addressed staabm's review comment. Here's what I did:

Changes

Dropped constant-float support from TemplateTypeFactory::create() as requested:

  1. src/Type/Generic/TemplateTypeFactory.php:

    • Reverted the FloatType branch back to its original strict check (removed || $bound instanceof ConstantFloatType).
    • Removed the now-unused ConstantFloatType import.
  2. tests/PHPStan/Analyser/nsrt/bug-10083.php:

    • Removed the narrowFloat test function that covered the const-float narrowing case.

The enum-case (ObjectType), integer-range (IntegerRangeType) and constant-bool routing all remain intact, since the reviewer only asked to drop const-float.

Verification

  • make phpstanNo errors
  • TypeCombinatorTestOK (1167 tests, 2331 assertions) — the boolean data set Fixed broken travis config #67 still passes
  • NodeScopeResolverTest --filter bug-10083OK — enum-case and integer-range narrowing still verified

Committed as a single logical change and pushed to create-pull-request/patch-m8a4z3z.

@staabm staabm changed the title Preserve template bound for enum-case, integer-range, constant-float and constant-bool subtypes in TemplateTypeFactory::create() Preserve template bound for enum-case, integer-range and constant-bool subtypes in TemplateTypeFactory::create() Jun 26, 2026
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@staabm staabm merged commit 69a7cf6 into phpstan:2.2.x Jun 26, 2026
671 of 673 checks passed
@staabm staabm deleted the create-pull-request/patch-m8a4z3z branch June 26, 2026 10:05
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

3 participants