Skip to content

Commit 46d1c77

Browse files
phpstan-botclaude
authored andcommitted
Report loosely-equal numeric switch cases as duplicates
`switch` compares subjects with loose `==`, so numerically-equal constant cases like `case 1`, `case '1'` and `case 1.0` all match the exact same subjects and cannot be told apart. Detect a later such case as a duplicate of an earlier one, in addition to the strict-equality check. Booleans, null and non-numeric strings are left to the strict check because their loose match sets are broader than a single value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1573518 commit 46d1c77

2 files changed

Lines changed: 41 additions & 1 deletion

File tree

src/Rules/Comparison/SwitchConditionRule.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Analyser\Scope;
1010
use PHPStan\Node\Printer\ExprPrinter;
1111
use PHPStan\Node\SwitchConditionNode;
12+
use PHPStan\Php\PhpVersion;
1213
use PHPStan\Rules\Rule;
1314
use PHPStan\Rules\RuleErrorBuilder;
1415
use PHPStan\Type\Type;
@@ -27,6 +28,7 @@ public function __construct(
2728
private PossiblyImpureTipHelper $possiblyImpureTipHelper,
2829
private ConstantConditionInTraitHelper $constantConditionInTraitHelper,
2930
private ExprPrinter $exprPrinter,
31+
private PhpVersion $phpVersion,
3032
private bool $treatPhpDocTypesAsCertain,
3133
)
3234
{
@@ -62,7 +64,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE
6264
$caseValueType = $finiteTypes[0];
6365
$firstSeen = null;
6466
foreach ($seenCases as $seenCase) {
65-
if ($seenCase['type']->equals($caseValueType)) {
67+
if ($this->isDuplicateCase($seenCase['type'], $caseValueType)) {
6668
$firstSeen = $seenCase;
6769
break;
6870
}
@@ -160,4 +162,32 @@ private function isConstantBoolean(Type $type): bool
160162
return $type->isTrue()->yes() || $type->isFalse()->yes();
161163
}
162164

165+
/**
166+
* A later `case` is a duplicate of an earlier one when both match the exact
167+
* same set of subject values. Besides identical values, `switch` compares
168+
* with loose `==`, so two numerically-equal constants (e.g. 1, '1' and 1.0)
169+
* are duplicates too - they cannot be told apart by a `switch`. Booleans,
170+
* null and non-numeric strings are intentionally left to the strict check
171+
* because their loose-comparison match sets are broader than a single value.
172+
*/
173+
private function isDuplicateCase(Type $seenType, Type $caseValueType): bool
174+
{
175+
if ($seenType->equals($caseValueType)) {
176+
return true;
177+
}
178+
179+
if (!$this->isNumericConstant($seenType) || !$this->isNumericConstant($caseValueType)) {
180+
return false;
181+
}
182+
183+
return $seenType->looseCompare($caseValueType, $this->phpVersion)->isTrue()->yes();
184+
}
185+
186+
private function isNumericConstant(Type $type): bool
187+
{
188+
return $type->isInteger()->yes()
189+
|| $type->isFloat()->yes()
190+
|| $type->isNumericString()->yes();
191+
}
192+
163193
}

tests/PHPStan/Rules/Comparison/SwitchConditionRuleTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Rules\Comparison;
44

55
use PHPStan\Node\Printer\ExprPrinter;
6+
use PHPStan\Php\PhpVersion;
67
use PHPStan\Rules\Rule;
78
use PHPStan\Testing\CompositeRule;
89
use PHPStan\Testing\RuleTestCase;
@@ -35,6 +36,7 @@ protected function getRule(): Rule
3536
new PossiblyImpureTipHelper(true),
3637
self::getContainer()->getByType(ConstantConditionInTraitHelper::class),
3738
self::getContainer()->getByType(ExprPrinter::class),
39+
self::getContainer()->getByType(PhpVersion::class),
3840
$this->treatPhpDocTypesAsCertain,
3941
),
4042
new ConstantConditionInTraitRule(),
@@ -93,6 +95,14 @@ public function testRule(): void
9395
'Case null in switch is a duplicate of case null on line 108.',
9496
112,
9597
],
98+
[
99+
'Case \'1\' in switch is a duplicate of case 1 on line 149.',
100+
151,
101+
],
102+
[
103+
'Case 1.0 in switch is a duplicate of case 1 on line 149.',
104+
153,
105+
],
96106
]);
97107
}
98108

0 commit comments

Comments
 (0)