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
27 changes: 26 additions & 1 deletion src/Analyzer/ClassDescription.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class ClassDescription
/** @var list<FullyQualifiedClassName> */
private array $attributes;

/** @var list<FullyQualifiedClassName> */
private array $traits;

private bool $final;

private bool $readonly;
Expand All @@ -41,8 +44,9 @@ class ClassDescription
* @param list<ClassDependency> $dependencies
* @param list<FullyQualifiedClassName> $interfaces
* @param list<FullyQualifiedClassName> $extends
* @param list<FullyQualifiedClassName> $attributes
* @param list<string> $docBlock
* @param list<FullyQualifiedClassName> $attributes
* @param list<FullyQualifiedClassName> $traits
*/
public function __construct(
FullyQualifiedClassName $FQCN,
Expand All @@ -57,6 +61,7 @@ public function __construct(
bool $enum,
array $docBlock,
array $attributes,
array $traits,
string $filePath
) {
$this->FQCN = $FQCN;
Expand All @@ -69,6 +74,7 @@ public function __construct(
$this->abstract = $abstract;
$this->docBlock = $docBlock;
$this->attributes = $attributes;
$this->traits = $traits;
$this->interface = $interface;
$this->trait = $trait;
$this->enum = $enum;
Expand Down Expand Up @@ -202,4 +208,23 @@ static function (bool $carry, FullyQualifiedClassName $attribute) use ($pattern)
false
);
}

/**
* @return list<FullyQualifiedClassName>
*/
public function getTraits(): array
{
return $this->traits;
}

public function hasTrait(string $pattern): bool
{
return array_reduce(
$this->traits,
static function (bool $carry, FullyQualifiedClassName $trait) use ($pattern): bool {
return $carry || $trait->matches($pattern);
},
false
);
}
}
13 changes: 13 additions & 0 deletions src/Analyzer/ClassDescriptionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ class ClassDescriptionBuilder
/** @var list<FullyQualifiedClassName> */
private array $attributes = [];

/** @var list<FullyQualifiedClassName> */
private array $traits = [];

private bool $interface = false;

private bool $trait = false;
Expand All @@ -49,6 +52,7 @@ public function clear(): void
$this->abstract = false;
$this->docBlock = [];
$this->attributes = [];
$this->traits = [];
$this->interface = false;
$this->trait = false;
$this->enum = false;
Expand Down Expand Up @@ -148,6 +152,14 @@ public function addAttribute(string $FQCN, int $line): self
return $this;
}

public function addTrait(string $FQCN, int $line): self
{
$this->addDependency(new ClassDependency($FQCN, $line));
$this->traits[] = FullyQualifiedClassName::fromString($FQCN);

return $this;
}

public function build(): ClassDescription
{
Assert::notNull($this->FQCN, 'You must set an FQCN');
Expand All @@ -166,6 +178,7 @@ public function build(): ClassDescription
$this->enum,
$this->docBlock,
$this->attributes,
$this->traits,
$this->filePath
);
}
Expand Down
15 changes: 15 additions & 0 deletions src/Analyzer/FileVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public function enterNode(Node $node): void
// handles trait definition like trait MyTrait {}
$this->handleTraitNode($node);

// handles trait usage like use MyTrait;
$this->handleTraitUseNode($node);

// handles code like $constantValue = StaticClass::constant;
$this->handleStaticClassConstantNode($node);

Expand Down Expand Up @@ -302,6 +305,18 @@ private function handleTraitNode(Node $node): void
$this->classDescriptionBuilder->setTrait(true);
}

private function handleTraitUseNode(Node $node): void
{
if (!($node instanceof Node\Stmt\TraitUse)) {
return;
}

foreach ($node->traits as $trait) {
$this->classDescriptionBuilder
->addTrait($trait->toString(), $trait->getLine());
}
}

