From f5be2f6d7e5318dc5bbbfc4abf939cf3e4390e50 Mon Sep 17 00:00:00 2001 From: Can Vural Date: Sun, 6 Jul 2025 09:27:15 +0200 Subject: [PATCH 1/2] feat: make AddCoversClassAttributeRector configurable --- .../adds_covers_class_functional_test.php.inc | 22 ++++ ...adds_covers_class_integration_test.php.inc | 22 ++++ .../config/configured_rule.php | 5 +- .../Class_/AddCoversClassAttributeRector.php | 123 +++++++++++------- src/ValueObject/TestClassSuffixesConfig.php | 24 ++++ 5 files changed, 151 insertions(+), 45 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/Fixture/adds_covers_class_functional_test.php.inc create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/Fixture/adds_covers_class_integration_test.php.inc create mode 100644 src/ValueObject/TestClassSuffixesConfig.php diff --git a/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/Fixture/adds_covers_class_functional_test.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/Fixture/adds_covers_class_functional_test.php.inc new file mode 100644 index 00000000..e5a7b8ee --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/Fixture/adds_covers_class_functional_test.php.inc @@ -0,0 +1,22 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/Fixture/adds_covers_class_integration_test.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/Fixture/adds_covers_class_integration_test.php.inc new file mode 100644 index 00000000..f158fb0a --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/Fixture/adds_covers_class_integration_test.php.inc @@ -0,0 +1,22 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/config/configured_rule.php b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/config/configured_rule.php index 3dabd2d4..ea5f3dd3 100644 --- a/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/config/configured_rule.php +++ b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/config/configured_rule.php @@ -4,7 +4,10 @@ use Rector\Config\RectorConfig; use Rector\PHPUnit\CodeQuality\Rector\Class_\AddCoversClassAttributeRector; +use Rector\PHPUnit\ValueObject\TestClassSuffixesConfig; return static function (RectorConfig $rectorConfig): void { - $rectorConfig->rule(AddCoversClassAttributeRector::class); + $rectorConfig->ruleWithConfiguration(AddCoversClassAttributeRector::class, [ + new TestClassSuffixesConfig(['Test', 'TestCase', 'FunctionalTest', 'IntegrationTest']), + ]); }; diff --git a/rules/CodeQuality/Rector/Class_/AddCoversClassAttributeRector.php b/rules/CodeQuality/Rector/Class_/AddCoversClassAttributeRector.php index e3622eeb..6023eee4 100644 --- a/rules/CodeQuality/Rector/Class_/AddCoversClassAttributeRector.php +++ b/rules/CodeQuality/Rector/Class_/AddCoversClassAttributeRector.php @@ -8,12 +8,15 @@ use PhpParser\Node\AttributeGroup; use PhpParser\Node\Stmt\Class_; use PHPStan\Reflection\ReflectionProvider; +use Rector\Contract\Rector\ConfigurableRectorInterface; use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer; use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory; use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; +use Rector\PHPUnit\ValueObject\TestClassSuffixesConfig; use Rector\Rector\AbstractRector; -use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Webmozart\Assert\Assert; use function array_filter; use function array_merge; use function count; @@ -24,48 +27,57 @@ use function strtolower; use function trim; -final class AddCoversClassAttributeRector extends AbstractRector +final class AddCoversClassAttributeRector extends AbstractRector implements ConfigurableRectorInterface { + /** + * @var string[] + */ + private array $testClassSuffixes = ['Test', 'TestCase']; + public function __construct( private readonly ReflectionProvider $reflectionProvider, private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory, private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer, - private readonly TestsNodeAnalyzer $testsNodeAnalyzer, + private readonly TestsNodeAnalyzer $testsNodeAnalyzer ) { } public function getRuleDefinition(): RuleDefinition { - return new RuleDefinition('Adds `#[CoversClass(...)]` attribute to test files guessing source class name.', [ - new CodeSample( - <<<'CODE_SAMPLE' - class SomeService - { - } - - use PHPUnit\Framework\TestCase; - - class SomeServiceTest extends TestCase - { - } - CODE_SAMPLE - , - <<<'CODE_SAMPLE' - class SomeService - { - } - - use PHPUnit\Framework\TestCase; - use PHPUnit\Framework\Attributes\CoversClass; - - #[CoversClass(SomeService::class)] - class SomeServiceTest extends TestCase - { - } - CODE_SAMPLE - , - ), - ]); + return new RuleDefinition( + 'Adds `#[CoversClass(...)]` attribute to test files guessing source class name.', + [ + new ConfiguredCodeSample( + <<<'CODE_SAMPLE' +class SomeService +{ +} + +use PHPUnit\Framework\TestCase; + +class SomeServiceFunctionalTest extends TestCase +{ +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +class SomeService +{ +} + +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\Attributes\CoversClass; + +#[CoversClass(SomeService::class)] +class SomeServiceFunctionalTest extends TestCase +{ +} +CODE_SAMPLE + , + [new TestClassSuffixesConfig(['Test', 'TestCase', 'FunctionalTest', 'IntegrationTest'])] + ), + ] + ); } /** @@ -91,11 +103,13 @@ public function refactor(Node $node): ?Node return null; } - if ($this->phpAttributeAnalyzer->hasPhpAttributes($node, [ - 'PHPUnit\\Framework\\Attributes\\CoversNothing', - 'PHPUnit\\Framework\\Attributes\\CoversClass', - 'PHPUnit\\Framework\\Attributes\\CoversFunction', - ])) { + if ( + $this->phpAttributeAnalyzer->hasPhpAttributes($node, [ + 'PHPUnit\\Framework\\Attributes\\CoversNothing', + 'PHPUnit\\Framework\\Attributes\\CoversClass', + 'PHPUnit\\Framework\\Attributes\\CoversFunction', + ]) + ) { return null; } @@ -113,6 +127,19 @@ public function refactor(Node $node): ?Node return $node; } + /** + * @param mixed[] $configuration + */ + public function configure(array $configuration): void + { + Assert::countBetween($configuration, 0, 1); + + if (isset($configuration[0])) { + Assert::isInstanceOf($configuration[0], TestClassSuffixesConfig::class); + $this->testClassSuffixes = $configuration[0]->getSuffixes(); + } + } + /** * @return string[] */ @@ -120,17 +147,25 @@ private function resolveSourceClassNames(string $className): array { $classNameParts = explode('\\', $className); $partCount = count($classNameParts); - $classNameParts[$partCount - 1] = preg_replace(['#TestCase$#', '#Test$#'], '', $classNameParts[$partCount - 1]); + + // Sort suffixes by length (longest first) to ensure more specific patterns match first + $sortedSuffixes = $this->testClassSuffixes; + usort($sortedSuffixes, static fn (string $a, string $b): int => strlen($b) <=> strlen($a)); + + $patterns = []; + foreach ($sortedSuffixes as $sortedSuffix) { + $patterns[] = '#' . preg_quote($sortedSuffix, '#') . '$#'; + } + + $classNameParts[$partCount - 1] = preg_replace($patterns, '', $classNameParts[$partCount - 1]); $possibleTestClassNames = [implode('\\', $classNameParts)]; $partsWithoutTests = array_filter( $classNameParts, - static fn (string|null $part): bool => $part === null ? false : ! in_array( - strtolower($part), - ['test', 'tests'], - true - ), + static fn (string|null $part): bool => $part === null + ? false + : ! in_array(strtolower($part), ['test', 'tests'], true) ); $possibleTestClassNames[] = implode('\\', $partsWithoutTests); diff --git a/src/ValueObject/TestClassSuffixesConfig.php b/src/ValueObject/TestClassSuffixesConfig.php new file mode 100644 index 00000000..d080f7a2 --- /dev/null +++ b/src/ValueObject/TestClassSuffixesConfig.php @@ -0,0 +1,24 @@ +suffixes; + } +} From f19e657af76f23b197b58fce20fb96b10a7b08a0 Mon Sep 17 00:00:00 2001 From: Can Vural Date: Sun, 6 Jul 2025 09:50:32 +0200 Subject: [PATCH 2/2] add new fixture dir and test case for the configured rule --- ...versClassAttributeRectorConfiguredTest.php | 28 +++++++++++++++++++ .../AddCoversClassAttributeRectorTest.php | 2 +- .../adds_covers_class_functional_test.php.inc | 6 ++-- ...adds_covers_class_integration_test.php.inc | 6 ++-- .../config/rule.php | 10 +++++++ 5 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/AddCoversClassAttributeRectorConfiguredTest.php rename rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/{Fixture => ConfiguredFixture}/adds_covers_class_functional_test.php.inc (78%) rename rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/{Fixture => ConfiguredFixture}/adds_covers_class_integration_test.php.inc (78%) create mode 100644 rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/config/rule.php diff --git a/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/AddCoversClassAttributeRectorConfiguredTest.php b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/AddCoversClassAttributeRectorConfiguredTest.php new file mode 100644 index 00000000..dc90ce10 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/AddCoversClassAttributeRectorConfiguredTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/ConfiguredFixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/AddCoversClassAttributeRectorTest.php b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/AddCoversClassAttributeRectorTest.php index 5846aaa9..082a69b6 100644 --- a/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/AddCoversClassAttributeRectorTest.php +++ b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/AddCoversClassAttributeRectorTest.php @@ -23,6 +23,6 @@ public static function provideData(): Iterator public function provideConfigFilePath(): string { - return __DIR__ . '/config/configured_rule.php'; + return __DIR__ . '/config/rule.php'; } } diff --git a/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/Fixture/adds_covers_class_functional_test.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/ConfiguredFixture/adds_covers_class_functional_test.php.inc similarity index 78% rename from rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/Fixture/adds_covers_class_functional_test.php.inc rename to rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/ConfiguredFixture/adds_covers_class_functional_test.php.inc index e5a7b8ee..c02acaf2 100644 --- a/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/Fixture/adds_covers_class_functional_test.php.inc +++ b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/ConfiguredFixture/adds_covers_class_functional_test.php.inc @@ -1,6 +1,6 @@ rule(AddCoversClassAttributeRector::class); +};