Repository navigation
Keep a narrowed float bound when creating a template type - #7070
Merged
ondrejmirtes merged 2 commits intoNov 14, 2026
Merged
Conversation
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Member
|
Thank you! |
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.
TemplateTypeFactory::create()accepts a float bound only when its class is exactlyFloatType, so a narrowed bound falls through to the last case and the template becomes aTemplateMixedTypewith amixedbound. PHPStan then no longer accepts the value asT of float. This PR accepts every float subtype as a float bound, as #5905 did for integer ranges, enum cases and constant booleans.Impact
T of float$tafterif ($t > 0.0)TT of float<5.0E-324, max>$tafterif (is_nan($t))TT of NAN$tin the false branch ofis_nan($t)TT of float<min, max>$tafterif ($t >= 1.5 && $t <= 1.5)T of mixed~(…)T of 1.5$tafter$t >= 0.0 ? … : …TT of float<0.0, max>|(T of NAN|float<min, -5.0E-324>)abs($t)T of floatfloatFunction positive() should return T of float but returns T.AT of intnarrowed the same way comes out asT of int<1, max>since Preserve template bound for enum-case, integer-range and constant-bool subtypes inTemplateTypeFactory::create()#5905, which added theIntegerRangeTypecase.$tthe merged type was a mixed-boundT, and aT of float|intnarrowed to a float range broke the same way.NAN|float<min, 0.0>, the false branch of$t > 0.0, already became aTemplateUnionType.@template T of float<0, 1>and@template T of 1.5used to be degraded to a mixed bound and are now kept, so a call to such a function returns the argument type.generics.notSupportedBoundstill reports both, as it reportsT of int<1, max>.abs()on a float template was typed as the template itself, becauseTemplateFloatTypeinheritedreturn $thisfromFloatType::toAbsoluteNumber(). That was already wrong forT of float, and with a kept bound it became a definite range on the wrong side of zero:abs($t)in the true branch of$t < 0.0wasT of float<min, -5.0E-324>, soreturn abs($t);in a function returningTwas accepted.TemplateFloatType::toAbsoluteNumber()now takes the absolute value of the bound, asTemplateIntegerTypedoes throughIntegerType::toAbsoluteNumber().Cause
The float case accepts a bound whose class is exactly
FloatType, or aTemplateFloatType. Narrowing produces the two other float subtypes,FloatRangeTypeandConstantFloatType, and both fail the check. The integer case next to it acceptsIntegerRangeTypeexplicitly and sends aConstantIntegerTypeto its ownTemplateConstantIntegerType. Floats have noTemplateConstantFloatType, andTemplateFloatTypeholds anyFloatTypeas its bound, so both subtypes belong in the float case.Fix
The float case now accepts any
FloatType, the way the object case accepts anyObjectTypeand for the same reason, which its comment states: a subtype without a dedicatedTemplate*class keeps its precise bound instead of being widened toTemplateMixedType. A constant bound takes the route the boolean case already takes for aConstantBooleanType, for which there is noTemplateConstantBooleanTypeeither. The nativeTemplateTypeFactoryandTemplateFloatTypein turbo-ext get the same changes.Not changed
These behave the same for
T of intbefore and after this PR:neverbound to the factory, which still becomes a mixed-bound template.if ($t < 0.0) {}followed byif ($t >= 1.5 && $t <= 1.5)therefore still ends inT of mixed~(…).float<5.0E-324, max>is only maybe a supertype ofT of float<5.0E-324, max>, so passing the narrowed value to a parameter of exactly that range is still reported.Testing
tests/PHPStan/Analyser/nsrt/bug-15714.phpcovers:T of intones it compares them with;T of float|intbound;abs()for float and integer templates.ReturnTypeRuleTest::testBug15714()runs functions based on the issue's reproducer and a few more:floatwhereTis expected is still reported.return abs($t);is now reported.turbo-ext/tests/type-family.phpgets range and constant float bounds and an observation oftoAbsoluteNumber(), so the differential test covers both native changes.Closes phpstan/phpstan#15714