Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Type/Generic/TemplateFloatType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

}
5 changes: 4 additions & 1 deletion src/Type/Generic/TemplateTypeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
163 changes: 163 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15714.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php declare(strict_types = 1);

namespace Bug15714;

use function PHPStan\Testing\assertType;

/**
* @template T of float
* @param T $t
* @return T
*/
function positive(float $t): float
{
if ($t > 0.0) {
assertType('T of float<5.0E-324, max> (function Bug15714\positive(), argument)', $t);
return $t;
}

assertType('T of NAN|float<min, 0.0> (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<min, max> (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<min, 0> (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<min, -5.0E-324> (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<min, -5.0E-324> (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));
}
22 changes: 22 additions & 0 deletions tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
],
]);
}

}
85 changes: 85 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-15714.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php declare(strict_types = 1);

namespace Bug15714ReturnType;

/**
* @template T of float
* @param T $t
* @return T
*/
function positive(float $t): float
{
if ($t > 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;
}
13 changes: 13 additions & 0 deletions turbo-ext/src/TemplateFloatType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ class TemplateFloatType
return pt_template_type_create<TemplateFloatType>(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;
};
Expand Down Expand Up @@ -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);

Expand Down
3 changes: 2 additions & 1 deletion turbo-ext/src/TemplateTypeFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
6 changes: 5 additions & 1 deletion turbo-ext/src/generated/TemplateFloatType.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,25 @@ 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 */
reg::packed(108, 0, 129), /* __construct $templateTypeVariance */
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<strings, args>;
} // namespace sigtab

/* 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
Expand Down
5 changes: 5 additions & 0 deletions turbo-ext/tests/type-family.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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()]));
Expand Down
Loading