diff --git a/config/set/downgrade-php81.php b/config/set/downgrade-php81.php index 52cdce05..f35d4674 100644 --- a/config/set/downgrade-php81.php +++ b/config/set/downgrade-php81.php @@ -9,6 +9,7 @@ use Rector\Config\RectorConfig; use Rector\ValueObject\PhpVersion; use Rector\DowngradePhp81\Rector\ClassConst\DowngradeFinalizePublicClassConstantRector; +use Rector\DowngradePhp81\Rector\ClassMethod\AddReturnTypeWillChangeAttributeRector; use Rector\DowngradePhp81\Rector\FuncCall\DowngradeArrayIsListRector; use Rector\DowngradePhp81\Rector\FuncCall\DowngradeFirstClassCallableSyntaxRector; use Rector\DowngradePhp81\Rector\FunctionLike\DowngradeNeverTypeDeclarationRector; @@ -25,6 +26,7 @@ $rectorConfig->phpVersion(PhpVersion::PHP_80); $rectorConfig->rules([ + AddReturnTypeWillChangeAttributeRector::class, DowngradeFinalizePublicClassConstantRector::class, DowngradeFirstClassCallableSyntaxRector::class, DowngradeNeverTypeDeclarationRector::class, diff --git a/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/AddReturnTypeWillChangeAttributeRectorTest.php b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/AddReturnTypeWillChangeAttributeRectorTest.php new file mode 100644 index 00000000..18d88acd --- /dev/null +++ b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/AddReturnTypeWillChangeAttributeRectorTest.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/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/array_access.php.inc b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/array_access.php.inc new file mode 100644 index 00000000..e619ae49 --- /dev/null +++ b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/array_access.php.inc @@ -0,0 +1,53 @@ + +----- + diff --git a/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/countable.php.inc b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/countable.php.inc new file mode 100644 index 00000000..2d5f538a --- /dev/null +++ b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/countable.php.inc @@ -0,0 +1,26 @@ + +----- + diff --git a/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/iterator.php.inc b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/iterator.php.inc new file mode 100644 index 00000000..fc63a459 --- /dev/null +++ b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/iterator.php.inc @@ -0,0 +1,62 @@ + +----- + diff --git a/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/iterator_aggregate.php.inc b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/iterator_aggregate.php.inc new file mode 100644 index 00000000..1f8d69e8 --- /dev/null +++ b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/iterator_aggregate.php.inc @@ -0,0 +1,26 @@ + +----- + diff --git a/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/skip_attribute_already_present.php.inc b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/skip_attribute_already_present.php.inc new file mode 100644 index 00000000..ac8797e9 --- /dev/null +++ b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/Fixture/skip_attribute_already_present.php.inc @@ -0,0 +1,26 @@ + +----- + diff --git a/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/config/configured_rule.php b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/config/configured_rule.php new file mode 100644 index 00000000..e516b61e --- /dev/null +++ b/rules-tests/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(AddReturnTypeWillChangeAttributeRector::class); +}; diff --git a/rules/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector.php b/rules/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector.php new file mode 100644 index 00000000..54364d20 --- /dev/null +++ b/rules/DowngradePhp81/Rector/ClassMethod/AddReturnTypeWillChangeAttributeRector.php @@ -0,0 +1,150 @@ + + * + * PHP 8.1 added provisional return types to these core interface methods. + * Implementations without matching return types trigger deprecation notices + * on PHP 8.1+. Adding #[\ReturnTypeWillChange] suppresses those notices, + * keeping the code compatible with older PHP versions simultaneously. + */ + private const array INTERFACE_METHOD_MAP = [ + 'ArrayAccess' => ['offsetGet', 'offsetExists', 'offsetSet', 'offsetUnset'], + 'Countable' => ['count'], + 'Iterator' => ['current', 'key', 'next', 'rewind', 'valid'], + 'IteratorAggregate' => ['getIterator'], + 'Stringable' => ['__toString'], + ]; + + private const string RETURN_TYPE_WILL_CHANGE = 'ReturnTypeWillChange'; + + public function __construct( + private readonly ReflectionProvider $reflectionProvider, + ) { + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Add #[\ReturnTypeWillChange] attribute to methods implementing PHP 8.1 interface methods with provisional return types, to suppress deprecation notices when running on PHP 8.1+ without the required return types', + [ + new CodeSample( + <<<'CODE_SAMPLE' +class SomeClass implements \ArrayAccess +{ + public function offsetGet($offset) + { + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +class SomeClass implements \ArrayAccess +{ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + $className = $this->getName($node); + if ($className === null) { + return null; + } + + if (! $this->reflectionProvider->hasClass($className)) { + return null; + } + + $classReflection = $this->reflectionProvider->getClass($className); + $hasChanged = false; + + foreach ($node->getMethods() as $classMethod) { + if ($this->hasReturnTypeWillChangeAttribute($classMethod)) { + continue; + } + + if ($classMethod->returnType instanceof Node) { + continue; + } + + $methodName = $this->getName($classMethod); + + foreach (self::INTERFACE_METHOD_MAP as $interface => $methods) { + if (! in_array($methodName, $methods, true)) { + continue; + } + + if (! $classReflection->is($interface)) { + continue; + } + + $classMethod->attrGroups[] = new AttributeGroup([ + new Attribute(new FullyQualified(self::RETURN_TYPE_WILL_CHANGE)), + ]); + + $hasChanged = true; + break; + } + } + + if ($hasChanged) { + return $node; + } + + return null; + } + + private function hasReturnTypeWillChangeAttribute(ClassMethod $classMethod): bool + { + foreach ($classMethod->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->getLast() === self::RETURN_TYPE_WILL_CHANGE) { + return true; + } + } + } + + return false; + } +}