Skip to content

Commit fb84c11

Browse files
jlherrenondrejmirtes
authored andcommitted
Improve specified type in comparisons
Fixes phpstan/phpstan#577
1 parent 69e68a7 commit fb84c11

14 files changed

+606
-25
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,8 @@ public function specifyTypesInCondition(
366366
);
367367

368368
} elseif ($expr instanceof Node\Expr\BinaryOp\Smaller || $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual) {
369-
$offset = $expr instanceof Node\Expr\BinaryOp\Smaller ? 1 : 0;
369+
$orEqual = $expr instanceof Node\Expr\BinaryOp\SmallerOrEqual;
370+
$offset = $orEqual ? 0 : 1;
370371
$leftType = $scope->getType($expr->left);
371372
$rightType = $scope->getType($expr->right);
372373

@@ -431,12 +432,6 @@ public function specifyTypesInCondition(
431432
$context
432433
));
433434
}
434-
435-
$result = $result->unionWith($this->createRangeTypes(
436-
$expr->right,
437-
IntegerRangeType::fromInterval($leftType->getValue(), null, $offset),
438-
$context
439-
));
440435
}
441436

442437
if ($rightType instanceof ConstantIntegerType) {
@@ -459,12 +454,22 @@ public function specifyTypesInCondition(
459454
$context
460455
));
461456
}
457+
}
462458

463-
$result = $result->unionWith($this->createRangeTypes(
464-
$expr->left,
465-
IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset),
466-
$context
467-
));
459+
if ($context->truthy()) {
460+
if (!$expr->left instanceof Node\Scalar) {
461+
$result = $result->unionWith($this->create($expr->left, $rightType->getSmallerType($orEqual), TypeSpecifierContext::createTruthy()));
462+
}
463+
if (!$expr->right instanceof Node\Scalar) {
464+
$result = $result->unionWith($this->create($expr->right, $leftType->getGreaterType($orEqual), TypeSpecifierContext::createTruthy()));
465+
}
466+
} elseif ($context->falsey()) {
467+
if (!$expr->left instanceof Node\Scalar) {
468+
$result = $result->unionWith($this->create($expr->left, $rightType->getGreaterType(!$orEqual), TypeSpecifierContext::createTruthy()));
469+
}
470+
if (!$expr->right instanceof Node\Scalar) {
471+
$result = $result->unionWith($this->create($expr->right, $leftType->getSmallerType(!$orEqual), TypeSpecifierContext::createTruthy()));
472+
}
468473
}
469474

470475
return $result;

src/Type/Constant/ConstantBooleanType.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
use PHPStan\Type\BooleanType;
66
use PHPStan\Type\ConstantScalarType;
7+
use PHPStan\Type\MixedType;
8+
use PHPStan\Type\NeverType;
9+
use PHPStan\Type\StaticTypeFactory;
710
use PHPStan\Type\Traits\ConstantScalarTypeTrait;
811
use PHPStan\Type\Type;
912
use PHPStan\Type\VerbosityLevel;
@@ -30,6 +33,38 @@ public function describe(VerbosityLevel $level): string
3033
return $this->value ? 'true' : 'false';
3134
}
3235

