Skip to content

Commit c0e6983

Browse files
authored
feat: make AddCoversClassAttributeRector configurable (#502)
* feat: make AddCoversClassAttributeRector configurable * add new fixture dir and test case for the configured rule
1 parent 7963cd9 commit c0e6983

File tree

8 files changed

+190
-46
lines changed

8 files changed

+190
-46
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\Class_\AddCoversClassAttributeRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class AddCoversClassAttributeRectorConfiguredTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/ConfiguredFixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}

rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/AddCoversClassAttributeRectorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ public static function provideData(): Iterator
2323

2424
public function provideConfigFilePath(): string
2525
{
26-
return __DIR__ . '/config/configured_rule.php';
26+
return __DIR__ . '/config/rule.php';
2727
}
2828
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Utils\Rector\Tests\Rector\AddCoversClassAttributeRector\ConfiguredFixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
class SomeServiceFunctionalTest extends TestCase {}
8+
class SomeService {}
9+
10+
?>
11+
-----
12+
<?php
13+
14+
namespace Utils\Rector\Tests\Rector\AddCoversClassAttributeRector\ConfiguredFixture;
15+
16+
use PHPUnit\Framework\TestCase;
17+
18+
#[\PHPUnit\Framework\Attributes\CoversClass(\Utils\Rector\Tests\Rector\AddCoversClassAttributeRector\ConfiguredFixture\SomeService::class)]
19+
class SomeServiceFunctionalTest extends TestCase {}
20+
class SomeService {}
21+
22+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Utils\Rector\Tests\Rector\AddCoversClassAttributeRector\ConfiguredFixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
7+
class UserRepositoryIntegrationTest extends TestCase {}
8+
class UserRepository {}
9+
10+
?>
11+
-----
12+
<?php
13+
14+
namespace Utils\Rector\Tests\Rector\AddCoversClassAttributeRector\ConfiguredFixture;
15+
16+
use PHPUnit\Framework\TestCase;
17+
18+
#[\PHPUnit\Framework\Attributes\CoversClass(\Utils\Rector\Tests\Rector\AddCoversClassAttributeRector\ConfiguredFixture\UserRepository::class)]
19+
class UserRepositoryIntegrationTest extends TestCase {}
20+
class UserRepository {}
21+
22+
?>

rules-tests/CodeQuality/Rector/Class_/AddCoversClassAttributeRector/config/configured_rule.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
use Rector\Config\RectorConfig;
66
use Rector\PHPUnit\CodeQuality\Rector\Class_\AddCoversClassAttributeRector;
7+
use Rector\PHPUnit\ValueObject\TestClassSuffixesConfig;
78

89
return static function (RectorConfig $rectorConfig): void {
9-
$rectorConfig->rule(AddCoversClassAttributeRector::class);
10+
$rectorConfig->ruleWithConfiguration(AddCoversClassAttributeRector::class, [
11+
new TestClassSuffixesConfig(['Test', 'TestCase', 'FunctionalTest', 'IntegrationTest']),
12+
]);
1013
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\PHPUnit\CodeQuality\Rector\Class_\AddCoversClassAttributeRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(AddCoversClassAttributeRector::class);
10+
};

rules/CodeQuality/Rector/Class_/AddCoversClassAttributeRector.php

Lines changed: 79 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
use PhpParser\Node\AttributeGroup;
99
use PhpParser\Node\Stmt\Class_;
1010
use PHPStan\Reflection\ReflectionProvider;
11+
use Rector\Contract\Rector\ConfigurableRectorInterface;
1112
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
1213
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
1314
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
15+
use Rector\PHPUnit\ValueObject\TestClassSuffixesConfig;
1416
use Rector\Rector\AbstractRector;
15-
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
17+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
1618
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
19+
use Webmozart\Assert\Assert;
1720
use function array_filter;
1821
use function array_merge;
1922
use function count;
@@ -24,48 +27,57 @@
2427
use function strtolower;
2528
use function trim;
2629

