[PHP 8.0] Remove NullsafeOperatorRector, as often breaking and promoting non-early returns and losen types (#900)

This commit is contained in:
Tomas Votruba 2021-09-19 12:11:26 +02:00 committed by GitHub
parent b5516368d4
commit ab182ff80b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 2 additions and 1804 deletions

View File

@ -1,4 +1,4 @@
# 477 Rules Overview
# 476 Rules Overview
<br>
@ -72,7 +72,7 @@
- [Php74](#php74) (15)
- [Php80](#php80) (17)
- [Php80](#php80) (16)
- [Php81](#php81) (5)
@ -7848,30 +7848,6 @@ Change ternary type resolve to `get_debug_type()`
<br>
### NullsafeOperatorRector
Change if null check with nullsafe operator ?-> with full short circuiting
- class: [`Rector\Php80\Rector\If_\NullsafeOperatorRector`](../rules/Php80/Rector/If_/NullsafeOperatorRector.php)
```diff
class SomeClass
{
public function run($someObject)
{
- $someObject2 = $someObject->mayFail1();
- if ($someObject2 === null) {
- return null;
- }
-
- return $someObject2->mayFail2();
+ return $someObject->mayFail1()?->mayFail2();
}
}
```
<br>
### OptionalParametersAfterRequiredRector
Move required parameters after optional ones

View File

@ -86,7 +86,6 @@ parameters:
paths:
- rules/Php70/EregToPcreTransformer.php
- packages/NodeTypeResolver/NodeTypeResolver.php
- rules/Php80/Rector/If_/NullsafeOperatorRector.php
- rules/Renaming/NodeManipulator/ClassRenamer.php
- rules/DowngradePhp72/Rector/FuncCall/DowngradePregUnmatchedAsNullConstantRector.php
@ -172,12 +171,6 @@ parameters:
- '#Access to an undefined property PhpParser\\Node\\Expr\\Error\|PhpParser\\Node\\Expr\\Variable\:\:\$name#'
# @todo loop magic, resolve later
-
message: '#Access to an undefined property PhpParser\\Node\:\:\$expr#'
paths:
- rules/Php80/Rector/If_/NullsafeOperatorRector.php
-
message: '#Comparison operation "<" between 0 and 2 is always true#'
paths:

View File

@ -1,31 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
final class DefaultValue
{
public function run()
{
if ($this->endWeekDate !== null) {
$param = $this->endWeekDate->format();
} else {
$param = "No data";
}
return $param;
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
final class DefaultValue
{
public function run()
{
$param = $this->endWeekDate?->format() ?? "No data";
return $param;
}
}
?>

View File

@ -1,32 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class Fixture
{
public function f($o)
{
$o2 = $o->mayFail1();
if ($o2 === null) {
return null;
}
return $o2->mayFail2();
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class Fixture
{
public function f($o)
{
return $o->mayFail1()?->mayFail2();
}
}
?>

View File

@ -1,47 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureMultipleIf
{
public function f($o)
{
$o2 = $o->mayFail1();
if ($o2 === null) {
return null;
}
$mayFail2 = $o2->mayFail2();
if ($mayFail2 === null) {
return null;
}
$mayFail3 = $mayFail2->mayFail3();
if ($mayFail3 === null) {
return null;
}
$mayFail4 = $mayFail3->mayFail4();
if ($mayFail4 === null) {
return null;
}
return $mayFail4->mayFail5();
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureMultipleIf
{
public function f($o)
{
return $o->mayFail1()?->mayFail2()?->mayFail3()?->mayFail4()?->mayFail5();
}
}
?>

View File

@ -1,32 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureNextIsProperty
{
public function f($o)
{
$o2 = $o->property1;
if ($o2 === null) {
return null;
}
return $o2->property2;
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureNextIsProperty
{
public function f($o)
{
return $o->property1?->property2;
}
}
?>

View File

@ -1,32 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureNoReturnLast
{
public function f($o)
{
$o2 = $o->mayFail1();
if ($o2 === null) {
return null;
}
$o2->mayFail2();
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureNoReturnLast
{
public function f($o)
{
$o->mayFail1()?->mayFail2();
}
}
?>

View File

@ -1,35 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureNotIdentical
{
public function f($o)
{
if ($o !== null) {
$user = $o->user;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->getCountry();
}
}
}
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureNotIdentical
{
public function f($o)
{
$country = $o?->user?->getAddress()?->getCountry();
}
}
?>

View File

@ -1,53 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureNotIdenticalProcessOnlyDirectIfAfterAssign
{
public function f($o)
{
if ($o !== null) {
$user = $o->user;
echo 'STATEMENT HERE';
if ($user !== null) {
$address = $user->getAddress();
echo 'STATEMENT HERE';
if ($address !== null) {
$country = $address->getCountry();
}
}
}
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureNotIdenticalProcessOnlyDirectIfAfterAssign
{
public function f($o)
{
if ($o !== null) {
$user = $o->user;
echo 'STATEMENT HERE';
if ($user !== null) {
$address = $user->getAddress();
echo 'STATEMENT HERE';
$country = $address?->getCountry();
}
}
}
}
?>

View File

@ -1,35 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureNotIdenticalYoda
{
public function f($o)
{
if (null !== $o) {
$user = $o->user;
if (null !== $user) {
$address = $user->getAddress();
if (null !== $address) {
$country = $address->getCountry();
}
}
}
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureNotIdenticalYoda
{
public function f($o)
{
$country = $o?->user?->getAddress()?->getCountry();
}
}
?>

View File

@ -1,51 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureProcessOnlyDirectUsageAfterIf
{
public function f($o)
{
$o2 = $o->mayFail1();
if ($o2 === null) {
return null;
}
echo 'STATEMENT HERE';
$mayFail2 = $o2->mayFail2();
if ($mayFail2 === null) {
return null;
}
$mayFail3 = $mayFail2->mayFail3();
if ($mayFail3 === null) {
return null;
}
return $mayFail3->mayFail4();
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureProcessOnlyDirectUsageAfterIf
{
public function f($o)
{
$o2 = $o->mayFail1();
if ($o2 === null) {
return null;
}
echo 'STATEMENT HERE';
return $o2->mayFail2()?->mayFail3()?->mayFail4();
}
}
?>

View File

@ -1,23 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureTernaryIsNotNull {
public function ternary() {
$b = $a !== null ? $a->getB() : null;
$c = $a !== null ? $a->c : null;
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureTernaryIsNotNull {
public function ternary() {
$b = $a?->getB();
$c = $a?->c;
}
}
?>

View File

@ -1,23 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureTernaryIsNull {
public function ternary() {
$b = $a === null ? null : $a->getB();
$c = $a === null ? null : $a->c;
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureTernaryIsNull {
public function ternary() {
$b = $a?->getB();
$c = $a?->c;
}
}
?>

View File

@ -1,32 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureYoda
{
public function f($o)
{
$o2 = $o->mayFail1();
if (null === $o2) {
return null;
}
return $o2->mayFail2();
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureYoda
{
public function f($o)
{
return $o->mayFail1()?->mayFail2();
}
}
?>

View File

@ -1,61 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
abstract class MaybeNull
{
public function addCover(Date $date, Money $amount = null): void
{
/** @var Money|null $currentAmount */
$currentAmount = $this->covers->get($date, null);
if ($currentAmount !== null) {
$amount = $currentAmount->add($amount);
}
$this->covers->put($date, $amount);
}
public function addCover2(Date $date, ?Money $amount): void
{
/** @var Money|null $currentAmount */
$currentAmount = $this->covers->get($date, null);
if ($currentAmount !== null) {
$amount = $currentAmount->add($amount);
}
$this->covers->put($date, $amount);
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
abstract class MaybeNull
{
public function addCover(Date $date, Money $amount = null): void
{
/** @var Money|null $currentAmount */
$currentAmount = $this->covers->get($date, null);
$amount = $currentAmount?->add($amount);
$this->covers->put($date, $amount);
}
public function addCover2(Date $date, ?Money $amount): void
{
/** @var Money|null $currentAmount */
$currentAmount = $this->covers->get($date, null);
$amount = $currentAmount?->add($amount);
$this->covers->put($date, $amount);
}
}
?>

View File

@ -1,33 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
final class MethodArgs
{
public function run()
{
if ($this->endWeekDate !== null) {
$param = $this->endWeekDate->format('Y-m-d');
} else {
$param = null;
}
return $param;
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
final class MethodArgs
{
public function run()
{
$param = $this->endWeekDate?->format('Y-m-d');
return $param;
}
}
?>

View File

@ -1,32 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class MethodArgsMultiCall
{
public function f($o)
{
$o2 = $o->mayFail1('a');
if ($o2 === null) {
return null;
}
return $o2->mayFail2('b');
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class MethodArgsMultiCall
{
public function f($o)
{
return $o->mayFail1('a')?->mayFail2('b');
}
}
?>

View File

@ -1,20 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
final class SkipDefaultValueMoreStmt
{
public function run()
{
if ($this->endWeekDate !== null) {
$param = $this->endWeekDate->format();
} else {
$param = "No data";
echo 'STATEMENT HERE';
}
return $param;
}
}
?>

View File

@ -1,32 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
final class SkipIfNoDirectUsage
{
private $myService;
public function withReturn($prop1, $prop2)
{
$prop3 = $this->getProp3($prop1);
if (null === $prop3) {
return null;
}
return $this->myService->myMethod($prop1, $prop2, $prop3);
}
public function withoutReturn($prop1, $prop2)
{
$prop3 = $this->getProp3($prop1);
if (null === $prop3) {
return null;
}
$this->myService->myMethod($prop1, $prop2, $prop3);
}
}
?>

View File

@ -1,33 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
abstract class SkipNeverNullValue
{
public function addCover(Date $date, Money $amount): void
{
/** @var Money|null $currentAmount */
$currentAmount = $this->covers->get($date, null);
if ($currentAmount !== null) {
$amount = $currentAmount->add($amount);
}
$this->covers->put($date, $amount);
}
public function addCover2(Date $date): void
{
$amount = new Money();
/** @var Money|null $currentAmount */
$currentAmount = $this->covers->get($date, null);
if ($currentAmount !== null) {
$amount = $currentAmount->add($amount);
}
$this->covers->put($date, $amount);
}
}
?>

View File

@ -1,45 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class SkipNoDirectIfAfterAssignment
{
public function f($o)
{
$o2 = $o->mayFail1();
echo 'STATEMENT HERE';
if ($o2 === null) {
return null;
}
$mayFail2 = $o2->mayFail2();
echo 'STATEMENT HERE';
if ($mayFail2 === null) {
return null;
}
$mayFail3 = $mayFail2->mayFail3();
echo 'STATEMENT HERE';
if ($mayFail3 === null) {
return null;
}
$mayFail4 = $mayFail3->mayFail4();
echo 'STATEMENT HERE';
if ($mayFail4 === null) {
return null;
}
return $mayFail4->mayFail5();
}
}
?>

View File

@ -1,41 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class SkipNoDirectUsageAfterIf
{
public function f($o)
{
$o2 = $o->mayFail1();
if ($o2 === null) {
return null;
}
echo 'STATEMENT HERE';
$mayFail2 = $o2->mayFail2();
if ($mayFail2 === null) {
return null;
}
echo 'STATEMENT HERE';
$mayFail3 = $mayFail2->mayFail3();
if ($mayFail3 === null) {
return null;
}
echo 'STATEMENT HERE';
$mayFail4 = $mayFail3->mayFail4();
if ($mayFail4 === null) {
return null;
}
echo 'STATEMENT HERE';
return $mayFail4->mayFail5();
}
}
?>

View File

@ -1,30 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class SkipNoDirectUsageAfterIfInNext
{
public function f($o)
{
$o2 = $o->mayFail1();
if ($o2 === null) {
return null;
}
$mayFail2 = $o2->mayFail2();
if ($mayFail2 === null) {
return null;
}
$mayFail3 = $mayFail2->mayFail3();
if ($mayFail3 === null) {
return null;
}
echo 'STATEMENT HERE';
return $mayFail3->mayFail4();
}
}
?>

View File

@ -1,32 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class SkipNoDirectUsageAfterIfInPrevAndNext
{
public function f($o)
{
$o2 = $o->mayFail1();
if ($o2 === null) {
return null;
}
echo 'STATEMENT HERE';
$mayFail2 = $o2->mayFail2();
if ($mayFail2 === null) {
return null;
}
$mayFail3 = $mayFail2->mayFail3();
if ($mayFail3 === null) {
return null;
}
echo 'STATEMENT HERE';
return $mayFail3->mayFail4();
}
}
?>

View File

@ -1,25 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class SkipNoResultUsage
{
/**
* @var mixed
*/
private $baseField;
public function f($o)
{
$value = $this->baseField->getValue($o);
if ($value === null) {
return null;
}
return $this->baseField->select($value);
}
}
?>

View File

@ -1,18 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class SkipNotIdenticalNoDirectAssignAfterIf
{
public function f($o)
{
if ($o !== null) {
echo 'STATEMENT HERE';
$user = $o->user;
}
}
}
?>

View File

@ -1,15 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class SkipNotIdenticalNotNull
{
public function f($o)
{
if ($o !== 'abc') {
$user = $o->user;
}
}
}
?>

View File

@ -1,25 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
final class SkipNotIdentifierAndReturnBooleanOp
{
private static function foo(string $f, ?string $b, bool $c): ?string
{
$e = self::check($f, $c);
if ($e === null) {
return null;
}
if ($b !== null) {
$a = self::check($b, $c);
if ($a === null) {
return null;
}
return $a . $d;
}
return $d;
}
}

View File

@ -1,11 +0,0 @@
<?php declare(strict_types = 1);
function test(?string $argument): void
{
/** @var string[] $array */
$array = [];
if ($argument !== null) {
$array[] = $argument;
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
final class SkipReturnNullInEitherCase
{
public function f()
{
$otherVar = null;
if ($otherVar === null) {
return null;
}
return null;
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class FixtureTernaryIsNull {
public function ternary() {
$b = $a === null ?: $a->getB();
}
}
?>

View File

@ -1,13 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
final class SkipTernaryNoDirectUsage
{
public function run()
{
return $someObject === null ? null : \Carbon\Carbon::parse($someObject)->toDateTimeString();
}
}
?>

View File

@ -1,16 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
abstract class SkipTernaryNoDirectUsage2
{
public ?string $value;
public function run(): ?string
{
return $this->value !== null ? $this->doSomething($this->value) : null;
}
abstract public function doSomething(string $value);
}
?>

View File

@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
final class SkipTernaryReturnsNonNullInIfAndElse
{
public function ternary()
{
$b = $a === null ? 'if case' : 'else case';
$b = $a !== null ? 'if case 2' : 'else case 2';
$b = $a === null ? 'is null' : $a->getB();
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class SoooManyNullChecks
{
public function f($main)
{
$o2 = $main->mayFail1();
if ($o2 === null) {
return null;
}
$o3 = $o2->mayFail2();
if (null === $o3) {
return null;
}
$o4 = $o3->mayFail3();
if (null === $o4) {
return null;
}
return $o4->mayFail4();
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\Fixture;
class SoooManyNullChecks
{
public function f($main)
{
return $main->mayFail1()?->mayFail2()?->mayFail3()?->mayFail4();
}
}
?>

View File

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector;
use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Symplify\SmartFileSystem\SmartFileInfo;
final class NullsafeOperatorRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(SmartFileInfo $fileInfo): void
{
$this->doTestFileInfo($fileInfo);
}
/**
* @return Iterator<SmartFileInfo>
*/
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}

View File

@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
use Rector\Php80\Rector\If_\NullsafeOperatorRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(NullsafeOperatorRector::class);
};

View File

@ -1,600 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\Php80\Rector\If_;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\BinaryOp\Coalesce;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\NullsafeMethodCall;
use PhpParser\Node\Expr\NullsafePropertyFetch;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Ternary;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Identifier;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Else_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Return_;
use Rector\Core\NodeAnalyzer\ParamAnalyzer;
use Rector\Core\NodeManipulator\IfManipulator;
use Rector\Core\NodeManipulator\NullsafeManipulator;
use Rector\Core\Rector\AbstractRector;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
/**
* @changelog https://wiki.php.net/rfc/nullsafe_operator
* @see \Rector\Tests\Php80\Rector\If_\NullsafeOperatorRector\NullsafeOperatorRectorTest
*/
final class NullsafeOperatorRector extends AbstractRector
{
/**
* @var array<class-string<Expr>>
*/
private const ALLOWED_NEXT_NODE_EXPRS = [
MethodCall::class,
PropertyFetch::class,
NullsafeMethodCall::class,
NullsafePropertyFetch::class,
];
public function __construct(
private IfManipulator $ifManipulator,
private NullsafeManipulator $nullsafeManipulator,
private ParamAnalyzer $paramAnalyzer
) {
}
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Change if null check with nullsafe operator ?-> with full short circuiting',
[
new CodeSample(
<<<'CODE_SAMPLE'
class SomeClass
{
public function run($someObject)
{
$someObject2 = $someObject->mayFail1();
if ($someObject2 === null) {
return null;
}
return $someObject2->mayFail2();
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
class SomeClass
{
public function run($someObject)
{
return $someObject->mayFail1()?->mayFail2();
}
}
CODE_SAMPLE
),
]
);
}
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [If_::class, Ternary::class];
}
/**
* @param If_|Ternary $node
*/
public function refactor(Node $node): ?Node
{
if ($node instanceof If_) {
return $this->handleIfNode($node);
}
return $this->handleTernaryNode($node);
}
private function handleIfNode(If_ $if): ?Node
{
$processNullSafeOperator = $this->processNullSafeOperatorIdentical($if);
if ($processNullSafeOperator !== null) {
/** @var Expression $prevNode */
$prevNode = $if->getAttribute(AttributeKey::PREVIOUS_NODE);
$this->removeNode($prevNode);
return $processNullSafeOperator;
}
return $this->processNullSafeOperatorNotIdentical($if);
}
private function processNullSafeOperatorIdentical(If_ $if, bool $isStartIf = true): ?Node
{
$comparedNode = $this->ifManipulator->matchIfValueReturnValue($if);
if (! $comparedNode instanceof Expr) {
return null;
}
$prevNode = $if->getAttribute(AttributeKey::PREVIOUS_NODE);
$nextNode = $if->getAttribute(AttributeKey::NEXT_NODE);
if (! $nextNode instanceof Node) {
return null;
}
if (! $prevNode instanceof Expression) {
return null;
}
$prevExpr = $prevNode->expr;
if (! $prevExpr instanceof Assign) {
return null;
}
if (! $this->ifManipulator->isIfCondUsingAssignIdenticalVariable($if, $prevExpr)) {
return null;
}
if ($this->hasIndirectUsageOnNextOfIf($prevExpr->var, $nextNode)) {
return null;
}
return $this->processAssign($prevExpr, $prevNode, $nextNode, $isStartIf);
}
private function hasIndirectUsageOnNextOfIf(Expr $expr, Node $nextNode): bool
{
if (! $nextNode instanceof Return_ && ! $nextNode instanceof Expression) {
return false;
}
if ($nextNode->expr instanceof PropertyFetch) {
return ! $this->nodeComparator->areNodesEqual($expr, $nextNode->expr->var);
}
if ($nextNode->expr instanceof MethodCall) {
return ! $this->nodeComparator->areNodesEqual($expr, $nextNode->expr->var);
}
return false;
}
private function shouldSkipNeverNullDefinedBefore(Assign $assign): bool
{
$variable = $assign->var;
$isReassign = (bool) $this->betterNodeFinder->findFirstPreviousOfNode(
$assign,
fn (Node $subNode): bool => $subNode instanceof Assign && $this->nodeComparator->areNodesEqual(
$subNode->var,
$variable
) && ! $this->valueResolver->isNull($subNode->expr)
);
if ($isReassign) {
return true;
}
$functionLike = $this->betterNodeFinder->findParentType($assign, FunctionLike::class);
if (! $functionLike instanceof FunctionLike) {
return false;
}
$params = $functionLike->getParams();
foreach ($params as $param) {
if ($this->nodeComparator->areNodesEqual($param->var, $variable)) {
return $this->isNotNullableAndNotHasDefaultNullValue($param);
}
}
return false;
}
private function isNotNullableAndNotHasDefaultNullValue(Param $param): bool
{
if ($this->paramAnalyzer->isNullable($param)) {
return false;
}
return ! $this->paramAnalyzer->hasDefaultNull($param);
}
private function verifyAssignMatchIfNotNullNextAssignment(If_ $if): ?Assign
{
$assign = $this->ifManipulator->matchIfNotNullNextAssignment($if);
if (! $assign instanceof Assign) {
return null;
}
if ($this->shouldSkipNeverNullDefinedBefore($assign)) {
return null;
}
return $assign;
}
private function processNullSafeOperatorNotIdentical(If_ $if, ?Expr $expr = null): ?Node
{
$assign = $this->verifyAssignMatchIfNotNullNextAssignment($if);
if (! $assign instanceof Assign) {
return null;
}
$assignExpr = $assign->expr;
if ($this->ifManipulator->isIfCondUsingAssignNotIdenticalVariable($if, $assignExpr)) {
return null;
}
$expression = $assign->getAttribute(AttributeKey::PARENT_NODE);
if (! $expression instanceof Expression) {
return null;
}
$nextNode = $expression->getAttribute(AttributeKey::NEXT_NODE);
$nullSafe = $this->nullsafeManipulator->processNullSafeExpr($assignExpr);
if ($nullSafe === null) {
return null;
}
if ($expr !== null) {
/** @var Identifier $nullSafeIdentifier */
$nullSafeIdentifier = $nullSafe->name;
/** @var NullsafeMethodCall|NullsafePropertyFetch $nullSafe */
$nullSafe = $this->nullsafeManipulator->processNullSafeExprResult($expr, $nullSafeIdentifier);
}
$nextOfNextNode = $this->processIfMayInNextNode($nextNode);
if ($nextOfNextNode !== null) {
return $nextOfNextNode;
}
if (! $nextNode instanceof If_) {
$nullSafe = $this->verifyDefaultValueInElse($if, $nullSafe, $assign);
if ($nullSafe === null) {
return null;
}
return new Assign($assign->var, $nullSafe);
}
return $this->processNullSafeOperatorNotIdentical($nextNode, $nullSafe);
}
private function verifyDefaultValueInElse(
If_ $if,
NullsafeMethodCall|NullsafePropertyFetch $nullSafe,
Assign $assign
): NullsafeMethodCall|NullsafePropertyFetch|Coalesce|null {
if (! $if->else instanceof Else_) {
return $nullSafe;
}
if (count($if->else->stmts) !== 1) {
return null;
}
$expression = $if->else->stmts[0];
if (! $expression instanceof Expression) {
return null;
}
$expressionAssign = $expression->expr;
if (! $expressionAssign instanceof Assign) {
return null;
}
if (! $this->nodeComparator->areNodesEqual($expressionAssign->var, $assign->var)) {
return null;
}
if ($this->valueResolver->isNull($expressionAssign->expr)) {
return $nullSafe;
}
return new Coalesce($nullSafe, $expressionAssign->expr);
}
private function processAssign(Assign $assign, Expression $prevExpression, Node $nextNode, bool $isStartIf): ?Node
{
if ($this->shouldProcessAssignInCurrentNode($assign, $nextNode)) {
return $this->processAssignInCurrentNode($assign, $prevExpression, $nextNode, $isStartIf);
}
return $this->processAssignMayInNextNode($nextNode);
}
private function shouldProcessAssignInCurrentNode(Assign $assign, Node $nextNode): bool
{
if (! $nextNode instanceof Return_ && ! $nextNode instanceof Expression) {
return false;
}
if ($nextNode->expr instanceof Assign) {
return false;
}
if (! $nextNode->expr instanceof Node) {
return false;
}
if (! $this->isMethodCallOrPropertyFetch($assign->expr)) {
return ! $this->valueResolver->isNull($nextNode->expr);
}
/** @var MethodCall|PropertyFetch $expr */
$expr = $assign->expr;
if (! $this->isMethodCallOrPropertyFetch($expr->var)) {
return ! $this->valueResolver->isNull($expr);
}
if (! $this->isMethodCallOrPropertyFetch($nextNode->expr)) {
return ! $this->valueResolver->isNull($nextNode->expr);
}
return false;
}
private function processIfMayInNextNode(?Node $nextNode = null): ?Node
{
if (! $nextNode instanceof Node) {
return null;
}
$nextOfNextNode = $nextNode->getAttribute(AttributeKey::NEXT_NODE);
while ($nextOfNextNode) {
if ($nextOfNextNode instanceof If_) {
/** @var If_ $beforeIf */
$beforeIf = $nextOfNextNode->getAttribute(AttributeKey::PARENT_NODE);
$nullSafe = $this->processNullSafeOperatorNotIdentical($nextOfNextNode);
if (! $nullSafe instanceof NullsafeMethodCall && ! $nullSafe instanceof PropertyFetch) {
return $beforeIf;
}
$beforeIf->stmts[count($beforeIf->stmts) - 1] = new Expression($nullSafe);
return $beforeIf;
}
$nextOfNextNode = $nextOfNextNode->getAttribute(AttributeKey::NEXT_NODE);
}
return null;
}
private function processAssignInCurrentNode(
Assign $assign,
Expression $expression,
Node $nextNode,
bool $isStartIf
): ?Node {
if (! in_array($nextNode->expr::class, self::ALLOWED_NEXT_NODE_EXPRS, true)) {
return null;
}
$assignNullSafe = $isStartIf
? $assign->expr
: $this->nullsafeManipulator->processNullSafeExpr($assign->expr);
$nullSafe = $this->nullsafeManipulator->processNullSafeExprResult($assignNullSafe, $nextNode->expr->name);
$prevAssign = $expression->getAttribute(AttributeKey::PREVIOUS_NODE);
if ($prevAssign instanceof If_) {
$nullSafe = $this->getNullSafeOnPrevAssignIsIf($prevAssign, $nextNode, $nullSafe);
}
$this->removeNode($nextNode);
if ($nextNode instanceof Return_) {
$nextNode->expr = $nullSafe;
return $nextNode;
}
return $nullSafe;
}
private function processAssignMayInNextNode(Node $nextNode): ?Node
{
if (! $nextNode instanceof Expression) {
return null;
}
if (! $nextNode->expr instanceof Assign) {
return null;
}
$mayNextIf = $nextNode->getAttribute(AttributeKey::NEXT_NODE);
if (! $mayNextIf instanceof If_) {
return null;
}
if ($this->ifManipulator->isIfCondUsingAssignIdenticalVariable($mayNextIf, $nextNode->expr)) {
return $this->processNullSafeOperatorIdentical($mayNextIf, false);
}
return null;
}
private function getNullSafeOnPrevAssignIsIf(If_ $if, Node $nextNode, ?Expr $expr): ?Expr
{
$prevIf = $if->getAttribute(AttributeKey::PREVIOUS_NODE);
if (! $prevIf instanceof Expression) {
return $expr;
}
if (! $this->ifManipulator->isIfCondUsingAssignIdenticalVariable($if, $prevIf->expr)) {
return $expr;
}
$start = $prevIf;
while ($prevIf instanceof Expression) {
$expressionNode = $prevIf->expr;
if (! $expressionNode instanceof Assign) {
return null;
}
$expr = $this->nullsafeManipulator->processNullSafeExpr($expressionNode->expr);
/** @var Node $prevPrevIf */
$prevPrevIf = $prevIf->getAttribute(AttributeKey::PREVIOUS_NODE);
/** @var Node $prevPrevPrevIf */
$prevPrevPrevIf = $prevPrevIf->getAttribute(AttributeKey::PREVIOUS_NODE);
if (! $prevPrevPrevIf instanceof Expression && $prevPrevPrevIf !== null) {
$start = $this->getPreviousIf($prevPrevPrevIf);
break;
}
$prevIf = $prevPrevPrevIf;
}
if (! $expr instanceof NullsafeMethodCall && ! $expr instanceof NullsafePropertyFetch) {
return $expr;
}
/** @var Expr $expr */
$expr = $expr->var->getAttribute(AttributeKey::PARENT_NODE);
$expr = $this->getNullSafeAfterStartUntilBeforeEnd($start, $expr);
return $this->nullsafeManipulator->processNullSafeExprResult($expr, $nextNode->expr->name);
}
private function getPreviousIf(Node $node): ?Node
{
/** @var If_ $if */
$if = $node->getAttribute(AttributeKey::NEXT_NODE);
/** @var Expression $expression */
$expression = $if->getAttribute(AttributeKey::NEXT_NODE);
/** @var Expression $nextExpression */
$nextExpression = $expression->getAttribute(AttributeKey::NEXT_NODE);
return $nextExpression->getAttribute(AttributeKey::NEXT_NODE);
}
private function getNullSafeAfterStartUntilBeforeEnd(?Node $node, ?Expr $expr): ?Expr
{
while ($node) {
$expr = $this->nullsafeManipulator->processNullSafeExprResult($expr, $node->expr->expr->name);
$node = $node->getAttribute(AttributeKey::NEXT_NODE);
while ($node) {
/** @var If_ $if */
$if = $node->getAttribute(AttributeKey::NEXT_NODE);
if ($node instanceof Expression && $this->ifManipulator->isIfCondUsingAssignIdenticalVariable(
$if,
$node->expr
)) {
break;
}
$node = $node->getAttribute(AttributeKey::NEXT_NODE);
}
}
return $expr;
}
private function handleTernaryNode(Ternary $ternary): ?Node
{
if ($this->shouldSkipTernary($ternary)) {
return null;
}
$nullSafeElse = $this->nullsafeManipulator->processNullSafeExpr($ternary->else);
if ($nullSafeElse !== null) {
return $nullSafeElse;
}
if ($ternary->if === null) {
return null;
}
return $this->nullsafeManipulator->processNullSafeExpr($ternary->if);
}
private function shouldSkipTernary(Ternary $ternary): bool
{
if (! $this->canTernaryReturnNull($ternary)) {
return true;
}
if (! $ternary->cond instanceof Identical && ! $ternary->cond instanceof NotIdentical) {
return true;
}
if (! $this->hasNullComparison($ternary->cond)) {
return true;
}
return $this->hasIndirectUsageOnElse($ternary->cond, $ternary->if, $ternary->else);
}
private function hasIndirectUsageOnElse(Identical|NotIdentical $cond, ?Expr $if, Expr $expr): bool
{
$left = $cond->left;
$right = $cond->right;
$object = $this->valueResolver->isNull($left)
? $right
: $left;
if ($this->valueResolver->isNull($expr)) {
if ($this->isMethodCallOrPropertyFetch($if)) {
/** @var MethodCall|PropertyFetch $if */
return ! $this->nodeComparator->areNodesEqual($if->var, $object);
}
return false;
}
if ($this->isMethodCallOrPropertyFetch($expr)) {
/** @var MethodCall|PropertyFetch $expr */
return ! $this->nodeComparator->areNodesEqual($expr->var, $object);
}
return false;
}
private function isMethodCallOrPropertyFetch(?Expr $expr): bool
{
return $expr instanceof MethodCall || $expr instanceof PropertyFetch;
}
private function hasNullComparison(NotIdentical|Identical $check): bool
{
if ($this->valueResolver->isNull($check->left)) {
return true;
}
return $this->valueResolver->isNull($check->right);
}
private function canTernaryReturnNull(Ternary $ternary): bool
{
if ($this->valueResolver->isNull($ternary->else)) {
return true;
}
if ($ternary->if === null) {
// $foo === null ?: 'xx' returns true if $foo is null
// therefore it does not return null in case of the elvis operator
return false;
}
return $this->valueResolver->isNull($ternary->if);
}
}

View File

@ -11,8 +11,6 @@ use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
use PhpParser\Node\Expr\Exit_;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Expression;
@ -63,40 +61,6 @@ final class IfManipulator
return $this->matchComparedAndReturnedNode($if->cond, $insideIfNode);
}
/**
* Matches:
*
* if (<$value> !== null) {
* $anotherValue = $value;
* }
*/
public function matchIfNotNullNextAssignment(If_ $if): ?Assign
{
if ($if->stmts === []) {
return null;
}
if (! $if->cond instanceof NotIdentical) {
return null;
}
if (! $this->isNotIdenticalNullCompare($if->cond)) {
return null;
}
$insideIfNode = $if->stmts[0];
if (! $insideIfNode instanceof Expression) {
return null;
}
$assignedExpr = $insideIfNode->expr;
if (! $assignedExpr instanceof Assign) {
return null;
}
return $assignedExpr;
}
/**
* Matches:
*
@ -282,32 +246,6 @@ final class IfManipulator
return count($if->stmts) === 1;
}
public function isIfCondUsingAssignIdenticalVariable(Node $if, Node $assign): bool
{
if (! ($if instanceof If_ && $assign instanceof Assign)) {
return false;
}
if (! $if->cond instanceof Identical) {
return false;
}
return $this->nodeComparator->areNodesEqual($this->getIfCondVar($if), $assign->var);
}
public function isIfCondUsingAssignNotIdenticalVariable(If_ $if, Node $node): bool
{
if (! $node instanceof MethodCall && ! $node instanceof PropertyFetch) {
return false;
}
if (! $if->cond instanceof NotIdentical) {
return false;
}
return ! $this->nodeComparator->areNodesEqual($this->getIfCondVar($if), $node->var);
}
public function isIfWithoutElseAndElseIfs(If_ $if): bool
{
if ($if->else !== null) {
@ -359,19 +297,6 @@ final class IfManipulator
return null;
}
private function isNotIdenticalNullCompare(NotIdentical $notIdentical): bool
{
if ($this->nodeComparator->areNodesEqual($notIdentical->left, $notIdentical->right)) {
return false;
}
if ($this->valueResolver->isNull($notIdentical->right)) {
return true;
}
return $this->valueResolver->isNull($notIdentical->left);
}
private function isIfWithOnlyStmtIf(If_ $if): bool
{
if (! $this->isIfWithoutElseAndElseIfs($if)) {
@ -393,12 +318,4 @@ final class IfManipulator
return is_a($stmts[0], $desiredType);
}
private function getIfCondVar(If_ $if): Expr
{
/** @var Identical|NotIdentical $ifCond */
$ifCond = $if->cond;
return $this->valueResolver->isNull($ifCond->left) ? $ifCond->right : $ifCond->left;
}
}

View File

@ -1,51 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\Core\NodeManipulator;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\NullsafeMethodCall;
use PhpParser\Node\Expr\NullsafePropertyFetch;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Identifier;
use Rector\NodeTypeResolver\Node\AttributeKey;
final class NullsafeManipulator
{
public function processNullSafeExpr(Expr $expr): NullsafeMethodCall | NullsafePropertyFetch | null
{
if ($expr instanceof MethodCall) {
return new NullsafeMethodCall($expr->var, $expr->name, $expr->args);
}
if ($expr instanceof PropertyFetch) {
return new NullsafePropertyFetch($expr->var, $expr->name);
}
return null;
}
/**
* @param Identifier|mixed $nextExprIdentifier
*/
public function processNullSafeExprResult(?Expr $expr, $nextExprIdentifier): ?Expr
{
if (! $nextExprIdentifier instanceof Identifier) {
return null;
}
if ($expr === null) {
return null;
}
$parentIdentifier = $nextExprIdentifier->getAttribute(AttributeKey::PARENT_NODE);
if ($parentIdentifier instanceof MethodCall || $parentIdentifier instanceof NullsafeMethodCall) {
return new NullsafeMethodCall($expr, $nextExprIdentifier, $parentIdentifier->args);
}
return new NullsafePropertyFetch($expr, $nextExprIdentifier);
}
}