Skip to content

Commit 08a2320

Browse files
AronNovakclaude
andauthored
Add NoRedundantTraitUseRule to detect nested traits (#906)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 969033b commit 08a2320

21 files changed

Lines changed: 187 additions & 70 deletions

.ddev/commands/web/phpstan

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,10 @@
44
## Usage: phpstan
55
## Example: "ddev phpstan"
66

7-
PHP_MEMORY_LIMIT=2G ./vendor/bin/phpstan --no-progress analyse -c phpstan.neon
7+
if [ $# -eq 0 ]; then
8+
# No arguments provided, run default analyse command
9+
PHP_MEMORY_LIMIT=2G ./vendor/bin/phpstan --no-progress analyse -c phpstan.neon
10+
else
11+
# Arguments provided, pass them through with configuration
12+
PHP_MEMORY_LIMIT=2G ./vendor/bin/phpstan "$@" -c phpstan.neon
13+
fi
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Drupal\PHPStan\Custom;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Stmt\Class_;
9+
use PhpParser\Node\Stmt\TraitUse;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Reflection\ReflectionProvider;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleErrorBuilder;
14+
use function array_filter;
15+
use function array_merge;
16+
use function array_unique;
17+
use function basename;
18+
use function count;
19+
use function in_array;
20+
use function sprintf;
21+
use function str_replace;
22+
23+
/**
24+
* Disallows redundant trait usage when a trait is already included via another.
25+
*
26+
* This rule checks classes that use multiple traits and ensures that they don't
27+
* use a trait that is already being used by another trait. For example, if
28+
* trait A uses trait B, then a class shouldn't use both A and B.
29+
*
30+
* @implements Rule<Class_>
31+
*/
32+
class NoRedundantTraitUseRule implements Rule {
33+
34+
private const ERROR_MESSAGE = 'Class uses trait "%s" redundantly as it is already included via trait "%s".';
35+
36+
/**
37+
* The reflection provider.
38+
*
39+
* @var \PHPStan\Reflection\ReflectionProvider
40+
*/
41+
private ReflectionProvider $reflectionProvider;
42+
43+
/**
44+
* Constructs a new NoRedundantTraitUseRule.
45+
*
46+
* @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider
47+
* The reflection provider.
48+
*/
49+
public function __construct(ReflectionProvider $reflectionProvider) {
50+
$this->reflectionProvider = $reflectionProvider;
51+
}
52+
53+
/**
54+
* {@inheritdoc}
55+
*/
56+
public function getNodeType(): string {
57+
return Class_::class;
58+
}
59+
60+
/**
61+
* {@inheritdoc}
62+
*/
63+
public function processNode(Node $node, Scope $scope): array {
64+
$errors = [];
65+
66+
// Get all trait use statements from the class.
67+
$traitUseNodes = array_filter($node->stmts, static fn ($stmt): bool => $stmt instanceof TraitUse);
68+
69+
if (count($traitUseNodes) < 2) {
70+
// Need at least 2 traits to have redundancy.
71+
return [];
72+
}
73+
74+
// Collect all directly used trait names with their resolved names.
75+
$directlyUsedTraits = [];
76+
foreach ($traitUseNodes as $traitUseNode) {
77+
foreach ($traitUseNode->traits as $trait) {
78+
$traitName = $scope->resolveName($trait);
79+
$directlyUsedTraits[] = $traitName;
80+
}
81+
}
82+
83+
// Build a map of trait -> [traits it uses] with full resolution.
84+
$traitDependencies = [];
85+
foreach ($directlyUsedTraits as $traitName) {
86+
try {
87+
if ($this->reflectionProvider->hasClass($traitName)) {
88+
$traitReflection = $this->reflectionProvider->getClass($traitName);
89+
if ($traitReflection->isTrait()) {
90+
$traitDependencies[$traitName] = $this->getAllTraitsUsedByTrait($traitName, []);
91+
}
92+
}
93+
}
94+
catch (\Throwable $e) {
95+
// Skip traits that can't be reflected.
96+
continue;
97+
}
98+
}
99+
100+
// Check for redundancies.
101+
foreach ($directlyUsedTraits as $traitA) {
102+
foreach ($directlyUsedTraits as $traitB) {
103+
if ($traitA === $traitB) {
104+
continue;
105+
}
106+
107+
// Check if traitA uses traitB (directly or transitively).
108+
if (isset($traitDependencies[$traitA]) && in_array($traitB, $traitDependencies[$traitA], TRUE)) {
109+
$shortNameA = basename(str_replace('\\', '/', $traitA));
110+
$shortNameB = basename(str_replace('\\', '/', $traitB));
111+
112+
$errors[] = RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $shortNameB, $shortNameA))
113+
->line($node->getStartLine())
114+
->identifier('traits.redundantTraitUse')
115+
->build();
116+
117+
// Only report each redundant trait once.
118+
break;
119+
}
120+
}
121+
}
122+
123+
return $errors;
124+
}
125+
126+
/**
127+
* Get all traits used by a given trait recursively.
128+
*
129+
* @param string $traitName
130+
* The fully qualified trait name.
131+
* @param array<string> $visited
132+
* Array to track visited traits (for cycle detection).
133+
*
134+
* @return array<string>
135+
* Array of all trait names used by the given trait (directly and
136+
* transitively).
137+
*/
138+
private function getAllTraitsUsedByTrait(string $traitName, array $visited = []): array {
139+
// Prevent infinite loops.
140+
if (in_array($traitName, $visited, TRUE)) {
141+
return [];
142+
}
143+
144+
$visited[] = $traitName;
145+
146+
try {
147+
if (!$this->reflectionProvider->hasClass($traitName)) {
148+
return [];
149+
}
150+
151+
$traitReflection = $this->reflectionProvider->getClass($traitName);
152+
if (!$traitReflection->isTrait()) {
153+
return [];
154+
}
155+
156+
$allTraits = [];
157+
158+
// Get direct traits used by this trait.
159+
foreach ($traitReflection->getTraits() as $trait) {
160+
$usedTraitName = $trait->getName();
161+
$allTraits[] = $usedTraitName;
162+
163+
// Recursively get traits used by the used trait.
164+
$nestedTraits = $this->getAllTraitsUsedByTrait($usedTraitName, $visited);
165+
$allTraits = array_merge($allTraits, $nestedTraits);
166+
}
167+
168+
return array_unique($allTraits);
169+
}
170+
catch (\Throwable $e) {
171+
return [];
172+
}
173+
}
174+
175+
}

