diff --git a/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/DemoObjectMethodCallRenameRectorTest.php b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/DemoObjectMethodCallRenameRectorTest.php new file mode 100644 index 00000000000..6344ac0d22d --- /dev/null +++ b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/DemoObjectMethodCallRenameRectorTest.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/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/assigned_new.php.inc b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/assigned_new.php.inc new file mode 100644 index 00000000000..856d11a191b --- /dev/null +++ b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/assigned_new.php.inc @@ -0,0 +1,29 @@ +oldMethod(); + } +} + +?> +----- +newMethod(); + } +} + +?> diff --git a/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/skip_other_class.php.inc b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/skip_other_class.php.inc new file mode 100644 index 00000000000..9e6a6690fb4 --- /dev/null +++ b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/skip_other_class.php.inc @@ -0,0 +1,12 @@ +oldMethod(); + } +} diff --git a/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/skip_unknown_variable.php.inc b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/skip_unknown_variable.php.inc new file mode 100644 index 00000000000..64219656bc2 --- /dev/null +++ b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/skip_unknown_variable.php.inc @@ -0,0 +1,11 @@ +oldMethod(); + } +} diff --git a/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/typed_param.php.inc b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/typed_param.php.inc new file mode 100644 index 00000000000..f203d30d887 --- /dev/null +++ b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/Fixture/typed_param.php.inc @@ -0,0 +1,27 @@ +oldMethod(); + } +} + +?> +----- +newMethod(); + } +} + +?> diff --git a/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/config/configured_rule.php b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/config/configured_rule.php new file mode 100644 index 00000000000..fcb87975289 --- /dev/null +++ b/rules-tests/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(DemoObjectMethodCallRenameRector::class); +}; diff --git a/rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector.php b/rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector.php index 50ad208a46a..acf657f6102 100644 --- a/rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector.php +++ b/rules/DeadCode/Rector/ClassMethod/RemoveParentDelegatingClassMethodRector.php @@ -4,7 +4,6 @@ namespace Rector\DeadCode\Rector\ClassMethod; -use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode; use PhpParser\Node; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\Variable; @@ -13,6 +12,7 @@ use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Return_; use PhpParser\NodeVisitor; +use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; @@ -117,7 +117,7 @@ public function refactor(Node $node): ?int private function hasRefiningDocblock(ClassMethod $classMethod): bool { $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod); - return array_any($phpDocInfo->getPhpDocNode()->children, fn(PhpDocChildNode $phpDocChildNode): bool => $phpDocChildNode instanceof PhpDocTagNode); + return array_any($phpDocInfo->getPhpDocNode()->children, fn (PhpDocChildNode $phpDocChildNode): bool => $phpDocChildNode instanceof PhpDocTagNode); } private function matchParentMethodReflection(ClassMethod $classMethod): ?ExtendedMethodReflection diff --git a/rules/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector.php b/rules/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector.php new file mode 100644 index 00000000000..88fc952d74e --- /dev/null +++ b/rules/Simple/Rector/ClassMethod/DemoObjectMethodCallRenameRector.php @@ -0,0 +1,115 @@ +oldMethod(); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +$dateTime = new DateTime(); +$dateTime->newMethod(); +CODE_SAMPLE + ), + ] + ); + } + + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + $stmts = $node->stmts; + if ($stmts === null) { + return null; + } + + $simpleScope = $this->simpleScopeResolver->resolve([$node]); + + $hasChanged = false; + $this->traverseNodesWithCallable($stmts, function (Node $subNode) use ($simpleScope, &$hasChanged): null { + if ($this->refactorMethodCall($subNode, $simpleScope)) { + $hasChanged = true; + } + + return null; + }); + + if (! $hasChanged) { + return null; + } + + return $node; + } + + private function refactorMethodCall(Node $node, SimpleScope $simpleScope): bool + { + if (! $node instanceof MethodCall) { + return false; + } + + if ($node->isFirstClassCallable()) { + return false; + } + + if (! $this->isName($node->name, self::OLD_METHOD)) { + return false; + } + + $simpleType = $simpleScope->getType($node->var); + if (! $simpleType instanceof ObjectType) { + return false; + } + + if ($simpleType->getClassName() !== self::TARGET_CLASS) { + return false; + } + + $node->name = new Identifier(self::NEW_METHOD); + + return true; + } +} diff --git a/src/SimpleScope/SimpleScope.php b/src/SimpleScope/SimpleScope.php new file mode 100644 index 00000000000..5cc84159a05 --- /dev/null +++ b/src/SimpleScope/SimpleScope.php @@ -0,0 +1,80 @@ + + */ + private array $variableTypes = []; + + public function setVariableType(string $name, SimpleTypeInterface $simpleType): void + { + $this->variableTypes[$name] = $simpleType; + } + + public function getType(Expr $expr): SimpleTypeInterface + { + if ($expr instanceof String_) { + return new StringType(); + } + + if ($expr instanceof Int_) { + return new IntegerType(); + } + + if ($expr instanceof Array_) { + return new ArrayType(); + } + + if ($expr instanceof ConstFetch) { + return $this->resolveConstFetchType($expr); + } + + if ($expr instanceof New_ && $expr->class instanceof Name) { + return new ObjectType($expr->class->toString()); + } + + if ($expr instanceof Variable && is_string($expr->name)) { + return $this->variableTypes[$expr->name] ?? new MixedType(); + } + + return new MixedType(); + } + + private function resolveConstFetchType(ConstFetch $constFetch): SimpleTypeInterface + { + $constantName = strtolower($constFetch->name->toString()); + + if ($constantName === 'null') { + return new NullType(); + } + + if ($constantName === 'true' || $constantName === 'false') { + return new BooleanType(); + } + + return new MixedType(); + } +} diff --git a/src/SimpleScope/SimpleScopeResolver.php b/src/SimpleScope/SimpleScopeResolver.php new file mode 100644 index 00000000000..4d7dc642fb7 --- /dev/null +++ b/src/SimpleScope/SimpleScopeResolver.php @@ -0,0 +1,88 @@ +nodeFinder = new NodeFinder(); + } + + /** + * @param Node[] $stmts + */ + public function resolve(array $stmts): SimpleScope + { + $simpleScope = new SimpleScope(); + + foreach ($this->nodeFinder->findInstanceOf($stmts, Param::class) as $param) { + $this->seedParam($simpleScope, $param); + } + + foreach ($this->nodeFinder->findInstanceOf($stmts, Assign::class) as $assign) { + if (! $assign->var instanceof Variable || ! is_string($assign->var->name)) { + continue; + } + + $simpleScope->setVariableType($assign->var->name, $simpleScope->getType($assign->expr)); + } + + return $simpleScope; + } + + private function seedParam(SimpleScope $simpleScope, Param $param): void + { + if (! $param->var instanceof Variable || ! is_string($param->var->name)) { + return; + } + + $paramType = $this->resolveParamType($param); + if (! $paramType instanceof SimpleTypeInterface) { + return; + } + + $simpleScope->setVariableType($param->var->name, $paramType); + } + + private function resolveParamType(Param $param): ?SimpleTypeInterface + { + if ($param->type instanceof Name) { + return new ObjectType($param->type->toString()); + } + + if (! $param->type instanceof Identifier) { + return null; + } + + return match ($param->type->toLowerString()) { + 'string' => new StringType(), + 'int' => new IntegerType(), + 'bool' => new BooleanType(), + 'array' => new ArrayType(), + default => null, + }; + } +} diff --git a/src/SimpleType/ArrayType.php b/src/SimpleType/ArrayType.php new file mode 100644 index 00000000000..edea4c8a019 --- /dev/null +++ b/src/SimpleType/ArrayType.php @@ -0,0 +1,15 @@ +className; + } + + public function getClassName(): string + { + return $this->className; + } +} diff --git a/src/SimpleType/StringType.php b/src/SimpleType/StringType.php new file mode 100644 index 00000000000..0940c8e60b8 --- /dev/null +++ b/src/SimpleType/StringType.php @@ -0,0 +1,15 @@ +simpleScopeResolver = new SimpleScopeResolver(); + } + + public function testResolvesNewAssignToObjectType(): void + { + $simpleScope = $this->resolveCode(<<<'PHP' +assertSame('DateTime', $simpleScope->getType(new Variable('dateTime'))->describe()); + } + + public function testResolvesTypedParam(): void + { + $simpleScope = $this->resolveCode(<<<'PHP' +assertSame('string', $simpleScope->getType(new Variable('name'))->describe()); + } + + public function testUnknownVariableIsMixed(): void + { + $simpleScope = $this->resolveCode(<<<'PHP' +assertSame('mixed', $simpleScope->getType(new Variable('missing'))->describe()); + } + + public function testResolvesLiteralType(): void + { + $simpleScope = $this->resolveCode('assertSame('string', $simpleScope->getType(new String_('hello'))->describe()); + } + + private function resolveCode(string $code): SimpleScope + { + $parser = new ParserFactory()->createForNewestSupportedVersion(); + $stmts = $parser->parse($code); + + return $this->simpleScopeResolver->resolve($stmts ?? []); + } +}