Skip to content

Commit

Permalink
add notInNamespace filter in finders
Browse files Browse the repository at this point in the history
  • Loading branch information
alekitto committed Mar 30, 2021
1 parent 0881f19 commit 1394507
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function accept(): bool
$classNamespace = ltrim($index !== false ? substr($fqen, 0, $index) : $fqen, '\\');

foreach ($this->namespaces as $namespace) {
if (strpos($classNamespace, $namespace) === 0) {
if ($classNamespace === $namespace || strpos($classNamespace, $namespace . '\\') === 0) {
return true;
}
}
Expand Down
50 changes: 50 additions & 0 deletions lib/FilterIterator/PhpDocumentor/NotNamespaceFilterIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Kcs\ClassFinder\FilterIterator\PhpDocumentor;

use FilterIterator;
use Iterator;
use phpDocumentor\Reflection\Element;

use function assert;
use function ltrim;
use function Safe\substr;
use function strpos;
use function strrpos;

final class NotNamespaceFilterIterator extends FilterIterator
{
/** @var string[] */
private array $namespaces;

/**
* @param Iterator<Element> $iterator
* @param string[] $namespaces
*/
public function __construct(Iterator $iterator, array $namespaces)
{
parent::__construct($iterator);

$this->namespaces = $namespaces;
}

public function accept(): bool
{
$reflector = $this->getInnerIterator()->current();
assert($reflector instanceof Element);

$fqen = (string) $reflector->getFqsen();
$index = strrpos($fqen, '\\');
$classNamespace = ltrim($index !== false ? substr($fqen, 0, $index) : $fqen, '\\');

foreach ($this->namespaces as $namespace) {
if ($classNamespace === $namespace || strpos($classNamespace, $namespace . '\\') === 0) {
return false;
}
}

return true;
}
}
4 changes: 2 additions & 2 deletions lib/Finder/ComposerFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ public function __construct(?ClassLoader $loader = null)
*/
public function getIterator(): Iterator
{
if ($this->namespaces || $this->dirs) {
$iterator = new FilteredComposerIterator($this->loader, $this->namespaces, $this->dirs);
if ($this->namespaces || $this->dirs || $this->notNamespaces) {
$iterator = new FilteredComposerIterator($this->loader, $this->namespaces, $this->notNamespaces, $this->dirs);
} else {
$iterator = new ComposerIterator($this->loader);
}
Expand Down
9 changes: 9 additions & 0 deletions lib/Finder/FinderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ public function in($dirs): self;
*/
public function inNamespace($namespaces): self;

/**
* Adds namespace(s) to exclude from search.
*
* @param string|string[] $namespaces
*
* @return $this
*/
public function notInNamespace($namespaces): self;

/**
* Sets a custom callback for class filtering.
* The callback will receive the class name as the only argument.
Expand Down
13 changes: 13 additions & 0 deletions lib/Finder/FinderTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ trait FinderTrait
/** @var string[] */
private ?array $namespaces = null;

/** @var string[] */
private ?array $notNamespaces = null;

/** @var string[] */
private ?array $paths = null;

Expand Down Expand Up @@ -117,6 +120,16 @@ public function inNamespace($namespaces): self
return $this;
}

/**
* {@inheritdoc}
*/
public function notInNamespace($namespaces): self
{
$this->notNamespaces = array_unique(array_merge($this->notNamespaces ?? [], (array) $namespaces));

return $this;
}

public function filter(?callable $callback): self
{
$this->callback = $callback;
Expand Down
4 changes: 4 additions & 0 deletions lib/Finder/PhpDocumentorFilterTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ private function applyFilters(Iterator $iterator): Iterator
$iterator = new Filters\NamespaceFilterIterator($iterator, $this->namespaces);
}

if ($this->notNamespaces) {
$iterator = new Filters\NotNamespaceFilterIterator($iterator, $this->notNamespaces);
}

if ($this->implements) {
$iterator = new Filters\InterfaceImplementationFilterIterator($iterator, $this->implements);
}
Expand Down
22 changes: 19 additions & 3 deletions lib/Iterator/FilteredComposerIterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,18 @@ final class FilteredComposerIterator extends ClassIterator
/** @var string[]|null */
private ?array $namespaces = null;

/** @var string[]|null */
private ?array $notNamespaces = null;

/** @var string[]|null */
private ?array $dirs = null;

/**
* @param string[]|null $namespaces
* @param string[]|null $notNamespaces
* @param string[]|null $dirs
*/
public function __construct(ClassLoader $classLoader, ?array $namespaces, ?array $dirs, int $flags = 0)
public function __construct(ClassLoader $classLoader, ?array $namespaces, ?array $notNamespaces, ?array $dirs, int $flags = 0)
{
$this->classLoader = $classLoader;
$this->dirs = $dirs !== null ? array_map(PathNormalizer::class . '::resolvePath', $dirs) : $dirs;
Expand All @@ -51,6 +55,10 @@ public function __construct(ClassLoader $classLoader, ?array $namespaces, ?array
$this->namespaces = array_unique($namespaces);
}

if ($notNamespaces !== null) {
$this->notNamespaces = array_unique($notNamespaces);
}

parent::__construct($flags);
}

Expand Down Expand Up @@ -92,11 +100,11 @@ private function searchInPsrMap(): Generator
}

foreach ($this->traversePrefixes($this->classLoader->getPrefixesPsr4()) as $ns => $dir) {
yield from new Psr4Iterator($ns, $dir, 0, $this->classLoader->getClassMap());
yield from new Psr4Iterator($ns, $dir, 0, $this->classLoader->getClassMap(), $this->notNamespaces);
}

foreach ($this->traversePrefixes($this->classLoader->getPrefixes()) as $ns => $dir) {
yield from new Psr0Iterator($ns, $dir, 0, $this->classLoader->getClassMap());
yield from new Psr0Iterator($ns, $dir, 0, $this->classLoader->getClassMap(), $this->notNamespaces);
}
}

