Skip to content

Commit

Permalink
Add ShouldInclude and ShouldNotInclude assertions
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosas authored Feb 8, 2024
2 parents 119108f + 861024e commit 9e6381a
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 2 deletions.
5 changes: 5 additions & 0 deletions docs/documentation/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ It asserts that the selected classes **implement** the target interfaces.

Also available: `shouldNotImplement()`

## shouldInclude()
It asserts that the selected classes **include** the target traits.

Also available: `shouldNotInclude()`

## shouldNotDependOn()
It asserts that the selected classes **do not depend** on the target classes.

Expand Down
12 changes: 12 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ services:
tags:
- phpstan.rules.rule

# ShouldInclude rules
-
class: PHPat\Rule\Assertion\Relation\ShouldInclude\IncludedTraitsRule
tags:
- phpstan.rules.rule

# ShouldNotDepend rules
-
class: PHPat\Rule\Assertion\Relation\ShouldNotDepend\DirectInterfacesRule
Expand Down Expand Up @@ -230,6 +236,12 @@ services:
tags:
- phpstan.rules.rule

# ShouldNotInclude rules
-
class: PHPat\Rule\Assertion\Relation\ShouldNotInclude\IncludedTraitsRule
tags:
- phpstan.rules.rule

# ShouldApplyAttribute rules
-
class: PHPat\Rule\Assertion\Relation\ShouldApplyAttribute\ClassAttributeRule
Expand Down
5 changes: 3 additions & 2 deletions src/Rule/Assertion/Relation/RelationAssertion.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPat\Rule\Assertion\Relation\ShouldApplyAttribute\ShouldApplyAttribute;
use PHPat\Rule\Assertion\Relation\ShouldExtend\ShouldExtend;
use PHPat\Rule\Assertion\Relation\ShouldImplement\ShouldImplement;
use PHPat\Rule\Assertion\Relation\ShouldInclude\ShouldInclude;
use PHPat\Selector\Classname;
use PHPat\Selector\SelectorInterface;
use PHPat\ShouldNotHappenException;
Expand Down Expand Up @@ -98,8 +99,8 @@ protected function ruleApplies(Scope $scope, array $nodes): bool
return false;
}

// Can not skip if the rule is a ShouldExtend, ShouldImplement or ShouldApplyAttribute rule
if (is_a($this, ShouldExtend::class) || is_a($this, ShouldImplement::class) || is_a($this, ShouldApplyAttribute::class)) {
// Can not skip if the rule is a ShouldExtend, ShouldImplement, ShouldInclude or ShouldApplyAttribute rule
if (is_a($this, ShouldExtend::class) || is_a($this, ShouldImplement::class) || is_a($this, ShouldInclude::class) || is_a($this, ShouldApplyAttribute::class)) {
return true;
}

Expand Down
15 changes: 15 additions & 0 deletions src/Rule/Assertion/Relation/ShouldInclude/IncludedTraitsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types=1);

namespace PHPat\Rule\Assertion\Relation\ShouldInclude;

use PHPat\Rule\Extractor\Relation\AllTraitsExtractor;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;

/**
* @implements Rule<InClassNode>
*/
final class IncludedTraitsRule extends ShouldInclude implements Rule
{
use AllTraitsExtractor;
}
50 changes: 50 additions & 0 deletions src/Rule/Assertion/Relation/ShouldInclude/ShouldInclude.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types=1);

namespace PHPat\Rule\Assertion\Relation\ShouldInclude;

use PHPat\Configuration;
use PHPat\Rule\Assertion\Relation\RelationAssertion;
use PHPat\Rule\Assertion\Relation\ValidationTrait;
use PHPat\Statement\Builder\StatementBuilderFactory;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\FileTypeMapper;

