diff --git a/src/UsedSymbolExtractor.php b/src/UsedSymbolExtractor.php index 1bffa9d..10537d1 100644 --- a/src/UsedSymbolExtractor.php +++ b/src/UsedSymbolExtractor.php @@ -31,6 +31,8 @@ use const T_USE; use const T_WHITESPACE; +// phpcs:disable Squiz.PHP.CommentedOutCode.Found + class UsedSymbolExtractor { @@ -68,8 +70,10 @@ public function parseUsedSymbols(): array $useStatements = []; $useStatementKinds = []; - $level = 0; + $level = 0; // {, }, {$, ${ + $squareLevel = 0; // [, ], #[ $inClassLevel = null; + $inAttributeSquareLevel = null; $numTokens = $this->numTokens; $tokens = $this->tokens; @@ -96,13 +100,17 @@ public function parseUsedSymbols(): array break; + case PHP_VERSION_ID > 80000 ? T_ATTRIBUTE : -1: + $inAttributeSquareLevel = ++$squareLevel; + break; + case PHP_VERSION_ID >= 80000 ? T_NAMESPACE : -1: $useStatements = []; // reset use statements on namespace change break; case PHP_VERSION_ID >= 80000 ? T_NAME_FULLY_QUALIFIED : -1: $symbolName = $this->normalizeBackslash($token[1]); - $kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer); + $kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null); $usedSymbols[$kind][$symbolName][] = $token[2]; break; @@ -111,7 +119,7 @@ public function parseUsedSymbols(): array if (isset($useStatements[$neededAlias])) { $symbolName = $useStatements[$neededAlias] . substr($token[1], strlen($neededAlias)); - $kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer); + $kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null); $usedSymbols[$kind][$symbolName][] = $token[2]; } @@ -143,7 +151,7 @@ public function parseUsedSymbols(): array $symbolName = $this->normalizeBackslash($this->parseNameForOldPhp()); if ($symbolName !== '') { // e.g. \array (NS separator followed by not-a-name) - $kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1); + $kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false); $usedSymbols[$kind][$symbolName][] = $token[2]; } @@ -163,7 +171,7 @@ public function parseUsedSymbols(): array if (isset($useStatements[$neededAlias])) { // qualified name $symbolName = $useStatements[$neededAlias] . substr($name, strlen($neededAlias)); - $kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1); + $kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false); $usedSymbols[$kind][$symbolName][] = $token[2]; } } @@ -183,6 +191,14 @@ public function parseUsedSymbols(): array } $level--; + } elseif ($token === '[') { + $squareLevel++; + } elseif ($token === ']') { + if ($squareLevel === $inAttributeSquareLevel) { + $inAttributeSquareLevel = null; + } + + $squareLevel--; } } @@ -306,8 +322,16 @@ private function normalizeBackslash(string $class): string /** * @return SymbolKind::CLASSLIKE|SymbolKind::FUNCTION */ - private function getFqnSymbolKind(int $pointerBeforeName, int $pointerAfterName): int + private function getFqnSymbolKind( + int $pointerBeforeName, + int $pointerAfterName, + bool $inAttribute + ): int { + if ($inAttribute) { + return SymbolKind::CLASSLIKE; + } + do { $tokenBeforeName = $this->tokens[$pointerBeforeName]; @@ -338,13 +362,9 @@ private function getFqnSymbolKind(int $pointerBeforeName, int $pointerAfterName) break; } while ($pointerAfterName < $this->numTokens); - // phpcs:disable Squiz.PHP.CommentedOutCode.Found if ( $tokenAfterName === '(' - && !( - $tokenBeforeName[0] === T_NEW // eliminate new \ClassName( - || (PHP_VERSION_ID > 80000 && $tokenBeforeName[0] === T_ATTRIBUTE) // eliminate #[\AttributeName( - ) + && $tokenBeforeName[0] !== T_NEW // eliminate new \ClassName( ) { return SymbolKind::FUNCTION; } diff --git a/tests/UsedSymbolExtractorTest.php b/tests/UsedSymbolExtractorTest.php index 5447e3e..dfe4833 100644 --- a/tests/UsedSymbolExtractorTest.php +++ b/tests/UsedSymbolExtractorTest.php @@ -114,16 +114,18 @@ public function provideVariants(): iterable [] ]; - yield 'attribute' => [ - __DIR__ . '/data/not-autoloaded/used-symbols/attribute.php', - PHP_VERSION_ID >= 80000 - ? [ + if (PHP_VERSION_ID >= 80000) { + yield 'attribute' => [ + __DIR__ . '/data/not-autoloaded/used-symbols/attribute.php', + [ SymbolKind::CLASSLIKE => [ 'SomeAttribute' => [3], + 'Assert\NotNull' => [7], + 'Assert\NotBlank' => [8], ], - ] - : [] - ]; + ], + ]; + } } } diff --git a/tests/data/not-autoloaded/used-symbols/attribute.php b/tests/data/not-autoloaded/used-symbols/attribute.php index c8ed873..15b10ef 100644 --- a/tests/data/not-autoloaded/used-symbols/attribute.php +++ b/tests/data/not-autoloaded/used-symbols/attribute.php @@ -2,3 +2,9 @@ #[\SomeAttribute()] class ClassWithAttribute {} + +#[ + \Assert\NotNull(foo: []), + \Assert\NotBlank(), +] +class ClassWithMultipleAttributes {}