Skip to content

Commit 2c18f12

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
Fix match(true) exhaustiveness for complementary integer comparisons
- Added resolveComplementaryComparison() method to MutatingScope that checks if the complement of a comparison expression has a stored type in the scope - When resolving $a >= $b, if $a < $b is already stored as false (or true), the complement is returned, allowing match(true) to detect exhaustive arms - Handles all four comparison pairs: < / >=, <= / >, > / <=, >= / < - New regression test in tests/PHPStan/Rules/Comparison/data/bug-5610.php Closes phpstan/phpstan#5610
1 parent 2681e50 commit 2c18f12

3 files changed

Lines changed: 65 additions & 4 deletions

File tree

src/Analyser/MutatingScope.php

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -987,19 +987,23 @@ private function resolveType(string $exprString, Expr $node): Type
987987
}
988988

989989
if ($node instanceof Expr\BinaryOp\Smaller) {
990-
return $this->getType($node->left)->isSmallerThan($this->getType($node->right), $this->phpVersion)->toBooleanType();
990+
return $this->resolveComplementaryComparison($node, new Expr\BinaryOp\GreaterOrEqual($node->left, $node->right))
991+
?? $this->getType($node->left)->isSmallerThan($this->getType($node->right), $this->phpVersion)->toBooleanType();
991992
}
992993

993994
if ($node instanceof Expr\BinaryOp\SmallerOrEqual) {
994-
return $this->getType($node->left)->isSmallerThanOrEqual($this->getType($node->right), $this->phpVersion)->toBooleanType();
995+
return $this->resolveComplementaryComparison($node, new Expr\BinaryOp\Greater($node->left, $node->right))
996+
?? $this->getType($node->left)->isSmallerThanOrEqual($this->getType($node->right), $this->phpVersion)->toBooleanType();
995997
}
996998

997999
if ($node instanceof Expr\BinaryOp\Greater) {
998-
return $this->getType($node->right)->isSmallerThan($this->getType($node->left), $this->phpVersion)->toBooleanType();
1000+
return $this->resolveComplementaryComparison($node, new Expr\BinaryOp\SmallerOrEqual($node->left, $node->right))
1001+
?? $this->getType($node->right)->isSmallerThan($this->getType($node->left), $this->phpVersion)->toBooleanType();
9991002
}
10001003

10011004
if ($node instanceof Expr\BinaryOp\GreaterOrEqual) {
1002-
return $this->getType($node->right)->isSmallerThanOrEqual($this->getType($node->left), $this->phpVersion)->toBooleanType();
1005+
return $this->resolveComplementaryComparison($node, new Expr\BinaryOp\Smaller($node->left, $node->right))
1006+
?? $this->getType($node->right)->isSmallerThanOrEqual($this->getType($node->left), $this->phpVersion)->toBooleanType();
10031007
}
10041008

10051009
if ($node instanceof Expr\BinaryOp\Equal) {
@@ -1609,6 +1613,24 @@ private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type
16091613
return $type;
16101614
}
16111615

1616+
private function resolveComplementaryComparison(Expr\BinaryOp $node, Expr\BinaryOp $complement): ?Type
1617+
{
1618+
$complementKey = $this->getNodeKey($complement);
1619+
if (!array_key_exists($complementKey, $this->expressionTypes)) {
1620+
return null;
1621+
}
1622+
1623+
$complementType = $this->expressionTypes[$complementKey]->getType();
1624+
if ($complementType->isTrue()->yes()) {
1625+
return new ConstantBooleanType(false);
1626+
}
1627+
if ($complementType->isFalse()->yes()) {
1628+
return new ConstantBooleanType(true);
1629+
}
1630+
1631+
return null;
1632+
}
1633+
16121634
private function transformVoidToNull(Type $type, Node $node): Type
16131635
{
16141636
if ($node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME) === true) {

tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,12 @@ public function testBug9534(): void
446446
]);
447447
}
448448

449+
#[RequiresPhp('>= 8.0')]
450+
public function testBug5610(): void
451+
{
452+
$this->analyse([__DIR__ . '/data/bug-5610.php'], []);
453+
}
454+
449455
#[RequiresPhp('>= 8.0')]
450456
public function testBug11310(): void
451457
{
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug5610;
6+
7+
function foo(int $bar, int $baz): int {
8+
return match (true) {
9+
$bar < $baz => 1,
10+
$bar >= $baz => 2,
11+
};
12+
}
13+
14+
function foo3(int $bar, int $baz): int {
15+
return match (true) {
16+
$bar > $baz => 1,
17+
$bar <= $baz => 2,
18+
};
19+
}
20+
21+
function foo4(int $bar, int $baz): int {
22+
return match (true) {
23+
$bar <= $baz => 1,
24+
$bar > $baz => 2,
25+
};
26+
}
27+
28+
function foo5(int $bar, int $baz): int {
29+
return match (true) {
30+
$bar >= $baz => 1,
31+
$bar < $baz => 2,
32+
};
33+
}

0 commit comments

Comments
 (0)