[PHP] Add NullCoalesceRector

This commit is contained in:
Tomas Votruba 2018-10-04 20:04:01 +08:00
parent 116bff36f7
commit 02d2ba69e3
13 changed files with 275 additions and 1 deletions

View File

@ -1,2 +1,3 @@
services:
Rector\Php\Rector\ExceptionHandlerTypehintRector: ~
Rector\Php\Rector\TernaryToNullCoalescingRector: ~

View File

@ -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

View File

@ -0,0 +1,127 @@
<?php declare(strict_types=1);
namespace Rector\Php\Rector;
use PhpParser\Node;
use PhpParser\Node\Expr\BinaryOp\Coalesce;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\Isset_;
use PhpParser\Node\Expr\Ternary;
use Rector\Printer\BetterStandardPrinter;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
final class TernaryToNullCoalescingRector extends AbstractRector
{
/**
* @var BetterStandardPrinter
*/
private $betterStandardPrinter;
public function __construct(BetterStandardPrinter $betterStandardPrinter)
{
$this->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';
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
$x = $a ?? null;
$x = $a[0] ?? 1;
$x=$a ?? null;
$x = $a[ $b[ "c" ]] ?? null;
$x = $a ?? $b[func(1, true)];
$x = ($a ?? isset($b)) ? $b : "";
$x = $a ?? isset($b) ? $b : isset($c) ? $c : "";

View File

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
$x = $a ?? 1;
$y = isset($b) ? "b" : 2;
$x = $c ?? 3;
$x = $a ?? $b ?? "";

View File

@ -0,0 +1,5 @@
<?php declare(strict_types=1);
$f = $g ?? 'g';
$ff = $gg ?? 'gg';

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
$d = $e ?? 'e';
$dd = $ee ?? 'ee';
$i = $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"} ?? 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;

View File

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\TernaryToNullCoalescingRector;
use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
/**
* @covers \Rector\Php\Rector\TernaryToNullCoalescingRector
*
* Some tests copied from:
* https://github.com/FriendsOfPHP/PHP-CS-Fixer/commit/0db4f91088a3888a7c8b26e5a36fba53c0d9507c#diff-02f477b178d0dc5b25ac05ab3b59e7c7
* https://github.com/slevomat/coding-standard/blob/master/tests/Sniffs/ControlStructures/data/requireNullCoalesceOperatorErrors.fixed.php
*/
final class TernaryToNullCoalescingRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideWrongToFixedFiles()
*/
public function test(string $wrong, string $fixed): void
{
$this->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';
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
$x = isset($a) ? $a : null;
$x = isset($a[0]) ? $a[0] : 1;
$x=isset($a)?$a:null;
$x = isset ( $a[$b["c"]]) ?$a[ $b[ "c" ]] : null;
$x = isset($a) ? $a : $b[func(1, true)];
$x = (isset($a) ? $a : isset($b)) ? $b : "";
$x = isset($a) ? $a : isset($b) ? $b : isset($c) ? $c : "";

View File

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
$x = isset($a) ? $a : 1;
$y = isset($b) ? "b" : 2;
$x = isset($c) ? $c : 3;
$x = isset($a) ? $a : (isset($b) ? $b : "");

View File

@ -0,0 +1,5 @@
<?php declare(strict_types=1);
$f = $g === null ? 'g' : $g;
$ff = null === $gg ? 'gg' : $gg;

View File

@ -0,0 +1,29 @@
<?php declare(strict_types=1);
$d = $e !== null ? $e : 'e';
$dd = null !== $ee ? $ee : 'ee';
$i = $this->${'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()();

View File

@ -0,0 +1,2 @@
services:
Rector\Php\Rector\TernaryToNullCoalescingRector: ~