Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

namespace Rector\Tests\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector\Fixture;

final class BooleanAndWithObject
final class BooleanAndGuardNotReused
{
public function run(\stdClass $someObject)
public function run(\stdClass $someObject, bool $flag)
{
if (is_object($someObject) && method_exists($someObject, 'some_method')) {
if (is_object($someObject) && $flag) {
return 100;
}

Expand All @@ -20,11 +20,11 @@ final class BooleanAndWithObject

namespace Rector\Tests\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector\Fixture;

final class BooleanAndWithObject
final class BooleanAndGuardNotReused
{
public function run(\stdClass $someObject)
public function run(\stdClass $someObject, bool $flag)
{
if (method_exists($someObject, 'some_method')) {
if ($flag) {
return 100;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Rector\Tests\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector\Fixture;

final class BooleanAndWithObject
{
public function run(\stdClass $someObject)
{
if (is_object($someObject) && method_exists($someObject, 'some_method')) {
return 100;
}

return 0;
}
}
67 changes: 67 additions & 0 deletions rules/DeadCode/Rector/If_/RemoveAlwaysTrueIfConditionRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@ final class RemoveAlwaysTrueIfConditionRector extends AbstractRector
'extension_loaded',
];

/**
* Type-assertion guards. Their "always true" result may come from a phpdoc-narrowed type
* that runtime can violate (e.g. user config, decoded JSON), so when the right operand reuses
* the guarded variable, removing the guard would change behavior.
*
* @var string[]
*/
private const array TYPE_ASSERTION_FUNCTIONS = [
'is_string',
'is_int',
'is_integer',
'is_float',
'is_double',
'is_bool',
'is_array',
'is_object',
'is_callable',
'is_iterable',
'is_numeric',
'is_scalar',
'is_countable',
'is_null',
'is_a',
];

public function __construct(
private readonly ExprAnalyzer $exprAnalyzer,
private readonly BetterNodeFinder $betterNodeFinder,
Expand Down Expand Up @@ -233,7 +258,49 @@ private function refactorIfWithBooleanAnd(If_ $if): ?If_
return null;
}

// keep a type-assertion guard (e.g. is_string($x)) when the right operand reuses the same
// variable: its "always true" type may be phpdoc-only and violated at runtime, so dropping
// the guard would let the right operand run on an unexpected type
if ($this->isReusedTypeGuard($booleanAnd)) {
return null;
}

$if->cond = $booleanAnd->right;
return $if;
}

private function isReusedTypeGuard(BooleanAnd $booleanAnd): bool
{
/** @var FuncCall[] $funcCalls */
$funcCalls = $this->betterNodeFinder->findInstancesOf($booleanAnd->left, [FuncCall::class]);
foreach ($funcCalls as $funcCall) {
if (! $this->isNames($funcCall, self::TYPE_ASSERTION_FUNCTIONS)) {
continue;
}

$args = $funcCall->getArgs();
if (! isset($args[0])) {
continue;
}

$subject = $args[0]->value;
if (! $subject instanceof Variable) {
continue;
}

$isReusedOnRight = (bool) $this->betterNodeFinder->findFirst(
$booleanAnd->right,
fn (Node $subNode): bool => $subNode instanceof Variable && $this->nodeComparator->areNodesEqual(
$subNode,
$subject
)
);

if ($isReusedOnRight) {
return true;
}
}

return false;
}
}
Loading