Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic ruleset collection #278

Merged
merged 9 commits into from
Jun 22, 2024
6 changes: 6 additions & 0 deletions ci/psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@
<UndefinedClass>
<errorLevel type="suppress"><file name="../src/Test/TestParser.php" /></errorLevel>
</UndefinedClass>
<InvalidReturnType>
<errorLevel type="suppress"><file name="../src/Test/TestExtractor.php" /></errorLevel>
</InvalidReturnType>
<InvalidReturnStatement>
<errorLevel type="suppress"><file name="../src/Test/TestExtractor.php" /></errorLevel>
</InvalidReturnStatement>

<PropertyNotSetInConstructor>
<errorLevel type="suppress"><directory name="../tests/unit/rules/" /></errorLevel>
Expand Down
32 changes: 32 additions & 0 deletions docs/documentation/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Rule>
*/
public function test_domain_independence(): iterable
{
foreach(self::DOMAINS as $domain) {
yield $domain => PHPat::rule()
->classes(Selector::inNamespace($domain))
->canOnlyDependOn()
->classes(Selector::inNamespace($domain));
}
}
}
```
5 changes: 1 addition & 4 deletions src/Test/RuleValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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() === []) {
Expand Down
11 changes: 11 additions & 0 deletions src/Test/RuleValidatorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace PHPat\Test;

interface RuleValidatorInterface
{
/**
* @throws \Exception
*/
public function validate(Rule $rule): void;
}
13 changes: 5 additions & 8 deletions src/Test/TestExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

use PHPat\ShouldNotHappenException;
use PHPStan\DependencyInjection\Container;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ReflectionProvider;

final class TestExtractor
final class TestExtractor implements TestExtractorInterface
{
private const TEST_TAG = 'phpat.test';

Expand All @@ -20,9 +19,6 @@ public function __construct(Container $container, ReflectionProvider $reflection
$this->reflectionProvider = $reflectionProvider;
}

/**
* @return iterable<ClassReflection>
*/
public function __invoke(): iterable
{
foreach ($this->container->getServicesByTag(self::TEST_TAG) as $test) {
Expand All @@ -38,14 +34,15 @@ public function __invoke(): iterable
}

/**
* @param class-string $test
* @param class-string $test
* @return null|\ReflectionClass<object>
*/
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();
}
}
11 changes: 11 additions & 0 deletions src/Test/TestExtractorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace PHPat\Test;

interface TestExtractorInterface
{
/**
* @return iterable<\ReflectionClass<object>>
*/
public function __invoke(): iterable;
}
20 changes: 12 additions & 8 deletions src/Test/TestParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ class TestParser
{
/** @var array<Rule> */
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;
Expand All @@ -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;
}
}
}
}
Expand Down
52 changes: 52 additions & 0 deletions tests/integration/test/TestParser/ParsesTestsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php declare(strict_types=1);

namespace Tests\PHPat\integration\test\TestParser;

use PHPat\Selector\Selector;
use PHPat\Test\PHPat;
use PHPat\Test\Rule;
use PHPat\Test\RuleValidatorInterface;
use PHPat\Test\TestExtractorInterface;
use PHPat\Test\TestParser;
use PHPUnit\Framework\TestCase;

/**
* @internal
* @coversNothing
*/
final class ParsesTestsTest extends TestCase
{
public function testClassCollectsMultipleRulesFromFunction(): void
{
$testParser = new TestParser(
new class() implements TestExtractorInterface {
public function __invoke(): iterable
{
yield new \ReflectionClass(TestClass::class);
}
},
new class() implements RuleValidatorInterface {
public function validate(Rule $rule): void {}
},
);

$rule1 = PHPat::rule()->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)());
}
}
32 changes: 32 additions & 0 deletions tests/integration/test/TestParser/TestClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types=1);

namespace Tests\PHPat\integration\test\TestParser;

use PHPat\Selector\Selector;
use PHPat\Test\Attributes\TestRule;
use PHPat\Test\Builder\Rule;
use PHPat\Test\PHPat;

final class TestClass
{
/**
* @return iterable<Rule>
*/
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'));
}
}
Loading