Expand All @@ -123,6 +131,14 @@ private function traversePrefixes(array $prefixes): Generator

private function validNamespace(string $class): bool
{
if ($this->notNamespaces !== null) {
foreach ($this->notNamespaces as $namespace) {
if (strpos($class, $namespace) === 0) {
return false;
}
}
}

if ($this->namespaces === null) {
return true;
}
Expand Down
15 changes: 14 additions & 1 deletion lib/Iterator/Psr0Iterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,20 @@ final class Psr0Iterator extends ClassIterator
/** @var string[] */
private array $classMap;

/** @var string[]|null */
private ?array $excludeNamespaces;

/**
* @param array<string, mixed> $classMap
* @param string[] $excludeNamespaces
*/
public function __construct(string $namespace, string $path, int $flags = 0, array $classMap = [])
public function __construct(string $namespace, string $path, int $flags = 0, array $classMap = [], ?array $excludeNamespaces = null)
{
$this->namespace = $namespace;
$this->path = PathNormalizer::resolvePath($path);
$this->pathLen = strlen($this->path);
$this->classMap = array_map(PathNormalizer::class . '::resolvePath', $classMap);
$this->excludeNamespaces = $excludeNamespaces;

parent::__construct($flags);
}
Expand All @@ -66,6 +71,14 @@ protected function getGenerator(): Generator
continue;
}

if ($this->excludeNamespaces !== null) {
foreach ($this->excludeNamespaces as $namespace) {
if (strpos($class, $namespace) === 0) {
continue 2;
}
}
}

if (! preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $class)) {
continue;
}
Expand Down
16 changes: 15 additions & 1 deletion lib/Iterator/Psr4Iterator.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use function Safe\substr;
use function str_replace;
use function strlen;
use function strpos;

final class Psr4Iterator extends ClassIterator
{
Expand All @@ -30,15 +31,20 @@ final class Psr4Iterator extends ClassIterator
/** @var array<string, mixed> */
private array $classMap;

/** @var string[]|null */
private ?array $excludeNamespaces;

/**
* @param array<string, mixed> $classMap
* @param string[] $excludeNamespaces
*/
public function __construct(string $namespace, string $path, int $flags = 0, array $classMap = [])
public function __construct(string $namespace, string $path, int $flags = 0, array $classMap = [], ?array $excludeNamespaces = null)
{
$this->namespace = $namespace;
$this->path = PathNormalizer::resolvePath($path);
$this->prefixLen = strlen($this->path);
$this->classMap = array_map(PathNormalizer::class . '::resolvePath', $classMap);
$this->excludeNamespaces = $excludeNamespaces;

parent::__construct($flags);
}
Expand All @@ -61,6 +67,14 @@ protected function getGenerator(): Generator

/** @phpstan-var class-string $class */
$class = $this->namespace . ltrim(str_replace('/', '\\', substr($path, $this->prefixLen, -strlen($m[0]))), '\\');
if ($this->excludeNamespaces !== null) {
foreach ($this->excludeNamespaces as $namespace) {
if (strpos($class, $namespace) === 0) {
continue 2;
}
}
}

if (! preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $class)) {
continue;
}
Expand Down
5 changes: 2 additions & 3 deletions lib/Iterator/RecursiveIteratorTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

use function is_dir;
use function is_file;
use function iterator_to_array;
use function Safe\glob;

