Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Analyzer/ClassDescription.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Arkitect\Expression\ForClasses;

use Arkitect\Analyzer\ClassDescription;
use Arkitect\Expression\Description;
use Arkitect\Expression\Expression;
use Arkitect\Rules\Violation;
use Arkitect\Rules\ViolationMessage;
use Arkitect\Rules\Violations;

class NotResideInOneOfTheseNamespacesExactly implements Expression
{
/** @var array<string> */
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);
}
}
}
49 changes: 49 additions & 0 deletions src/Expression/ForClasses/ResideInOneOfTheseNamespacesExactly.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Arkitect\Expression\ForClasses;

use Arkitect\Analyzer\ClassDescription;
use Arkitect\Expression\Description;
use Arkitect\Expression\Expression;
use Arkitect\Rules\Violation;
use Arkitect\Rules\ViolationMessage;
use Arkitect\Rules\Violations;

class ResideInOneOfTheseNamespacesExactly implements Expression
{
/** @var array<string> */
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses;

use Arkitect\Analyzer\ClassDescription;
use Arkitect\Expression\ForClasses\NotResideInOneOfTheseNamespacesExactly;
use Arkitect\Rules\Violations;
use PHPUnit\Framework\TestCase;

class NotResideInOneOfTheseNamespacesExactlyTest extends TestCase
{
public function test_it_should_return_true_if_not_reside_in_namespace(): void
{
$haveNameMatching = new NotResideInOneOfTheseNamespacesExactly('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::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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses;

use Arkitect\Analyzer\ClassDescription;
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespacesExactly;
use Arkitect\Rules\Violations;
use PHPUnit\Framework\TestCase;

class ResideInOneOfTheseNamespacesExactlyTest extends TestCase
{
public static function shouldMatchNamespacesProvider(): array
{
return [
['Food\Vegetables', 'Food\Vegetables\Carrot', 'matches a class in the exact namespace'],
['Food', 'Food\Vegetables', 'matches a class in the exact namespace'],
['', 'Carrot', 'matches a class in the root namespace'],
];
}

/**
* @dataProvider shouldMatchNamespacesProvider
*
* @param mixed $expectedNamespace
* @param mixed $actualFQCN
* @param mixed $explanation
*/
public function test_it_should_match_exact_namespace($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::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()
);
}
}