diff --git a/baseline.xml b/baseline.xml index 83e0f6182..cf2f683fa 100644 --- a/baseline.xml +++ b/baseline.xml @@ -1,34 +1,5 @@ - - - |mixed, - * must_not: array|mixed, - * private: bool, - * type: string}]]> - - - - - , - * private: bool, - * type: string}]]> - - - - - - - - - - array{private: bool, type: string, value: string} - - rules) ? array_values($this->rules[$type]) : []]]> diff --git a/config/services.php b/config/services.php index a0de735ff..127d3d60d 100644 --- a/config/services.php +++ b/config/services.php @@ -72,6 +72,7 @@ use Qossmic\Deptrac\Core\Layer\Collector\MethodCollector; use Qossmic\Deptrac\Core\Layer\Collector\PhpInternalCollector; use Qossmic\Deptrac\Core\Layer\Collector\SuperglobalCollector; +use Qossmic\Deptrac\Core\Layer\Collector\TagValueRegexCollector; use Qossmic\Deptrac\Core\Layer\Collector\TraitCollector; use Qossmic\Deptrac\Core\Layer\Collector\UsesCollector; use Qossmic\Deptrac\Core\Layer\LayerResolver; @@ -258,6 +259,9 @@ $services ->set(ClassNameRegexCollector::class) ->tag('collector', ['type' => CollectorType::TYPE_CLASS_NAME_REGEX->value]); + $services + ->set(TagValueRegexCollector::class) + ->tag('collector', ['type' => CollectorType::TYPE_TAG_VALUE_REGEX->value]); $services ->set(DirectoryCollector::class) ->tag('collector', ['type' => CollectorType::TYPE_DIRECTORY->value]); @@ -336,7 +340,10 @@ ->tag('kernel.event_subscriber'); $services ->set(DependsOnInternalToken::class) - ->tag('kernel.event_subscriber'); + ->tag('kernel.event_subscriber') + ->args([ + '$config' => param('analyser'), + ]); $services ->set(UnmatchedSkippedViolations::class) ->tag('kernel.event_subscriber'); diff --git a/deptrac.config.php b/deptrac.config.php index 47978956d..f055aa292 100755 --- a/deptrac.config.php +++ b/deptrac.config.php @@ -1,6 +1,7 @@ paths('src') - ->analysers( - EmitterType::CLASS_TOKEN, - EmitterType::CLASS_SUPERGLOBAL_TOKEN, - EmitterType::FILE_TOKEN, - EmitterType::FUNCTION_TOKEN, - EmitterType::FUNCTION_SUPERGLOBAL_TOKEN, - EmitterType::FUNCTION_CALL, + ->analyser( + AnalyserConfig::create() + ->internalTag( '@internal' ) + ->types( + EmitterType::CLASS_TOKEN, + EmitterType::CLASS_SUPERGLOBAL_TOKEN, + EmitterType::FILE_TOKEN, + EmitterType::FUNCTION_TOKEN, + EmitterType::FUNCTION_SUPERGLOBAL_TOKEN, + EmitterType::FUNCTION_CALL + ) ) ->layers( $analyser = Layer::withName('Analyser')->collectors( diff --git a/deptrac.yaml b/deptrac.yaml index a08abaa71..270d15fde 100644 --- a/deptrac.yaml +++ b/deptrac.yaml @@ -8,6 +8,7 @@ deptrac: - ./src analyser: + internal_tag: "@internal" types: - class - class_superglobal diff --git a/docs/collectors.md b/docs/collectors.md index bdb660c10..98f1ae769 100644 --- a/docs/collectors.md +++ b/docs/collectors.md @@ -97,6 +97,31 @@ deptrac: Every class name that matches the regular expression becomes a part of the *controller* layer. +## `tagValueRegex` Collector + +The `tagValueRegex` collector allows collecting classes and functions by +matching the name and value of any tag in their phpdoc block, such as @internal +or @deprecated. + +Any matching class will be added to the assigned layer. + +```yaml +deptrac: + layers: + - name: Deprecated + collectors: + - type: tagValueRegex + tag: '@deprecated' + - name: DeprecatedSinceV2 + collectors: + - type: tagValueRegex + tag: '@deprecated' + value: '/^since v2/i' +``` +All classes tagged with "@deprecated" become part of the +*Deprecated* layer. All classes that specify that they have been +deprecated since v2 also become part of the *DeprecatedSinceV2* layer. + ## `composer` Collector The `composer` collector allows you to define dependencies on composer `require` or `require-dev` packages that follow PSR-0 or PSR-4 autoloading convention. With this collector you can for example enforce: diff --git a/docs/configuration.md b/docs/configuration.md index 847cf68a1..a90d33afb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,6 +20,23 @@ The following table shows the available config keys for Deptrac. +analyser.internal_tag + +Specifies a custom doc block tag which deptrac should use to identify layer-internal +class-like structures. The tag @deptrac-internal will always be used +for this purpose. This option allows an additional tag to be specified, such as +@layer-internal or plain @internal. + + + +```yaml +deptrac: + analyser: + internal_tag: "@layer-internal" +``` + + + analyser.types diff --git a/src/Contract/Ast/TaggedTokenReferenceInterface.php b/src/Contract/Ast/TaggedTokenReferenceInterface.php new file mode 100644 index 000000000..54ec8518e --- /dev/null +++ b/src/Contract/Ast/TaggedTokenReferenceInterface.php @@ -0,0 +1,18 @@ + + */ + public function getTagLines(string $name): ?array; +} diff --git a/src/Contract/Config/AnalyserConfig.php b/src/Contract/Config/AnalyserConfig.php new file mode 100644 index 000000000..3364380bf --- /dev/null +++ b/src/Contract/Config/AnalyserConfig.php @@ -0,0 +1,55 @@ + */ + private array $types = []; + + /** @var ?string */ + private ?string $internalTag = null; + + private function __construct() {} + + /** @param ?array $types */ + public static function create(array $types = null, string $internalTag = null): self + { + $analyser = new self(); + + $types ??= [EmitterType::CLASS_TOKEN, EmitterType::FUNCTION_TOKEN]; + $analyser->types(...$types); + + $analyser->internalTag($internalTag); + + return $analyser; + } + + public function types(EmitterType ...$types): self + { + $this->types = []; + foreach ($types as $type) { + $this->types[$type->value] = $type; + } + + return $this; + } + + public function internalTag(?string $tag): self + { + $this->internalTag = $tag; + + return $this; + } + + /** @return array */ + public function toArray(): array + { + return [ + 'types' => array_map(static fn (EmitterType $emitterType) => $emitterType->value, $this->types), + 'internal_tag' => $this->internalTag, + ]; + } +} diff --git a/src/Contract/Config/Collector/TagValueRegexConfig.php b/src/Contract/Config/Collector/TagValueRegexConfig.php new file mode 100644 index 000000000..7cfa246df --- /dev/null +++ b/src/Contract/Config/Collector/TagValueRegexConfig.php @@ -0,0 +1,36 @@ +value = $regexpr; + + return $this; + } + + public function toArray(): array + { + return [ + 'tag' => $this->tag, + 'value' => $this->value, + ] + parent::toArray(); + } +} diff --git a/src/Contract/Config/CollectorConfig.php b/src/Contract/Config/CollectorConfig.php index ec5d835f4..7ff73c1d1 100644 --- a/src/Contract/Config/CollectorConfig.php +++ b/src/Contract/Config/CollectorConfig.php @@ -16,7 +16,7 @@ public function private(): self return $this; } - /** @return array{'type': string, 'private': bool} */ + /** @return array{'type': string, 'private': bool, ...} */ public function toArray(): array { return [ diff --git a/src/Contract/Config/CollectorType.php b/src/Contract/Config/CollectorType.php index 44d678e3f..2238e6b78 100644 --- a/src/Contract/Config/CollectorType.php +++ b/src/Contract/Config/CollectorType.php @@ -12,6 +12,7 @@ enum CollectorType: string case TYPE_CLASS = 'class'; case TYPE_CLASSLIKE = 'classLike'; case TYPE_CLASS_NAME_REGEX = 'classNameRegex'; + case TYPE_TAG_VALUE_REGEX = 'tagValueRegex'; case TYPE_DIRECTORY = 'directory'; case TYPE_EXTENDS = 'extends'; case TYPE_FUNCTION_NAME = 'functionName'; diff --git a/src/Contract/Config/DeptracConfig.php b/src/Contract/Config/DeptracConfig.php index a7ad3cedf..cb10fd3d8 100644 --- a/src/Contract/Config/DeptracConfig.php +++ b/src/Contract/Config/DeptracConfig.php @@ -21,18 +21,23 @@ final class DeptracConfig implements ConfigBuilderInterface private array $formatters = []; /** @var array */ private array $rulesets = []; - /** @var array */ - private array $analyser = []; + private ?AnalyserConfig $analyser = null; /** @var array> */ private array $skipViolations = []; /** @var array */ private array $excludeFiles = []; + /** + * @deprecated use analyser(AnalyserConfig::create()) instead + */ public function analysers(EmitterType ...$types): self { - foreach ($types as $type) { - $this->analyser[$type->value] = $type; - } + return $this->analyser(AnalyserConfig::create($types)); + } + + public function analyser(AnalyserConfig $analyser): self + { + $this->analyser = $analyser; return $this; } @@ -109,8 +114,8 @@ public function toArray(): array $config['paths'] = $this->paths; } - if ([] !== $this->analyser) { - $config['analyser']['types'] = array_map(static fn (EmitterType $emitterType) => $emitterType->value, $this->analyser); + if ($this->analyser) { + $config['analyser'] = $this->analyser->toArray(); } if ([] !== $this->formatters) { diff --git a/src/Core/Analyser/EventHandler/DependsOnInternalToken.php b/src/Core/Analyser/EventHandler/DependsOnInternalToken.php index 71d1017b5..89971aad6 100644 --- a/src/Core/Analyser/EventHandler/DependsOnInternalToken.php +++ b/src/Core/Analyser/EventHandler/DependsOnInternalToken.php @@ -14,7 +14,17 @@ */ class DependsOnInternalToken implements ViolationCreatingInterface { - public function __construct(private readonly EventHelper $eventHelper) {} + private ?string $internalTag; + + /** + * @param array{internal_tag:string|null, ...} $config + */ + public function __construct( + private readonly EventHelper $eventHelper, + array $config) + { + $this->internalTag = $config['internal_tag']; + } public static function getSubscribedEvents() { @@ -29,10 +39,17 @@ public function invoke(ProcessEvent $event): void foreach ($event->dependentLayers as $dependentLayer => $_) { if ($event->dependerLayer !== $dependentLayer && $event->dependentReference instanceof ClassLikeReference - && $event->dependentReference->isInternal ) { - $this->eventHelper->addSkippableViolation($event, $ruleset, $dependentLayer, $this); - $event->stopPropagation(); + $isInternal = $event->dependentReference->hasTag('@deptrac-internal'); + + if (!$isInternal && null !== $this->internalTag) { + $isInternal = $event->dependentReference->hasTag($this->internalTag); + } + + if ($isInternal) { + $this->eventHelper->addSkippableViolation($event, $ruleset, $dependentLayer, $this); + $event->stopPropagation(); + } } } } diff --git a/src/Core/Ast/AstMap/ClassLike/ClassLikeReference.php b/src/Core/Ast/AstMap/ClassLike/ClassLikeReference.php index eb29f1d60..95ee8fc96 100644 --- a/src/Core/Ast/AstMap/ClassLike/ClassLikeReference.php +++ b/src/Core/Ast/AstMap/ClassLike/ClassLikeReference.php @@ -4,30 +4,32 @@ namespace Qossmic\Deptrac\Core\Ast\AstMap\ClassLike; -use Qossmic\Deptrac\Contract\Ast\TokenReferenceInterface; use Qossmic\Deptrac\Core\Ast\AstMap\AstInherit; use Qossmic\Deptrac\Core\Ast\AstMap\DependencyToken; use Qossmic\Deptrac\Core\Ast\AstMap\File\FileReference; +use Qossmic\Deptrac\Core\Ast\AstMap\TaggedTokenReference; /** * @psalm-immutable */ -class ClassLikeReference implements TokenReferenceInterface +class ClassLikeReference extends TaggedTokenReference { public readonly ClassLikeType $type; /** * @param AstInherit[] $inherits * @param DependencyToken[] $dependencies + * @param array> $tags */ public function __construct( private readonly ClassLikeToken $classLikeName, ClassLikeType $classLikeType = null, public readonly array $inherits = [], public readonly array $dependencies = [], - public readonly bool $isInternal = false, + public readonly array $tags = [], private readonly ?FileReference $fileReference = null ) { + parent::__construct($tags); $this->type = $classLikeType ?? ClassLikeType::TYPE_CLASSLIKE; } @@ -38,7 +40,7 @@ public function withFileReference(FileReference $astFileReference): self $this->type, $this->inherits, $this->dependencies, - $this->isInternal, + $this->tags, $astFileReference ); } diff --git a/src/Core/Ast/AstMap/ClassLike/ClassLikeReferenceBuilder.php b/src/Core/Ast/AstMap/ClassLike/ClassLikeReferenceBuilder.php index 651bb0a87..217bf1915 100644 --- a/src/Core/Ast/AstMap/ClassLike/ClassLikeReferenceBuilder.php +++ b/src/Core/Ast/AstMap/ClassLike/ClassLikeReferenceBuilder.php @@ -16,47 +16,52 @@ final class ClassLikeReferenceBuilder extends ReferenceBuilder /** * @param list $tokenTemplates + * @param array> $tags */ private function __construct( array $tokenTemplates, string $filepath, private readonly ClassLikeToken $classLikeToken, private readonly ClassLikeType $classLikeType, - private readonly bool $isInternal + private readonly array $tags ) { parent::__construct($tokenTemplates, $filepath); } /** * @param list $classTemplates + * @param array> $tags */ - public static function createClassLike(string $filepath, string $classLikeName, array $classTemplates, bool $isInternal): self + public static function createClassLike(string $filepath, string $classLikeName, array $classTemplates, array $tags): self { - return new self($classTemplates, $filepath, ClassLikeToken::fromFQCN($classLikeName), ClassLikeType::TYPE_CLASSLIKE, $isInternal); + return new self($classTemplates, $filepath, ClassLikeToken::fromFQCN($classLikeName), ClassLikeType::TYPE_CLASSLIKE, $tags); } /** * @param list $classTemplates + * @param array> $tags */ - public static function createClass(string $filepath, string $classLikeName, array $classTemplates, bool $isInternal): self + public static function createClass(string $filepath, string $classLikeName, array $classTemplates, array $tags): self { - return new self($classTemplates, $filepath, ClassLikeToken::fromFQCN($classLikeName), ClassLikeType::TYPE_CLASS, $isInternal); + return new self($classTemplates, $filepath, ClassLikeToken::fromFQCN($classLikeName), ClassLikeType::TYPE_CLASS, $tags); } /** * @param list $classTemplates + * @param array> $tags */ - public static function createTrait(string $filepath, string $classLikeName, array $classTemplates, bool $isInternal): self + public static function createTrait(string $filepath, string $classLikeName, array $classTemplates, array $tags): self { - return new self($classTemplates, $filepath, ClassLikeToken::fromFQCN($classLikeName), ClassLikeType::TYPE_TRAIT, $isInternal); + return new self($classTemplates, $filepath, ClassLikeToken::fromFQCN($classLikeName), ClassLikeType::TYPE_TRAIT, $tags); } /** * @param list $classTemplates + * @param array> $tags */ - public static function createInterface(string $filepath, string $classLikeName, array $classTemplates, bool $isInternal): self + public static function createInterface(string $filepath, string $classLikeName, array $classTemplates, array $tags): self { - return new self($classTemplates, $filepath, ClassLikeToken::fromFQCN($classLikeName), ClassLikeType::TYPE_INTERFACE, $isInternal); + return new self($classTemplates, $filepath, ClassLikeToken::fromFQCN($classLikeName), ClassLikeType::TYPE_INTERFACE, $tags); } /** @internal */ @@ -67,7 +72,7 @@ public function build(): ClassLikeReference $this->classLikeType, $this->inherits, $this->dependencies, - $this->isInternal + $this->tags ); } diff --git a/src/Core/Ast/AstMap/File/FileReferenceBuilder.php b/src/Core/Ast/AstMap/File/FileReferenceBuilder.php index fc097578d..e433b8272 100644 --- a/src/Core/Ast/AstMap/File/FileReferenceBuilder.php +++ b/src/Core/Ast/AstMap/File/FileReferenceBuilder.php @@ -38,10 +38,11 @@ public function useStatement(string $classLikeName, int $occursAtLine): self /** * @param list $templateTypes + * @param array> $tags */ - public function newClass(string $classLikeName, array $templateTypes, bool $isInternal): ClassLikeReferenceBuilder + public function newClass(string $classLikeName, array $templateTypes, array $tags): ClassLikeReferenceBuilder { - $classReference = ClassLikeReferenceBuilder::createClass($this->filepath, $classLikeName, $templateTypes, $isInternal); + $classReference = ClassLikeReferenceBuilder::createClass($this->filepath, $classLikeName, $templateTypes, $tags); $this->classReferences[] = $classReference; return $classReference; @@ -49,10 +50,11 @@ public function newClass(string $classLikeName, array $templateTypes, bool $isIn /** * @param list $templateTypes + * @param array> $tags */ - public function newTrait(string $classLikeName, array $templateTypes, bool $isInternal): ClassLikeReferenceBuilder + public function newTrait(string $classLikeName, array $templateTypes, array $tags): ClassLikeReferenceBuilder { - $classReference = ClassLikeReferenceBuilder::createTrait($this->filepath, $classLikeName, $templateTypes, $isInternal); + $classReference = ClassLikeReferenceBuilder::createTrait($this->filepath, $classLikeName, $templateTypes, $tags); $this->classReferences[] = $classReference; return $classReference; @@ -60,10 +62,11 @@ public function newTrait(string $classLikeName, array $templateTypes, bool $isIn /** * @param list $templateTypes + * @param array> $tags */ - public function newClassLike(string $classLikeName, array $templateTypes, bool $isInternal): ClassLikeReferenceBuilder + public function newClassLike(string $classLikeName, array $templateTypes, array $tags): ClassLikeReferenceBuilder { - $classReference = ClassLikeReferenceBuilder::createClassLike($this->filepath, $classLikeName, $templateTypes, $isInternal); + $classReference = ClassLikeReferenceBuilder::createClassLike($this->filepath, $classLikeName, $templateTypes, $tags); $this->classReferences[] = $classReference; return $classReference; @@ -71,10 +74,11 @@ public function newClassLike(string $classLikeName, array $templateTypes, bool $ /** * @param list $templateTypes + * @param array> $tags */ - public function newInterface(string $classLikeName, array $templateTypes, bool $isInternal): ClassLikeReferenceBuilder + public function newInterface(string $classLikeName, array $templateTypes, array $tags): ClassLikeReferenceBuilder { - $classReference = ClassLikeReferenceBuilder::createInterface($this->filepath, $classLikeName, $templateTypes, $isInternal); + $classReference = ClassLikeReferenceBuilder::createInterface($this->filepath, $classLikeName, $templateTypes, $tags); $this->classReferences[] = $classReference; return $classReference; @@ -82,10 +86,11 @@ public function newInterface(string $classLikeName, array $templateTypes, bool $ /** * @param list $templateTypes + * @param array> $tags */ - public function newFunction(string $functionName, array $templateTypes = []): FunctionReferenceBuilder + public function newFunction(string $functionName, array $templateTypes = [], array $tags = []): FunctionReferenceBuilder { - $functionReference = FunctionReferenceBuilder::create($this->filepath, $functionName, $templateTypes); + $functionReference = FunctionReferenceBuilder::create($this->filepath, $functionName, $templateTypes, $tags); $this->functionReferences[] = $functionReference; return $functionReference; diff --git a/src/Core/Ast/AstMap/Function/FunctionReference.php b/src/Core/Ast/AstMap/Function/FunctionReference.php index 7201c79e2..9ce31a53e 100644 --- a/src/Core/Ast/AstMap/Function/FunctionReference.php +++ b/src/Core/Ast/AstMap/Function/FunctionReference.php @@ -5,29 +5,34 @@ namespace Qossmic\Deptrac\Core\Ast\AstMap\Function; use Qossmic\Deptrac\Contract\Ast\TokenInterface; -use Qossmic\Deptrac\Contract\Ast\TokenReferenceInterface; use Qossmic\Deptrac\Core\Ast\AstMap\DependencyToken; use Qossmic\Deptrac\Core\Ast\AstMap\File\FileReference; +use Qossmic\Deptrac\Core\Ast\AstMap\TaggedTokenReference; /** * @psalm-immutable */ -class FunctionReference implements TokenReferenceInterface +class FunctionReference extends TaggedTokenReference { /** * @param DependencyToken[] $dependencies + * @param array> $tags */ public function __construct( private readonly FunctionToken $functionName, public readonly array $dependencies = [], + public readonly array $tags = [], private readonly ?FileReference $fileReference = null - ) {} + ) { + parent::__construct($tags); + } public function withFileReference(FileReference $astFileReference): self { return new self( $this->functionName, $this->dependencies, + $this->tags, $astFileReference ); } diff --git a/src/Core/Ast/AstMap/Function/FunctionReferenceBuilder.php b/src/Core/Ast/AstMap/Function/FunctionReferenceBuilder.php index fc52bbbcc..1ac328b8f 100644 --- a/src/Core/Ast/AstMap/Function/FunctionReferenceBuilder.php +++ b/src/Core/Ast/AstMap/Function/FunctionReferenceBuilder.php @@ -10,18 +10,27 @@ class FunctionReferenceBuilder extends ReferenceBuilder { /** * @param list $tokenTemplates + * @param array> $tags */ - private function __construct(array $tokenTemplates, string $filepath, private readonly string $functionName) - { - parent::__construct($tokenTemplates, $filepath); + private function __construct( + array $tokenTemplates, + string $filepath, + private readonly string $functionName, + private readonly array $tags + ) { + parent::__construct( + $tokenTemplates, + $filepath + ); } /** * @param list $functionTemplates + * @param array> $tags */ - public static function create(string $filepath, string $functionName, array $functionTemplates): self + public static function create(string $filepath, string $functionName, array $functionTemplates, array $tags): self { - return new self($functionTemplates, $filepath, $functionName); + return new self($functionTemplates, $filepath, $functionName, $tags); } /** @internal */ @@ -29,7 +38,8 @@ public function build(): FunctionReference { return new FunctionReference( FunctionToken::fromFQCN($this->functionName), - $this->dependencies + $this->dependencies, + $this->tags ); } } diff --git a/src/Core/Ast/AstMap/TaggedTokenReference.php b/src/Core/Ast/AstMap/TaggedTokenReference.php new file mode 100644 index 000000000..70b5c4991 --- /dev/null +++ b/src/Core/Ast/AstMap/TaggedTokenReference.php @@ -0,0 +1,35 @@ +> $tags + */ + protected function __construct( + private readonly array $tags + ) {} + + public function hasTag(string $name): bool + { + return isset($this->tags[$name]); + } + + /** + * @return ?list + */ + public function getTagLines(string $name): ?array + { + return $this->tags[$name] ?? null; + } +} diff --git a/src/Core/Ast/Parser/NikicPhpParser/FileReferenceVisitor.php b/src/Core/Ast/Parser/NikicPhpParser/FileReferenceVisitor.php index 617e5136a..529acf436 100644 --- a/src/Core/Ast/Parser/NikicPhpParser/FileReferenceVisitor.php +++ b/src/Core/Ast/Parser/NikicPhpParser/FileReferenceVisitor.php @@ -113,7 +113,7 @@ public function leaveNode(Node $node) /** * @return ?array{PhpDocNode, int} DocNode, comment start line */ - private function getDocNodeCrate(ClassLike $node): ?array + private function getDocNodeCrate(Node $node): ?array { $docComment = $node->getDocComment(); if (null === $docComment) { @@ -129,21 +129,19 @@ private function enterClassLike(ClassLike $node): void $name = $this->getClassReferenceName($node); $docNodeCrate = $this->getDocNodeCrate($node); if (null !== $name) { - $isInternal = false; + $tags = []; + if (null !== $docNodeCrate) { - $isInternal = [] !== array_merge( - $docNodeCrate[0]->getTagsByName('@internal'), - $docNodeCrate[0]->getTagsByName('@deptrac-internal') - ); + $tags = $this->extractTags($docNodeCrate[0]); } match (true) { - $node instanceof Interface_ => $this->enterInterface($name, $node, $isInternal), - $node instanceof Class_ => $this->enterClass($name, $node, $isInternal), + $node instanceof Interface_ => $this->enterInterface($name, $node, $tags), + $node instanceof Class_ => $this->enterClass($name, $node, $tags), $node instanceof Trait_ => $this->currentReference = - $this->fileReferenceBuilder->newTrait($name, $this->templatesFromDocs($node), $isInternal), + $this->fileReferenceBuilder->newTrait($name, $this->templatesFromDocs($node), $tags), default => $this->currentReference = - $this->fileReferenceBuilder->newClassLike($name, $this->templatesFromDocs($node), $isInternal) + $this->fileReferenceBuilder->newClassLike($name, $this->templatesFromDocs($node), $tags) }; } @@ -169,7 +167,14 @@ private function enterFunction(Node\Stmt\Function_ $node): void $name = $node->name->toString(); } - $this->currentReference = $this->fileReferenceBuilder->newFunction($name, $this->templatesFromDocs($node)); + $docNodeCrate = $this->getDocNodeCrate($node); + $tags = []; + + if (null !== $docNodeCrate) { + $tags = $this->extractTags($docNodeCrate[0]); + } + + $this->currentReference = $this->fileReferenceBuilder->newFunction($name, $this->templatesFromDocs($node), $tags); foreach ($node->getParams() as $param) { if (null !== $param->type) { @@ -208,18 +213,24 @@ private function getClassReferenceName(ClassLike $node): ?string return null; } - private function enterInterface(string $name, Interface_ $node, bool $isInternal): void + /** + * @param array> $tags + */ + private function enterInterface(string $name, Interface_ $node, array $tags): void { - $this->currentReference = $this->fileReferenceBuilder->newInterface($name, $this->templatesFromDocs($node), $isInternal); + $this->currentReference = $this->fileReferenceBuilder->newInterface($name, $this->templatesFromDocs($node), $tags); foreach ($node->extends as $extend) { $this->currentReference->implements($extend->toCodeString(), $extend->getLine()); } } - private function enterClass(string $name, Class_ $node, bool $isInternal): void + /** + * @param array> $tags + */ + private function enterClass(string $name, Class_ $node, array $tags): void { - $this->currentReference = $this->fileReferenceBuilder->newClass($name, $this->templatesFromDocs($node), $isInternal); + $this->currentReference = $this->fileReferenceBuilder->newClass($name, $this->templatesFromDocs($node), $tags); if ($node->extends instanceof Name) { $this->currentReference->extends($node->extends->toCodeString(), $node->extends->getLine()); } @@ -298,4 +309,18 @@ private function processClassLikeDocs(array $docNodeCrate): void } } } + + /** + * @return array> + */ + private function extractTags(PhpDocNode $doc): array + { + $tags = []; + + foreach ($doc->getTags() as $tag) { + $tags[$tag->name][] = (string) $tag->value; + } + + return $tags; + } } diff --git a/src/Core/Layer/Collector/TagValueRegexCollector.php b/src/Core/Layer/Collector/TagValueRegexCollector.php new file mode 100644 index 000000000..2d2e6fc81 --- /dev/null +++ b/src/Core/Layer/Collector/TagValueRegexCollector.php @@ -0,0 +1,68 @@ +> $config + */ + public function satisfy(array $config, TokenReferenceInterface $reference): bool + { + if (!$reference instanceof TaggedTokenReferenceInterface) { + return false; + } + + $tagLines = $reference->getTagLines($this->getTagName($config)); + $pattern = $this->getValidatedPattern($config); + + if (null === $tagLines || [] === $tagLines) { + return false; + } + + foreach ($tagLines as $line) { + if (preg_match($pattern, $line)) { + return true; + } + } + + return false; + } + + /** + * @param array|null> $config + * + * @throws InvalidCollectorDefinitionException + */ + protected function getTagName(array $config): string + { + if (!isset($config['tag']) || !is_string($config['tag'])) { + throw InvalidCollectorDefinitionException::invalidCollectorConfiguration('TagValueRegexCollector needs the tag name.'); + } + + if (!preg_match('/^@[-\w]+$/', $config['tag'])) { + throw InvalidCollectorDefinitionException::invalidCollectorConfiguration('TagValueRegexCollector needs a valid tag name.'); + } + + return $config['tag']; + } + + protected function getPattern(array $config): string + { + if (!isset($config['value'])) { + return '/^.?/'; // any string + } + + if (!is_string($config['value'])) { + throw InvalidCollectorDefinitionException::invalidCollectorConfiguration('TagValueRegexCollector regex value must be a string.'); + } + + return $config['value']; + } +} diff --git a/src/Supportive/DependencyInjection/Configuration.php b/src/Supportive/DependencyInjection/Configuration.php index 05f5120fe..7f98a758d 100644 --- a/src/Supportive/DependencyInjection/Configuration.php +++ b/src/Supportive/DependencyInjection/Configuration.php @@ -207,8 +207,10 @@ private function appendEmitterTypes(ArrayNodeDefinition $node): void ->arrayNode('analyser') ->addDefaultsIfNotSet() ->children() + ->scalarNode('internal_tag') + ->defaultNull() + ->end() ->arrayNode('types') - ->isRequired() ->defaultValue([ EmitterType::CLASS_TOKEN->value, EmitterType::FUNCTION_TOKEN->value, diff --git a/tests/Contract/Config/DeptracConfigTest.php b/tests/Contract/Config/DeptracConfigTest.php new file mode 100644 index 000000000..876d3489b --- /dev/null +++ b/tests/Contract/Config/DeptracConfigTest.php @@ -0,0 +1,76 @@ +process($def->getConfigTreeBuilder()->buildTree(), ['deptrac' => $config]); + $this->addToAssertionCount(1); + } + + public static function provideConfig() + { + $config = new DeptracConfig(); + $expected = []; + yield 'empty' => [$config, $expected]; + + $config = (new DeptracConfig())->analyser(AnalyserConfig::create()->types( + EmitterType::FUNCTION_CALL + )); + $expected = [ + 'analyser' => [ + 'types' => [EmitterType::FUNCTION_CALL->value => EmitterType::FUNCTION_CALL->value], + ], + ]; + yield 'analyser types' => [$config, $expected]; + + $config = (new DeptracConfig())->analyser( + AnalyserConfig::create()->internalTag('@layer-internal') + ); + $expected = [ + 'analyser' => [ + 'internal_tag' => '@layer-internal', + ], + ]; + yield 'internal_tag' => [$config, $expected]; + } + + /** + * @dataProvider provideConfig + */ + public function testConfigCompliance(DeptracConfig $config, array $expected): void + { + $array = $config->toArray(); + $this->validateConfig($array); + $this->assertArrayContainsRecursive($expected, $array); + } + + private function assertArrayContainsRecursive(array $expected, array $actual) + { + foreach ($expected as $key => $value) { + self::assertArrayHasKey($key, $actual); + + if (is_array($value)) { + $this->assertIsArray($actual[$key], $key); + + $this->assertArrayContainsRecursive($value, $actual[$key]); + } else { + $this->assertSame($value, $actual[$key], $key); + } + } + } +} diff --git a/tests/Core/Analyser/EventHandler/DependsOnInternalTokenTest.php b/tests/Core/Analyser/EventHandler/DependsOnInternalTokenTest.php new file mode 100644 index 000000000..67949817a --- /dev/null +++ b/tests/Core/Analyser/EventHandler/DependsOnInternalTokenTest.php @@ -0,0 +1,124 @@ + true], + new AnalysisResult() + ); + + return $event; + } + + public function testInvoke(): void + { + $helper = new EventHelper([], new LayerProvider([])); + $handler = new DependsOnInternalToken($helper, ['internal_tag' => '@layer-internal']); + + $event = $this->makeEvent([], []); + $handler->invoke($event); + + $this->assertFalse( + $event->isPropagationStopped(), + 'Propagation should continue if neither reference has the "layer-internal" tag' + ); + + $event = $this->makeEvent(['@layer-internal' => ['']], []); + $handler->invoke($event); + + $this->assertFalse( + $event->isPropagationStopped(), + 'Propagation should continue if only the depender is marked @layer-internal' + ); + + $event = $this->makeEvent([], ['@layer-internal' => ['']]); + $handler->invoke($event); + + $this->assertTrue( + $event->isPropagationStopped(), + 'Propagation should be stopped if the dependent is marked @layer-internal' + ); + + $event = $this->makeEvent([], ['@layer-internal' => ['']], 'DependerLayer'); + $handler->invoke($event); + + $this->assertFalse( + $event->isPropagationStopped(), + 'Propagation should not be stopped if the dependent is marked @layer-internal '. + 'but dependent is in the same layer' + ); + + $event = $this->makeEvent([], ['@deptrac-internal' => ['']]); + $handler->invoke($event); + + $this->assertTrue( + $event->isPropagationStopped(), + 'Propagation should be stopped if the dependent is marked @deptrac-internal' + ); + } + + public function testDefaultInternalTag(): void + { + $helper = new EventHelper([], new LayerProvider([])); + $handler = new DependsOnInternalToken($helper, ['internal_tag' => null]); + + $event = $this->makeEvent([], ['@internal' => ['']]); + $handler->invoke($event); + + $this->assertFalse( + $event->isPropagationStopped(), + 'The @internal tag should not be used per default' + ); + + $event = $this->makeEvent([], ['@deptrac-internal' => ['']]); + $handler->invoke($event); + + $this->assertTrue( + $event->isPropagationStopped(), + 'The @deptrac-internal tag should be used per default' + ); + } +} diff --git a/tests/Core/Ast/AstMap/ClassLike/ClassLikeReferenceTest.php b/tests/Core/Ast/AstMap/ClassLike/ClassLikeReferenceTest.php new file mode 100644 index 000000000..80176c20c --- /dev/null +++ b/tests/Core/Ast/AstMap/ClassLike/ClassLikeReferenceTest.php @@ -0,0 +1,28 @@ + ['foo1'], + '@bar' => ['bar1', 'bar2'], + ]; + + $ref = $this->newWithTags($tags); + + self::assertTrue($ref->hasTag('@foo'), 'has @foo'); + self::assertTrue($ref->hasTag('@bar'), 'has @bar'); + self::assertFalse($ref->hasTag('foo'), 'has foo'); + self::assertFalse($ref->hasTag('@xyzzy'), 'has @xyzzy'); + + self::assertSame(['foo1'], $ref->getTagLines('@foo'), 'get @foo lines'); + self::assertSame(['bar1', 'bar2'], $ref->getTagLines('@bar'), 'get @bar lines'); + self::assertnull($ref->getTagLines('foo'), 'get foo lines'); + self::assertnull($ref->getTagLines('@xyzzy'), 'get @xyzzy lines'); + } +} diff --git a/tests/Core/Ast/Parser/NikicPhpParser/Fixtures/DocTags.php b/tests/Core/Ast/Parser/NikicPhpParser/Fixtures/DocTags.php new file mode 100644 index 000000000..7b75b13d4 --- /dev/null +++ b/tests/Core/Ast/Parser/NikicPhpParser/Fixtures/DocTags.php @@ -0,0 +1,19 @@ +create(ParserFactory::ONLY_PHP7, new Lexer()), - new AstFileReferenceInMemoryCache(), - $typeResolver, - [] - ); + $parser = $this->getParser(); $filePath = __DIR__.'/Fixtures/CountingUseStatements.php'; self::assertCount(1, $parser->parseFile($filePath)->dependencies); @@ -54,13 +48,7 @@ public function testParseDoesNotIgnoreUsesByDefault(): void */ public function testParseAttributes(): void { - $typeResolver = new TypeResolver(); - $parser = new NikicPhpParser( - (new ParserFactory())->create(ParserFactory::ONLY_PHP7, new Lexer()), - new AstFileReferenceInMemoryCache(), - $typeResolver, - [] - ); + $parser = $this->getParser(); $filePath = __DIR__.'/Fixtures/Attributes.php'; $astFileReference = $parser->parseFile($filePath); @@ -85,4 +73,67 @@ public function testParseTemplateTypes(): void $astClassReferences = $astFileReference->classLikeReferences; self::assertCount(0, $astClassReferences[0]->dependencies); } + + public function testParseClassDocTags(): void + { + $parser = $this->getParser(); + $filePath = __DIR__.'/Fixtures/DocTags.php'; + $astFileReference = $parser->parseFile($filePath); + + self::assertCount(2, $astFileReference->classLikeReferences); + $classesByName = $this->refsByName($astFileReference->classLikeReferences); + + $this->assertSame( + [ + '@internal' => [''], + '@note' => ['Note one', 'Note two'], + ], + $classesByName['TaggedThing']->tags + ); + $this->assertSame([], $classesByName['UntaggedThing']->tags); + } + + public function testParseFunctionDocTags(): void + { + $parser = $this->getParser(); + $filePath = __DIR__.'/Fixtures/Functions.php'; + $astFileReference = $parser->parseFile($filePath); + + self::assertCount(2, $astFileReference->functionReferences); + $functionsByName = $this->refsByName($astFileReference->functionReferences); + + $this->assertSame( + ['@param' => ['string $foo', 'string $bar']], + $functionsByName['taggedFunction()']->tags + ); + $this->assertSame([], $functionsByName['untaggedFunction()']->tags); + } + + private function refsByName(array $refs): array + { + $refsByName = []; + + foreach ($refs as $ref) { + $name = preg_replace('/^.*\\\\(\w+(\(\))?)$/', '$1', $ref->getToken()->toString()); + $refsByName[$name] = $ref; + } + + return $refsByName; + } + + private function getParser(): NikicPhpParser + { + $typeResolver = new TypeResolver(); + $parser = new NikicPhpParser( + (new ParserFactory())->create( + ParserFactory::ONLY_PHP7, + new Lexer() + ), + new AstFileReferenceInMemoryCache(), + $typeResolver, + [] + ); + + return $parser; + } } diff --git a/tests/Core/Layer/Collector/AttributeCollectorTest.php b/tests/Core/Layer/Collector/AttributeCollectorTest.php index 3ddb44152..d28667930 100644 --- a/tests/Core/Layer/Collector/AttributeCollectorTest.php +++ b/tests/Core/Layer/Collector/AttributeCollectorTest.php @@ -41,7 +41,7 @@ public static function dataProviderSatisfy(): iterable public function testSatisfy(array $config, bool $expected): void { $classLikeReference = FileReferenceBuilder::create('Foo.php') - ->newClass('App\Foo', [], false) + ->newClass('App\Foo', [], []) ->attribute('App\MyAttribute', 2) ->attribute('MyAttribute', 3) ->build(); diff --git a/tests/Core/Layer/Collector/DirectoryCollectorTest.php b/tests/Core/Layer/Collector/DirectoryCollectorTest.php index 9b88db3f9..d6da24ae9 100644 --- a/tests/Core/Layer/Collector/DirectoryCollectorTest.php +++ b/tests/Core/Layer/Collector/DirectoryCollectorTest.php @@ -34,7 +34,7 @@ public static function dataProviderSatisfy(): iterable public function testSatisfy(array $configuration, string $filePath, bool $expected): void { $fileReferenceBuilder = FileReferenceBuilder::create($filePath); - $fileReferenceBuilder->newClassLike('Test', [], false); + $fileReferenceBuilder->newClassLike('Test', [], []); $fileReference = $fileReferenceBuilder->build(); $actual = $this->collector->satisfy( @@ -48,7 +48,7 @@ public function testSatisfy(array $configuration, string $filePath, bool $expect public function testMissingRegexThrowsException(): void { $fileReferenceBuilder = FileReferenceBuilder::create('/some/path/to/file.php'); - $fileReferenceBuilder->newClassLike('Test', [], false); + $fileReferenceBuilder->newClassLike('Test', [], []); $fileReference = $fileReferenceBuilder->build(); $this->expectException(InvalidCollectorDefinitionException::class); @@ -63,7 +63,7 @@ public function testMissingRegexThrowsException(): void public function testInvalidRegexParam(): void { $fileReferenceBuilder = FileReferenceBuilder::create('/some/path/to/file.php'); - $fileReferenceBuilder->newClassLike('Test', [], false); + $fileReferenceBuilder->newClassLike('Test', [], []); $fileReference = $fileReferenceBuilder->build(); $this->expectException(InvalidCollectorDefinitionException::class); diff --git a/tests/Core/Layer/Collector/ExtendsCollectorTest.php b/tests/Core/Layer/Collector/ExtendsCollectorTest.php index 5190175db..e146881b3 100644 --- a/tests/Core/Layer/Collector/ExtendsCollectorTest.php +++ b/tests/Core/Layer/Collector/ExtendsCollectorTest.php @@ -28,28 +28,28 @@ public function testSatisfy(array $configuration, bool $expected): void { $fooFileReferenceBuilder = FileReferenceBuilder::create('foo.php'); $fooFileReferenceBuilder - ->newClassLike('App\Foo', [], false) + ->newClassLike('App\Foo', [], []) ->implements('App\Bar', 2); $fooFileReference = $fooFileReferenceBuilder->build(); $barFileReferenceBuilder = FileReferenceBuilder::create('bar.php'); $barFileReferenceBuilder - ->newClassLike('App\Bar', [], false) + ->newClassLike('App\Bar', [], []) ->implements('App\Baz', 2); $barFileReference = $barFileReferenceBuilder->build(); $bazFileReferenceBuilder = FileReferenceBuilder::create('baz.php'); - $bazFileReferenceBuilder->newClassLike('App\Baz', [], false); + $bazFileReferenceBuilder->newClassLike('App\Baz', [], []); $bazFileReference = $bazFileReferenceBuilder->build(); $fizTraitFileReferenceBuilder = FileReferenceBuilder::create('fiztrait.php'); $fizTraitFileReferenceBuilder - ->newClassLike('App\FizTrait', [], false); + ->newClassLike('App\FizTrait', [], []); $fizTraitFileReference = $fizTraitFileReferenceBuilder->build(); $fooBarFileReferenceBuilder = FileReferenceBuilder::create('foobar.php'); $fooBarFileReferenceBuilder - ->newClassLike('App\FooBar', [], false) + ->newClassLike('App\FooBar', [], []) ->extends('App\Foo', 2) ->trait('App\FizTrait', 4); $fooBarFileReference = $fooBarFileReferenceBuilder->build(); diff --git a/tests/Core/Layer/Collector/GlobCollectorTest.php b/tests/Core/Layer/Collector/GlobCollectorTest.php index 710550369..9d297a4d0 100644 --- a/tests/Core/Layer/Collector/GlobCollectorTest.php +++ b/tests/Core/Layer/Collector/GlobCollectorTest.php @@ -34,7 +34,7 @@ public static function dataProviderSatisfy(): iterable public function testSatisfy(array $configuration, string $filePath, bool $expected): void { $fileReferenceBuilder = FileReferenceBuilder::create($filePath); - $fileReferenceBuilder->newClassLike('Test', [], false); + $fileReferenceBuilder->newClassLike('Test', [], []); $fileReference = $fileReferenceBuilder->build(); $actual = $this->collector->satisfy( diff --git a/tests/Core/Layer/Collector/ImplementsCollectorTest.php b/tests/Core/Layer/Collector/ImplementsCollectorTest.php index be63dcf21..0532bb483 100644 --- a/tests/Core/Layer/Collector/ImplementsCollectorTest.php +++ b/tests/Core/Layer/Collector/ImplementsCollectorTest.php @@ -28,28 +28,28 @@ public function testSatisfy(array $configuration, bool $expected): void { $fooFileReferenceBuilder = FileReferenceBuilder::create('foo.php'); $fooFileReferenceBuilder - ->newClassLike('App\Foo', [], false) + ->newClassLike('App\Foo', [], []) ->implements('App\Bar', 2); $fooFileReference = $fooFileReferenceBuilder->build(); $barFileReferenceBuilder = FileReferenceBuilder::create('bar.php'); $barFileReferenceBuilder - ->newClassLike('App\Bar', [], false) + ->newClassLike('App\Bar', [], []) ->implements('App\Baz', 2); $barFileReference = $barFileReferenceBuilder->build(); $bazFileReferenceBuilder = FileReferenceBuilder::create('baz.php'); - $bazFileReferenceBuilder->newClassLike('App\Baz', [], false); + $bazFileReferenceBuilder->newClassLike('App\Baz', [], []); $bazFileReference = $bazFileReferenceBuilder->build(); $fizTraitFileReferenceBuilder = FileReferenceBuilder::create('fiztrait.php'); $fizTraitFileReferenceBuilder - ->newClassLike('App\FizTrait', [], false); + ->newClassLike('App\FizTrait', [], []); $fizTraitFileReference = $fizTraitFileReferenceBuilder->build(); $fooBarFileReferenceBuilder = FileReferenceBuilder::create('foobar.php'); $fooBarFileReferenceBuilder - ->newClassLike('App\FooBar', [], false) + ->newClassLike('App\FooBar', [], []) ->extends('App\Foo', 2) ->trait('App\FizTrait', 4); $fooBarFileReference = $fooBarFileReferenceBuilder->build(); diff --git a/tests/Core/Layer/Collector/InheritsCollectorTest.php b/tests/Core/Layer/Collector/InheritsCollectorTest.php index e83cf3c1a..95787bb95 100644 --- a/tests/Core/Layer/Collector/InheritsCollectorTest.php +++ b/tests/Core/Layer/Collector/InheritsCollectorTest.php @@ -28,28 +28,28 @@ public function testSatisfy(array $configuration, bool $expected): void { $fooFileReferenceBuilder = FileReferenceBuilder::create('foo.php'); $fooFileReferenceBuilder - ->newClassLike('App\Foo', [], false) + ->newClassLike('App\Foo', [], []) ->implements('App\Bar', 2); $fooFileReference = $fooFileReferenceBuilder->build(); $barFileReferenceBuilder = FileReferenceBuilder::create('bar.php'); $barFileReferenceBuilder - ->newClassLike('App\Bar', [], false) + ->newClassLike('App\Bar', [], []) ->implements('App\Baz', 2); $barFileReference = $barFileReferenceBuilder->build(); $bazFileReferenceBuilder = FileReferenceBuilder::create('baz.php'); - $bazFileReferenceBuilder->newClassLike('App\Baz', [], false); + $bazFileReferenceBuilder->newClassLike('App\Baz', [], []); $bazFileReference = $bazFileReferenceBuilder->build(); $fizTraitFileReferenceBuilder = FileReferenceBuilder::create('fiztrait.php'); $fizTraitFileReferenceBuilder - ->newClassLike('App\FizTrait', [], false); + ->newClassLike('App\FizTrait', [], []); $fizTraitFileReference = $fizTraitFileReferenceBuilder->build(); $fooBarFileReferenceBuilder = FileReferenceBuilder::create('foobar.php'); $fooBarFileReferenceBuilder - ->newClassLike('App\FooBar', [], false) + ->newClassLike('App\FooBar', [], []) ->extends('App\Foo', 2) ->trait('App\FizTrait', 4); $fooBarFileReference = $fooBarFileReferenceBuilder->build(); diff --git a/tests/Core/Layer/Collector/TagValueRegexCollectorTest.php b/tests/Core/Layer/Collector/TagValueRegexCollectorTest.php new file mode 100644 index 000000000..bfe7b0acf --- /dev/null +++ b/tests/Core/Layer/Collector/TagValueRegexCollectorTest.php @@ -0,0 +1,114 @@ +collector = new TagValueRegexCollector(); + } + + public static function dataProviderSatisfy(): iterable + { + yield [TagValueRegexConfig::create('@foo'), ['@foo' => ['']]]; + + yield [TagValueRegexConfig::create('@foo'), ['@foo' => ['anything']]]; + + yield [ + TagValueRegexConfig::create('@foo-bar', '/some/'), + ['@xyz' => [''], '@foo-bar' => ['anything', 'something']], + ]; + + yield [ + TagValueRegexConfig::create('@foo-bar')->match('!thing$!'), + ['@xyz' => [''], '@foo-bar' => ['anything', 'something']], + ]; + } + + /** + * @dataProvider dataProviderSatisfy + */ + public function testSatisfy(TagValueRegexConfig $configuration, array $tags): void + { + $actual = $this->collector->satisfy( + $configuration->toArray(), + new ClassLikeReference(ClassLikeToken::fromFQCN('Dummy'), ClassLikeType::TYPE_CLASS, [], [], $tags) + ); + + self::assertTrue($actual); + } + + public static function dataProviderNotSatisfy(): iterable + { + yield [TagValueRegexConfig::create('@foo'), ['@bar' => ['anything']]]; + + yield [ + TagValueRegexConfig::create('@foo', '/something/'), + ['@xyz' => [''], '@foo' => ['anything', 'another thing']], + ]; + yield [ + TagValueRegexConfig::create('@foo', '/^thing/'), + ['@xyz' => [''], '@foo' => ['anything', 'another thing']], + ]; + } + + /** + * @dataProvider dataProviderNotSatisfy + */ + public function testNotSatisfy(TagValueRegexConfig $configuration, array $tags): void + { + $actual = $this->collector->satisfy( + $configuration->toArray(), + new ClassLikeReference(ClassLikeToken::fromFQCN('Dummy'), ClassLikeType::TYPE_CLASS, [], [], $tags) + ); + + self::assertFalse($actual); + } + + public static function dataProviderBadConfig(): iterable + { + yield 'empty' => [[]]; + + yield 'no tag name' => [['value' => '/.+/']]; + + yield 'empty tag name' => [['tag' => '']]; + + yield 'tag name with missing ampersand' => [['tag' => 'foo']]; + + yield 'tag name with space' => [['tag' => '@foo bar']]; + + yield 'non-string tag name' => [['tag' => 1234]]; + + yield 'non-string regex value' => [['tag' => '@test', 'value' => 1234]]; + + yield 'bad regex value' => [['tag' => '@test', 'value' => '(((]]]']]; + } + + /** + * @dataProvider dataProviderBadConfig + */ + public function testBadConfig($config): void + { + $this->expectException(InvalidCollectorDefinitionException::class); + + $this->collector->satisfy( + $config, + new ClassLikeReference(ClassLikeToken::fromFQCN('Foo')) + ); + } +} diff --git a/tests/Core/Layer/Collector/UsesCollectorTest.php b/tests/Core/Layer/Collector/UsesCollectorTest.php index f2374dcf0..d9f631044 100644 --- a/tests/Core/Layer/Collector/UsesCollectorTest.php +++ b/tests/Core/Layer/Collector/UsesCollectorTest.php @@ -28,28 +28,28 @@ public function testSatisfy(array $configuration, bool $expected): void { $fooFileReferenceBuilder = FileReferenceBuilder::create('foo.php'); $fooFileReferenceBuilder - ->newClassLike('App\Foo', [], false) + ->newClassLike('App\Foo', [], []) ->implements('App\Bar', 2); $fooFileReference = $fooFileReferenceBuilder->build(); $barFileReferenceBuilder = FileReferenceBuilder::create('bar.php'); $barFileReferenceBuilder - ->newClassLike('App\Bar', [], false) + ->newClassLike('App\Bar', [], []) ->implements('App\Baz', 2); $barFileReference = $barFileReferenceBuilder->build(); $bazFileReferenceBuilder = FileReferenceBuilder::create('baz.php'); - $bazFileReferenceBuilder->newClassLike('App\Baz', [], false); + $bazFileReferenceBuilder->newClassLike('App\Baz', [], []); $bazFileReference = $bazFileReferenceBuilder->build(); $fizTraitFileReferenceBuilder = FileReferenceBuilder::create('fiztrait.php'); $fizTraitFileReferenceBuilder - ->newClassLike('App\FizTrait', [], false); + ->newClassLike('App\FizTrait', [], []); $fizTraitFileReference = $fizTraitFileReferenceBuilder->build(); $fooBarFileReferenceBuilder = FileReferenceBuilder::create('foobar.php'); $fooBarFileReferenceBuilder - ->newClassLike('App\FooBar', [], false) + ->newClassLike('App\FooBar', [], []) ->extends('App\Foo', 2) ->trait('App\FizTrait', 4); $fooBarFileReference = $fooBarFileReferenceBuilder->build(); diff --git a/tests/Supportive/DependencyInjection/DeptracExtensionTest.php b/tests/Supportive/DependencyInjection/DeptracExtensionTest.php index aa0b00908..b2d607b0b 100644 --- a/tests/Supportive/DependencyInjection/DeptracExtensionTest.php +++ b/tests/Supportive/DependencyInjection/DeptracExtensionTest.php @@ -17,7 +17,8 @@ final class DeptracExtensionTest extends TestCase { private ContainerBuilder $container; private DeptracExtension $extension; - private array $formatterDefaults = [ + + private const FORMATTER_DEFAULTS = [ 'graphviz' => [ 'hidden_layers' => [], 'groups' => [], @@ -32,6 +33,16 @@ final class DeptracExtensionTest extends TestCase ], ]; + private const ANALYSER_DEFAULTS = [ + 'internal_tag' => null, + 'types' => [ + // Unfortunately, we can't use the enum type here, see + // https://wiki.php.net/rfc/fetch_property_in_const_expressions + 'class', // ClassLikeType::CLASS + 'function', // ClassLikeType::FUNCTION + ], + ]; + protected function setUp(): void { parent::setUp(); @@ -51,8 +62,8 @@ public function testDefaults(): void self::assertSame([], $this->container->getParameter('layers')); self::assertSame([], $this->container->getParameter('ruleset')); self::assertSame([], $this->container->getParameter('skip_violations')); - self::assertSame($this->formatterDefaults, $this->container->getParameter('formatters')); - self::assertSame(['types' => [EmitterType::CLASS_TOKEN->value, EmitterType::FUNCTION_TOKEN->value]], $this->container->getParameter('analyser')); + self::assertSame(self::FORMATTER_DEFAULTS, $this->container->getParameter('formatters')); + self::assertSame(self::ANALYSER_DEFAULTS, $this->container->getParameter('analyser')); self::assertSame(true, $this->container->getParameter('ignore_uncovered_internal_classes')); } @@ -69,8 +80,8 @@ public function testDefaultsWithEmptyRoot(): void self::assertSame([], $this->container->getParameter('layers')); self::assertSame([], $this->container->getParameter('ruleset')); self::assertSame([], $this->container->getParameter('skip_violations')); - self::assertSame($this->formatterDefaults, $this->container->getParameter('formatters')); - self::assertSame(['types' => [EmitterType::CLASS_TOKEN->value, EmitterType::FUNCTION_TOKEN->value]], $this->container->getParameter('analyser')); + self::assertSame(self::FORMATTER_DEFAULTS, $this->container->getParameter('formatters')); + self::assertSame(self::ANALYSER_DEFAULTS, $this->container->getParameter('analyser')); self::assertSame(true, $this->container->getParameter('ignore_uncovered_internal_classes')); } @@ -307,10 +318,12 @@ public function testNullAnalyser(): void ], ]; - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The child config "types" under "deptrac.analyser" must be configured.'); - $this->extension->load($configs, $this->container); + + self::assertSame( + self::ANALYSER_DEFAULTS, + $this->container->getParameter('analyser') + ); } public function testEmptyAnalyser(): void @@ -321,10 +334,12 @@ public function testEmptyAnalyser(): void ], ]; - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The child config "types" under "deptrac.analyser" must be configured.'); - $this->extension->load($configs, $this->container); + + self::assertSame( + self::ANALYSER_DEFAULTS, + $this->container->getParameter('analyser') + ); } public function testInvalidAnalyserTypes(): void @@ -333,7 +348,7 @@ public function testInvalidAnalyserTypes(): void 'deptrac' => [ 'analyser' => [ 'types' => ['invalid'], - ], + ] + self::ANALYSER_DEFAULTS, ], ]; @@ -349,14 +364,14 @@ public function testNullAnalyserTypes(): void 'deptrac' => [ 'analyser' => [ 'types' => null, - ], + ] + self::ANALYSER_DEFAULTS, ], ]; $this->extension->load($configs, $this->container); self::assertSame( - ['types' => []], + ['types' => []] + self::ANALYSER_DEFAULTS, $this->container->getParameter('analyser') ); } @@ -367,14 +382,14 @@ public function testEmptyAnalyserTypes(): void 'deptrac' => [ 'analyser' => [ 'types' => [], - ], + ] + self::ANALYSER_DEFAULTS, ], ]; $this->extension->load($configs, $this->container); self::assertSame( - ['types' => []], + ['types' => []] + self::ANALYSER_DEFAULTS, $this->container->getParameter('analyser') ); } @@ -385,14 +400,15 @@ public function testAnalyserWithDuplicateTypes(): void 'deptrac' => [ 'analyser' => [ 'types' => [EmitterType::CLASS_TOKEN->value, EmitterType::CLASS_TOKEN->value], - ], + ] + self::ANALYSER_DEFAULTS, ], ]; $this->extension->load($configs, $this->container); self::assertSame( - ['types' => [EmitterType::CLASS_TOKEN->value, EmitterType::CLASS_TOKEN->value]], + ['types' => [EmitterType::CLASS_TOKEN->value, EmitterType::CLASS_TOKEN->value]] + + self::ANALYSER_DEFAULTS, $this->container->getParameter('analyser') ); } @@ -422,7 +438,7 @@ public function testGraphvizFormatterWithEmptyNodes(): void $this->extension->load($configs, $this->container); - $expectedFormatterConfig = $this->formatterDefaults; + $expectedFormatterConfig = self::FORMATTER_DEFAULTS; $actualFormatterConfig = $this->container->getParameter('formatters'); krsort($expectedFormatterConfig); @@ -445,7 +461,7 @@ public function testGraphvizFormatterWithOldPointToGroupsConfig(): void $this->extension->load($configs, $this->container); - $expectedFormatterConfig = $this->formatterDefaults; + $expectedFormatterConfig = self::FORMATTER_DEFAULTS; $expectedFormatterConfig['graphviz'] = [ 'point_to_groups' => true, 'hidden_layers' => [], @@ -474,7 +490,7 @@ public function testGraphvizFormattersWithHiddenLayers(): void $this->extension->load($configs, $this->container); - $expectedFormatterConfig = $this->formatterDefaults; + $expectedFormatterConfig = self::FORMATTER_DEFAULTS; $expectedFormatterConfig['graphviz'] = [ 'hidden_layers' => ['Utils'], 'groups' => [ @@ -499,7 +515,7 @@ public function testCodeclimateFormatterWithEmptyNodes(): void $this->extension->load($configs, $this->container); - $expectedFormatterConfig = $this->formatterDefaults; + $expectedFormatterConfig = self::FORMATTER_DEFAULTS; $actualFormatterConfig = $this->container->getParameter('formatters'); krsort($expectedFormatterConfig);