trait RecursiveIteratorTrait
Expand All @@ -25,15 +24,15 @@ private function search(): Generator
{
foreach (glob($this->path . '/*') as $path) {
if (is_dir($path)) {
$files = iterator_to_array(new RecursiveIteratorIterator(
$files = new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS),
static function (SplFileInfo $file) {
return $file->getBasename()[0] !== '.';
}
),
RecursiveIteratorIterator::LEAVES_ONLY
));
);

foreach ($files as $filepath => $info) {
if (! $info->isFile()) {
Expand Down
14 changes: 14 additions & 0 deletions tests/Finder/ComposerFinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ public function testFinderShouldFilterByNamespace(): void
], iterator_to_array($finder));
}

public function testFinderShouldFilterByExcludedNamespace(): void
{
$finder = new ComposerFinder();
$finder
->inNamespace(['Kcs\ClassFinder\Fixtures'])
->notInNamespace(['Kcs\ClassFinder\Fixtures\Psr4']);

self::assertEquals([
Psr0\BarBar::class => new ReflectionClass(Psr0\BarBar::class),
Psr0\Foobar::class => new ReflectionClass(Psr0\Foobar::class),
Psr0\SubNs\FooBaz::class => new ReflectionClass(Psr0\SubNs\FooBaz::class),
], iterator_to_array($finder));
}

public function testFinderShouldFilterByDirectory(): void
{
$finder = new ComposerFinder();
Expand Down
24 changes: 24 additions & 0 deletions tests/Finder/PhpDocumentorFinderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ public function testFinderShouldFilterByNamespace(): void

$classes = iterator_to_array($finder);

self::assertCount(7, $classes);
self::assertArrayHasKey(Psr4\BarBar::class, $classes);
self::assertInstanceOf(Class_::class, $classes[Psr4\BarBar::class]);
self::assertArrayHasKey(Psr4\Foobar::class, $classes);
self::assertInstanceOf(Class_::class, $classes[Psr4\Foobar::class]);
self::assertArrayHasKey(Psr4\AbstractClass::class, $classes);
self::assertInstanceOf(Class_::class, $classes[Psr4\AbstractClass::class]);
self::assertArrayHasKey(Psr4\HiddenClass::class, $classes);
self::assertInstanceOf(Class_::class, $classes[Psr4\HiddenClass::class]);
self::assertArrayHasKey(Psr4\SubNs\FooBaz::class, $classes);
self::assertInstanceOf(Class_::class, $classes[Psr4\SubNs\FooBaz::class]);
self::assertArrayHasKey(Psr4\FooInterface::class, $classes);
Expand All @@ -46,6 +49,27 @@ public function testFinderShouldFilterByNamespace(): void
self::assertInstanceOf(Trait_::class, $classes[Psr4\FooTrait::class]);
}

public function testFinderShouldFilterByExcludedNamespace(): void
{
$finder = new PhpDocumentorFinder(__DIR__ . '/../../data');
$finder
->inNamespace(['Kcs\ClassFinder\Fixtures'])
->notInNamespace([
'Kcs\ClassFinder\Fixtures\Psr4',
'Kcs\ClassFinder\Fixtures\Psr4WithClassMap',
]);

$classes = iterator_to_array($finder);

self::assertCount(3, $classes);
self::assertArrayHasKey(Psr0\BarBar::class, $classes);
self::assertInstanceOf(Class_::class, $classes[Psr0\BarBar::class]);
self::assertArrayHasKey(Psr0\Foobar::class, $classes);
self::assertInstanceOf(Class_::class, $classes[Psr0\Foobar::class]);
self::assertArrayHasKey(Psr0\SubNs\FooBaz::class, $classes);
self::assertInstanceOf(Class_::class, $classes[Psr0\SubNs\FooBaz::class]);
}

public function testFinderShouldFilterByDirectory(): void
{
$finder = new PhpDocumentorFinder(__DIR__ . '/../../data');
Expand Down
4 changes: 2 additions & 2 deletions tests/Iterator/FilteredComposerIteratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ protected function setUp(): void

public function testComposerIteratorShouldWork(): void
{
$iterator = new FilteredComposerIterator($this->loader, null, null);
$iterator = new FilteredComposerIterator($this->loader, null, null, null);

self::assertEquals([
self::class => new ReflectionClass(self::class),
Expand All @@ -68,7 +68,7 @@ public function testComposerIteratorShouldFilterNotIntersectingPath(): void
// intersects perfectly with the requested dirs. The upper finder should filter out the
// non-matching results.

$iterator = new FilteredComposerIterator($this->loader, null, [__DIR__ . '/../..' . '/data/Composer/Psr4/SubNs']);
$iterator = new FilteredComposerIterator($this->loader, null, null, [__DIR__ . '/../..' . '/data/Composer/Psr4/SubNs']);

self::assertEquals([
Psr4\BarBar::class => new ReflectionClass(Psr4\BarBar::class),
Expand Down

0 comments on commit 1394507

Please sign in to comment.