diff --git a/src/Type/Generic/TemplateFloatType.php b/src/Type/Generic/TemplateFloatType.php index e8ec3de856e..97c41f510a0 100644 --- a/src/Type/Generic/TemplateFloatType.php +++ b/src/Type/Generic/TemplateFloatType.php @@ -37,4 +37,10 @@ public function __construct( $this->default = $default; } + /** The absolute value of a T is not a T, and FloatType's own answer, $this, would claim it is. */ + public function toAbsoluteNumber(): Type + { + return $this->getBound()->toAbsoluteNumber(); + } + } diff --git a/src/Type/Generic/TemplateTypeFactory.php b/src/Type/Generic/TemplateTypeFactory.php index 4b797ca724f..90b3e968956 100644 --- a/src/Type/Generic/TemplateTypeFactory.php +++ b/src/Type/Generic/TemplateTypeFactory.php @@ -85,7 +85,10 @@ public static function create(TemplateTypeScope $scope, string $name, ?Type $bou return new TemplateConstantIntegerType($scope, $strategy, $variance, $name, $bound, $default); } - if ($bound instanceof FloatType && ($boundClass === FloatType::class || $bound instanceof TemplateType)) { + // Catches plain FloatType and the float subtypes that narrowing a bound + // produces, a FloatRangeType or a ConstantFloatType, neither of which has a + // dedicated Template* class to fall into. + if ($bound instanceof FloatType) { return new TemplateFloatType($scope, $strategy, $variance, $name, $bound, $default); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-15714.php b/tests/PHPStan/Analyser/nsrt/bug-15714.php new file mode 100644 index 00000000000..9c1f67c77cc --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15714.php @@ -0,0 +1,163 @@ + 0.0) { + assertType('T of float<5.0E-324, max> (function Bug15714\positive(), argument)', $t); + return $t; + } + + assertType('T of NAN|float (function Bug15714\positive(), argument)', $t); + + return $t; +} + +/** + * @template T of float + * @param T $t + * @return T + */ +function nanCheck(float $t): float +{ + if (is_nan($t)) { + assertType('T of NAN (function Bug15714\nanCheck(), argument)', $t); + return $t; + } + + assertType('T of float (function Bug15714\nanCheck(), argument)', $t); + + return $t; +} + +/** + * @template T of int + * @param T $t + * @return T + */ +function positiveInt(int $t): int +{ + if ($t > 0) { + assertType('T of int<1, max> (function Bug15714\positiveInt(), argument)', $t); + return $t; + } + + assertType('T of int (function Bug15714\positiveInt(), argument)', $t); + + return $t; +} + +/** + * @template T of float + * @param T $t + * @return T + */ +function absolute(float $t): float +{ + assertType('float', abs($t)); + + if ($t < 0.0) { + assertType('T of float (function Bug15714\absolute(), argument)', $t); + assertType('float<5.0E-324, max>', abs($t)); + } + + return $t; +} + +/** + * @template T of int + * @param T $t + * @return T + */ +function absoluteInt(int $t): int +{ + assertType('int<0, max>', abs($t)); + + if ($t < 0) { + assertType('int<0, max>', abs($t)); + } + + return $t; +} + +/** + * @template T of float + * @param T $t + * @return T + */ +function constant(float $t): float +{ + if ($t >= 1.5 && $t <= 1.5) { + assertType('T of 1.5 (function Bug15714\constant(), argument)', $t); + return $t; + } + + return $t; +} + +/** + * @template T of float + * @param T $t + * @return T + */ +function merged(float $t): float +{ + $sign = $t >= 0.0 ? 'positive' : 'negative'; + assertType('T of float<0.0, max> (function Bug15714\merged(), argument)|(T of NAN|float (function Bug15714\merged(), argument))', $t); + + return $t; +} + +/** + * @template T of float|int + * @param T $t + * @return T + */ +function unionBound($t) +{ + if (is_float($t) && $t > 0.0) { + assertType('T of float<5.0E-324, max> (function Bug15714\unionBound(), argument)', $t); + return $t; + } + + return $t; +} + +/** + * @template T of float<0, 1> + * @param T $t + * @return T + */ +function rangeBound(float $t): float +{ + assertType('T of float<0.0, 1.0> (function Bug15714\rangeBound(), argument)', $t); + assertType('float<0.0, 1.0>', abs($t)); + + return $t; +} + +/** + * @template T of 1.5 + * @param T $t + * @return T + */ +function constantBound(float $t): float +{ + assertType('T of 1.5 (function Bug15714\constantBound(), argument)', $t); + + return $t; +} + +function explicitBounds(): void +{ + assertType('0.5', rangeBound(0.5)); + assertType('1.5', constantBound(1.5)); +} diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index b1868e4f98b..55f68eca67d 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -583,4 +583,26 @@ public function testBug15245(): void ]); } + public function testBug15714(): void + { + $this->checkNullables = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-15714.php'], [ + [ + 'Function Bug15714ReturnType\\returnsFloat() should return T of float but returns float.', + 55, + 'Type float is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Function Bug15714ReturnType\\absolute() should return T of float but returns float.', + 69, + ], + [ + 'Function Bug15714ReturnType\\absolute() should return T of float but returns float.', + 72, + 'Type float is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-15714.php b/tests/PHPStan/Rules/Functions/data/bug-15714.php new file mode 100644 index 00000000000..3e29a92c2e2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-15714.php @@ -0,0 +1,85 @@ + 0.0) { + return $t; + } + + return $t; +} + +/** + * @template T of float + * @param T $t + * @return T + */ +function nanCheck(float $t): float +{ + if (is_nan($t)) { + return $t; + } + + return $t; +} + +/** + * @template T of float + * @param T $t + * @return float + */ +function widened(float $t): float +{ + if ($t > 0.0) { + return $t; + } + + return $t; +} + +/** + * @template T of float + * @param T $t + * @return T + */ +function returnsFloat(float $t, float $other): float +{ + if ($t > 0.0) { + return $other; + } + + return $t; +} + +/** + * @template T of float + * @param T $t + * @return T + */ +function absolute(float $t): float +{ + if ($t < 0.0) { + return abs($t); + } + + return abs($t); +} + +/** + * @template T of float + * @param T $t + * @return T + */ +function merged(float $t): float +{ + $sign = $t >= 0.0 ? 'positive' : 'negative'; + + return $t; +} diff --git a/turbo-ext/src/TemplateFloatType.cpp b/turbo-ext/src/TemplateFloatType.cpp index 6983d792f37..2935fdfeba3 100644 --- a/turbo-ext/src/TemplateFloatType.cpp +++ b/turbo-ext/src/TemplateFloatType.cpp @@ -40,6 +40,17 @@ class TemplateFloatType return pt_template_type_create(pt_ce_template_float_type, scope, strategy, variance, name, bound, defaultType); } + /* $this->getBound()->toAbsoluteNumber() — the trait's getter is the slot + * (the class is final), the bound's method goes through its class entry + * (a FloatType, FloatRangeType, ConstantFloatType or another template); + * UNDEF = pending exception */ + zv::Val toAbsoluteNumber() const + { + zval *bound = pt_template_type_bound(self, pt_ce_template_float_type); + if (UNEXPECTED(bound == NULL)) return zv::Val(); + return pt_type_call(Z_OBJ_P(bound), PT_LC("toabsolutenumber"), 0, NULL); + } + private: zend_object *self; }; @@ -69,6 +80,8 @@ PT_MINIT_REGISTRATION(pt_register_template_float_type) if (UNEXPECTED(!PT_THIS.construct(args.scope, args.strategy, args.variance, args.name, args.bound, args.defaultType))) RETURN_THROWS(); }); + cls.method<&TemplateFloatType::toAbsoluteNumber>(sigs::toAbsoluteNumber); + /* the traits the twin uses, after the class's own methods */ ptdecl::TemplateFloatType::registerTraits(cls); diff --git a/turbo-ext/src/TemplateTypeFactory.cpp b/turbo-ext/src/TemplateTypeFactory.cpp index f34213114dd..8fa4d8ae75f 100644 --- a/turbo-ext/src/TemplateTypeFactory.cpp +++ b/turbo-ext/src/TemplateTypeFactory.cpp @@ -102,7 +102,8 @@ class TemplateTypeFactory return of(pt_template_constant_integer_type_new, scope, strategy, variance, nameStr, bound, defaultType); } - if (instanceof_function(boundClass, pt_ce_float_type) && (boundClass == pt_ce_float_type || isTemplate)) { + /* Catches plain FloatType and the float subtypes that narrowing a bound produces, a FloatRangeType or a ConstantFloatType, neither of which has a dedicated Template* class to fall into — like the twin */ + if (instanceof_function(boundClass, pt_ce_float_type)) { return of(pt_template_float_type_new, scope, strategy, variance, nameStr, bound, defaultType); } diff --git a/turbo-ext/src/generated/TemplateFloatType.h b/turbo-ext/src/generated/TemplateFloatType.h index 65cb7aaeedb..db9dce69ef0 100644 --- a/turbo-ext/src/generated/TemplateFloatType.h +++ b/turbo-ext/src/generated/TemplateFloatType.h @@ -53,7 +53,9 @@ inline constexpr char strings[] = "PHPStan\\Type\\FloatType\0" /* 182 */ "default\0" /* 205 */ "PHPStan\\Type\\Type\0" /* 213 */ - "__construct"; /* 231 */ + "__construct\0" /* 231 */ + "\0" /* 243 */ + "toAbsoluteNumber"; /* 244 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 6), /* __construct $scope */ reg::packed(45, 0, 66), /* __construct $templateTypeStrategy */ @@ -61,6 +63,7 @@ inline constexpr reg::PackedArg args[] = { reg::packed(171, MAY_BE_STRING), /* __construct $name */ reg::packed(176, 0, 182), /* __construct $bound */ reg::packed(205, MAY_BE_NULL, 213), /* __construct $default */ + reg::packed(243, 0, 213), /* toAbsoluteNumber return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -68,6 +71,7 @@ using Sig = reg::Sig; /* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ namespace sig { inline constexpr sigtab::Sig __construct = { { 231 /* __construct */, 6, 0, 6, reg::NoArg, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig toAbsoluteNumber = { { 244 /* toAbsoluteNumber */, 0, 6, 0, 6, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::TemplateFloatType diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index 6ca6b112e5c..e593c007c5a 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -5499,6 +5499,10 @@ public function resolve(): \PHPStan\Type\Type 'int1' => new \PHPStan\Type\Constant\ConstantIntegerType(1), 'range' => \PHPStan\Type\IntegerRangeType::fromInterval(0, 10), 'float' => new \PHPStan\Type\FloatType(), + 'floatRange' => \PHPStan\Type\FloatRangeType::fromInterval(0.0, 10.0), + 'floatRangeNegative' => \PHPStan\Type\FloatRangeType::fromInterval(-10.0, -1.0), + 'floatMinus1_5' => new \PHPStan\Type\Constant\ConstantFloatType(-1.5), + 'nan' => new \PHPStan\Type\Constant\ConstantFloatType(NAN), 'bool' => new \PHPStan\Type\BooleanType(), 'true' => new \PHPStan\Type\Constant\ConstantBooleanType(true), 'string' => new \PHPStan\Type\StringType(), @@ -5637,6 +5641,7 @@ public function resolve(): \PHPStan\Type\Type $r["$name comparisons"] = [$view($subject->isGreaterThan($others['null'], $templatePhpVersion)), $view($subject->isGreaterThan($others['int'], $templatePhpVersion)), $view($subject->isGreaterThanOrEqual($others['null'], $templatePhpVersion)), $view($subject->isGreaterThanOrEqual($others['int'], $templatePhpVersion)), $view($subject->isSmallerThan($others['int'], $templatePhpVersion)), $view($subject->isSmallerThanOrEqual($others['int'], $templatePhpVersion)), $view($subject->looseCompare($others['int'], $templatePhpVersion))]; $r["$name predicates"] = [$view($subject->isNull()), $view($subject->isObject()), $view($subject->isArray()), $view($subject->isString()), $view($subject->isInteger()), $view($subject->isIterable()), $view($subject->isCallable()), $view($subject->isScalar()), $view($subject->isConstantValue()), $view($subject->isSuperTypeOf($subject)), $view($subject->getObjectClassNames())]; $r["$name conversions"] = [$view($subject->toBoolean()), $view($subject->toNumber()), $view($subject->toString()), $view($subject->toInteger()), $view($subject->toFloat()), $view($subject->toArray()), $view($subject->getIterableKeyType()), $view($subject->getIterableValueType()), $view($subject->getClassStringType())]; + $r["$name toAbsoluteNumber"] = $view($subject->toAbsoluteNumber()); $r["$name array ops"] = [$view($subject->getKeysArray()), $view($subject->getValuesArray()), $view($subject->popArray()), $view($subject->flipArray()), $view($subject->setOffsetValueType(new \PHPStan\Type\Constant\ConstantStringType('b'), new \PHPStan\Type\IntegerType())), $view($subject->setOffsetValueType(null, new \PHPStan\Type\FloatType())), $view($subject->unsetOffset(new \PHPStan\Type\Constant\ConstantStringType('a'))), $view($subject->getOffsetValueType(new \PHPStan\Type\Constant\ConstantStringType('a')))]; if ($subject instanceof \PHPStan\Type\Generic\GenericObjectType) { $r["$name changeVariances"] = $view($subject->changeVariances([\PHPStan\Type\Generic\TemplateTypeVariance::createCovariant(), \PHPStan\Type\Generic\TemplateTypeVariance::createCovariant()]));