phpstan-rules/phpstan-extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ services:
1616
class: Drupal\PHPStan\Custom\CacheableDependency
1717
tags:
1818
- phpstan.rules.rule
19+
-
20+
class: Drupal\PHPStan\Custom\NoRedundantTraitUseRule
21+
tags:
22+
- phpstan.rules.rule

robo-components/DeploymentTrait.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ trait DeploymentTrait {
5454
'ci-scripts',
5555
'pantheon.upstream.yml',
5656
'phpstan.neon',
57+
'phpstan-rules',
5758
'phpunit.xml.dist',
5859
'README.md',
5960
'RoboFile.php',

robo-components/TranslationManagement/ImportToUi.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace RoboComponents\TranslationManagement;
44

55
use Robo\ResultData;
6-
use RoboComponents\DeploymentTrait;
76

87
/**
98
* Logic to import translations into Drupal UI translations.
@@ -12,8 +11,6 @@
1211
*/
1312
trait ImportToUi {
1413

15-
use DeploymentTrait;
16-
1714
/**
1815
* Import the interface translations from a PO file.
1916
*

web/modules/custom/server_general/src/Plugin/EntityViewBuilder/MediaImage.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Drupal\media\MediaInterface;
88
use Drupal\pluggable_entity_view_builder\EntityViewBuilderPluginAbstract;
99
use Drupal\server_general\ThemeTrait\ElementMediaThemeTrait;
10-
use Drupal\server_general\ThemeTrait\ElementWrapThemeTrait;
1110

1211
/**
1312
* The "Media: Image" plugin.
@@ -21,7 +20,6 @@
2120
class MediaImage extends EntityViewBuilderPluginAbstract {
2221

2322
use ElementMediaThemeTrait;
24-
use ElementWrapThemeTrait;
2523

2624
/**
2725
* The iFrame URL helper service.

web/modules/custom/server_general/src/Plugin/EntityViewBuilder/MediaVideo.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Drupal\media\MediaInterface;
88
use Drupal\pluggable_entity_view_builder\EntityViewBuilderPluginAbstract;
99
use Drupal\server_general\ThemeTrait\ElementMediaThemeTrait;
10-
use Drupal\server_general\ThemeTrait\ElementWrapThemeTrait;
1110
use Symfony\Component\DependencyInjection\ContainerInterface;
1211

1312
/**
@@ -22,7 +21,6 @@
2221
class MediaVideo extends EntityViewBuilderPluginAbstract {
2322

2423
use ElementMediaThemeTrait;
25-
use ElementWrapThemeTrait;
2624

2725
// Update from design as needed.
2826
const VIDEO_FULL_MAX_WIDTH = 1920;

web/modules/custom/server_general/src/Plugin/EntityViewBuilder/NodeNews.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,12 @@
44

55
use Drupal\media\MediaInterface;
66
use Drupal\node\NodeInterface;
7-
use Drupal\server_general\EntityDateTrait;
87
use Drupal\server_general\EntityViewBuilder\NodeViewBuilderAbstract;
98
use Drupal\server_general\SocialShareTrait;
109
use Drupal\server_general\TagTrait;
11-
use Drupal\server_general\ThemeTrait\ElementLayoutThemeTrait;
1210
use Drupal\server_general\ThemeTrait\ElementNodeNewsThemeTrait;
13-
use Drupal\server_general\ThemeTrait\LineSeparatorThemeTrait;
14-
use Drupal\server_general\ThemeTrait\LinkThemeTrait;
1511
use Drupal\server_general\ThemeTrait\NewsTeasersThemeTrait;
1612
use Drupal\server_general\ThemeTrait\SearchThemeTrait;
17-
use Drupal\server_general\ThemeTrait\TitleAndLabelsThemeTrait;
1813
use Symfony\Component\DependencyInjection\ContainerInterface;
1914

2015
/**
@@ -28,16 +23,11 @@
2823
*/
2924
class NodeNews extends NodeViewBuilderAbstract {
3025

31-
use ElementLayoutThemeTrait;
3226
use ElementNodeNewsThemeTrait;
33-
use EntityDateTrait;
34-
use LineSeparatorThemeTrait;
35-
use LinkThemeTrait;
3627
use NewsTeasersThemeTrait;
3728
use SearchThemeTrait;
3829
use SocialShareTrait;
3930
use TagTrait;
40-
use TitleAndLabelsThemeTrait;
4131

4232
/**
4333
* The renderer.

web/modules/custom/server_general/src/Plugin/EntityViewBuilder/ParagraphAccordion.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use Drupal\server_general\ProcessedTextBuilderTrait;
1010
use Drupal\server_general\ThemeTrait\AccordionThemeTrait;
1111
use Drupal\server_general\ThemeTrait\ElementLayoutThemeTrait;
12-
use Drupal\server_general\ThemeTrait\ElementWrapThemeTrait;
1312

1413
/**
1514
* The "Accordion" paragraph plugin.
@@ -26,7 +25,6 @@ class ParagraphAccordion extends EntityViewBuilderPluginAbstract {
2625

2726
use AccordionThemeTrait;
2827
use ElementLayoutThemeTrait;
29-
use ElementWrapThemeTrait;
3028
use ProcessedTextBuilderTrait;
3129

3230
/**

web/modules/custom/server_general/src/Plugin/EntityViewBuilder/ParagraphCta.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
use Drupal\paragraphs\ParagraphInterface;
77
use Drupal\pluggable_entity_view_builder\EntityViewBuilderPluginAbstract;
88
use Drupal\server_general\ProcessedTextBuilderTrait;
9-
use Drupal\server_general\ThemeTrait\ButtonThemeTrait;
109
use Drupal\server_general\ThemeTrait\CtaThemeTrait;
11-
use Drupal\server_general\ThemeTrait\ElementWrapThemeTrait;
1210

1311
/**
1412
* The "Call to Action" paragraph plugin.
@@ -21,9 +19,7 @@
2119
*/
2220
class ParagraphCta extends EntityViewBuilderPluginAbstract {
2321

24-
use ButtonThemeTrait;
2522
use CtaThemeTrait;
26-
use ElementWrapThemeTrait;
2723
use ProcessedTextBuilderTrait;
2824

2925
/**

0 commit comments

Comments
 (0)