From 3b6166d4f45d9cdbdf77145841b98bbb01730aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Wed, 3 Dec 2025 22:13:31 +0100 Subject: [PATCH] Rename DependencyConstraintsRule to ForbiddenDependenciesRule #20 --- docs/Rules.md | 3 +- docs/rules/Dependency-Constraints-Rule.md | 29 +- .../DependencyConstraintsRule.php | 412 +---------------- .../ForbiddenDependenciesRule.php | 425 ++++++++++++++++++ .../DependencyConstraintsRuleFqcnTest.php | 86 ++-- ...dencyConstraintsRuleSelectiveTypesTest.php | 10 +- 6 files changed, 506 insertions(+), 459 deletions(-) create mode 100644 src/Architecture/ForbiddenDependenciesRule.php diff --git a/docs/Rules.md b/docs/Rules.md index 968e3d3..f76bd2e 100644 --- a/docs/Rules.md +++ b/docs/Rules.md @@ -55,8 +55,9 @@ services: - phpstan.rules.rule # Dependency constraints - enforce layer boundaries + # Note: Use ForbiddenDependenciesRule (DependencyConstraintsRule is deprecated) - - class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule + class: Phauthentic\PHPStanRules\Architecture\ForbiddenDependenciesRule arguments: forbiddenDependencies: # Domain layer cannot depend on Application, Infrastructure, or Presentation diff --git a/docs/rules/Dependency-Constraints-Rule.md b/docs/rules/Dependency-Constraints-Rule.md index a58bc7f..17f2de7 100644 --- a/docs/rules/Dependency-Constraints-Rule.md +++ b/docs/rules/Dependency-Constraints-Rule.md @@ -1,5 +1,7 @@ # Dependency Constraints Rule +> **⚠️ DEPRECATED:** This rule has been renamed to `ForbiddenDependenciesRule`. The `DependencyConstraintsRule` class is kept for backward compatibility but will be removed in a future major version. Please update your configuration to use `ForbiddenDependenciesRule` instead. + Enforces dependency constraints between namespaces by checking `use` statements and optionally fully qualified class names (FQCNs). The constructor takes an array of namespace dependencies. The key is the namespace that should not depend on the namespaces in the array of values. @@ -10,6 +12,21 @@ In the example below nothing from `App\Domain` can depend on anything from `App\ ### Basic Usage (Use Statements Only) +**Recommended (using new class name):** + +```neon + - + class: Phauthentic\PHPStanRules\Architecture\ForbiddenDependenciesRule + arguments: + forbiddenDependencies: [ + '/^App\\Domain(?:\\\w+)*$/': ['/^App\\Controller\\/'] + ] + tags: + - phpstan.rules.rule +``` + +**Deprecated (backward compatibility):** + ```neon - class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule @@ -23,9 +40,11 @@ In the example below nothing from `App\Domain` can depend on anything from `App\ ### With FQCN Checking Enabled +**Recommended (using new class name):** + ```neon - - class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule + class: Phauthentic\PHPStanRules\Architecture\ForbiddenDependenciesRule arguments: forbiddenDependencies: [ '/^App\\Capability(?:\\\w+)*$/': [ @@ -40,9 +59,11 @@ In the example below nothing from `App\Domain` can depend on anything from `App\ ### With Selective Reference Types +**Recommended (using new class name):** + ```neon - - class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule + class: Phauthentic\PHPStanRules\Architecture\ForbiddenDependenciesRule arguments: forbiddenDependencies: [ '/^App\\Capability(?:\\\w+)*$/': [ @@ -86,7 +107,7 @@ This example prevents usage of PHP's built-in `DateTime` and `DateTimeImmutable` ```neon - - class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule + class: Phauthentic\PHPStanRules\Architecture\ForbiddenDependenciesRule arguments: forbiddenDependencies: [ '/^App\\Capability(?:\\\w+)*$/': [ @@ -113,7 +134,7 @@ If you only want to check specific reference types (e.g., to improve performance ```neon - - class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule + class: Phauthentic\PHPStanRules\Architecture\ForbiddenDependenciesRule arguments: forbiddenDependencies: [ '/^App\\Capability(?:\\\w+)*$/': [ diff --git a/src/Architecture/DependencyConstraintsRule.php b/src/Architecture/DependencyConstraintsRule.php index 5791d1a..d79e08b 100644 --- a/src/Architecture/DependencyConstraintsRule.php +++ b/src/Architecture/DependencyConstraintsRule.php @@ -4,20 +4,6 @@ namespace Phauthentic\PHPStanRules\Architecture; -use PhpParser\Node; -use PhpParser\Node\ComplexType; -use PhpParser\Node\Identifier; -use PhpParser\Node\IntersectionType; -use PhpParser\Node\Name; -use PhpParser\Node\NullableType; -use PhpParser\Node\Stmt\Use_; -use PhpParser\Node\UnionType; -use PHPStan\Analyser\Scope; -use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; -use PHPStan\Rules\RuleErrorBuilder; -use PHPStan\ShouldNotHappenException; - /** * Specification: * @@ -26,399 +12,13 @@ * - Reports an error if a forbidden dependency is detected. * - Optionally checks fully qualified class names (FQCNs) in various contexts. * - * @implements Rule + * @deprecated Use ForbiddenDependenciesRule instead. This class is kept for backward compatibility. + * + * @see ForbiddenDependenciesRule */ -class DependencyConstraintsRule implements Rule +class DependencyConstraintsRule extends ForbiddenDependenciesRule { - private const ERROR_MESSAGE = 'Dependency violation: A class in namespace `%s` is not allowed to depend on `%s`.'; - - private const IDENTIFIER = 'phauthentic.architecture.dependencyConstraints'; - - private const ALL_REFERENCE_TYPES = [ - 'new', - 'param', - 'return', - 'property', - 'static_call', - 'static_property', - 'class_const', - 'instanceof', - 'catch', - 'extends', - 'implements', - ]; - - /** - * @var array> - * An array where the key is a regex for the source namespace and the value is - * an array of regexes for disallowed dependency namespaces. - * e.g., ['#^App\\Domain\\.*#' => ['#^App\\Infrastructure\\.*#']] - */ - private array $forbiddenDependencies; - - /** - * @var bool - */ - private bool $checkFqcn; - - /** - * @var array - */ - private array $fqcnReferenceTypes; - - /** - * @param array> $forbiddenDependencies - * @param bool $checkFqcn Enable checking of fully qualified class names (default: false for backward compatibility) - * @param array $fqcnReferenceTypes Which reference types to check when checkFqcn is enabled (default: all) - */ - public function __construct( - array $forbiddenDependencies, - bool $checkFqcn = false, - array $fqcnReferenceTypes = self::ALL_REFERENCE_TYPES - ) { - $this->forbiddenDependencies = $forbiddenDependencies; - $this->checkFqcn = $checkFqcn; - $this->fqcnReferenceTypes = $fqcnReferenceTypes; - } - - public function getNodeType(): string - { - return Node::class; - } - - /** - * @return array - * @throws ShouldNotHappenException - */ - public function processNode(Node $node, Scope $scope): array - { - $currentNamespace = $scope->getNamespace(); - if ($currentNamespace === null) { - return []; - } - - $errors = []; - - // Process use statements (original behavior - always active) - if ($node instanceof Use_) { - foreach ($this->forbiddenDependencies as $sourceNamespacePattern => $disallowedDependencyPatterns) { - if (!preg_match($sourceNamespacePattern, $currentNamespace)) { - continue; - } - - $errors = $this->validateUseStatements($node, $disallowedDependencyPatterns, $currentNamespace, $errors); - } - } - - // Process FQCN references (new behavior - optional) - if ($this->checkFqcn) { - $errors = array_merge($errors, $this->processFqcnNode($node, $currentNamespace)); - } - - return $errors; - } - - /** - * @param Use_ $node - * @param array $disallowedDependencyPatterns - * @param string $currentNamespace - * @param array $errors - * @return array - * @throws ShouldNotHappenException - */ - public function validateUseStatements(Use_ $node, array $disallowedDependencyPatterns, string $currentNamespace, array $errors): array - { - foreach ($node->uses as $use) { - $usedClassName = $use->name->toString(); - foreach ($disallowedDependencyPatterns as $disallowedPattern) { - if (preg_match($disallowedPattern, $usedClassName)) { - $errors[] = RuleErrorBuilder::message(sprintf( - self::ERROR_MESSAGE, - $currentNamespace, - $usedClassName - )) - ->identifier(self::IDENTIFIER) - ->line($use->getStartLine()) - ->build(); - } - } - } - - return $errors; - } - - /** - * Process FQCN references in various node types - * - * @param Node $node - * @param string $currentNamespace - * @return array - */ - private function processFqcnNode(Node $node, string $currentNamespace): array - { - $classNames = $this->extractClassNamesFromFqcnNode($node); - - $errors = []; - foreach ($classNames as $className) { - $errors = array_merge($errors, $this->validateClassReference($className, $currentNamespace, $node)); - } - - return $errors; - } - - /** - * Extract class names from FQCN nodes based on node type - * - * @param Node $node - * @return array - */ - private function extractClassNamesFromFqcnNode(Node $node): array - { - $classNames = []; - - if ($this->isExpressionNode($node)) { - $classNames = $this->extractFromExpressionNode($node); - } elseif ($this->isStatementNode($node)) { - $classNames = $this->extractFromStatementNode($node); - } - - return $classNames; - } - - /** - * Check if node is an expression node we want to process - * - * @param Node $node - * @return bool - */ - private function isExpressionNode(Node $node): bool - { - return $node instanceof Node\Expr\New_ - || $node instanceof Node\Expr\StaticCall - || $node instanceof Node\Expr\StaticPropertyFetch - || $node instanceof Node\Expr\ClassConstFetch - || $node instanceof Node\Expr\Instanceof_; - } - - /** - * Check if node is a statement node we want to process - * - * @param Node $node - * @return bool - */ - private function isStatementNode(Node $node): bool - { - return $node instanceof Node\Stmt\Catch_ - || $node instanceof Node\Stmt\Class_ - || $node instanceof Node\Stmt\ClassMethod - || $node instanceof Node\Stmt\Property; - } - - /** - * Extract class names from expression nodes - * - * @param Node $node - * @return array - */ - private function extractFromExpressionNode(Node $node): array - { - $mapping = [ - Node\Expr\New_::class => 'new', - Node\Expr\StaticCall::class => 'static_call', - Node\Expr\StaticPropertyFetch::class => 'static_property', - Node\Expr\ClassConstFetch::class => 'class_const', - Node\Expr\Instanceof_::class => 'instanceof', - ]; - - foreach ($mapping as $nodeClass => $referenceType) { - if ($node instanceof $nodeClass && $this->shouldCheckReferenceType($referenceType)) { - return $this->extractClassNamesFromNode($node->class); - } - } - - return []; - } - - /** - * Extract class names from statement nodes - * - * @param Node $node - * @return array - */ - private function extractFromStatementNode(Node $node): array - { - $classNames = []; - - if ($node instanceof Node\Stmt\Catch_ && $this->shouldCheckReferenceType('catch')) { - $classNames = $this->extractFromCatchNode($node); - } elseif ($node instanceof Node\Stmt\Class_) { - $classNames = $this->extractFromClassNode($node); - } elseif ($node instanceof Node\Stmt\ClassMethod) { - $classNames = $this->extractFromClassMethodNode($node); - } elseif ($node instanceof Node\Stmt\Property && $this->shouldCheckReferenceType('property')) { - if ($node->type !== null) { - $classNames = $this->extractClassNamesFromType($node->type); - } - } - - return $classNames; - } - - /** - * Extract class names from catch nodes - * - * @param Node\Stmt\Catch_ $node - * @return array - */ - private function extractFromCatchNode(Node\Stmt\Catch_ $node): array - { - $classNames = []; - foreach ($node->types as $type) { - $classNames = array_merge($classNames, $this->extractClassNamesFromNode($type)); - } - return $classNames; - } - - /** - * Extract class names from class nodes (extends/implements) - * - * @param Node\Stmt\Class_ $node - * @return array - */ - private function extractFromClassNode(Node\Stmt\Class_ $node): array - { - $classNames = []; - - if ($node->extends !== null && $this->shouldCheckReferenceType('extends')) { - $classNames = array_merge($classNames, $this->extractClassNamesFromNode($node->extends)); - } - - if ($this->shouldCheckReferenceType('implements')) { - foreach ($node->implements as $interface) { - $classNames = array_merge($classNames, $this->extractClassNamesFromNode($interface)); - } - } - - return $classNames; - } - - /** - * Extract class names from class method nodes (parameters and return types) - * - * @param Node\Stmt\ClassMethod $node - * @return array - */ - private function extractFromClassMethodNode(Node\Stmt\ClassMethod $node): array - { - $classNames = []; - - if ($this->shouldCheckReferenceType('param')) { - foreach ($node->params as $param) { - if ($param->type !== null) { - $classNames = array_merge($classNames, $this->extractClassNamesFromType($param->type)); - } - } - } - - if ($this->shouldCheckReferenceType('return') && $node->returnType !== null) { - $classNames = array_merge($classNames, $this->extractClassNamesFromType($node->returnType)); - } - - return $classNames; - } - - /** - * Check if a reference type should be validated - * - * @param string $referenceType - * @return bool - */ - private function shouldCheckReferenceType(string $referenceType): bool - { - return in_array($referenceType, $this->fqcnReferenceTypes, true); - } - - /** - * Extract class names from a node (handles Name nodes) - * - * @param Node|Identifier|Name|ComplexType $node - * @return array - */ - private function extractClassNamesFromNode($node): array - { - if ($node instanceof Name && $this->isFullyQualifiedName($node)) { - return [$node->toString()]; - } - return []; - } - - /** - * Extract class names from type declarations (handles complex types) - * - * @param Identifier|Name|ComplexType $type - * @return array - */ - private function extractClassNamesFromType($type): array - { - $classNames = []; - - if ($type instanceof Name) { - if ($this->isFullyQualifiedName($type)) { - $classNames[] = $type->toString(); - } - } elseif ($type instanceof NullableType) { - $classNames = array_merge($classNames, $this->extractClassNamesFromType($type->type)); - } elseif ($type instanceof UnionType || $type instanceof IntersectionType) { - foreach ($type->types as $subType) { - $classNames = array_merge($classNames, $this->extractClassNamesFromType($subType)); - } - } - - return $classNames; - } - - /** - * Check if a Name node represents a fully qualified class name - * - * @param Name $name - * @return bool - */ - private function isFullyQualifiedName(Name $name): bool - { - return $name instanceof Name\FullyQualified; - } - - /** - * Validate a class reference against forbidden dependencies - * - * @param string $className - * @param string $currentNamespace - * @param Node $node - * @return array - */ - private function validateClassReference(string $className, string $currentNamespace, Node $node): array - { - $errors = []; - - foreach ($this->forbiddenDependencies as $sourceNamespacePattern => $disallowedDependencyPatterns) { - if (!preg_match($sourceNamespacePattern, $currentNamespace)) { - continue; - } - - foreach ($disallowedDependencyPatterns as $disallowedPattern) { - if (preg_match($disallowedPattern, $className)) { - $errors[] = RuleErrorBuilder::message(sprintf( - self::ERROR_MESSAGE, - $currentNamespace, - $className - )) - ->identifier(self::IDENTIFIER) - ->line($node->getLine()) - ->build(); - } - } - } + protected const ERROR_MESSAGE = 'Dependency violation: A class in namespace `%s` is not allowed to depend on `%s`.'; - return $errors; - } + protected const IDENTIFIER = 'phauthentic.architecture.dependencyConstraints'; } diff --git a/src/Architecture/ForbiddenDependenciesRule.php b/src/Architecture/ForbiddenDependenciesRule.php new file mode 100644 index 0000000..fd036c2 --- /dev/null +++ b/src/Architecture/ForbiddenDependenciesRule.php @@ -0,0 +1,425 @@ + + */ +class ForbiddenDependenciesRule implements Rule +{ + protected const ERROR_MESSAGE = 'Forbidden dependency: A class in namespace `%s` is not allowed to depend on `%s`.'; + + protected const IDENTIFIER = 'phauthentic.architecture.forbiddenDependencies'; + + private const ALL_REFERENCE_TYPES = [ + 'new', + 'param', + 'return', + 'property', + 'static_call', + 'static_property', + 'class_const', + 'instanceof', + 'catch', + 'extends', + 'implements', + ]; + + /** + * @var array> + * An array where the key is a regex for the source namespace and the value is + * an array of regexes for disallowed dependency namespaces. + * e.g., ['#^App\\Domain\\.*#' => ['#^App\\Infrastructure\\.*#']] + */ + private array $forbiddenDependencies; + + /** + * @var bool + */ + private bool $checkFqcn; + + /** + * @var array + */ + private array $fqcnReferenceTypes; + + /** + * @param array> $forbiddenDependencies + * @param bool $checkFqcn Enable checking of fully qualified class names (default: false for backward compatibility) + * @param array $fqcnReferenceTypes Which reference types to check when checkFqcn is enabled (default: all) + */ + public function __construct( + array $forbiddenDependencies, + bool $checkFqcn = false, + array $fqcnReferenceTypes = self::ALL_REFERENCE_TYPES + ) { + $this->forbiddenDependencies = $forbiddenDependencies; + $this->checkFqcn = $checkFqcn; + $this->fqcnReferenceTypes = $fqcnReferenceTypes; + } + + public function getNodeType(): string + { + return Node::class; + } + + /** + * @return array + * @throws ShouldNotHappenException + */ + public function processNode(Node $node, Scope $scope): array + { + $currentNamespace = $scope->getNamespace(); + if ($currentNamespace === null) { + return []; + } + + $errors = []; + + // Process use statements (original behavior - always active) + if ($node instanceof Use_) { + foreach ($this->forbiddenDependencies as $sourceNamespacePattern => $disallowedDependencyPatterns) { + if (!preg_match($sourceNamespacePattern, $currentNamespace)) { + continue; + } + + $errors = $this->validateUseStatements($node, $disallowedDependencyPatterns, $currentNamespace, $errors); + } + } + + // Process FQCN references (new behavior - optional) + if ($this->checkFqcn) { + $errors = array_merge($errors, $this->processFqcnNode($node, $currentNamespace)); + } + + return $errors; + } + + /** + * @param Use_ $node + * @param array $disallowedDependencyPatterns + * @param string $currentNamespace + * @param array $errors + * @return array + * @throws ShouldNotHappenException + */ + public function validateUseStatements(Use_ $node, array $disallowedDependencyPatterns, string $currentNamespace, array $errors): array + { + foreach ($node->uses as $use) { + $usedClassName = $use->name->toString(); + foreach ($disallowedDependencyPatterns as $disallowedPattern) { + if (preg_match($disallowedPattern, $usedClassName)) { + $errors[] = RuleErrorBuilder::message(sprintf( + static::ERROR_MESSAGE, + $currentNamespace, + $usedClassName + )) + ->identifier(static::IDENTIFIER) + ->line($use->getStartLine()) + ->build(); + } + } + } + + return $errors; + } + + /** + * Process FQCN references in various node types + * + * @param Node $node + * @param string $currentNamespace + * @return array + */ + private function processFqcnNode(Node $node, string $currentNamespace): array + { + $classNames = $this->extractClassNamesFromFqcnNode($node); + + $errors = []; + foreach ($classNames as $className) { + $errors = array_merge($errors, $this->validateClassReference($className, $currentNamespace, $node)); + } + + return $errors; + } + + /** + * Extract class names from FQCN nodes based on node type + * + * @param Node $node + * @return array + */ + private function extractClassNamesFromFqcnNode(Node $node): array + { + $classNames = []; + + if ($this->isExpressionNode($node)) { + $classNames = $this->extractFromExpressionNode($node); + } elseif ($this->isStatementNode($node)) { + $classNames = $this->extractFromStatementNode($node); + } + + return $classNames; + } + + /** + * Check if node is an expression node we want to process + * + * @param Node $node + * @return bool + */ + private function isExpressionNode(Node $node): bool + { + return $node instanceof Node\Expr\New_ + || $node instanceof Node\Expr\StaticCall + || $node instanceof Node\Expr\StaticPropertyFetch + || $node instanceof Node\Expr\ClassConstFetch + || $node instanceof Node\Expr\Instanceof_; + } + + /** + * Check if node is a statement node we want to process + * + * @param Node $node + * @return bool + */ + private function isStatementNode(Node $node): bool + { + return $node instanceof Node\Stmt\Catch_ + || $node instanceof Node\Stmt\Class_ + || $node instanceof Node\Stmt\ClassMethod + || $node instanceof Node\Stmt\Property; + } + + /** + * Extract class names from expression nodes + * + * @param Node $node + * @return array + */ + private function extractFromExpressionNode(Node $node): array + { + $mapping = [ + Node\Expr\New_::class => 'new', + Node\Expr\StaticCall::class => 'static_call', + Node\Expr\StaticPropertyFetch::class => 'static_property', + Node\Expr\ClassConstFetch::class => 'class_const', + Node\Expr\Instanceof_::class => 'instanceof', + ]; + + foreach ($mapping as $nodeClass => $referenceType) { + if ($node instanceof $nodeClass && $this->shouldCheckReferenceType($referenceType)) { + return $this->extractClassNamesFromNode($node->class); + } + } + + return []; + } + + /** + * Extract class names from statement nodes + * + * @param Node $node + * @return array + */ + private function extractFromStatementNode(Node $node): array + { + $classNames = []; + + if ($node instanceof Node\Stmt\Catch_ && $this->shouldCheckReferenceType('catch')) { + $classNames = $this->extractFromCatchNode($node); + } elseif ($node instanceof Node\Stmt\Class_) { + $classNames = $this->extractFromClassNode($node); + } elseif ($node instanceof Node\Stmt\ClassMethod) { + $classNames = $this->extractFromClassMethodNode($node); + } elseif ($node instanceof Node\Stmt\Property && $this->shouldCheckReferenceType('property')) { + if ($node->type !== null) { + $classNames = $this->extractClassNamesFromType($node->type); + } + } + + return $classNames; + } + + /** + * Extract class names from catch nodes + * + * @param Node\Stmt\Catch_ $node + * @return array + */ + private function extractFromCatchNode(Node\Stmt\Catch_ $node): array + { + $classNames = []; + foreach ($node->types as $type) { + $classNames = array_merge($classNames, $this->extractClassNamesFromNode($type)); + } + return $classNames; + } + + /** + * Extract class names from class nodes (extends/implements) + * + * @param Node\Stmt\Class_ $node + * @return array + */ + private function extractFromClassNode(Node\Stmt\Class_ $node): array + { + $classNames = []; + + if ($node->extends !== null && $this->shouldCheckReferenceType('extends')) { + $classNames = array_merge($classNames, $this->extractClassNamesFromNode($node->extends)); + } + + if ($this->shouldCheckReferenceType('implements')) { + foreach ($node->implements as $interface) { + $classNames = array_merge($classNames, $this->extractClassNamesFromNode($interface)); + } + } + + return $classNames; + } + + /** + * Extract class names from class method nodes (parameters and return types) + * + * @param Node\Stmt\ClassMethod $node + * @return array + */ + private function extractFromClassMethodNode(Node\Stmt\ClassMethod $node): array + { + $classNames = []; + + if ($this->shouldCheckReferenceType('param')) { + foreach ($node->params as $param) { + if ($param->type !== null) { + $classNames = array_merge($classNames, $this->extractClassNamesFromType($param->type)); + } + } + } + + if ($this->shouldCheckReferenceType('return') && $node->returnType !== null) { + $classNames = array_merge($classNames, $this->extractClassNamesFromType($node->returnType)); + } + + return $classNames; + } + + /** + * Check if a reference type should be validated + * + * @param string $referenceType + * @return bool + */ + private function shouldCheckReferenceType(string $referenceType): bool + { + return in_array($referenceType, $this->fqcnReferenceTypes, true); + } + + /** + * Extract class names from a node (handles Name nodes) + * + * @param Node|Identifier|Name|ComplexType $node + * @return array + */ + private function extractClassNamesFromNode($node): array + { + if ($node instanceof Name && $this->isFullyQualifiedName($node)) { + return [$node->toString()]; + } + return []; + } + + /** + * Extract class names from type declarations (handles complex types) + * + * @param Identifier|Name|ComplexType $type + * @return array + */ + private function extractClassNamesFromType($type): array + { + $classNames = []; + + if ($type instanceof Name) { + if ($this->isFullyQualifiedName($type)) { + $classNames[] = $type->toString(); + } + } elseif ($type instanceof NullableType) { + $classNames = array_merge($classNames, $this->extractClassNamesFromType($type->type)); + } elseif ($type instanceof UnionType || $type instanceof IntersectionType) { + foreach ($type->types as $subType) { + $classNames = array_merge($classNames, $this->extractClassNamesFromType($subType)); + } + } + + return $classNames; + } + + /** + * Check if a Name node represents a fully qualified class name + * + * @param Name $name + * @return bool + */ + private function isFullyQualifiedName(Name $name): bool + { + return $name instanceof Name\FullyQualified; + } + + /** + * Validate a class reference against forbidden dependencies + * + * @param string $className + * @param string $currentNamespace + * @param Node $node + * @return array + */ + private function validateClassReference(string $className, string $currentNamespace, Node $node): array + { + $errors = []; + + foreach ($this->forbiddenDependencies as $sourceNamespacePattern => $disallowedDependencyPatterns) { + if (!preg_match($sourceNamespacePattern, $currentNamespace)) { + continue; + } + + foreach ($disallowedDependencyPatterns as $disallowedPattern) { + if (preg_match($disallowedPattern, $className)) { + $errors[] = RuleErrorBuilder::message(sprintf( + static::ERROR_MESSAGE, + $currentNamespace, + $className + )) + ->identifier(static::IDENTIFIER) + ->line($node->getLine()) + ->build(); + } + } + } + + return $errors; + } +} + diff --git a/tests/TestCases/Architecture/DependencyConstraintsRuleFqcnTest.php b/tests/TestCases/Architecture/DependencyConstraintsRuleFqcnTest.php index 2479b46..10419f0 100644 --- a/tests/TestCases/Architecture/DependencyConstraintsRuleFqcnTest.php +++ b/tests/TestCases/Architecture/DependencyConstraintsRuleFqcnTest.php @@ -4,20 +4,20 @@ namespace Phauthentic\PHPStanRules\Tests\TestCases\Architecture; -use Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule; +use Phauthentic\PHPStanRules\Architecture\ForbiddenDependenciesRule; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; /** * Test FQCN detection with all reference types enabled * - * @extends RuleTestCase + * @extends RuleTestCase */ class DependencyConstraintsRuleFqcnTest extends RuleTestCase { protected function getRule(): Rule { - return new DependencyConstraintsRule( + return new ForbiddenDependenciesRule( [ '/^App\\\\Capability(?:\\\\\\w+)*$/' => [ '/^DateTime$/', @@ -38,11 +38,11 @@ public function testNewInstantiation(): void { $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/NewInstantiation.php'], [ [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 12, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 18, ], ]); @@ -55,39 +55,39 @@ public function testTypeHints(): void { $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/TypeHints.php'], [ [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 10, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 12, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 17, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 23, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 29, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 35, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 46, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 52, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 52, ], ]); @@ -100,23 +100,23 @@ public function testStaticReferences(): void { $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/StaticCalls.php'], [ [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 12, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 18, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 24, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 30, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 36, ], ]); @@ -129,11 +129,11 @@ public function testInstanceofAndCatch(): void { $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/InstanceofAndCatch.php'], [ [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 12, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 18, ], ]); @@ -146,27 +146,27 @@ public function testExtendsAndImplements(): void { $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/ExtendsAndImplements.php'], [ [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 8, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeInterface`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeInterface`.', 13, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeZone`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeZone`.', 20, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeInterface`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeInterface`.', 35, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateInterval`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateInterval`.', 35, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateInterval`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateInterval`.', 37, ], ]); @@ -179,31 +179,31 @@ public function testAllReferenceTypes(): void { $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/SelectiveReferenceTypes.php'], [ [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 10, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 13, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 19, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 27, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 33, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 39, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 45, ], ]); @@ -218,39 +218,39 @@ public function testMixedUsageDetection(): void { $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/MixedUsageForbidden.php'], [ [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 10, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 11, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 16, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 18, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 24, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 28, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 30, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 34, ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', 36, ], ]); diff --git a/tests/TestCases/Architecture/DependencyConstraintsRuleSelectiveTypesTest.php b/tests/TestCases/Architecture/DependencyConstraintsRuleSelectiveTypesTest.php index 8f3a9e7..d884c3e 100644 --- a/tests/TestCases/Architecture/DependencyConstraintsRuleSelectiveTypesTest.php +++ b/tests/TestCases/Architecture/DependencyConstraintsRuleSelectiveTypesTest.php @@ -4,7 +4,7 @@ namespace Phauthentic\PHPStanRules\Tests\TestCases\Architecture; -use Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule; +use Phauthentic\PHPStanRules\Architecture\ForbiddenDependenciesRule; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; @@ -12,14 +12,14 @@ * Test selective reference type checking * Tests that only specified reference types are checked when configured * - * @extends RuleTestCase + * @extends RuleTestCase */ class DependencyConstraintsRuleSelectiveTypesTest extends RuleTestCase { protected function getRule(): Rule { // Only check 'new' and 'return' reference types - return new DependencyConstraintsRule( + return new ForbiddenDependenciesRule( ['/^App\\\\Capability(?:\\\\\\w+)*$/' => ['/^DateTime$/']], true, ['new', 'return'] @@ -34,11 +34,11 @@ public function testSelectiveReferenceTypes(): void { $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/SelectiveReferenceTypes.php'], [ [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 19, // Return type hint ], [ - 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 'Forbidden dependency: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', 27, // New instantiation ], ]);