From 6e07cc692be880bd29bae785f21c9dc95d1005bb Mon Sep 17 00:00:00 2001 From: Rustam Date: Fri, 20 Oct 2023 16:29:13 +0500 Subject: [PATCH 01/15] Tokenizer classifier & refactoring filters --- src/AbstractClassifier.php | 73 ++++---- src/ClassifierInterface.php | 4 + src/Filter/ClassAttributes.php | 33 ++++ src/Filter/ClassImplements.php | 27 +++ src/Filter/FilterInterface.php | 19 ++ src/Filter/SubclassOf.php | 19 ++ src/Filter/TargetAttribute.php | 29 +++ src/NativeClassifier.php | 66 ++----- src/ReflectionFile.php | 287 ++++++++++++++++++++++++++++++ src/TokenizerClassifier.php | 27 +++ tests/BaseClassifierTest.php | 26 ++- tests/ReflectionFileTest.php | 20 +++ tests/TokenizerClassifierTest.php | 16 ++ 13 files changed, 548 insertions(+), 98 deletions(-) create mode 100644 src/Filter/ClassAttributes.php create mode 100644 src/Filter/ClassImplements.php create mode 100644 src/Filter/FilterInterface.php create mode 100644 src/Filter/SubclassOf.php create mode 100644 src/Filter/TargetAttribute.php create mode 100644 src/ReflectionFile.php create mode 100644 src/TokenizerClassifier.php create mode 100644 tests/ReflectionFileTest.php create mode 100644 tests/TokenizerClassifierTest.php diff --git a/src/AbstractClassifier.php b/src/AbstractClassifier.php index 5ad78d8..59ba42f 100644 --- a/src/AbstractClassifier.php +++ b/src/AbstractClassifier.php @@ -4,7 +4,9 @@ namespace Yiisoft\Classifier; +use ReflectionClass; use Symfony\Component\Finder\Finder; +use Yiisoft\Classifier\Filter\FilterInterface; /** * Base implementation for {@see ClassifierInterface} with common filters. @@ -12,17 +14,14 @@ abstract class AbstractClassifier implements ClassifierInterface { /** - * @var string[] - */ - protected array $interfaces = []; - /** - * @var string[] + * @psalm-var array */ - protected array $attributes = []; + protected static array $reflectionsCache = []; + /** - * @psalm-var class-string + * @var FilterInterface[] */ - protected ?string $parentClass = null; + private array $filters = []; /** * @var string[] */ @@ -33,34 +32,10 @@ public function __construct(string $directory, string ...$directories) $this->directories = [$directory, ...array_values($directories)]; } - /** - * @psalm-param class-string ...$interfaces - */ - public function withInterface(string ...$interfaces): self + public function withFilter(FilterInterface ...$filter): static { $new = clone $this; - array_push($new->interfaces, ...array_values($interfaces)); - - return $new; - } - - /** - * @psalm-param class-string $parentClass - */ - public function withParentClass(string $parentClass): self - { - $new = clone $this; - $new->parentClass = $parentClass; - return $new; - } - - /** - * @psalm-param class-string ...$attributes - */ - public function withAttribute(string ...$attributes): self - { - $new = clone $this; - array_push($new->attributes, ...array_values($attributes)); + array_push($new->filters, ...array_values($filter)); return $new; } @@ -70,11 +45,16 @@ public function withAttribute(string ...$attributes): self */ public function find(): iterable { - if (empty($this->interfaces) && empty($this->attributes) && $this->parentClass === null) { + if (empty($this->filters)) { return []; } - yield from $this->getAvailableClasses(); + foreach ($this->getAvailableDeclarations() as $declaration) { + if ($this->skipDeclaration($declaration)) { + continue; + } + yield $declaration; + } } protected function getFiles(): Finder @@ -86,8 +66,25 @@ protected function getFiles(): Finder ->files(); } + private function skipDeclaration(string $declaration): bool + { + $reflectionClass = self::$reflectionsCache[$declaration] ??= new ReflectionClass($declaration); + + if ($reflectionClass->isInternal() || $reflectionClass->isAnonymous()) { + return true; + } + + foreach ($this->filters as $filter) { + if (!$filter->match($reflectionClass)) { + return true; + } + } + + return false; + } + /** - * @return iterable + * @return iterable */ - abstract protected function getAvailableClasses(): iterable; + abstract protected function getAvailableDeclarations(): iterable; } diff --git a/src/ClassifierInterface.php b/src/ClassifierInterface.php index 4e03824..db7b652 100644 --- a/src/ClassifierInterface.php +++ b/src/ClassifierInterface.php @@ -4,11 +4,15 @@ namespace Yiisoft\Classifier; +use Yiisoft\Classifier\Filter\FilterInterface; + /** * `Classifier` is a class finder that represents the classes found. */ interface ClassifierInterface { + public function withFilter(FilterInterface ...$filter): static; + /** * Returns all the class names found. * diff --git a/src/Filter/ClassAttributes.php b/src/Filter/ClassAttributes.php new file mode 100644 index 0000000..53b8e1c --- /dev/null +++ b/src/Filter/ClassAttributes.php @@ -0,0 +1,33 @@ +attributes = $attributes; + } + + public function match(ReflectionClass $reflectionClass): bool + { + if (empty($this->attributes)) { + return false; + } + + $attributes = $reflectionClass->getAttributes(); + $attributeNames = array_map( + static fn(ReflectionAttribute $attribute) => $attribute->getName(), + $attributes + ); + + return count(array_intersect($this->attributes, $attributeNames)) === count($this->attributes); + } +} diff --git a/src/Filter/ClassImplements.php b/src/Filter/ClassImplements.php new file mode 100644 index 0000000..39c34e0 --- /dev/null +++ b/src/Filter/ClassImplements.php @@ -0,0 +1,27 @@ +interfaces = $interfaces; + } + + public function match(ReflectionClass $reflectionClass): bool + { + if (empty($this->interfaces) || $reflectionClass->isInterface()) { + return false; + } + $interfaces = $reflectionClass->getInterfaceNames(); + + return count(array_intersect($this->interfaces, $interfaces)) === count($this->interfaces); + } +} diff --git a/src/Filter/FilterInterface.php b/src/Filter/FilterInterface.php new file mode 100644 index 0000000..964615e --- /dev/null +++ b/src/Filter/FilterInterface.php @@ -0,0 +1,19 @@ +isSubclassOf($this->class); + } +} diff --git a/src/Filter/TargetAttribute.php b/src/Filter/TargetAttribute.php new file mode 100644 index 0000000..dc85477 --- /dev/null +++ b/src/Filter/TargetAttribute.php @@ -0,0 +1,29 @@ +target = $target; + } + + public function match(ReflectionClass $reflectionClass): bool + { + $attributes = $reflectionClass->getAttributes($this->target, ReflectionAttribute::IS_INSTANCEOF); + $attributeNames = array_map( + static fn(ReflectionAttribute $attribute) => $attribute->getName(), + $attributes + ); + + return !empty($attributeNames); + } +} diff --git a/src/NativeClassifier.php b/src/NativeClassifier.php index 22ed6bf..3b252f5 100644 --- a/src/NativeClassifier.php +++ b/src/NativeClassifier.php @@ -4,23 +4,16 @@ namespace Yiisoft\Classifier; -use ReflectionAttribute; -use ReflectionClass; - /** * `NativeClassifier` is a classifier that finds classes using PHP's native function {@see get_declared_classes()}. */ final class NativeClassifier extends AbstractClassifier { - /** - * @psalm-var array - */ - private static array $reflectionsCache = []; /** * @psalm-suppress UnresolvableInclude */ - protected function getAvailableClasses(): iterable + protected function getAvailableDeclarations(): iterable { $files = $this->getFiles(); @@ -32,25 +25,12 @@ protected function getAvailableClasses(): iterable } } - foreach (get_declared_classes() as $className) { - if ($this->skipClass($className)) { - continue; - } - - yield $className; - } - } - - /** - * @psalm-param class-string $className - */ - private function skipClass(string $className): bool - { - $reflectionClass = self::$reflectionsCache[$className] ??= new ReflectionClass($className); + $declarations = array_merge( + get_declared_classes(), + get_declared_interfaces(), + get_declared_traits() + ); - if ($reflectionClass->isInternal() || $reflectionClass->isAnonymous()) { - return true; - } $directories = $this->directories; $isWindows = DIRECTORY_SEPARATOR === '\\'; @@ -63,36 +43,18 @@ private function skipClass(string $className): bool // @codeCoverageIgnoreEnd } - $matchedDirs = array_filter( - $directories, - static fn($directory) => str_starts_with($reflectionClass->getFileName(), $directory) - ); - - if (count($matchedDirs) === 0) { - return true; - } - - if (!empty($this->interfaces)) { - $interfaces = $reflectionClass->getInterfaces(); - $interfaces = array_map(static fn(ReflectionClass $class) => $class->getName(), $interfaces); + foreach ($declarations as $declaration) { + $reflectionClass = self::$reflectionsCache[$declaration] ??= new \ReflectionClass($declaration); - if (count(array_intersect($this->interfaces, $interfaces)) !== count($this->interfaces)) { - return true; - } - } - - if (!empty($this->attributes)) { - $attributes = $reflectionClass->getAttributes(); - $attributes = array_map( - static fn(ReflectionAttribute $attribute) => $attribute->getName(), - $attributes + $matchedDirs = array_filter( + $directories, + static fn($directory) => $reflectionClass->getFileName() && str_starts_with($reflectionClass->getFileName(), $directory) ); - if (count(array_intersect($this->attributes, $attributes)) !== count($this->attributes)) { - return true; + if (count($matchedDirs) === 0) { + continue; } + yield $reflectionClass->getName(); } - - return ($this->parentClass !== null) && !is_subclass_of($reflectionClass->getName(), $this->parentClass); } } diff --git a/src/ReflectionFile.php b/src/ReflectionFile.php new file mode 100644 index 0000000..185e226 --- /dev/null +++ b/src/ReflectionFile.php @@ -0,0 +1,287 @@ +tokens = \PhpToken::tokenize(file_get_contents($this->filename)); + $this->countTokens = \count($this->tokens); + + //Looking for declarations + $this->locateDeclarations(); + } + + /** + * Filename. + */ + public function getFilename(): string + { + return $this->filename; + } + + + /** + * List of declarations names + */ + public function getDeclarations(): array + { + return \array_keys($this->declarations); + } + + /** + * Get list of tokens associated with given file. + * + * @return \PhpToken[] + */ + public function getTokens(): array + { + return $this->tokens; + } + + /** + * Locate every class, interface, trait or enum definition. + */ + private function locateDeclarations(): void + { + foreach ($this->getTokens() as $tokenID => $token) { + if ($token->isIgnorable() || !\in_array($token->id, self::TOKENS, true)) { + continue; + } + + switch ($token->id) { + case T_NAMESPACE: + $this->registerNamespace($tokenID); + break; + + case T_CLASS: + case T_TRAIT: + case T_INTERFACE: + case T_ENUM: + if ($this->isClassNameConst($tokenID)) { + // PHP5.5 ClassName::class constant + continue 2; + } + + if ($this->isAnonymousClass($tokenID)) { + // PHP7.0 Anonymous classes new class ('foo', 'bar') + continue 2; + } + + if (!$this->isCorrectDeclaration($tokenID)) { + // PHP8.0 Named parameters ->foo(class: 'bar') + continue 2; + } + + $this->registerDeclaration($tokenID, $token->getTokenName() ?? $token->text); + break; + } + } + + //Dropping empty namespace + if (isset($this->namespaces[''])) { + $this->namespaces['\\'] = $this->namespaces['']; + unset($this->namespaces['']); + } + } + + /** + * Handle namespace declaration. + */ + private function registerNamespace(int $tokenID): void + { + $namespace = ''; + $localID = $tokenID + 1; + + do { + $token = $this->tokens[$localID++]; + if ($token->text === '{') { + break; + } + + $namespace .= $token->text; + } while ( + isset($this->tokens[$localID]) + && $this->tokens[$localID]->text !== '{' + && $this->tokens[$localID]->text !== ';' + ); + + //Whitespaces + $namespace = \trim($namespace); + + $uses = $this->namespaces[$namespace] ?? []; + + if ($this->tokens[$localID]->text === ';') { + $endingID = \count($this->tokens) - 1; + } else { + $endingID = $this->endingToken($tokenID); + } + + $this->namespaces[$namespace] = [ + self::O_TOKEN => $tokenID, + self::C_TOKEN => $endingID, + ]; + } + + /** + * Handle declaration of class, trait of interface. Declaration will be stored under it's token + * type in declarations array. + */ + private function registerDeclaration(int $tokenID): void + { + $localID = $tokenID + 1; + while ($this->tokens[$localID]->id !== T_STRING) { + ++$localID; + } + + $name = $this->tokens[$localID]->text; + if (!empty($namespace = $this->activeNamespace($tokenID))) { + $name = $namespace . self::NS_SEPARATOR . $name; + } + + $this->declarations[$name] = [ + self::O_TOKEN => $tokenID, + self::C_TOKEN => $this->endingToken($tokenID), + ]; + } + + /** + * Check if token ID represents `ClassName::class` constant statement. + */ + private function isClassNameConst(int $tokenID): bool + { + return $this->tokens[$tokenID]->id === T_CLASS + && isset($this->tokens[$tokenID - 1]) + && $this->tokens[$tokenID - 1]->id === T_PAAMAYIM_NEKUDOTAYIM; + } + + /** + * Check if token ID represents anonymous class creation, e.g. `new class ('foo', 'bar')`. + */ + private function isAnonymousClass(int|string $tokenID): bool + { + return $this->tokens[$tokenID]->id === T_CLASS + && isset($this->tokens[$tokenID - 2]) + && $this->tokens[$tokenID - 2]->id === T_NEW; + } + + /** + * Check if token ID represents named parameter with name `class`, e.g. `foo(class: SomeClass::name)`. + */ + private function isCorrectDeclaration(int|string $tokenID): bool + { + return \in_array($this->tokens[$tokenID]->id, [T_CLASS, T_TRAIT, T_INTERFACE, T_ENUM], true) + && isset($this->tokens[$tokenID + 2]) + && $this->tokens[$tokenID + 1]->id === T_WHITESPACE + && $this->tokens[$tokenID + 2]->id === T_STRING; + } + + /** + * Get namespace name active at specified token position. + */ + private function activeNamespace(int $tokenID): string + { + foreach ($this->namespaces as $namespace => $position) { + if ($tokenID >= $position[self::O_TOKEN] && $tokenID <= $position[self::C_TOKEN]) { + return $namespace; + } + } + + //Seems like no namespace declaration + $this->namespaces[''] = [ + self::O_TOKEN => 0, + self::C_TOKEN => \count($this->tokens), + ]; + + return ''; + } + + /** + * Find token ID of ending brace. + */ + private function endingToken(int $tokenID): int + { + $level = null; + for ($localID = $tokenID; $localID < $this->countTokens; ++$localID) { + $token = $this->tokens[$localID]; + if ($token->text === '{') { + ++$level; + continue; + } + + if ($token->text === '}') { + --$level; + } + + if ($level === 0) { + break; + } + } + + return $localID; + } +} diff --git a/src/TokenizerClassifier.php b/src/TokenizerClassifier.php new file mode 100644 index 0000000..99fd0de --- /dev/null +++ b/src/TokenizerClassifier.php @@ -0,0 +1,27 @@ +getFiles(); + $declarations = []; + + foreach ($files as $file) { + $reflectionFile = new ReflectionFile($file->getPathname()); + array_push($declarations, ...$reflectionFile->getDeclarations()); + } + + return $declarations; + } +} diff --git a/tests/BaseClassifierTest.php b/tests/BaseClassifierTest.php index bf9fd5e..4591897 100644 --- a/tests/BaseClassifierTest.php +++ b/tests/BaseClassifierTest.php @@ -6,6 +6,9 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Classifier\ClassifierInterface; +use Yiisoft\Classifier\Filter\ClassAttributes; +use Yiisoft\Classifier\Filter\ClassImplements; +use Yiisoft\Classifier\Filter\SubclassOf; use Yiisoft\Classifier\Tests\Support\Attributes\AuthorAttribute; use Yiisoft\Classifier\Tests\Support\Author; use Yiisoft\Classifier\Tests\Support\AuthorPost; @@ -26,7 +29,7 @@ public function testMultipleUse(): void { $dirs = [__DIR__ . '/Support/Dir1', __DIR__ . '/Support/Dir2']; $finder = $this->createClassifier(...$dirs); - $finder = $finder->withInterface(UserInterface::class); + $finder = $finder->withFilter(new ClassImplements(UserInterface::class)); $result = $finder->find(); @@ -37,7 +40,7 @@ public function testMultipleDirectories(): void { $dirs = [__DIR__ . '/Support/Dir1', __DIR__ . '/Support/Dir2']; $finder = $this->createClassifier(...$dirs); - $finder = $finder->withInterface(UserInterface::class); + $finder = $finder->withFilter(new ClassImplements(UserInterface::class)); $result = $finder->find(); @@ -50,7 +53,7 @@ public function testMultipleDirectories(): void public function testInterfaces(string $directory, array $interfaces, array $expectedClasses): void { $finder = $this->createClassifier($directory); - $finder = $finder->withInterface(...$interfaces); + $finder = $finder->withFilter(new ClassImplements(...$interfaces)); $result = $finder->find(); @@ -78,7 +81,15 @@ public static function interfacesDataProvider(): array [ __DIR__, [UserInterface::class], - [UserInDir1::class, UserInDir2::class, PostUser::class, SuperSuperUser::class, SuperUser::class, User::class, UserSubclass::class], + [ + UserInDir1::class, + UserInDir2::class, + PostUser::class, + SuperSuperUser::class, + SuperUser::class, + User::class, + UserSubclass::class, + ], ], [ __DIR__, @@ -104,7 +115,7 @@ public static function interfacesDataProvider(): array public function testAttributes(array $attributes, array $expectedClasses): void { $finder = $this->createClassifier(__DIR__); - $finder = $finder->withAttribute(...$attributes); + $finder = $finder->withFilter(new ClassAttributes(...$attributes)); $result = $finder->find(); @@ -117,7 +128,7 @@ public function testAttributes(array $attributes, array $expectedClasses): void public function testParentClass(string $parent, array $expectedClasses): void { $finder = $this->createClassifier(__DIR__); - $finder = $finder->withParentClass($parent); + $finder = $finder->withFilter(new SubclassOf($parent)); $result = $finder->find(); @@ -145,8 +156,7 @@ public function testMixed(array $attributes, array $interfaces, array $expectedC { $finder = $this->createClassifier(__DIR__); $finder = $finder - ->withAttribute(...$attributes) - ->withInterface(...$interfaces); + ->withFilter(new ClassAttributes(...$attributes), new ClassImplements(...$interfaces)); $result = $finder->find(); diff --git a/tests/ReflectionFileTest.php b/tests/ReflectionFileTest.php new file mode 100644 index 0000000..9c58206 --- /dev/null +++ b/tests/ReflectionFileTest.php @@ -0,0 +1,20 @@ +assertNotEmpty($reflectionFile->getDeclarations()); + } +} diff --git a/tests/TokenizerClassifierTest.php b/tests/TokenizerClassifierTest.php new file mode 100644 index 0000000..caa48ba --- /dev/null +++ b/tests/TokenizerClassifierTest.php @@ -0,0 +1,16 @@ + Date: Fri, 20 Oct 2023 11:29:33 +0000 Subject: [PATCH 02/15] Apply fixes from StyleCI --- src/NativeClassifier.php | 1 - src/ReflectionFile.php | 2 -- tests/ReflectionFileTest.php | 2 -- 3 files changed, 5 deletions(-) diff --git a/src/NativeClassifier.php b/src/NativeClassifier.php index 3b252f5..2b416b4 100644 --- a/src/NativeClassifier.php +++ b/src/NativeClassifier.php @@ -9,7 +9,6 @@ */ final class NativeClassifier extends AbstractClassifier { - /** * @psalm-suppress UnresolvableInclude */ diff --git a/src/ReflectionFile.php b/src/ReflectionFile.php index 185e226..a32fbdf 100644 --- a/src/ReflectionFile.php +++ b/src/ReflectionFile.php @@ -4,7 +4,6 @@ namespace Yiisoft\Classifier; - /** * This file was copied from {@link https://github.com/spiral/tokenizer}. * @@ -84,7 +83,6 @@ public function getFilename(): string return $this->filename; } - /** * List of declarations names */ diff --git a/tests/ReflectionFileTest.php b/tests/ReflectionFileTest.php index 9c58206..68166e0 100644 --- a/tests/ReflectionFileTest.php +++ b/tests/ReflectionFileTest.php @@ -5,8 +5,6 @@ namespace Yiisoft\Classifier\Tests; use PHPUnit\Framework\TestCase; -use Yiisoft\Classifier\ClassifierInterface; -use Yiisoft\Classifier\NativeClassifier; use Yiisoft\Classifier\ReflectionFile; class ReflectionFileTest extends TestCase From d7411e6e28ca5715a25de28381ac263c59edac60 Mon Sep 17 00:00:00 2001 From: rustamwin Date: Fri, 20 Oct 2023 11:31:20 +0000 Subject: [PATCH 03/15] Apply Rector changes (CI) --- src/Filter/TargetAttribute.php | 5 +---- src/NativeClassifier.php | 6 +----- src/ReflectionFile.php | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Filter/TargetAttribute.php b/src/Filter/TargetAttribute.php index dc85477..30916ca 100644 --- a/src/Filter/TargetAttribute.php +++ b/src/Filter/TargetAttribute.php @@ -9,11 +9,8 @@ final class TargetAttribute implements FilterInterface { - private string $target; - - public function __construct(string $target) + public function __construct(private string $target) { - $this->target = $target; } public function match(ReflectionClass $reflectionClass): bool diff --git a/src/NativeClassifier.php b/src/NativeClassifier.php index 2b416b4..d9a6ce0 100644 --- a/src/NativeClassifier.php +++ b/src/NativeClassifier.php @@ -24,11 +24,7 @@ protected function getAvailableDeclarations(): iterable } } - $declarations = array_merge( - get_declared_classes(), - get_declared_interfaces(), - get_declared_traits() - ); + $declarations = [...get_declared_classes(), ...get_declared_interfaces(), ...get_declared_traits()]; $directories = $this->directories; $isWindows = DIRECTORY_SEPARATOR === '\\'; diff --git a/src/ReflectionFile.php b/src/ReflectionFile.php index a32fbdf..b4d6d79 100644 --- a/src/ReflectionFile.php +++ b/src/ReflectionFile.php @@ -135,7 +135,7 @@ private function locateDeclarations(): void continue 2; } - $this->registerDeclaration($tokenID, $token->getTokenName() ?? $token->text); + $this->registerDeclaration($tokenID); break; } } From 43187b81d073d65e72c1c6c5bb96625350bba617 Mon Sep 17 00:00:00 2001 From: Rustam Date: Fri, 20 Oct 2023 16:31:53 +0500 Subject: [PATCH 04/15] Fix composer.json --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 9485fda..d3a5060 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "prefer-stable": true, "require": { "php": "^8.0", + "ext-tokenizer": "*", "symfony/finder": "^5.4|^6.0" }, "require-dev": { From 340a878a3a7456ba5f9417d766c8d548b4f80426 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sat, 21 Oct 2023 09:33:56 +0500 Subject: [PATCH 05/15] Improvements & tests --- src/AbstractClassifier.php | 9 +++++---- src/ReflectionFile.php | 18 +++++++++--------- tests/ReflectionFileTest.php | 12 +++++++++++- tests/Support/namespace.php | 14 ++++++++++++++ 4 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 tests/Support/namespace.php diff --git a/src/AbstractClassifier.php b/src/AbstractClassifier.php index 59ba42f..f6f6725 100644 --- a/src/AbstractClassifier.php +++ b/src/AbstractClassifier.php @@ -45,10 +45,6 @@ public function withFilter(FilterInterface ...$filter): static */ public function find(): iterable { - if (empty($this->filters)) { - return []; - } - foreach ($this->getAvailableDeclarations() as $declaration) { if ($this->skipDeclaration($declaration)) { continue; @@ -68,6 +64,11 @@ protected function getFiles(): Finder private function skipDeclaration(string $declaration): bool { + if ( + !(interface_exists($declaration) || class_exists($declaration) || trait_exists($declaration)) + ) { + return true; + } $reflectionClass = self::$reflectionsCache[$declaration] ??= new ReflectionClass($declaration); if ($reflectionClass->isInternal() || $reflectionClass->isAnonymous()) { diff --git a/src/ReflectionFile.php b/src/ReflectionFile.php index b4d6d79..b847c8e 100644 --- a/src/ReflectionFile.php +++ b/src/ReflectionFile.php @@ -31,14 +31,14 @@ final class ReflectionFile '{', '}', ';', - T_PAAMAYIM_NEKUDOTAYIM, - T_NAMESPACE, - T_STRING, - T_CLASS, - T_INTERFACE, - T_TRAIT, - T_ENUM, - T_NS_SEPARATOR, + 'T_PAAMAYIM_NEKUDOTAYIM', + 'T_NAMESPACE', + 'T_STRING', + 'T_CLASS', + 'T_INTERFACE', + 'T_TRAIT', + 'T_ENUM', + 'T_NS_SEPARATOR', ]; /** @@ -107,7 +107,7 @@ public function getTokens(): array private function locateDeclarations(): void { foreach ($this->getTokens() as $tokenID => $token) { - if ($token->isIgnorable() || !\in_array($token->id, self::TOKENS, true)) { + if ($token->isIgnorable() || !\in_array($token->getTokenName(), self::TOKENS, true)) { continue; } diff --git a/tests/ReflectionFileTest.php b/tests/ReflectionFileTest.php index 68166e0..0a054e5 100644 --- a/tests/ReflectionFileTest.php +++ b/tests/ReflectionFileTest.php @@ -6,13 +6,23 @@ use PHPUnit\Framework\TestCase; use Yiisoft\Classifier\ReflectionFile; +use Yiisoft\Classifier\Tests\Support\User; class ReflectionFileTest extends TestCase { - public function testClasses() + public function testPsr4File(): void { $reflectionFile = new ReflectionFile(__DIR__ . '/Support/User.php'); $this->assertNotEmpty($reflectionFile->getDeclarations()); + $this->assertContains(User::class, $reflectionFile->getDeclarations()); + } + + public function testNamespaceDeclaration(): void + { + $reflectionFile = new ReflectionFile(__DIR__ . '/Support/namespace.php'); + + $this->assertCount(2, $reflectionFile->getDeclarations()); + $this->assertContains('Support\\Entity\\Person', $reflectionFile->getDeclarations()); } } diff --git a/tests/Support/namespace.php b/tests/Support/namespace.php new file mode 100644 index 0000000..59789e9 --- /dev/null +++ b/tests/Support/namespace.php @@ -0,0 +1,14 @@ + Date: Sat, 21 Oct 2023 04:34:13 +0000 Subject: [PATCH 06/15] Apply fixes from StyleCI --- tests/Support/namespace.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/Support/namespace.php b/tests/Support/namespace.php index 59789e9..5ecfef1 100644 --- a/tests/Support/namespace.php +++ b/tests/Support/namespace.php @@ -2,13 +2,12 @@ declare(strict_types=1); -namespace Support\Entity { +namespace Support\Entity; - class Person - { - } +class Person +{ +} - class Client - { - } +class Client +{ } From 01a59862290b1beb69bb3ddfec3cb6fd22f4c0b2 Mon Sep 17 00:00:00 2001 From: rustamwin Date: Sat, 21 Oct 2023 04:34:41 +0000 Subject: [PATCH 07/15] Apply Rector changes (CI) --- tests/ReflectionFileTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ReflectionFileTest.php b/tests/ReflectionFileTest.php index 0a054e5..6908f2a 100644 --- a/tests/ReflectionFileTest.php +++ b/tests/ReflectionFileTest.php @@ -23,6 +23,6 @@ public function testNamespaceDeclaration(): void $reflectionFile = new ReflectionFile(__DIR__ . '/Support/namespace.php'); $this->assertCount(2, $reflectionFile->getDeclarations()); - $this->assertContains('Support\\Entity\\Person', $reflectionFile->getDeclarations()); + $this->assertContains(\Support\Entity\Person::class, $reflectionFile->getDeclarations()); } } From c89047869a460308b6900e22e6b61fd3bb3b0195 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sat, 21 Oct 2023 09:48:53 +0500 Subject: [PATCH 08/15] Raise PHP version --- .github/workflows/build.yml | 2 +- .github/workflows/composer-require-checker.yml | 2 +- .github/workflows/mutation.yml | 2 +- .github/workflows/rector.yml | 2 +- .github/workflows/static.yml | 2 +- composer.json | 2 +- src/ReflectionFile.php | 18 +++++++++--------- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8506ea1..f12ea5e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,4 +28,4 @@ jobs: os: >- ['ubuntu-latest', 'windows-latest'] php: >- - ['8.0', '8.1'] + ['8.1', '8.2'] diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index 6cb4099..b115ab1 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -30,4 +30,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.0'] + ['8.1'] diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index c1aca98..03b72c0 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -26,6 +26,6 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1'] + ['8.2'] secrets: STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index adacd73..c597e60 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -18,4 +18,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.0'] + ['8.2'] diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 96b2679..301ab7c 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -28,4 +28,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.0', '8.1'] + ['8.1', '8.2'] diff --git a/composer.json b/composer.json index d3a5060..3f3fff5 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "minimum-stability": "dev", "prefer-stable": true, "require": { - "php": "^8.0", + "php": "^8.1", "ext-tokenizer": "*", "symfony/finder": "^5.4|^6.0" }, diff --git a/src/ReflectionFile.php b/src/ReflectionFile.php index b847c8e..b4d6d79 100644 --- a/src/ReflectionFile.php +++ b/src/ReflectionFile.php @@ -31,14 +31,14 @@ final class ReflectionFile '{', '}', ';', - 'T_PAAMAYIM_NEKUDOTAYIM', - 'T_NAMESPACE', - 'T_STRING', - 'T_CLASS', - 'T_INTERFACE', - 'T_TRAIT', - 'T_ENUM', - 'T_NS_SEPARATOR', + T_PAAMAYIM_NEKUDOTAYIM, + T_NAMESPACE, + T_STRING, + T_CLASS, + T_INTERFACE, + T_TRAIT, + T_ENUM, + T_NS_SEPARATOR, ]; /** @@ -107,7 +107,7 @@ public function getTokens(): array private function locateDeclarations(): void { foreach ($this->getTokens() as $tokenID => $token) { - if ($token->isIgnorable() || !\in_array($token->getTokenName(), self::TOKENS, true)) { + if ($token->isIgnorable() || !\in_array($token->id, self::TOKENS, true)) { continue; } From 236b609332580043b9a1d2b3ff90004961fecec1 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sat, 21 Oct 2023 19:33:52 +0500 Subject: [PATCH 09/15] Improvements & add tests --- src/AbstractClassifier.php | 7 ++-- src/Filter/TargetAttribute.php | 4 +-- src/ReflectionFile.php | 22 +----------- tests/BaseClassifierTest.php | 35 ++++++++++++++++++- tests/Declarations/Car.php | 9 +++++ tests/Declarations/ClassWithAnonymous.php | 15 ++++++++ .../ClassWithoutNamespace.php} | 6 ---- tests/Declarations/StatusEnum.php | 11 ++++++ tests/Declarations/namespace.php | 14 ++++++++ tests/ReflectionFileTest.php | 35 ++++++++++++++++++- tests/Support/Attributes/UserAttribute.php | 12 +++++++ tests/Support/Author.php | 2 ++ tests/Support/AuthorPost.php | 2 ++ 13 files changed, 139 insertions(+), 35 deletions(-) create mode 100644 tests/Declarations/Car.php create mode 100644 tests/Declarations/ClassWithAnonymous.php rename tests/{Support/namespace.php => Declarations/ClassWithoutNamespace.php} (52%) create mode 100644 tests/Declarations/StatusEnum.php create mode 100644 tests/Declarations/namespace.php create mode 100644 tests/Support/Attributes/UserAttribute.php diff --git a/src/AbstractClassifier.php b/src/AbstractClassifier.php index f6f6725..8d6fb32 100644 --- a/src/AbstractClassifier.php +++ b/src/AbstractClassifier.php @@ -64,12 +64,11 @@ protected function getFiles(): Finder private function skipDeclaration(string $declaration): bool { - if ( - !(interface_exists($declaration) || class_exists($declaration) || trait_exists($declaration)) - ) { + try { + $reflectionClass = self::$reflectionsCache[$declaration] ??= new ReflectionClass($declaration); + } catch (\Throwable) { return true; } - $reflectionClass = self::$reflectionsCache[$declaration] ??= new ReflectionClass($declaration); if ($reflectionClass->isInternal() || $reflectionClass->isAnonymous()) { return true; diff --git a/src/Filter/TargetAttribute.php b/src/Filter/TargetAttribute.php index 30916ca..44edbf2 100644 --- a/src/Filter/TargetAttribute.php +++ b/src/Filter/TargetAttribute.php @@ -9,13 +9,13 @@ final class TargetAttribute implements FilterInterface { - public function __construct(private string $target) + public function __construct(private string $attribute) { } public function match(ReflectionClass $reflectionClass): bool { - $attributes = $reflectionClass->getAttributes($this->target, ReflectionAttribute::IS_INSTANCEOF); + $attributes = $reflectionClass->getAttributes($this->attribute, ReflectionAttribute::IS_INSTANCEOF); $attributeNames = array_map( static fn(ReflectionAttribute $attribute) => $attribute->getName(), $attributes diff --git a/src/ReflectionFile.php b/src/ReflectionFile.php index b4d6d79..3b68036 100644 --- a/src/ReflectionFile.php +++ b/src/ReflectionFile.php @@ -75,14 +75,6 @@ public function __construct( $this->locateDeclarations(); } - /** - * Filename. - */ - public function getFilename(): string - { - return $this->filename; - } - /** * List of declarations names */ @@ -91,22 +83,12 @@ public function getDeclarations(): array return \array_keys($this->declarations); } - /** - * Get list of tokens associated with given file. - * - * @return \PhpToken[] - */ - public function getTokens(): array - { - return $this->tokens; - } - /** * Locate every class, interface, trait or enum definition. */ private function locateDeclarations(): void { - foreach ($this->getTokens() as $tokenID => $token) { + foreach ($this->tokens as $tokenID => $token) { if ($token->isIgnorable() || !\in_array($token->id, self::TOKENS, true)) { continue; } @@ -171,8 +153,6 @@ private function registerNamespace(int $tokenID): void //Whitespaces $namespace = \trim($namespace); - $uses = $this->namespaces[$namespace] ?? []; - if ($this->tokens[$localID]->text === ';') { $endingID = \count($this->tokens) - 1; } else { diff --git a/tests/BaseClassifierTest.php b/tests/BaseClassifierTest.php index 4591897..95d3d57 100644 --- a/tests/BaseClassifierTest.php +++ b/tests/BaseClassifierTest.php @@ -9,7 +9,9 @@ use Yiisoft\Classifier\Filter\ClassAttributes; use Yiisoft\Classifier\Filter\ClassImplements; use Yiisoft\Classifier\Filter\SubclassOf; +use Yiisoft\Classifier\Filter\TargetAttribute; use Yiisoft\Classifier\Tests\Support\Attributes\AuthorAttribute; +use Yiisoft\Classifier\Tests\Support\Attributes\UserAttribute; use Yiisoft\Classifier\Tests\Support\Author; use Yiisoft\Classifier\Tests\Support\AuthorPost; use Yiisoft\Classifier\Tests\Support\Dir1\UserInDir1; @@ -122,6 +124,19 @@ public function testAttributes(array $attributes, array $expectedClasses): void $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); } + /** + * @dataProvider targetAttributeDataProvider + */ + public function testTargetAttribute(string $attribute, array $expectedClasses): void + { + $finder = $this->createClassifier(__DIR__); + $finder = $finder->withFilter(new TargetAttribute($attribute)); + + $result = $finder->find(); + + $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); + } + /** * @dataProvider parentClassDataProvider */ @@ -143,7 +158,25 @@ public static function attributesDataProvider(): array [], ], [ - [AuthorAttribute::class], + [AuthorAttribute::class, UserAttribute::class], + [Author::class, AuthorPost::class], + ], + ]; + } + + public static function targetAttributeDataProvider(): array + { + return [ + [ + UserSubclass::class, + [], + ], + [ + UserAttribute::class, + [Author::class, AuthorPost::class], + ], + [ + AuthorAttribute::class, [Author::class, AuthorPost::class], ], ]; diff --git a/tests/Declarations/Car.php b/tests/Declarations/Car.php new file mode 100644 index 0000000..bd40ed0 --- /dev/null +++ b/tests/Declarations/Car.php @@ -0,0 +1,9 @@ +assertCount(2, $reflectionFile->getDeclarations()); $this->assertContains(\Support\Entity\Person::class, $reflectionFile->getDeclarations()); } + + + public function testEnumDeclaration(): void + { + $reflectionFile = new ReflectionFile(__DIR__ . '/Declarations/StatusEnum.php'); + + $this->assertCount(1, $reflectionFile->getDeclarations()); + $this->assertEquals(StatusEnum::class, $reflectionFile->getDeclarations()[0]); + } + + public function testWithoutNamespace(): void + { + $reflectionFile = new ReflectionFile(__DIR__ . '/Declarations/ClassWithoutNamespace.php'); + + $this->assertCount(1, $reflectionFile->getDeclarations()); + $this->assertEquals(\Person::class, $reflectionFile->getDeclarations()[0]); + } + + public function testContainingClassKeyword(): void + { + $reflectionFile = new ReflectionFile(__DIR__ . '/Declarations/Car.php'); + + $this->assertCount(1, $reflectionFile->getDeclarations()); + $this->assertEquals(\Car::class, $reflectionFile->getDeclarations()[0]); + } + + public function testBrokenClass(): void + { + $reflectionFile = new ReflectionFile(__DIR__ . '/Declarations/ClassWithAnonymous.php'); + + $this->assertCount(1, $reflectionFile->getDeclarations()); + } } diff --git a/tests/Support/Attributes/UserAttribute.php b/tests/Support/Attributes/UserAttribute.php new file mode 100644 index 0000000..8f5bc09 --- /dev/null +++ b/tests/Support/Attributes/UserAttribute.php @@ -0,0 +1,12 @@ + Date: Sat, 21 Oct 2023 14:34:21 +0000 Subject: [PATCH 10/15] Apply fixes from StyleCI --- tests/Declarations/ClassWithAnonymous.php | 3 +-- tests/Declarations/ClassWithoutNamespace.php | 2 +- tests/Declarations/namespace.php | 13 ++++++------- tests/ReflectionFileTest.php | 1 - 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/Declarations/ClassWithAnonymous.php b/tests/Declarations/ClassWithAnonymous.php index 3295eb4..c165364 100644 --- a/tests/Declarations/ClassWithAnonymous.php +++ b/tests/Declarations/ClassWithAnonymous.php @@ -8,8 +8,7 @@ class ClassWithAnonymous { public function bar() { - $class = new class { - + $class = new class () { }; } } diff --git a/tests/Declarations/ClassWithoutNamespace.php b/tests/Declarations/ClassWithoutNamespace.php index 052be08..c02d3cf 100644 --- a/tests/Declarations/ClassWithoutNamespace.php +++ b/tests/Declarations/ClassWithoutNamespace.php @@ -2,6 +2,6 @@ declare(strict_types=1); -class Person +class ClassWithoutNamespace { } diff --git a/tests/Declarations/namespace.php b/tests/Declarations/namespace.php index 1a087e5..5ecfef1 100644 --- a/tests/Declarations/namespace.php +++ b/tests/Declarations/namespace.php @@ -2,13 +2,12 @@ declare(strict_types=1); -namespace Support\Entity{ +namespace Support\Entity; - class Person - { - } +class Person +{ +} - class Client - { - } +class Client +{ } diff --git a/tests/ReflectionFileTest.php b/tests/ReflectionFileTest.php index 55b39bc..dc8ff5c 100644 --- a/tests/ReflectionFileTest.php +++ b/tests/ReflectionFileTest.php @@ -27,7 +27,6 @@ public function testNamespaceDeclaration(): void $this->assertContains(\Support\Entity\Person::class, $reflectionFile->getDeclarations()); } - public function testEnumDeclaration(): void { $reflectionFile = new ReflectionFile(__DIR__ . '/Declarations/StatusEnum.php'); From d39a2f460e0df5fd0675c726f95d627ca7ba5c17 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sat, 21 Oct 2023 20:25:22 +0500 Subject: [PATCH 11/15] Fix --- tests/ReflectionFileTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ReflectionFileTest.php b/tests/ReflectionFileTest.php index dc8ff5c..6d467ad 100644 --- a/tests/ReflectionFileTest.php +++ b/tests/ReflectionFileTest.php @@ -40,7 +40,7 @@ public function testWithoutNamespace(): void $reflectionFile = new ReflectionFile(__DIR__ . '/Declarations/ClassWithoutNamespace.php'); $this->assertCount(1, $reflectionFile->getDeclarations()); - $this->assertEquals(\Person::class, $reflectionFile->getDeclarations()[0]); + $this->assertEquals('ClassWithoutNamespace', $reflectionFile->getDeclarations()[0]); } public function testContainingClassKeyword(): void From 925eb83a62ba81ad2b2155b642ce17c889509a17 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sun, 22 Oct 2023 08:07:22 +0500 Subject: [PATCH 12/15] Fix psalm issues --- src/AbstractClassifier.php | 7 ++- src/ClassifierInterface.php | 3 +- src/Filter/SubclassOf.php | 3 + src/Filter/TargetAttribute.php | 3 + src/ReflectionFile.php | 107 ++++++++++++++++++--------------- src/TokenizerClassifier.php | 2 + 6 files changed, 72 insertions(+), 53 deletions(-) diff --git a/src/AbstractClassifier.php b/src/AbstractClassifier.php index 8d6fb32..ce96d1a 100644 --- a/src/AbstractClassifier.php +++ b/src/AbstractClassifier.php @@ -14,7 +14,7 @@ abstract class AbstractClassifier implements ClassifierInterface { /** - * @psalm-var array + * @var array */ protected static array $reflectionsCache = []; @@ -41,7 +41,7 @@ public function withFilter(FilterInterface ...$filter): static } /** - * @psalm-return iterable + * @return iterable */ public function find(): iterable { @@ -62,6 +62,9 @@ protected function getFiles(): Finder ->files(); } + /** + * @param class-string|trait-string $declaration + */ private function skipDeclaration(string $declaration): bool { try { diff --git a/src/ClassifierInterface.php b/src/ClassifierInterface.php index db7b652..e4c487e 100644 --- a/src/ClassifierInterface.php +++ b/src/ClassifierInterface.php @@ -16,8 +16,7 @@ public function withFilter(FilterInterface ...$filter): static; /** * Returns all the class names found. * - * @return iterable List of class names. - * @psalm-return iterable + * @return iterable List of class names. */ public function find(): iterable; } diff --git a/src/Filter/SubclassOf.php b/src/Filter/SubclassOf.php index 5dd78cb..7ebbf81 100644 --- a/src/Filter/SubclassOf.php +++ b/src/Filter/SubclassOf.php @@ -8,6 +8,9 @@ final class SubclassOf implements FilterInterface { + /** + * @param class-string $class + */ public function __construct(private string $class) { } diff --git a/src/Filter/TargetAttribute.php b/src/Filter/TargetAttribute.php index 44edbf2..ed6c9d0 100644 --- a/src/Filter/TargetAttribute.php +++ b/src/Filter/TargetAttribute.php @@ -9,6 +9,9 @@ final class TargetAttribute implements FilterInterface { + /** + * @param class-string $attribute + */ public function __construct(private string $attribute) { } diff --git a/src/ReflectionFile.php b/src/ReflectionFile.php index 3b68036..1886523 100644 --- a/src/ReflectionFile.php +++ b/src/ReflectionFile.php @@ -8,6 +8,8 @@ * This file was copied from {@link https://github.com/spiral/tokenizer}. * * @internal + * + * @psalm-type TPosition = list{int, int} */ final class ReflectionFile { @@ -43,6 +45,8 @@ final class ReflectionFile /** * Parsed tokens array. + * + * @var array */ private array $tokens; @@ -54,14 +58,14 @@ final class ReflectionFile /** * Namespaces used in file and their token positions. * - * @internal + * @psalm-var array */ private array $namespaces = []; /** * Declarations of classes, interfaces and traits. * - * @internal + * @psalm-var array */ private array $declarations = []; @@ -77,6 +81,8 @@ public function __construct( /** * List of declarations names + * + * @return array */ public function getDeclarations(): array { @@ -88,36 +94,36 @@ public function getDeclarations(): array */ private function locateDeclarations(): void { - foreach ($this->tokens as $tokenID => $token) { - if ($token->isIgnorable() || !\in_array($token->id, self::TOKENS, true)) { + foreach ($this->tokens as $tokenIndex => $token) { + if (!\in_array($token->id, self::TOKENS)) { continue; } switch ($token->id) { case T_NAMESPACE: - $this->registerNamespace($tokenID); + $this->registerNamespace($tokenIndex); break; case T_CLASS: case T_TRAIT: case T_INTERFACE: case T_ENUM: - if ($this->isClassNameConst($tokenID)) { + if ($this->isClassNameConst($tokenIndex)) { // PHP5.5 ClassName::class constant continue 2; } - if ($this->isAnonymousClass($tokenID)) { + if ($this->isAnonymousClass($tokenIndex)) { // PHP7.0 Anonymous classes new class ('foo', 'bar') continue 2; } - if (!$this->isCorrectDeclaration($tokenID)) { + if (!$this->isCorrectDeclaration($tokenIndex)) { // PHP8.0 Named parameters ->foo(class: 'bar') continue 2; } - $this->registerDeclaration($tokenID); + $this->registerDeclaration($tokenIndex); break; } } @@ -132,36 +138,36 @@ private function locateDeclarations(): void /** * Handle namespace declaration. */ - private function registerNamespace(int $tokenID): void + private function registerNamespace(int $tokenIndex): void { $namespace = ''; - $localID = $tokenID + 1; + $localIndex = $tokenIndex + 1; do { - $token = $this->tokens[$localID++]; + $token = $this->tokens[$localIndex++]; if ($token->text === '{') { break; } $namespace .= $token->text; } while ( - isset($this->tokens[$localID]) - && $this->tokens[$localID]->text !== '{' - && $this->tokens[$localID]->text !== ';' + isset($this->tokens[$localIndex]) + && $this->tokens[$localIndex]->text !== '{' + && $this->tokens[$localIndex]->text !== ';' ); //Whitespaces $namespace = \trim($namespace); - if ($this->tokens[$localID]->text === ';') { - $endingID = \count($this->tokens) - 1; + if ($this->tokens[$localIndex]->text === ';') { + $endingIndex = \count($this->tokens) - 1; } else { - $endingID = $this->endingToken($tokenID); + $endingIndex = $this->endingToken($tokenIndex); } $this->namespaces[$namespace] = [ - self::O_TOKEN => $tokenID, - self::C_TOKEN => $endingID, + self::O_TOKEN => $tokenIndex, + self::C_TOKEN => $endingIndex, ]; } @@ -169,62 +175,65 @@ private function registerNamespace(int $tokenID): void * Handle declaration of class, trait of interface. Declaration will be stored under it's token * type in declarations array. */ - private function registerDeclaration(int $tokenID): void + private function registerDeclaration(int $tokenIndex): void { - $localID = $tokenID + 1; - while ($this->tokens[$localID]->id !== T_STRING) { - ++$localID; + $localIndex = $tokenIndex + 1; + while ($this->tokens[$localIndex]->id !== T_STRING) { + ++$localIndex; } - $name = $this->tokens[$localID]->text; - if (!empty($namespace = $this->activeNamespace($tokenID))) { + $name = $this->tokens[$localIndex]->text; + if (!empty($namespace = $this->activeNamespace($tokenIndex))) { $name = $namespace . self::NS_SEPARATOR . $name; } + /** @var class-string|trait-string $name */ $this->declarations[$name] = [ - self::O_TOKEN => $tokenID, - self::C_TOKEN => $this->endingToken($tokenID), + self::O_TOKEN => $tokenIndex, + self::C_TOKEN => $this->endingToken($tokenIndex), ]; } /** * Check if token ID represents `ClassName::class` constant statement. */ - private function isClassNameConst(int $tokenID): bool + private function isClassNameConst(int $tokenIndex): bool { - return $this->tokens[$tokenID]->id === T_CLASS - && isset($this->tokens[$tokenID - 1]) - && $this->tokens[$tokenID - 1]->id === T_PAAMAYIM_NEKUDOTAYIM; + return $this->tokens[$tokenIndex]->id === T_CLASS + && isset($this->tokens[$tokenIndex - 1]) + && $this->tokens[$tokenIndex - 1]->id === T_PAAMAYIM_NEKUDOTAYIM; } /** * Check if token ID represents anonymous class creation, e.g. `new class ('foo', 'bar')`. */ - private function isAnonymousClass(int|string $tokenID): bool + private function isAnonymousClass(int $tokenIndex): bool { - return $this->tokens[$tokenID]->id === T_CLASS - && isset($this->tokens[$tokenID - 2]) - && $this->tokens[$tokenID - 2]->id === T_NEW; + return $this->tokens[$tokenIndex]->id === T_CLASS + && isset($this->tokens[$tokenIndex - 2]) + && $this->tokens[$tokenIndex - 2]->id === T_NEW; } /** * Check if token ID represents named parameter with name `class`, e.g. `foo(class: SomeClass::name)`. */ - private function isCorrectDeclaration(int|string $tokenID): bool + private function isCorrectDeclaration(int $tokenIndex): bool { - return \in_array($this->tokens[$tokenID]->id, [T_CLASS, T_TRAIT, T_INTERFACE, T_ENUM], true) - && isset($this->tokens[$tokenID + 2]) - && $this->tokens[$tokenID + 1]->id === T_WHITESPACE - && $this->tokens[$tokenID + 2]->id === T_STRING; + return \in_array($this->tokens[$tokenIndex]->id, [T_CLASS, T_TRAIT, T_INTERFACE, T_ENUM], true) + && isset($this->tokens[$tokenIndex + 2]) + && $this->tokens[$tokenIndex + 1]->id === T_WHITESPACE + && $this->tokens[$tokenIndex + 2]->id === T_STRING; } /** * Get namespace name active at specified token position. + * + * @return array-key */ - private function activeNamespace(int $tokenID): string + private function activeNamespace(int $tokenIndex): string { foreach ($this->namespaces as $namespace => $position) { - if ($tokenID >= $position[self::O_TOKEN] && $tokenID <= $position[self::C_TOKEN]) { + if ($tokenIndex >= $position[self::O_TOKEN] && $tokenIndex <= $position[self::C_TOKEN]) { return $namespace; } } @@ -239,13 +248,13 @@ private function activeNamespace(int $tokenID): string } /** - * Find token ID of ending brace. + * Find token index of ending brace. */ - private function endingToken(int $tokenID): int + private function endingToken(int $tokenIndex): int { - $level = null; - for ($localID = $tokenID; $localID < $this->countTokens; ++$localID) { - $token = $this->tokens[$localID]; + $level = 0; + for ($localIndex = $tokenIndex; $localIndex < $this->countTokens; ++$localIndex) { + $token = $this->tokens[$localIndex]; if ($token->text === '{') { ++$level; continue; @@ -260,6 +269,6 @@ private function endingToken(int $tokenID): int } } - return $localID; + return $localIndex; } } diff --git a/src/TokenizerClassifier.php b/src/TokenizerClassifier.php index 99fd0de..cb34b07 100644 --- a/src/TokenizerClassifier.php +++ b/src/TokenizerClassifier.php @@ -11,6 +11,8 @@ final class TokenizerClassifier extends AbstractClassifier { /** * @psalm-suppress UnresolvableInclude + * + * @return array */ protected function getAvailableDeclarations(): iterable { From deed6eceee4feb63658913fccb9893fde479c6b3 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sun, 22 Oct 2023 18:46:47 +0500 Subject: [PATCH 13/15] Fixes --- src/ReflectionFile.php | 19 ++++++++++--------- tests/Declarations/namespace.php | 16 ++++++++-------- tests/ReflectionFileTest.php | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/ReflectionFile.php b/src/ReflectionFile.php index 1886523..e5b1030 100644 --- a/src/ReflectionFile.php +++ b/src/ReflectionFile.php @@ -24,15 +24,18 @@ final class ReflectionFile public const O_TOKEN = 0; public const C_TOKEN = 1; + public const T_OPEN_CURLY_BRACES = 123; + public const T_CLOSE_CURLY_BRACES = 125; + public const T_SEMICOLON = 59; /** * Set of tokens required to detect classes, traits, interfaces declarations. We * don't need any other token for that. */ private const TOKENS = [ - '{', - '}', - ';', + self::T_OPEN_CURLY_BRACES, + self::T_CLOSE_CURLY_BRACES, + self::T_SEMICOLON, T_PAAMAYIM_NEKUDOTAYIM, T_NAMESPACE, T_STRING, @@ -95,7 +98,7 @@ public function getDeclarations(): array private function locateDeclarations(): void { foreach ($this->tokens as $tokenIndex => $token) { - if (!\in_array($token->id, self::TOKENS)) { + if (!\in_array($token->id, self::TOKENS, true)) { continue; } @@ -145,10 +148,6 @@ private function registerNamespace(int $tokenIndex): void do { $token = $this->tokens[$localIndex++]; - if ($token->text === '{') { - break; - } - $namespace .= $token->text; } while ( isset($this->tokens[$localIndex]) @@ -253,10 +252,12 @@ private function activeNamespace(int $tokenIndex): string private function endingToken(int $tokenIndex): int { $level = 0; + $hasOpen = false; for ($localIndex = $tokenIndex; $localIndex < $this->countTokens; ++$localIndex) { $token = $this->tokens[$localIndex]; if ($token->text === '{') { ++$level; + $hasOpen = true; continue; } @@ -264,7 +265,7 @@ private function endingToken(int $tokenIndex): int --$level; } - if ($level === 0) { + if ($hasOpen && $level === 0) { break; } } diff --git a/tests/Declarations/namespace.php b/tests/Declarations/namespace.php index 5ecfef1..9c0c0a7 100644 --- a/tests/Declarations/namespace.php +++ b/tests/Declarations/namespace.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Support\Entity; - -class Person -{ -} - -class Client -{ +namespace Yiisoft\Classifier\Tests\Declarations { + class Person + { + } + + class Client + { + } } diff --git a/tests/ReflectionFileTest.php b/tests/ReflectionFileTest.php index 6d467ad..63f97a2 100644 --- a/tests/ReflectionFileTest.php +++ b/tests/ReflectionFileTest.php @@ -24,7 +24,7 @@ public function testNamespaceDeclaration(): void $reflectionFile = new ReflectionFile(__DIR__ . '/Declarations/namespace.php'); $this->assertCount(2, $reflectionFile->getDeclarations()); - $this->assertContains(\Support\Entity\Person::class, $reflectionFile->getDeclarations()); + $this->assertContains(\Yiisoft\Classifier\Tests\Declarations\Person::class, $reflectionFile->getDeclarations()); } public function testEnumDeclaration(): void From ff1285f688c0896ad494d56fc9b76bc22a6ce57e Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sun, 22 Oct 2023 13:47:04 +0000 Subject: [PATCH 14/15] Apply fixes from StyleCI --- tests/Declarations/namespace.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/Declarations/namespace.php b/tests/Declarations/namespace.php index 9c0c0a7..05e2d38 100644 --- a/tests/Declarations/namespace.php +++ b/tests/Declarations/namespace.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Yiisoft\Classifier\Tests\Declarations { - class Person - { - } - - class Client - { - } +namespace Yiisoft\Classifier\Tests\Declarations; + +class Person +{ +} + +class Client +{ } From 527cdceefe382b94b90a289058ec498e6caca6dd Mon Sep 17 00:00:00 2001 From: Rustam Date: Sun, 22 Oct 2023 18:54:38 +0500 Subject: [PATCH 15/15] Fix styleci --- .styleci.yml | 1 + tests/Declarations/namespace.php | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.styleci.yml b/.styleci.yml index 17f8dc4..f83bea2 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -9,6 +9,7 @@ finder: - vendor not-name: - wrong_file.php + - namespace.php enabled: - alpha_ordered_traits diff --git a/tests/Declarations/namespace.php b/tests/Declarations/namespace.php index 05e2d38..9c0c0a7 100644 --- a/tests/Declarations/namespace.php +++ b/tests/Declarations/namespace.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Yiisoft\Classifier\Tests\Declarations; - -class Person -{ -} - -class Client -{ +namespace Yiisoft\Classifier\Tests\Declarations { + class Person + { + } + + class Client + { + } }