Skip to content

Commit 4ffe733

Browse files
committed
Updated Rector to commit f8f34877ad3c75a7e3ec1ff4193556d82f9c9384
rectorphp/rector-src@f8f3487 [Php70] Allow is_null() conversion on TernaryToNullCoalescingRector with parentheses handling (#7969)
1 parent 6a5893f commit 4ffe733

7 files changed

Lines changed: 82 additions & 14 deletions

File tree

rules/Php70/Rector/Ternary/TernaryToNullCoalescingRector.php

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
namespace Rector\Php70\Rector\Ternary;
55

66
use PhpParser\Node;
7+
use PhpParser\Node\Arg;
78
use PhpParser\Node\Expr;
89
use PhpParser\Node\Expr\BinaryOp;
910
use PhpParser\Node\Expr\BinaryOp\Coalesce;
1011
use PhpParser\Node\Expr\BinaryOp\Identical;
1112
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
13+
use PhpParser\Node\Expr\BooleanNot;
14+
use PhpParser\Node\Expr\FuncCall;
1215
use PhpParser\Node\Expr\Isset_;
1316
use PhpParser\Node\Expr\Ternary;
1417
use Rector\NodeTypeResolver\Node\AttributeKey;
@@ -34,7 +37,7 @@ public function __construct(ValueResolver $valueResolver)
3437
}
3538
public function getRuleDefinition(): RuleDefinition
3639
{
37-
return new RuleDefinition('Changes unneeded null check to ?? operator', [new CodeSample('$value === null ? 10 : $value;', '$value ?? 10;'), new CodeSample('isset($value) ? $value : 10;', '$value ?? 10;')]);
40+
return new RuleDefinition('Changes unneeded null check to ?? operator', [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;')]);
3841
}
3942
/**
4043
* @return array<class-string<Node>>
@@ -51,6 +54,12 @@ public function refactor(Node $node): ?Node
5154
if ($node->cond instanceof Isset_) {
5255
return $this->processTernaryWithIsset($node, $node->cond);
5356
}
57+
if ($node->cond instanceof FuncCall && $this->isName($node->cond, 'is_null')) {
58+
return $this->processTernaryWithIsNull($node, $node->cond, \false);
59+
}
60+
if ($node->cond instanceof BooleanNot && $node->cond->expr instanceof FuncCall && $this->isName($node->cond->expr, 'is_null')) {
61+
return $this->processTernaryWithIsNull($node, $node->cond->expr, \true);
62+
}
5463
if ($node->cond instanceof Identical) {
5564
$checkedNode = $node->else;
5665
$fallbackNode = $node->if;
@@ -69,17 +78,46 @@ public function refactor(Node $node): ?Node
6978
}
7079
$ternaryCompareNode = $node->cond;
7180
if ($this->isNullMatch($ternaryCompareNode->left, $ternaryCompareNode->right, $checkedNode)) {
72-
return new Coalesce($checkedNode, $fallbackNode);
81+
return $this->createCoalesce($checkedNode, $fallbackNode);
7382
}
7483
if ($this->isNullMatch($ternaryCompareNode->right, $ternaryCompareNode->left, $checkedNode)) {
75-
return new Coalesce($checkedNode, $fallbackNode);
84+
return $this->createCoalesce($checkedNode, $fallbackNode);
7685
}
7786
return null;
7887
}
7988
public function provideMinPhpVersion(): int
8089
{
8190
return PhpVersionFeature::NULL_COALESCE;
8291
}
92+
private function processTernaryWithIsNull(Ternary $ternary, FuncCall $isNullFuncCall, bool $isNegated): ?Coalesce
93+
{
94+
if (count($isNullFuncCall->args) !== 1) {
95+
return null;
96+
}
97+
$firstArg = $isNullFuncCall->args[0];
98+
if (!$firstArg instanceof Arg) {
99+
return null;
100+
}
101+
$checkedExpr = $firstArg->value;
102+
if ($isNegated) {
103+
if (!$ternary->if instanceof Expr) {
104+
return null;
105+
}
106+
if (!$this->nodeComparator->areNodesEqual($ternary->if, $checkedExpr)) {
107+
return null;
108+
}
109+
$this->preserveWrappedFallback($ternary->else);
110+
return $this->createCoalesce($ternary->if, $ternary->else);
111+
}
112+
if (!$this->nodeComparator->areNodesEqual($ternary->else, $checkedExpr)) {
113+
return null;
114+
}
115+
if (!$ternary->if instanceof Expr) {
116+
return null;
117+
}
118+
$this->preserveWrappedFallback($ternary->if);
119+
return $this->createCoalesce($ternary->else, $ternary->if);
120+
}
83121
private function processTernaryWithIsset(Ternary $ternary, Isset_ $isset): ?Coalesce
84122
{
85123
if (!$ternary->if instanceof Expr) {
@@ -98,7 +136,7 @@ private function processTernaryWithIsset(Ternary $ternary, Isset_ $isset): ?Coal
98136
if (($ternary->else instanceof Ternary || $ternary->else instanceof BinaryOp) && $this->isTernaryParenthesized($this->getFile(), $ternary->cond, $ternary)) {
99137
$ternary->else->setAttribute(AttributeKey::WRAPPED_IN_PARENTHESES, \true);
100138
}
101-
return new Coalesce($ternary->if, $ternary->else);
139+
return $this->createCoalesce($ternary->if, $ternary->else);
102140
}
103141
private function isTernaryParenthesized(File $file, Expr $expr, Ternary $ternary): bool
104142
{
@@ -129,4 +167,34 @@ private function isNullMatch(Expr $possibleNullExpr, Expr $firstNode, Expr $seco
129167
}
130168
return $this->nodeComparator->areNodesEqual($firstNode, $secondNode);
131169
}
170+
private function createCoalesce(Expr $checkedExpr, Expr $fallbackExpr): Coalesce
171+
{
172+
if ($this->isExprParenthesized($this->getFile(), $checkedExpr)) {
173+
$checkedExpr->setAttribute(AttributeKey::WRAPPED_IN_PARENTHESES, \true);
174+
}
175+
return new Coalesce($checkedExpr, $fallbackExpr);
176+
}
177+
private function preserveWrappedFallback(Expr $expr): void
178+
{
179+
if (!$expr instanceof BinaryOp && !$expr instanceof Ternary) {
180+
return;
181+
}
182+
if (!$this->isExprParenthesized($this->getFile(), $expr)) {
183+
return;
184+
}
185+
$expr->setAttribute(AttributeKey::WRAPPED_IN_PARENTHESES, \true);
186+
}
187+
private function isExprParenthesized(File $file, Expr $expr): bool
188+
{
189+
$oldTokens = $file->getOldTokens();
190+
$startTokenPos = $expr->getStartTokenPos();
191+
$endTokenPos = $expr->getEndTokenPos();
192+
while (isset($oldTokens[$startTokenPos - 1]) && trim((string) $oldTokens[$startTokenPos - 1]) === '') {
193+
--$startTokenPos;
194+
}
195+
while (isset($oldTokens[$endTokenPos + 1]) && trim((string) $oldTokens[$endTokenPos + 1]) === '') {
196+
++$endTokenPos;
197+
}
198+
return isset($oldTokens[$startTokenPos - 1], $oldTokens[$endTokenPos + 1]) && (string) $oldTokens[$startTokenPos - 1] === '(' && (string) $oldTokens[$endTokenPos + 1] === ')';
199+
}
132200
}

src/Application/VersionResolver.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ final class VersionResolver
1919
* @api
2020
* @var string
2121
*/
22-
public const PACKAGE_VERSION = '84fe920db62fecb447335cbfff9081d67c672a94';
22+
public const PACKAGE_VERSION = 'f8f34877ad3c75a7e3ec1ff4193556d82f9c9384';
2323
/**
2424
* @api
2525
* @var string
2626
*/
27-
public const RELEASE_DATE = '2026-04-11 21:41:03';
27+
public const RELEASE_DATE = '2026-04-14 12:03:53';
2828
/**
2929
* @var int
3030
*/

vendor/composer/installed.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1884,12 +1884,12 @@
18841884
"source": {
18851885
"type": "git",
18861886
"url": "https:\/\/github.com\/rectorphp\/rector-symfony.git",
1887-
"reference": "5673dc2171aeeb02a9000dd2d904fabfebe39187"
1887+
"reference": "9534590075b0f76ff539cfb2246abc87cc1e2e1b"
18881888
},
18891889
"dist": {
18901890
"type": "zip",
1891-
"url": "https:\/\/api.github.com\/repos\/rectorphp\/rector-symfony\/zipball\/5673dc2171aeeb02a9000dd2d904fabfebe39187",
1892-
"reference": "5673dc2171aeeb02a9000dd2d904fabfebe39187",
1891+
"url": "https:\/\/api.github.com\/repos\/rectorphp\/rector-symfony\/zipball\/9534590075b0f76ff539cfb2246abc87cc1e2e1b",
1892+
"reference": "9534590075b0f76ff539cfb2246abc87cc1e2e1b",
18931893
"shasum": ""
18941894
},
18951895
"require": {
@@ -1923,7 +1923,7 @@
19231923
"tomasvotruba\/unused-public": "^2.2",
19241924
"tracy\/tracy": "^2.11"
19251925
},
1926-
"time": "2026-04-07T21:37:40+00:00",
1926+
"time": "2026-04-14T04:56:06+00:00",
19271927
"default-branch": true,
19281928
"type": "rector-extension",
19291929
"extra": {

0 commit comments

Comments
 (0)