abstract class ShouldInclude extends RelationAssertion
{
use ValidationTrait;

public function __construct(
StatementBuilderFactory $statementBuilderFactory,
Configuration $configuration,
ReflectionProvider $reflectionProvider,
FileTypeMapper $fileTypeMapper
) {
parent::__construct(
__CLASS__,
$statementBuilderFactory,
$configuration,
$reflectionProvider,
$fileTypeMapper
);
}

protected function applyValidation(
string $ruleName,
ClassReflection $subject,
array $targets,
array $targetExcludes,
array $nodes,
array $tips
): array {
return $this->applyShould($ruleName, $subject, $targets, $targetExcludes, $nodes, $tips);
}

protected function getMessage(string $ruleName, string $subject, string $target): string
{
return $this->prepareMessage(
$ruleName,
sprintf('%s should include %s', $subject, $target),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types=1);

namespace PHPat\Rule\Assertion\Relation\ShouldNotInclude;

use PHPat\Rule\Extractor\Relation\AllTraitsExtractor;
use PHPStan\Node\InClassNode;
use PHPStan\Rules\Rule;

/**
* @implements Rule<InClassNode>
*/
final class IncludedTraitsRule extends ShouldNotInclude implements Rule
{
use AllTraitsExtractor;
}
50 changes: 50 additions & 0 deletions src/Rule/Assertion/Relation/ShouldNotInclude/ShouldNotInclude.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types=1);

namespace PHPat\Rule\Assertion\Relation\ShouldNotInclude;

use PHPat\Configuration;
use PHPat\Rule\Assertion\Relation\RelationAssertion;
use PHPat\Rule\Assertion\Relation\ValidationTrait;
use PHPat\Statement\Builder\StatementBuilderFactory;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\FileTypeMapper;

abstract class ShouldNotInclude extends RelationAssertion
{
use ValidationTrait;

public function __construct(
StatementBuilderFactory $statementBuilderFactory,
Configuration $configuration,
ReflectionProvider $reflectionProvider,
FileTypeMapper $fileTypeMapper
) {
parent::__construct(
__CLASS__,
$statementBuilderFactory,
$configuration,
$reflectionProvider,
$fileTypeMapper
);
}

protected function applyValidation(
string $ruleName,
ClassReflection $subject,
array $targets,
array $targetExcludes,
array $nodes,
array $tips
): array {
return $this->applyShouldNot($ruleName, $subject, $targets, $targetExcludes, $nodes, $tips);
}

protected function getMessage(string $ruleName, string $subject, string $target): string
{
return $this->prepareMessage(
$ruleName,
sprintf('%s should not include %s', $subject, $target),
);
}
}
28 changes: 28 additions & 0 deletions src/Rule/Extractor/Relation/AllTraitsExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php declare(strict_types=1);

namespace PHPat\Rule\Extractor\Relation;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassNode;
use PHPStan\Reflection\ClassReflection;

trait AllTraitsExtractor
{
public function getNodeType(): string
{
return InClassNode::class;
}

/**
* @param InClassNode $node
* @return array<class-string>
*/
protected function extractNodeClassNames(Node $node, Scope $scope): array
{
return array_map(
static fn (ClassReflection $c) => $c->getName(),
$node->getClassReflection()->getTraits()
);
}
}
48 changes: 48 additions & 0 deletions src/Selector/ClassIncludes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types=1);

namespace PHPat\Selector;

use PHPStan\Reflection\ClassReflection;

final class ClassIncludes implements SelectorInterface
{
private string $traitname;
private bool $isRegex;

/**
* @param class-string|string $traitname
*/
public function __construct(string $traitname, bool $isRegex)
{
$this->traitname = $traitname;
$this->isRegex = $isRegex;
}

public function getName(): string
{
return $this->traitname;
}

public function matches(ClassReflection $classReflection): bool
{
if ($this->isRegex) {
return $this->matchesRegex($classReflection->getTraits());
}

return $classReflection->hasTraitUse($this->traitname);
}

/**
* @param array<ClassReflection> $traits
*/
private function matchesRegex(array $traits): bool
{
foreach ($traits as $trait) {
if (preg_match($this->traitname, $trait->getName()) === 1) {
return true;
}
}

return false;
}
}
8 changes: 8 additions & 0 deletions src/Selector/SelectorPrimitive.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,12 @@ public static function appliesAttribute(string $fqcn, bool $regex = false): Appl
{
return new AppliesAttribute($fqcn, $regex);
}

