diff --git a/README.md b/README.md index 7f1581e..aaa47ef 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ Because PHPStan is a widely used static analysis tool in the PHP community. It a It is also more or less easy to write your own rules if you need to enforce something specific that is not covered by the existing rules. +## What if I need to ignore a Rule in a certain Place? + +Use the inline annotations PHPStan provides and add a comment explaining *why* in this case the rule is allowed to be broken. An inline annotation is the best because you should keep the information close to where it happens, visible right at the root of the problem. + ### Alternative Tools If you don't like this library, you can also check out other tools. Some of them provide a fluent interface instead of a Regex. If this feels more comfortable for you, you might want to check them out: diff --git a/composer.json b/composer.json index 87eb75c..cf053ed 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ }, "autoload-dev": { "psr-4": { - "Phauthentic\\PHPStanRules\\Tests\\": "tests/" + "Phauthentic\\PHPStanRules\\Tests\\": "tests/", + "App\\": "data/" } }, "authors": [ diff --git a/data/DependencyConstraintsRuleFqcn/ExtendsAndImplements.php b/data/DependencyConstraintsRuleFqcn/ExtendsAndImplements.php new file mode 100644 index 0000000..d940f02 --- /dev/null +++ b/data/DependencyConstraintsRuleFqcn/ExtendsAndImplements.php @@ -0,0 +1,49 @@ +property = $param; + } + + // Return type hint + public function getDate(): \DateTime + { + return $this->property; + } + + // New instantiation + public function createNew() + { + return new \DateTime('now'); + } + + // Static call + public function createFromFormat() + { + return \DateTime::createFromFormat('Y-m-d', '2023-01-01'); + } + + // Class constant + public function getClassName() + { + return \DateTime::class; + } + + // instanceof + public function checkType($value): bool + { + return $value instanceof \DateTime; + } +} + diff --git a/data/DependencyConstraintsRuleFqcn/StaticCalls.php b/data/DependencyConstraintsRuleFqcn/StaticCalls.php new file mode 100644 index 0000000..514d008 --- /dev/null +++ b/data/DependencyConstraintsRuleFqcn/StaticCalls.php @@ -0,0 +1,45 @@ +dateTime = $date; + } + + // This should be caught when checkFqcn is enabled with 'param' reference type + public function setImmutableDateTime(\DateTimeImmutable $date): void + { + $this->immutableDateTime = $date; + } + + // This should be caught when checkFqcn is enabled with 'return' reference type + public function getDateTime(): \DateTime + { + return $this->dateTime; + } + + // This should be caught when checkFqcn is enabled with 'return' reference type + public function getImmutableDateTime(): \DateTimeImmutable + { + return $this->immutableDateTime; + } + + public function getAllowed(): \stdClass + { + return $this->allowed; + } + + // Test nullable types + public function getNullableDateTime(): ?\DateTime + { + return null; + } + + // Test union types (PHP 8.0+) + public function getUnionType(): \DateTime|\DateTimeImmutable + { + return $this->dateTime; + } +} + diff --git a/docs/rules/Dependency-Constraints-Rule.md b/docs/rules/Dependency-Constraints-Rule.md index d6cef48..a58bc7f 100644 --- a/docs/rules/Dependency-Constraints-Rule.md +++ b/docs/rules/Dependency-Constraints-Rule.md @@ -1,6 +1,6 @@ # Dependency Constraints Rule -Enforces dependency constraints between namespaces by checking `use` statements. +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. @@ -8,6 +8,8 @@ In the example below nothing from `App\Domain` can depend on anything from `App\ ## Configuration Example +### Basic Usage (Use Statements Only) + ```neon - class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule @@ -19,7 +21,111 @@ In the example below nothing from `App\Domain` can depend on anything from `App\ - phpstan.rules.rule ``` +### With FQCN Checking Enabled + +```neon + - + class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule + arguments: + forbiddenDependencies: [ + '/^App\\Capability(?:\\\w+)*$/': [ + '/^DateTime$/', + '/^DateTimeImmutable$/' + ] + ] + checkFqcn: true + tags: + - phpstan.rules.rule +``` + +### With Selective Reference Types + +```neon + - + class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule + arguments: + forbiddenDependencies: [ + '/^App\\Capability(?:\\\w+)*$/': [ + '/^DateTime$/', + '/^DateTimeImmutable$/' + ] + ] + checkFqcn: true + fqcnReferenceTypes: ['new', 'param', 'return', 'property'] + tags: + - phpstan.rules.rule +``` + ## Parameters - `forbiddenDependencies`: Array where keys are namespace patterns that should not depend on the namespace patterns in their value arrays. +- `checkFqcn` (optional, default: `false`): Enable checking of fully qualified class names in addition to use statements. +- `fqcnReferenceTypes` (optional, default: all types): Array of reference types to check when `checkFqcn` is enabled. + +## FQCN Reference Types + +When `checkFqcn` is enabled, the following reference types can be checked: + +- `new` - Class instantiations (e.g., `new \DateTime()`) +- `param` - Parameter type hints (e.g., `function foo(\DateTime $date)`) +- `return` - Return type hints (e.g., `function foo(): \DateTime`) +- `property` - Property type hints (e.g., `private \DateTime $date`) +- `static_call` - Static method calls (e.g., `\DateTime::createFromFormat()`) +- `static_property` - Static property access (e.g., `\DateTime::ATOM`) +- `class_const` - Class constant (e.g., `\DateTime::class`) +- `instanceof` - instanceof checks (e.g., `$x instanceof \DateTime`) +- `catch` - catch blocks (e.g., `catch (\Exception $e)`) +- `extends` - class inheritance (e.g., `class Foo extends \DateTime`) +- `implements` - interface implementation (e.g., `class Foo implements \DateTimeInterface`) + +## Use Cases + +### Preventing DateTime Usage in Domain Layer + +This example prevents usage of PHP's built-in `DateTime` and `DateTimeImmutable` classes in your capability layer, encouraging the use of domain-specific date/time objects: + +```neon + - + class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule + arguments: + forbiddenDependencies: [ + '/^App\\Capability(?:\\\w+)*$/': [ + '/^DateTime$/', + '/^DateTimeImmutable$/' + ] + ] + checkFqcn: true + tags: + - phpstan.rules.rule +``` + +This will catch: +- `use DateTime;` (use statement) +- `new \DateTime()` (instantiation) +- `function foo(\DateTime $date)` (parameter type) +- `function bar(): \DateTime` (return type) +- `private \DateTime $date` (property type) +- And all other reference types listed above + +### Selective Checking for Performance + +If you only want to check specific reference types (e.g., to improve performance or focus on certain usage patterns): + +```neon + - + class: Phauthentic\PHPStanRules\Architecture\DependencyConstraintsRule + arguments: + forbiddenDependencies: [ + '/^App\\Capability(?:\\\w+)*$/': [ + '/^DateTime$/' + ] + ] + checkFqcn: true + fqcnReferenceTypes: ['new', 'return'] # Only check instantiations and return types + tags: + - phpstan.rules.rule +``` + +## Backward Compatibility +By default, `checkFqcn` is `false`, so existing configurations will continue to work exactly as before, checking only `use` statements. The new FQCN checking feature must be explicitly enabled. diff --git a/src/Architecture/CircularModuleDependencyRule.php b/src/Architecture/CircularModuleDependencyRule.php index b310666..c04287c 100644 --- a/src/Architecture/CircularModuleDependencyRule.php +++ b/src/Architecture/CircularModuleDependencyRule.php @@ -203,4 +203,3 @@ public static function resetDependencyTracking(): void self::$moduleDependencies = []; } } - diff --git a/src/Architecture/ClassMustHaveSpecificationDocblockRule.php b/src/Architecture/ClassMustHaveSpecificationDocblockRule.php index ff5938d..83e5cd8 100644 --- a/src/Architecture/ClassMustHaveSpecificationDocblockRule.php +++ b/src/Architecture/ClassMustHaveSpecificationDocblockRule.php @@ -57,19 +57,19 @@ public function getNodeType(): string private function buildInvalidFormatMessage(): string { $parts = ["Expected format: \"{$this->specificationHeader}\" header"]; - + if ($this->requireBlankLineAfterHeader) { $parts[] = "blank line"; } - + $parts[] = "then list items starting with \"-\""; - + $message = implode(', ', $parts) . '.'; - + if ($this->requireListItemsEndWithPeriod) { $message .= ' List items must end with a period.'; } - + return $message; } @@ -92,7 +92,7 @@ public function processNode(Node $node, Scope $scope): array $className = $node->name->toString(); $namespaceName = $scope->getNamespace() ?? ''; $fullClassName = $namespaceName . '\\' . $className; - + // Determine the type for error messages $type = $node instanceof Interface_ ? 'Interface' : 'Class'; @@ -165,10 +165,10 @@ private function extractDocblockLines(string $text): array if ($cleaned === null) { return []; } - + // Split by lines $lines = explode("\n", $cleaned); - + // Remove leading * and whitespace from each line $lines = array_map(function (string $line): string { $line = ltrim($line); @@ -237,17 +237,17 @@ private function hasValidSpecificationFormat(array $lines): bool $hasListItem = false; $lineCount = count($lines); $startIndex = $this->requireBlankLineAfterHeader ? $specIndex + 2 : $specIndex + 1; - + $currentListItem = ''; $inListItem = false; - + for ($i = $startIndex; $i < $lineCount; $i++) { if (!isset($lines[$i])) { break; } - + $trimmedLine = trim($lines[$i]); - + // Skip blank lines if ($trimmedLine === '') { // If we were in a list item, finalize it @@ -260,7 +260,7 @@ private function hasValidSpecificationFormat(array $lines): bool } continue; } - + // If we hit an @ annotation, finalize current list item and stop if (strpos($trimmedLine, '@') === 0) { if ($inListItem && $currentListItem !== '') { @@ -270,7 +270,7 @@ private function hasValidSpecificationFormat(array $lines): bool } break; } - + // Check if this is a new list item (starts with -) if (strpos($trimmedLine, '-') === 0) { // Finalize previous list item if exists @@ -279,26 +279,26 @@ private function hasValidSpecificationFormat(array $lines): bool return false; } } - + // Start new list item $hasListItem = true; $inListItem = true; $currentListItem = $trimmedLine; continue; } - + // If we're in a list item, this is a continuation line if ($inListItem) { $currentListItem .= ' ' . $trimmedLine; continue; } - + // If we encounter non-list, non-annotation, non-blank line before finding a list item, invalid if (!$hasListItem) { return false; } } - + // Finalize the last list item if exists if ($inListItem && $currentListItem !== '') { if (!$this->validateListItem($currentListItem)) { @@ -317,7 +317,7 @@ private function validateListItem(string $listItem): bool if ($this->requireListItemsEndWithPeriod) { return $this->listItemEndsWithPeriod($listItem); } - + return true; } @@ -354,4 +354,3 @@ private function buildInvalidDocblockError(string $type, string $fullName, Node ->build(); } } - diff --git a/src/Architecture/DependencyConstraintsRule.php b/src/Architecture/DependencyConstraintsRule.php index 91b563f..5791d1a 100644 --- a/src/Architecture/DependencyConstraintsRule.php +++ b/src/Architecture/DependencyConstraintsRule.php @@ -5,7 +5,13 @@ 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; @@ -18,8 +24,9 @@ * - Checks use statements in PHP code. * - A class in a namespace matching a given regex is not allowed to depend on any namespace defined by a set of other regexes. * - Reports an error if a forbidden dependency is detected. + * - Optionally checks fully qualified class names (FQCNs) in various contexts. * - * @implements Rule + * @implements Rule */ class DependencyConstraintsRule implements Rule { @@ -27,6 +34,20 @@ class DependencyConstraintsRule implements Rule 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 @@ -35,20 +56,38 @@ class DependencyConstraintsRule implements Rule */ 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) - { + 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 Use_::class; + return Node::class; } /** + * @return array * @throws ShouldNotHappenException */ public function processNode(Node $node, Scope $scope): array @@ -60,26 +99,34 @@ public function processNode(Node $node, Scope $scope): array $errors = []; - foreach ($this->forbiddenDependencies as $sourceNamespacePattern => $disallowedDependencyPatterns) { - if (!preg_match($sourceNamespacePattern, $currentNamespace)) { - continue; + // 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); } + } - $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 Node $node + * @param Use_ $node * @param array $disallowedDependencyPatterns * @param string $currentNamespace * @param array $errors * @return array * @throws ShouldNotHappenException */ - public function validateUseStatements(Node $node, array $disallowedDependencyPatterns, string $currentNamespace, array $errors): array + public function validateUseStatements(Use_ $node, array $disallowedDependencyPatterns, string $currentNamespace, array $errors): array { foreach ($node->uses as $use) { $usedClassName = $use->name->toString(); @@ -99,4 +146,279 @@ public function validateUseStatements(Node $node, array $disallowedDependencyPat 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(); + } + } + } + + return $errors; + } } diff --git a/src/Architecture/ModularArchitectureRule.php b/src/Architecture/ModularArchitectureRule.php index 308ccba..6abba1c 100644 --- a/src/Architecture/ModularArchitectureRule.php +++ b/src/Architecture/ModularArchitectureRule.php @@ -219,15 +219,15 @@ private function isModularNamespace(string $namespace): bool private function parseModuleInfo(string $namespace): ?array { $escapedBase = str_replace('\\', '\\\\', $this->baseNamespace); - + // Build dynamic pattern from configured layer names $layerNames = array_keys($this->layerDependencies); $layerPattern = implode('|', array_map('preg_quote', $layerNames)); - + // Match: BaseNamespace\ModuleName[\Layer[\...]] // Captures module name and optionally layer name $pattern = '/^' . $escapedBase . '\\\\([^\\\\]+)(?:\\\\(' . $layerPattern . ')(?:\\\\.*)?|\\\\.*)?$/'; - + if (preg_match($pattern, $namespace, $matches)) { return [ 'module' => $matches[1], @@ -325,4 +325,3 @@ private function isAllowedCrossModuleImport(string $fullyQualifiedClassName): bo return false; } } - diff --git a/src/PhpParser/ParentNodeAttributeVisitor.php b/src/PhpParser/ParentNodeAttributeVisitor.php index 61b6122..3fc4d8f 100644 --- a/src/PhpParser/ParentNodeAttributeVisitor.php +++ b/src/PhpParser/ParentNodeAttributeVisitor.php @@ -56,4 +56,3 @@ private function setParentAttributeForArrayOfNodes(array $nodes, Node $parentNod } } } - diff --git a/tests/TestCases/Architecture/CircularModuleDependencyRuleTest.php b/tests/TestCases/Architecture/CircularModuleDependencyRuleTest.php index 5d58b2a..9b054a4 100644 --- a/tests/TestCases/Architecture/CircularModuleDependencyRuleTest.php +++ b/tests/TestCases/Architecture/CircularModuleDependencyRuleTest.php @@ -64,4 +64,3 @@ public function testCircularDependencyDetection(): void ); } } - diff --git a/tests/TestCases/Architecture/ClassMustBeFinalRuleWithAbstractNotIgnoredTest.php b/tests/TestCases/Architecture/ClassMustBeFinalRuleWithAbstractNotIgnoredTest.php index 0a8bb2c..934aae3 100644 --- a/tests/TestCases/Architecture/ClassMustBeFinalRuleWithAbstractNotIgnoredTest.php +++ b/tests/TestCases/Architecture/ClassMustBeFinalRuleWithAbstractNotIgnoredTest.php @@ -33,4 +33,3 @@ public function testConcreteClassMustBeFinal(): void ]); } } - diff --git a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleCustomHeaderTest.php b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleCustomHeaderTest.php index 8051ab4..cddacd3 100644 --- a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleCustomHeaderTest.php +++ b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleCustomHeaderTest.php @@ -37,4 +37,3 @@ public function testInvalidWithDefaultHeader(): void ]); } } - diff --git a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleInterfaceTest.php b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleInterfaceTest.php index e8b2089..4751099 100644 --- a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleInterfaceTest.php +++ b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleInterfaceTest.php @@ -46,4 +46,3 @@ public function testInterfaceWithoutDocblockTriggersError(): void ]); } } - diff --git a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMethodTest.php b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMethodTest.php index 4ecf27a..8e0dc82 100644 --- a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMethodTest.php +++ b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMethodTest.php @@ -47,4 +47,3 @@ public function testInvalidMethodDocblock(): void ]); } } - diff --git a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMultiLineTest.php b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMultiLineTest.php index e284559..82d148a 100644 --- a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMultiLineTest.php +++ b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMultiLineTest.php @@ -36,4 +36,3 @@ public function testMultiLineComplexFormat(): void $this->analyse([__DIR__ . '/../../../data/SpecificationDocblock/ValidMultiLineComplexClass.php'], []); } } - diff --git a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMultiLineWithPeriodsTest.php b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMultiLineWithPeriodsTest.php index 3a791aa..6c232f5 100644 --- a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMultiLineWithPeriodsTest.php +++ b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleMultiLineWithPeriodsTest.php @@ -38,4 +38,3 @@ public function testInvalidMultiLineNoPeriod(): void ]); } } - diff --git a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleNoBlankLineTest.php b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleNoBlankLineTest.php index a295125..63c1340 100644 --- a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleNoBlankLineTest.php +++ b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleNoBlankLineTest.php @@ -34,4 +34,3 @@ public function testInvalidWithBlankLineWhenNotRequired(): void $this->analyse([__DIR__ . '/../../../data/SpecificationDocblock/ValidSpecificationClass.php'], []); } } - diff --git a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleTest.php b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleTest.php index e17fa07..146d456 100644 --- a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleTest.php +++ b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleTest.php @@ -76,5 +76,3 @@ public function testInvalidSpecificationNoListItem(): void ]); } } - - diff --git a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleWithPeriodsTest.php b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleWithPeriodsTest.php index 1ea1ab5..63df9b4 100644 --- a/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleWithPeriodsTest.php +++ b/tests/TestCases/Architecture/ClassMustHaveSpecificationDocblockRuleWithPeriodsTest.php @@ -38,4 +38,3 @@ public function testInvalidMissingPeriods(): void ]); } } - diff --git a/tests/TestCases/Architecture/DependencyConstraintsRuleBackwardCompatibilityTest.php b/tests/TestCases/Architecture/DependencyConstraintsRuleBackwardCompatibilityTest.php new file mode 100644 index 0000000..e0ce703 --- /dev/null +++ b/tests/TestCases/Architecture/DependencyConstraintsRuleBackwardCompatibilityTest.php @@ -0,0 +1,72 @@ + + */ +class DependencyConstraintsRuleBackwardCompatibilityTest extends RuleTestCase +{ + protected function getRule(): Rule + { + // Default behavior - checkFqcn is false + return new DependencyConstraintsRule([ + '/^App\\\\Capability(?:\\\\\\w+)*$/' => ['/^DateTime$/', '/^DateTimeImmutable$/'] + ]); + } + + /** + * Test that FQCN checking is disabled by default + * Only use statements should be caught, not FQCN references + */ + public function testFqcnCheckingDisabledByDefault(): void + { + // Should only catch the use statements, not the FQCN usages + $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/MixedUsageForbidden.php'], [ + [ + 'Dependency violation: 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`.', + 11, + ], + ]); + } + + /** + * Test that FQCN instantiations are not caught when disabled + */ + public function testFqcnInstantiationNotCaught(): void + { + // Should have no errors since we're only checking use statements + $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/NewInstantiation.php'], []); + } + + /** + * Test that FQCN type hints are not caught when disabled + */ + public function testFqcnTypeHintsNotCaught(): void + { + // Should have no errors since we're only checking use statements + $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/TypeHints.php'], []); + } + + /** + * Test that existing functionality still works (use statements are caught) + */ + public function testExistingFunctionalityWorks(): void + { + // This is the original test from NamespaceDependencyRuleTest + // to ensure backward compatibility + $this->analyse([__DIR__ . '/../../../data/DependencyRuleTest/Domain/Aggregate.php'], []); + } +} diff --git a/tests/TestCases/Architecture/DependencyConstraintsRuleFqcnTest.php b/tests/TestCases/Architecture/DependencyConstraintsRuleFqcnTest.php new file mode 100644 index 0000000..2479b46 --- /dev/null +++ b/tests/TestCases/Architecture/DependencyConstraintsRuleFqcnTest.php @@ -0,0 +1,267 @@ + + */ +class DependencyConstraintsRuleFqcnTest extends RuleTestCase +{ + protected function getRule(): Rule + { + return new DependencyConstraintsRule( + [ + '/^App\\\\Capability(?:\\\\\\w+)*$/' => [ + '/^DateTime$/', + '/^DateTimeImmutable$/', + '/^DateTimeInterface$/', + '/^DateTimeZone$/', + '/^DateInterval$/', + ] + ], + true // Enable FQCN checking with all reference types (default) + ); + } + + /** + * Test new instantiation detection + */ + 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`.', + 12, + ], + [ + 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 18, + ], + ]); + } + + /** + * Test type hints (property, param, return) detection + */ + 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`.', + 10, + ], + [ + 'Dependency violation: 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`.', + 17, + ], + [ + 'Dependency violation: 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`.', + 29, + ], + [ + 'Dependency violation: 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`.', + 46, + ], + [ + 'Dependency violation: 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`.', + 52, + ], + ]); + } + + /** + * Test static calls, properties, and class constants detection + */ + 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`.', + 12, + ], + [ + 'Dependency violation: 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`.', + 24, + ], + [ + 'Dependency violation: 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`.', + 36, + ], + ]); + } + + /** + * Test instanceof and catch block detection + */ + 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`.', + 12, + ], + [ + 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTimeImmutable`.', + 18, + ], + ]); + } + + /** + * Test extends and implements detection + */ + 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`.', + 8, + ], + [ + 'Dependency violation: 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`.', + 20, + ], + [ + 'Dependency violation: 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`.', + 35, + ], + [ + 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateInterval`.', + 37, + ], + ]); + } + + /** + * Test that all reference types work together + */ + 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`.', + 10, + ], + [ + 'Dependency violation: 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`.', + 19, + ], + [ + 'Dependency violation: 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`.', + 33, + ], + [ + 'Dependency violation: 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`.', + 45, + ], + ]); + } + + /** + * Test that both use statements and FQCN are caught when enabled + * Note: PHPStan resolves imported names to FQCN, so both explicit FQCN + * and imported class usages are caught by FQCN checking + */ + 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`.', + 10, + ], + [ + 'Dependency violation: 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`.', + 16, + ], + [ + 'Dependency violation: 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`.', + 24, + ], + [ + 'Dependency violation: 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`.', + 30, + ], + [ + 'Dependency violation: 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`.', + 36, + ], + ]); + } + + /** + * Test that allowed classes are not caught + */ + public function testAllowedClasses(): void + { + // Should have no errors for allowed classes + $this->analyse([__DIR__ . '/../../../data/DependencyConstraintsRuleFqcn/MixedUsageAllowed.php'], []); + } +} diff --git a/tests/TestCases/Architecture/DependencyConstraintsRuleSelectiveTypesTest.php b/tests/TestCases/Architecture/DependencyConstraintsRuleSelectiveTypesTest.php new file mode 100644 index 0000000..8f3a9e7 --- /dev/null +++ b/tests/TestCases/Architecture/DependencyConstraintsRuleSelectiveTypesTest.php @@ -0,0 +1,46 @@ + + */ +class DependencyConstraintsRuleSelectiveTypesTest extends RuleTestCase +{ + protected function getRule(): Rule + { + // Only check 'new' and 'return' reference types + return new DependencyConstraintsRule( + ['/^App\\\\Capability(?:\\\\\\w+)*$/' => ['/^DateTime$/']], + true, + ['new', 'return'] + ); + } + + /** + * Test that only selected reference types are checked + * Should catch 'new' and 'return' but not 'property', 'param', 'static_call', etc. + */ + 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`.', + 19, // Return type hint + ], + [ + 'Dependency violation: A class in namespace `App\Capability` is not allowed to depend on `DateTime`.', + 27, // New instantiation + ], + ]); + } +} diff --git a/tests/TestCases/Architecture/MethodSignatureMustMatchRuleTest.php b/tests/TestCases/Architecture/MethodSignatureMustMatchRuleTest.php index b7239dc..e8c0ee6 100644 --- a/tests/TestCases/Architecture/MethodSignatureMustMatchRuleTest.php +++ b/tests/TestCases/Architecture/MethodSignatureMustMatchRuleTest.php @@ -143,13 +143,13 @@ public function testRule(): void 'Method TestClass::testMethodWithWrongType parameter #1 should be of type string, int given.', 17, ], - + // Errors for testMaxParams - exceeds max parameters [ 'Method TestClass::testMaxParams has 4 parameters, but at most 2 allowed.', 22, ], - + // Errors for testNameMismatch - parameter names don't match patterns [ 'Method TestClass::testNameMismatch parameter #1 name "wrongName" does not match pattern /^param/.', @@ -159,15 +159,15 @@ public function testRule(): void 'Method TestClass::testNameMismatch parameter #2 name "anotherWrong" does not match pattern /^param/.', 27, ], - + // No errors for testNullableTypes - nullable types should match correctly - + // No errors for testClassTypes - class types should match correctly - + // No errors for testProtectedMethod - protected visibility matches - + // No errors for testNoVisibilityReq - no visibility requirement specified - + // No errors for testValidMethod - everything matches correctly ]); } diff --git a/tests/TestCases/Architecture/ModularArchitectureCustomCrossModuleRuleTest.php b/tests/TestCases/Architecture/ModularArchitectureCustomCrossModuleRuleTest.php index abb694c..fc86648 100644 --- a/tests/TestCases/Architecture/ModularArchitectureCustomCrossModuleRuleTest.php +++ b/tests/TestCases/Architecture/ModularArchitectureCustomCrossModuleRuleTest.php @@ -10,7 +10,7 @@ /** * Test custom cross-module pattern configuration - * + * * @extends RuleTestCase */ class ModularArchitectureCustomCrossModuleRuleTest extends RuleTestCase @@ -64,4 +64,3 @@ public function testStillAllowsDefaultFacadePattern(): void ); } } - diff --git a/tests/TestCases/Architecture/ModularArchitectureCustomLayersRuleTest.php b/tests/TestCases/Architecture/ModularArchitectureCustomLayersRuleTest.php index e5ab04d..4acca1b 100644 --- a/tests/TestCases/Architecture/ModularArchitectureCustomLayersRuleTest.php +++ b/tests/TestCases/Architecture/ModularArchitectureCustomLayersRuleTest.php @@ -10,7 +10,7 @@ /** * Test custom layer configuration - * + * * @extends RuleTestCase */ class ModularArchitectureCustomLayersRuleTest extends RuleTestCase diff --git a/tests/TestCases/Architecture/ModularArchitectureNoCrossModuleRuleTest.php b/tests/TestCases/Architecture/ModularArchitectureNoCrossModuleRuleTest.php index baf40da..98fb20b 100644 --- a/tests/TestCases/Architecture/ModularArchitectureNoCrossModuleRuleTest.php +++ b/tests/TestCases/Architecture/ModularArchitectureNoCrossModuleRuleTest.php @@ -10,7 +10,7 @@ /** * Test that without cross-module patterns, all cross-module imports are blocked - * + * * @extends RuleTestCase */ class ModularArchitectureNoCrossModuleRuleTest extends RuleTestCase @@ -56,4 +56,3 @@ public function testIntraModuleDependenciesStillWork(): void ); } } - diff --git a/tests/TestCases/Architecture/ModularArchitectureRuleTest.php b/tests/TestCases/Architecture/ModularArchitectureRuleTest.php index 8dcc1e6..ad3e55e 100644 --- a/tests/TestCases/Architecture/ModularArchitectureRuleTest.php +++ b/tests/TestCases/Architecture/ModularArchitectureRuleTest.php @@ -181,4 +181,3 @@ public function testCustomDtoNotAllowedByDefault(): void ); } } - diff --git a/tests/TestCases/CleanCode/MaxLineLengthRuleIgnoreUseTest.php b/tests/TestCases/CleanCode/MaxLineLengthRuleIgnoreUseTest.php index 286d796..f3c7e32 100644 --- a/tests/TestCases/CleanCode/MaxLineLengthRuleIgnoreUseTest.php +++ b/tests/TestCases/CleanCode/MaxLineLengthRuleIgnoreUseTest.php @@ -25,4 +25,3 @@ public function testUseStatementsAreIgnored(): void $this->analyse([__DIR__ . '/../../../data/MaxLineLengthUseStatementsClass.php'], []); } } - diff --git a/tests/TestCases/CleanCode/MaxLineLengthRuleWithExclusionTest.php b/tests/TestCases/CleanCode/MaxLineLengthRuleWithExclusionTest.php index 1573329..b2a9b43 100644 --- a/tests/TestCases/CleanCode/MaxLineLengthRuleWithExclusionTest.php +++ b/tests/TestCases/CleanCode/MaxLineLengthRuleWithExclusionTest.php @@ -36,4 +36,3 @@ public function testNonExcludedFileIsChecked(): void ]); } } - diff --git a/tests/TestCases/CleanCode/TooManyArgumentsRuleWithPatternsTest.php b/tests/TestCases/CleanCode/TooManyArgumentsRuleWithPatternsTest.php index 75124f9..38a03ed 100644 --- a/tests/TestCases/CleanCode/TooManyArgumentsRuleWithPatternsTest.php +++ b/tests/TestCases/CleanCode/TooManyArgumentsRuleWithPatternsTest.php @@ -30,4 +30,3 @@ public function testRuleWithPatterns(): void ]); } } -