Skip to content

Commit

Permalink
Add PHPStan rule to detect duplicate enum values (#343)
Browse files Browse the repository at this point in the history
  • Loading branch information
spawnia authored Nov 15, 2023
1 parent caa3ebd commit b4320ac
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 7 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion extension.neon
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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%%'
55 changes: 55 additions & 0 deletions src/PHPStan/UniqueValuesRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php declare(strict_types=1);

namespace BenSampo\Enum\PHPStan;

use BenSampo\Enum\Enum;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

/** @implements Rule<InClassNode> */
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();
}

$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($duplicateConstants);

return [
RuleErrorBuilder::message("Enum class {$fqcn} contains constants with duplicate values: {$constantsString}.")
->build(),
];
}

return [];
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php declare(strict_types=1);

namespace BenSampo\Enum\Tests;
namespace BenSampo\Enum\Tests\PHPStan;

use BenSampo\Enum\PHPStan\EnumMethodsClassReflectionExtension;
use BenSampo\Enum\Tests\Enums\AnnotatedConstants;
Expand All @@ -9,7 +9,7 @@
use PHPStan\Reflection\MethodReflection;
use PHPStan\Testing\PHPStanTestCase;

final class PHPStanTest extends PHPStanTestCase
final class EnumMethodsClassReflectionExtensionTest extends PHPStanTestCase
{
private EnumMethodsClassReflectionExtension $reflectionExtension;

Expand Down
17 changes: 17 additions & 0 deletions tests/PHPStan/Fixtures/DuplicateValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php declare(strict_types=1);

namespace BenSampo\Enum\Tests\PHPStan\Fixtures;

use BenSampo\Enum\Enum;

/**
* @extends Enum<string>
*
* @method static static A()
* @method static static B()
*/
final class DuplicateValue extends Enum
{
public const A = 'A';
public const B = 'A';
}
31 changes: 31 additions & 0 deletions tests/PHPStan/UniqueValuesRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types=1);

namespace BenSampo\Enum\Tests\PHPStan;

use BenSampo\Enum\PHPStan\UniqueValuesRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/** @extends RuleTestCase<UniqueValuesRule> */
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","B"]].',
13,
],
],
);
}
}

0 comments on commit b4320ac

Please sign in to comment.