Skip to content

Commit 9453554

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

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 PHPStan\Reflection\ReflectionProvider;
13+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
14+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
15+
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover;
16+
use Rector\Comments\NodeDocBlock\DocBlockUpdater;
17+
use Rector\PhpAttribute\NodeFactory\PhpAttributeGroupFactory;
18+
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
19+
use Rector\Rector\AbstractRector;
20+
use Rector\ValueObject\PhpVersionFeature;
21+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
22+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
23+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
24+
25+
/**
26+
* @see \Rector\PHPUnit\Tests\AnnotationsToAttributes\Rector\Class_\RequiresAnnotationWithValueToAttributeRector\RequiresAnnotationWithValueToAttributeRectorTest
27+
*/
28+
final class RequiresAnnotationWithValueToAttributeRector extends AbstractRector implements MinPhpVersionInterface
29+
{
30+
public function __construct(
31+
private readonly PhpDocTagRemover $phpDocTagRemover,
32+
private readonly PhpAttributeGroupFactory $phpAttributeGroupFactory,
33+
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
34+
private readonly DocBlockUpdater $docBlockUpdater,
35+
private readonly PhpDocInfoFactory $phpDocInfoFactory,
36+
private readonly ReflectionProvider $reflectionProvider
37+
) {
38+
}
39+
40+
public function getRuleDefinition(): RuleDefinition
41+
{
42+
return new RuleDefinition('Change Requires annotations with values to attributes', [
43+
new CodeSample(
44+
<<<'CODE_SAMPLE'
45+
use PHPUnit\Framework\TestCase;
46+
47+
/**
48+
* @requires PHP > 8.4
49+
* @requires PHPUnit >= 10
50+
* @requires OS Windows
51+
* @requires OSFAMILY Darwin
52+
* @requires function someFunction
53+
* @requires function \some\className::someMethod
54+
* @requires extension mysqli
55+
* @requires extension mysqli >= 8.3.0
56+
* @requires setting date.timezone Europe/Berlin
57+
*/
58+
59+
final class SomeTest extends TestCase
60+
{
61+
/**
62+
* @requires PHP > 8.4
63+
* @requires PHPUnit >= 10
64+
* @requires OS Windows
65+
* @requires OSFAMILY Darwin
66+
* @requires function someFunction
67+
* @requires function \some\className::someMethod
68+
* @requires extension mysqli
69+
* @requires extension mysqli >= 8.3.0
70+
* @requires setting date.timezone Europe/Berlin
71+
*/
72+
public function test()
73+
{
74+
}
75+
}
76+
CODE_SAMPLE
77+
78+
,
79+
<<<'CODE_SAMPLE'
80+
use PHPUnit\Framework\TestCase;
81+
82+
#[\PHPUnit\Framework\Attributes\RequiresPhp('> 8.4')]
83+
#[\PHPUnit\Framework\Attributes\RequiresPhpunit('>= 10')]
84+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystem('Windows')]
85+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily('Darwin')]
86+
#[\PHPUnit\Framework\Attributes\RequiresFunction('someFunction')]
87+
#[\PHPUnit\Framework\Attributes\RequiresMethod(\some\className::class, 'someMethod')]
88+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli')]
89+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli', '>= 8.3.0')]
90+
#[\PHPUnit\Framework\Attributes\RequiresSetting('date.timezone', 'Europe/Berlin')]
91+
final class SomeTest extends TestCase
92+
{
93+
94+
#[\PHPUnit\Framework\Attributes\RequiresPhp('> 8.4')]
95+
#[\PHPUnit\Framework\Attributes\RequiresPhpunit('>= 10')]
96+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystem('Windows')]
97+
#[\PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily('Darwin')]
98+
#[\PHPUnit\Framework\Attributes\RequiresFunction('someFunction')]
99+
#[\PHPUnit\Framework\Attributes\RequiresMethod(\some\className::class, 'someMethod')]
100+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli')]
101+
#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli', '>= 8.3.0')]
102+
#[\PHPUnit\Framework\Attributes\RequiresSetting('date.timezone', 'Europe/Berlin')]
103+
public function test()
104+
{
105+
}
106+
}
107+
CODE_SAMPLE
108+
),
109+
]);
110+
}
111+
112+
/**
113+
* @return array<class-string<Node>>
114+
*/
115+
public function getNodeTypes(): array
116+
{
117+
return [Class_::class, ClassMethod::class];
118+
}
119+
120+
public function provideMinPhpVersion(): int
121+
{
122+
return PhpVersionFeature::ATTRIBUTES;
123+
}
124+
125+
/**
126+
* @param Class_|ClassMethod $node
127+
*/
128+
public function refactor(Node $node): ?Node
129+
{
130+
if (! $this->testsNodeAnalyzer->isInTestClass($node)) {
131+
return null;
132+
}
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+
}
143+
}
144+
145+
foreach ($node->getMethods() as $classNode) {
146+
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($classNode);
147+
if ($phpDocInfo instanceof PhpDocInfo) {
148+
$requiresAttributeGroups = $this->handleRequires($phpDocInfo);
149+
if (! ($requiresAttributeGroups === [])) {
150+
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classNode);
151+
$classNode->attrGroups = array_merge($classNode->attrGroups, $requiresAttributeGroups);
152+
$this->removeMethodRequiresAnnotations($phpDocInfo);
153+
}
154+
}
155+
}
156+
157+
158+
return $node;
159+
}
160+
161+
return 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>
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)