36+
public function getSmallerType(bool $orEqual = false): Type
37+
{
38+
if ($orEqual) {
39+
if ($this->value) {
40+
return new MixedType();
41+
}
42+
return StaticTypeFactory::falsey();
43+
}
44+
45+
if ($this->value) {
46+
return StaticTypeFactory::falsey();
47+
}
48+
49+
return new NeverType();
50+
}
51+
52+
public function getGreaterType(bool $orEqual = false): Type
53+
{
54+
if ($orEqual) {
55+
if ($this->value) {
56+
return StaticTypeFactory::truthy();
57+
}
58+
return new MixedType();
59+
}
60+
61+
if ($this->value) {
62+
return new NeverType();
63+
}
64+
65+
return StaticTypeFactory::truthy();
66+
}
67+
3368
public function toBoolean(): BooleanType
3469
{
3570
return $this;

src/Type/Constant/ConstantFloatType.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Type\CompoundType;
77
use PHPStan\Type\ConstantScalarType;
88
use PHPStan\Type\FloatType;
9+
use PHPStan\Type\Traits\ConstantNumericComparisonTypeTrait;
910
use PHPStan\Type\Traits\ConstantScalarTypeTrait;
1011
use PHPStan\Type\Type;
1112
use PHPStan\Type\VerbosityLevel;
@@ -15,6 +16,7 @@ class ConstantFloatType extends FloatType implements ConstantScalarType
1516

1617
use ConstantScalarTypeTrait;
1718
use ConstantScalarToBooleanTrait;
19+
use ConstantNumericComparisonTypeTrait;
1820

1921
private float $value;
2022

src/Type/Constant/ConstantIntegerType.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPStan\Type\ConstantScalarType;
88
use PHPStan\Type\IntegerRangeType;
99
use PHPStan\Type\IntegerType;
10+
use PHPStan\Type\Traits\ConstantNumericComparisonTypeTrait;
1011
use PHPStan\Type\Traits\ConstantScalarTypeTrait;
1112
use PHPStan\Type\Type;
1213
use PHPStan\Type\VerbosityLevel;
@@ -16,6 +17,7 @@ class ConstantIntegerType extends IntegerType implements ConstantScalarType
1617

1718
use ConstantScalarTypeTrait;
1819
use ConstantScalarToBooleanTrait;
20+
use ConstantNumericComparisonTypeTrait;
1921

2022
private int $value;
2123

src/Type/Constant/ConstantStringType.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
use PHPStan\Type\ErrorType;
1515
use PHPStan\Type\Generic\GenericClassStringType;
1616
use PHPStan\Type\Generic\TemplateType;
17+
use PHPStan\Type\IntegerRangeType;
1718
use PHPStan\Type\MixedType;
19+
use PHPStan\Type\NullType;
1820
use PHPStan\Type\ObjectType;
1921
use PHPStan\Type\StaticType;
2022
use PHPStan\Type\StringType;
2123
use PHPStan\Type\Traits\ConstantScalarTypeTrait;
2224
use PHPStan\Type\Type;
25+
use PHPStan\Type\TypeCombinator;
2326
use PHPStan\Type\VerbosityLevel;
2427

2528
class ConstantStringType extends StringType implements ConstantScalarType
@@ -299,6 +302,45 @@ public function generalize(): Type
299302
return new StringType();
300303
}
301304

305+
public function getSmallerType(bool $orEqual = false): Type
306+
{
307+
$subtractedTypes = [
308+
IntegerRangeType::createAllGreaterThan((float) $this->value, !$orEqual),
309+
];
310+
311+
if ($this->value === '' && !$orEqual) {
312+
$subtractedTypes[] = new NullType();
313+
$subtractedTypes[] = new StringType();
314+
}
315+
316+
$boolValue = (bool) $this->value;
317+
if (!$boolValue && !$orEqual) {
318+
$subtractedTypes[] = new ConstantBooleanType(false);
319+
}
320+
if (!$boolValue || !$orEqual) {
321+
$subtractedTypes[] = new ConstantBooleanType(true);
322+
}
323+
324+
return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
325+
}
326+
327+
public function getGreaterType(bool $orEqual = false): Type
328+
{
329+
$subtractedTypes = [
330+
IntegerRangeType::createAllSmallerThan((float) $this->value, !$orEqual),
331+
];
332+
333+
$boolValue = (bool) $this->value;
334+
if ($boolValue || !$orEqual) {
335+
$subtractedTypes[] = new ConstantBooleanType(false);
336+
}
337+
if ($boolValue && !$orEqual) {
338+
$subtractedTypes[] = new ConstantBooleanType(true);
339+
}
340+
341+
return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
342+
}
343+
302344
/**
303345
* @param mixed[] $properties
304346
* @return Type

src/Type/IntegerRangeType.php

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,62 @@ protected static function isDisjoint(?int $minA, ?int $maxA, ?int $minB, ?int $m
4747
|| $maxA !== null && $minB !== null && $maxA + $offset < $minB;
4848
}
4949

50+
/**
51+
* Return the range of integers smaller than (or equal to) the given value
52+
*
53+
* @param int|float $value
54+
* @param bool $orEqual
55+
* @return Type
56+
*/
57+
public static function createAllSmallerThan($value, bool $orEqual = false): Type
58+
{
59+
if (is_int($value)) {
60+
return self::fromInterval(null, $value, $orEqual ? 0 : -1);
61+
}
62+
63+
if ($value > PHP_INT_MAX || $value >= PHP_INT_MAX && $orEqual) {
64+
return new IntegerType();
65+
}
66+
67+
if ($value < PHP_INT_MIN || $value <= PHP_INT_MIN && !$orEqual) {
68+
return new NeverType();
69+
}
70+
71+
if ($orEqual) {
72+
return self::fromInterval(null, (int) floor($value));
73+
}
74+
75+
return self::fromInterval(null, (int) ceil($value), -1);
76+
}
77+
78+
/**
79+
* Return the range of integers greater than (or equal to) the given value
80+
*
81+
* @param int|float $value
82+
* @param bool $orEqual
83+
* @return Type
84+
*/
85+
public static function createAllGreaterThan($value, bool $orEqual = false): Type
86+
{
87+
if (is_int($value)) {
88+
return self::fromInterval($value, null, $orEqual ? 0 : 1);
89+
}
90+
91+
if ($value < PHP_INT_MIN || $value <= PHP_INT_MIN && $orEqual) {
92+
return new IntegerType();
93+
}
94+
95+
if ($value > PHP_INT_MAX || $value >= PHP_INT_MAX && !$orEqual) {
96+
return new NeverType();
97+
}
98+
99+
if ($orEqual) {
100+
return self::fromInterval((int) ceil($value), null);
101+
}
102+
103+
return self::fromInterval((int) floor($value), null, 1);
104+
}
105+
50106
public function getMin(): ?int
51107
{
52108
return $this->min;
@@ -213,6 +269,43 @@ public function isGreaterThan(Type $otherType, bool $orEqual = false): TrinaryLo
213269
return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller);
214270
}
215271

