diff --git a/docs/documentation/assertions.md b/docs/documentation/assertions.md index 7d5833c4..fad01858 100644 --- a/docs/documentation/assertions.md +++ b/docs/documentation/assertions.md @@ -34,6 +34,9 @@ It asserts that the selected classes **do not depend** on the target classes. ## shouldNotConstruct() It asserts that the selected classes **do not use the constructor** of the target classes. +## shouldHaveAttribute() +It asserts that the selected classes **apply** the target attributes. + ## canOnlyDependOn() It asserts that the selected classes **do not depend** on anything else than the target classes. diff --git a/extension.neon b/extension.neon index 6cfc8e3b..34bb348f 100644 --- a/extension.neon +++ b/extension.neon @@ -218,6 +218,12 @@ services: tags: - phpstan.rules.rule + # ShouldHaveAttribute rules + - + class: PHPat\Rule\Assertion\Relation\ShouldHaveAttribute\ClassAttributeRule + tags: + - phpstan.rules.rule + parametersSchema: phpat: structure([ ignore_doc_comments: bool(), diff --git a/src/Rule/Assertion/Relation/RelationAssertion.php b/src/Rule/Assertion/Relation/RelationAssertion.php index 9cc5e33d..2f696a82 100644 --- a/src/Rule/Assertion/Relation/RelationAssertion.php +++ b/src/Rule/Assertion/Relation/RelationAssertion.php @@ -5,6 +5,7 @@ use PHPat\Configuration; use PHPat\Rule\Assertion\Assertion; use PHPat\Rule\Assertion\Relation\ShouldExtend\ShouldExtend; +use PHPat\Rule\Assertion\Relation\ShouldHaveAttribute\ShouldHaveAttribute; use PHPat\Rule\Assertion\Relation\ShouldImplement\ShouldImplement; use PHPat\Selector\Classname; use PHPat\Selector\SelectorInterface; @@ -97,8 +98,8 @@ protected function ruleApplies(Scope $scope, array $nodes): bool return false; } - // Can not skip if the rule is a ShouldExtend or ShouldImplement rule - if (is_a($this, ShouldExtend::class) || is_a($this, ShouldImplement::class)) { + // Can not skip if the rule is a ShouldExtend, ShouldImplement or ShouldHaveAttribute rule + if (is_a($this, ShouldExtend::class) || is_a($this, ShouldImplement::class) || is_a($this, ShouldHaveAttribute::class)) { return true; } diff --git a/src/Rule/Assertion/Relation/ShouldHaveAttribute/ClassAttributeRule.php b/src/Rule/Assertion/Relation/ShouldHaveAttribute/ClassAttributeRule.php new file mode 100644 index 00000000..bca0322e --- /dev/null +++ b/src/Rule/Assertion/Relation/ShouldHaveAttribute/ClassAttributeRule.php @@ -0,0 +1,15 @@ + + */ +final class ClassAttributeRule extends ShouldHaveAttribute implements Rule +{ + use ClassAttributeExtractor; +} diff --git a/src/Rule/Assertion/Relation/ShouldHaveAttribute/ShouldHaveAttribute.php b/src/Rule/Assertion/Relation/ShouldHaveAttribute/ShouldHaveAttribute.php new file mode 100644 index 00000000..34a7654e --- /dev/null +++ b/src/Rule/Assertion/Relation/ShouldHaveAttribute/ShouldHaveAttribute.php @@ -0,0 +1,50 @@ +applyShould($ruleName, $subject, $targets, $targetExcludes, $nodes, $tips); + } + + protected function getMessage(string $ruleName, string $subject, string $target): string + { + return $this->prepareMessage( + $ruleName, + sprintf('%s should have attribute %s', $subject, $target), + ); + } +} diff --git a/src/Test/Builder/AssertionStep.php b/src/Test/Builder/AssertionStep.php index a84cdbfe..fb2000f6 100644 --- a/src/Test/Builder/AssertionStep.php +++ b/src/Test/Builder/AssertionStep.php @@ -10,6 +10,7 @@ use PHPat\Rule\Assertion\Declaration\ShouldNotBeFinal\ShouldNotBeFinal; use PHPat\Rule\Assertion\Relation\CanOnlyDepend\CanOnlyDepend; use PHPat\Rule\Assertion\Relation\ShouldExtend\ShouldExtend; +use PHPat\Rule\Assertion\Relation\ShouldHaveAttribute\ShouldHaveAttribute; use PHPat\Rule\Assertion\Relation\ShouldImplement\ShouldImplement; use PHPat\Rule\Assertion\Relation\ShouldNotConstruct\ShouldNotConstruct; use PHPat\Rule\Assertion\Relation\ShouldNotDepend\ShouldNotDepend; @@ -108,4 +109,11 @@ public function shouldHaveOnlyOnePublicMethod(): Rule return new BuildStep($this->rule); } + + public function shouldHaveAttribute(): TargetStep + { + $this->rule->assertion = ShouldHaveAttribute::class; + + return new TargetStep($this->rule); + } } diff --git a/tests/fixtures/Simple/SimpleAttributeTwo.php b/tests/fixtures/Simple/SimpleAttributeTwo.php new file mode 100644 index 00000000..fc86e12c --- /dev/null +++ b/tests/fixtures/Simple/SimpleAttributeTwo.php @@ -0,0 +1,6 @@ + + * @internal + * @coversNothing + */ +class ClassAttributeTest extends RuleTestCase +{ + public const RULE_NAME = 'test_FixtureClassShouldHaveSimpleAttributeTwo'; + + public function testRule(): void + { + $this->analyse(['tests/fixtures/FixtureClass.php'], [ + [sprintf('%s should have attribute %s', FixtureClass::class, SimpleAttributeTwo::class), 29], + ]); + } + + protected function getRule(): Rule + { + $testParser = FakeTestParser::create( + self::RULE_NAME, + ShouldHaveAttribute::class, + [new Classname(FixtureClass::class, false)], + [new Classname(SimpleAttributeTwo::class, false)] + ); + + return new ClassAttributeRule( + new StatementBuilderFactory($testParser), + new Configuration(false, true, false), + $this->createReflectionProvider(), + self::getContainer()->getByType(FileTypeMapper::class) + ); + } +} diff --git a/tests/unit/rules/ShouldHaveAttribute/SimpleClassAttributeTest.php b/tests/unit/rules/ShouldHaveAttribute/SimpleClassAttributeTest.php new file mode 100644 index 00000000..6f0054a5 --- /dev/null +++ b/tests/unit/rules/ShouldHaveAttribute/SimpleClassAttributeTest.php @@ -0,0 +1,49 @@ + + * @internal + * @coversNothing + */ +class SimpleClassAttributeTest extends RuleTestCase +{ + public const RULE_NAME = 'test_SimpleClassShouldHaveSimpleAttribute'; + + public function testRule(): void + { + $this->analyse(['tests/fixtures/Simple/SimpleClass.php'], [ + [sprintf('%s should have attribute %s', SimpleClass::class, SimpleAttribute::class), 5], + ]); + } + + protected function getRule(): Rule + { + $testParser = FakeTestParser::create( + self::RULE_NAME, + ShouldHaveAttribute::class, + [new Classname(SimpleClass::class, false)], + [new Classname(SimpleAttribute::class, false)] + ); + + return new ClassAttributeRule( + new StatementBuilderFactory($testParser), + new Configuration(false, true, false), + $this->createReflectionProvider(), + self::getContainer()->getByType(FileTypeMapper::class) + ); + } +}