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-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/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 new file mode 100644 index 00000000000..72773f443c9 --- /dev/null +++ b/rules/Renaming/Rector/MethodCall/RenameDeprecatedMethodCallRector.php @@ -0,0 +1,85 @@ +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; + } + + if ($this->getName($node->name) === null) { + return null; + } + + $methodReflection = $node instanceof MethodCall + ? $this->reflectionResolver->resolveMethodReflectionFromMethodCall($node) + : $this->reflectionResolver->resolveMethodReflectionFromStaticCall($node); + + if (! $methodReflection instanceof MethodReflection) { + return null; + } + + $newMethodName = $this->deprecatedMethodCallReplacementResolver->resolve($methodReflection); + if ($newMethodName === null) { + return null; + } + + $node->name = new Identifier($newMethodName); + + return $node; + } +} 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([