Skip to content

Commit d194c2c

Browse files
Improve EnumSanityRule
1 parent decbf5d commit d194c2c

File tree

4 files changed

+102
-22
lines changed

4 files changed

+102
-22
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ lint:
2020
--exclude tests/PHPStan/Rules/Names/data \
2121
--exclude tests/PHPStan/Rules/Operators/data/invalid-inc-dec.php \
2222
--exclude tests/PHPStan/Rules/Arrays/data/offset-access-without-dim-for-reading.php \
23+
--exclude tests/PHPStan/Rules/Classes/data/bug-13768.php \
2324
--exclude tests/PHPStan/Rules/Classes/data/duplicate-declarations.php \
2425
--exclude tests/PHPStan/Rules/Classes/data/duplicate-enum-cases.php \
2526
--exclude tests/PHPStan/Rules/Classes/data/enum-sanity.php \

src/Rules/Classes/EnumSanityRule.php

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\DependencyInjection\RegisteredRule;
88
use PHPStan\Node\InClassNode;
9+
use PHPStan\Reflection\InitializerExprContext;
10+
use PHPStan\Reflection\InitializerExprTypeResolver;
911
use PHPStan\Rules\Rule;
1012
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\ShouldNotHappenException;
1114
use PHPStan\Type\IntegerType;
1215
use PHPStan\Type\StringType;
1316
use PHPStan\Type\VerbosityLevel;
@@ -16,6 +19,8 @@
1619
use function count;
1720
use function implode;
1821
use function in_array;
22+
use function is_int;
23+
use function is_string;
1924
use function sprintf;
2025

2126
/**
@@ -31,6 +36,12 @@ final class EnumSanityRule implements Rule
3136
'__invoke' => true,
3237
];
3338

39+
public function __construct(
40+
private InitializerExprTypeResolver $initializerExprTypeResolver,
41+
)
42+
{
43+
}
44+
3445
public function getNodeType(): string
3546
{
3647
return InClassNode::class;
@@ -145,27 +156,20 @@ public function processNode(Node $node, Scope $scope): array
145156
}
146157
$caseName = $stmt->name->name;
147158

148-
if ($stmt->expr instanceof Node\Scalar\Int_ || $stmt->expr instanceof Node\Scalar\String_) {
149-
if ($enumNode->scalarType === null) {
150-
$errors[] = RuleErrorBuilder::message(sprintf(
151-
'Enum %s is not backed, but case %s has value %s.',
152-
$classReflection->getDisplayName(),
153-
$caseName,
154-
$stmt->expr->value,
155-
))
156-
->identifier('enum.caseWithValue')
157-
->line($stmt->getStartLine())
158-
->nonIgnorable()
159-
->build();
160-
} else {
161-
$caseValue = $stmt->expr->value;
162-
163-
if (!isset($enumCases[$caseValue])) {
164-
$enumCases[$caseValue] = [];
165-
}
166-
167-
$enumCases[$caseValue][] = $caseName;
168-
}
159+
if ($enumNode->scalarType === null && $stmt->expr !== null) {
160+
$errors[] = RuleErrorBuilder::message(sprintf(
161+
'Enum %s is not backed, but case %s has value %s.',
162+
$classReflection->getDisplayName(),
163+
$caseName,
164+
$this->initializerExprTypeResolver->getType(
165+
$stmt->expr,
166+
InitializerExprContext::fromScope($scope),
167+
)->describe(VerbosityLevel::value()),
168+
))
169+
->identifier('enum.caseWithValue')
170+
->line($stmt->getStartLine())
171+
->nonIgnorable()
172+
->build();
169173
}
170174

171175
if ($enumNode->scalarType === null) {
@@ -189,6 +193,20 @@ public function processNode(Node $node, Scope $scope): array
189193
$exprType = $scope->getType($stmt->expr);
190194
$scalarType = $enumNode->scalarType->toLowerString() === 'int' ? new IntegerType() : new StringType();
191195
if ($scalarType->isSuperTypeOf($exprType)->yes()) {
196+
$constantValues = $exprType->getConstantScalarValues();
197+
if (count($constantValues) === 1) {
198+
$caseValue = $constantValues[0];
199+
if (!is_string($caseValue) && !is_int($caseValue)) {
200+
throw new ShouldNotHappenException();
201+
}
202+
203+
if (!isset($enumCases[$caseValue])) {
204+
$enumCases[$caseValue] = [];
205+
}
206+
207+
$enumCases[$caseValue][] = $caseName;
208+
}
209+
192210
continue;
193211
}
194212

tests/PHPStan/Rules/Classes/EnumSanityRuleTest.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Rules\Classes;
44

5+
use PHPStan\Reflection\InitializerExprTypeResolver;
56
use PHPStan\Rules\Rule;
67
use PHPStan\Testing\RuleTestCase;
78
use PHPUnit\Framework\Attributes\RequiresPhp;
@@ -14,7 +15,9 @@ class EnumSanityRuleTest extends RuleTestCase
1415

1516
protected function getRule(): Rule
1617
{
17-
return new EnumSanityRule();
18+
return new EnumSanityRule(
19+
self::getContainer()->getByType(InitializerExprTypeResolver::class),
20+
);
1821
}
1922

2023
#[RequiresPhp('>= 8.1')]
@@ -141,4 +144,39 @@ public function testBug11592(): void
141144
]);
142145
}
143146

147+
#[RequiresPhp('>= 8.1')]
148+
public function testBug13768(): void
149+
{
150+
$this->analyse([__DIR__ . '/data/bug-13768.php'], [
151+
[
152+
'Enum Bug13768\Order is not backed, but case A has value 1.5.',
153+
7,
154+
],
155+
[
156+
'Enum Bug13768\Order is not backed, but case B has value 2.5.',
157+
8,
158+
],
159+
[
160+
'Enum Bug13768\Order is not backed, but case C has value 3.',
161+
9,
162+
],
163+
[
164+
'Enum Bug13768\Order is not backed, but case D has value \'3\'.',
165+
10,
166+
],
167+
[
168+
'Enum Bug13768\Order is not backed, but case E has value false.',
169+
11,
170+
],
171+
[
172+
'Enum Bug13768\Order is not backed, but case F has value 1.',
173+
12,
174+
],
175+
[
176+
'Enum Bug13768\Backed has duplicate value 1 for cases One, Two.',
177+
20,
178+
],
179+
]);
180+
}
181+
144182
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug13768;
4+
5+
enum Order {
6+
case U;
7+
case A = 1.5;
8+
case B = 2.5;
9+
case C = 3;
10+
case D = '3';
11+
case E = false;
12+
case F = Foo::A;
13+
}
14+
15+
class Foo
16+
{
17+
public const A = 1;
18+
}
19+
20+
enum Backed: int {
21+
case One = Foo::A;
22+
case Two = Foo::A;
23+
}

0 commit comments

Comments
 (0)