forked from mglaman/phpstan-drupal
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNoRedundantTraitUseRule.php
More file actions
143 lines (121 loc) · 4.84 KB
/
Copy pathNoRedundantTraitUseRule.php
File metadata and controls
143 lines (121 loc) · 4.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<?php declare(strict_types=1);
namespace mglaman\PHPStanDrupal\Rules\Drupal;
use Exception;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\TraitUse;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
/**
* Disallows redundant trait usage when a trait is already included via another trait.
*
* This rule checks classes that use multiple traits and ensures that
* they don't use a trait that is already being used by another trait.
* For example, if trait A uses trait B, then a class shouldn't use both A and B.
*
* @implements Rule<Class_>
*/
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], true)) {
$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<string> $visited Array to track visited traits (for cycle detection).
*
* @return array<string> 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, true)) {
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 [];
}
}
}