/**
* @param class-string|non-empty-string $fqcn
*/
public static function includes(string $fqcn, bool $regex = false): ClassIncludes
{
return new ClassIncludes($fqcn, $regex);
}
}
16 changes: 16 additions & 0 deletions src/Test/Builder/AssertionStep.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
use PHPat\Rule\Assertion\Relation\ShouldApplyAttribute\ShouldApplyAttribute;
use PHPat\Rule\Assertion\Relation\ShouldExtend\ShouldExtend;
use PHPat\Rule\Assertion\Relation\ShouldImplement\ShouldImplement;
use PHPat\Rule\Assertion\Relation\ShouldInclude\ShouldInclude;
use PHPat\Rule\Assertion\Relation\ShouldNotConstruct\ShouldNotConstruct;
use PHPat\Rule\Assertion\Relation\ShouldNotDepend\ShouldNotDepend;
use PHPat\Rule\Assertion\Relation\ShouldNotExtend\ShouldNotExtend;
use PHPat\Rule\Assertion\Relation\ShouldNotImplement\ShouldNotImplement;
use PHPat\Rule\Assertion\Relation\ShouldNotInclude\ShouldNotInclude;

class AssertionStep extends AbstractStep
{
Expand Down Expand Up @@ -106,6 +108,20 @@ public function shouldImplement(): TargetStep
return new TargetStep($this->rule);
}

public function shouldNotInclude(): TargetStep
{
$this->rule->assertion = ShouldNotInclude::class;

return new TargetStep($this->rule);
}

public function shouldInclude(): TargetStep
{
$this->rule->assertion = ShouldInclude::class;

return new TargetStep($this->rule);
}

public function shouldExtend(): TargetStep
{
$this->rule->assertion = ShouldExtend::class;
Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/Simple/SimpleTraitTwo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php declare(strict_types=1);

namespace Tests\PHPat\fixtures\Simple;

trait SimpleTraitTwo {}
49 changes: 49 additions & 0 deletions tests/unit/rules/ShouldInclude/IncludedTraitsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types=1);

namespace Tests\PHPat\unit\rules\ShouldInclude;

use PHPat\Configuration;
use PHPat\Rule\Assertion\Relation\ShouldInclude\IncludedTraitsRule;
use PHPat\Rule\Assertion\Relation\ShouldInclude\ShouldInclude;
use PHPat\Selector\Classname;
use PHPat\Statement\Builder\StatementBuilderFactory;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\FileTypeMapper;
use Tests\PHPat\fixtures\FixtureClass;
use Tests\PHPat\fixtures\Simple\SimpleTraitTwo;
use Tests\PHPat\unit\FakeTestParser;

/**
* @extends RuleTestCase<IncludedTraitsRule>
* @internal
* @coversNothing
*/
class IncludedTraitsTest extends RuleTestCase
{
public const RULE_NAME = 'test_FixtureClassShouldIncludeSimpleTraitTwo';

public function testRule(): void
{
$this->analyse(['tests/fixtures/FixtureClass.php'], [
[sprintf('%s should include %s', FixtureClass::class, SimpleTraitTwo::class), 29],
]);
}

protected function getRule(): Rule
{
$testParser = FakeTestParser::create(
self::RULE_NAME,
ShouldInclude::class,
[new Classname(FixtureClass::class, false)],
[new Classname(SimpleTraitTwo::class, false)]
);

return new IncludedTraitsRule(
new StatementBuilderFactory($testParser),
new Configuration(false, true, false),
$this->createReflectionProvider(),
self::getContainer()->getByType(FileTypeMapper::class)
);
}
}
Loading

0 comments on commit 9e6381a

Please sign in to comment.