diff --git a/build/target-repository/docs/rector_rules_overview.md b/build/target-repository/docs/rector_rules_overview.md
index d31902d9a69..e29b393904e 100644
--- a/build/target-repository/docs/rector_rules_overview.md
+++ b/build/target-repository/docs/rector_rules_overview.md
@@ -6,9 +6,9 @@
- [Arguments](#arguments) (6)
-- [CodeQuality](#codequality) (77)
+- [CodeQuality](#codequality) (78)
-- [CodingStyle](#codingstyle) (38)
+- [CodingStyle](#codingstyle) (37)
- [Compatibility](#compatibility) (1)
@@ -528,6 +528,31 @@ Change `array_push()` to direct variable assign
+### CleanupUnneededNullsafeOperatorRector
+
+Cleanup unneeded nullsafe operator
+
+- class: [`Rector\CodeQuality\Rector\NullsafeMethodCall\CleanupUnneededNullsafeOperatorRector`](../rules/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector.php)
+
+```diff
+ class HelloWorld {
+ public function getString(): string
+ {
+ return 'hello world';
+ }
+ }
+
+ function get(): HelloWorld
+ {
+ return new HelloWorld();
+ }
+
+-echo get()?->getString();
++echo get()->getString();
+```
+
+
+
### CombineIfRector
Merges nested if statements
@@ -2357,26 +2382,6 @@ return static function (RectorConfig $rectorConfig): void {
-### RemoveDoubleUnderscoreInMethodNameRector
-
-Non-magic PHP object methods cannot start with "__"
-
-- class: [`Rector\CodingStyle\Rector\ClassMethod\RemoveDoubleUnderscoreInMethodNameRector`](../rules/CodingStyle/Rector/ClassMethod/RemoveDoubleUnderscoreInMethodNameRector.php)
-
-```diff
- class SomeClass
- {
-- public function __getName($anotherObject)
-+ public function getName($anotherObject)
- {
-- $anotherObject->__getSurname();
-+ $anotherObject->getSurname();
- }
- }
-```
-
-
-
### RemoveFinalFromConstRector
Remove final from constants in classes defined as final
diff --git a/config/set/code-quality.php b/config/set/code-quality.php
index 19a1bd1869f..80a38211a52 100644
--- a/config/set/code-quality.php
+++ b/config/set/code-quality.php
@@ -3,7 +3,6 @@
declare(strict_types=1);
use Rector\CodeQuality\Rector\Array_\CallableThisArrayToAnonymousFunctionRector;
-
use Rector\CodeQuality\Rector\Assign\CombinedAssignRector;
use Rector\CodeQuality\Rector\Assign\SplitListAssignToSeparateLineRector;
use Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector;
@@ -82,6 +81,7 @@
use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector;
use Rector\CodingStyle\Rector\FuncCall\CallUserFuncToMethodCallRector;
use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector;
+use Rector\CodeQuality\Rector\NullsafeMethodCall\CleanupUnneededNullsafeOperatorRector;
use Rector\Config\RectorConfig;
use Rector\Php52\Rector\Property\VarToPublicPropertyRector;
use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector;
@@ -197,5 +197,6 @@
OptionalParametersAfterRequiredRector::class,
SimplifyEmptyCheckOnEmptyArrayRector::class,
SwitchTrueToIfRector::class,
+ CleanupUnneededNullsafeOperatorRector::class,
]);
};
diff --git a/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/CleanupUnneededNullsafeOperatorRectorTest.php b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/CleanupUnneededNullsafeOperatorRectorTest.php
new file mode 100644
index 00000000000..ff96658dabd
--- /dev/null
+++ b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/CleanupUnneededNullsafeOperatorRectorTest.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/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace.php.inc b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace.php.inc
new file mode 100644
index 00000000000..01a7155b5e6
--- /dev/null
+++ b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace.php.inc
@@ -0,0 +1,41 @@
+getString();
+
+?>
+-----
+getString();
+
+?>
diff --git a/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace_on_method_call.php.inc b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace_on_method_call.php.inc
new file mode 100644
index 00000000000..ed0e4088803
--- /dev/null
+++ b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace_on_method_call.php.inc
@@ -0,0 +1,47 @@
+get2()?->getString2();
+
+?>
+-----
+get2()->getString2();
+
+?>
diff --git a/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace_with_invalid_doc.php.inc b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace_with_invalid_doc.php.inc
new file mode 100644
index 00000000000..dc5aaef5838
--- /dev/null
+++ b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace_with_invalid_doc.php.inc
@@ -0,0 +1,47 @@
+getString();
+
+?>
+-----
+getString();
+
+?>
diff --git a/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/skip_nullable.php.inc b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/skip_nullable.php.inc
new file mode 100644
index 00000000000..3faf89a876c
--- /dev/null
+++ b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/skip_nullable.php.inc
@@ -0,0 +1,20 @@
+getString();
+
+?>
diff --git a/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/config/configured_rule.php b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/config/configured_rule.php
new file mode 100644
index 00000000000..8df188a1f29
--- /dev/null
+++ b/rules-tests/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/config/configured_rule.php
@@ -0,0 +1,10 @@
+rule(CleanupUnneededNullsafeOperatorRector::class);
+};
diff --git a/rules/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector.php b/rules/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector.php
new file mode 100644
index 00000000000..146a73a86e2
--- /dev/null
+++ b/rules/CodeQuality/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector.php
@@ -0,0 +1,114 @@
+getString();
+CODE_SAMPLE
+ ,
+ <<<'CODE_SAMPLE'
+class HelloWorld {
+ public function getString(): string
+ {
+ return 'hello world';
+ }
+}
+
+function get(): HelloWorld
+{
+ return new HelloWorld();
+}
+
+echo get()->getString();
+CODE_SAMPLE
+ ),
+ ]
+ );
+ }
+
+ /**
+ * @return array>
+ */
+ public function getNodeTypes(): array
+ {
+ return [NullsafeMethodCall::class];
+ }
+
+ /**
+ * @param NullsafeMethodCall $node
+ */
+ public function refactor(Node $node): ?Node
+ {
+ if (! $node->name instanceof Identifier) {
+ return null;
+ }
+
+ if (! $node->var instanceof FuncCall && ! $node->var instanceof MethodCall && ! $node->var instanceof StaticCall) {
+ return null;
+ }
+
+ $returnNode = $this->returnStrictTypeAnalyzer->resolveMethodCallReturnNode($node->var);
+
+ if (! $returnNode instanceof Node) {
+ return null;
+ }
+
+ $type = $this->getType($returnNode);
+ if (! $type instanceof FullyQualifiedObjectType) {
+ return null;
+ }
+
+ return new MethodCall($node->var, $node->name, $node->args);
+ }
+
+ public function provideMinPhpVersion(): int
+ {
+ return PhpVersionFeature::NULLSAFE_OPERATOR;
+ }
+}
diff --git a/rules/TypeDeclaration/TypeAnalyzer/ReturnStrictTypeAnalyzer.php b/rules/TypeDeclaration/TypeAnalyzer/ReturnStrictTypeAnalyzer.php
index 34d51051d89..37a0ad85f30 100644
--- a/rules/TypeDeclaration/TypeAnalyzer/ReturnStrictTypeAnalyzer.php
+++ b/rules/TypeDeclaration/TypeAnalyzer/ReturnStrictTypeAnalyzer.php
@@ -60,7 +60,7 @@ public function collectStrictReturnTypes(array $returns): array
return $this->typeNodeUnwrapper->uniquateNodes($returnedStrictTypeNodes);
}
- private function resolveMethodCallReturnNode(MethodCall | StaticCall | FuncCall $call): ?Node
+ public function resolveMethodCallReturnNode(MethodCall | StaticCall | FuncCall $call): ?Node
{
$methodReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($call);
if ($methodReflection === null) {
diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php
index 78853a62cd9..eedfca164f9 100644
--- a/src/ValueObject/PhpVersionFeature.php
+++ b/src/ValueObject/PhpVersionFeature.php
@@ -433,6 +433,11 @@ final class PhpVersionFeature
*/
public const STATIC_VISIBILITY_SET_STATE = PhpVersion::PHP_80;
+ /**
+ * @var int
+ */
+ public const NULLSAFE_OPERATOR = PhpVersion::PHP_80;
+
/**
* @var int
*/