diff --git a/ci/psalm.xml b/ci/psalm.xml index a575f85d..16cf8729 100644 --- a/ci/psalm.xml +++ b/ci/psalm.xml @@ -45,6 +45,12 @@ + + + + + + diff --git a/docs/documentation/rules.md b/docs/documentation/rules.md index ea3c8db4..88f1ed13 100644 --- a/docs/documentation/rules.md +++ b/docs/documentation/rules.md @@ -86,3 +86,35 @@ final class UserDomainTest extends AbstractDomainTest ``` Note that you would only need to register the `UserDomainTest` class as a PHPat test in the PHPStan config file. + +## Dynamic Rule Sets +It is possible to dynamically create rules by returning an iterable of Rules from your method: + +```php +namespace App\Tests\Architecture; + +use PHPat\Selector\Selector; +use PHPat\Test\Builder\Rule; +use PHPat\Test\PHPat; + +final class ConfigurationTest +{ + private const DOMAINS = [ + 'App\Domain1', + 'App\Domain2', + ]; + + /** + * @return iterable + */ + public function test_domain_independence(): iterable + { + foreach(self::DOMAINS as $domain) { + yield $domain => PHPat::rule() + ->classes(Selector::inNamespace($domain)) + ->canOnlyDependOn() + ->classes(Selector::inNamespace($domain)); + } + } +} +``` diff --git a/src/Test/RuleValidator.php b/src/Test/RuleValidator.php index 61026b55..a2214bf0 100644 --- a/src/Test/RuleValidator.php +++ b/src/Test/RuleValidator.php @@ -4,11 +4,8 @@ use PHPat\Rule\Assertion\Relation\RelationAssertion; -final class RuleValidator +final class RuleValidator implements RuleValidatorInterface { - /** - * @throws \Exception - */ public function validate(Rule $rule): void { if ($rule->getSubjects() === []) { diff --git a/src/Test/RuleValidatorInterface.php b/src/Test/RuleValidatorInterface.php new file mode 100644 index 00000000..cf80e09e --- /dev/null +++ b/src/Test/RuleValidatorInterface.php @@ -0,0 +1,11 @@ +reflectionProvider = $reflectionProvider; } - /** - * @return iterable - */ public function __invoke(): iterable { foreach ($this->container->getServicesByTag(self::TEST_TAG) as $test) { @@ -38,14 +34,15 @@ public function __invoke(): iterable } /** - * @param class-string $test + * @param class-string $test + * @return null|\ReflectionClass */ - private function reflectTest(string $test): ?ClassReflection + private function reflectTest(string $test): ?\ReflectionClass { if (!$this->reflectionProvider->hasClass($test)) { return null; } - return $this->reflectionProvider->getClass($test); + return $this->reflectionProvider->getClass($test)->getNativeReflection(); } } diff --git a/src/Test/TestExtractorInterface.php b/src/Test/TestExtractorInterface.php new file mode 100644 index 00000000..98883b4c --- /dev/null +++ b/src/Test/TestExtractorInterface.php @@ -0,0 +1,11 @@ +> + */ + public function __invoke(): iterable; +} diff --git a/src/Test/TestParser.php b/src/Test/TestParser.php index 3d52034f..401b0583 100644 --- a/src/Test/TestParser.php +++ b/src/Test/TestParser.php @@ -9,10 +9,10 @@ class TestParser { /** @var array */ private static array $result = []; - private TestExtractor $extractor; - private RuleValidator $ruleValidator; + private TestExtractorInterface $extractor; + private RuleValidatorInterface $ruleValidator; - public function __construct(TestExtractor $extractor, RuleValidator $ruleValidator) + public function __construct(TestExtractorInterface $extractor, RuleValidatorInterface $ruleValidator) { $this->extractor = $extractor; $this->ruleValidator = $ruleValidator; @@ -38,18 +38,22 @@ private function parse(): array $tests = ($this->extractor)(); $rules = []; - foreach ($tests as $test) { - $methods = []; - $reflected = $test->getNativeReflection(); + foreach ($tests as $reflected) { $classname = $reflected->getName(); $object = $reflected->newInstanceWithoutConstructor(); foreach ($reflected->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { if ( - !empty($method->getAttributes(TestRule::class)) + method_exists($method, 'getAttributes') && !empty($method->getAttributes(TestRule::class)) || preg_match('/^(test)[A-Za-z0-9_\x80-\xff]*/', $method->getName()) ) { $ruleBuilder = $object->{$method->getName()}(); - $rules[$classname.':'.$method->getName()] = $ruleBuilder; + if (is_iterable($ruleBuilder)) { + foreach ($ruleBuilder as $name => $rule) { + $rules[$classname.':'.$method->getName().':'.$name] = $rule; + } + } else { + $rules[$classname.':'.$method->getName()] = $ruleBuilder; + } } } } diff --git a/tests/integration/test/TestParser/ParsesTestsTest.php b/tests/integration/test/TestParser/ParsesTestsTest.php new file mode 100644 index 00000000..3018321c --- /dev/null +++ b/tests/integration/test/TestParser/ParsesTestsTest.php @@ -0,0 +1,52 @@ +classes(Selector::classname('1'))(); + $rule1->ruleName = TestClass::class.':test_rules_from_iterator:one'; + + $rule2 = PHPat::rule()->classes(Selector::classname('2'))(); + $rule2->ruleName = TestClass::class.':test_rules_from_iterator:two'; + + $rule3 = PHPat::rule()->classes(Selector::classname('3'))(); + $rule3->ruleName = TestClass::class.':test_rule'; + + $rule4 = PHPat::rule()->classes(Selector::classname('4'))(); + $rule4->ruleName = TestClass::class.':test_rule_from_attribute'; + + self::assertEquals([ + $rule1, + $rule2, + $rule3, + $rule4, + ], ($testParser)()); + } +} diff --git a/tests/integration/test/TestParser/TestClass.php b/tests/integration/test/TestParser/TestClass.php new file mode 100644 index 00000000..322e9b1f --- /dev/null +++ b/tests/integration/test/TestParser/TestClass.php @@ -0,0 +1,32 @@ + + */ + public function test_rules_from_iterator(): iterable + { + yield 'one' => PHPat::rule()->classes(Selector::classname('1')); + + yield 'two' => PHPat::rule()->classes(Selector::classname('2')); + } + + public function test_rule(): Rule + { + return PHPat::rule()->classes(Selector::classname('3')); + } + + #[TestRule] + public function test_rule_from_attribute(): Rule + { + return PHPat::rule()->classes(Selector::classname('4')); + } +}