Skip to content

Commit 0501e73

Browse files
authored
Fix false positive in multi-attribute usages (#124)
1 parent 2f65e4f commit 0501e73

File tree

3 files changed

+46
-18
lines changed

3 files changed

+46
-18
lines changed

src/UsedSymbolExtractor.php

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
use const T_USE;
3232
use const T_WHITESPACE;
3333

34+
// phpcs:disable Squiz.PHP.CommentedOutCode.Found
35+
3436
class UsedSymbolExtractor
3537
{
3638

@@ -68,8 +70,10 @@ public function parseUsedSymbols(): array
6870
$useStatements = [];
6971
$useStatementKinds = [];
7072

71-
$level = 0;
73+
$level = 0; // {, }, {$, ${
74+
$squareLevel = 0; // [, ], #[
7275
$inClassLevel = null;
76+
$inAttributeSquareLevel = null;
7377

7478
$numTokens = $this->numTokens;
7579
$tokens = $this->tokens;
@@ -96,13 +100,17 @@ public function parseUsedSymbols(): array
96100

97101
break;
98102

103+
case PHP_VERSION_ID > 80000 ? T_ATTRIBUTE : -1:
104+
$inAttributeSquareLevel = ++$squareLevel;
105+
break;
106+
99107
case PHP_VERSION_ID >= 80000 ? T_NAMESPACE : -1:
100108
$useStatements = []; // reset use statements on namespace change
101109
break;
102110

103111
case PHP_VERSION_ID >= 80000 ? T_NAME_FULLY_QUALIFIED : -1:
104112
$symbolName = $this->normalizeBackslash($token[1]);
105-
$kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer);
113+
$kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null);
106114
$usedSymbols[$kind][$symbolName][] = $token[2];
107115
break;
108116

@@ -111,7 +119,7 @@ public function parseUsedSymbols(): array
111119

112120
if (isset($useStatements[$neededAlias])) {
113121
$symbolName = $useStatements[$neededAlias] . substr($token[1], strlen($neededAlias));
114-
$kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer);
122+
$kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null);
115123
$usedSymbols[$kind][$symbolName][] = $token[2];
116124
}
117125

@@ -143,7 +151,7 @@ public function parseUsedSymbols(): array
143151
$symbolName = $this->normalizeBackslash($this->parseNameForOldPhp());
144152

145153
if ($symbolName !== '') { // e.g. \array (NS separator followed by not-a-name)
146-
$kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1);
154+
$kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false);
147155
$usedSymbols[$kind][$symbolName][] = $token[2];
148156
}
149157

@@ -163,7 +171,7 @@ public function parseUsedSymbols(): array
163171

164172
if (isset($useStatements[$neededAlias])) { // qualified name
165173
$symbolName = $useStatements[$neededAlias] . substr($name, strlen($neededAlias));
166-
$kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1);
174+
$kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false);
167175
$usedSymbols[$kind][$symbolName][] = $token[2];
168176
}
169177
}
@@ -183,6 +191,14 @@ public function parseUsedSymbols(): array
183191
}
184192

185193
$level--;
194+
} elseif ($token === '[') {
195+
$squareLevel++;
196+
} elseif ($token === ']') {
197+
if ($squareLevel === $inAttributeSquareLevel) {
198+
$inAttributeSquareLevel = null;
199+
}
200+
201+
$squareLevel--;
186202
}
187203
}
188204

@@ -306,8 +322,16 @@ private function normalizeBackslash(string $class): string
306322
/**
307323
* @return SymbolKind::CLASSLIKE|SymbolKind::FUNCTION
308324
*/
309-
private function getFqnSymbolKind(int $pointerBeforeName, int $pointerAfterName): int
325+
private function getFqnSymbolKind(
326+
int $pointerBeforeName,
327+
int $pointerAfterName,
328+
bool $inAttribute
329+
): int
310330
{
331+
if ($inAttribute) {
332+
return SymbolKind::CLASSLIKE;
333+
}
334+
311335
do {
312336
$tokenBeforeName = $this->tokens[$pointerBeforeName];
313337

@@ -338,13 +362,9 @@ private function getFqnSymbolKind(int $pointerBeforeName, int $pointerAfterName)
338362
break;
339363
} while ($pointerAfterName < $this->numTokens);
340364

341-
// phpcs:disable Squiz.PHP.CommentedOutCode.Found
342365
if (
343366
$tokenAfterName === '('
344-
&& !(
345-
$tokenBeforeName[0] === T_NEW // eliminate new \ClassName(
346-
|| (PHP_VERSION_ID > 80000 && $tokenBeforeName[0] === T_ATTRIBUTE) // eliminate #[\AttributeName(
347-
)
367+
&& $tokenBeforeName[0] !== T_NEW // eliminate new \ClassName(
348368
) {
349369
return SymbolKind::FUNCTION;
350370
}

tests/UsedSymbolExtractorTest.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,18 @@ public function provideVariants(): iterable
114114
[]
115115
];
116116

117-
yield 'attribute' => [
118-
__DIR__ . '/data/not-autoloaded/used-symbols/attribute.php',
119-
PHP_VERSION_ID >= 80000
120-
? [
117+
if (PHP_VERSION_ID >= 80000) {
118+
yield 'attribute' => [
119+
__DIR__ . '/data/not-autoloaded/used-symbols/attribute.php',
120+
[
121121
SymbolKind::CLASSLIKE => [
122122
'SomeAttribute' => [3],
123+
'Assert\NotNull' => [7],
124+
'Assert\NotBlank' => [8],
123125
],
124-
]
125-
: []
126-
];
126+
],
127+
];
128+
}
127129
}
128130

129131
}

tests/data/not-autoloaded/used-symbols/attribute.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,9 @@
22

33
#[\SomeAttribute()]
44
class ClassWithAttribute {}
5+
6+
#[
7+
\Assert\NotNull(foo: []),
8+
\Assert\NotBlank(),
9+
]
10+
class ClassWithMultipleAttributes {}

0 commit comments

Comments
 (0)