Skip to content

Commit

Permalink
Added package name collector
Browse files Browse the repository at this point in the history
  • Loading branch information
ariddlestone committed Jul 12, 2023
1 parent bd7cf22 commit 506667f
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 0 deletions.
4 changes: 4 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
use Qossmic\Deptrac\Core\Layer\Collector\InterfaceCollector;
use Qossmic\Deptrac\Core\Layer\Collector\LayerCollector;
use Qossmic\Deptrac\Core\Layer\Collector\MethodCollector;
use Qossmic\Deptrac\Core\Layer\Collector\PackageNameCollector;
use Qossmic\Deptrac\Core\Layer\Collector\PhpInternalCollector;
use Qossmic\Deptrac\Core\Layer\Collector\SuperglobalCollector;
use Qossmic\Deptrac\Core\Layer\Collector\TraitCollector;
Expand Down Expand Up @@ -291,6 +292,9 @@
$services
->set(MethodCollector::class)
->tag('collector', ['type' => CollectorType::TYPE_METHOD->value]);
$services
->set(PackageNameCollector::class)
->tag('collector', ['type' => CollectorType::TYPE_PACKAGE_NAME->value]);
$services
->set(SuperglobalCollector::class)
->tag('collector', ['type' => CollectorType::TYPE_SUPERGLOBAL->value]);
Expand Down
18 changes: 18 additions & 0 deletions docs/collectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,24 @@ deptrac:
Every class having a method that matches the regular expression `.*foo`,
e.g. `getFoo()` or `setFoo()` becomes a part of the *Foo services* layer.

## `packageName` Collector

The `packageName` collector allows collecting classes and anything similar to
classes like interfaces, traits or enums, based on a `@package` annotation in
their docblock. Any matching class-like will be added to the assigned layer.

```yaml
deptrac:
layers:
- name: FooPackages
collectors:
- type: packageName
value: ^Foo$
```

Every class-like with an annotation of `@package Foo` in their docblock will
become part of the *FooPackages* layer.

## `superglobal` Collector

The `superglobal` collector allows collecting superglobal PHP variables matching
Expand Down
1 change: 1 addition & 0 deletions src/Contract/Config/CollectorType.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ enum CollectorType: string
case TYPE_INTERFACE = 'interface';
case TYPE_LAYER = 'layer';
case TYPE_METHOD = 'method';
case TYPE_PACKAGE_NAME = 'packageName';
case TYPE_SUPERGLOBAL = 'superglobal';
case TYPE_TRAIT = 'trait';
case TYPE_USES = 'uses';
Expand Down
95 changes: 95 additions & 0 deletions src/Core/Layer/Collector/PackageNameCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace Qossmic\Deptrac\Core\Layer\Collector;

use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use Qossmic\Deptrac\Contract\Ast\CouldNotParseFileException;
use Qossmic\Deptrac\Contract\Ast\TokenReferenceInterface;
use Qossmic\Deptrac\Contract\Layer\InvalidCollectorDefinitionException;
use Qossmic\Deptrac\Core\Ast\AstMap\ClassLike\ClassLikeReference;
use Qossmic\Deptrac\Core\Ast\Parser\NikicPhpParser\NikicPhpParser;

class PackageNameCollector extends RegexCollector
{
private readonly Lexer $lexer;
private readonly PhpDocParser $docParser;

public function __construct(
private readonly NikicPhpParser $nikicPhpParser
) {
$this->lexer = new Lexer();
$this->docParser = new PhpDocParser(new TypeParser(), new ConstExprParser());
}

public function satisfy(array $config, TokenReferenceInterface $reference): bool
{
if (!$reference instanceof ClassLikeReference) {
return false;
}

$regex = $this->getValidatedPattern($config);

foreach ($this->getPackages($reference) as $package) {
if (1 === preg_match($regex, $package)) {
return true;
}
}

return false;
}

protected function getPattern(array $config): string
{
if (!isset($config['value']) || !is_string($config['value'])) {
throw new InvalidCollectorDefinitionException('PackageNameCollector needs the value configuration.');
}

return '/'.$config['value'].'/im';
}

/**
* @return array<string>
* @throws CouldNotParseFileException
*/
private function getPackages(ClassLikeReference $reference): array
{
$docBlock = $this->getCommentDoc($reference);

if (!$docBlock) {
return [];
}

$tokens = new TokenIterator($this->lexer->tokenize($docBlock));
$docNode = $this->docParser->parse($tokens);

return array_map(
static fn (PhpDocTagNode $node) => (string) $node->value,
$docNode->getTagsByName('@package')
);
}

/**
* @throws CouldNotParseFileException
*/
private function getCommentDoc(ClassLikeReference $reference): string
{
$node = $this->nikicPhpParser->getNodeForClassLikeReference($reference);

if (null === $node) {
return '';
}

$doc = $node->getDocComment();

if (null === $doc) {
return '';
}

return $doc->getText();
}
}
122 changes: 122 additions & 0 deletions tests/Core/Layer/Collector/PackageNameCollectorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

