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/ConfiguredFixture/adds_covers_class_functional_test.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/ConfiguredFixture/adds_covers_class_functional_test.php.inc new file mode 100644 index 00000000..c02acaf2 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/ConfiguredFixture/adds_covers_class_functional_test.php.inc @@ -0,0 +1,22 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/ConfiguredFixture/adds_covers_class_integration_test.php.inc b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/ConfiguredFixture/adds_covers_class_integration_test.php.inc new file mode 100644 index 00000000..a2175eaf --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/ConfiguredFixture/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-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/config/rule.php b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/config/rule.php new file mode 100644 index 00000000..3dabd2d4 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/config/rule.php @@ -0,0 +1,10 @@ +rule(AddCoversClassAttributeRector::class); +}; 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; + } +}