From cc18671ba47cb3254092fd83eb22f2a596034e61 Mon Sep 17 00:00:00 2001 From: Filipp Shcherbanich Date: Sat, 4 Jan 2025 23:34:44 +0500 Subject: [PATCH 01/10] Adding new method to iterate identifiers by type --- .../Type/AbstractSourceLocator.php | 29 +++++++++++ .../Type/AggregateSourceLocator.php | 16 ++++++ .../AnonymousClassObjectSourceLocator.php | 34 ++++++++++++ .../Type/ClosureSourceLocator.php | 31 +++++++++++ .../Type/Composer/PsrAutoloaderLocator.php | 20 ++++++- .../Type/DirectoriesSourceLocator.php | 17 +++++- .../Type/FileIteratorSourceLocator.php | 52 +++++++++++++------ .../Type/MemoizingSourceLocator.php | 31 +++++++++++ .../Type/SingleFileSourceLocator.php | 24 ++++++++- .../Type/SourceFilter/SourceFilter.php | 12 +++++ src/SourceLocator/Type/SourceLocator.php | 13 +++++ 11 files changed, 258 insertions(+), 21 deletions(-) create mode 100644 src/SourceLocator/Type/SourceFilter/SourceFilter.php diff --git a/src/SourceLocator/Type/AbstractSourceLocator.php b/src/SourceLocator/Type/AbstractSourceLocator.php index cfc2aadda..7876a20d5 100644 --- a/src/SourceLocator/Type/AbstractSourceLocator.php +++ b/src/SourceLocator/Type/AbstractSourceLocator.php @@ -4,6 +4,7 @@ namespace Roave\BetterReflection\SourceLocator\Type; +use Generator; use Roave\BetterReflection\Identifier\Identifier; use Roave\BetterReflection\Identifier\IdentifierType; use Roave\BetterReflection\Reflection\Reflection; @@ -12,6 +13,7 @@ use Roave\BetterReflection\SourceLocator\Ast\Exception\ParseToAstFailure; use Roave\BetterReflection\SourceLocator\Ast\Locator as AstLocator; use Roave\BetterReflection\SourceLocator\Located\LocatedSource; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter; abstract class AbstractSourceLocator implements SourceLocator { @@ -68,4 +70,31 @@ final public function locateIdentifiersByType(Reflector $reflector, IdentifierTy $identifierType, ); } + + /** + * {@inheritDoc} + * + * @throws ParseToAstFailure + */ + public function iterateIdentifiersByType( + Reflector $reflector, + IdentifierType $identifierType, + ?SourceFilter $sourceFilter, + ): Generator { + $locatedSource = $this->createLocatedSource(new Identifier(Identifier::WILDCARD, $identifierType)); + + if (!$locatedSource || $sourceFilter?->isAllowed( + $locatedSource->getSource(), + $locatedSource->getName(), + $locatedSource->getFileName(), + ) === false) { + return; + } + + yield from $this->astLocator->findReflectionsOfType( + $reflector, + $locatedSource, + $identifierType, + ); + } } diff --git a/src/SourceLocator/Type/AggregateSourceLocator.php b/src/SourceLocator/Type/AggregateSourceLocator.php index a5a760093..06a69590e 100644 --- a/src/SourceLocator/Type/AggregateSourceLocator.php +++ b/src/SourceLocator/Type/AggregateSourceLocator.php @@ -4,10 +4,12 @@ namespace Roave\BetterReflection\SourceLocator\Type; +use Generator; use Roave\BetterReflection\Identifier\Identifier; use Roave\BetterReflection\Identifier\IdentifierType; use Roave\BetterReflection\Reflection\Reflection; use Roave\BetterReflection\Reflector\Reflector; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter; use function array_map; use function array_merge; @@ -42,4 +44,18 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id ...array_map(static fn (SourceLocator $sourceLocator): array => $sourceLocator->locateIdentifiersByType($reflector, $identifierType), $this->sourceLocators), ); } + + /** + * {@inheritDoc} + */ + public function iterateIdentifiersByType( + Reflector $reflector, + IdentifierType $identifierType, + ?SourceFilter $sourceFilter, + ): Generator + { + foreach ($this->sourceLocators as $sourceLocator) { + yield from $sourceLocator->iterateIdentifiersByType($reflector, $identifierType, $sourceFilter); + } + } } diff --git a/src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php b/src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php index 23734964b..5a0746a81 100644 --- a/src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php +++ b/src/SourceLocator/Type/AnonymousClassObjectSourceLocator.php @@ -4,6 +4,7 @@ namespace Roave\BetterReflection\SourceLocator\Type; +use Generator; use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use PhpParser\NodeTraverser; @@ -23,6 +24,7 @@ use Roave\BetterReflection\SourceLocator\Exception\TwoAnonymousClassesOnSameLine; use Roave\BetterReflection\SourceLocator\FileChecker; use Roave\BetterReflection\SourceLocator\Located\AnonymousLocatedSource; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter; use Roave\BetterReflection\Util\FileHelper; use function array_filter; @@ -60,6 +62,38 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id return array_filter([$this->getReflectionClass($reflector, $identifierType)]); } + /** + * {@inheritDoc} + * + * @throws ParseToAstFailure + */ + public function iterateIdentifiersByType( + Reflector $reflector, + IdentifierType $identifierType, + ?SourceFilter $sourceFilter + ): Generator + { + $reflections = $this->locateIdentifiersByType($reflector, $identifierType); + if (! $reflections) { + return; + } + + foreach ($reflections as $reflection) { + $locatedSource = $reflection->getLocatedSource(); + $isAllowed = $sourceFilter?->isAllowed( + $locatedSource->getSource(), + $locatedSource->getName(), + $locatedSource->getFileName(), + ); + + if ($isAllowed === false) { + continue; + } + + yield $reflection; + } + } + private function getReflectionClass(Reflector $reflector, IdentifierType $identifierType): ReflectionClass|null { if (! $identifierType->isClass()) { diff --git a/src/SourceLocator/Type/ClosureSourceLocator.php b/src/SourceLocator/Type/ClosureSourceLocator.php index 74cc006f1..379aebc31 100644 --- a/src/SourceLocator/Type/ClosureSourceLocator.php +++ b/src/SourceLocator/Type/ClosureSourceLocator.php @@ -5,6 +5,7 @@ namespace Roave\BetterReflection\SourceLocator\Type; use Closure; +use Generator; use PhpParser\Node; use PhpParser\Node\Stmt\Namespace_; use PhpParser\NodeTraverser; @@ -24,6 +25,7 @@ use Roave\BetterReflection\SourceLocator\Exception\TwoClosuresOnSameLine; use Roave\BetterReflection\SourceLocator\FileChecker; use Roave\BetterReflection\SourceLocator\Located\AnonymousLocatedSource; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter; use Roave\BetterReflection\Util\FileHelper; use function array_filter; @@ -61,6 +63,35 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id return array_filter([$this->getReflectionFunction($reflector, $identifierType)]); } + /** + * {@inheritDoc} + * + * @throws ParseToAstFailure + */ + public function iterateIdentifiersByType( + Reflector $reflector, + IdentifierType $identifierType, + ?SourceFilter $sourceFilter + ): Generator { + $reflection = $this->getReflectionFunction($reflector, $identifierType); + if (! $reflection) { + return; + } + + $locatedSource = $reflection->getLocatedSource(); + $isAllowed = $sourceFilter?->isAllowed( + $locatedSource->getSource(), + $locatedSource->getName(), + $locatedSource->getFileName(), + ); + + if ($isAllowed === false) { + return ; + } + + yield $reflection; + } + private function getReflectionFunction(Reflector $reflector, IdentifierType $identifierType): ReflectionFunction|null { if (! $identifierType->isFunction()) { diff --git a/src/SourceLocator/Type/Composer/PsrAutoloaderLocator.php b/src/SourceLocator/Type/Composer/PsrAutoloaderLocator.php index ec6b97ca7..14c1e55fe 100644 --- a/src/SourceLocator/Type/Composer/PsrAutoloaderLocator.php +++ b/src/SourceLocator/Type/Composer/PsrAutoloaderLocator.php @@ -4,6 +4,7 @@ namespace Roave\BetterReflection\SourceLocator\Type\Composer; +use Generator; use Roave\BetterReflection\Identifier\Identifier; use Roave\BetterReflection\Identifier\IdentifierType; use Roave\BetterReflection\Reflection\Reflection; @@ -15,8 +16,8 @@ use Roave\BetterReflection\SourceLocator\Located\LocatedSource; use Roave\BetterReflection\SourceLocator\Type\Composer\Psr\PsrAutoloaderMapping; use Roave\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter; use Roave\BetterReflection\SourceLocator\Type\SourceLocator; - use function file_get_contents; final class PsrAutoloaderLocator implements SourceLocator @@ -63,4 +64,21 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id $this->astLocator, ))->locateIdentifiersByType($reflector, $identifierType); } + + /** + * Iterate filtered identifiers of a type + * + * @return Generator + */ + public function iterateIdentifiersByType( + Reflector $reflector, + IdentifierType $identifierType, + ?SourceFilter $sourceFilter, + ): Generator + { + return (new DirectoriesSourceLocator( + $this->mapping->directories(), + $this->astLocator, + ))->iterateIdentifiersByType($reflector, $identifierType, $sourceFilter); + } } diff --git a/src/SourceLocator/Type/DirectoriesSourceLocator.php b/src/SourceLocator/Type/DirectoriesSourceLocator.php index ff3256b26..8301dab3a 100644 --- a/src/SourceLocator/Type/DirectoriesSourceLocator.php +++ b/src/SourceLocator/Type/DirectoriesSourceLocator.php @@ -4,6 +4,8 @@ namespace Roave\BetterReflection\SourceLocator\Type; +use FilesystemIterator; +use Generator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use Roave\BetterReflection\Identifier\Identifier; @@ -13,6 +15,7 @@ use Roave\BetterReflection\SourceLocator\Ast\Locator; use Roave\BetterReflection\SourceLocator\Exception\InvalidDirectory; use Roave\BetterReflection\SourceLocator\Exception\InvalidFileInfo; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter; use function array_map; use function is_dir; @@ -41,7 +44,7 @@ static function (string $directory) use ($astLocator): FileIteratorSourceLocator return new FileIteratorSourceLocator( new RecursiveIteratorIterator(new RecursiveDirectoryIterator( $directory, - RecursiveDirectoryIterator::SKIP_DOTS, + FilesystemIterator::SKIP_DOTS, )), $astLocator, ); @@ -62,4 +65,16 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id { return $this->aggregateSourceLocator->locateIdentifiersByType($reflector, $identifierType); } + + /** + * {@inheritDoc} + */ + public function iterateIdentifiersByType( + Reflector $reflector, + IdentifierType $identifierType, + ?SourceFilter $sourceFilter, + ): Generator + { + return $this->aggregateSourceLocator->iterateIdentifiersByType($reflector, $identifierType, $sourceFilter); + } } diff --git a/src/SourceLocator/Type/FileIteratorSourceLocator.php b/src/SourceLocator/Type/FileIteratorSourceLocator.php index dcd9cc0f0..7991aa952 100644 --- a/src/SourceLocator/Type/FileIteratorSourceLocator.php +++ b/src/SourceLocator/Type/FileIteratorSourceLocator.php @@ -4,6 +4,7 @@ namespace Roave\BetterReflection\SourceLocator\Type; +use Generator; use Iterator; use Roave\BetterReflection\Identifier\Identifier; use Roave\BetterReflection\Identifier\IdentifierType; @@ -12,14 +13,10 @@ use Roave\BetterReflection\SourceLocator\Ast\Locator; use Roave\BetterReflection\SourceLocator\Exception\InvalidFileInfo; use Roave\BetterReflection\SourceLocator\Exception\InvalidFileLocation; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter; use SplFileInfo; -use function array_filter; -use function array_map; -use function array_values; -use function iterator_to_array; use function pathinfo; - use const PATHINFO_EXTENSION; /** @@ -27,6 +24,8 @@ */ class FileIteratorSourceLocator implements SourceLocator { + private const PHP_FILE_EXTENSION = 'php'; + private AggregateSourceLocator|null $aggregateSourceLocator = null; /** @var Iterator */ @@ -52,20 +51,22 @@ public function __construct(Iterator $fileInfoIterator, private Locator $astLoca /** @throws InvalidFileLocation */ private function getAggregatedSourceLocator(): AggregateSourceLocator { - // @infection-ignore-all Coalesce: There's no difference, it's just optimization - return $this->aggregateSourceLocator - ?? $this->aggregateSourceLocator = new AggregateSourceLocator(array_values(array_filter(array_map( - function (SplFileInfo $item): SingleFileSourceLocator|null { - $realPath = $item->getRealPath(); + if ($this->aggregateSourceLocator) { + return $this->aggregateSourceLocator; + } - if (! ($item->isFile() && pathinfo($realPath, PATHINFO_EXTENSION) === 'php')) { - return null; - } + $sourceLocators = []; + foreach ($this->fileSystemIterator as $fileInfo) { + $realPath = $fileInfo->getRealPath(); + if ( + $fileInfo->isFile() && + pathinfo($fileInfo->getRealPath(), PATHINFO_EXTENSION) === self::PHP_FILE_EXTENSION + ) { + $sourceLocators[] = new SingleFileSourceLocator($realPath, $this->astLocator); + } + } - return new SingleFileSourceLocator($realPath, $this->astLocator); - }, - iterator_to_array($this->fileSystemIterator), - )))); + return $this->aggregateSourceLocator = new AggregateSourceLocator($sourceLocators); } /** @@ -87,4 +88,21 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id { return $this->getAggregatedSourceLocator()->locateIdentifiersByType($reflector, $identifierType); } + + /** + * {@inheritDoc} + * + * @throws InvalidFileLocation + */ + public function iterateIdentifiersByType( + Reflector $reflector, + IdentifierType $identifierType, + ?SourceFilter $sourceFilter, + ): Generator { + return $this->getAggregatedSourceLocator()->iterateIdentifiersByType( + $reflector, + $identifierType, + $sourceFilter, + ); + } } diff --git a/src/SourceLocator/Type/MemoizingSourceLocator.php b/src/SourceLocator/Type/MemoizingSourceLocator.php index 8ac20e31a..ef45acbc9 100644 --- a/src/SourceLocator/Type/MemoizingSourceLocator.php +++ b/src/SourceLocator/Type/MemoizingSourceLocator.php @@ -4,10 +4,13 @@ namespace Roave\BetterReflection\SourceLocator\Type; +use Generator; +use Iterator; use Roave\BetterReflection\Identifier\Identifier; use Roave\BetterReflection\Identifier\IdentifierType; use Roave\BetterReflection\Reflection\Reflection; use Roave\BetterReflection\Reflector\Reflector; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter; use function array_key_exists; use function spl_object_id; @@ -21,6 +24,9 @@ final class MemoizingSourceLocator implements SourceLocator /** @var array> indexed by reflector key and identifier type cache key */ private array $cacheByIdentifierTypeKeyAndOid = []; + /** @var array> indexed by reflector key and identifier type cache key */ + private array $cacheIteratorByIdentifierTypeKeyAndOid = []; + public function __construct(private SourceLocator $wrappedSourceLocator) { } @@ -50,6 +56,31 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id = $this->wrappedSourceLocator->locateIdentifiersByType($reflector, $identifierType); } + /** @return Generator */ + public function iterateIdentifiersByType( + Reflector $reflector, + IdentifierType $identifierType, + ?SourceFilter $sourceFilter, + ): Generator { + $cacheKey = sprintf('%s_%s_%s', + $this->reflectorCacheKey($reflector), + $this->identifierTypeToCacheKey($identifierType), + $sourceFilter?->getKey() ?? '' + ); + + if (array_key_exists($cacheKey, $this->cacheIteratorByIdentifierTypeKeyAndOid)) { + yield from $this->cacheIteratorByIdentifierTypeKeyAndOid[$cacheKey]; + return; + } + + foreach ( + $this->wrappedSourceLocator->iterateIdentifiersByType($reflector, $identifierType, $sourceFilter) as $item + ) { + $this->cacheIteratorByIdentifierTypeKeyAndOid[$cacheKey][] = $item; + yield $item; + } + } + private function reflectorCacheKey(Reflector $reflector): string { return sprintf('type:%s#oid:%d', $reflector::class, spl_object_id($reflector)); diff --git a/src/SourceLocator/Type/SingleFileSourceLocator.php b/src/SourceLocator/Type/SingleFileSourceLocator.php index b0873b9f0..a1cd269bc 100644 --- a/src/SourceLocator/Type/SingleFileSourceLocator.php +++ b/src/SourceLocator/Type/SingleFileSourceLocator.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use Roave\BetterReflection\Identifier\Identifier; +use Roave\BetterReflection\Reflection\ReflectionClass; use Roave\BetterReflection\SourceLocator\Ast\Locator; use Roave\BetterReflection\SourceLocator\Exception\InvalidFileLocation; use Roave\BetterReflection\SourceLocator\FileChecker; @@ -43,9 +44,28 @@ public function __construct(private string $fileName, Locator $astLocator) */ protected function createLocatedSource(Identifier $identifier): LocatedSource|null { + $content = file_get_contents($this->fileName); + if (! $content) { + return null; + } + + $name = $identifier->getName(); + + if ( + $name !== Identifier::WILDCARD && + ! str_starts_with($name, ReflectionClass::ANONYMOUS_CLASS_NAME_PREFIX) + ) { + + $shortName = array_reverse(explode('\\', $identifier->getName()))[0]; + + if (! str_contains($content, $shortName)) { + return null; + } + } + return new LocatedSource( - file_get_contents($this->fileName), - $identifier->getName(), + $content, + $name, $this->fileName, ); } diff --git a/src/SourceLocator/Type/SourceFilter/SourceFilter.php b/src/SourceLocator/Type/SourceFilter/SourceFilter.php new file mode 100644 index 000000000..b7d6ff6bf --- /dev/null +++ b/src/SourceLocator/Type/SourceFilter/SourceFilter.php @@ -0,0 +1,12 @@ + */ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array; + + /** + * Iterate filtered identifiers of a type + * + * @return Generator + */ + public function iterateIdentifiersByType( + Reflector $reflector, + IdentifierType $identifierType, + ?SourceFilter $sourceFilter, + ): Generator; } From 2cd9196511abf679c384f6feae54b47dacc84313 Mon Sep 17 00:00:00 2001 From: Filipp Shcherbanich Date: Sat, 4 Jan 2025 23:35:19 +0500 Subject: [PATCH 02/10] Adding new method to iterate classes --- src/Reflector/DefaultReflector.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Reflector/DefaultReflector.php b/src/Reflector/DefaultReflector.php index 8fb250047..2f975286e 100644 --- a/src/Reflector/DefaultReflector.php +++ b/src/Reflector/DefaultReflector.php @@ -4,14 +4,15 @@ namespace Roave\BetterReflection\Reflector; +use Iterator; use Roave\BetterReflection\Identifier\Identifier; use Roave\BetterReflection\Identifier\IdentifierType; use Roave\BetterReflection\Reflection\ReflectionClass; use Roave\BetterReflection\Reflection\ReflectionConstant; use Roave\BetterReflection\Reflection\ReflectionFunction; use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter; use Roave\BetterReflection\SourceLocator\Type\SourceLocator; - use function assert; final class DefaultReflector implements Reflector @@ -56,6 +57,23 @@ public function reflectAllClasses(): iterable return $allClasses; } + /** + * Iterate classes available in the scope specified by the SourceLocator. + * + * @return Iterator + */ + public function iterateClasses(?SourceFilter $filter = null): Iterator + { + /** @var Iterator $allClasses */ + $allClasses = $this->sourceLocator->iterateIdentifiersByType( + $this, + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + $filter, + ); + + yield from $allClasses; + } + /** * Create a ReflectionFunction for the specified $functionName. * From a04eaaf8e80ae3bbcfbbda216afb8987f58ed33d Mon Sep 17 00:00:00 2001 From: Filipp Shcherbanich Date: Sat, 4 Jan 2025 23:35:43 +0500 Subject: [PATCH 03/10] Adding source filters --- .../Type/SourceFilter/AggregateFilter.php | 32 +++++++++++++++++++ .../Type/SourceFilter/FileSizeFilter.php | 22 +++++++++++++ .../SourceFilter/SourceContainsFilter.php | 28 ++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 src/SourceLocator/Type/SourceFilter/AggregateFilter.php create mode 100644 src/SourceLocator/Type/SourceFilter/FileSizeFilter.php create mode 100644 src/SourceLocator/Type/SourceFilter/SourceContainsFilter.php diff --git a/src/SourceLocator/Type/SourceFilter/AggregateFilter.php b/src/SourceLocator/Type/SourceFilter/AggregateFilter.php new file mode 100644 index 000000000..2a6b6e901 --- /dev/null +++ b/src/SourceLocator/Type/SourceFilter/AggregateFilter.php @@ -0,0 +1,32 @@ +filters = $filters; + } + + public function getKey(): string + { + $keys = array_map(static fn(SourceFilter $filter) => $filter->getKey(), $this->filters); + + return 'group_' . md5(serialize($keys)); + } + + public function isAllowed(string $source, ?string $name, ?string $filename = null): bool + { + foreach ($this->filters as $filter) { + if (!$filter->isAllowed($source, $name, $filename)) { + return false; + } + } + return true; + } +} diff --git a/src/SourceLocator/Type/SourceFilter/FileSizeFilter.php b/src/SourceLocator/Type/SourceFilter/FileSizeFilter.php new file mode 100644 index 000000000..8d9f41ddb --- /dev/null +++ b/src/SourceLocator/Type/SourceFilter/FileSizeFilter.php @@ -0,0 +1,22 @@ +maxFileSize}"; + } + + public function isAllowed(string $source, ?string $name, ?string $filename = null): bool + { + return mb_strlen($source) <= $this->maxFileSize; + } +} diff --git a/src/SourceLocator/Type/SourceFilter/SourceContainsFilter.php b/src/SourceLocator/Type/SourceFilter/SourceContainsFilter.php new file mode 100644 index 000000000..6c8340a72 --- /dev/null +++ b/src/SourceLocator/Type/SourceFilter/SourceContainsFilter.php @@ -0,0 +1,28 @@ +substrings)); + } + + public function isAllowed(string $source, ?string $name, ?string $filename = null): bool + { + foreach ($this->substrings as $substring) { + if (str_contains($source, $substring)) { + return true; + } + } + return false; + } +} From 348d53abc49955b58cbc5f013edaecb3a1efe673 Mon Sep 17 00:00:00 2001 From: Filipp Shcherbanich Date: Sat, 4 Jan 2025 23:36:06 +0500 Subject: [PATCH 04/10] Adding new examples --- demo/parsing-whole-directory/example3.php | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 demo/parsing-whole-directory/example3.php diff --git a/demo/parsing-whole-directory/example3.php b/demo/parsing-whole-directory/example3.php new file mode 100644 index 000000000..cd43419e9 --- /dev/null +++ b/demo/parsing-whole-directory/example3.php @@ -0,0 +1,37 @@ +astLocator() + ), +]); + +$reflector = new DefaultReflector($sourceLocator); + +$classReflections = $reflector->iterateClasses( + new AggregateFilter( + new FileSizeFilter(10000), + new SourceContainsFilter(['class ReflectionMethod', 'class ReflectionClass']) + ), +); + +print iterator_count($classReflections); + +$classReflections = $reflector->iterateClasses(new SourceContainsFilter(['class MyClass'])); + +print iterator_count($classReflections); From 27361f3a48e16fb50f5008ebb9e789f7921745e1 Mon Sep 17 00:00:00 2001 From: Filipp Shcherbanich Date: Sat, 4 Jan 2025 23:36:19 +0500 Subject: [PATCH 05/10] Adding source filters tests --- .../Type/SourceFilter/AggregateFilterTest.php | 133 ++++++++++++++++++ .../Type/SourceFilter/FileSizeFilterTest.php | 72 ++++++++++ .../SourceFilter/SourceContainsFilterTest.php | 81 +++++++++++ 3 files changed, 286 insertions(+) create mode 100644 test/unit/SourceLocator/Type/SourceFilter/AggregateFilterTest.php create mode 100644 test/unit/SourceLocator/Type/SourceFilter/FileSizeFilterTest.php create mode 100644 test/unit/SourceLocator/Type/SourceFilter/SourceContainsFilterTest.php diff --git a/test/unit/SourceLocator/Type/SourceFilter/AggregateFilterTest.php b/test/unit/SourceLocator/Type/SourceFilter/AggregateFilterTest.php new file mode 100644 index 000000000..ff5064708 --- /dev/null +++ b/test/unit/SourceLocator/Type/SourceFilter/AggregateFilterTest.php @@ -0,0 +1,133 @@ +subFilterKey; + } + + public function isAllowed(string $source, ?string $name, ?string $filename = null): bool + { + return true; + } + }; + } + + $filter = new AggregateFilter(...$subFilters); + + $this->assertSame($expected, $filter->getKey()); + } + + public static function getKeyProvider(): array + { + return [ + 'Without sub filters' => [ + 'subFiltersKeys' => [], + 'expected' => 'group_40cd750bba9870f18aada2478b24840a', + ], + 'With sub filters case 1' => [ + 'subFiltersKeys' => [ + '111', + ], + 'expected' => 'group_a4628f8734c9d9f1c0af618f5f2e9109', + ], + 'With sub filters case 2' => [ + 'subFiltersKeys' => [ + '111', + '333', + 'def11', + ], + 'expected' => 'group_93519df5c4a8d75f8fc858f126563cb7', + ], + ]; + } + + #[DataProvider('isAllowedProvider')] + public function testIsAllowed( + array $results, + bool $expected, + ): void { + + $source = 'testSource'; + $name = 'testName'; + $fileName = 'testFileName'; + + $subFilters = []; + foreach ($results as $result) { + + $sourceFilter = $this->createMock(SourceFilter::class); + $sourceFilter + ->method('isAllowed') + ->with($source, $name, $fileName) + ->willReturn($result); + + $subFilters[] = $sourceFilter; + } + + $filter = new AggregateFilter(...$subFilters); + + $this->assertSame($expected, $filter->isAllowed($source, $name, $fileName)); + } + + public static function isAllowedProvider(): array + { + return [ + 'Without sub filters' => [ + 'results' => [], + 'expected' => true, + ], + 'With one false sub filter' => [ + 'results' => [false], + 'expected' => false, + ], + 'With different filters case 1' => [ + 'results' => [true, false], + 'expected' => false, + ], + 'With different filters case 2' => [ + 'results' => [true, false, false], + 'expected' => false, + ], + 'With true filter' => [ + 'results' => [true], + 'expected' => true, + ], + 'With same filters case 1' => [ + 'results' => [true, true], + 'expected' => true, + ], + 'With same filters case 2' => [ + 'results' => [false, false], + 'expected' => false, + ], + ]; + } +} diff --git a/test/unit/SourceLocator/Type/SourceFilter/FileSizeFilterTest.php b/test/unit/SourceLocator/Type/SourceFilter/FileSizeFilterTest.php new file mode 100644 index 000000000..0e3b8c3b7 --- /dev/null +++ b/test/unit/SourceLocator/Type/SourceFilter/FileSizeFilterTest.php @@ -0,0 +1,72 @@ +assertSame($expected, $filter->getKey()); + } + + public static function getKeyProvider(): array + { + return [ + 'Key 1' => [ + 'fileSize' => 0, + 'expected' => 'fileSize_0', + ], + 'Key 2' => [ + 'fileSize' => 19324, + 'expected' => 'fileSize_19324', + ], + ]; + } + + #[DataProvider('isAllowedProvider')] + public function testIsAllowed( + int $maxFileSize, + string $source, + bool $expected, + ): void { + + $filter = new FileSizeFilter($maxFileSize); + + $this->assertSame($expected, $filter->isAllowed($source, null)); + } + + public static function isAllowedProvider(): array + { + return [ + 'Allowed size' => [ + 'maxFileSize' => 10, + 'source' => 'qqq', + 'expected' => true, + ], + 'Allowed size (max size)' => [ + 'maxFileSize' => 4, + 'source' => 'qqqq', + 'expected' => true, + ], + 'Not allowed size' => [ + 'maxFileSize' => 4, + 'source' => 'qqqqq', + 'expected' => false, + ], + ]; + } +} diff --git a/test/unit/SourceLocator/Type/SourceFilter/SourceContainsFilterTest.php b/test/unit/SourceLocator/Type/SourceFilter/SourceContainsFilterTest.php new file mode 100644 index 000000000..162ad9b6c --- /dev/null +++ b/test/unit/SourceLocator/Type/SourceFilter/SourceContainsFilterTest.php @@ -0,0 +1,81 @@ +assertSame($expected, $filter->getKey()); + } + + public static function getKeyProvider(): array + { + return [ + 'Without substrings' => [ + 'substrings' => [], + 'expected' => 'sourceContains_40cd750bba9870f18aada2478b24840a', + ], + 'With one substring' => [ + 'substrings' => ['eff'], + 'expected' => 'sourceContains_188297be5710cb65088c823bb6909954', + ], + 'With several substring' => [ + 'substrings' => ['eff', '333', 'gtre'], + 'expected' => 'sourceContains_c4f41f83316c44c3b24385808eadc245', + ], + ]; + } + + #[DataProvider('isAllowedProvider')] + public function testIsAllowed( + array $substrings, + string $source, + bool $expected, + ): void { + + $filter = new SourceContainsFilter($substrings); + + $this->assertSame($expected, $filter->isAllowed($source, null)); + } + + public static function isAllowedProvider(): array + { + return [ + 'Without substrings' => [ + 'substrings' => [], + 'source' => 'qqq', + 'expected' => false, + ], + 'With substring' => [ + 'substrings' => ['qq'], + 'source' => 'qqq', + 'expected' => true, + ], + 'With several substring. The source contains one of them' => [ + 'substrings' => ['cc', 'aaa', 'bb'], + 'source' => 'qqq bb cc', + 'expected' => true, + ], + 'With several substring. The source does not contain them' => [ + 'substrings' => ['11', 'aaa', '22'], + 'source' => 'qqq bb cc', + 'expected' => false, + ], + ]; + } +} From fa0920bc9098b76ad6fcba00caa5507dc3609fb4 Mon Sep 17 00:00:00 2001 From: Filipp Shcherbanich Date: Sat, 4 Jan 2025 23:36:37 +0500 Subject: [PATCH 06/10] Adding new test cases for source locators --- .../Type/AbstractSourceLocatorTest.php | 116 ++++++++++++++++++ .../Type/AggregateSourceLocatorTest.php | 38 ++++++ .../AnonymousClassObjectSourceLocatorTest.php | 15 +++ .../Type/ClosureSourceLocatorTest.php | 67 ++++++++++ .../Type/DirectoriesSourceLocatorTest.php | 45 +++++++ .../Type/FileIteratorSourceLocatorTest.php | 43 +++++++ .../Type/MemoizingSourceLocatorTest.php | 77 ++++++++++++ 7 files changed, 401 insertions(+) diff --git a/test/unit/SourceLocator/Type/AbstractSourceLocatorTest.php b/test/unit/SourceLocator/Type/AbstractSourceLocatorTest.php index a7a913ae0..f56a15dbe 100644 --- a/test/unit/SourceLocator/Type/AbstractSourceLocatorTest.php +++ b/test/unit/SourceLocator/Type/AbstractSourceLocatorTest.php @@ -14,6 +14,7 @@ use Roave\BetterReflection\SourceLocator\Ast\Locator as AstLocator; use Roave\BetterReflection\SourceLocator\Located\LocatedSource; use Roave\BetterReflection\SourceLocator\Type\AbstractSourceLocator; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceContainsFilter; #[CoversClass(AbstractSourceLocator::class)] class AbstractSourceLocatorTest extends TestCase @@ -129,6 +130,93 @@ public function testLocateIdentifiersByTypeCallsFindReflectionsOfType(): void self::assertSame([$mockReflection], $sourceLocator->locateIdentifiersByType($mockReflector, $identifierType)); } + public function testIterateIdentifiersByTypeCallsFindReflectionsOfType(): void + { + $mockReflector = $this->createMock(Reflector::class); + + $locatedSource = new LocatedSource('createMock(ReflectionClass::class); + + $astLocator = $this->createMock(AstLocator::class); + + $astLocator->expects($this->once()) + ->method('findReflectionsOfType') + ->with($mockReflector, $locatedSource, $identifierType) + ->willReturn([$mockReflection]); + + $sourceLocator = $this->getMockBuilder(AbstractSourceLocator::class) + ->setConstructorArgs([$astLocator]) + ->onlyMethods(['createLocatedSource']) + ->getMock(); + + $sourceLocator->expects($this->once()) + ->method('createLocatedSource') + ->willReturn($locatedSource); + + $result = []; + foreach ($sourceLocator->iterateIdentifiersByType($mockReflector, $identifierType, null) as $identifier) { + $result[] = $identifier; + } + + self::assertSame([$mockReflection], $result); + } + + public function testIterateIdentifiersByTypeWithFilter(): void + { + $mockReflector = $this->createMock(Reflector::class); + + $locatedSource = new LocatedSource('createMock(ReflectionClass::class); + + $astLocator = $this->createMock(AstLocator::class); + + $astLocator->expects($this->once()) + ->method('findReflectionsOfType') + ->with($mockReflector, $locatedSource, $identifierType) + ->willReturn([$mockReflection]); + + $sourceLocator = $this->getMockBuilder(AbstractSourceLocator::class) + ->setConstructorArgs([$astLocator]) + ->onlyMethods(['createLocatedSource']) + ->getMock(); + + $sourceLocator->expects($this->exactly(2)) + ->method('createLocatedSource') + ->willReturn($locatedSource); + + $result = []; + foreach ( + $sourceLocator->iterateIdentifiersByType( + $mockReflector, + $identifierType, + new SourceContainsFilter(['1111']), + ) as $identifier + ) { + $result[] = $identifier; + } + + self::assertSame([], $result); + + $result = []; + foreach ( + $sourceLocator->iterateIdentifiersByType( + $mockReflector, + $identifierType, + new SourceContainsFilter(['Foo']), + ) as $identifier + ) { + $result[] = $identifier; + } + + self::assertSame([$mockReflection], $result); + } + public function testLocateIdentifiersByTypeReturnsEmptyArrayWithoutTryingToFindReflectionsWhenUnableToLocateSource(): void { $mockReflector = $this->createMock(Reflector::class); @@ -151,4 +239,32 @@ public function testLocateIdentifiersByTypeReturnsEmptyArrayWithoutTryingToFindR self::assertSame([], $sourceLocator->locateIdentifiersByType($mockReflector, $identifierType)); } + + public function testIterateIdentifiersByTypeReturnsEmptyArrayWithoutTryingToFindReflectionsWhenUnableToLocateSource(): void + { + $mockReflector = $this->createMock(Reflector::class); + + $identifierType = new IdentifierType(IdentifierType::IDENTIFIER_CLASS); + + $astLocator = $this->createMock(AstLocator::class); + + $astLocator->expects($this->never()) + ->method('findReflectionsOfType'); + + $sourceLocator = $this->getMockBuilder(AbstractSourceLocator::class) + ->setConstructorArgs([$astLocator]) + ->onlyMethods(['createLocatedSource']) + ->getMock(); + + $sourceLocator->expects($this->once()) + ->method('createLocatedSource') + ->willReturn(null); + + $result = []; + foreach ($sourceLocator->iterateIdentifiersByType($mockReflector, $identifierType, null) as $identifier) { + $result[] = $identifier; + } + + self::assertSame([], $result); + } } diff --git a/test/unit/SourceLocator/Type/AggregateSourceLocatorTest.php b/test/unit/SourceLocator/Type/AggregateSourceLocatorTest.php index 267cbd55d..0d1ed65c2 100644 --- a/test/unit/SourceLocator/Type/AggregateSourceLocatorTest.php +++ b/test/unit/SourceLocator/Type/AggregateSourceLocatorTest.php @@ -125,4 +125,42 @@ public function testLocateIdentifiersByTypeAggregatesSource(): void ]))->locateIdentifiersByType($this->getMockReflector(), $identifierType), ); } + + public function testIterateIdentifiersByTypeAggregatesSource(): void + { + $identifierType = new IdentifierType(); + + $locator1 = $this->createMock(SourceLocator::class); + $locator2 = $this->createMock(SourceLocator::class); + $locator3 = $this->createMock(SourceLocator::class); + $locator4 = $this->createMock(SourceLocator::class); + + $source2 = $this->createMock(ReflectionClass::class); + + $source3 = $this->createMock(ReflectionClass::class); + + $locator1->expects($this->once())->method('iterateIdentifiersByType')->willReturnCallback(fn() => yield from []); + $locator2->expects($this->once())->method('iterateIdentifiersByType')->willReturnCallback(fn() => yield $source2); + $locator3->expects($this->once())->method('iterateIdentifiersByType')->willReturnCallback(fn() => yield $source3); + $locator4->expects($this->once())->method('iterateIdentifiersByType')->willReturnCallback(fn() => yield from []); + + $locator = (new AggregateSourceLocator([ + $locator1, + $locator2, + $locator3, + $locator4, + ])); + + $generator = $locator->iterateIdentifiersByType($this->getMockReflector(), $identifierType, null); + + $result = []; + foreach ($generator as $identifier) { + $result[] = $identifier; + } + + self::assertSame( + [$source2, $source3], + $result, + ); + } } diff --git a/test/unit/SourceLocator/Type/AnonymousClassObjectSourceLocatorTest.php b/test/unit/SourceLocator/Type/AnonymousClassObjectSourceLocatorTest.php index cdc9f1a04..fb4be83e9 100644 --- a/test/unit/SourceLocator/Type/AnonymousClassObjectSourceLocatorTest.php +++ b/test/unit/SourceLocator/Type/AnonymousClassObjectSourceLocatorTest.php @@ -144,6 +144,21 @@ public function testLocateIdentifiersByTypeWithFunctionIdentifier(): void self::assertCount(0, $reflections); } + public function testIterateIdentifiersByTypeWithFunctionIdentifier(): void + { + $anonymousClass = new class { + }; + + /** @var list $reflections */ + $reflections = (new AnonymousClassObjectSourceLocator($anonymousClass, $this->parser))->iterateIdentifiersByType( + $this->reflector, + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + null, + ); + + self::assertSame(0, iterator_count($reflections)); + } + public function testExceptionIfAnonymousClassNotFoundOnExpectedLine(): void { $anonymousClass = new class { diff --git a/test/unit/SourceLocator/Type/ClosureSourceLocatorTest.php b/test/unit/SourceLocator/Type/ClosureSourceLocatorTest.php index 7e2d31e54..08dae10e5 100644 --- a/test/unit/SourceLocator/Type/ClosureSourceLocatorTest.php +++ b/test/unit/SourceLocator/Type/ClosureSourceLocatorTest.php @@ -5,6 +5,7 @@ namespace Roave\BetterReflectionTest\SourceLocator\Type; use Closure; +use Generator; use PhpParser\Parser; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; @@ -21,6 +22,7 @@ use Roave\BetterReflection\SourceLocator\Exception\NoClosureOnLine; use Roave\BetterReflection\SourceLocator\Exception\TwoClosuresOnSameLine; use Roave\BetterReflection\SourceLocator\Type\ClosureSourceLocator; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceContainsFilter; use Roave\BetterReflection\Util\FileHelper; use Roave\BetterReflectionTest\BetterReflectionSingleton; @@ -123,6 +125,56 @@ public function testLocateIdentifiersByType(Closure $closure, string|null $names self::assertStringContainsString('Hello world!', $reflections[0]->getLocatedSource()->getSource()); } + #[DataProvider('closuresProvider')] + public function testIterateIdentifiersByType(Closure $closure, string|null $namespace, string $file, int $startLine, int $endLine): void + { + /** @var Generator $generator */ + $generator = (new ClosureSourceLocator($closure, $this->parser))->iterateIdentifiersByType( + $this->reflector, + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + null, + ); + + $reflections = []; + foreach ($generator as $reflection) { + $reflections[] = $reflection; + } + + self::assertCount(1, $reflections); + self::assertArrayHasKey(0, $reflections); + + self::assertTrue($reflections[0]->isClosure()); + self::assertSame(ReflectionFunction::CLOSURE_NAME, $reflections[0]->getShortName()); + self::assertSame($namespace, $reflections[0]->getNamespaceName()); + self::assertSame($file, $reflections[0]->getFileName()); + self::assertSame($startLine, $reflections[0]->getStartLine()); + self::assertSame($endLine, $reflections[0]->getEndLine()); + self::assertStringContainsString('Hello world!', $reflections[0]->getLocatedSource()->getSource()); + } + + public function testIterateIdentifiersByTypeWithFilter(): void + { + $closure = require FileHelper::normalizeWindowsPath(self::realPath(__DIR__ . '/../../Fixture/ClosureInNamespace.php')); + + /** @var Generator $generator */ + $generator = (new ClosureSourceLocator($closure, $this->parser))->iterateIdentifiersByType( + $this->reflector, + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + new SourceContainsFilter(['Hello world!']), + ); + + self::assertSame(1, iterator_count($generator)); + + /** @var Generator $generator2 */ + $generator2 = (new ClosureSourceLocator($closure, $this->parser))->iterateIdentifiersByType( + $this->reflector, + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + new SourceContainsFilter(['iiiii']), + ); + + self::assertSame(0, iterator_count($generator2)); + } + public function testExceptionIfClosureNotFoundOnExpectedLine(): void { $closure = static function (): void { @@ -163,6 +215,21 @@ public function testLocateIdentifiersByTypeWithClassIdentifier(): void self::assertCount(0, $reflections); } + public function testIterateIdentifiersByTypeWithClassIdentifier(): void + { + $closure = static function (): void { + }; + + /** @var list $reflections */ + $reflections = (new ClosureSourceLocator($closure, $this->parser))->iterateIdentifiersByType( + $this->reflector, + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + null, + ); + + self::assertSame(0, iterator_count($reflections)); + } + /** @return list */ public static function exceptionIfTwoClosuresOnSameLineProvider(): array { diff --git a/test/unit/SourceLocator/Type/DirectoriesSourceLocatorTest.php b/test/unit/SourceLocator/Type/DirectoriesSourceLocatorTest.php index 934832c19..29c0beb65 100644 --- a/test/unit/SourceLocator/Type/DirectoriesSourceLocatorTest.php +++ b/test/unit/SourceLocator/Type/DirectoriesSourceLocatorTest.php @@ -4,6 +4,7 @@ namespace Roave\BetterReflectionTest\SourceLocator\Type; +use Generator; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -13,6 +14,7 @@ use Roave\BetterReflection\Reflector\DefaultReflector; use Roave\BetterReflection\SourceLocator\Exception\InvalidDirectory; use Roave\BetterReflection\SourceLocator\Type\DirectoriesSourceLocator; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceContainsFilter; use Roave\BetterReflectionTest\Assets\DirectoryScannerAssets; use Roave\BetterReflectionTest\Assets\DirectoryScannerAssetsFoo; use Roave\BetterReflectionTest\BetterReflectionSingleton; @@ -62,6 +64,49 @@ public function testScanDirectoryClasses(): void self::assertEquals(DirectoryScannerAssets\Foo::class, $classNames[3]); } + #[DataProvider('iterateClassesProvider')] + public function testIterateClasses( + array $filter, + array $expected, + ): void + { + /** @var Generator $generator */ + $generator = $this->sourceLocator->iterateIdentifiersByType( + new DefaultReflector($this->sourceLocator), + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + $filter ? new SourceContainsFilter($filter) : null, + ); + + $classes = []; + foreach ($generator as $class) { + $classes[] = $class->getName(); + } + + $this->assertSame($expected, $classes); + } + + public static function iterateClassesProvider(): array + { + return [ + 'Without filter' => [ + 'filter' => [], + 'expected' => [ + DirectoryScannerAssets\Bar\FooBar::class, + DirectoryScannerAssets\Foo::class, + DirectoryScannerAssetsFoo\Bar\FooBar::class, + DirectoryScannerAssetsFoo\Foo::class + ], + ], + 'With filter' => [ + 'filter' => ['FooBar'], + 'expected' => [ + DirectoryScannerAssets\Bar\FooBar::class, + DirectoryScannerAssetsFoo\Bar\FooBar::class, + ], + ] + ]; + } + public function testLocateIdentifier(): void { $class = $this->sourceLocator->locateIdentifier( diff --git a/test/unit/SourceLocator/Type/FileIteratorSourceLocatorTest.php b/test/unit/SourceLocator/Type/FileIteratorSourceLocatorTest.php index a13ab201b..5ef9b5f5d 100644 --- a/test/unit/SourceLocator/Type/FileIteratorSourceLocatorTest.php +++ b/test/unit/SourceLocator/Type/FileIteratorSourceLocatorTest.php @@ -5,7 +5,9 @@ namespace Roave\BetterReflectionTest\SourceLocator\Type; use ArrayIterator; +use Generator; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -15,6 +17,7 @@ use Roave\BetterReflection\Reflector\DefaultReflector; use Roave\BetterReflection\SourceLocator\Exception\InvalidFileInfo; use Roave\BetterReflection\SourceLocator\Type\FileIteratorSourceLocator; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceContainsFilter; use Roave\BetterReflectionTest\Assets\DirectoryScannerAssets; use Roave\BetterReflectionTest\BetterReflectionSingleton; use stdClass; @@ -61,6 +64,46 @@ public function testScanDirectoryClasses(): void self::assertEquals(DirectoryScannerAssets\Foo::class, $classNames[1]); } + #[DataProvider('iterateClassesProvider')] + public function testIterateClasses( + array $filter, + array $expected, + ): void + { + /** @var Generator $generator */ + $generator = $this->sourceLocator->iterateIdentifiersByType( + new DefaultReflector($this->sourceLocator), + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + $filter ? new SourceContainsFilter($filter) : null, + ); + + $classes = []; + foreach ($generator as $class) { + $classes[] = $class->getName(); + } + + $this->assertSame($expected, $classes); + } + + public static function iterateClassesProvider(): array + { + return [ + 'Without filter' => [ + 'filter' => [], + 'expected' => [ + DirectoryScannerAssets\Bar\FooBar::class, + DirectoryScannerAssets\Foo::class, + ], + ], + 'With filter' => [ + 'filter' => ['FooBar'], + 'expected' => [ + DirectoryScannerAssets\Bar\FooBar::class, + ], + ] + ]; + } + public function testLocateIdentifier(): void { $class = $this->sourceLocator->locateIdentifier( diff --git a/test/unit/SourceLocator/Type/MemoizingSourceLocatorTest.php b/test/unit/SourceLocator/Type/MemoizingSourceLocatorTest.php index 6263ee2cd..7061c2a89 100644 --- a/test/unit/SourceLocator/Type/MemoizingSourceLocatorTest.php +++ b/test/unit/SourceLocator/Type/MemoizingSourceLocatorTest.php @@ -4,6 +4,8 @@ namespace Roave\BetterReflectionTest\SourceLocator\Type; +use Generator; +use Iterator; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -12,6 +14,7 @@ use Roave\BetterReflection\Reflection\Reflection; use Roave\BetterReflection\Reflector\Reflector; use Roave\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; +use Roave\BetterReflection\SourceLocator\Type\SourceFilter\SourceFilter; use Roave\BetterReflection\SourceLocator\Type\SourceLocator; use function array_filter; @@ -164,6 +167,80 @@ public function testMemoizationByTypeDistinguishesBetweenSourceLocatorsAndType() } } + public function testMemoizationIterator(): void + { + $types = [ + new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION), + new IdentifierType(IdentifierType::IDENTIFIER_CLASS), + ]; + $symbols1 = [ + IdentifierType::IDENTIFIER_FUNCTION => [$this->createMock(Reflection::class)], + IdentifierType::IDENTIFIER_CLASS => [$this->createMock(Reflection::class)], + ]; + $symbols2 = [ + IdentifierType::IDENTIFIER_FUNCTION => [$this->createMock(Reflection::class)], + IdentifierType::IDENTIFIER_CLASS => [$this->createMock(Reflection::class)], + ]; + + $this + ->wrappedLocator + ->expects($this->exactly(6)) + ->method('iterateIdentifiersByType') + ->with(self::logicalOr($this->reflector1, $this->reflector2)) + ->willReturnCallback(function ( + Reflector $reflector, + IdentifierType $identifierType, + ) use ( + $symbols1, + $symbols2, + ): Generator { + if ($reflector === $this->reflector1) { + yield from $symbols1[$identifierType->getName()]; + return; + } + + yield from $symbols2[$identifierType->getName()]; + }); + + $sourceFilterMock = $this->createMock(SourceFilter::class); + $sourceFilterMock->expects($this->exactly(4))->method('getKey')->willReturn('test'); + + foreach ($types as $type) { + $iterator1 = $this->memoizingLocator->iterateIdentifiersByType($this->reflector1, $type, null); + self::assertSame( + $symbols1[$type->getName()], + iterator_to_array($iterator1), + ); + $iterator2 = $this->memoizingLocator->iterateIdentifiersByType($this->reflector2, $type, null); + self::assertSame( + $symbols2[$type->getName()], + iterator_to_array($iterator2), + ); + $iterator3 = $this->memoizingLocator->iterateIdentifiersByType($this->reflector2, $type, $sourceFilterMock); + self::assertSame( + $symbols2[$type->getName()], + iterator_to_array($iterator3), + ); + + // second execution - ensures that memoization is in place + $iterator1 = $this->memoizingLocator->iterateIdentifiersByType($this->reflector1, $type, null); + self::assertSame( + $symbols1[$type->getName()], + iterator_to_array($iterator1), + ); + $iterator2 = $this->memoizingLocator->iterateIdentifiersByType($this->reflector2, $type, null); + self::assertSame( + $symbols2[$type->getName()], + iterator_to_array($iterator2), + ); + $iterator3 = $this->memoizingLocator->iterateIdentifiersByType($this->reflector2, $type, $sourceFilterMock); + self::assertSame( + $symbols2[$type->getName()], + iterator_to_array($iterator3), + ); + } + } + /** * @param list $identifiers * @param list $reflectors From a20df62feb08d346c25f5c0e1ccd015d4ce6711c Mon Sep 17 00:00:00 2001 From: Filipp Shcherbanich Date: Sat, 4 Jan 2025 23:39:19 +0500 Subject: [PATCH 07/10] Check new demo --- test/demo/check-demo.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/test/demo/check-demo.sh b/test/demo/check-demo.sh index d7e4bff9b..d72707478 100755 --- a/test/demo/check-demo.sh +++ b/test/demo/check-demo.sh @@ -20,3 +20,4 @@ check demo/basic-reflection/example2.php $'Roave\BetterReflection\Reflection\Ref check demo/basic-reflection/example3.php $'MyClass\nprivate' check demo/parsing-whole-directory/example1.php $'success' check demo/parsing-whole-directory/example2.php $'success' +check demo/parsing-whole-directory/example3.php $'51' From 595328db1de209300ac79c5cc6a44bfca0b17d01 Mon Sep 17 00:00:00 2001 From: Filipp Shcherbanich Date: Sat, 4 Jan 2025 23:47:07 +0500 Subject: [PATCH 08/10] Adding doc --- docs/usage.md | 59 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index d6466d412..b6c8109a9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,16 +1,16 @@ # Basic Usage -The starting point for creating a reflection class does not match the typical core reflection API. Instead of -instantiating a `new \ReflectionClass`, you must use the appropriate `\Roave\BetterReflection\Reflector\Reflector` +The starting point for creating a reflection class does not match the typical core reflection API. Instead of +instantiating a `new \ReflectionClass`, you must use the appropriate `\Roave\BetterReflection\Reflector\Reflector` helper. All `*Reflector` classes require a class that implements the `SourceLocator` interface as a dependency. ## Basic Reflection -Better Reflection is, in most cases, able to automatically reflect on classes by using a similar creation technique to +Better Reflection is, in most cases, able to automatically reflect on classes by using a similar creation technique to PHP's internal reflection. However, this works on the basic assumption that whichever autoloader you are using will -attempt to load a file, and only one file, which should contain the class you are trying to reflect. For example, the +attempt to load a file, and only one file, which should contain the class you are trying to reflect. For example, the autoloader that Composer provides will work with this technique. ```php @@ -23,10 +23,10 @@ $classInfo = (new BetterReflection) ->reflectClass(\Foo\Bar\MyClass::class); ``` -If this instantiation technique is not possible - for example, your autoloader does not load classes from file, then +If this instantiation technique is not possible - for example, your autoloader does not load classes from file, then you *must* use `SourceLocator` creation. -> Fun fact... using the method described above actually uses a SourceLocator under the hood - it uses the +> Fun fact... using the method described above actually uses a SourceLocator under the hood - it uses the `AutoloadSourceLocator`. ### Initialisers @@ -60,20 +60,20 @@ ReflectionProperty::createFromInstance(new \ReflectionClass(\stdClass::class), ' ## SourceLocators -Source locators are helpers that identify how to load code that can be used within the `Reflector`s. The library comes +Source locators are helpers that identify how to load code that can be used within the `Reflector`s. The library comes bundled with the following `SourceLocator` classes: - * `ComposerSourceLocator` - you'll probably use this most of the time. This uses Composer's built-in autoloader to + * `ComposerSourceLocator` - you'll probably use this most of the time. This uses Composer's built-in autoloader to locate a class and return the source. - + * `SingleFileSourceLocator` - this locator loads the filename specified in the constructor. - - * `StringSourceLocator` - pass a string as a constructor argument which will be used directly. Note that any + + * `StringSourceLocator` - pass a string as a constructor argument which will be used directly. Note that any references to filenames when using this locator will be `null` because no files are loaded. - * `AutoloadSourceLocator` - this is a little hacky, but works on the assumption that when a registered autoloader + * `AutoloadSourceLocator` - this is a little hacky, but works on the assumption that when a registered autoloader identifies a file and attempts to open it, then that file will contain the class. Internally, it works by overriding - the `file://` protocol stream wrapper to grab the path of the file the autoloader is trying to locate. This source + the `file://` protocol stream wrapper to grab the path of the file the autoloader is trying to locate. This source locator is used internally by the `ReflectionClass::createFromName` static constructor. * `EvaledCodeSourceLocator` - used to perform reflection on code that is already loaded into memory using `eval()` @@ -84,7 +84,7 @@ bundled with the following `SourceLocator` classes: * `ClosureSourceLocator` - used to perform reflection on a closure. - * `AggregateSourceLocator` - a combination of multiple `SourceLocator`s which are hunted through in the given order to + * `AggregateSourceLocator` - a combination of multiple `SourceLocator`s which are hunted through in the given order to locate the source. * `FileIteratorSourceLocator` - iterates all files in a given iterator containing `SplFileInfo` instances. @@ -94,7 +94,7 @@ bundled with the following `SourceLocator` classes: A `SourceLocator` is a callable, which when invoked must be given an `Identifier` (which describes a class/function/etc) . The `SourceLocator` should be written so that it returns a `Reflection` object directly. -> Note that using `EvaledCodeSourceLocator` and `PhpInternalSourceLocator` will result in specific types of +> Note that using `EvaledCodeSourceLocator` and `PhpInternalSourceLocator` will result in specific types of `LocatedSource` within the reflection - namely `EvaledLocatedSource` and `InternalLocatedSource` respectively. > Note that if you use a locator other than the default and the class you want to reflect extends a built-in PHP class (e.g. `\Exception`) @@ -103,12 +103,12 @@ A `SourceLocator` is a callable, which when invoked must be given an `Identifier ## Reflecting Classes -The `Reflector` is used to create Better Reflection `ReflectionClass` instances. You may pass it any +The `Reflector` is used to create Better Reflection `ReflectionClass` instances. You may pass it any `SourceLocator` to reflect on any class that can be located using the given that `SourceLocator`. ### Using the AutoloadSourceLocator -There is no need to use the `AutoloadSourceLocator` directly. Use the static constructors for `ReflectionClass` +There is no need to use the `AutoloadSourceLocator` directly. Use the static constructors for `ReflectionClass` and `ReflectionFunction`: ```php @@ -237,10 +237,31 @@ $reflector = new DefaultReflector($directoriesSourceLocator); $classes = $reflector->reflectAllClasses(); ``` +### Iterate filtered reflections in a directory + +```php +astLocator(); + +$directoriesSourceLocator = new DirectoriesSourceLocator(['path/to/directory1'], $astLocator); + +$reflector = new DefaultReflector($directoriesSourceLocator); + +$generator = $reflector->iterateClasses(new SourceContainsFilter(['some substring'])); + +foreach ($generator as $reflection) { + $reflection->getName(); +} +``` ## Reflecting Functions -The `Reflector` is used to create Better Reflection `ReflectionFunction` instances. You may pass it any +The `Reflector` is used to create Better Reflection `ReflectionFunction` instances. You may pass it any `SourceLocator` to reflect on any class that can be located using the given `SourceLocator`. ### Using the AutoloadSourceLocator @@ -277,5 +298,5 @@ $myClosure = function () { $functionInfo = ReflectionFunction::createFromClosure($myClosure); ``` -> Note that when you reflect on a closure, in order to match the core reflection API, the function "short" name will be +> Note that when you reflect on a closure, in order to match the core reflection API, the function "short" name will be just `{closure}`. From ae010077337bc4f469f545499fefef04b89c21cd Mon Sep 17 00:00:00 2001 From: Filipp Shcherbanich Date: Sat, 4 Jan 2025 23:55:26 +0500 Subject: [PATCH 09/10] Fixes --- demo/parsing-whole-directory/example3.php | 2 +- src/Reflector/DefaultReflector.php | 8 ++++---- src/SourceLocator/Type/MemoizingSourceLocator.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/demo/parsing-whole-directory/example3.php b/demo/parsing-whole-directory/example3.php index cd43419e9..7abbd55c7 100644 --- a/demo/parsing-whole-directory/example3.php +++ b/demo/parsing-whole-directory/example3.php @@ -1,6 +1,6 @@ + * @return Generator */ - public function iterateClasses(?SourceFilter $filter = null): Iterator + public function iterateClasses(?SourceFilter $filter = null): Generator { - /** @var Iterator $allClasses */ + /** @var Generator $allClasses */ $allClasses = $this->sourceLocator->iterateIdentifiersByType( $this, new IdentifierType(IdentifierType::IDENTIFIER_CLASS), diff --git a/src/SourceLocator/Type/MemoizingSourceLocator.php b/src/SourceLocator/Type/MemoizingSourceLocator.php index ef45acbc9..7c94bee55 100644 --- a/src/SourceLocator/Type/MemoizingSourceLocator.php +++ b/src/SourceLocator/Type/MemoizingSourceLocator.php @@ -24,7 +24,7 @@ final class MemoizingSourceLocator implements SourceLocator /** @var array> indexed by reflector key and identifier type cache key */ private array $cacheByIdentifierTypeKeyAndOid = []; - /** @var array> indexed by reflector key and identifier type cache key */ + /** @var array> indexed by reflector key, filter key and identifier type cache key */ private array $cacheIteratorByIdentifierTypeKeyAndOid = []; public function __construct(private SourceLocator $wrappedSourceLocator) From 7d67b06f388436edb57b2ed38861607b079ead08 Mon Sep 17 00:00:00 2001 From: Filipp Shcherbanich Date: Sun, 5 Jan 2025 01:10:16 +0500 Subject: [PATCH 10/10] Adding new fileName source filter --- .../SourceFilter/FileNameContainsFilter.php | 37 +++++++ .../FileNameContainsFilterTest.php | 100 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 src/SourceLocator/Type/SourceFilter/FileNameContainsFilter.php create mode 100644 test/unit/SourceLocator/Type/SourceFilter/FileNameContainsFilterTest.php diff --git a/src/SourceLocator/Type/SourceFilter/FileNameContainsFilter.php b/src/SourceLocator/Type/SourceFilter/FileNameContainsFilter.php new file mode 100644 index 000000000..8a775f39c --- /dev/null +++ b/src/SourceLocator/Type/SourceFilter/FileNameContainsFilter.php @@ -0,0 +1,37 @@ +substrings)); + } + + public function isAllowed(string $source, ?string $name, ?string $filename = null): bool + { + foreach ($this->substrings as $substring) { + + if (is_null($filename) && is_null($substring)) { + return true; + } + + if (is_null($filename) || is_null($substring)) { + return false; + } + + if (str_contains($filename, $substring)) { + return true; + } + } + return false; + } +} diff --git a/test/unit/SourceLocator/Type/SourceFilter/FileNameContainsFilterTest.php b/test/unit/SourceLocator/Type/SourceFilter/FileNameContainsFilterTest.php new file mode 100644 index 000000000..1bb0b8703 --- /dev/null +++ b/test/unit/SourceLocator/Type/SourceFilter/FileNameContainsFilterTest.php @@ -0,0 +1,100 @@ +assertSame($expected, $filter->getKey()); + } + + public static function getKeyProvider(): array + { + return [ + 'Without substrings' => [ + 'substrings' => [], + 'expected' => 'fileNameContains_40cd750bba9870f18aada2478b24840a', + ], + 'With one substring' => [ + 'substrings' => ['2d4f4'], + 'expected' => 'fileNameContains_c1b5c5d2b3c7d6af36c3a6469ec32678', + ], + 'With null substring' => [ + 'substrings' => [null], + 'expected' => 'fileNameContains_38017a839aaeb8ff1a658fce9af6edd3', + ], + 'With several substring' => [ + 'substrings' => ['vvvx', '64g22', 'gwqw', '0000', null], + 'expected' => 'fileNameContains_62ed0da44ff7d9fe6312fd10dfa3a4fc', + ], + ]; + } + + #[DataProvider('isAllowedProvider')] + public function testIsAllowed( + array $substrings, + ?string $fileName, + bool $expected, + ): void { + + $filter = new FileNameContainsFilter($substrings); + + $this->assertSame($expected, $filter->isAllowed('', null, $fileName)); + } + + public static function isAllowedProvider(): array + { + return [ + 'Without substrings' => [ + 'substrings' => [], + 'fileName' => 'sqfw', + 'expected' => false, + ], + 'Without substrings and null fileName' => [ + 'substrings' => [], + 'fileName' => null, + 'expected' => false, + ], + 'With null substring' => [ + 'substrings' => [null], + 'fileName' => 'sqfw', + 'expected' => false, + ], + 'With null substring and null fileName' => [ + 'substrings' => [null], + 'fileName' => null, + 'expected' => true, + ], + 'With substring' => [ + 'substrings' => ['qq'], + 'fileName' => 'qqq', + 'expected' => true, + ], + 'With several substring. The fileName contains one of them' => [ + 'substrings' => ['cc', 'aaa', 'bb'], + 'fileName' => 'qqq bb cc', + 'expected' => true, + ], + 'With several substring. The fileName does not contain them' => [ + 'substrings' => ['11', 'aaa', '22'], + 'fileName' => 'qqq bb cc', + 'expected' => false, + ], + ]; + } +}