From 30a9bb2c39d632355628c9d8a5d030f113035103 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 30 May 2026 23:21:50 +0200 Subject: [PATCH 1/3] [Renaming] Add RenameDeprecatedMethodCallRector inferring rename from @deprecated docblock --- .../Fixture/rename_replaced_by.php.inc | 25 ++++ .../Fixture/rename_see_tag.php.inc | 25 ++++ .../Fixture/rename_static_call.php.inc | 25 ++++ .../Fixture/rename_use_instead.php.inc | 25 ++++ .../Fixture/skip_no_suggestion.php.inc | 11 ++ .../Fixture/skip_not_deprecated.php.inc | 10 ++ .../RenameDeprecatedMethodCallRectorTest.php | 28 ++++ .../Source/SomeApiClient.php | 58 ++++++++ .../config/configured_rule.php | 9 ++ .../RenameDeprecatedMethodCallRector.php | 135 ++++++++++++++++++ 10 files changed, 351 insertions(+) create mode 100644 rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_replaced_by.php.inc create mode 100644 rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_see_tag.php.inc create mode 100644 rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_static_call.php.inc create mode 100644 rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_use_instead.php.inc create mode 100644 rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/skip_no_suggestion.php.inc create mode 100644 rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/skip_not_deprecated.php.inc create mode 100644 rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/RenameDeprecatedMethodCallRectorTest.php create mode 100644 rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Source/SomeApiClient.php create mode 100644 rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/config/configured_rule.php create mode 100644 rules/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector.php diff --git a/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_replaced_by.php.inc b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_replaced_by.php.inc new file mode 100644 index 00000000000..ccace790ff0 --- /dev/null +++ b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_replaced_by.php.inc @@ -0,0 +1,25 @@ +loadData(); +} + +?> +----- +fetchData(); +} + +?> diff --git a/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_see_tag.php.inc b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_see_tag.php.inc new file mode 100644 index 00000000000..b810181fc02 --- /dev/null +++ b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_see_tag.php.inc @@ -0,0 +1,25 @@ +readData(); +} + +?> +----- +fetchData(); +} + +?> diff --git a/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_static_call.php.inc b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_static_call.php.inc new file mode 100644 index 00000000000..ab615b8989f --- /dev/null +++ b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_static_call.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_use_instead.php.inc b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_use_instead.php.inc new file mode 100644 index 00000000000..147e12b2dd3 --- /dev/null +++ b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/rename_use_instead.php.inc @@ -0,0 +1,25 @@ +getData(); +} + +?> +----- +fetchData(); +} + +?> diff --git a/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/skip_no_suggestion.php.inc b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/skip_no_suggestion.php.inc new file mode 100644 index 00000000000..fa30f86cb71 --- /dev/null +++ b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/skip_no_suggestion.php.inc @@ -0,0 +1,11 @@ +legacyData(); +} diff --git a/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/skip_not_deprecated.php.inc b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/skip_not_deprecated.php.inc new file mode 100644 index 00000000000..0c4d9f5a3b4 --- /dev/null +++ b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Fixture/skip_not_deprecated.php.inc @@ -0,0 +1,10 @@ +fetchData(); +} diff --git a/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/RenameDeprecatedMethodCallRectorTest.php b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/RenameDeprecatedMethodCallRectorTest.php new file mode 100644 index 00000000000..c2197f31579 --- /dev/null +++ b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/RenameDeprecatedMethodCallRectorTest.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/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Source/SomeApiClient.php b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Source/SomeApiClient.php new file mode 100644 index 00000000000..85c21398859 --- /dev/null +++ b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/Source/SomeApiClient.php @@ -0,0 +1,58 @@ +fetchData(); + } + + /** + * @deprecated replaced by fetchData() + */ + public function loadData(): array + { + return $this->fetchData(); + } + + /** + * @deprecated {@see fetchData()} + */ + public function readData(): array + { + return $this->fetchData(); + } + + /** + * @deprecated since 2.0, use the repository layer instead + */ + public function legacyData(): array + { + return $this->fetchData(); + } + + public function fetchData(): array + { + return []; + } + + /** + * @deprecated use make() instead + */ + public static function makeOld(): self + { + return new self(); + } + + public static function make(): self + { + return new self(); + } +} diff --git a/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/config/configured_rule.php b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/config/configured_rule.php new file mode 100644 index 00000000000..65d1ee0f8e5 --- /dev/null +++ b/rules-tests/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([RenameDeprecatedMethodCallRector::class]); diff --git a/rules/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector.php b/rules/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector.php new file mode 100644 index 00000000000..8a73533d1e9 --- /dev/null +++ b/rules/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector.php @@ -0,0 +1,135 @@ +\w+)\(\)#i'; + + public function __construct( + private readonly ReflectionResolver $reflectionResolver + ) { + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Rename method calls whose target method is "@deprecated" and suggests a replacement method on the same class', + [ + new CodeSample( + <<<'CODE_SAMPLE' +$someObject->oldMethod(); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +$someObject->newMethod(); +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [MethodCall::class, StaticCall::class]; + } + + /** + * @param MethodCall|StaticCall $node + */ + public function refactor(Node $node): ?Node + { + if ($node->isFirstClassCallable()) { + return null; + } + + $callName = $this->getName($node->name); + if ($callName === null) { + return null; + } + + $methodReflection = $node instanceof MethodCall + ? $this->reflectionResolver->resolveMethodReflectionFromMethodCall($node) + : $this->reflectionResolver->resolveMethodReflectionFromStaticCall($node); + + if (! $methodReflection instanceof MethodReflection) { + return null; + } + + if (! $methodReflection->isDeprecated()->yes()) { + return null; + } + + $newMethodName = $this->matchNewMethodName($methodReflection->getDeprecatedDescription()); + if ($newMethodName === null) { + return null; + } + + // already the suggested name? nothing to do + if ($this->nodeNameResolver->isStringName($callName, $newMethodName)) { + return null; + } + + if (! $this->isExistingNonDeprecatedMethod($methodReflection->getDeclaringClass(), $newMethodName)) { + return null; + } + + $node->name = new Identifier($newMethodName); + + return $node; + } + + private function matchNewMethodName(?string $deprecatedDescription): ?string + { + if ($deprecatedDescription === null || $deprecatedDescription === '') { + return null; + } + + $match = Strings::match($deprecatedDescription, self::RENAME_SUGGESTION_REGEX); + if ($match === null) { + return null; + } + + return $match['method']; + } + + private function isExistingNonDeprecatedMethod(ClassReflection $classReflection, string $newMethodName): bool + { + if (! $classReflection->hasMethod($newMethodName)) { + return false; + } + + // do not rename onto another deprecated method, to avoid suggesting a dead end + $extendedMethodReflection = $classReflection->getNativeMethod($newMethodName); + return ! $extendedMethodReflection->isDeprecated() + ->yes(); + } +} From 31830049203256f3476edc11472b507f3e27596c Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 2 Jun 2026 11:10:04 +0200 Subject: [PATCH 2/3] cs --- tests/Issues/Issue9771/config/configured_rule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Issues/Issue9771/config/configured_rule.php b/tests/Issues/Issue9771/config/configured_rule.php index 7707a529952..d3074f68195 100644 --- a/tests/Issues/Issue9771/config/configured_rule.php +++ b/tests/Issues/Issue9771/config/configured_rule.php @@ -2,10 +2,10 @@ declare(strict_types=1); -use Rector\DeadCode\Rector\MethodCall\RemoveNullNamedArgOnNullDefaultParamRector; use Rector\CodeQuality\Rector\CallLike\AddNameToNullArgumentRector; use Rector\CodeQuality\Rector\FuncCall\SortCallLikeNamedArgsRector; use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\MethodCall\RemoveNullNamedArgOnNullDefaultParamRector; return RectorConfig::configure() ->withRules([ From 857000deae0db38218e3fc916d1be24569c6f6c7 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 2 Jun 2026 11:25:29 +0200 Subject: [PATCH 3/3] extract DeprecatedMethodCallReplacementResolver --- ...catedMethodCallReplacementResolverTest.php | 52 +++++++++++++ .../Source/DeprecatedMethodsClient.php | 74 ++++++++++++++++++ ...eprecatedMethodCallReplacementResolver.php | 78 +++++++++++++++++++ .../RenameDeprecatedMethodCallRector.php | 60 ++------------ 4 files changed, 209 insertions(+), 55 deletions(-) create mode 100644 rules-tests/Renaming/NodeAnalyzer/DeprecatedMethodCallReplacementResolverTest.php create mode 100644 rules-tests/Renaming/NodeAnalyzer/Source/DeprecatedMethodsClient.php create mode 100644 rules/Renaming/NodeAnalyzer/DeprecatedMethodCallReplacementResolver.php diff --git a/rules-tests/Renaming/NodeAnalyzer/DeprecatedMethodCallReplacementResolverTest.php b/rules-tests/Renaming/NodeAnalyzer/DeprecatedMethodCallReplacementResolverTest.php new file mode 100644 index 00000000000..ca89b45031d --- /dev/null +++ b/rules-tests/Renaming/NodeAnalyzer/DeprecatedMethodCallReplacementResolverTest.php @@ -0,0 +1,52 @@ +deprecatedMethodCallReplacementResolver = $this->make(DeprecatedMethodCallReplacementResolver::class); + $this->reflectionProvider = $this->make(ReflectionProvider::class); + } + + #[DataProvider('provideData')] + public function test(string $methodName, ?string $expectedReplacement): void + { + $classReflection = $this->reflectionProvider->getClass(DeprecatedMethodsClient::class); + $extendedMethodReflection = $classReflection->getNativeMethod($methodName); + + $resolvedReplacement = $this->deprecatedMethodCallReplacementResolver->resolve($extendedMethodReflection); + $this->assertSame($expectedReplacement, $resolvedReplacement); + } + + /** + * @return Iterator + */ + public static function provideData(): Iterator + { + yield 'use ...() instead' => ['getData', 'fetchData']; + yield 'replaced by ...()' => ['loadData', 'fetchData']; + yield '{@see ...()}' => ['readData', 'fetchData']; + yield 'static use ...() instead' => ['makeOld', 'make']; + yield 'deprecated without method suggestion' => ['legacyData', null]; + yield 'suggested method does not exist' => ['vanishedData', null]; + yield 'suggested method is itself deprecated' => ['deadEndData', null]; + yield 'not deprecated at all' => ['fetchData', null]; + } +} diff --git a/rules-tests/Renaming/NodeAnalyzer/Source/DeprecatedMethodsClient.php b/rules-tests/Renaming/NodeAnalyzer/Source/DeprecatedMethodsClient.php new file mode 100644 index 00000000000..8c0cb859e70 --- /dev/null +++ b/rules-tests/Renaming/NodeAnalyzer/Source/DeprecatedMethodsClient.php @@ -0,0 +1,74 @@ +fetchData(); + } + + /** + * @deprecated replaced by fetchData() + */ + public function loadData(): array + { + return $this->fetchData(); + } + + /** + * @deprecated {@see fetchData()} + */ + public function readData(): array + { + return $this->fetchData(); + } + + /** + * @deprecated since 2.0, use the repository layer instead + */ + public function legacyData(): array + { + return $this->fetchData(); + } + + /** + * @deprecated use missingMethod() instead + */ + public function vanishedData(): array + { + return $this->fetchData(); + } + + /** + * @deprecated use loadData() instead + */ + public function deadEndData(): array + { + return $this->fetchData(); + } + + public function fetchData(): array + { + return []; + } + + /** + * @deprecated use make() instead + */ + public static function makeOld(): self + { + return new self(); + } + + public static function make(): self + { + return new self(); + } +} diff --git a/rules/Renaming/NodeAnalyzer/DeprecatedMethodCallReplacementResolver.php b/rules/Renaming/NodeAnalyzer/DeprecatedMethodCallReplacementResolver.php new file mode 100644 index 00000000000..cb7d4dfbbea --- /dev/null +++ b/rules/Renaming/NodeAnalyzer/DeprecatedMethodCallReplacementResolver.php @@ -0,0 +1,78 @@ +\w+)\(\)#i'; + + /** + * Resolves a non-deprecated replacement method name suggested by the "@deprecated" docblock + * of the given method, or null when there is no usable suggestion. + */ + public function resolve(MethodReflection $methodReflection): ?string + { + if (! $methodReflection->isDeprecated()->yes()) { + return null; + } + + $newMethodName = $this->matchNewMethodName($methodReflection->getDeprecatedDescription()); + if ($newMethodName === null) { + return null; + } + + // already the suggested name? nothing to do + if (strtolower($methodReflection->getName()) === strtolower($newMethodName)) { + return null; + } + + if (! $this->isExistingNonDeprecatedMethod($methodReflection->getDeclaringClass(), $newMethodName)) { + return null; + } + + return $newMethodName; + } + + private function matchNewMethodName(?string $deprecatedDescription): ?string + { + if ($deprecatedDescription === null || $deprecatedDescription === '') { + return null; + } + + $match = Strings::match($deprecatedDescription, self::RENAME_SUGGESTION_REGEX); + if ($match === null) { + return null; + } + + return $match['method']; + } + + private function isExistingNonDeprecatedMethod(ClassReflection $classReflection, string $newMethodName): bool + { + if (! $classReflection->hasMethod($newMethodName)) { + return false; + } + + // do not rename onto another deprecated method, to avoid suggesting a dead end + $extendedMethodReflection = $classReflection->getNativeMethod($newMethodName); + return ! $extendedMethodReflection->isDeprecated() + ->yes(); + } +} diff --git a/rules/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector.php b/rules/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector.php index 8a73533d1e9..72773f443c9 100644 --- a/rules/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector.php +++ b/rules/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector.php @@ -4,15 +4,14 @@ namespace Rector\Renaming\Rector\MethodCall; -use Nette\Utils\Strings; use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Identifier; -use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; use Rector\Rector\AbstractRector; use Rector\Reflection\ReflectionResolver; +use Rector\Renaming\NodeAnalyzer\DeprecatedMethodCallReplacementResolver; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -21,18 +20,9 @@ */ final class RenameDeprecatedMethodCallRector extends AbstractRector { - /** - * Matches the new method name suggested inside a "@deprecated" description, e.g.: - * - "use newMethod() instead" - * - "replaced by newMethod()" - * - "{@see newMethod()}" - * - * Cross-class suggestions ("Other::newMethod()") are intentionally skipped for now. - */ - private const string RENAME_SUGGESTION_REGEX = '#(?:\buse\b|\breplaced by\b|\{@see)\s+(?\w+)\(\)#i'; - public function __construct( - private readonly ReflectionResolver $reflectionResolver + private readonly ReflectionResolver $reflectionResolver, + private readonly DeprecatedMethodCallReplacementResolver $deprecatedMethodCallReplacementResolver ) { } @@ -71,8 +61,7 @@ public function refactor(Node $node): ?Node return null; } - $callName = $this->getName($node->name); - if ($callName === null) { + if ($this->getName($node->name) === null) { return null; } @@ -84,52 +73,13 @@ public function refactor(Node $node): ?Node return null; } - if (! $methodReflection->isDeprecated()->yes()) { - return null; - } - - $newMethodName = $this->matchNewMethodName($methodReflection->getDeprecatedDescription()); + $newMethodName = $this->deprecatedMethodCallReplacementResolver->resolve($methodReflection); if ($newMethodName === null) { return null; } - // already the suggested name? nothing to do - if ($this->nodeNameResolver->isStringName($callName, $newMethodName)) { - return null; - } - - if (! $this->isExistingNonDeprecatedMethod($methodReflection->getDeclaringClass(), $newMethodName)) { - return null; - } - $node->name = new Identifier($newMethodName); return $node; } - - private function matchNewMethodName(?string $deprecatedDescription): ?string - { - if ($deprecatedDescription === null || $deprecatedDescription === '') { - return null; - } - - $match = Strings::match($deprecatedDescription, self::RENAME_SUGGESTION_REGEX); - if ($match === null) { - return null; - } - - return $match['method']; - } - - private function isExistingNonDeprecatedMethod(ClassReflection $classReflection, string $newMethodName): bool - { - if (! $classReflection->hasMethod($newMethodName)) { - return false; - } - - // do not rename onto another deprecated method, to avoid suggesting a dead end - $extendedMethodReflection = $classReflection->getNativeMethod($newMethodName); - return ! $extendedMethodReflection->isDeprecated() - ->yes(); - } }