From 4e2899cd4c64f5aa8937bb300f42c3fbbf84f51e Mon Sep 17 00:00:00 2001 From: Anna Damm Date: Thu, 13 Jun 2024 17:56:52 +0200 Subject: [PATCH 1/9] allow test function to return an iterable of RuleBuilders --- src/Test/TestParser.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Test/TestParser.php b/src/Test/TestParser.php index 3d52034f..d5971351 100644 --- a/src/Test/TestParser.php +++ b/src/Test/TestParser.php @@ -39,7 +39,6 @@ private function parse(): array $rules = []; foreach ($tests as $test) { - $methods = []; $reflected = $test->getNativeReflection(); $classname = $reflected->getName(); $object = $reflected->newInstanceWithoutConstructor(); @@ -49,7 +48,13 @@ private function parse(): array || 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; + } } } } From 84cb841aad2a640eb92928e2e687cc1f88365340 Mon Sep 17 00:00:00 2001 From: Anna Damm Date: Thu, 13 Jun 2024 18:03:54 +0200 Subject: [PATCH 2/9] add readme entry to dynamic rule sets --- docs/documentation/rules.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) 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)); + } + } +} +``` From e41a372f17f8a6dccbe9780f05d4232758e60509 Mon Sep 17 00:00:00 2001 From: Anna Damm Date: Thu, 13 Jun 2024 18:48:59 +0200 Subject: [PATCH 3/9] refactor test parser to require interfaces instead of classes directly, refactor TestExtractor to return native ReflectionClasses instead of PhpStan ClassReflection --- src/Test/RuleValidator.php | 4 ++-- src/Test/RuleValidatorInterface.php | 13 +++++++++++++ src/Test/TestExtractor.php | 9 ++++----- src/Test/TestExtractorInterface.php | 13 +++++++++++++ src/Test/TestParser.php | 9 ++++----- 5 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 src/Test/RuleValidatorInterface.php create mode 100644 src/Test/TestExtractorInterface.php diff --git a/src/Test/RuleValidator.php b/src/Test/RuleValidator.php index 61026b55..51f0e64e 100644 --- a/src/Test/RuleValidator.php +++ b/src/Test/RuleValidator.php @@ -4,10 +4,10 @@ use PHPat\Rule\Assertion\Relation\RelationAssertion; -final class RuleValidator +final class RuleValidator implements RuleValidatorInterface { /** - * @throws \Exception + * @inheritDoc */ public function validate(Rule $rule): void { diff --git a/src/Test/RuleValidatorInterface.php b/src/Test/RuleValidatorInterface.php new file mode 100644 index 00000000..d1969135 --- /dev/null +++ b/src/Test/RuleValidatorInterface.php @@ -0,0 +1,13 @@ + + * @inheritDoc */ public function __invoke(): iterable { @@ -40,12 +39,12 @@ public function __invoke(): iterable /** * @param class-string $test */ - 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..493dd705 --- /dev/null +++ b/src/Test/TestExtractorInterface.php @@ -0,0 +1,13 @@ + + */ + public function __invoke(): iterable; +} \ No newline at end of file diff --git a/src/Test/TestParser.php b/src/Test/TestParser.php index d5971351..9968a915 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,8 +38,7 @@ private function parse(): array $tests = ($this->extractor)(); $rules = []; - foreach ($tests as $test) { - $reflected = $test->getNativeReflection(); + foreach ($tests as $reflected) { $classname = $reflected->getName(); $object = $reflected->newInstanceWithoutConstructor(); foreach ($reflected->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { From 5db2e8e08b08436014b0d26fd4a523be59136052 Mon Sep 17 00:00:00 2001 From: Anna Damm Date: Thu, 13 Jun 2024 18:49:12 +0200 Subject: [PATCH 4/9] add test case for test parser --- .../test/TestParser/ParsesTestsTest.php | 52 +++++++++++++++++++ .../integration/test/TestParser/TestClass.php | 31 +++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/integration/test/TestParser/ParsesTestsTest.php create mode 100644 tests/integration/test/TestParser/TestClass.php diff --git a/tests/integration/test/TestParser/ParsesTestsTest.php b/tests/integration/test/TestParser/ParsesTestsTest.php new file mode 100644 index 00000000..ea8018f6 --- /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)()); + } +} \ No newline at end of file diff --git a/tests/integration/test/TestParser/TestClass.php b/tests/integration/test/TestParser/TestClass.php new file mode 100644 index 00000000..14bfe799 --- /dev/null +++ b/tests/integration/test/TestParser/TestClass.php @@ -0,0 +1,31 @@ + */ + 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')); + } +} \ No newline at end of file From c2472398cc219184d44df2838984875b928189a4 Mon Sep 17 00:00:00 2001 From: Anna Damm Date: Thu, 13 Jun 2024 18:55:48 +0200 Subject: [PATCH 5/9] [dynamic-ruleset-collection] fix problems from static code analysis --- src/Test/TestExtractor.php | 2 +- src/Test/TestExtractorInterface.php | 2 +- src/Test/TestParser.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Test/TestExtractor.php b/src/Test/TestExtractor.php index b58a8689..ae7aadce 100644 --- a/src/Test/TestExtractor.php +++ b/src/Test/TestExtractor.php @@ -37,7 +37,7 @@ public function __invoke(): iterable } /** - * @param class-string $test + * @param class-string $test */ private function reflectTest(string $test): ?\ReflectionClass { diff --git a/src/Test/TestExtractorInterface.php b/src/Test/TestExtractorInterface.php index 493dd705..84136d3b 100644 --- a/src/Test/TestExtractorInterface.php +++ b/src/Test/TestExtractorInterface.php @@ -7,7 +7,7 @@ interface TestExtractorInterface { /** - * @return iterable<\ReflectionClass> + * @return iterable<\ReflectionClass> */ public function __invoke(): iterable; } \ No newline at end of file diff --git a/src/Test/TestParser.php b/src/Test/TestParser.php index 9968a915..401b0583 100644 --- a/src/Test/TestParser.php +++ b/src/Test/TestParser.php @@ -43,7 +43,7 @@ private function parse(): array $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()}(); From aa94c17ad61847da555ac57de9796b952d21700f Mon Sep 17 00:00:00 2001 From: Anna Damm Date: Thu, 13 Jun 2024 18:57:03 +0200 Subject: [PATCH 6/9] [dynamic-ruleset-collection] fix problems from static code analysis --- src/Test/TestExtractor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Test/TestExtractor.php b/src/Test/TestExtractor.php index ae7aadce..aaafa5a6 100644 --- a/src/Test/TestExtractor.php +++ b/src/Test/TestExtractor.php @@ -38,6 +38,7 @@ public function __invoke(): iterable /** * @param class-string $test + * @return \ReflectionClass */ private function reflectTest(string $test): ?\ReflectionClass { From 3458ccb896f110a884ba4daf89a7d9111104cec3 Mon Sep 17 00:00:00 2001 From: Anna Damm Date: Thu, 13 Jun 2024 18:59:58 +0200 Subject: [PATCH 7/9] [dynamic-ruleset-collection] fix code style --- src/Test/RuleValidator.php | 3 --- src/Test/RuleValidatorInterface.php | 6 ++--- src/Test/TestExtractor.php | 5 +--- src/Test/TestExtractorInterface.php | 6 ++--- .../test/TestParser/ParsesTestsTest.php | 24 +++++++++---------- .../integration/test/TestParser/TestClass.php | 13 +++++----- 6 files changed, 24 insertions(+), 33 deletions(-) diff --git a/src/Test/RuleValidator.php b/src/Test/RuleValidator.php index 51f0e64e..a2214bf0 100644 --- a/src/Test/RuleValidator.php +++ b/src/Test/RuleValidator.php @@ -6,9 +6,6 @@ final class RuleValidator implements RuleValidatorInterface { - /** - * @inheritDoc - */ public function validate(Rule $rule): void { if ($rule->getSubjects() === []) { diff --git a/src/Test/RuleValidatorInterface.php b/src/Test/RuleValidatorInterface.php index d1969135..cf80e09e 100644 --- a/src/Test/RuleValidatorInterface.php +++ b/src/Test/RuleValidatorInterface.php @@ -1,6 +1,4 @@ -reflectionProvider = $reflectionProvider; } - /** - * @inheritDoc - */ public function __invoke(): iterable { foreach ($this->container->getServicesByTag(self::TEST_TAG) as $test) { @@ -37,7 +34,7 @@ public function __invoke(): iterable } /** - * @param class-string $test + * @param class-string $test * @return \ReflectionClass */ private function reflectTest(string $test): ?\ReflectionClass diff --git a/src/Test/TestExtractorInterface.php b/src/Test/TestExtractorInterface.php index 84136d3b..98883b4c 100644 --- a/src/Test/TestExtractorInterface.php +++ b/src/Test/TestExtractorInterface.php @@ -1,6 +1,4 @@ -> */ public function __invoke(): iterable; -} \ No newline at end of file +} diff --git a/tests/integration/test/TestParser/ParsesTestsTest.php b/tests/integration/test/TestParser/ParsesTestsTest.php index ea8018f6..3018321c 100644 --- a/tests/integration/test/TestParser/ParsesTestsTest.php +++ b/tests/integration/test/TestParser/ParsesTestsTest.php @@ -1,6 +1,4 @@ -classes(Selector::classname('1'))(); - $rule1->ruleName = TestClass::class . ':test_rules_from_iterator' . ':one'; + $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'; + $rule2->ruleName = TestClass::class.':test_rules_from_iterator:two'; $rule3 = PHPat::rule()->classes(Selector::classname('3'))(); - $rule3->ruleName = TestClass::class . ':test_rule'; + $rule3->ruleName = TestClass::class.':test_rule'; $rule4 = PHPat::rule()->classes(Selector::classname('4'))(); - $rule4->ruleName = TestClass::class . ':test_rule_from_attribute'; + $rule4->ruleName = TestClass::class.':test_rule_from_attribute'; self::assertEquals([ $rule1, @@ -49,4 +49,4 @@ public function validate(Rule $rule): void $rule4, ], ($testParser)()); } -} \ No newline at end of file +} diff --git a/tests/integration/test/TestParser/TestClass.php b/tests/integration/test/TestParser/TestClass.php index 14bfe799..322e9b1f 100644 --- a/tests/integration/test/TestParser/TestClass.php +++ b/tests/integration/test/TestParser/TestClass.php @@ -1,20 +1,21 @@ - */ + /** + * @return iterable + */ public function test_rules_from_iterator(): iterable { yield 'one' => PHPat::rule()->classes(Selector::classname('1')); + yield 'two' => PHPat::rule()->classes(Selector::classname('2')); } @@ -28,4 +29,4 @@ public function test_rule_from_attribute(): Rule { return PHPat::rule()->classes(Selector::classname('4')); } -} \ No newline at end of file +} From ab6ccd05e92926dd124508160e6984995eca9aa3 Mon Sep 17 00:00:00 2001 From: Anna Damm Date: Thu, 13 Jun 2024 19:05:51 +0200 Subject: [PATCH 8/9] [dynamic-ruleset-collection] fix psalm errors --- src/Test/TestExtractor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Test/TestExtractor.php b/src/Test/TestExtractor.php index 930dcb02..c21af868 100644 --- a/src/Test/TestExtractor.php +++ b/src/Test/TestExtractor.php @@ -34,8 +34,8 @@ public function __invoke(): iterable } /** - * @param class-string $test - * @return \ReflectionClass + * @param class-string $test + * @return null|\ReflectionClass|\ReflectionEnum */ private function reflectTest(string $test): ?\ReflectionClass { From cfa13f65327079260b039a7e9a62d8986f21cd41 Mon Sep 17 00:00:00 2001 From: Anna Damm Date: Thu, 13 Jun 2024 19:11:11 +0200 Subject: [PATCH 9/9] [dynamic-ruleset-collection] try to fix psalm errors --- ci/psalm.xml | 6 ++++++ src/Test/TestExtractor.php | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) 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/src/Test/TestExtractor.php b/src/Test/TestExtractor.php index c21af868..f617a6de 100644 --- a/src/Test/TestExtractor.php +++ b/src/Test/TestExtractor.php @@ -34,8 +34,8 @@ public function __invoke(): iterable } /** - * @param class-string $test - * @return null|\ReflectionClass|\ReflectionEnum + * @param class-string $test + * @return null|\ReflectionClass */ private function reflectTest(string $test): ?\ReflectionClass {