272+
public function getSmallerType(bool $orEqual = false): Type
273+
{
274+
$subtractedTypes = [];
275+
276+
if ($this->max !== null) {
277+
$subtractedTypes[] = self::createAllGreaterThan($this->max, !$orEqual);
278+
}
279+
280+
if (!$orEqual) {
281+
$subtractedTypes[] = new ConstantBooleanType(true);
282+
}
283+
284+
return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
285+
}
286+
287+
public function getGreaterType(bool $orEqual = false): Type
288+
{
289+
$subtractedTypes = [];
290+
291+
if ($this->min !== null) {
292+
$subtractedTypes[] = self::createAllSmallerThan($this->min, !$orEqual);
293+
}
294+
295+
$alwaysTruthy = $this->min !== null && $this->min > 0 || $this->max !== null && $this->max < 0;
296+
297+
if ($alwaysTruthy || !$orEqual) {
298+
$subtractedTypes[] = new NullType();
299+
$subtractedTypes[] = new ConstantBooleanType(false);
300+
}
301+
302+
if ($alwaysTruthy && !$orEqual) {
303+
$subtractedTypes[] = new ConstantBooleanType(true);
304+
}
305+
306+
return TypeCombinator::remove(new MixedType(), TypeCombinator::union(...$subtractedTypes));
307+
}
308+
216309
public function toNumber(): Type
217310
{
218311
return new parent();

src/Type/IntersectionType.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,20 @@ public function isGreaterThan(Type $otherType, bool $orEqual = false): TrinaryLo
351351
});
352352
}
353353

354+
public function getSmallerType(bool $orEqual = false): Type
355+
{
356+
return $this->intersectTypes(static function (Type $type) use ($orEqual): Type {
357+
return $type->getSmallerType($orEqual);
358+
});
359+
}
360+
361+
public function getGreaterType(bool $orEqual = false): Type
362+
{
363+
return $this->intersectTypes(static function (Type $type) use ($orEqual): Type {
364+
return $type->getGreaterType($orEqual);
365+
});
366+
}
367+
354368
public function toBoolean(): BooleanType
355369
{
356370
$type = $this->intersectTypes(static function (Type $type): BooleanType {

src/Type/NullType.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use PHPStan\TrinaryLogic;
66
use PHPStan\Type\Constant\ConstantArrayType;
7+
use PHPStan\Type\Constant\ConstantBooleanType;
8+
use PHPStan\Type\Constant\ConstantFloatType;
79
use PHPStan\Type\Constant\ConstantIntegerType;
810
use PHPStan\Type\Constant\ConstantStringType;
911
use PHPStan\Type\Traits\FalseyBooleanTypeTrait;
@@ -155,6 +157,40 @@ public function isNumericString(): TrinaryLogic
155157
return TrinaryLogic::createNo();
156158
}
157159

160+
public function getSmallerType(bool $orEqual = false): Type
161+
{
162+
if ($orEqual) {
163+
// All falsey types except '0'
164+
return new UnionType([
165+
new NullType(),
166+
new ConstantBooleanType(false),
167+
new ConstantIntegerType(0),
168+
new ConstantFloatType(0.0),
169+
new ConstantStringType(''),
170+
new ConstantArrayType([], []),
171+
]);
172+
}
173+
174+
return new NeverType();
175+
}
176+
177+
public function getGreaterType(bool $orEqual = false): Type
178+
{
179+
if ($orEqual) {
180+
return new MixedType();
181+
}
182+
183+
// All truthy types, but also '0'
184+
return new MixedType(false, new UnionType([
185+
new NullType(),
186+
new ConstantBooleanType(false),
187+
new ConstantIntegerType(0),
188+
new ConstantFloatType(0.0),
189+
new ConstantStringType(''),
190+
new ConstantArrayType([], []),
191+
]));
192+
}
193+
158194
/**
159195
* @param mixed[] $properties
160196
* @return Type

0 commit comments

Comments
 (0)