Skip to content

Commit f8f3487

Browse files
samsonasikSoean
andauthored
[Php70] Allow is_null() conversion on TernaryToNullCoalescingRector with parentheses handling (#7969)
* feat: add support for converting is_null ternaries to null coalescing operator * Fix errors * fix parenthesized * cs * trigger CI --------- Co-authored-by: Sören Wünsch <soeren.wrede@parshipgroup.com>
1 parent 84fe920 commit f8f3487

File tree

3 files changed

+153
-3
lines changed

3 files changed

+153
-3
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php70\Rector\Ternary\TernaryToNullCoalescingRector\Fixture;
4+
5+
function ternaryWithIsNull()
6+
{
7+
$x = is_null($value) ? 10 : $value;
8+
9+
$y = is_null($a) ? $b : $a;
10+
11+
$z = !is_null($value) ? $value : 10;
12+
}
13+
14+
?>
15+
-----
16+
<?php
17+
18+
namespace Rector\Tests\Php70\Rector\Ternary\TernaryToNullCoalescingRector\Fixture;
19+
20+
function ternaryWithIsNull()
21+
{
22+
$x = $value ?? 10;
23+
24+
$y = $a ?? $b;
25+
26+
$z = $value ?? 10;
27+
}
28+
29+
?>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php70\Rector\Ternary\TernaryToNullCoalescingRector\Fixture;
4+
5+
function isNullWithParentheses()
6+
{
7+
$w = !is_null($c = $x) ? ($c = $x) : $d;
8+
$w = is_null($c = $x) ? $d : ($c = $x);
9+
$w = !is_null($c) ? $c : ($a + $b);
10+
}
11+
12+
?>
13+
-----
14+
<?php
15+
16+
namespace Rector\Tests\Php70\Rector\Ternary\TernaryToNullCoalescingRector\Fixture;
17+
18+
function isNullWithParentheses()
19+
{
20+
$w = ($c = $x) ?? $d;
21+
$w = ($c = $x) ?? $d;
22+
$w = $c ?? ($a + $b);
23+
}
24+
25+
?>

rules/Php70/Rector/Ternary/TernaryToNullCoalescingRector.php

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
namespace Rector\Php70\Rector\Ternary;
66

77
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
89
use PhpParser\Node\Expr;
910
use PhpParser\Node\Expr\BinaryOp;
1011
use PhpParser\Node\Expr\BinaryOp\Coalesce;
1112
use PhpParser\Node\Expr\BinaryOp\Identical;
1213
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
14+
use PhpParser\Node\Expr\BooleanNot;
15+
use PhpParser\Node\Expr\FuncCall;
1316
use PhpParser\Node\Expr\Isset_;
1417
use PhpParser\Node\Expr\Ternary;
1518
use Rector\NodeTypeResolver\Node\AttributeKey;
@@ -38,6 +41,7 @@ public function getRuleDefinition(): RuleDefinition
3841
[
3942
new CodeSample('$value === null ? 10 : $value;', '$value ?? 10;'),
4043
new CodeSample('isset($value) ? $value : 10;', '$value ?? 10;'),
44+
new CodeSample('is_null($value) ? 10 : $value;', '$value ?? 10;'),
4145
]
4246
);
4347
}
@@ -59,6 +63,17 @@ public function refactor(Node $node): ?Node
5963
return $this->processTernaryWithIsset($node, $node->cond);
6064
}
6165

66+
if ($node->cond instanceof FuncCall && $this->isName($node->cond, 'is_null')) {
67+
return $this->processTernaryWithIsNull($node, $node->cond, false);
68+
}
69+
70+
if (
71+
$node->cond instanceof BooleanNot && $node->cond->expr instanceof FuncCall
72+
&& $this->isName($node->cond->expr, 'is_null')
73+
) {
74+
return $this->processTernaryWithIsNull($node, $node->cond->expr, true);
75+
}
76+
6277
if ($node->cond instanceof Identical) {
6378
$checkedNode = $node->else;
6479
$fallbackNode = $node->if;
@@ -80,11 +95,11 @@ public function refactor(Node $node): ?Node
8095

8196
$ternaryCompareNode = $node->cond;
8297
if ($this->isNullMatch($ternaryCompareNode->left, $ternaryCompareNode->right, $checkedNode)) {
83-
return new Coalesce($checkedNode, $fallbackNode);
98+
return $this->createCoalesce($checkedNode, $fallbackNode);
8499
}
85100

86101
if ($this->isNullMatch($ternaryCompareNode->right, $ternaryCompareNode->left, $checkedNode)) {
87-
return new Coalesce($checkedNode, $fallbackNode);
102+
return $this->createCoalesce($checkedNode, $fallbackNode);
88103
}
89104

90105
return null;
@@ -95,6 +110,46 @@ public function provideMinPhpVersion(): int
95110
return PhpVersionFeature::NULL_COALESCE;
96111
}
97112