27-
final class AddCoversClassAttributeRector extends AbstractRector
30+
final class AddCoversClassAttributeRector extends AbstractRector implements ConfigurableRectorInterface
2831
{
32+
/**
33+
* @var string[]
34+
*/
35+
private array $testClassSuffixes = ['Test', 'TestCase'];
36+
2937
public function __construct(
3038
private readonly ReflectionProvider $reflectionProvider,
3139
private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
3240
private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer,
33-
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
41+
private readonly TestsNodeAnalyzer $testsNodeAnalyzer
3442
) {
3543
}
3644

3745
public function getRuleDefinition(): RuleDefinition
3846
{
39-
return new RuleDefinition('Adds `#[CoversClass(...)]` attribute to test files guessing source class name.', [
40-
new CodeSample(
41-
<<<'CODE_SAMPLE'
42-
class SomeService
43-
{
44-
}
45-
46-
use PHPUnit\Framework\TestCase;
47-
48-
class SomeServiceTest extends TestCase
49-
{
50-
}
51-
CODE_SAMPLE
52-
,
53-
<<<'CODE_SAMPLE'
54-
class SomeService
55-
{
56-
}
57-
58-
use PHPUnit\Framework\TestCase;
59-
use PHPUnit\Framework\Attributes\CoversClass;
60-
61-
#[CoversClass(SomeService::class)]
62-
class SomeServiceTest extends TestCase
63-
{
64-
}
65-
CODE_SAMPLE
66-
,
67-
),
68-
]);
47+
return new RuleDefinition(
48+
'Adds `#[CoversClass(...)]` attribute to test files guessing source class name.',
49+
[
50+
new ConfiguredCodeSample(
51+
<<<'CODE_SAMPLE'
52+
class SomeService
53+
{
54+
}
55+
56+
use PHPUnit\Framework\TestCase;
57+
58+
class SomeServiceFunctionalTest extends TestCase
59+
{
60+
}
61+
CODE_SAMPLE
62+
,
63+
<<<'CODE_SAMPLE'
64+
class SomeService
65+
{
66+
}
67+
68+
use PHPUnit\Framework\TestCase;
69+
use PHPUnit\Framework\Attributes\CoversClass;
70+
71+
#[CoversClass(SomeService::class)]
72+
class SomeServiceFunctionalTest extends TestCase
73+
{
74+
}
75+
CODE_SAMPLE
76+
,
77+
[new TestClassSuffixesConfig(['Test', 'TestCase', 'FunctionalTest', 'IntegrationTest'])]
78+
),
79+
]
80+
);
6981
}
7082

7183
/**
@@ -91,11 +103,13 @@ public function refactor(Node $node): ?Node
91103
return null;
92104
}
93105

94-
if ($this->phpAttributeAnalyzer->hasPhpAttributes($node, [
95-
'PHPUnit\\Framework\\Attributes\\CoversNothing',
96-
'PHPUnit\\Framework\\Attributes\\CoversClass',
97-
'PHPUnit\\Framework\\Attributes\\CoversFunction',
98-
])) {
106+
if (
107+
$this->phpAttributeAnalyzer->hasPhpAttributes($node, [
108+
'PHPUnit\\Framework\\Attributes\\CoversNothing',
109+
'PHPUnit\\Framework\\Attributes\\CoversClass',
110+
'PHPUnit\\Framework\\Attributes\\CoversFunction',
111+
])
112+
) {
99113
return null;
100114
}
101115

@@ -113,24 +127,45 @@ public function refactor(Node $node): ?Node
113127
return $node;
114128
}
115129

130+
/**
131+
* @param mixed[] $configuration
132+
*/
133+
public function configure(array $configuration): void
134+
{
135+
Assert::countBetween($configuration, 0, 1);
136+
137+
if (isset($configuration[0])) {
138+
Assert::isInstanceOf($configuration[0], TestClassSuffixesConfig::class);
139+
$this->testClassSuffixes = $configuration[0]->getSuffixes();
140+
}
141+
}
142+
116143
/**
117144
* @return string[]
118145
*/
119146
private function resolveSourceClassNames(string $className): array
120147
{
121148
$classNameParts = explode('\\', $className);
122149
$partCount = count($classNameParts);
123-
$classNameParts[$partCount - 1] = preg_replace(['#TestCase$#', '#Test$#'], '', $classNameParts[$partCount - 1]);
150+
151+
// Sort suffixes by length (longest first) to ensure more specific patterns match first
152+
$sortedSuffixes = $this->testClassSuffixes;
153+
usort($sortedSuffixes, static fn (string $a, string $b): int => strlen($b) <=> strlen($a));
154+
155+
$patterns = [];
156+
foreach ($sortedSuffixes as $sortedSuffix) {
157+
$patterns[] = '#' . preg_quote($sortedSuffix, '#') . '$#';
158+
}
159+
160+
$classNameParts[$partCount - 1] = preg_replace($patterns, '', $classNameParts[$partCount - 1]);
124161

125162
$possibleTestClassNames = [implode('\\', $classNameParts)];
126163

127164
$partsWithoutTests = array_filter(
128165
$classNameParts,
129-
static fn (string|null $part): bool => $part === null ? false : ! in_array(
130-
strtolower($part),
131-
['test', 'tests'],
132-
true
133-
),
166+
static fn (string|null $part): bool => $part === null
167+
? false
168+
: ! in_array(strtolower($part), ['test', 'tests'], true)
134169
);
135170

136171
$possibleTestClassNames[] = implode('\\', $partsWithoutTests);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\ValueObject;
6+
7+
final readonly class TestClassSuffixesConfig
8+
{
9+
/**
10+
* @param string[] $suffixes
11+
*/
12+
public function __construct(
13+
private array $suffixes = ['Test', 'TestCase']
14+
) {
15+
}
16+
17+
/**
18+
* @return string[]
19+
*/
20+
public function getSuffixes(): array
21+
{
22+
return $this->suffixes;
23+
}
24+
}

0 commit comments

Comments
 (0)