From bcb7ffcb224dd529b6cbba75c6c9efa5dd4236a4 Mon Sep 17 00:00:00 2001 From: Rustam Date: Fri, 22 Sep 2023 10:05:43 +0500 Subject: [PATCH 01/20] Refactoring & PhpParserClassifier --- README.md | 2 +- composer.json | 1 + src/AbstractClassifier.php | 144 +++++++++++++++++++++++++ src/Classifier.php | 134 ++--------------------- src/ClassifierInterface.php | 10 ++ src/ClassifierVisitor.php | 49 +++++++++ src/PhpParserClassifier.php | 49 +++++++++ tests/BaseClassifierTest.php | 173 ++++++++++++++++++++++++++++++ tests/ClassifierTest.php | 161 +-------------------------- tests/PhpParserClassifierTest.php | 17 +++ 10 files changed, 456 insertions(+), 284 deletions(-) create mode 100644 src/AbstractClassifier.php create mode 100644 src/ClassifierInterface.php create mode 100644 src/ClassifierVisitor.php create mode 100644 src/PhpParserClassifier.php create mode 100644 tests/BaseClassifierTest.php create mode 100644 tests/PhpParserClassifierTest.php diff --git a/README.md b/README.md index 351ccff..3839160 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The package ... The package could be installed with composer: ```shell -composer require yiisoft/classifier --prefer-dist +composer require yiisoft/classifier ``` ## General usage diff --git a/composer.json b/composer.json index 6e11cf7..a781ce5 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ }, "require-dev": { "maglnet/composer-require-checker": "^4.2", + "nikic/php-parser": "^4.17", "phpunit/phpunit": "^9.5", "rector/rector": "^0.18.0", "roave/infection-static-analysis-plugin": "^1.16", diff --git a/src/AbstractClassifier.php b/src/AbstractClassifier.php new file mode 100644 index 0000000..aaa7d77 --- /dev/null +++ b/src/AbstractClassifier.php @@ -0,0 +1,144 @@ + + */ + private static array $reflectionsCache = []; + + /** + * @var string[] + */ + private array $interfaces = []; + /** + * @var string[] + */ + private array $attributes = []; + /** + * @psalm-var class-string + */ + private ?string $parentClass = null; + /** + * @var string[] + */ + private array $directories; + + public function __construct(string $directory, string ...$directories) + { + $this->directories = [$directory, ...array_values($directories)]; + } + + /** + * @psalm-param class-string ...$interfaces + */ + public function withInterface(string ...$interfaces): self + { + $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)); + + return $new; + } + + /** + * @psalm-return iterable + */ + public function find(): iterable + { + if (count($this->interfaces) === 0 && count($this->attributes) === 0 && $this->parentClass === null) { + return []; + } + + yield from $this->getAvailableClasses(); + } + + protected function getFiles(): Finder + { + return (new Finder()) + ->in($this->directories) + ->name('*.php') + ->sortByName() + ->files(); + } + + /** + * @psalm-param class-string $className + */ + protected function skipClass(string $className): bool + { + $reflectionClass = self::$reflectionsCache[$className] ??= new ReflectionClass($className); + + if ($reflectionClass->isInternal()) { + return true; + } + $countInterfaces = count($this->interfaces); + $countAttributes = count($this->attributes); + $directories = $this->directories; + + $matchedDirs = array_filter( + $directories, + static fn($directory) => str_starts_with($reflectionClass->getFileName(), $directory) + ); + + if (count($matchedDirs) === 0) { + return true; + } + + if ($countInterfaces > 0) { + $interfaces = $reflectionClass->getInterfaces(); + $interfaces = array_map(static fn(ReflectionClass $class) => $class->getName(), $interfaces); + + if (count(array_intersect($this->interfaces, $interfaces)) !== $countInterfaces) { + return true; + } + } + + if ($countAttributes > 0) { + $attributes = $reflectionClass->getAttributes(); + $attributes = array_map( + static fn(ReflectionAttribute $attribute) => $attribute->getName(), + $attributes + ); + + if (count(array_intersect($this->attributes, $attributes)) !== $countAttributes) { + return true; + } + } + + return ($this->parentClass !== null) && !is_subclass_of($reflectionClass->getName(), $this->parentClass); + } + + /** + * @return iterable + */ + abstract protected function getAvailableClasses(): iterable; +} diff --git a/src/Classifier.php b/src/Classifier.php index 8fcf9ed..72cb47f 100644 --- a/src/Classifier.php +++ b/src/Classifier.php @@ -8,143 +8,25 @@ use ReflectionClass; use Symfony\Component\Finder\Finder; -final class Classifier +final class Classifier extends AbstractClassifier { /** - * @var string[] - */ - private array $interfaces = []; - /** - * @var string[] - */ - private array $attributes = []; - /** - * @psalm-var class-string - */ - private ?string $parentClass = null; - /** - * @var string[] - */ - private array $directories; - - public function __construct(string $directory, string ...$directories) - { - $this->directories = [$directory, ...array_values($directories)]; - } - - /** - * @psalm-param class-string ...$interfaces - */ - public function withInterface(string ...$interfaces): self - { - $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)); - - return $new; - } - - /** - * @psalm-return iterable + * @psalm-suppress UnresolvableInclude */ - public function find(): iterable + protected function getAvailableClasses(): iterable { - $countInterfaces = count($this->interfaces); - $countAttributes = count($this->attributes); + $files = $this->getFiles(); - if ($countInterfaces === 0 && $countAttributes === 0 && $this->parentClass === null) { - return []; - } - - $this->scanFiles(); - - $classesToFind = get_declared_classes(); - $isWindows = DIRECTORY_SEPARATOR === '\\'; - $directories = $this->directories; - - if ($isWindows) { - /** @var string[] $directories */ - $directories = str_replace('/', '\\', $directories); + foreach ($files as $file) { + require_once $file; } - foreach ($classesToFind as $className) { - $reflection = new ReflectionClass($className); - - if (!$reflection->isUserDefined()) { - continue; - } - - $matchedDirs = array_filter( - $directories, - static fn($directory) => str_starts_with($reflection->getFileName(), $directory) - ); - - if (count($matchedDirs) === 0) { - continue; - } - - if ($countInterfaces > 0) { - $interfaces = $reflection->getInterfaces(); - $interfaces = array_map(static fn(ReflectionClass $class) => $class->getName(), $interfaces); - - if (count(array_intersect($this->interfaces, $interfaces)) !== $countInterfaces) { - continue; - } - } - - if ($countAttributes > 0) { - $attributes = $reflection->getAttributes(); - $attributes = array_map( - static fn(ReflectionAttribute $attribute) => $attribute->getName(), - $attributes - ); - - if (count(array_intersect($this->attributes, $attributes)) !== $countAttributes) { - continue; - } - } - - if (($this->parentClass !== null) && !is_subclass_of($className, $this->parentClass)) { + foreach (get_declared_classes() as $className) { + if ($this->skipClass($className)) { continue; } yield $className; } } - - /** - * @psalm-suppress UnresolvableInclude - */ - private function scanFiles(): void - { - $files = (new Finder()) - ->in($this->directories) - ->name('*.php') - ->sortByName() - ->files(); - - foreach ($files as $file) { - require_once $file; - } - } } diff --git a/src/ClassifierInterface.php b/src/ClassifierInterface.php new file mode 100644 index 0000000..f45ecaa --- /dev/null +++ b/src/ClassifierInterface.php @@ -0,0 +1,10 @@ + + */ + private array $classNames = []; + + /** + * @param \Closure(class-string): bool $shouldSkipClass + */ + public function __construct(private \Closure $shouldSkipClass) + { + } + + public function enterNode(Node $node) + { + if ($node instanceof Node\Stmt\Class_) { + /** + * @psalm-var class-string|null $className + */ + $className = $node->namespacedName?->toString(); + if ($className !== null && !call_user_func($this->shouldSkipClass, $className)) { + $this->classNames[] = $className; + } + } + + return parent::enterNode($node); + } + + /** + * @return array + */ + public function getClassNames(): array + { + return $this->classNames; + } +} diff --git a/src/PhpParserClassifier.php b/src/PhpParserClassifier.php new file mode 100644 index 0000000..f6cddfe --- /dev/null +++ b/src/PhpParserClassifier.php @@ -0,0 +1,49 @@ +parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); + + $filter = fn(string $className): bool => /** @psalm-var class-string $className */ $this->skipClass($className); + $visitor = new ClassifierVisitor($filter); + $traverser = new NodeTraverser(); + $traverser->addVisitor(new NameResolver()); + $traverser->addVisitor($visitor); + + $this->visitor = $visitor; + $this->nodeTraverser = $traverser; + } + + /** + * @return iterable + */ + protected function getAvailableClasses(): iterable + { + $files = $this->getFiles(); + + foreach ($files as $file) { + $nodes = $this->parser->parse($file->getContents()); + if ($nodes !== null) { + $this->nodeTraverser->traverse($nodes); + } + } + + yield from new \ArrayIterator($this->visitor->getClassNames()); + } +} diff --git a/tests/BaseClassifierTest.php b/tests/BaseClassifierTest.php new file mode 100644 index 0000000..463b8a3 --- /dev/null +++ b/tests/BaseClassifierTest.php @@ -0,0 +1,173 @@ +withInterface(UserInterface::class); + + $result = $finder->find(); + + $this->assertEqualsCanonicalizing([UserInDir1::class, UserInDir2::class], iterator_to_array($result)); + } + + /** + * @dataProvider interfacesDataProvider + */ + public function testInterfaces(string $directory, array $interfaces, array $expectedClasses): void + { + $finder = new Classifier($directory); + $finder = $finder->withInterface(...$interfaces); + + $result = $finder->find(); + + $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); + } + + public function interfacesDataProvider(): array + { + return [ + [ + __DIR__, + [], + [], + ], + [ + __DIR__, + [PostInterface::class], + [AuthorPost::class, Post::class, PostUser::class], + ], + [ + __DIR__, + [PostInterface::class], + [AuthorPost::class, Post::class, PostUser::class], + ], + [ + __DIR__, + [UserInterface::class], + [UserInDir1::class, UserInDir2::class, PostUser::class, SuperSuperUser::class, SuperUser::class, User::class, UserSubclass::class], + ], + [ + __DIR__, + [PostInterface::class, UserInterface::class], + [PostUser::class], + ], + [ + __DIR__ . '/Support/Dir1', + [UserInterface::class], + [UserInDir1::class], + ], + [ + __DIR__ . '/Support/Dir2', + [UserInterface::class], + [UserInDir2::class], + ], + ]; + } + + /** + * @dataProvider attributesDataProvider + */ + public function testAttributes(array $attributes, array $expectedClasses): void + { + $finder = new Classifier(__DIR__); + $finder = $finder->withAttribute(...$attributes); + + $result = $finder->find(); + + $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); + } + + /** + * @dataProvider parentClassDataProvider + */ + public function testParentClass(string $parent, array $expectedClasses): void + { + $finder = new Classifier(__DIR__); + $finder = $finder->withParentClass($parent); + + $result = $finder->find(); + + $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); + } + + public function attributesDataProvider(): array + { + return [ + [ + [], + [], + ], + [ + [AuthorAttribute::class], + [Author::class, AuthorPost::class], + ], + ]; + } + + /** + * @dataProvider mixedDataProvider + */ + public function testMixed(array $attributes, array $interfaces, array $expectedClasses): void + { + $finder = new Classifier(__DIR__); + $finder = $finder + ->withAttribute(...$attributes) + ->withInterface(...$interfaces); + + $result = $finder->find(); + + $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); + } + + public function mixedDataProvider(): array + { + return [ + [ + [], + [], + [], + ], + [ + [AuthorAttribute::class], + [PostInterface::class], + [AuthorPost::class], + ], + ]; + } + + public function parentClassDataProvider(): array + { + return [ + [ + User::class, + [SuperSuperUser::class, SuperUser::class], + ], + ]; + } + + abstract protected function createClassifier(string ...$dirs): ClassifierInterface; +} diff --git a/tests/ClassifierTest.php b/tests/ClassifierTest.php index 3db3823..fef95d4 100644 --- a/tests/ClassifierTest.php +++ b/tests/ClassifierTest.php @@ -4,167 +4,14 @@ namespace Yiisoft\Classifier\Tests; -use PHPUnit\Framework\TestCase; use Yiisoft\Classifier\Classifier; -use Yiisoft\Classifier\Tests\Support\Attributes\AuthorAttribute; -use Yiisoft\Classifier\Tests\Support\Author; -use Yiisoft\Classifier\Tests\Support\AuthorPost; -use Yiisoft\Classifier\Tests\Support\Dir1\UserInDir1; -use Yiisoft\Classifier\Tests\Support\Dir2\UserInDir2; -use Yiisoft\Classifier\Tests\Support\Interfaces\PostInterface; -use Yiisoft\Classifier\Tests\Support\Interfaces\UserInterface; -use Yiisoft\Classifier\Tests\Support\Post; -use Yiisoft\Classifier\Tests\Support\PostUser; -use Yiisoft\Classifier\Tests\Support\SuperSuperUser; -use Yiisoft\Classifier\Tests\Support\SuperUser; -use Yiisoft\Classifier\Tests\Support\User; -use Yiisoft\Classifier\Tests\Support\UserSubclass; +use Yiisoft\Classifier\ClassifierInterface; -final class ClassifierTest extends TestCase +class ClassifierTest extends BaseClassifierTest { - public function testMultipleDirectories() - { - $dirs = [__DIR__ . '/Support/Dir1', __DIR__ . '/Support/Dir2']; - $finder = new Classifier(...$dirs); - $finder = $finder->withInterface(UserInterface::class); - - $result = $finder->find(); - - $this->assertEqualsCanonicalizing([UserInDir1::class, UserInDir2::class], iterator_to_array($result)); - } - - /** - * @dataProvider interfacesDataProvider - */ - public function testInterfaces(string $directory, array $interfaces, array $expectedClasses): void - { - $finder = new Classifier($directory); - $finder = $finder->withInterface(...$interfaces); - - $result = $finder->find(); - - $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); - } - - public function interfacesDataProvider(): array - { - return [ - [ - __DIR__, - [], - [], - ], - [ - __DIR__, - [PostInterface::class], - [AuthorPost::class, Post::class, PostUser::class], - ], - [ - __DIR__, - [PostInterface::class], - [AuthorPost::class, Post::class, PostUser::class], - ], - [ - __DIR__, - [UserInterface::class], - [UserInDir1::class, UserInDir2::class, PostUser::class, SuperSuperUser::class, SuperUser::class, User::class, UserSubclass::class], - ], - [ - __DIR__, - [PostInterface::class, UserInterface::class], - [PostUser::class], - ], - [ - __DIR__ . '/Support/Dir1', - [UserInterface::class], - [UserInDir1::class], - ], - [ - __DIR__ . '/Support/Dir2', - [UserInterface::class], - [UserInDir2::class], - ], - ]; - } - - /** - * @dataProvider attributesDataProvider - */ - public function testAttributes(array $attributes, array $expectedClasses): void - { - $finder = new Classifier(__DIR__); - $finder = $finder->withAttribute(...$attributes); - - $result = $finder->find(); - - $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); - } - - /** - * @dataProvider parentClassDataProvider - */ - public function testParentClass(string $parent, array $expectedClasses): void - { - $finder = new Classifier(__DIR__); - $finder = $finder->withParentClass($parent); - - $result = $finder->find(); - - $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); - } - - public function attributesDataProvider(): array - { - return [ - [ - [], - [], - ], - [ - [AuthorAttribute::class], - [Author::class, AuthorPost::class], - ], - ]; - } - - /** - * @dataProvider mixedDataProvider - */ - public function testMixed(array $attributes, array $interfaces, array $expectedClasses): void - { - $finder = new Classifier(__DIR__); - $finder = $finder - ->withAttribute(...$attributes) - ->withInterface(...$interfaces); - - $result = $finder->find(); - - $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); - } - - public function mixedDataProvider(): array - { - return [ - [ - [], - [], - [], - ], - [ - [AuthorAttribute::class], - [PostInterface::class], - [AuthorPost::class], - ], - ]; - } - public function parentClassDataProvider(): array + protected function createClassifier(string ...$dirs): ClassifierInterface { - return [ - [ - User::class, - [SuperSuperUser::class, SuperUser::class], - ], - ]; + return new Classifier(...$dirs); } } diff --git a/tests/PhpParserClassifierTest.php b/tests/PhpParserClassifierTest.php new file mode 100644 index 0000000..6c143d0 --- /dev/null +++ b/tests/PhpParserClassifierTest.php @@ -0,0 +1,17 @@ + Date: Fri, 22 Sep 2023 05:06:06 +0000 Subject: [PATCH 02/20] Apply fixes from StyleCI --- src/Classifier.php | 4 ---- src/ClassifierVisitor.php | 2 +- tests/ClassifierTest.php | 1 - tests/PhpParserClassifierTest.php | 1 - 4 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Classifier.php b/src/Classifier.php index 72cb47f..ced754c 100644 --- a/src/Classifier.php +++ b/src/Classifier.php @@ -4,10 +4,6 @@ namespace Yiisoft\Classifier; -use ReflectionAttribute; -use ReflectionClass; -use Symfony\Component\Finder\Finder; - final class Classifier extends AbstractClassifier { /** diff --git a/src/ClassifierVisitor.php b/src/ClassifierVisitor.php index 0caf364..87e626d 100644 --- a/src/ClassifierVisitor.php +++ b/src/ClassifierVisitor.php @@ -31,7 +31,7 @@ public function enterNode(Node $node) * @psalm-var class-string|null $className */ $className = $node->namespacedName?->toString(); - if ($className !== null && !call_user_func($this->shouldSkipClass, $className)) { + if ($className !== null && !($this->shouldSkipClass)($className)) { $this->classNames[] = $className; } } diff --git a/tests/ClassifierTest.php b/tests/ClassifierTest.php index fef95d4..5a725e7 100644 --- a/tests/ClassifierTest.php +++ b/tests/ClassifierTest.php @@ -9,7 +9,6 @@ class ClassifierTest extends BaseClassifierTest { - protected function createClassifier(string ...$dirs): ClassifierInterface { return new Classifier(...$dirs); diff --git a/tests/PhpParserClassifierTest.php b/tests/PhpParserClassifierTest.php index 6c143d0..39272f2 100644 --- a/tests/PhpParserClassifierTest.php +++ b/tests/PhpParserClassifierTest.php @@ -9,7 +9,6 @@ class PhpParserClassifierTest extends BaseClassifierTest { - protected function createClassifier(string ...$dirs): ClassifierInterface { return new PhpParserClassifier(...$dirs); From f4c00018a811a487d9a01c5d2265c4221b5b4c55 Mon Sep 17 00:00:00 2001 From: Rustam Date: Fri, 22 Sep 2023 10:11:28 +0500 Subject: [PATCH 03/20] Fix tests --- tests/BaseClassifierTest.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/BaseClassifierTest.php b/tests/BaseClassifierTest.php index 463b8a3..b05d7b2 100644 --- a/tests/BaseClassifierTest.php +++ b/tests/BaseClassifierTest.php @@ -26,7 +26,7 @@ abstract class BaseClassifierTest extends TestCase public function testMultipleDirectories() { $dirs = [__DIR__ . '/Support/Dir1', __DIR__ . '/Support/Dir2']; - $finder = new Classifier(...$dirs); + $finder = $this->createClassifier(...$dirs); $finder = $finder->withInterface(UserInterface::class); $result = $finder->find(); @@ -39,7 +39,7 @@ public function testMultipleDirectories() */ public function testInterfaces(string $directory, array $interfaces, array $expectedClasses): void { - $finder = new Classifier($directory); + $finder = $this->createClassifier($directory); $finder = $finder->withInterface(...$interfaces); $result = $finder->find(); @@ -93,7 +93,7 @@ public function interfacesDataProvider(): array */ public function testAttributes(array $attributes, array $expectedClasses): void { - $finder = new Classifier(__DIR__); + $finder = $this->createClassifier(__DIR__); $finder = $finder->withAttribute(...$attributes); $result = $finder->find(); @@ -106,7 +106,7 @@ public function testAttributes(array $attributes, array $expectedClasses): void */ public function testParentClass(string $parent, array $expectedClasses): void { - $finder = new Classifier(__DIR__); + $finder = $this->createClassifier(__DIR__); $finder = $finder->withParentClass($parent); $result = $finder->find(); @@ -133,7 +133,7 @@ public function attributesDataProvider(): array */ public function testMixed(array $attributes, array $interfaces, array $expectedClasses): void { - $finder = new Classifier(__DIR__); + $finder = $this->createClassifier(__DIR__); $finder = $finder ->withAttribute(...$attributes) ->withInterface(...$interfaces); From 9cc0ec160c979d740eaea393d3f2009ae7a6719e Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 22 Sep 2023 05:14:20 +0000 Subject: [PATCH 04/20] Apply fixes from StyleCI --- tests/BaseClassifierTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/BaseClassifierTest.php b/tests/BaseClassifierTest.php index b05d7b2..f9826a1 100644 --- a/tests/BaseClassifierTest.php +++ b/tests/BaseClassifierTest.php @@ -5,7 +5,6 @@ namespace Yiisoft\Classifier\Tests; use PHPUnit\Framework\TestCase; -use Yiisoft\Classifier\Classifier; use Yiisoft\Classifier\ClassifierInterface; use Yiisoft\Classifier\Tests\Support\Attributes\AuthorAttribute; use Yiisoft\Classifier\Tests\Support\Author; From 4f5a06660ee9a160ed93689b04cfd4aa61033bc2 Mon Sep 17 00:00:00 2001 From: Rustam Date: Fri, 22 Sep 2023 10:37:27 +0500 Subject: [PATCH 05/20] Fix visitor registering --- src/PhpParserClassifier.php | 10 ++++------ tests/BaseClassifierTest.php | 10 +++++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/PhpParserClassifier.php b/src/PhpParserClassifier.php index f6cddfe..51b9e39 100644 --- a/src/PhpParserClassifier.php +++ b/src/PhpParserClassifier.php @@ -12,7 +12,6 @@ final class PhpParserClassifier extends AbstractClassifier { private Parser $parser; - private ClassifierVisitor $visitor; private NodeTraverser $nodeTraverser; public function __construct(string $directory, string ...$directories) @@ -20,13 +19,9 @@ public function __construct(string $directory, string ...$directories) parent::__construct($directory, ...$directories); $this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); - $filter = fn(string $className): bool => /** @psalm-var class-string $className */ $this->skipClass($className); - $visitor = new ClassifierVisitor($filter); $traverser = new NodeTraverser(); $traverser->addVisitor(new NameResolver()); - $traverser->addVisitor($visitor); - $this->visitor = $visitor; $this->nodeTraverser = $traverser; } @@ -36,6 +31,9 @@ public function __construct(string $directory, string ...$directories) protected function getAvailableClasses(): iterable { $files = $this->getFiles(); + $filter = fn(string $className): bool => /** @psalm-var class-string $className */ $this->skipClass($className); + $visitor = new ClassifierVisitor($filter); + $this->nodeTraverser->addVisitor($visitor); foreach ($files as $file) { $nodes = $this->parser->parse($file->getContents()); @@ -44,6 +42,6 @@ protected function getAvailableClasses(): iterable } } - yield from new \ArrayIterator($this->visitor->getClassNames()); + yield from new \ArrayIterator($visitor->getClassNames()); } } diff --git a/tests/BaseClassifierTest.php b/tests/BaseClassifierTest.php index f9826a1..9386edc 100644 --- a/tests/BaseClassifierTest.php +++ b/tests/BaseClassifierTest.php @@ -22,7 +22,7 @@ abstract class BaseClassifierTest extends TestCase { - public function testMultipleDirectories() + public function testMultipleDirectories(): void { $dirs = [__DIR__ . '/Support/Dir1', __DIR__ . '/Support/Dir2']; $finder = $this->createClassifier(...$dirs); @@ -46,7 +46,7 @@ public function testInterfaces(string $directory, array $interfaces, array $expe $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); } - public function interfacesDataProvider(): array + public static function interfacesDataProvider(): array { return [ [ @@ -113,7 +113,7 @@ public function testParentClass(string $parent, array $expectedClasses): void $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); } - public function attributesDataProvider(): array + public static function attributesDataProvider(): array { return [ [ @@ -142,7 +142,7 @@ public function testMixed(array $attributes, array $interfaces, array $expectedC $this->assertEqualsCanonicalizing($expectedClasses, iterator_to_array($result)); } - public function mixedDataProvider(): array + public static function mixedDataProvider(): array { return [ [ @@ -158,7 +158,7 @@ public function mixedDataProvider(): array ]; } - public function parentClassDataProvider(): array + public static function parentClassDataProvider(): array { return [ [ From 018591368ac357f2a1d7e5cc596d7165436d2628 Mon Sep 17 00:00:00 2001 From: Rustam Date: Fri, 22 Sep 2023 11:05:47 +0500 Subject: [PATCH 06/20] Minor fix --- src/AbstractClassifier.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/AbstractClassifier.php b/src/AbstractClassifier.php index aaa7d77..af8cd1a 100644 --- a/src/AbstractClassifier.php +++ b/src/AbstractClassifier.php @@ -35,6 +35,11 @@ abstract class AbstractClassifier implements ClassifierInterface public function __construct(string $directory, string ...$directories) { $this->directories = [$directory, ...array_values($directories)]; + $isWindows = DIRECTORY_SEPARATOR === '\\'; + + if ($isWindows) { + $this->directories = str_replace('/', '\\', $this->directories); + } } /** From 947ac8a3f8d073531e971039a80a60ef5490b39a Mon Sep 17 00:00:00 2001 From: Rustam Date: Fri, 22 Sep 2023 21:35:19 +0500 Subject: [PATCH 07/20] Refactor --- src/AbstractClassifier.php | 69 +++---------------------------------- src/Classifier.php | 59 +++++++++++++++++++++++++++++++ src/ClassifierVisitor.php | 52 ++++++++++++++++++++++------ src/PhpParserClassifier.php | 3 +- 4 files changed, 107 insertions(+), 76 deletions(-) diff --git a/src/AbstractClassifier.php b/src/AbstractClassifier.php index af8cd1a..47c0ec8 100644 --- a/src/AbstractClassifier.php +++ b/src/AbstractClassifier.php @@ -4,42 +4,30 @@ namespace Yiisoft\Classifier; -use ReflectionAttribute; -use ReflectionClass; use Symfony\Component\Finder\Finder; abstract class AbstractClassifier implements ClassifierInterface { - /** - * @psalm-var array - */ - private static array $reflectionsCache = []; - /** * @var string[] */ - private array $interfaces = []; + protected array $interfaces = []; /** * @var string[] */ - private array $attributes = []; + protected array $attributes = []; /** * @psalm-var class-string */ - private ?string $parentClass = null; + protected ?string $parentClass = null; /** * @var string[] */ - private array $directories; + protected array $directories; public function __construct(string $directory, string ...$directories) { $this->directories = [$directory, ...array_values($directories)]; - $isWindows = DIRECTORY_SEPARATOR === '\\'; - - if ($isWindows) { - $this->directories = str_replace('/', '\\', $this->directories); - } } /** @@ -79,7 +67,7 @@ public function withAttribute(string ...$attributes): self */ public function find(): iterable { - if (count($this->interfaces) === 0 && count($this->attributes) === 0 && $this->parentClass === null) { + if (empty($this->interfaces) && empty($this->attributes) && $this->parentClass === null) { return []; } @@ -95,53 +83,6 @@ protected function getFiles(): Finder ->files(); } - /** - * @psalm-param class-string $className - */ - protected function skipClass(string $className): bool - { - $reflectionClass = self::$reflectionsCache[$className] ??= new ReflectionClass($className); - - if ($reflectionClass->isInternal()) { - return true; - } - $countInterfaces = count($this->interfaces); - $countAttributes = count($this->attributes); - $directories = $this->directories; - - $matchedDirs = array_filter( - $directories, - static fn($directory) => str_starts_with($reflectionClass->getFileName(), $directory) - ); - - if (count($matchedDirs) === 0) { - return true; - } - - if ($countInterfaces > 0) { - $interfaces = $reflectionClass->getInterfaces(); - $interfaces = array_map(static fn(ReflectionClass $class) => $class->getName(), $interfaces); - - if (count(array_intersect($this->interfaces, $interfaces)) !== $countInterfaces) { - return true; - } - } - - if ($countAttributes > 0) { - $attributes = $reflectionClass->getAttributes(); - $attributes = array_map( - static fn(ReflectionAttribute $attribute) => $attribute->getName(), - $attributes - ); - - if (count(array_intersect($this->attributes, $attributes)) !== $countAttributes) { - return true; - } - } - - return ($this->parentClass !== null) && !is_subclass_of($reflectionClass->getName(), $this->parentClass); - } - /** * @return iterable */ diff --git a/src/Classifier.php b/src/Classifier.php index ced754c..5fb16e4 100644 --- a/src/Classifier.php +++ b/src/Classifier.php @@ -4,8 +4,16 @@ namespace Yiisoft\Classifier; +use ReflectionAttribute; +use ReflectionClass; + final class Classifier extends AbstractClassifier { + /** + * @psalm-var array + */ + private static array $reflectionsCache = []; + /** * @psalm-suppress UnresolvableInclude */ @@ -25,4 +33,55 @@ protected function getAvailableClasses(): iterable yield $className; } } + + /** + * @psalm-param class-string $className + */ + protected function skipClass(string $className): bool + { + $reflectionClass = self::$reflectionsCache[$className] ??= new ReflectionClass($className); + + if ($reflectionClass->isInternal()) { + return true; + } + $directories = $this->directories; + $isWindows = DIRECTORY_SEPARATOR === '\\'; + + if ($isWindows) { + /** @psalm-var string[] $directories */ + $directories = str_replace('/', '\\', $directories); + } + + $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); + + 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 + ); + + if (count(array_intersect($this->attributes, $attributes)) !== count($this->attributes)) { + return true; + } + } + + return ($this->parentClass !== null) && !is_subclass_of($reflectionClass->getName(), $this->parentClass); + } } diff --git a/src/ClassifierVisitor.php b/src/ClassifierVisitor.php index 87e626d..d99ae22 100644 --- a/src/ClassifierVisitor.php +++ b/src/ClassifierVisitor.php @@ -5,10 +5,11 @@ namespace Yiisoft\Classifier; use PhpParser\Node; +use PhpParser\Node\Stmt\Class_; use PhpParser\NodeVisitorAbstract; /** - * @internal Visitor for Classifier + * @internal for PhpParserClassifier */ final class ClassifierVisitor extends NodeVisitorAbstract { @@ -18,22 +19,24 @@ final class ClassifierVisitor extends NodeVisitorAbstract private array $classNames = []; /** - * @param \Closure(class-string): bool $shouldSkipClass + * @psalm-param class-string $allowedParentClass */ - public function __construct(private \Closure $shouldSkipClass) - { + public function __construct( + private array $allowedInterfaces, + private array $allowedAttributes, + private ?string $allowedParentClass = null + ) { } public function enterNode(Node $node) { - if ($node instanceof Node\Stmt\Class_) { + if (($node instanceof Class_) && !$this->skipClass($node)) { /** - * @psalm-var class-string|null $className + * @var class-string $className + * @psalm-suppress PossiblyNullReference checked in {@see skipClass} method. */ - $className = $node->namespacedName?->toString(); - if ($className !== null && !($this->shouldSkipClass)($className)) { - $this->classNames[] = $className; - } + $className = $node->namespacedName->toString(); + $this->classNames[] = $className; } return parent::enterNode($node); @@ -46,4 +49,33 @@ public function getClassNames(): array { return $this->classNames; } + + private function skipClass(Class_ $class): bool + { + if ($class->namespacedName === null) { + return true; + } + $className = $class->namespacedName->toString(); + $interfacesNames = class_implements($className); + if ( + $interfacesNames !== false && + count(array_intersect($this->allowedInterfaces, $interfacesNames)) !== count($this->allowedInterfaces) + ) { + return true; + } + $attributesNames = []; + foreach ($class->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + $attributesNames[] = $attr->name->toString(); + } + } + if (count(array_intersect($this->allowedAttributes, $attributesNames)) !== count($this->allowedAttributes)) { + return true; + } + + $classParents = class_parents($className); + + return ($this->allowedParentClass !== null && $classParents !== false) && + !in_array($this->allowedParentClass, $classParents, true); + } } diff --git a/src/PhpParserClassifier.php b/src/PhpParserClassifier.php index 51b9e39..a1c65b7 100644 --- a/src/PhpParserClassifier.php +++ b/src/PhpParserClassifier.php @@ -31,8 +31,7 @@ public function __construct(string $directory, string ...$directories) protected function getAvailableClasses(): iterable { $files = $this->getFiles(); - $filter = fn(string $className): bool => /** @psalm-var class-string $className */ $this->skipClass($className); - $visitor = new ClassifierVisitor($filter); + $visitor = new ClassifierVisitor($this->interfaces, $this->attributes, $this->parentClass); $this->nodeTraverser->addVisitor($visitor); foreach ($files as $file) { From fb2c95e707f967ccb9abf2a556206db56e707321 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sat, 23 Sep 2023 09:18:53 +0500 Subject: [PATCH 08/20] Increase code coverage --- README.md | 4 ++-- src/Classifier.php | 8 ++++++-- src/ClassifierVisitor.php | 2 +- tests/Support/test.php | 9 +++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 tests/Support/test.php diff --git a/README.md b/README.md index 3839160..dfa3313 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ [![Latest Stable Version](https://poser.pugx.org/yiisoft/classifier/v/stable.png)](https://packagist.org/packages/yiisoft/classifier) [![Total Downloads](https://poser.pugx.org/yiisoft/classifier/downloads.png)](https://packagist.org/packages/yiisoft/classifier) [![Build status](https://github.com/yiisoft/classifier/workflows/build/badge.svg)](https://github.com/yiisoft/classifier/actions?query=workflow%3Abuild) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/yiisoft/classifier/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/classifier/?branch=master) -[![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/classifier/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/classifier/?branch=master) +[![Code Coverage](https://codecov.io/gh/yiisoft/classifier/branch/master/graph/badge.svg)](https://codecov.io/gh/yiisoft/classifier) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fclassifier%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/classifier/master) [![static analysis](https://github.com/yiisoft/classifier/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/classifier/actions?query=workflow%3A%22static+analysis%22) [![type-coverage](https://shepherd.dev/github/yiisoft/classifier/coverage.svg)](https://shepherd.dev/github/yiisoft/classifier) +[![psalm-level](https://shepherd.dev/github/yiisoft/classifier/level.svg)](https://shepherd.dev/github/yiisoft/classifier) The package ... diff --git a/src/Classifier.php b/src/Classifier.php index 5fb16e4..0ac8d31 100644 --- a/src/Classifier.php +++ b/src/Classifier.php @@ -41,15 +41,19 @@ protected function skipClass(string $className): bool { $reflectionClass = self::$reflectionsCache[$className] ??= new ReflectionClass($className); - if ($reflectionClass->isInternal()) { + if ($reflectionClass->isInternal() || $reflectionClass->isAnonymous()) { return true; } $directories = $this->directories; $isWindows = DIRECTORY_SEPARATOR === '\\'; if ($isWindows) { - /** @psalm-var string[] $directories */ + /** + * @psalm-var string[] $directories + */ + // @codeCoverageIgnoreStart $directories = str_replace('/', '\\', $directories); + // @codeCoverageIgnoreEnd } $matchedDirs = array_filter( diff --git a/src/ClassifierVisitor.php b/src/ClassifierVisitor.php index d99ae22..dfa0059 100644 --- a/src/ClassifierVisitor.php +++ b/src/ClassifierVisitor.php @@ -52,7 +52,7 @@ public function getClassNames(): array private function skipClass(Class_ $class): bool { - if ($class->namespacedName === null) { + if ($class->namespacedName === null || $class->isAnonymous()) { return true; } $className = $class->namespacedName->toString(); diff --git a/tests/Support/test.php b/tests/Support/test.php new file mode 100644 index 0000000..c788eb2 --- /dev/null +++ b/tests/Support/test.php @@ -0,0 +1,9 @@ + Date: Sat, 23 Sep 2023 04:19:11 +0000 Subject: [PATCH 09/20] Apply fixes from StyleCI --- tests/Support/test.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Support/test.php b/tests/Support/test.php index c788eb2..5b0c65c 100644 --- a/tests/Support/test.php +++ b/tests/Support/test.php @@ -4,6 +4,5 @@ use Yiisoft\Classifier\Tests\Support\Interfaces\PostInterface; -return new class implements PostInterface -{ +return new class () implements PostInterface { }; From 33017d61d025a7885c049193259fac677f5820a2 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sat, 23 Sep 2023 09:44:13 +0500 Subject: [PATCH 10/20] Fix method visibility --- src/Classifier.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Classifier.php b/src/Classifier.php index 0ac8d31..3034733 100644 --- a/src/Classifier.php +++ b/src/Classifier.php @@ -37,7 +37,7 @@ protected function getAvailableClasses(): iterable /** * @psalm-param class-string $className */ - protected function skipClass(string $className): bool + private function skipClass(string $className): bool { $reflectionClass = self::$reflectionsCache[$className] ??= new ReflectionClass($className); From 9d70f935a0a8a99cb029a0c3aebd3beb94491aea Mon Sep 17 00:00:00 2001 From: Rustam Date: Sun, 24 Sep 2023 22:51:45 +0500 Subject: [PATCH 11/20] [Skip CI] Add phpdoc --- src/PhpParserClassifier.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PhpParserClassifier.php b/src/PhpParserClassifier.php index a1c65b7..672320c 100644 --- a/src/PhpParserClassifier.php +++ b/src/PhpParserClassifier.php @@ -9,6 +9,10 @@ use PhpParser\Parser; use PhpParser\ParserFactory; +/** + * `PhpParserClassifier` finds classes using the `nikic/PHP-Parser` package and this may require performance tuning, so + * you may need follow {@see https://github.com/nikic/PHP-Parser/blob/master/doc/component/Performance.markdown} instructions. + */ final class PhpParserClassifier extends AbstractClassifier { private Parser $parser; From 8f715bba39a905d48cb609d3fd57dd3224c4786a Mon Sep 17 00:00:00 2001 From: Rustam Date: Thu, 28 Sep 2023 11:34:48 +0500 Subject: [PATCH 12/20] Adjust naming & add phpdocs --- src/AbstractClassifier.php | 3 +++ src/ClassifierInterface.php | 9 ++++++++ src/{Classifier.php => NativeClassifier.php} | 2 +- ...serClassifier.php => ParserClassifier.php} | 23 +++++++++++-------- ...lassifierVisitor.php => ParserVisitor.php} | 4 ++-- ...ifierTest.php => NativeClassifierTest.php} | 6 ++--- ...ifierTest.php => ParserClassifierTest.php} | 6 ++--- 7 files changed, 35 insertions(+), 18 deletions(-) rename src/{Classifier.php => NativeClassifier.php} (97%) rename src/{PhpParserClassifier.php => ParserClassifier.php} (53%) rename src/{ClassifierVisitor.php => ParserVisitor.php} (95%) rename tests/{ClassifierTest.php => NativeClassifierTest.php} (59%) rename tests/{PhpParserClassifierTest.php => ParserClassifierTest.php} (58%) diff --git a/src/AbstractClassifier.php b/src/AbstractClassifier.php index 47c0ec8..5ad78d8 100644 --- a/src/AbstractClassifier.php +++ b/src/AbstractClassifier.php @@ -6,6 +6,9 @@ use Symfony\Component\Finder\Finder; +/** + * Base implementation for {@see ClassifierInterface} with common filters. + */ abstract class AbstractClassifier implements ClassifierInterface { /** diff --git a/src/ClassifierInterface.php b/src/ClassifierInterface.php index f45ecaa..4e03824 100644 --- a/src/ClassifierInterface.php +++ b/src/ClassifierInterface.php @@ -4,7 +4,16 @@ namespace Yiisoft\Classifier; +/** + * `Classifier` is a class finder that represents the classes found. + */ interface ClassifierInterface { + /** + * Returns all the class names found. + * + * @return iterable List of class names. + * @psalm-return iterable + */ public function find(): iterable; } diff --git a/src/Classifier.php b/src/NativeClassifier.php similarity index 97% rename from src/Classifier.php rename to src/NativeClassifier.php index 3034733..7c4795d 100644 --- a/src/Classifier.php +++ b/src/NativeClassifier.php @@ -7,7 +7,7 @@ use ReflectionAttribute; use ReflectionClass; -final class Classifier extends AbstractClassifier +final class NativeClassifier extends AbstractClassifier { /** * @psalm-var array diff --git a/src/PhpParserClassifier.php b/src/ParserClassifier.php similarity index 53% rename from src/PhpParserClassifier.php rename to src/ParserClassifier.php index 672320c..13a8b19 100644 --- a/src/PhpParserClassifier.php +++ b/src/ParserClassifier.php @@ -10,10 +10,11 @@ use PhpParser\ParserFactory; /** - * `PhpParserClassifier` finds classes using the `nikic/PHP-Parser` package and this may require performance tuning, so - * you may need follow {@see https://github.com/nikic/PHP-Parser/blob/master/doc/component/Performance.markdown} instructions. + * `ParserClassifier` finds classes using [`nikic/PHP-Parser`](https://github.com/nikic/PHP-Parser). + * This may require performance tuning, so you may need + * follow {@see https://github.com/nikic/PHP-Parser/blob/master/doc/component/Performance.markdown} instructions. */ -final class PhpParserClassifier extends AbstractClassifier +final class ParserClassifier extends AbstractClassifier { private Parser $parser; private NodeTraverser $nodeTraverser; @@ -21,7 +22,7 @@ final class PhpParserClassifier extends AbstractClassifier public function __construct(string $directory, string ...$directories) { parent::__construct($directory, ...$directories); - $this->parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); + $this->parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7); $traverser = new NodeTraverser(); $traverser->addVisitor(new NameResolver()); @@ -35,16 +36,20 @@ public function __construct(string $directory, string ...$directories) protected function getAvailableClasses(): iterable { $files = $this->getFiles(); - $visitor = new ClassifierVisitor($this->interfaces, $this->attributes, $this->parentClass); + $visitor = new ParserVisitor($this->interfaces, $this->attributes, $this->parentClass); $this->nodeTraverser->addVisitor($visitor); foreach ($files as $file) { - $nodes = $this->parser->parse($file->getContents()); - if ($nodes !== null) { - $this->nodeTraverser->traverse($nodes); + try { + $nodes = $this->parser->parse($file->getContents()); + if ($nodes !== null) { + $this->nodeTraverser->traverse($nodes); + } + } catch (\Throwable) { + // Ignore broken files or parsing errors } } - yield from new \ArrayIterator($visitor->getClassNames()); + return $visitor->getClassNames(); } } diff --git a/src/ClassifierVisitor.php b/src/ParserVisitor.php similarity index 95% rename from src/ClassifierVisitor.php rename to src/ParserVisitor.php index dfa0059..7f71627 100644 --- a/src/ClassifierVisitor.php +++ b/src/ParserVisitor.php @@ -9,9 +9,9 @@ use PhpParser\NodeVisitorAbstract; /** - * @internal for PhpParserClassifier + * @internal for ParserClassifier */ -final class ClassifierVisitor extends NodeVisitorAbstract +final class ParserVisitor extends NodeVisitorAbstract { /** * @var array diff --git a/tests/ClassifierTest.php b/tests/NativeClassifierTest.php similarity index 59% rename from tests/ClassifierTest.php rename to tests/NativeClassifierTest.php index 5a725e7..372ffdb 100644 --- a/tests/ClassifierTest.php +++ b/tests/NativeClassifierTest.php @@ -4,13 +4,13 @@ namespace Yiisoft\Classifier\Tests; -use Yiisoft\Classifier\Classifier; +use Yiisoft\Classifier\NativeClassifier; use Yiisoft\Classifier\ClassifierInterface; -class ClassifierTest extends BaseClassifierTest +class NativeClassifierTest extends BaseClassifierTest { protected function createClassifier(string ...$dirs): ClassifierInterface { - return new Classifier(...$dirs); + return new NativeClassifier(...$dirs); } } diff --git a/tests/PhpParserClassifierTest.php b/tests/ParserClassifierTest.php similarity index 58% rename from tests/PhpParserClassifierTest.php rename to tests/ParserClassifierTest.php index 39272f2..488355a 100644 --- a/tests/PhpParserClassifierTest.php +++ b/tests/ParserClassifierTest.php @@ -5,12 +5,12 @@ namespace Yiisoft\Classifier\Tests; use Yiisoft\Classifier\ClassifierInterface; -use Yiisoft\Classifier\PhpParserClassifier; +use Yiisoft\Classifier\ParserClassifier; -class PhpParserClassifierTest extends BaseClassifierTest +class ParserClassifierTest extends BaseClassifierTest { protected function createClassifier(string ...$dirs): ClassifierInterface { - return new PhpParserClassifier(...$dirs); + return new ParserClassifier(...$dirs); } } From 1fd7d54d13e72ef168386ad0a5a0616f0c76fd62 Mon Sep 17 00:00:00 2001 From: Rustam Date: Thu, 28 Sep 2023 11:43:37 +0500 Subject: [PATCH 13/20] Add phpdoc & fix composer --- composer.json | 3 +++ src/NativeClassifier.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/composer.json b/composer.json index a781ce5..9485fda 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,9 @@ "Yiisoft\\Classifier\\Tests\\": "tests" } }, + "suggest": { + "nikic/php-parser": "Need for ParserClassifier implementation" + }, "extra": { "branch-alias": { "dev-master": "3.0.x-dev" diff --git a/src/NativeClassifier.php b/src/NativeClassifier.php index 7c4795d..2d31bd4 100644 --- a/src/NativeClassifier.php +++ b/src/NativeClassifier.php @@ -7,6 +7,9 @@ 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 { /** From baa2ae486951d838afdd93c26e10e0295c2ae8d6 Mon Sep 17 00:00:00 2001 From: Rustam Date: Thu, 28 Sep 2023 11:54:55 +0500 Subject: [PATCH 14/20] Fix composer checker --- composer-require-checker.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 composer-require-checker.json diff --git a/composer-require-checker.json b/composer-require-checker.json new file mode 100644 index 0000000..0809396 --- /dev/null +++ b/composer-require-checker.json @@ -0,0 +1,11 @@ +{ + "symbol-whitelist": [ + "PhpParser\\Node", + "PhpParser\\NodeTraverser", + "PhpParser\\NodeVisitorAbstract", + "PhpParser\\NodeVisitor\\NameResolver", + "PhpParser\\Node\\Stmt\\Class_", + "PhpParser\\Parser", + "PhpParser\\ParserFactory" + ] +} From 13f251df50f1c7ad49c78b9493c3e5c59dd303fc Mon Sep 17 00:00:00 2001 From: Rustam Date: Thu, 28 Sep 2023 18:19:06 +0500 Subject: [PATCH 15/20] Optimize visitor --- src/NativeClassifier.php | 6 +++++- src/ParserVisitor.php | 9 +++++++-- tests/Support/wrong_file.php | 8 ++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 tests/Support/wrong_file.php diff --git a/src/NativeClassifier.php b/src/NativeClassifier.php index 2d31bd4..22ed6bf 100644 --- a/src/NativeClassifier.php +++ b/src/NativeClassifier.php @@ -25,7 +25,11 @@ protected function getAvailableClasses(): iterable $files = $this->getFiles(); foreach ($files as $file) { - require_once $file; + try { + require_once $file; + } catch (\Throwable) { + // Ignore syntax errors + } } foreach (get_declared_classes() as $className) { diff --git a/src/ParserVisitor.php b/src/ParserVisitor.php index 7f71627..6344a87 100644 --- a/src/ParserVisitor.php +++ b/src/ParserVisitor.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Class_; +use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; /** @@ -30,7 +31,11 @@ public function __construct( public function enterNode(Node $node) { - if (($node instanceof Class_) && !$this->skipClass($node)) { + if (!($node instanceof Class_)) { + return parent::enterNode($node); + } + + if (!$this->skipClass($node)) { /** * @var class-string $className * @psalm-suppress PossiblyNullReference checked in {@see skipClass} method. @@ -39,7 +44,7 @@ public function enterNode(Node $node) $this->classNames[] = $className; } - return parent::enterNode($node); + return NodeTraverser::DONT_TRAVERSE_CHILDREN; } /** diff --git a/tests/Support/wrong_file.php b/tests/Support/wrong_file.php new file mode 100644 index 0000000..33442fd --- /dev/null +++ b/tests/Support/wrong_file.php @@ -0,0 +1,8 @@ + Date: Thu, 28 Sep 2023 18:30:22 +0500 Subject: [PATCH 16/20] Ignore wrong file --- .styleci.yml | 1 + rector.php | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.styleci.yml b/.styleci.yml index 1ab379b..691bbde 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -7,6 +7,7 @@ finder: exclude: - docs - vendor + - tests/Support/wrong_file.php enabled: - alpha_ordered_traits diff --git a/rector.php b/rector.php index 63713ce..65372ac 100644 --- a/rector.php +++ b/rector.php @@ -12,6 +12,10 @@ __DIR__ . '/tests', ]); + $rectorConfig->skip([ + __DIR__ . '/tests/Support/wrong_file.php' + ]); + // register a single rule $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); From 3fe9c9c59ad175a6403b4fec14e81660f01fd01b Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 28 Sep 2023 13:30:41 +0000 Subject: [PATCH 17/20] Apply fixes from StyleCI --- rector.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rector.php b/rector.php index 65372ac..531f028 100644 --- a/rector.php +++ b/rector.php @@ -13,7 +13,7 @@ ]); $rectorConfig->skip([ - __DIR__ . '/tests/Support/wrong_file.php' + __DIR__ . '/tests/Support/wrong_file.php', ]); // register a single rule From f8716a67706590de737c36539c9fa1fb8f0e1d11 Mon Sep 17 00:00:00 2001 From: Rustam Date: Thu, 28 Sep 2023 18:43:01 +0500 Subject: [PATCH 18/20] Fix styleci config --- .styleci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.styleci.yml b/.styleci.yml index 691bbde..17f8dc4 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -7,7 +7,8 @@ finder: exclude: - docs - vendor - - tests/Support/wrong_file.php + not-name: + - wrong_file.php enabled: - alpha_ordered_traits From 46cf634006f617b16f778757476c7f844d024d75 Mon Sep 17 00:00:00 2001 From: Rustam Date: Sun, 15 Oct 2023 18:05:33 +0500 Subject: [PATCH 19/20] Minor test --- tests/BaseClassifierTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/BaseClassifierTest.php b/tests/BaseClassifierTest.php index 9386edc..bf9fd5e 100644 --- a/tests/BaseClassifierTest.php +++ b/tests/BaseClassifierTest.php @@ -22,6 +22,17 @@ abstract class BaseClassifierTest extends TestCase { + public function testMultipleUse(): void + { + $dirs = [__DIR__ . '/Support/Dir1', __DIR__ . '/Support/Dir2']; + $finder = $this->createClassifier(...$dirs); + $finder = $finder->withInterface(UserInterface::class); + + $result = $finder->find(); + + $this->assertEqualsCanonicalizing(iterator_to_array($finder->find()), iterator_to_array($result)); + } + public function testMultipleDirectories(): void { $dirs = [__DIR__ . '/Support/Dir1', __DIR__ . '/Support/Dir2']; From be48a614b838c7bcc9a7edf6877ebc4df8067fea Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Tue, 16 Jul 2024 22:22:57 +0300 Subject: [PATCH 20/20] Remove Classifier.php that returned during merge --- src/Classifier.php | 163 --------------------------------------------- 1 file changed, 163 deletions(-) delete mode 100644 src/Classifier.php diff --git a/src/Classifier.php b/src/Classifier.php deleted file mode 100644 index 65f3f07..0000000 --- a/src/Classifier.php +++ /dev/null @@ -1,163 +0,0 @@ -directories = [$directory, ...array_values($directories)]; - } - - /** - * @param string ...$interfaces Interfaces to search for. - * @psalm-param class-string ...$interfaces - */ - public function withInterface(string ...$interfaces): self - { - $new = clone $this; - array_push($new->interfaces, ...array_values($interfaces)); - - return $new; - } - - /** - * @param string $parentClass Parent class to search for. - * @psalm-param class-string $parentClass - */ - public function withParentClass(string $parentClass): self - { - $new = clone $this; - $new->parentClass = $parentClass; - return $new; - } - - /** - * @para string ...$attributes Attributes to search for. - * @psalm-param class-string ...$attributes - */ - public function withAttribute(string ...$attributes): self - { - $new = clone $this; - array_push($new->attributes, ...array_values($attributes)); - - return $new; - } - - /** - * @return string[] Classes found. - * @psalm-return iterable - */ - public function find(): iterable - { - $countInterfaces = count($this->interfaces); - $countAttributes = count($this->attributes); - - if ($countInterfaces === 0 && $countAttributes === 0 && $this->parentClass === null) { - return []; - } - - $this->scanFiles(); - - $classesToFind = get_declared_classes(); - $isWindows = DIRECTORY_SEPARATOR === '\\'; - $directories = $this->directories; - - if ($isWindows) { - /** @var string[] $directories */ - $directories = str_replace('/', '\\', $directories); - } - - foreach ($classesToFind as $className) { - $reflection = new ReflectionClass($className); - - if (!$reflection->isUserDefined()) { - continue; - } - - $matchedDirs = array_filter( - $directories, - static fn($directory) => str_starts_with($reflection->getFileName(), $directory) - ); - - if (count($matchedDirs) === 0) { - continue; - } - - if ($countInterfaces > 0) { - $interfaces = $reflection->getInterfaces(); - $interfaces = array_map(static fn(ReflectionClass $class) => $class->getName(), $interfaces); - - if (count(array_intersect($this->interfaces, $interfaces)) !== $countInterfaces) { - continue; - } - } - - if ($countAttributes > 0) { - $attributes = $reflection->getAttributes(); - $attributes = array_map( - static fn(ReflectionAttribute $attribute) => $attribute->getName(), - $attributes - ); - - if (count(array_intersect($this->attributes, $attributes)) !== $countAttributes) { - continue; - } - } - - if (($this->parentClass !== null) && !is_subclass_of($className, $this->parentClass)) { - continue; - } - - yield $className; - } - } - - /** - * Find all PHP files and require each one so these could be further analyzed via reflection. - * @psalm-suppress UnresolvableInclude - */ - private function scanFiles(): void - { - $files = (new Finder()) - ->in($this->directories) - ->name('*.php') - ->sortByName() - ->files(); - - foreach ($files as $file) { - require_once $file; - } - } -}