113+
private function processTernaryWithIsNull(Ternary $ternary, FuncCall $isNullFuncCall, bool $isNegated): ?Coalesce
114+
{
115+
if (count($isNullFuncCall->args) !== 1) {
116+
return null;
117+
}
118+
119+
$firstArg = $isNullFuncCall->args[0];
120+
if (! $firstArg instanceof Arg) {
121+
return null;
122+
}
123+
124+
$checkedExpr = $firstArg->value;
125+
126+
if ($isNegated) {
127+
if (! $ternary->if instanceof Expr) {
128+
return null;
129+
}
130+
131+
if (! $this->nodeComparator->areNodesEqual($ternary->if, $checkedExpr)) {
132+
return null;
133+
}
134+
135+
$this->preserveWrappedFallback($ternary->else);
136+
137+
return $this->createCoalesce($ternary->if, $ternary->else);
138+
}
139+
140+
if (! $this->nodeComparator->areNodesEqual($ternary->else, $checkedExpr)) {
141+
return null;
142+
}
143+
144+
if (! $ternary->if instanceof Expr) {
145+
return null;
146+
}
147+
148+
$this->preserveWrappedFallback($ternary->if);
149+
150+
return $this->createCoalesce($ternary->else, $ternary->if);
151+
}
152+
98153
private function processTernaryWithIsset(Ternary $ternary, Isset_ $isset): ?Coalesce
99154
{
100155
if (! $ternary->if instanceof Expr) {
@@ -122,7 +177,7 @@ private function processTernaryWithIsset(Ternary $ternary, Isset_ $isset): ?Coal
122177
$ternary->else->setAttribute(AttributeKey::WRAPPED_IN_PARENTHESES, true);
123178
}
124179

125-
return new Coalesce($ternary->if, $ternary->else);
180+
return $this->createCoalesce($ternary->if, $ternary->else);
126181
}
127182

128183
private function isTernaryParenthesized(File $file, Expr $expr, Ternary $ternary): bool
@@ -163,4 +218,45 @@ private function isNullMatch(Expr $possibleNullExpr, Expr $firstNode, Expr $seco
163218

164219
return $this->nodeComparator->areNodesEqual($firstNode, $secondNode);
165220
}
221+
222+
private function createCoalesce(Expr $checkedExpr, Expr $fallbackExpr): Coalesce
223+
{
224+
if ($this->isExprParenthesized($this->getFile(), $checkedExpr)) {
225+
$checkedExpr->setAttribute(AttributeKey::WRAPPED_IN_PARENTHESES, true);
226+
}
227+
228+
return new Coalesce($checkedExpr, $fallbackExpr);
229+
}
230+
231+
private function preserveWrappedFallback(Expr $expr): void
232+
{
233+
if (! $expr instanceof BinaryOp && ! $expr instanceof Ternary) {
234+
return;
235+
}
236+
237+
if (! $this->isExprParenthesized($this->getFile(), $expr)) {
238+
return;
239+
}
240+
241+
$expr->setAttribute(AttributeKey::WRAPPED_IN_PARENTHESES, true);
242+
}
243+
244+
private function isExprParenthesized(File $file, Expr $expr): bool
245+
{
246+
$oldTokens = $file->getOldTokens();
247+
$startTokenPos = $expr->getStartTokenPos();
248+
$endTokenPos = $expr->getEndTokenPos();
249+
250+
while (isset($oldTokens[$startTokenPos - 1]) && trim((string) $oldTokens[$startTokenPos - 1]) === '') {
251+
--$startTokenPos;
252+
}
253+
254+
while (isset($oldTokens[$endTokenPos + 1]) && trim((string) $oldTokens[$endTokenPos + 1]) === '') {
255+
++$endTokenPos;
256+
}
257+
258+
return isset($oldTokens[$startTokenPos - 1], $oldTokens[$endTokenPos + 1])
259+
&& (string) $oldTokens[$startTokenPos - 1] === '('
260+
&& (string) $oldTokens[$endTokenPos + 1] === ')';
261+
}
166262
}

0 commit comments

Comments
 (0)