Skip to content

Commit 8986bbe

Browse files
committed
[AnnotationsToAttributes] Add @requires translation to attributes (#441)
1 parent 4e82fcd commit 8986bbe

File tree

4 files changed

+350
-0
lines changed

4 files changed

+350
-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: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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+
$hasChanged = false;
133+
134+
135+
if ($node instanceof Class_) {
136+
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($node);
137+
if ($phpDocInfo instanceof PhpDocInfo) {
138+
$requiresAttributeGroups = $this->handleRequires($phpDocInfo);
139+
if (! ($requiresAttributeGroups === [])) {
140+
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node);
141+
$node->attrGroups = array_merge($node->attrGroups, $requiresAttributeGroups);
142+
$this->removeMethodRequiresAnnotations($phpDocInfo);
143+
$hasChanged = true;
144+
}
145+
}
146+
147+
foreach ($node->getMethods() as $classNode) {
148+
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($classNode);
149+
if ($phpDocInfo instanceof PhpDocInfo) {
150+
$requiresAttributeGroups = $this->handleRequires($phpDocInfo);
151+
if (! ($requiresAttributeGroups === [])) {
152+
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classNode);
153+
$classNode->attrGroups = array_merge($classNode->attrGroups, $requiresAttributeGroups);
154+
$this->removeMethodRequiresAnnotations($phpDocInfo);
155+
$hasChanged = true;
156+
}
157+
}
158+
}
159+
}
160+
161+
return $hasChanged ? $node : null;
162+
}
163+
164+
private function createAttributeGroup(string $annotationValue): ?AttributeGroup
165+
{
166+
$annotationValues = explode(' ', $annotationValue, 2);
167+
$type = array_shift($annotationValues);
168+
$attributeValue = array_shift($annotationValues);
169+
switch ($type) {
170+
case 'PHP':
171+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresPhp';
172+
$attributeValue = [$attributeValue];
173+
break;
174+
case 'PHPUnit':
175+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresPhpunit';
176+
$attributeValue = [$attributeValue];
177+
break;
178+
case 'OS':
179+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresOperatingSystem';
180+
$attributeValue = [$attributeValue];
181+
break;
182+
case 'OSFAMILY':
183+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily';
184+
$attributeValue = [$attributeValue];
185+
break;
186+
case 'function':
187+
if (str_contains($attributeValue, '::')) {
188+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresMethod';
189+
$attributeValue = explode('::', $attributeValue);
190+
$attributeValue[0] .= '::class';
191+
} else {
192+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresFunction';
193+
$attributeValue = [$attributeValue];
194+
}
195+
break;
196+
case 'extension':
197+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresExtension';
198+
$attributeValue = explode(' ', $attributeValue, 2);
199+
break;
200+
case 'setting':
201+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresSetting';
202+
$attributeValue = explode(' ', $attributeValue, 2);
203+
break;
204+
default:
205+
return null;
206+
}
207+
208+
return $this->phpAttributeGroupFactory->createFromClassWithItems($attributeClass, [...$attributeValue]);
209+
}
210+
211+
/**
212+
* @return array<string, AttributeGroup|null>
213+
*/
214+
private function handleRequires(PhpDocInfo $phpDocInfo): array
215+
{
216+
$attributeGroups = [];
217+
$desiredTagValueNodes = $phpDocInfo->getTagsByName('requires');
218+
foreach ($desiredTagValueNodes as $desiredTagValueNode) {
219+
if (! $desiredTagValueNode->value instanceof GenericTagValueNode) {
220+
continue;
221+
}
222+
223+
$requires = $desiredTagValueNode->value->value;
224+
$attributeGroups[$requires] = $this->createAttributeGroup($requires);
225+
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $desiredTagValueNode);
226+
}
227+
228+
return $attributeGroups;
229+
}
230+
231+
private function removeMethodRequiresAnnotations(PhpDocInfo $phpDocInfo): bool
232+
{
233+
$hasChanged = false;
234+
235+
$desiredTagValueNodes = $phpDocInfo->getTagsByName('requires');
236+
foreach ($desiredTagValueNodes as $desiredTagValueNode) {
237+
if (! $desiredTagValueNode->value instanceof GenericTagValueNode) {
238+
continue;
239+
}
240+
241+
$this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $desiredTagValueNode);
242+
$hasChanged = true;
243+
}
244+
245+
return $hasChanged;
246+
}
247+
}

0 commit comments

Comments
 (0)