Skip to content
Draft
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
25 changes: 25 additions & 0 deletions 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 @@ -42,6 +45,7 @@ class ClassDescription
* @param list<FullyQualifiedClassName> $interfaces
* @param list<FullyQualifiedClassName> $extends
* @param list<FullyQualifiedClassName> $attributes
* @param list<FullyQualifiedClassName> $traits
* @param list<string> $docBlock
*/
public function __construct(
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);
}
}
}
71 changes: 71 additions & 0 deletions tests/Unit/Expressions/ForClasses/HaveTraitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses;

use Arkitect\Analyzer\ClassDescriptionBuilder;
use Arkitect\Expression\ForClasses\HaveTrait;
use Arkitect\Rules\Violations;
use PHPUnit\Framework\TestCase;

class HaveTraitTest extends TestCase
{
public function test_it_should_return_true_if_class_uses_trait(): void
{
$expression = new HaveTrait('MyTrait');

$classDescription = (new ClassDescriptionBuilder())
->setFilePath('src/Foo.php')
->setClassName('HappyIsland\Myclass')
->addTrait('MyTrait', 1)
->build();

$because = 'we want to add this rule for our software';
$violations = new Violations();
$expression->evaluate($classDescription, $violations, $because);

self::assertEquals(0, $violations->count());
self::assertEquals(
'should use the trait MyTrait because we want to add this rule for our software',
$expression->describe($classDescription, $because)->toString()
);
}

public function test_it_should_return_true_if_class_uses_trait_without_because(): void
{
$expression = new HaveTrait('MyTrait');

$classDescription = (new ClassDescriptionBuilder())
->setFilePath('src/Foo.php')
->setClassName('HappyIsland\Myclass')
->addTrait('MyTrait', 1)
->build();

$violations = new Violations();
$expression->evaluate($classDescription, $violations, '');

self::assertEquals(0, $violations->count());
self::assertEquals(
'should use the trait MyTrait',
$expression->describe($classDescription, '')->toString()
);
}

public function test_it_should_return_false_if_class_does_not_use_trait(): void
{
$expression = new HaveTrait('AnotherTrait');

$classDescription = (new ClassDescriptionBuilder())
->setFilePath('src/Foo.php')
->setClassName('HappyIsland\Myclass')
->addTrait('MyTrait', 1)
->build();

$because = 'we want to add this rule for our software';
$violations = new Violations();
$expression->evaluate($classDescription, $violations, $because);

self::assertEquals(1, $violations->count());
}
}
Loading