Skip to content

Commit a6d7151

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

File tree

6 files changed

+417
-0
lines changed

6 files changed

+417
-0
lines changed

config/sets/annotations-to-attributes.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Rector\Php80\ValueObject\AnnotationToAttribute;
88
use Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_\AnnotationWithValueToAttributeRector;
99
use Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_\CoversAnnotationWithValueToAttributeRector;
10+
use Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_\RequiresAnnotationWithValueToAttributeRector;
1011
use Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_\TicketAnnotationToAttributeRector;
1112
use Rector\PHPUnit\AnnotationsToAttributes\Rector\ClassMethod\DataProviderAnnotationToAttributeRector;
1213
use Rector\PHPUnit\AnnotationsToAttributes\Rector\ClassMethod\DependsAnnotationWithValueToAttributeRector;
@@ -19,6 +20,7 @@
1920
TestWithAnnotationToAttributeRector::class,
2021
DataProviderAnnotationToAttributeRector::class,
2122
CoversAnnotationWithValueToAttributeRector::class,
23+
RequiresAnnotationWithValueToAttributeRector::class,
2224

2325
/**
2426
* Currently handle:
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+
if ($node instanceof Class_) {
135+
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($node);
136+
if ($phpDocInfo instanceof PhpDocInfo) {
137+
$requiresAttributeGroups = $this->handleRequires($phpDocInfo);
138+
if ($requiresAttributeGroups !== []) {
139+
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node);
140+
$node->attrGroups = array_merge($node->attrGroups, $requiresAttributeGroups);
141+
$this->removeMethodRequiresAnnotations($phpDocInfo);
142+
$hasChanged = true;
143+
}
144+
}
145+
146+
foreach ($node->getMethods() as $classMethod) {
147+
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($classMethod);
148+
if ($phpDocInfo instanceof PhpDocInfo) {
149+
$requiresAttributeGroups = $this->handleRequires($phpDocInfo);
150+
if ($requiresAttributeGroups !== []) {
151+
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classMethod);
152+
$classMethod->attrGroups = array_merge($classMethod->attrGroups, $requiresAttributeGroups);
153+
$this->removeMethodRequiresAnnotations($phpDocInfo);
154+
$hasChanged = true;
155+
}
156+
}
157+
}
158+
}
159+
160+
return $hasChanged ? $node : null;
161+
}
162+
163+
private function createAttributeGroup(string $annotationValue): ?AttributeGroup
164+
{
165+
$annotationValues = explode(' ', $annotationValue, 2);
166+
$type = array_shift($annotationValues);
167+
$attributeValue = array_shift($annotationValues);
168+
switch ($type) {
169+
case 'PHP':
170+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresPhp';
171+
$attributeValue = [$attributeValue];
172+
break;
173+
case 'PHPUnit':
174+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresPhpunit';
175+
$attributeValue = [$attributeValue];
176+
break;
177+
case 'OS':
178+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresOperatingSystem';
179+
$attributeValue = [$attributeValue];
180+
break;
181+
case 'OSFAMILY':
182+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily';
183+
$attributeValue = [$attributeValue];
184+
break;
185+
case 'function':
186+
if (str_contains((string) $attributeValue, '::')) {
187+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresMethod';
188+
$attributeValue = explode('::', (string) $attributeValue);
189+
$attributeValue[0] .= '::class';
190+
} else {
191+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresFunction';
192+
$attributeValue = [$attributeValue];
193+
}
194+
195+
break;
196+
case 'extension':
197+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresExtension';
198+
$attributeValue = explode(' ', (string) $attributeValue, 2);
199+
break;
200+
case 'setting':
201+
$attributeClass = 'PHPUnit\Framework\Attributes\RequiresSetting';
202+
$attributeValue = explode(' ', (string) $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)