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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,15 @@ $rules[] = Rule::allClasses()
->because('we want uniform naming for services');
```

### Use a trait

```php
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('Tests\Feature'))
->should(new HaveTrait('Illuminate\Foundation\Testing\DatabaseTransactions'))
->because('we want all Feature tests to run transactions');
```

### Implements an interface

```php
Expand All @@ -235,6 +244,15 @@ $rules[] = Rule::allClasses()
->because('all public controllers should not be container aware');
```

### Not use a trait

```php
$rules[] = Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('Tests\Feature'))
->should(new NotHaveTrait('Illuminate\Foundation\Testing\RefreshDatabase'))
->because('we want all Feature tests to never refresh the database for performance reasons');
```

### Is abstract

```php
Expand Down
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);
}
}
}
Loading