From df17f8ef108183a86224d22f7534dde1dc973368 Mon Sep 17 00:00:00 2001 From: Aron Novak Date: Wed, 27 Aug 2025 08:26:47 +0200 Subject: [PATCH 1/3] Add NoRedundantTraitUseRule to detect redundant trait usage This rule detects when a class uses traits redundantly - specifically when a class uses both a trait A and trait B, where trait A already uses trait B (either directly or transitively). For example: ```php trait BarTrait { public function bar() {} } trait FooTrait { use BarTrait; public function foo() {} } class MyClass { use FooTrait; use BarTrait; // Redundant! Already included via FooTrait } ``` The rule helps improve code maintainability by: - Reducing unnecessary trait usage - Making trait dependencies clearer - Potentially improving performance by reducing trait resolution overhead Includes comprehensive test coverage for: - Direct redundant trait usage - Transitive redundant trait usage - Valid non-redundant trait usage - Single trait usage (no redundancy possible) --- rules.neon | 1 + src/Rules/Drupal/NoRedundantTraitUseRule.php | 142 ++++++++++++++++++ .../src/Rules/NoRedundantTraitUseRuleTest.php | 58 +++++++ .../no-redundant-trait-use-single-trait.php | 14 ++ .../no-redundant-trait-use-transitive.php | 24 +++ .../data/no-redundant-trait-use-valid.php | 22 +++ .../src/Rules/data/no-redundant-trait-use.php | 24 +++ 7 files changed, 285 insertions(+) create mode 100644 src/Rules/Drupal/NoRedundantTraitUseRule.php create mode 100644 tests/src/Rules/NoRedundantTraitUseRuleTest.php create mode 100644 tests/src/Rules/data/no-redundant-trait-use-single-trait.php create mode 100644 tests/src/Rules/data/no-redundant-trait-use-transitive.php create mode 100644 tests/src/Rules/data/no-redundant-trait-use-valid.php create mode 100644 tests/src/Rules/data/no-redundant-trait-use.php diff --git a/rules.neon b/rules.neon index 34f4b3b6..85fa027f 100644 --- a/rules.neon +++ b/rules.neon @@ -12,6 +12,7 @@ rules: - mglaman\PHPStanDrupal\Rules\Drupal\LoadIncludes - mglaman\PHPStanDrupal\Rules\Drupal\EntityQuery\EntityQueryHasAccessCheckRule - mglaman\PHPStanDrupal\Rules\Drupal\TestClassesProtectedPropertyModulesRule + - mglaman\PHPStanDrupal\Rules\Drupal\NoRedundantTraitUseRule conditionalTags: mglaman\PHPStanDrupal\Rules\Drupal\Tests\TestClassSuffixNameRule: diff --git a/src/Rules/Drupal/NoRedundantTraitUseRule.php b/src/Rules/Drupal/NoRedundantTraitUseRule.php new file mode 100644 index 00000000..40269ad8 --- /dev/null +++ b/src/Rules/Drupal/NoRedundantTraitUseRule.php @@ -0,0 +1,142 @@ + + */ +class NoRedundantTraitUseRule implements Rule +{ + private const ERROR_MESSAGE = 'Class uses trait "%s" redundantly as it is already included via trait "%s".'; + + public function __construct( + private ReflectionProvider $reflectionProvider, + ) { + } + + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $errors = []; + + // Get all trait use statements from the class. + $traitUseNodes = array_filter($node->stmts, fn($stmt) => $stmt instanceof TraitUse); + + if (count($traitUseNodes) < 2) { + // Need at least 2 traits to have redundancy. + return []; + } + + // Collect all directly used trait names with their resolved names. + $directlyUsedTraits = []; + foreach ($traitUseNodes as $traitUseNode) { + foreach ($traitUseNode->traits as $trait) { + $traitName = $scope->resolveName($trait); + $directlyUsedTraits[] = $traitName; + } + } + + // Build a map of trait -> [traits it uses] with full resolution. + $traitDependencies = []; + foreach ($directlyUsedTraits as $traitName) { + try { + if ($this->reflectionProvider->hasClass($traitName)) { + $traitReflection = $this->reflectionProvider->getClass($traitName); + if ($traitReflection->isTrait()) { + $traitDependencies[$traitName] = $this->getAllTraitsUsedByTrait($traitName, []); + } + } + } catch (\Exception $e) { + // Skip traits that can't be reflected. + continue; + } + } + + // Check for redundancies. + foreach ($directlyUsedTraits as $traitA) { + foreach ($directlyUsedTraits as $traitB) { + if ($traitA === $traitB) { + continue; + } + + // Check if traitA uses traitB (directly or transitively). + if (isset($traitDependencies[$traitA]) && in_array($traitB, $traitDependencies[$traitA])) { + $shortNameA = basename(str_replace('\\', '/', $traitA)); + $shortNameB = basename(str_replace('\\', '/', $traitB)); + + $errors[] = RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $shortNameB, $shortNameA)) + ->line($node->getStartLine()) + ->identifier('traits.redundantTraitUse') + ->build(); + + // Only report each redundant trait once + break; + } + } + } + + return $errors; + } + + /** + * Get all traits used by a given trait recursively. + * + * @param string $traitName The fully qualified trait name. + * @param array $visited Array to track visited traits (for cycle detection). + * + * @return array Array of all trait names used by the given trait (directly and transitively). + */ + private function getAllTraitsUsedByTrait(string $traitName, array $visited = []): array + { + // Prevent infinite loops. + if (in_array($traitName, $visited)) { + return []; + } + $visited[] = $traitName; + + try { + if (!$this->reflectionProvider->hasClass($traitName)) { + return []; + } + + $traitReflection = $this->reflectionProvider->getClass($traitName); + if (!$traitReflection->isTrait()) { + return []; + } + + $allTraits = []; + + // Get direct traits used by this trait. + foreach ($traitReflection->getTraits() as $trait) { + $usedTraitName = $trait->getName(); + $allTraits[] = $usedTraitName; + + // Recursively get traits used by the used trait. + $nestedTraits = $this->getAllTraitsUsedByTrait($usedTraitName, $visited); + $allTraits = array_merge($allTraits, $nestedTraits); + } + + return array_unique($allTraits); + } catch (\Exception $e) { + return []; + } + } +} \ No newline at end of file diff --git a/tests/src/Rules/NoRedundantTraitUseRuleTest.php b/tests/src/Rules/NoRedundantTraitUseRuleTest.php new file mode 100644 index 00000000..493b529a --- /dev/null +++ b/tests/src/Rules/NoRedundantTraitUseRuleTest.php @@ -0,0 +1,58 @@ +createReflectionProvider()); + } + + /** + * @dataProvider resultData + * + * @param list $errorMessages + */ + public function testRule(string $path, array $errorMessages): void + { + $this->analyse([$path], $errorMessages); + } + + public static function resultData(): \Generator + { + yield [ + __DIR__ . '/data/no-redundant-trait-use.php', + [ + [ + 'Class uses trait "BarTrait" redundantly as it is already included via trait "FooTrait".', + 20 + ], + ] + ]; + + yield [ + __DIR__ . '/data/no-redundant-trait-use-valid.php', + [] + ]; + + yield [ + __DIR__ . '/data/no-redundant-trait-use-single-trait.php', + [] + ]; + + yield [ + __DIR__ . '/data/no-redundant-trait-use-transitive.php', + [ + [ + 'Class uses trait "BaseTrait" redundantly as it is already included via trait "WrapperTrait".', + 20 + ], + ] + ]; + } +} \ No newline at end of file diff --git a/tests/src/Rules/data/no-redundant-trait-use-single-trait.php b/tests/src/Rules/data/no-redundant-trait-use-single-trait.php new file mode 100644 index 00000000..63a125d3 --- /dev/null +++ b/tests/src/Rules/data/no-redundant-trait-use-single-trait.php @@ -0,0 +1,14 @@ + Date: Wed, 27 Aug 2025 08:32:46 +0200 Subject: [PATCH 2/3] Fix PHPCS violations in NoRedundantTraitUseRule - Add Exception import statement instead of using fully qualified name - Add missing newline at end of file --- src/Rules/Drupal/NoRedundantTraitUseRule.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Rules/Drupal/NoRedundantTraitUseRule.php b/src/Rules/Drupal/NoRedundantTraitUseRule.php index 40269ad8..a5866601 100644 --- a/src/Rules/Drupal/NoRedundantTraitUseRule.php +++ b/src/Rules/Drupal/NoRedundantTraitUseRule.php @@ -2,6 +2,7 @@ namespace mglaman\PHPStanDrupal\Rules\Drupal; +use Exception; use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\TraitUse; @@ -64,7 +65,7 @@ public function processNode(Node $node, Scope $scope): array $traitDependencies[$traitName] = $this->getAllTraitsUsedByTrait($traitName, []); } } - } catch (\Exception $e) { + } catch (Exception $e) { // Skip traits that can't be reflected. continue; } @@ -135,8 +136,8 @@ private function getAllTraitsUsedByTrait(string $traitName, array $visited = []) } return array_unique($allTraits); - } catch (\Exception $e) { + } catch (Exception $e) { return []; } } -} \ No newline at end of file +} From 9de666b43213140cbf7a318aba895b4b4bf2f61f Mon Sep 17 00:00:00 2001 From: Aron Novak Date: Wed, 27 Aug 2025 08:34:49 +0200 Subject: [PATCH 3/3] Fix PHPStan strict comparison violations Add strict comparison parameter (true) to in_array() calls as required by PHPStan strict rules configuration. --- src/Rules/Drupal/NoRedundantTraitUseRule.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rules/Drupal/NoRedundantTraitUseRule.php b/src/Rules/Drupal/NoRedundantTraitUseRule.php index a5866601..13bba410 100644 --- a/src/Rules/Drupal/NoRedundantTraitUseRule.php +++ b/src/Rules/Drupal/NoRedundantTraitUseRule.php @@ -79,7 +79,7 @@ public function processNode(Node $node, Scope $scope): array } // Check if traitA uses traitB (directly or transitively). - if (isset($traitDependencies[$traitA]) && in_array($traitB, $traitDependencies[$traitA])) { + if (isset($traitDependencies[$traitA]) && in_array($traitB, $traitDependencies[$traitA], true)) { $shortNameA = basename(str_replace('\\', '/', $traitA)); $shortNameB = basename(str_replace('\\', '/', $traitB)); @@ -108,7 +108,7 @@ public function processNode(Node $node, Scope $scope): array private function getAllTraitsUsedByTrait(string $traitName, array $visited = []): array { // Prevent infinite loops. - if (in_array($traitName, $visited)) { + if (in_array($traitName, $visited, true)) { return []; } $visited[] = $traitName;