Skip to content

Commit b4320ac

Browse files
authored
Add PHPStan rule to detect duplicate enum values (#343)
1 parent caa3ebd commit b4320ac

8 files changed

+120
-7
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
## 6.7.0
11+
12+
### Added
13+
14+
- Add PHPStan rule to detect duplicate enum values
15+
1016
## 6.6.4
1117

1218
### Fixed

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -859,11 +859,11 @@ Use the [nova-enum-field](https://github.com/simplesquid/nova-enum-field) packag
859859

860860
## PHPStan Integration
861861

862-
If you are using [PHPStan](https://github.com/phpstan/phpstan) for static
863-
analysis, you can enable the extension for proper recognition of the
864-
magic instantiation methods.
862+
If you are using [PHPStan](https://github.com/phpstan/phpstan) for static analysis, enable the extension for:
863+
- proper recognition of the magic instantiation methods
864+
- detection of duplicate enum values
865865

866-
Add the following to your projects `phpstan.neon` includes:
866+
Use [PHPStan Extension Installer](https://github.com/phpstan/extension-installer) or add the following to your projects `phpstan.neon` includes:
867867

868868
```neon
869869
includes:

extension.neon

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
services:
2-
- class: \BenSampo\Enum\PHPStan\EnumMethodsClassReflectionExtension
2+
- class: BenSampo\Enum\PHPStan\EnumMethodsClassReflectionExtension
33
tags:
44
- phpstan.broker.methodsClassReflectionExtension
5+
- class: BenSampo\Enum\PHPStan\UniqueValuesRule
6+
tags:
7+
- phpstan.rules.rule

phpstan.neon

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ parameters:
2121
- '#invalid type Illuminate\\Process#'
2222
excludePaths:
2323
- tests/Enums/ToNativeFixtures # Fails with PHP < 8.1
24+
- tests/PHPStan/Fixtures
2425
# Install https://plugins.jetbrains.com/plugin/7677-awesome-console to make those links clickable
2526
editorUrl: '%%relFile%%:%%line%%'
2627
editorUrlTitle: '%%relFile%%:%%line%%'

src/PHPStan/UniqueValuesRule.php

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace BenSampo\Enum\PHPStan;
4+
5+
use BenSampo\Enum\Enum;
6+
use PhpParser\Node;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\InClassNode;
9+
use PHPStan\Rules\Rule;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
12+
/** @implements Rule<InClassNode> */
13+
final class UniqueValuesRule implements Rule
14+
{
15+
public function getNodeType(): string
16+
{
17+
return InClassNode::class;
18+
}
19+
20+
public function processNode(Node $node, Scope $scope): array
21+
{
22+
assert($node instanceof InClassNode);
23+
24+
$reflection = $node->getClassReflection();
25+
if (! $reflection->isSubclassOf(Enum::class)) {
26+
return [];
27+
}
28+
29+
$constants = [];
30+
foreach ($reflection->getNativeReflection()->getReflectionConstants() as $constant) {
31+
$constants[$constant->name] = $constant->getValue();
32+
}
33+
34+
$duplicateConstants = [];
35+
foreach ($constants as $name => $value) {
36+
$constantsWithValue = array_filter($constants, fn (mixed $v): bool => $v === $value);
37+
if (count($constantsWithValue) > 1) {
38+
$duplicateConstants []= array_keys($constantsWithValue);
39+
}
40+
}
41+
$duplicateConstants = array_unique($duplicateConstants);
42+
43+
if (count($duplicateConstants) > 0) {
44+
$fqcn = $reflection->getName();
45+
$constantsString = json_encode($duplicateConstants);
46+
47+
return [
48+
RuleErrorBuilder::message("Enum class {$fqcn} contains constants with duplicate values: {$constantsString}.")
49+
->build(),
50+
];
51+
}
52+
53+
return [];
54+
}
55+
}

tests/PHPStanTest.php renamed to tests/PHPStan/EnumMethodsClassReflectionExtensionTest.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php declare(strict_types=1);
22

3-
namespace BenSampo\Enum\Tests;
3+
namespace BenSampo\Enum\Tests\PHPStan;
44

55
use BenSampo\Enum\PHPStan\EnumMethodsClassReflectionExtension;
66
use BenSampo\Enum\Tests\Enums\AnnotatedConstants;
@@ -9,7 +9,7 @@
99
use PHPStan\Reflection\MethodReflection;
1010
use PHPStan\Testing\PHPStanTestCase;
1111

12-
final class PHPStanTest extends PHPStanTestCase
12+
final class EnumMethodsClassReflectionExtensionTest extends PHPStanTestCase
1313
{
1414
private EnumMethodsClassReflectionExtension $reflectionExtension;
1515

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace BenSampo\Enum\Tests\PHPStan\Fixtures;
4+
5+
use BenSampo\Enum\Enum;
6+
7+
/**
8+
* @extends Enum<string>
9+
*
10+
* @method static static A()
11+
* @method static static B()
12+
*/
13+
final class DuplicateValue extends Enum
14+
{
15+
public const A = 'A';
16+
public const B = 'A';
17+
}
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace BenSampo\Enum\Tests\PHPStan;
4+
5+
use BenSampo\Enum\PHPStan\UniqueValuesRule;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/** @extends RuleTestCase<UniqueValuesRule> */
10+
final class UniqueValuesRuleTest extends RuleTestCase
11+
{
12+
protected function getRule(): Rule
13+
{
14+
return new UniqueValuesRule();
15+
}
16+
17+
public function testRule(): void
18+
{
19+
$this->analyse(
20+
[
21+
__DIR__ . '/Fixtures/DuplicateValue.php',
22+
],
23+
[
24+
[
25+
'Enum class BenSampo\Enum\Tests\PHPStan\Fixtures\DuplicateValue contains constants with duplicate values: [["A","B"]].',
26+
13,
27+
],
28+
],
29+
);
30+
}
31+
}

0 commit comments

Comments
 (0)