From 7a9546f144ba6ef02c92712afba3625ae617a59f Mon Sep 17 00:00:00 2001 From: Denis Brumann Date: Sat, 21 May 2022 14:24:28 +0200 Subject: [PATCH] New collector for attributes. --- config/services.php | 4 ++ docs/collectors.md | 15 +++++ src/Layer/Collector/AttributeCollector.php | 57 +++++++++++++++++++ src/Layer/Collector/CollectorTypes.php | 1 + src/Layer/Collector/UsesCollector.php | 6 +- .../Collector/AttributeCollectorTest.php | 51 +++++++++++++++++ 6 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 src/Layer/Collector/AttributeCollector.php create mode 100644 tests/Layer/Collector/AttributeCollectorTest.php diff --git a/config/services.php b/config/services.php index acd41bece..292f5b2e4 100644 --- a/config/services.php +++ b/config/services.php @@ -48,6 +48,7 @@ use Qossmic\Deptrac\File\YmlFileLoader; use Qossmic\Deptrac\InputCollector\FileInputCollector; use Qossmic\Deptrac\InputCollector\InputCollectorInterface; +use Qossmic\Deptrac\Layer\Collector\AttributeCollector; use Qossmic\Deptrac\Layer\Collector\BoolCollector; use Qossmic\Deptrac\Layer\Collector\ClassCollector; use Qossmic\Deptrac\Layer\Collector\ClassLikeCollector; @@ -197,6 +198,9 @@ ]); $services->set(CollectorResolver::class); $services->alias(CollectorResolverInterface::class, CollectorResolver::class); + $services + ->set(AttributeCollector::class) + ->tag('collector', ['type' => CollectorTypes::TYPE_ATTRIBUTE]); $services ->set(BoolCollector::class) ->tag('collector', ['type' => CollectorTypes::TYPE_BOOL]); diff --git a/docs/collectors.md b/docs/collectors.md index 9ffc4e127..95525a30e 100644 --- a/docs/collectors.md +++ b/docs/collectors.md @@ -3,6 +3,21 @@ Collectors decide if a node (typically a class) is part of a layer. You can use multiple different collectors for a layer. +## `attribute` Collector + +The `attribute` collector finds all class-likes, functions or files using the +provided attribute. You can provide the full attribute name or a substring that +should be matched. + +```yaml +parameters: + layers: + - name: Entities + collectors: + - type: attribute + value: Doctrine\ORM\Mapping\Entity +``` + ## `bool` Collector The `bool` collector allows combining other collectors with or without negation. diff --git a/src/Layer/Collector/AttributeCollector.php b/src/Layer/Collector/AttributeCollector.php new file mode 100644 index 000000000..f0b2234ca --- /dev/null +++ b/src/Layer/Collector/AttributeCollector.php @@ -0,0 +1,57 @@ +getSearchedSubstring($config); + + foreach ($reference->getDependencies() as $dependency) { + if (DependencyToken::ATTRIBUTE !== $dependency->getType()) { + continue; + } + + $usedAttribute = $dependency->getToken()->toString(); + + if (str_contains($usedAttribute, $match)) { + return true; + } + } + + return false; + } + + public function resolvable(array $config): bool + { + return true; + } + + /** + * @param array> $config + */ + private function getSearchedSubstring(array $config): string + { + if (!isset($config['value']) || !is_string($config['value'])) { + throw new LogicException('UsesCollector needs the trait name as a string.'); + } + + return $config['value']; + } +} diff --git a/src/Layer/Collector/CollectorTypes.php b/src/Layer/Collector/CollectorTypes.php index 27de6f185..fac867428 100644 --- a/src/Layer/Collector/CollectorTypes.php +++ b/src/Layer/Collector/CollectorTypes.php @@ -6,6 +6,7 @@ final class CollectorTypes { + public const TYPE_ATTRIBUTE = 'attribute'; public const TYPE_BOOL = 'bool'; public const TYPE_CLASS = 'class'; public const TYPE_CLASSLIKE = 'classLike'; diff --git a/src/Layer/Collector/UsesCollector.php b/src/Layer/Collector/UsesCollector.php index a154c0ef8..7120ff68d 100644 --- a/src/Layer/Collector/UsesCollector.php +++ b/src/Layer/Collector/UsesCollector.php @@ -23,10 +23,10 @@ public function satisfy(array $config, TokenReferenceInterface $reference, AstMa return false; } - $interfaceName = $this->getInterfaceName($config); + $traitName = $this->getTraitName($config); foreach ($astMap->getClassInherits($reference->getToken()) as $inherit) { - if ($inherit->isUses() && $inherit->getClassLikeName()->equals($interfaceName)) { + if ($inherit->isUses() && $inherit->getClassLikeName()->equals($traitName)) { return true; } } @@ -37,7 +37,7 @@ public function satisfy(array $config, TokenReferenceInterface $reference, AstMa /** * @param array> $config */ - private function getInterfaceName(array $config): ClassLikeToken + private function getTraitName(array $config): ClassLikeToken { if (isset($config['uses']) && !isset($config['value'])) { trigger_deprecation('qossmic/deptrac', '0.20.0', 'UsesCollector should use the "value" key from this version'); diff --git a/tests/Layer/Collector/AttributeCollectorTest.php b/tests/Layer/Collector/AttributeCollectorTest.php new file mode 100644 index 000000000..d4d3eac77 --- /dev/null +++ b/tests/Layer/Collector/AttributeCollectorTest.php @@ -0,0 +1,51 @@ +collector = new AttributeCollector(); + } + + public function dataProviderSatisfy(): iterable + { + yield 'matches usage of attribute with only partial name' => [ + ['value' => 'MyAttribute'], + true, + ]; + yield 'does not match unescaped fully qualified class name' => [ + ['value' => 'App\MyAttribute'], + true, + ]; + yield 'does not match other attributes' => [ + ['value' => 'OtherAttribute'], + false, + ]; + } + + /** + * @dataProvider dataProviderSatisfy + */ + public function testSatisfy(array $config, bool $expected): void + { + $classLikeReference = FileReferenceBuilder::create('Foo.php') + ->newClass('App\Foo') + ->attribute('App\MyAttribute', 2) + ->attribute('MyAttribute', 3) + ->build(); + $actual = $this->collector->satisfy($config, $classLikeReference, new AstMap([])); + + self::assertSame($expected, $actual); + } +}