From f7c8184bc303904c88cb16c0e059bc759ac0b87a Mon Sep 17 00:00:00 2001 From: GeorgII Date: Mon, 14 Sep 2026 10:35:17 +0200 Subject: [PATCH] Add AddReturnTypeWillChangeRector for PHP 8.1 compatibility Introduce a new rector that adds the `#[ReturnTypeWillChange]` attribute to methods with incompatible return types downgraded for PHP 7.4, ensuring compatibility with PHP 8.1+. Register the rule in the PHP 7.4 config. --- .../Rules/AddReturnTypeWillChangeRector.php | 147 ++++++++++++++++++ rector/toPhp7.4.php | 2 + 2 files changed, 149 insertions(+) create mode 100755 rector/Rules/AddReturnTypeWillChangeRector.php diff --git a/rector/Rules/AddReturnTypeWillChangeRector.php b/rector/Rules/AddReturnTypeWillChangeRector.php new file mode 100755 index 00000000..3c24c66e --- /dev/null +++ b/rector/Rules/AddReturnTypeWillChangeRector.php @@ -0,0 +1,147 @@ + methods carrying a tentative return type since PHP 8.1. + * + * @var array> + */ + private const TENTATIVE_RETURN_TYPES = [ + 'ArrayAccess' => ['offsetExists', 'offsetGet', 'offsetSet', 'offsetUnset'], + 'Countable' => ['count'], + 'IteratorAggregate' => ['getIterator'], + 'JsonSerializable' => ['jsonSerialize'], + 'Iterator' => ['current', 'key', 'next', 'rewind', 'valid'], + 'Serializable' => ['serialize', 'unserialize'], + ]; + + /** + * Return types that `->withDowngradeSets(php74: true)` removes entirely, + * leaving the declaration without a native return type. + * + * @var list + */ + private const UNSUPPORTED_RETURN_TYPES = ['mixed', 'never', 'null']; + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class, Interface_::class, Trait_::class]; + } + + public function refactor(Node $node): ?Node + { + $hasChanged = false; + + foreach (self::TENTATIVE_RETURN_TYPES as $interface => $methodNames) { + if (!$this->isObjectType($node, new ObjectType($interface))) { + continue; + } + + foreach ($methodNames as $methodName) { + $classMethod = $node->getMethod($methodName); + if (!$classMethod instanceof ClassMethod) { + continue; + } + + if ($this->refactorClassMethod($classMethod)) { + $hasChanged = true; + } + } + } + + return $hasChanged ? $node : null; + } + + private function hasAttribute(ClassMethod $classMethod): bool + { + foreach ($classMethod->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attribute) { + if (strtolower($attribute->name->toString()) === 'returntypewillchange') { + return true; + } + } + } + + return false; + } + + /** + * True when the downgrade leaves (or already left) the method without a + * native return type. + */ + private function losesReturnType(ClassMethod $classMethod): bool + { + $returnType = $classMethod->returnType; + + // Already stripped by a downgrade rule that ran before this one. + if ($returnType === null) { + return true; + } + + // Union and intersection types do not exist in PHP 7.4. + if ($returnType instanceof UnionType || $returnType instanceof IntersectionType) { + return true; + } + + if ($returnType instanceof Identifier) { + return in_array(strtolower($returnType->toString()), self::UNSUPPORTED_RETURN_TYPES, true); + } + + // `static` as a return type is PHP 8.0+. + return $returnType instanceof Name && strtolower($returnType->toString()) === 'static'; + } + + private function refactorClassMethod(ClassMethod $classMethod): bool + { + if (!$this->losesReturnType($classMethod)) { + return false; + } + + if ($this->hasAttribute($classMethod)) { + return false; + } + + $classMethod->attrGroups[] = new AttributeGroup([ + new Attribute(new FullyQualified('ReturnTypeWillChange')), + ]); + + return true; + } +} diff --git a/rector/toPhp7.4.php b/rector/toPhp7.4.php index 944aae3c..74811e6a 100755 --- a/rector/toPhp7.4.php +++ b/rector/toPhp7.4.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Rector\Rules\AddReturnTypeWillChangeRector; use App\Rector\Rules\DowngradeStandaloneLiteralParamTypeRector; use Rector\Config\RectorConfig; @@ -10,6 +11,7 @@ __DIR__ . '/../src', ]) ->withRules([ + AddReturnTypeWillChangeRector::class, DowngradeStandaloneLiteralParamTypeRector::class, ]) ->withDowngradeSets(php74: true);