Skip to content

Commit 8aca8b1

Browse files
authored
Add rule to detect duplicated Enum values in the same class (#31)
* Add rule to detect duplicated Enum values in the same class * Add rules.neon files
1 parent db1759c commit 8aca8b1

File tree

5 files changed

+132
-1
lines changed

5 files changed

+132
-1
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"extra": {
2929
"phpstan": {
3030
"includes": [
31-
"extension.neon"
31+
"extension.neon",
32+
"rules.neon"
3233
]
3334
}
3435
}

rules.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
rules:
2+
- Timeweb\PHPStan\Rule\NoDuplicateEnumValueRule
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Timeweb\PHPStan\Rule;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Class_;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Node\ClassConstantsNode;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\ShouldNotHappenException;
13+
14+
/**
15+
* @phpstan-implements Rule<ClassConstantsNode>
16+
*/
17+
class NoDuplicateEnumValueRule implements Rule
18+
{
19+
public function getNodeType(): string
20+
{
21+
return ClassConstantsNode::class;
22+
}
23+
24+
public function processNode(Node $node, Scope $scope): array
25+
{
26+
if (!$node->getClass() instanceof Class_) {
27+
return [];
28+
}
29+
30+
$classReflection = $scope->getClassReflection();
31+
if (!$scope->isInClass() || !$classReflection) {
32+
throw new ShouldNotHappenException();
33+
}
34+
35+
$duplicatedKeysValue = $this->findDuplicatedKeys($node);
36+
if (empty($duplicatedKeysValue)) {
37+
return [];
38+
}
39+
40+
return [
41+
sprintf(
42+
'Enum %s contains duplicated values for %s properties',
43+
$classReflection->getName(),
44+
implode(', ', $duplicatedKeysValue)
45+
),
46+
];
47+
}
48+
49+
/**
50+
* @return string[]
51+
*/
52+
private function findDuplicatedKeys(ClassConstantsNode $node): array
53+
{
54+
$constants = $this->getNodeConstants($node);
55+
56+
$duplicatedValues = array_filter(
57+
array_count_values($constants),
58+
static function ($value) {
59+
return $value > 1;
60+
}
61+
);
62+
63+
$duplicatedKeys = [];
64+
foreach ($duplicatedValues as $value => $count) {
65+
$duplicatedKeys = array_merge($duplicatedKeys, array_keys($constants, $value));
66+
}
67+
68+
return array_unique($duplicatedKeys);
69+
}
70+
71+
/**
72+
* @return array<string, mixed>
73+
*/
74+
private function getNodeConstants(ClassConstantsNode $node): array
75+
{
76+
$constants = [];
77+
foreach ($node->getConstants() as $constant) {
78+
foreach ($constant->consts as $const) {
79+
$constantName = $const->name->toString();
80+
$constants[$constantName] = $const->value->value ?? null;
81+
}
82+
}
83+
84+
return $constants;
85+
}
86+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Timeweb\Tests\PHPStan\Rule;
6+
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Testing\RuleTestCase;
9+
use Timeweb\PHPStan\Rule\NoDuplicateEnumValueRule;
10+
11+
/**
12+
* @coversDefaultClass \Timeweb\PHPStan\Rule\NoDuplicateEnumValueRule
13+
*/
14+
class NoDuplicateEnumValueRuleTest extends RuleTestCase
15+
{
16+
protected function getRule(): Rule
17+
{
18+
return new NoDuplicateEnumValueRule();
19+
}
20+
21+
public function testRule(): void
22+
{
23+
$this->analyse([__DIR__ . '/data/duplicated-enum-values.php'], [
24+
[
25+
'Enum DuplicateEumValues\MyEnum contains duplicated values for PROP_1, DUPLICATED properties',
26+
7,
27+
],
28+
]);
29+
}
30+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace DuplicateEumValues;
4+
5+
use MyCLabs\Enum\Enum;
6+
7+
class MyEnum extends Enum
8+
{
9+
public const PROP_1 = 1;
10+
protected const PROP_2 = 2;
11+
private const DUPLICATED = 1;
12+
}

0 commit comments

Comments
 (0)