Skip to content

Commit 4563fe2

Browse files
committed
Make FloatRangeType a CompoundType and describe it by verbosity level
Other types now ask a float range back when comparing with it or accepting it, so 0 < float<1, 2> is true and 5.0 does not accept float<1, 2>. At type-only verbosity a float range is just float; rule messages switch to value verbosity when a float range is involved, as for integer ranges, and accepting NAN in a float range explains that no float range holds NAN. Amp-Thread-ID: https://ampcode.com/threads/T-01a1a1e7-5e72-7235-a4b2-ba1b689f7e1b
1 parent 9aeade7 commit 4563fe2

15 files changed

Lines changed: 1129 additions & 66 deletions

‎phpstan-baseline.neon‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,12 @@ parameters:
10361036
-
10371037
rawMessage: 'Doing instanceof PHPStan\Type\FloatType is error-prone and deprecated. Use Type::isFloat() instead.'
10381038
identifier: phpstanApi.instanceofType
1039+
count: 4
1040+
path: src/Type/FloatRangeType.php
1041+
1042+
-
1043+
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
1044+
identifier: phpstanApi.instanceofType
10391045
count: 2
10401046
path: src/Type/FloatRangeType.php
10411047

‎src/Type/FloatRangeType.php‎

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
88
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
99
use PHPStan\ShouldNotHappenException;
10+
use PHPStan\TrinaryLogic;
1011
use PHPStan\Turbo\ShadowedByTurboExtension;
1112
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
1213
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
@@ -15,6 +16,7 @@
1516
use PHPStan\Type\Constant\ConstantBooleanType;
1617
use PHPStan\Type\Constant\ConstantFloatType;
1718
use PHPStan\Type\Constant\ConstantIntegerType;
19+
use function array_map;
1820
use function get_class;
1921
use function is_finite;
2022
use function is_float;
@@ -44,7 +46,7 @@
4446
* integer, and every interval operation the integer one.
4547
*/
4648
#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/FloatRangeType.cpp')]
47-
class FloatRangeType extends FloatType
49+
class FloatRangeType extends FloatType implements CompoundType
4850
{
4951

5052
private function __construct(private float $min, private float $max)
@@ -176,14 +178,34 @@ public function getMax(): float
176178

177179
public function describe(VerbosityLevel $level): string
178180
{
179-
return sprintf(
180-
'float<%s, %s>',
181-
$this->min === -INF ? 'min' : (new ConstantFloatType($this->min))->describe(VerbosityLevel::precise()),
182-
$this->max === INF ? 'max' : (new ConstantFloatType($this->max))->describe(VerbosityLevel::precise()),
181+
return $level->handle(
182+
static fn (): string => 'float',
183+
fn (): string => sprintf(
184+
'float<%s, %s>',
185+
$this->min === -INF ? 'min' : (new ConstantFloatType($this->min))->describe(VerbosityLevel::precise()),
186+
$this->max === INF ? 'max' : (new ConstantFloatType($this->max))->describe(VerbosityLevel::precise()),
187+
),
183188
);
184189
}
185190

186191
public function accepts(Type $type, bool $strictTypes): AcceptsResult
192+
{
193+
$result = $this->acceptsWithoutReasons($type, $strictTypes);
194+
if ($result->yes() || (new ConstantFloatType(NAN))->isSuperTypeOf($type)->no()) {
195+
return $result;
196+
}
197+
198+
// tell why when NAN is all that is not accepted
199+
if (!$this->acceptsWithoutReasons(TypeCombinator::remove($type, new ConstantFloatType(NAN)), $strictTypes)->yes()) {
200+
return $result;
201+
}
202+
203+
return new AcceptsResult($result->result, [
204+
sprintf('NAN is in no float range. Use %s|NAN to accept it.', $this->describe(VerbosityLevel::precise())),
205+
]);
206+
}
207+
208+
private function acceptsWithoutReasons(Type $type, bool $strictTypes): AcceptsResult
187209
{
188210
// an integer is coerced to the float closest to it, which keeps the order of the integers
189211
if ($type instanceof ConstantIntegerType) {
@@ -241,6 +263,83 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
241263
return IsSuperTypeOfResult::createNo();
242264
}
243265

266+
public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
267+
{
268+
if ($otherType instanceof parent || $otherType instanceof IntersectionType) {
269+
return $otherType->isSuperTypeOf($this);
270+
}
271+
272+
if ($otherType instanceof UnionType) {
273+
return IsSuperTypeOfResult::createNo()->or(...array_map(fn (Type $innerType): IsSuperTypeOfResult => $innerType->isSuperTypeOf($this), $otherType->getTypes()));
274+
}
275+
276+
// the other type cannot tell floats apart
277+
return $otherType->isSuperTypeOf(new FloatType());
278+
}
279+
280+
public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
281+
{
282+
if ($acceptingType instanceof parent || $acceptingType instanceof IntersectionType) {
283+
return $acceptingType->isSuperTypeOf($this)->toAcceptsResult();
284+
}
285+
286+
if ($acceptingType instanceof UnionType) {
287+
return AcceptsResult::createNo()->or(...array_map(fn (Type $innerType): AcceptsResult => $innerType->accepts($this, $strictTypes), $acceptingType->getTypes()));
288+
}
289+
290+
// the other type cannot tell floats apart
291+
return $acceptingType->accepts(new FloatType(), $strictTypes);
292+
}
293+
294+
public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
295+
{
296+
return $this->compareEachPoint($otherType, false, false, $phpVersion) ?? parent::isSmallerThan($otherType, $phpVersion);
297+
}
298+
299+
public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
300+
{
301+
return $this->compareEachPoint($otherType, false, true, $phpVersion) ?? parent::isSmallerThanOrEqual($otherType, $phpVersion);
302+
}
303+
304+
public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
305+
{
306+
return $this->compareEachPoint($otherType, true, false, $phpVersion) ?? $otherType->isSmallerThan(new FloatType(), $phpVersion);
307+
}
308+
309+
public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
310+
{
311+
return $this->compareEachPoint($otherType, true, true, $phpVersion) ?? $otherType->isSmallerThanOrEqual(new FloatType(), $phpVersion);
312+
}
313+
314+
/**
315+
* Compares $otherType with each float of the range, the other type on the
316+
* left of `<` or `<=` when $otherOnLeft. Numbers, booleans and null compare
317+
* with the floats in their order, so the comparison holds for the whole range
318+
* when it holds for both bounds. A bool or null compares by truthiness, which
319+
* changes only at zero. Null for other types, which can compare with floats
320+
* in any order.
321+
*/
322+
private function compareEachPoint(Type $otherType, bool $otherOnLeft, bool $orEqual, PhpVersion $phpVersion): ?TrinaryLogic
323+
{
324+
$ordered = TypeCombinator::union(new IntegerType(), new FloatType(), new BooleanType(), new NullType());
325+
if (!$ordered->isSuperTypeOf($otherType)->yes()) {
326+
return null;
327+
}
328+
329+
$points = [new ConstantFloatType($this->min), new ConstantFloatType($this->max)];
330+
if (!$this->excludesZero()) {
331+
$points[] = new ConstantFloatType(0.0);
332+
}
333+
334+
$results = [];
335+
foreach ($points as $point) {
336+
[$left, $right] = $otherOnLeft ? [$otherType, $point] : [$point, $otherType];
337+
$results[] = $orEqual ? $left->isSmallerThanOrEqual($right, $phpVersion) : $left->isSmallerThan($right, $phpVersion);
338+
}
339+
340+
return TrinaryLogic::extremeIdentity(...$results);
341+
}
342+
244343
public function equals(Type $type): bool
245344
{
246345
return $type instanceof self && $this->min === $type->min && $this->max === $type->max;

‎src/Type/VerbosityLevel.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ public static function getRecommendedLevelByType(Type $acceptingType, ?Type $acc
184184
$veryVerbose = true;
185185
return $type;
186186
}
187-
if ($type instanceof IntegerRangeType) {
187+
if ($type instanceof IntegerRangeType || $type instanceof FloatRangeType) {
188188
$moreVerbose = true;
189189
return $type;
190190
}

‎tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3413,4 +3413,41 @@ private function getBug15487Errors(): array
34133413
];
34143414
}
34153415

3416+
public function testFloatRangeArgument(): void
3417+
{
3418+
$this->analyse([__DIR__ . '/data/float-range-argument.php'], [
3419+
[
3420+
'Parameter #1 $x of function FloatRangeArgument\\unit expects float<0.0, 1.0>, float given.',
3421+
17,
3422+
],
3423+
[
3424+
'Parameter #1 $x of function FloatRangeArgument\\unit expects float<0.0, 1.0>, 5.0 given.',
3425+
18,
3426+
],
3427+
[
3428+
'Parameter #1 $x of function FloatRangeArgument\\unit expects float<0.0, 1.0>, NAN given.',
3429+
19,
3430+
'NAN is in no float range. Use float<0.0, 1.0>|NAN to accept it.',
3431+
],
3432+
[
3433+
'Parameter #1 $x of function FloatRangeArgument\\unit expects float<0.0, 1.0>, int given.',
3434+
20,
3435+
],
3436+
[
3437+
'Parameter #1 $x of function FloatRangeArgument\\unit expects float<0.0, 1.0>, 2 given.',
3438+
21,
3439+
],
3440+
[
3441+
'Parameter #1 $x of function FloatRangeArgument\\notNan expects float<min, max>, float given.',
3442+
25,
3443+
'NAN is in no float range. Use float<min, max>|NAN to accept it.',
3444+
],
3445+
[
3446+
'Parameter #1 $x of function FloatRangeArgument\\notNan expects float<min, max>, NAN given.',
3447+
26,
3448+
'NAN is in no float range. Use float<min, max>|NAN to accept it.',
3449+
],
3450+
]);
3451+
}
3452+
34163453
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace FloatRangeArgument;
4+
5+
/** @param float<0, 1> $x */
6+
function unit(float $x): void
7+
{
8+
}
9+
10+
/** @param float<min, max> $x */
11+
function notNan(float $x): void
12+
{
13+
}
14+
15+
function doFoo(float $f, int $i): void
16+
{
17+
unit($f);
18+
unit(5.0);
19+
unit(NAN);
20+
unit($i);
21+
unit(2);
22+
unit(1);
23+
unit(0.5);
24+
25+
notNan($f);
26+
notNan(NAN);
27+
notNan($i);
28+
}

‎tests/PHPStan/Type/Constant/ConstantFloatTypeTest.php‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@
33
namespace PHPStan\Type\Constant;
44

55
use PHPStan\Testing\PHPStanTestCase;
6+
use PHPStan\TrinaryLogic;
7+
use PHPStan\Type\FloatRangeType;
8+
use PHPStan\Type\Type;
9+
use PHPStan\Type\TypeCombinator;
610
use PHPStan\Type\VerbosityLevel;
711
use PHPUnit\Framework\Attributes\DataProvider;
12+
use function sprintf;
13+
use const INF;
14+
use const NAN;
815

916
class ConstantFloatTypeTest extends PHPStanTestCase
1017
{
@@ -48,4 +55,34 @@ public function testDescribe(
4855
$this->assertSame($expectedDescription, $type->describe(VerbosityLevel::precise()));
4956
}
5057

58+
public static function dataAccepts(): iterable
59+
{
60+
$range = FloatRangeType::fromInterval(1.0, 2.0);
61+
62+
yield [new ConstantFloatType(1.5), $range, TrinaryLogic::createMaybe()];
63+
yield [new ConstantFloatType(1.0), $range, TrinaryLogic::createMaybe()];
64+
yield [new ConstantFloatType(5.0), $range, TrinaryLogic::createNo()];
65+
yield [new ConstantFloatType(NAN), $range, TrinaryLogic::createNo()];
66+
yield [new ConstantFloatType(INF), $range, TrinaryLogic::createNo()];
67+
yield [new ConstantFloatType(INF), FloatRangeType::fromInterval(1.0, INF), TrinaryLogic::createMaybe()];
68+
yield [new ConstantFloatType(-0.0), FloatRangeType::fromInterval(0.0, 1.0), TrinaryLogic::createMaybe()];
69+
yield [new ConstantFloatType(5.0), TypeCombinator::union($range, new ConstantFloatType(5.0)), TrinaryLogic::createMaybe()];
70+
yield [new ConstantFloatType(5.0), TypeCombinator::union($range, FloatRangeType::fromInterval(3.0, 4.0)), TrinaryLogic::createNo()];
71+
}
72+
73+
#[DataProvider('dataAccepts')]
74+
public function testAccepts(ConstantFloatType $type, Type $otherType, TrinaryLogic $expectedResult): void
75+
{
76+
$this->assertSame(
77+
$expectedResult->describe(),
78+
$type->accepts($otherType, true)->result->describe(),
79+
sprintf('%s -> accepts(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())),
80+
);
81+
$this->assertSame(
82+
$expectedResult->describe(),
83+
$type->isSuperTypeOf($otherType)->result->describe(),
84+
sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())),
85+
);
86+
}
87+
5188
}

0 commit comments

Comments
 (0)