From 206fa2603a2a12e677115e687a7bc3a6bcf23894 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 15 Nov 2023 16:01:46 +0100 Subject: [PATCH 1/3] Add PHPStan rule to detect duplicate enum values --- CHANGELOG.md | 6 +++ extension.neon | 5 +- phpstan.neon | 1 + src/PHPStan/UniqueValuesRule.php | 46 +++++++++++++++++++ ...umMethodsClassReflectionExtensionTest.php} | 4 +- tests/PHPStan/Fixtures/DuplicateValue.php | 17 +++++++ tests/PHPStan/UniqueValuesRuleTest.php | 31 +++++++++++++ 7 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 src/PHPStan/UniqueValuesRule.php rename tests/{PHPStanTest.php => PHPStan/EnumMethodsClassReflectionExtensionTest.php} (97%) create mode 100644 tests/PHPStan/Fixtures/DuplicateValue.php create mode 100644 tests/PHPStan/UniqueValuesRuleTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2201563a..4b27cd0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## 6.7.0 + +### Added + +- Add PHPStan rule to detect duplicate enum values + ## 6.6.4 ### Fixed diff --git a/extension.neon b/extension.neon index bdbeebf6..50777eb0 100644 --- a/extension.neon +++ b/extension.neon @@ -1,4 +1,7 @@ services: -- class: \BenSampo\Enum\PHPStan\EnumMethodsClassReflectionExtension +- class: BenSampo\Enum\PHPStan\EnumMethodsClassReflectionExtension tags: - phpstan.broker.methodsClassReflectionExtension +- class: BenSampo\Enum\PHPStan\UniqueValuesRule + tags: + - phpstan.rules.rule diff --git a/phpstan.neon b/phpstan.neon index 3484cf73..d58182b4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -21,6 +21,7 @@ parameters: - '#invalid type Illuminate\\Process#' excludePaths: - tests/Enums/ToNativeFixtures # Fails with PHP < 8.1 + - tests/PHPStan/Fixtures # Install https://plugins.jetbrains.com/plugin/7677-awesome-console to make those links clickable editorUrl: '%%relFile%%:%%line%%' editorUrlTitle: '%%relFile%%:%%line%%' diff --git a/src/PHPStan/UniqueValuesRule.php b/src/PHPStan/UniqueValuesRule.php new file mode 100644 index 00000000..f63bfc0b --- /dev/null +++ b/src/PHPStan/UniqueValuesRule.php @@ -0,0 +1,46 @@ + */ +final class UniqueValuesRule implements Rule +{ + public function getNodeType(): string + { + return InClassNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + assert($node instanceof InClassNode); + + $reflection = $node->getClassReflection(); + if (! $reflection->isSubclassOf(Enum::class)) { + return []; + } + + $constants = []; + foreach ($reflection->getNativeReflection()->getReflectionConstants() as $constant) { + $constants[$constant->name] = $constant->getValue(); + } + + if (count($constants) !== count(array_unique($constants))) { + $fqcn = $reflection->getName(); + $constantsString = json_encode($constants); + + return [ + RuleErrorBuilder::message("Enum class {$fqcn} contains constants with duplicate values: {$constantsString}.") + ->build(), + ]; + } + + return []; + } +} diff --git a/tests/PHPStanTest.php b/tests/PHPStan/EnumMethodsClassReflectionExtensionTest.php similarity index 97% rename from tests/PHPStanTest.php rename to tests/PHPStan/EnumMethodsClassReflectionExtensionTest.php index 130e34ac..8ecf5b5a 100644 --- a/tests/PHPStanTest.php +++ b/tests/PHPStan/EnumMethodsClassReflectionExtensionTest.php @@ -1,6 +1,6 @@ + * + * @method static static A() + * @method static static B() + */ +final class DuplicateValue extends Enum +{ + public const A = 'A'; + public const B = 'A'; +} diff --git a/tests/PHPStan/UniqueValuesRuleTest.php b/tests/PHPStan/UniqueValuesRuleTest.php new file mode 100644 index 00000000..3aeeca02 --- /dev/null +++ b/tests/PHPStan/UniqueValuesRuleTest.php @@ -0,0 +1,31 @@ + */ +final class UniqueValuesRuleTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new UniqueValuesRule(); + } + + public function testRule(): void + { + $this->analyse( + [ + __DIR__ . '/Fixtures/DuplicateValue.php', + ], + [ + [ + 'Enum class BenSampo\Enum\Tests\PHPStan\Fixtures\DuplicateValue contains constants with duplicate values: {"A":"A","B":"A"}.', + 13, + ], + ], + ); + } +} From f65900f5d1de1efd7a8c9e162d580f8b5a409447 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 15 Nov 2023 16:05:48 +0100 Subject: [PATCH 2/3] doc --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a0a21cd4..770f6077 100644 --- a/README.md +++ b/README.md @@ -859,11 +859,11 @@ Use the [nova-enum-field](https://github.com/simplesquid/nova-enum-field) packag ## PHPStan Integration -If you are using [PHPStan](https://github.com/phpstan/phpstan) for static -analysis, you can enable the extension for proper recognition of the -magic instantiation methods. +If you are using [PHPStan](https://github.com/phpstan/phpstan) for static analysis, enable the extension for: +- proper recognition of the magic instantiation methods +- detection of duplicate enum values -Add the following to your projects `phpstan.neon` includes: +Use [PHPStan Extension Installer](https://github.com/phpstan/extension-installer) or add the following to your projects `phpstan.neon` includes: ```neon includes: From 93819a9642c4b63c7fbba1860d932c9649eb0dc3 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Wed, 15 Nov 2023 16:27:53 +0100 Subject: [PATCH 3/3] Improve output --- src/PHPStan/UniqueValuesRule.php | 13 +++++++++++-- tests/PHPStan/UniqueValuesRuleTest.php | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/PHPStan/UniqueValuesRule.php b/src/PHPStan/UniqueValuesRule.php index f63bfc0b..6bf8ab79 100644 --- a/src/PHPStan/UniqueValuesRule.php +++ b/src/PHPStan/UniqueValuesRule.php @@ -31,9 +31,18 @@ public function processNode(Node $node, Scope $scope): array $constants[$constant->name] = $constant->getValue(); } - if (count($constants) !== count(array_unique($constants))) { + $duplicateConstants = []; + foreach ($constants as $name => $value) { + $constantsWithValue = array_filter($constants, fn (mixed $v): bool => $v === $value); + if (count($constantsWithValue) > 1) { + $duplicateConstants []= array_keys($constantsWithValue); + } + } + $duplicateConstants = array_unique($duplicateConstants); + + if (count($duplicateConstants) > 0) { $fqcn = $reflection->getName(); - $constantsString = json_encode($constants); + $constantsString = json_encode($duplicateConstants); return [ RuleErrorBuilder::message("Enum class {$fqcn} contains constants with duplicate values: {$constantsString}.") diff --git a/tests/PHPStan/UniqueValuesRuleTest.php b/tests/PHPStan/UniqueValuesRuleTest.php index 3aeeca02..228e865b 100644 --- a/tests/PHPStan/UniqueValuesRuleTest.php +++ b/tests/PHPStan/UniqueValuesRuleTest.php @@ -22,7 +22,7 @@ public function testRule(): void ], [ [ - 'Enum class BenSampo\Enum\Tests\PHPStan\Fixtures\DuplicateValue contains constants with duplicate values: {"A":"A","B":"A"}.', + 'Enum class BenSampo\Enum\Tests\PHPStan\Fixtures\DuplicateValue contains constants with duplicate values: [["A","B"]].', 13, ], ],