diff --git a/build/target-repository/docs/rector_rules_overview.md b/build/target-repository/docs/rector_rules_overview.md
index 8915de87d35..52768d7dae9 100644
--- a/build/target-repository/docs/rector_rules_overview.md
+++ b/build/target-repository/docs/rector_rules_overview.md
@@ -1,4 +1,4 @@
-# 414 Rules Overview
+# 415 Rules Overview
@@ -8,7 +8,7 @@
- [CodeQuality](#codequality) (78)
-- [CodingStyle](#codingstyle) (39)
+- [CodingStyle](#codingstyle) (40)
- [Compatibility](#compatibility) (1)
@@ -1946,6 +1946,31 @@ Type and name of catch exception should match
+### CleanupUnneededNullsafeOperatorRector
+
+Cleanup unneeded nullsafe operator
+
+- class: [`Rector\CodingStyle\Rector\NullsafeMethodCall\CleanupUnneededNullsafeOperatorRector`](../rules/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector.php)
+
+```diff
+ class HelloWorld {
+ public function getString(): string
+ {
+ return 'hello world';
+ }
+ }
+
+ public function get(): HelloWorld
+ {
+ return new HelloWorld();
+ }
+
+-echo $this->get()?->getHelloWorld();
++echo $this->get()->getHelloWorld();
+```
+
+
+
### ConsistentImplodeRector
Changes various implode forms to consistent one
diff --git a/config/set/code-quality.php b/config/set/code-quality.php
index 0dbeb96a60e..7d43cd8518f 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;
@@ -83,6 +82,7 @@
use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector;
use Rector\CodingStyle\Rector\FuncCall\CallUserFuncToMethodCallRector;
use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector;
+use Rector\CodingStyle\Rector\NullsafeMethodCall\CleanupUnneededNullsafeOperatorRector;
use Rector\Config\RectorConfig;
use Rector\Php52\Rector\Property\VarToPublicPropertyRector;
use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector;
@@ -199,5 +199,6 @@
TernaryEmptyArrayArrayDimFetchToCoalesceRector::class,
OptionalParametersAfterRequiredRector::class,
SimplifyEmptyCheckOnEmptyArrayRector::class,
+ CleanupUnneededNullsafeOperatorRector::class,
]);
};
diff --git a/config/set/php80.php b/config/set/php80.php
index 7f8de75376f..a7fe67c20f8 100644
--- a/config/set/php80.php
+++ b/config/set/php80.php
@@ -3,7 +3,6 @@
declare(strict_types=1);
use Rector\Arguments\Rector\ClassMethod\ArgumentAdderRector;
-
use Rector\Arguments\Rector\FuncCall\FunctionArgumentDefaultValueReplacerRector;
use Rector\Arguments\ValueObject\ArgumentAdder;
use Rector\Arguments\ValueObject\ReplaceFuncCallArgumentDefaultValue;
diff --git a/rules-tests/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/CleanupUnneededNullsafeOperatorRectorTest.php b/rules-tests/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/CleanupUnneededNullsafeOperatorRectorTest.php
new file mode 100644
index 00000000000..406af628540
--- /dev/null
+++ b/rules-tests/CodingStyle/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/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace.php.inc b/rules-tests/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace.php.inc
new file mode 100644
index 00000000000..b881aa514bb
--- /dev/null
+++ b/rules-tests/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace.php.inc
@@ -0,0 +1,41 @@
+get()?->getReplace();
+
+?>
+-----
+get()->getReplace();
+
+?>
diff --git a/rules-tests/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace_with_invalid_doc.php.inc b/rules-tests/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace_with_invalid_doc.php.inc
new file mode 100644
index 00000000000..1e49058b1c2
--- /dev/null
+++ b/rules-tests/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/replace_with_invalid_doc.php.inc
@@ -0,0 +1,47 @@
+get()?->getReplaceWithInvalidDoc();
+
+?>
+-----
+get()->getReplaceWithInvalidDoc();
+
+?>
diff --git a/rules-tests/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/skip.php.inc b/rules-tests/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/skip.php.inc
new file mode 100644
index 00000000000..2910e3a9699
--- /dev/null
+++ b/rules-tests/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/Fixture/skip.php.inc
@@ -0,0 +1,20 @@
+get()?->getReplace();
+
+?>
diff --git a/rules-tests/Php80/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/config/configured_rule.php b/rules-tests/Php80/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/config/configured_rule.php
new file mode 100644
index 00000000000..7b273257dd5
--- /dev/null
+++ b/rules-tests/Php80/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector/config/configured_rule.php
@@ -0,0 +1,10 @@
+rule(CleanupUnneededNullsafeOperatorRector::class);
+};
diff --git a/rules/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector.php b/rules/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector.php
new file mode 100644
index 00000000000..c1059dc7020
--- /dev/null
+++ b/rules/CodingStyle/Rector/NullsafeMethodCall/CleanupUnneededNullsafeOperatorRector.php
@@ -0,0 +1,105 @@
+get()?->getHelloWorld();
+CODE_SAMPLE
+ ,
+ <<<'CODE_SAMPLE'
+class HelloWorld {
+ public function getString(): string
+ {
+ return 'hello world';
+ }
+}
+
+public function get(): HelloWorld
+{
+ return new HelloWorld();
+}
+
+echo $this->get()->getHelloWorld();
+CODE_SAMPLE
+ ),
+ ]
+ );
+ }
+
+ /**
+ * @return array>
+ */
+ public function getNodeTypes(): array
+ {
+ return [NullsafeMethodCall::class];
+ }
+
+ /**
+ * @param NullsafeMethodCall $node
+ */
+ public function refactor(Node $node): ?Node
+ {
+ if (!$node->var instanceof MethodCall) {
+ return null;
+ }
+
+ $returnNode = $this->returnStrictTypeAnalyzer->resolveMethodCallReturnNode($node->var);
+
+ if (null === $returnNode) {
+ return null;
+ }
+
+ // Remove not needed Nullsafe for method call.
+ $node = $node->var;
+
+ return $node;
+ }
+
+ public function provideMinPhpVersion(): int
+ {
+ return PhpVersionFeature::NULLSAFE_OPERATOR;
+ }
+}
diff --git a/rules/TypeDeclaration/TypeAnalyzer/ReturnStrictTypeAnalyzer.php b/rules/TypeDeclaration/TypeAnalyzer/ReturnStrictTypeAnalyzer.php
index 7346885c0f8..2bcb5e49579 100644
--- a/rules/TypeDeclaration/TypeAnalyzer/ReturnStrictTypeAnalyzer.php
+++ b/rules/TypeDeclaration/TypeAnalyzer/ReturnStrictTypeAnalyzer.php
@@ -59,7 +59,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 d0e71f75b5d..b8d4f356704 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
*/