diff --git a/rules-tests/Php70/Rector/Ternary/TernaryToNullCoalescingRector/Fixture/is_null_function.php.inc b/rules-tests/Php70/Rector/Ternary/TernaryToNullCoalescingRector/Fixture/is_null_function.php.inc new file mode 100644 index 00000000000..b8f36c9c646 --- /dev/null +++ b/rules-tests/Php70/Rector/Ternary/TernaryToNullCoalescingRector/Fixture/is_null_function.php.inc @@ -0,0 +1,29 @@ + +----- + diff --git a/rules-tests/Php70/Rector/Ternary/TernaryToNullCoalescingRector/Fixture/is_null_with_parentheses.php.inc b/rules-tests/Php70/Rector/Ternary/TernaryToNullCoalescingRector/Fixture/is_null_with_parentheses.php.inc new file mode 100644 index 00000000000..7dc5b204a5b --- /dev/null +++ b/rules-tests/Php70/Rector/Ternary/TernaryToNullCoalescingRector/Fixture/is_null_with_parentheses.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/rules/Php70/Rector/Ternary/TernaryToNullCoalescingRector.php b/rules/Php70/Rector/Ternary/TernaryToNullCoalescingRector.php index 681fc708e99..5c4f6291486 100644 --- a/rules/Php70/Rector/Ternary/TernaryToNullCoalescingRector.php +++ b/rules/Php70/Rector/Ternary/TernaryToNullCoalescingRector.php @@ -5,11 +5,14 @@ namespace Rector\Php70\Rector\Ternary; use PhpParser\Node; +use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp; use PhpParser\Node\Expr\BinaryOp\Coalesce; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\BinaryOp\NotIdentical; +use PhpParser\Node\Expr\BooleanNot; +use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Isset_; use PhpParser\Node\Expr\Ternary; use Rector\NodeTypeResolver\Node\AttributeKey; @@ -38,6 +41,7 @@ public function getRuleDefinition(): RuleDefinition [ new CodeSample('$value === null ? 10 : $value;', '$value ?? 10;'), new CodeSample('isset($value) ? $value : 10;', '$value ?? 10;'), + new CodeSample('is_null($value) ? 10 : $value;', '$value ?? 10;'), ] ); } @@ -59,6 +63,17 @@ public function refactor(Node $node): ?Node return $this->processTernaryWithIsset($node, $node->cond); } + if ($node->cond instanceof FuncCall && $this->isName($node->cond, 'is_null')) { + return $this->processTernaryWithIsNull($node, $node->cond, false); + } + + if ( + $node->cond instanceof BooleanNot && $node->cond->expr instanceof FuncCall + && $this->isName($node->cond->expr, 'is_null') + ) { + return $this->processTernaryWithIsNull($node, $node->cond->expr, true); + } + if ($node->cond instanceof Identical) { $checkedNode = $node->else; $fallbackNode = $node->if; @@ -80,11 +95,11 @@ public function refactor(Node $node): ?Node $ternaryCompareNode = $node->cond; if ($this->isNullMatch($ternaryCompareNode->left, $ternaryCompareNode->right, $checkedNode)) { - return new Coalesce($checkedNode, $fallbackNode); + return $this->createCoalesce($checkedNode, $fallbackNode); } if ($this->isNullMatch($ternaryCompareNode->right, $ternaryCompareNode->left, $checkedNode)) { - return new Coalesce($checkedNode, $fallbackNode); + return $this->createCoalesce($checkedNode, $fallbackNode); } return null; @@ -95,6 +110,46 @@ public function provideMinPhpVersion(): int return PhpVersionFeature::NULL_COALESCE; } + private function processTernaryWithIsNull(Ternary $ternary, FuncCall $isNullFuncCall, bool $isNegated): ?Coalesce + { + if (count($isNullFuncCall->args) !== 1) { + return null; + } + + $firstArg = $isNullFuncCall->args[0]; + if (! $firstArg instanceof Arg) { + return null; + } + + $checkedExpr = $firstArg->value; + + if ($isNegated) { + if (! $ternary->if instanceof Expr) { + return null; + } + + if (! $this->nodeComparator->areNodesEqual($ternary->if, $checkedExpr)) { + return null; + } + + $this->preserveWrappedFallback($ternary->else); + + return $this->createCoalesce($ternary->if, $ternary->else); + } + + if (! $this->nodeComparator->areNodesEqual($ternary->else, $checkedExpr)) { + return null; + } + + if (! $ternary->if instanceof Expr) { + return null; + } + + $this->preserveWrappedFallback($ternary->if); + + return $this->createCoalesce($ternary->else, $ternary->if); + } + private function processTernaryWithIsset(Ternary $ternary, Isset_ $isset): ?Coalesce { if (! $ternary->if instanceof Expr) { @@ -122,7 +177,7 @@ private function processTernaryWithIsset(Ternary $ternary, Isset_ $isset): ?Coal $ternary->else->setAttribute(AttributeKey::WRAPPED_IN_PARENTHESES, true); } - return new Coalesce($ternary->if, $ternary->else); + return $this->createCoalesce($ternary->if, $ternary->else); } private function isTernaryParenthesized(File $file, Expr $expr, Ternary $ternary): bool @@ -163,4 +218,45 @@ private function isNullMatch(Expr $possibleNullExpr, Expr $firstNode, Expr $seco return $this->nodeComparator->areNodesEqual($firstNode, $secondNode); } + + private function createCoalesce(Expr $checkedExpr, Expr $fallbackExpr): Coalesce + { + if ($this->isExprParenthesized($this->getFile(), $checkedExpr)) { + $checkedExpr->setAttribute(AttributeKey::WRAPPED_IN_PARENTHESES, true); + } + + return new Coalesce($checkedExpr, $fallbackExpr); + } + + private function preserveWrappedFallback(Expr $expr): void + { + if (! $expr instanceof BinaryOp && ! $expr instanceof Ternary) { + return; + } + + if (! $this->isExprParenthesized($this->getFile(), $expr)) { + return; + } + + $expr->setAttribute(AttributeKey::WRAPPED_IN_PARENTHESES, true); + } + + private function isExprParenthesized(File $file, Expr $expr): bool + { + $oldTokens = $file->getOldTokens(); + $startTokenPos = $expr->getStartTokenPos(); + $endTokenPos = $expr->getEndTokenPos(); + + while (isset($oldTokens[$startTokenPos - 1]) && trim((string) $oldTokens[$startTokenPos - 1]) === '') { + --$startTokenPos; + } + + while (isset($oldTokens[$endTokenPos + 1]) && trim((string) $oldTokens[$endTokenPos + 1]) === '') { + ++$endTokenPos; + } + + return isset($oldTokens[$startTokenPos - 1], $oldTokens[$endTokenPos + 1]) + && (string) $oldTokens[$startTokenPos - 1] === '(' + && (string) $oldTokens[$endTokenPos + 1] === ')'; + } }