diff --git a/src/Analyzer/ClassDescription.php b/src/Analyzer/ClassDescription.php index 93545a98..9e8d7899 100644 --- a/src/Analyzer/ClassDescription.php +++ b/src/Analyzer/ClassDescription.php @@ -103,6 +103,11 @@ public function namespaceMatches(string $pattern): bool return $this->FQCN->matches($pattern); } + public function namespaceMatchesExactly(string $namespace): bool + { + return $this->FQCN->namespace() === $namespace; + } + public function namespaceMatchesOneOfTheseNamespaces(array $classesToBeExcluded): bool { foreach ($classesToBeExcluded as $classToBeExcluded) { diff --git a/src/Expression/ForClasses/NotResideInOneOfTheseNamespacesExactly.php b/src/Expression/ForClasses/NotResideInOneOfTheseNamespacesExactly.php new file mode 100644 index 00000000..a35524c2 --- /dev/null +++ b/src/Expression/ForClasses/NotResideInOneOfTheseNamespacesExactly.php @@ -0,0 +1,49 @@ + */ + private $namespaces; + + public function __construct(string ...$namespaces) + { + $this->namespaces = $namespaces; + } + + public function describe(ClassDescription $theClass, string $because): Description + { + $descr = implode(', ', $this->namespaces); + + return new Description("should not reside in one of these namespaces exactly: $descr", $because); + } + + public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void + { + $resideInNamespace = false; + foreach ($this->namespaces as $namespace) { + if ($theClass->namespaceMatchesExactly($namespace)) { + $resideInNamespace = true; + } + } + + if ($resideInNamespace) { + $violation = Violation::create( + $theClass->getFQCN(), + ViolationMessage::selfExplanatory($this->describe($theClass, $because)), + $theClass->getFilePath() + ); + $violations->add($violation); + } + } +} diff --git a/src/Expression/ForClasses/ResideInOneOfTheseNamespacesExactly.php b/src/Expression/ForClasses/ResideInOneOfTheseNamespacesExactly.php new file mode 100644 index 00000000..92e498e3 --- /dev/null +++ b/src/Expression/ForClasses/ResideInOneOfTheseNamespacesExactly.php @@ -0,0 +1,49 @@ + */ + private $namespaces; + + public function __construct(string ...$namespaces) + { + $this->namespaces = array_values(array_unique($namespaces)); + } + + public function describe(ClassDescription $theClass, string $because): Description + { + $descr = implode(', ', $this->namespaces); + + return new Description("should reside in one of these namespaces exactly: $descr", $because); + } + + public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void + { + $resideInNamespace = false; + foreach ($this->namespaces as $namespace) { + if ($theClass->namespaceMatchesExactly($namespace)) { + $resideInNamespace = true; + } + } + + if (!$resideInNamespace) { + $violation = Violation::create( + $theClass->getFQCN(), + ViolationMessage::selfExplanatory($this->describe($theClass, $because)), + $theClass->getFilePath() + ); + $violations->add($violation); + } + } +} diff --git a/tests/Unit/Expressions/ForClasses/NotResideInOneOfTheseNamespacesExactlyTest.php b/tests/Unit/Expressions/ForClasses/NotResideInOneOfTheseNamespacesExactlyTest.php new file mode 100644 index 00000000..d20dfc89 --- /dev/null +++ b/tests/Unit/Expressions/ForClasses/NotResideInOneOfTheseNamespacesExactlyTest.php @@ -0,0 +1,75 @@ +build(); + $because = 'we want to add this rule for our software'; + $violations = new Violations(); + $haveNameMatching->evaluate($classDesc, $violations, $because); + + self::assertEquals(0, $violations->count()); + } + + public function test_it_should_return_true_if_reside_in_child_namespace(): void + { + $haveNameMatching = new NotResideInOneOfTheseNamespacesExactly('MyNamespace'); + + $classDesc = ClassDescription::getBuilder('MyNamespace\Child\HappyIsland', 'src/Foo.php')->build(); + $because = 'we want to add this rule for our software'; + $violations = new Violations(); + $haveNameMatching->evaluate($classDesc, $violations, $because); + + self::assertEquals(0, $violations->count(), 'should not violate when in child namespace'); + } + + public function test_it_should_return_false_if_reside_in_exact_namespace(): void + { + $namespace = 'MyNamespace'; + $haveNameMatching = new NotResideInOneOfTheseNamespacesExactly($namespace); + + $classDesc = ClassDescription::getBuilder('MyNamespace\HappyIsland', 'src/Foo.php')->build(); + $because = 'we want to add this rule for our software'; + $violations = new Violations(); + $haveNameMatching->evaluate($classDesc, $violations, $because); + + self::assertEquals(1, $violations->count()); + self::assertEquals( + 'should not reside in one of these namespaces exactly: '.$namespace.' because we want to add this rule for our software', + $haveNameMatching->describe($classDesc, $because)->toString() + ); + } + + public function test_it_should_check_multiple_namespaces_in_or(): void + { + $haveNameMatching = new NotResideInOneOfTheseNamespacesExactly('AnotherNamespace', 'ASecondNamespace', 'AThirdNamespace'); + + $classDesc = ClassDescription::getBuilder('AnotherNamespace\HappyIsland', 'src/Foo.php')->build(); + $violations = new Violations(); + $because = 'we want to add this rule for our software'; + $haveNameMatching->evaluate($classDesc, $violations, $because); + self::assertEquals(1, $violations->count()); + + $classDesc = ClassDescription::getBuilder('MyNamespace\HappyIsland', 'src/Foo.php')->build(); + $violations = new Violations(); + $haveNameMatching->evaluate($classDesc, $violations, $because); + self::assertEquals(0, $violations->count()); + + $classDesc = ClassDescription::getBuilder('AThirdNamespace\HappyIsland', 'src/Foo.php')->build(); + $violations = new Violations(); + $haveNameMatching->evaluate($classDesc, $violations, $because); + self::assertEquals(1, $violations->count()); + } +} diff --git a/tests/Unit/Expressions/ForClasses/ResideInOneOfTheseNamespacesExactlyTest.php b/tests/Unit/Expressions/ForClasses/ResideInOneOfTheseNamespacesExactlyTest.php new file mode 100644 index 00000000..f324cd84 --- /dev/null +++ b/tests/Unit/Expressions/ForClasses/ResideInOneOfTheseNamespacesExactlyTest.php @@ -0,0 +1,118 @@ +build(); + $because = 'we want to add this rule for our software'; + $violations = new Violations(); + $haveNameMatching->evaluate($classDesc, $violations, $because); + + self::assertEquals(0, $violations->count(), $explanation); + } + + public static function shouldNotMatchNamespacesProvider(): array + { + return [ + ['Food\Vegetables', 'Food\Vegetables\Roots\Carrot', 'should not match a class in a child namespace'], + ['Food\Vegetables', 'Food\Vegetables\Roots\Orange\Carrot', 'should not match a class in a child of a child namespace'], + ['Food', 'Food\Vegetables\Carrot', 'should not match a class in a child namespace'], + ['Food\Vegetables\Roots', 'Food\Vegetables\Carrot', 'should not match a class in a different namespace'], + ]; + } + + /** + * @dataProvider shouldNotMatchNamespacesProvider + * + * @param mixed $expectedNamespace + * @param mixed $actualFQCN + * @param mixed $explanation + */ + public function test_it_should_not_match_child_namespaces($expectedNamespace, $actualFQCN, $explanation): void + { + $haveNameMatching = new ResideInOneOfTheseNamespacesExactly($expectedNamespace); + + $classDesc = ClassDescription::getBuilder($actualFQCN, 'src/Foo.php')->build(); + $because = 'we want to add this rule for our software'; + $violations = new Violations(); + $haveNameMatching->evaluate($classDesc, $violations, $because); + + self::assertNotEquals(0, $violations->count(), $explanation); + } + + public function test_it_should_return_false_if_not_reside_in_namespace(): void + { + $haveNameMatching = new ResideInOneOfTheseNamespacesExactly('MyNamespace'); + + $classDesc = ClassDescription::getBuilder('AnotherNamespace\HappyIsland', 'src/Foo.php')->build(); + $because = 'we want to add this rule for our software'; + $violations = new Violations(); + $haveNameMatching->evaluate($classDesc, $violations, $because); + + self::assertNotEquals(0, $violations->count()); + } + + public function test_it_should_check_multiple_namespaces_in_or(): void + { + $haveNameMatching = new ResideInOneOfTheseNamespacesExactly('MyNamespace', 'AnotherNamespace', 'AThirdNamespace'); + + $classDesc = ClassDescription::getBuilder('AnotherNamespace\HappyIsland', 'src/Foo.php')->build(); + $violations = new Violations(); + $because = 'we want to add this rule for our software'; + $haveNameMatching->evaluate($classDesc, $violations, $because); + self::assertEquals(0, $violations->count()); + + $classDesc = ClassDescription::getBuilder('MyNamespace\HappyIsland', 'src/Foo.php')->build(); + $violations = new Violations(); + $haveNameMatching->evaluate($classDesc, $violations, $because); + self::assertEquals(0, $violations->count()); + + $classDesc = ClassDescription::getBuilder('AThirdNamespace\HappyIsland', 'src/Foo.php')->build(); + $violations = new Violations(); + $haveNameMatching->evaluate($classDesc, $violations, $because); + self::assertEquals(0, $violations->count()); + + $classDesc = ClassDescription::getBuilder('NopeNamespace\HappyIsland', 'src/Foo.php')->build(); + $violations = new Violations(); + $haveNameMatching->evaluate($classDesc, $violations, $because); + self::assertNotEquals(0, $violations->count()); + } + + public function test_duplicate_namespaces_are_removed(): void + { + $expression = new ResideInOneOfTheseNamespacesExactly('A', 'B', 'A', 'C', 'D', 'D'); + + self::assertSame( + 'should reside in one of these namespaces exactly: A, B, C, D because rave', + $expression->describe(ClassDescription::getBuilder('Marko', 'src/Foo.php')->build(), 'rave')->toString() + ); + } +}