Skip to content

Commit a15d46d

Browse files
committed
[AnnotationsToAttributes] Add @requires translation to attributes (#441)
1 parent c0eaa38 commit a15d46d

File tree

4 files changed

+348
-0
lines changed

4 files changed

+348
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
use PHPUnit\Framework\TestCase;
4+
5+
/**
6+
* @requires PHP > 8.4
7+
* @requires PHPUnit >= 10
8+
* @requires OS Windows
9+
* @requires OSFAMILY Darwin
10+
* @requires function someFunction
11+
* @requires function \some\className::someMethod
12+
* @requires extension mysqli
13+
* @requires extension mysqli >= 8.3.0
14+
* @requires setting date.timezone Europe/Berlin
15+
*/
16+
class BarController extends TestCase
17+
{
18+
/**
19+
* @requires PHP > 8.4
20+
* @requires PHPUnit >= 10
21+
* @requires OS Windows
22+
* @requires OSFAMILY Darwin
23+
* @requires function someFunction
24+
* @requires function \some\className::someMethod
25+
* @requires extension mysqli
26+
* @requires extension mysqli >= 8.3.0
27+
* @requires setting date.timezone Europe/Berlin
28+
*/
29+
public function testWithRequires()
30+
{
31+
}
32+
}
33+
34+
?>
35+
-----
36+
<?php
37+
38+
use PHPUnit\Framework\TestCase;
39+
40+
#[\PHPUnit\Framework\Attributes\RequiresPhp('> 8.4')]
41+
#[\PHPUnit\Framework\Attributes\RequiresPhpunit('>= 10')]
42+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystem('Windows')]
43+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily('Darwin')]
44+
#[\PHPUnit\Framework\Attributes\RequiresFunction('someFunction')]
45+
#[\PHPUnit\Framework\Attributes\RequiresMethod(\some\className::class, 'someMethod')]
46+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli')]
47+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli', '>= 8.3.0')]
48+
#[\PHPUnit\Framework\Attributes\RequiresSetting('date.timezone', 'Europe/Berlin')]
49+
class BarController extends TestCase
50+
{
51+
#[\PHPUnit\Framework\Attributes\RequiresPhp('> 8.4')]
52+
#[\PHPUnit\Framework\Attributes\RequiresPhpunit('>= 10')]
53+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystem('Windows')]
54+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily('Darwin')]
55+
#[\PHPUnit\Framework\Attributes\RequiresFunction('someFunction')]
56+
#[\PHPUnit\Framework\Attributes\RequiresMethod(\some\className::class, 'someMethod')]
57+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli')]
58+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli', '>= 8.3.0')]
59+
#[\PHPUnit\Framework\Attributes\RequiresSetting('date.timezone', 'Europe/Berlin')]
60+
public function testWithRequires()
61+
{
62+
}
63+
}
64+
65+
?>
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\AnnotationsToAttributes\Rector\Class_\RequiresAnnotationWithValueToAttributeRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class RequiresAnnotationWithValueToAttributeRectorTest 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__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
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\AnnotationsToAttributes\Rector\Class_\RequiresAnnotationWithValueToAttributeRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(RequiresAnnotationWithValueToAttributeRector::class);
10+
};
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\AttributeGroup;
9+
use PhpParser\Node\Stmt\Class_;
10+
use PhpParser\Node\Stmt\ClassMethod;
11+
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
12+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
13+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
14+
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover;
15+
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
16+
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
17+
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
18+
use Rector\Rector\AbstractRector;
19+
use Rector\ValueObject\PhpVersionFeature;
20+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
21+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
22+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
23+
24+
/**
25+
* @see \Rector\PHPUnit\Tests\AnnotationsToAttributes\Rector\Class_\RequiresAnnotationWithValueToAttributeRector\RequiresAnnotationWithValueToAttributeRectorTest
26+
*/
27+
final class RequiresAnnotationWithValueToAttributeRector extends AbstractRector implements MinPhpVersionInterface
28+
{
29+
public function __construct(
30+
private readonly PhpDocTagRemover $phpDocTagRemover,
31+
private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
32+
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
33+
private readonly DocBlockUpdater $docBlockUpdater,
34+
private readonly PhpDocInfoFactory $phpDocInfoFactory,
35+
) {
36+
}
37+
38+
public function getRuleDefinition(): RuleDefinition
39+
{
40+
return new RuleDefinition('Change Requires annotations with values to attributes', [
41+
new CodeSample(
42+
<<<'CODE_SAMPLE'
43+
use PHPUnit\Framework\TestCase;
44+
45+
/**
46+
* @requires PHP > 8.4
47+
* @requires PHPUnit >= 10
48+
* @requires OS Windows
49+
* @requires OSFAMILY Darwin
50+
* @requires function someFunction
51+
* @requires function \some\className::someMethod
52+
* @requires extension mysqli
53+
* @requires extension mysqli >= 8.3.0
54+
* @requires setting date.timezone Europe/Berlin
55+
*/
56+
57+
final class SomeTest extends TestCase
58+
{
59+
/**
60+
* @requires PHP > 8.4
61+
* @requires PHPUnit >= 10
62+
* @requires OS Windows
63+
* @requires OSFAMILY Darwin
64+
* @requires function someFunction
65+
* @requires function \some\className::someMethod
66+
* @requires extension mysqli
67+
* @requires extension mysqli >= 8.3.0
68+
* @requires setting date.timezone Europe/Berlin
69+
*/
70+
public function test()
71+
{
72+
}
73+
}
74+
CODE_SAMPLE
75+
76+
,
77+
<<<'CODE_SAMPLE'
78+
use PHPUnit\Framework\TestCase;
79+
80+
#[\PHPUnit\Framework\Attributes\RequiresPhp('> 8.4')]
81+
#[\PHPUnit\Framework\Attributes\RequiresPhpunit('>= 10')]
82+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystem('Windows')]
83+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily('Darwin')]
84+
#[\PHPUnit\Framework\Attributes\RequiresFunction('someFunction')]
85+
#[\PHPUnit\Framework\Attributes\RequiresMethod(\some\className::class, 'someMethod')]
86+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli')]
87+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli', '>= 8.3.0')]
88+
#[\PHPUnit\Framework\Attributes\RequiresSetting('date.timezone', 'Europe/Berlin')]
89+
final class SomeTest extends TestCase
90+
{
91+
92+
#[\PHPUnit\Framework\Attributes\RequiresPhp('> 8.4')]
93+
#[\PHPUnit\Framework\Attributes\RequiresPhpunit('>= 10')]
94+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystem('Windows')]
95+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily('Darwin')]
96+
#[\PHPUnit\Framework\Attributes\RequiresFunction('someFunction')]
97+
#[\PHPUnit\Framework\Attributes\RequiresMethod(\some\className::class, 'someMethod')]
98+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli')]
99+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli', '>= 8.3.0')]
100+
#[\PHPUnit\Framework\Attributes\RequiresSetting('date.timezone', 'Europe/Berlin')]
101+
public function test()
102+
{
103+
}
104+
}
105+
CODE_SAMPLE
106+
),
107+
]);
108+
}
109+
110+
/**
111+
* @return array<class-string<Node>>
112+
*/
113+
public function getNodeTypes(): array
114+
{
115+
return [Class_::class, ClassMethod::class];
116+
}
117+
118+
public function provideMinPhpVersion(): int
119+
{
120+
return PhpVersionFeature::ATTRIBUTES;
121+
}
122+
123+
/**
124+
* @param Class_|ClassMethod $node
125+
*/
126+
public function refactor(Node $node): ?Node
127+
{
128+
if (! $this->testsNodeAnalyzer->isInTestClass($node)) {
129+
return null;
130+
}
131+
132+
if ($node instanceof Class_) {
133+
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($node);
134+
if ($phpDocInfo instanceof PhpDocInfo) {
135+
$requiresAttributeGroups = $this->handleRequires($phpDocInfo);
136+
if (! ($requiresAttributeGroups === [])) {
137+
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node);
138+
$node->attrGroups = array_merge($node->attrGroups, $requiresAttributeGroups);
139+
$this->removeMethodRequiresAnnotations($phpDocInfo);
140+
}
141+
}
142+
143+
foreach ($node->getMethods() as $classNode) {
144+
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($classNode);
145+
if ($phpDocInfo instanceof PhpDocInfo) {
146+
$requiresAttributeGroups = $this->handleRequires($phpDocInfo);
147+
if (! ($requiresAttributeGroups === [])) {
148+
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classNode);
149+
$classNode->attrGroups = array_merge($classNode->attrGroups, $requiresAttributeGroups);
150+
$this->removeMethodRequiresAnnotations($phpDocInfo);
151+
}
152+
}
153+
}
154+
155+
156+
return $node;
157+
}
158+
159+
return null;
160+
}
161+
162+
private function createAttributeGroup(string $annotationValue): ?AttributeGroup
163+
{
164+
$annotationValues = explode(' ', $annotationValue, 2);
165+
$type = array_shift($annotationValues);
166+
$attributeValue = array_shift($annotationValues);
167+
switch ($type) {
168+
case 'PHP':
169+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresPhp';
170+
$attributeValue = [$attributeValue];
171+
break;
172+
case 'PHPUnit':
173+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresPhpunit';
174+
$attributeValue = [$attributeValue];
175+
break;
176+
case 'OS':
177+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresOperatingSystem';
178+
$attributeValue = [$attributeValue];
179+
break;
180+
case 'OSFAMILY':
181+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily';
182+
$attributeValue = [$attributeValue];
183+
break;
184+
case 'function':
185+
if (str_contains($attributeValue, '::')) {
186+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresMethod';
187+
$attributeValue = explode('::', $attributeValue);
188+
$attributeValue[0] .= '::class';
189+
} else {
190+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresFunction';
191+
$attributeValue = [$attributeValue];
192+
}
193+
break;
194+
case 'extension':
195+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresExtension';
196+
$attributeValue = explode(' ', $attributeValue, 2);
197+
break;
198+
case 'setting':
199+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresSetting';
200+
$attributeValue = explode(' ', $attributeValue, 2);
201+
break;
202+
default:
203+
return null;
204+
}
205+
206+
return $this->phpAttributeGroupFactory->createFromClassWithItems($attributeClass, [...$attributeValue]);
207+
}
208+
209+
/**
210+
* @return array<string, AttributeGroup|null>
211+
*/
212+
private function handleRequires(PhpDocInfo $phpDocInfo): array
213+
{
214+
$attributeGroups = [];
215+
$desiredTagValueNodes = $phpDocInfo->getTagsByName('requires');
216+
foreach ($desiredTagValueNodes as $desiredTagValueNode) {
217+
if (! $desiredTagValueNode->value instanceof GenericTagValueNode) {
218+
continue;
219+
}
220+
221+
$requires = $desiredTagValueNode->value->value;
222+
$attributeGroups[$requires] = $this->createAttributeGroup($requires);
223+
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $desiredTagValueNode);
224+
}
225+
226+
return $attributeGroups;
227+
}
228+
229+
private function removeMethodRequiresAnnotations(PhpDocInfo $phpDocInfo): bool
230+
{
231+
$hasChanged = false;
232+
233+
$desiredTagValueNodes = $phpDocInfo->getTagsByName('requires');
234+
foreach ($desiredTagValueNodes as $desiredTagValueNode) {
235+
if (! $desiredTagValueNode->value instanceof GenericTagValueNode) {
236+
continue;
237+
}
238+
239+
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $desiredTagValueNode);
240+
$hasChanged = true;
241+
}
242+
243+
return $hasChanged;
244+
}
245+
}

0 commit comments

Comments
 (0)