diff --git a/README.md b/README.md index 882dc41a..1ad2a7c2 100644 --- a/README.md +++ b/README.md @@ -368,10 +368,12 @@ You can add multiple parameters, the violation will happen when one of them matc ```php $rules[] = Rule::allClasses() ->that(new ResideInOneOfTheseNamespaces('App\Domain')) - ->should(new NotHaveDependencyOutsideNamespace('App\Domain', ['Ramsey\Uuid'], true)) + ->should(new NotHaveDependencyOutsideNamespace('App\Domain', ['Ramsey\Uuid'])) ->because('we want protect our domain except for Ramsey\Uuid'); ``` +Note: PHP core classes (e.g., `DateTime`, `Exception`, `PDO`) are automatically excluded from dependency checks. + ### Not have a name matching a pattern ```php diff --git a/src/Analyzer/ClassDescriptionBuilder.php b/src/Analyzer/ClassDescriptionBuilder.php index fb137d3e..a3a54700 100644 --- a/src/Analyzer/ClassDescriptionBuilder.php +++ b/src/Analyzer/ClassDescriptionBuilder.php @@ -78,6 +78,11 @@ public function addInterface(string $FQCN, int $line): self public function addDependency(ClassDependency $cd): self { + // Filter out PHP core classes + if ($this->isPhpCoreClass($cd)) { + return $this; + } + $this->classDependencies[] = $cd; return $this; @@ -169,4 +174,26 @@ public function build(): ClassDescription $this->filePath ); } + + /** + * Checks if a dependency is a PHP core/internal class. + * Uses Reflection to detect PHP built-in classes from core and extensions + * (e.g., DateTime, Exception, MongoDB\Driver\Manager, Swoole\Server). + */ + private function isPhpCoreClass(ClassDependency $dependency): bool + { + $fqcn = $dependency->getFQCN(); + + try { + /** @var class-string $className */ + $className = $fqcn->toString(); + $reflection = new \ReflectionClass($className); + + return $reflection->isInternal(); + } catch (\ReflectionException $e) { + // Class doesn't exist in the current environment + // It's likely a user-defined class in the project being analyzed + return false; + } + } } diff --git a/src/Expression/ForClasses/NotHaveDependencyOutsideNamespace.php b/src/Expression/ForClasses/NotHaveDependencyOutsideNamespace.php index 4501d6e1..24fa721b 100644 --- a/src/Expression/ForClasses/NotHaveDependencyOutsideNamespace.php +++ b/src/Expression/ForClasses/NotHaveDependencyOutsideNamespace.php @@ -19,13 +19,10 @@ class NotHaveDependencyOutsideNamespace implements Expression /** @var array */ private array $externalDependenciesToExclude; - private bool $excludeCoreNamespace; - - public function __construct(string $namespace, array $externalDependenciesToExclude = [], bool $excludeCoreNamespace = false) + public function __construct(string $namespace, array $externalDependenciesToExclude = []) { $this->namespace = $namespace; $this->externalDependenciesToExclude = $externalDependenciesToExclude; - $this->excludeCoreNamespace = $excludeCoreNamespace; } public function describe(ClassDescription $theClass, string $because): Description @@ -49,10 +46,6 @@ public function evaluate(ClassDescription $theClass, Violations $violations, str continue; } - if ($this->excludeCoreNamespace && '' === $externalDep->getFQCN()->namespace()) { - continue; - } - $violation = Violation::createWithErrorLine( $theClass->getFQCN(), ViolationMessage::withDescription( diff --git a/tests/Unit/Analyzer/ClassDescriptionBuilderTest.php b/tests/Unit/Analyzer/ClassDescriptionBuilderTest.php index 21a55e40..a12ba3a5 100644 --- a/tests/Unit/Analyzer/ClassDescriptionBuilderTest.php +++ b/tests/Unit/Analyzer/ClassDescriptionBuilderTest.php @@ -180,4 +180,101 @@ public function test_it_should_create_not_trait(): void self::assertInstanceOf(ClassDescription::class, $classDescription); self::assertFalse($classDescription->isTrait()); } + + public function test_it_should_filter_out_php_core_classes(): void + { + $FQCN = 'MyClass'; + + $classDescription = (new ClassDescriptionBuilder()) + ->setFilePath('src/Foo.php') + ->setClassName($FQCN) + ->addDependency(new ClassDependency('DateTime', 10)) + ->addDependency(new ClassDependency('Exception', 15)) + ->addDependency(new ClassDependency('PDO', 20)) + ->build(); + + self::assertInstanceOf(ClassDescription::class, $classDescription); + + // PHP core classes should be filtered out + self::assertCount(0, $classDescription->getDependencies()); + } + + public function test_it_should_not_filter_user_defined_classes_in_root_namespace(): void + { + $FQCN = 'MyClass'; + + $classDescription = (new ClassDescriptionBuilder()) + ->setFilePath('src/Foo.php') + ->setClassName($FQCN) + ->addDependency(new ClassDependency('NonExistentUserClass', 10)) + ->build(); + + self::assertInstanceOf(ClassDescription::class, $classDescription); + + // User-defined classes in root namespace should NOT be filtered + self::assertCount(1, $classDescription->getDependencies()); + self::assertEquals('NonExistentUserClass', $classDescription->getDependencies()[0]->getFQCN()->toString()); + } + + public function test_it_should_not_filter_user_defined_classes_with_namespace(): void + { + $FQCN = 'MyClass'; + + $classDescription = (new ClassDescriptionBuilder()) + ->setFilePath('src/Foo.php') + ->setClassName($FQCN) + ->addDependency(new ClassDependency('Vendor\Package\SomeClass', 10)) + ->addDependency(new ClassDependency('App\Domain\Entity', 15)) + ->build(); + + self::assertInstanceOf(ClassDescription::class, $classDescription); + + // User-defined classes with namespaces should not be filtered + self::assertCount(2, $classDescription->getDependencies()); + } + + public function test_it_should_filter_mixed_dependencies_correctly(): void + { + $FQCN = 'MyClass'; + + $classDescription = (new ClassDescriptionBuilder()) + ->setFilePath('src/Foo.php') + ->setClassName($FQCN) + ->addDependency(new ClassDependency('DateTime', 10)) // PHP core - filtered + ->addDependency(new ClassDependency('Vendor\Package\SomeClass', 15)) // Namespaced - kept + ->addDependency(new ClassDependency('Exception', 20)) // PHP core - filtered + ->addDependency(new ClassDependency('NonExistentUserClass', 25)) // User root class - kept + ->addDependency(new ClassDependency('PDO', 30)) // PHP core - filtered + ->build(); + + self::assertInstanceOf(ClassDescription::class, $classDescription); + + // Should keep only the 2 non-PHP-core dependencies + self::assertCount(2, $classDescription->getDependencies()); + + $dependencies = $classDescription->getDependencies(); + self::assertEquals('Vendor\Package\SomeClass', $dependencies[0]->getFQCN()->toString()); + self::assertEquals('NonExistentUserClass', $dependencies[1]->getFQCN()->toString()); + } + + public function test_it_should_filter_internal_classes_with_namespaces(): void + { + $FQCN = 'MyClass'; + + // ReflectionClass is a PHP internal class in the root namespace + // If other internal namespaced classes exist (e.g., MongoDB\Driver\Manager), + // they should also be filtered. We test with ReflectionClass which is always available. + $classDescription = (new ClassDescriptionBuilder()) + ->setFilePath('src/Foo.php') + ->setClassName($FQCN) + ->addDependency(new ClassDependency('ReflectionClass', 10)) // Internal root - filtered + ->addDependency(new ClassDependency('App\MyClass', 15)) // User namespaced - kept + ->build(); + + self::assertInstanceOf(ClassDescription::class, $classDescription); + + // ReflectionClass should be filtered, only App\MyClass should remain + self::assertCount(1, $classDescription->getDependencies()); + self::assertEquals('App\MyClass', $classDescription->getDependencies()[0]->getFQCN()->toString()); + } } diff --git a/tests/Unit/Expressions/ForClasses/NotHaveDependencyOutsideNamespaceTest.php b/tests/Unit/Expressions/ForClasses/NotHaveDependencyOutsideNamespaceTest.php index bcc3e0d5..aa729738 100644 --- a/tests/Unit/Expressions/ForClasses/NotHaveDependencyOutsideNamespaceTest.php +++ b/tests/Unit/Expressions/ForClasses/NotHaveDependencyOutsideNamespaceTest.php @@ -57,14 +57,13 @@ public function test_it_should_return_false_if_depends_on_namespace(): void ->setClassName('HappyIsland') ->addDependency(new ClassDependency('myNamespace', 100)) ->addDependency(new ClassDependency('another\class', 200)) - ->addDependency(new ClassDependency('\DateTime', 300)) ->build(); $because = 'we want to add this rule for our software'; $violations = new Violations(); $notHaveDependencyOutsideNamespace->evaluate($classDescription, $violations, $because); - self::assertEquals(2, $violations->count()); + self::assertEquals(1, $violations->count()); } public function test_it_should_not_return_violation_error_if_dependency_excluded(): void @@ -84,20 +83,22 @@ public function test_it_should_not_return_violation_error_if_dependency_excluded self::assertEquals(0, $violations->count()); } - public function test_it_should_not_return_violation_error_if_core_dependency_excluded(): void + public function test_it_should_automatically_exclude_php_core_classes(): void { - $notHaveDependencyOutsideNamespace = new NotHaveDependencyOutsideNamespace('myNamespace', [], true); + $notHaveDependencyOutsideNamespace = new NotHaveDependencyOutsideNamespace('myNamespace'); $classDescription = (new ClassDescriptionBuilder()) ->setFilePath('src/Foo.php') ->setClassName('HappyIsland') - ->addDependency(new ClassDependency('\DateTime', 100)) + ->addDependency(new ClassDependency('another\class', 100)) ->build(); $because = 'we want to add this rule for our software'; $violations = new Violations(); $notHaveDependencyOutsideNamespace->evaluate($classDescription, $violations, $because); - self::assertEquals(0, $violations->count()); + // PHP core classes are automatically filtered at the ClassDescriptionBuilder level + // So only 'another\class' should be reported as a violation + self::assertEquals(1, $violations->count()); } }