private function handleReturnTypeDependency(Node $node): void
{
if (!($node instanceof Node\Stmt\ClassMethod)) {
Expand Down
42 changes: 42 additions & 0 deletions src/Expression/ForClasses/HaveTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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;

final class HaveTrait implements Expression
{
/** @var string */
private $trait;

public function __construct(string $trait)
{
$this->trait = $trait;
}

public function describe(ClassDescription $theClass, string $because): Description
{
return new Description("should use the trait {$this->trait}", $because);
}

public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
{
if ($theClass->hasTrait($this->trait)) {
return;
}

$violations->add(
Violation::create(
$theClass->getFQCN(),
ViolationMessage::selfExplanatory($this->describe($theClass, $because)),
$theClass->getFilePath()
)
);
}
}
56 changes: 56 additions & 0 deletions src/Expression/ForClasses/NotHaveTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Arkitect\Expression\ForClasses;

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

class NotHaveTrait implements Expression
{
/** @var string */
private $trait;

public function __construct(string $trait)
{
$this->trait = $trait;
}

public function describe(ClassDescription $theClass, string $because): Description
{
return new Description("should not use the trait {$this->trait}", $because);
}

public function appliesTo(ClassDescription $theClass): bool
{
return !($theClass->isInterface() || $theClass->isTrait());
}

public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
{
if ($theClass->isInterface() || $theClass->isTrait()) {
return;
}

$trait = $this->trait;
$traits = $theClass->getTraits();
$usesTrait = function (FullyQualifiedClassName $FQCN) use ($trait): bool {
return $FQCN->matches($trait);
};

if (\count(array_filter($traits, $usesTrait)) > 0) {
$violation = Violation::create(
$theClass->getFQCN(),
ViolationMessage::selfExplanatory($this->describe($theClass, $because)),
$theClass->getFilePath()
);
$violations->add($violation);
}
}
}
141 changes: 141 additions & 0 deletions tests/Integration/CheckClassHaveTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Integration\PHPUnit;

use Arkitect\Expression\ForClasses\HaveTrait;
use Arkitect\Expression\ForClasses\NotHaveTrait;
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use Arkitect\Rules\Rule;
use Arkitect\Tests\Utils\TestRunner;
use org\bovigo\vfs\vfsStream;
use PHPUnit\Framework\TestCase;

final class CheckClassHaveTraitTest extends TestCase
{
public function test_feature_tests_should_use_database_transactions_trait(): void
{
$dir = vfsStream::setup('root', null, $this->createDirStructure())->url();

$runner = TestRunner::create('8.4');

$rule = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('Tests\Feature'))
->should(new HaveTrait('DatabaseTransactions'))
->because('we want all Feature tests to run transactions');

$runner->run($dir, $rule);

self::assertCount(1, $runner->getViolations());
self::assertCount(0, $runner->getParsingErrors());

self::assertEquals('Tests\Feature\UserFeatureTest', $runner->getViolations()->get(0)->getFqcn());
}

public function test_feature_tests_should_not_use_refresh_database_trait(): void
{
$dir = vfsStream::setup('root', null, $this->createDirStructure())->url();

$runner = TestRunner::create('8.4');

$rule = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('Tests\Feature'))
->should(new NotHaveTrait('RefreshDatabase'))
->because('we want all Feature tests to never refresh the database for performance reasons');

$runner->run($dir, $rule);

self::assertCount(1, $runner->getViolations());
self::assertCount(0, $runner->getParsingErrors());

self::assertEquals('Tests\Feature\ProductFeatureTest', $runner->getViolations()->get(0)->getFqcn());
}

public function test_classes_with_uuid_should_have_uuid_trait(): void
{
$dir = vfsStream::setup('root', null, $this->createDirStructure())->url();

$runner = TestRunner::create('8.4');

$rule = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\Models'))
->should(new HaveTrait('HasUuid'))
->because('all models should use UUID');

$runner->run($dir, $rule);

self::assertCount(0, $runner->getViolations());
self::assertCount(0, $runner->getParsingErrors());
}

public function createDirStructure(): array
{
return [
'Feature' => [
'OrderFeatureTest.php' => <<<'EOT'
<?php

namespace Tests\Feature;

use DatabaseTransactions;

class OrderFeatureTest
{
use DatabaseTransactions;
}
EOT,
'ProductFeatureTest.php' => <<<'EOT'
<?php

namespace Tests\Feature;

use DatabaseTransactions;
use RefreshDatabase;

class ProductFeatureTest
{
use DatabaseTransactions;
use RefreshDatabase;
}
EOT,
'UserFeatureTest.php' => <<<'EOT'
<?php

namespace Tests\Feature;

class UserFeatureTest
{
// Missing DatabaseTransactions trait
}
EOT,
],
'Models' => [
'User.php' => <<<'EOT'
<?php

namespace App\Models;

use HasUuid;

class User
{
use HasUuid;
}
EOT,
'Product.php' => <<<'EOT'
<?php

namespace App\Models;

use HasUuid;

class Product
{
use HasUuid;
}
EOT,
],
];
}
}
16 changes: 16 additions & 0 deletions tests/Unit/Analyzer/ClassDescriptionBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,22 @@ public function test_it_should_add_attributes(): void
);
}

public function test_it_should_add_traits(): void
{
$FQCN = 'HappyIsland';

$classDescription = (new ClassDescriptionBuilder())
->setFilePath('src/Foo.php')
->setClassName($FQCN)
->addTrait('TraitClass', 15)
->build();

self::assertEquals(
[FullyQualifiedClassName::fromString('TraitClass')],
$classDescription->getTraits()
);
}

public function test_it_should_create_interface(): void
{
$FQCN = 'HappyIsland';
Expand Down
Loading