Preserve template bound for enum-case, integer-range and constant-bool subtypes in TemplateTypeFactory::create()#5905
Merged
staabm merged 2 commits intoJun 26, 2026
Conversation
…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`).
9c06248 to
1c8dcd9
Compare
staabm
reviewed
Jun 26, 2026
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); |
Contributor
There was a problem hiding this comment.
drop const-float support for now. I think thats not really useful atm
Collaborator
Author
There was a problem hiding this comment.
I've addressed staabm's review comment. Here's what I did:
Changes
Dropped constant-float support from TemplateTypeFactory::create() as requested:
-
src/Type/Generic/TemplateTypeFactory.php:- Reverted the
FloatTypebranch back to its original strict check (removed|| $bound instanceof ConstantFloatType). - Removed the now-unused
ConstantFloatTypeimport.
- Reverted the
-
tests/PHPStan/Analyser/nsrt/bug-10083.php:- Removed the
narrowFloattest function that covered the const-float narrowing case.
- Removed the
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 phpstan→ No errorsTypeCombinatorTest→ OK (1167 tests, 2331 assertions) — the boolean data set Fixed broken travis config #67 still passesNodeScopeResolverTest --filter bug-10083→ OK — enum-case and integer-range narrowing still verified
Committed as a single logical change and pushed to create-pull-request/patch-m8a4z3z.
TemplateTypeFactory::create()TemplateTypeFactory::create()
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VincentLanglet
approved these changes
Jun 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a template-typed variable was narrowed by a comparison (e.g.
Foo::Abc === $foowhere$fooisTFoo of Foo::*), PHPStan lost the generic type information: the narrowed value becameTFoo of mixedinstead ofTFoo of Foo::Abc, which then caused spuriousargument.typeerrors such as "expects Foo::Abc|Foo::Bcd, TFoo given".The root cause is in
TemplateTypeFactory::create(), which builds theTemplate*wrapper for a narrowed/computed bound. It dispatched on the exact bound class viaget_class(), so any bound that is a subtype of a handled base type but lacks its own dedicatedTemplate*class fell through to the finalTemplateMixedTypecatch-all — silently widening the bound tomixed.Changes
src/Type/Generic/TemplateTypeFactory.php:GenericObjectTypebranch before theObjectTypebranch and broadened the latter to accept anyObjectTypesubtype (notablyEnumCaseObjectType), mapping it toTemplateObjectTypewith the precise bound preserved.IntegerRangeType(subtype ofIntegerType) toTemplateIntegerType.isTrue()/isFalse()) toTemplateBooleanType.tests/PHPStan/Type/TypeCombinatorTest.php: updated data set Fixed broken travis config #67, which previously asserted the buggyTemplateMixedType/'T (class Foo, parameter)'result and carried// should be ...comments; it now expectsTemplateBooleanType/'T of true (class Foo, parameter)'.Root cause
TemplateTypeFactory::create()is a dispatch table that maps a boundTypeto the matchingTemplate*type. Each branch guarded with$boundClass === SomeType::classto 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 baseTemplate*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:TFoo of Foo::*in both branches of anifand through a ternary (the reported case),TInt of int(>= 0 && <= 5→TInt of int<0, 5>),TypeCombinatorTest::testRemovedata set Fixed broken travis config #67 (removingfalsefrom aT of booltemplate now yieldsT of true).Fixes phpstan/phpstan#10083