diff --git a/config/level/php/php70.yml b/config/level/php/php70.yml index dcab44d209e..05b3d934672 100644 --- a/config/level/php/php70.yml +++ b/config/level/php/php70.yml @@ -1,2 +1,3 @@ services: Rector\Php\Rector\ExceptionHandlerTypehintRector: ~ + Rector\Php\Rector\TernaryToNullCoalescingRector: ~ diff --git a/packages/Php/src/Rector/ExceptionHandlerTypehintRector.php b/packages/Php/src/Rector/ExceptionHandlerTypehintRector.php index c98ab544cd4..0e2f948066f 100644 --- a/packages/Php/src/Rector/ExceptionHandlerTypehintRector.php +++ b/packages/Php/src/Rector/ExceptionHandlerTypehintRector.php @@ -12,7 +12,6 @@ use Rector\RectorDefinition\CodeSample; use Rector\RectorDefinition\RectorDefinition; /** - * @group php7 * @source https://wiki.php.net/rfc/typed_properties_v2#proposal */ final class ExceptionHandlerTypehintRector extends AbstractRector diff --git a/packages/Php/src/Rector/TernaryToNullCoalescingRector.php b/packages/Php/src/Rector/TernaryToNullCoalescingRector.php new file mode 100644 index 00000000000..c1e45f3f4ba --- /dev/null +++ b/packages/Php/src/Rector/TernaryToNullCoalescingRector.php @@ -0,0 +1,127 @@ +betterStandardPrinter = $betterStandardPrinter; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition( + 'Changes unneeded null check to ?? operator', + [ + new CodeSample('$value === null ? 10 : $value;', '$value ?? 10;'), + new CodeSample('isset($value) ? $value : 10;', '$value ?? 10;'), + ] + ); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Ternary::class]; + } + + /** + * @param Ternary $ternaryNode + */ + public function refactor(Node $ternaryNode): ?Node + { + if ($ternaryNode->cond instanceof Isset_) { + $coalesceNode = $this->processTernaryWithIsset($ternaryNode); + if ($coalesceNode) { + return $coalesceNode; + } + } + + if ($ternaryNode->cond instanceof Identical) { + [$checkedNode, $fallbackNode] = [$ternaryNode->else, $ternaryNode->if]; + } elseif ($ternaryNode->cond instanceof NotIdentical) { + [$checkedNode, $fallbackNode] = [$ternaryNode->if, $ternaryNode->else]; + } else { + // not a match + return $ternaryNode; + } + + /** @var Identical|NotIdentical $ternaryCompareNode */ + $ternaryCompareNode = $ternaryNode->cond; + + if ($this->isNullMatch($ternaryCompareNode->left, $ternaryCompareNode->right, $checkedNode)) { + return new Coalesce($checkedNode, $fallbackNode); + } + + if ($this->isNullMatch($ternaryCompareNode->right, $ternaryCompareNode->left, $checkedNode)) { + return new Coalesce($checkedNode, $fallbackNode); + } + + return $ternaryNode; + } + + private function processTernaryWithIsset(Ternary $ternaryNode): ?Coalesce + { + /** @var Isset_ $issetNode */ + $issetNode = $ternaryNode->cond; + + // none or multiple isset values cannot be handled here + if (! isset($issetNode->vars[0]) || count($issetNode->vars) > 1) { + return null; + } + + if ($ternaryNode->if === null) { + return null; + } + + $ifContent = $this->betterStandardPrinter->prettyPrint([$ternaryNode->if]); + $varNodeContent = $this->betterStandardPrinter->prettyPrint([$issetNode->vars[0]]); + + if ($ifContent === $varNodeContent) { + return new Coalesce($ternaryNode->if, $ternaryNode->else); + } + + return null; + } + + private function isNullMatch(Node $possibleNullNode, Node $firstNode, Node $secondNode): bool + { + if (! $this->isNullConstant($possibleNullNode)) { + return false; + } + + $firstNodeContent = $this->betterStandardPrinter->prettyPrint([$firstNode]); + $secondNodeContent = $this->betterStandardPrinter->prettyPrint([$secondNode]); + + return $firstNodeContent === $secondNodeContent; + } + + private function isNullConstant(Node $node): bool + { + if (! $node instanceof ConstFetch) { + return false; + } + + return $node->name->toLowerString() === 'null'; + } +} diff --git a/packages/Php/tests/Rector/TernaryToNullCoalescingRector/Correct/correct.php.inc b/packages/Php/tests/Rector/TernaryToNullCoalescingRector/Correct/correct.php.inc new file mode 100644 index 00000000000..f7fe3001eab --- /dev/null +++ b/packages/Php/tests/Rector/TernaryToNullCoalescingRector/Correct/correct.php.inc @@ -0,0 +1,15 @@ +${'a'}[0]->$$b[1][2]::$c[3][4][5]->xxx->{" $d"} ?? 0; + +$j = $this->${'a'}[0]->$$b[1][2]::$c[3][4][5]->xxx->{" $d"} ?? false; + +$k = $this + ->${'a'}[0] + ->$$b[1][2] + ::$c[3][4][5] + ->{" $d"} ?? true; + +$l = \Whatever\Something::$anything ?? 1; + +$m = $object->anything ?? 0; + +$n = ($something ?? false); + +$o[$something ?? true] = true; + +$p = doSomething()() ?? false; diff --git a/packages/Php/tests/Rector/TernaryToNullCoalescingRector/TernaryToNullCoalescingRectorTest.php b/packages/Php/tests/Rector/TernaryToNullCoalescingRector/TernaryToNullCoalescingRectorTest.php new file mode 100644 index 00000000000..f915625c16c --- /dev/null +++ b/packages/Php/tests/Rector/TernaryToNullCoalescingRector/TernaryToNullCoalescingRectorTest.php @@ -0,0 +1,37 @@ +doTestFileMatchesExpectedContent($wrong, $fixed); + } + + public function provideWrongToFixedFiles(): Iterator + { + yield [__DIR__ . '/Wrong/wrong.php.inc', __DIR__ . '/Correct/correct.php.inc']; + yield [__DIR__ . '/Wrong/wrong2.php.inc', __DIR__ . '/Correct/correct2.php.inc']; + yield [__DIR__ . '/Wrong/wrong3.php.inc', __DIR__ . '/Correct/correct3.php.inc']; + yield [__DIR__ . '/Wrong/wrong4.php.inc', __DIR__ . '/Correct/correct4.php.inc']; + } + + protected function provideConfig(): string + { + return __DIR__ . '/config.yml'; + } +} diff --git a/packages/Php/tests/Rector/TernaryToNullCoalescingRector/Wrong/wrong.php.inc b/packages/Php/tests/Rector/TernaryToNullCoalescingRector/Wrong/wrong.php.inc new file mode 100644 index 00000000000..48f802853b9 --- /dev/null +++ b/packages/Php/tests/Rector/TernaryToNullCoalescingRector/Wrong/wrong.php.inc @@ -0,0 +1,15 @@ +${'a'}[0]->$$b[1][2]::$c[3][4][5]->xxx->{" $d"} !== null ? $this->${'a'}[0]->$$b[1][2]::$c[3][4][5]->xxx->{" $d"} : 0; + +$j = $this->${'a'}[0]->$$b[1][2]::$c[3][4][5]->xxx->{" $d"} === null ? false : $this->${'a'}[0]->$$b[1][2]::$c[3][4][5]->xxx->{" $d"}; + +$k = $this + ->${'a'}[0]->$$b[1][2] + ::$c[3][4][5]->{" $d"} !== null + ? $this + ->${'a'}[0] + ->$$b[1][2] + ::$c[3][4][5] + ->{" $d"} + : true; + +$l = \Whatever\Something::$anything !== null ? \Whatever\Something::$anything : 1; + +$m = $object->anything === null ? 0 : $object->anything; + +$n = ($something === null ? false : $something); + +$o[$something === null ? true : $something] = true; + +$p = doSomething()() === null ? false : doSomething()(); diff --git a/packages/Php/tests/Rector/TernaryToNullCoalescingRector/config.yml b/packages/Php/tests/Rector/TernaryToNullCoalescingRector/config.yml new file mode 100644 index 00000000000..4de44e435d3 --- /dev/null +++ b/packages/Php/tests/Rector/TernaryToNullCoalescingRector/config.yml @@ -0,0 +1,2 @@ +services: + Rector\Php\Rector\TernaryToNullCoalescingRector: ~