declare(strict_types=1);

namespace Tests\Qossmic\Deptrac\Core\Layer\Collector;

use PhpParser\Comment\Doc;
use PhpParser\Node;
use PHPUnit\Framework\TestCase;
use Qossmic\Deptrac\Contract\Layer\InvalidCollectorDefinitionException;
use Qossmic\Deptrac\Core\Ast\AstMap\ClassLike\ClassLikeReference;
use Qossmic\Deptrac\Core\Ast\AstMap\ClassLike\ClassLikeToken;
use Qossmic\Deptrac\Core\Ast\Parser\NikicPhpParser\NikicPhpParser;
use Qossmic\Deptrac\Core\Layer\Collector\PackageNameCollector;

final class PackageNameCollectorTest extends TestCase
{
private NikicPhpParser $astParser;
private PackageNameCollector $collector;

protected function setUp(): void
{
parent::setUp();

$this->astParser = $this->createMock(NikicPhpParser::class);

$this->collector = new PackageNameCollector($this->astParser);
}

public function provideSatisfy(): iterable
{
yield [
['value' => 'abc'],
$this->getPackageDocBlock(['abc', 'abcdef', 'xyz']),
true,
];

yield [
['value' => 'abc'],
$this->getPackageDocBlock(['abc', 'xyz']),
true,
];

yield [
['value' => 'abc'],
$this->getPackageDocBlock(['xyz']),
false,
];
}

/**
* @dataProvider provideSatisfy
*/
public function testSatisfy(array $configuration, Doc $docBlock, bool $expected): void
{
$astClassReference = new ClassLikeReference(ClassLikeToken::fromFQCN('foo'));

$classLike = $this->createMock(Node\Stmt\ClassLike::class);
$classLike->method('getDocComment')->willReturn($docBlock);

$this->astParser
->method('getNodeForClassLikeReference')
->with($astClassReference)
->willReturn($classLike);

$actual = $this->collector->satisfy(
$configuration,
$astClassReference,
);

self::assertSame($expected, $actual);
}

public function testClassLikeAstNotFoundDoesNotSatisfy(): void
{
$astClassReference = new ClassLikeReference(ClassLikeToken::fromFQCN('foo'));
$this->astParser
->method('getNodeForClassLikeReference')
->with($astClassReference)
->willReturn(null);

$actual = $this->collector->satisfy(
['value' => 'abc'],
$astClassReference,
);

self::assertFalse($actual);
}

public function testMissingValueThrowsException(): void
{
$astClassReference = new ClassLikeReference(ClassLikeToken::fromFQCN('foo'));

$this->expectException(InvalidCollectorDefinitionException::class);
$this->expectExceptionMessage('PackageNameCollector needs the value configuration.');

$this->collector->satisfy(
[],
$astClassReference,
);
}

public function testInvalidRegexParam(): void
{
$astClassReference = new ClassLikeReference(ClassLikeToken::fromFQCN('foo'));

$this->expectException(InvalidCollectorDefinitionException::class);

$this->collector->satisfy(
['value' => '/'],
$astClassReference,
);
}

private function getPackageDocBlock(array $packageNames): Doc
{
return new Doc(sprintf(
" /**\n%s */",
implode('', array_map(static fn ($packageName) => ' * @package '.$packageName."\n", $packageNames))
));
}
}

0 comments on commit 506667f

Please sign in to comment.