diff --git a/rules-tests/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector/Fixture/requires_fixture.php.inc b/rules-tests/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector/Fixture/requires_fixture.php.inc new file mode 100644 index 00000000..39909a22 --- /dev/null +++ b/rules-tests/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector/Fixture/requires_fixture.php.inc @@ -0,0 +1,65 @@ + 8.4 + * @requires PHPUnit >= 10 + * @requires OS Windows + * @requires OSFAMILY Darwin + * @requires function someFunction + * @requires function \some\className::someMethod + * @requires extension mysqli + * @requires extension mysqli >= 8.3.0 + * @requires setting date.timezone Europe/Berlin + */ +class BarController extends TestCase +{ + /** + * @requires PHP > 8.4 + * @requires PHPUnit >= 10 + * @requires OS Windows + * @requires OSFAMILY Darwin + * @requires function someFunction + * @requires function \some\className::someMethod + * @requires extension mysqli + * @requires extension mysqli >= 8.3.0 + * @requires setting date.timezone Europe/Berlin + */ + public function testWithRequires() + { + } +} + +?> +----- + 8.4')] +#[\PHPUnit\Framework\Attributes\RequiresPhpunit('>= 10')] +#[\PHPUnit\Framework\Attributes\RequiresOperatingSystem('Windows')] +#[\PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily('Darwin')] +#[\PHPUnit\Framework\Attributes\RequiresFunction('someFunction')] +#[\PHPUnit\Framework\Attributes\RequiresMethod(\some\className::class, 'someMethod')] +#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli')] +#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli', '>= 8.3.0')] +#[\PHPUnit\Framework\Attributes\RequiresSetting('date.timezone', 'Europe/Berlin')] +class BarController extends TestCase +{ + #[\PHPUnit\Framework\Attributes\RequiresPhp('> 8.4')] + #[\PHPUnit\Framework\Attributes\RequiresPhpunit('>= 10')] + #[\PHPUnit\Framework\Attributes\RequiresOperatingSystem('Windows')] + #[\PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily('Darwin')] + #[\PHPUnit\Framework\Attributes\RequiresFunction('someFunction')] + #[\PHPUnit\Framework\Attributes\RequiresMethod(\some\className::class, 'someMethod')] + #[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli')] + #[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli', '>= 8.3.0')] + #[\PHPUnit\Framework\Attributes\RequiresSetting('date.timezone', 'Europe/Berlin')] + public function testWithRequires() + { + } +} + +?> diff --git a/rules-tests/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector/RequiresAnnotationWithValueToAttributeRectorTest.php b/rules-tests/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector/RequiresAnnotationWithValueToAttributeRectorTest.php new file mode 100644 index 00000000..2255c75d --- /dev/null +++ b/rules-tests/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector/RequiresAnnotationWithValueToAttributeRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector/config/configured_rule.php b/rules-tests/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector/config/configured_rule.php new file mode 100644 index 00000000..d9f4f077 --- /dev/null +++ b/rules-tests/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(RequiresAnnotationWithValueToAttributeRector::class); +}; diff --git a/rules/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector.php b/rules/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector.php new file mode 100644 index 00000000..c086ed17 --- /dev/null +++ b/rules/AnnotationsToAttributes/Rector/Class_/RequiresAnnotationWithValueToAttributeRector.php @@ -0,0 +1,247 @@ + 8.4 + * @requires PHPUnit >= 10 + * @requires OS Windows + * @requires OSFAMILY Darwin + * @requires function someFunction + * @requires function \some\className::someMethod + * @requires extension mysqli + * @requires extension mysqli >= 8.3.0 + * @requires setting date.timezone Europe/Berlin + */ + +final class SomeTest extends TestCase +{ + /** + * @requires PHP > 8.4 + * @requires PHPUnit >= 10 + * @requires OS Windows + * @requires OSFAMILY Darwin + * @requires function someFunction + * @requires function \some\className::someMethod + * @requires extension mysqli + * @requires extension mysqli >= 8.3.0 + * @requires setting date.timezone Europe/Berlin + */ + public function test() + { + } +} +CODE_SAMPLE + + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +#[\PHPUnit\Framework\Attributes\RequiresPhp('> 8.4')] +#[\PHPUnit\Framework\Attributes\RequiresPhpunit('>= 10')] +#[\PHPUnit\Framework\Attributes\RequiresOperatingSystem('Windows')] +#[\PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily('Darwin')] +#[\PHPUnit\Framework\Attributes\RequiresFunction('someFunction')] +#[\PHPUnit\Framework\Attributes\RequiresMethod(\some\className::class, 'someMethod')] +#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli')] +#[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli', '>= 8.3.0')] +#[\PHPUnit\Framework\Attributes\RequiresSetting('date.timezone', 'Europe/Berlin')] +final class SomeTest extends TestCase +{ + + #[\PHPUnit\Framework\Attributes\RequiresPhp('> 8.4')] + #[\PHPUnit\Framework\Attributes\RequiresPhpunit('>= 10')] + #[\PHPUnit\Framework\Attributes\RequiresOperatingSystem('Windows')] + #[\PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily('Darwin')] + #[\PHPUnit\Framework\Attributes\RequiresFunction('someFunction')] + #[\PHPUnit\Framework\Attributes\RequiresMethod(\some\className::class, 'someMethod')] + #[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli')] + #[\PHPUnit\Framework\Attributes\RequiresExtension('mysqli', '>= 8.3.0')] + #[\PHPUnit\Framework\Attributes\RequiresSetting('date.timezone', 'Europe/Berlin')] + public function test() + { + } +} +CODE_SAMPLE + ), + ]); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class, ClassMethod::class]; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::ATTRIBUTES; + } + + /** + * @param Class_|ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + $hasChanged = false; + + + if ($node instanceof Class_) { + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + if ($phpDocInfo instanceof PhpDocInfo) { + $requiresAttributeGroups = $this->handleRequires($phpDocInfo); + if (! ($requiresAttributeGroups === [])) { + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + $node->attrGroups = array_merge($node->attrGroups, $requiresAttributeGroups); + $this->removeMethodRequiresAnnotations($phpDocInfo); + $hasChanged = true; + } + } + + foreach ($node->getMethods() as $classNode) { + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($classNode); + if ($phpDocInfo instanceof PhpDocInfo) { + $requiresAttributeGroups = $this->handleRequires($phpDocInfo); + if (! ($requiresAttributeGroups === [])) { + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classNode); + $classNode->attrGroups = array_merge($classNode->attrGroups, $requiresAttributeGroups); + $this->removeMethodRequiresAnnotations($phpDocInfo); + $hasChanged = true; + } + } + } + } + + return $hasChanged ? $node : null; + } + + private function createAttributeGroup(string $annotationValue): ?AttributeGroup + { + $annotationValues = explode(' ', $annotationValue, 2); + $type = array_shift($annotationValues); + $attributeValue = array_shift($annotationValues); + switch ($type) { + case 'PHP': + $attributeClass = 'PHPUnit\Framework\Attributes\RequiresPhp'; + $attributeValue = [$attributeValue]; + break; + case 'PHPUnit': + $attributeClass = 'PHPUnit\Framework\Attributes\RequiresPhpunit'; + $attributeValue = [$attributeValue]; + break; + case 'OS': + $attributeClass = 'PHPUnit\Framework\Attributes\RequiresOperatingSystem'; + $attributeValue = [$attributeValue]; + break; + case 'OSFAMILY': + $attributeClass = 'PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily'; + $attributeValue = [$attributeValue]; + break; + case 'function': + if (str_contains($attributeValue, '::')) { + $attributeClass = 'PHPUnit\Framework\Attributes\RequiresMethod'; + $attributeValue = explode('::', $attributeValue); + $attributeValue[0] .= '::class'; + } else { + $attributeClass = 'PHPUnit\Framework\Attributes\RequiresFunction'; + $attributeValue = [$attributeValue]; + } + break; + case 'extension': + $attributeClass = 'PHPUnit\Framework\Attributes\RequiresExtension'; + $attributeValue = explode(' ', $attributeValue, 2); + break; + case 'setting': + $attributeClass = 'PHPUnit\Framework\Attributes\RequiresSetting'; + $attributeValue = explode(' ', $attributeValue, 2); + break; + default: + return null; + } + + return $this->phpAttributeGroupFactory->createFromClassWithItems($attributeClass, [...$attributeValue]); + } + + /** + * @return array + */ + private function handleRequires(PhpDocInfo $phpDocInfo): array + { + $attributeGroups = []; + $desiredTagValueNodes = $phpDocInfo->getTagsByName('requires'); + foreach ($desiredTagValueNodes as $desiredTagValueNode) { + if (! $desiredTagValueNode->value instanceof GenericTagValueNode) { + continue; + } + + $requires = $desiredTagValueNode->value->value; + $attributeGroups[$requires] = $this->createAttributeGroup($requires); + $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $desiredTagValueNode); + } + + return $attributeGroups; + } + + private function removeMethodRequiresAnnotations(PhpDocInfo $phpDocInfo): bool + { + $hasChanged = false; + + $desiredTagValueNodes = $phpDocInfo->getTagsByName('requires'); + foreach ($desiredTagValueNodes as $desiredTagValueNode) { + if (! $desiredTagValueNode->value instanceof GenericTagValueNode) { + continue; + } + + $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $desiredTagValueNode); + $hasChanged = true; + } + + return $hasChanged; + } +}