Skip to content
Merged
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
@@ -0,0 +1,25 @@
<?php

namespace Rector\Tests\Php56\Rector\FuncCall\PowToExpRector\Fixture;

function keepBareRightOperand($a)
{
echo pow(2, -3);
echo pow(2, ~3);
echo pow(2, (int) $a);
}

?>
-----
<?php

namespace Rector\Tests\Php56\Rector\FuncCall\PowToExpRector\Fixture;

function keepBareRightOperand($a)
{
echo 2 ** -3;
echo 2 ** ~3;
echo 2 ** (int) $a;
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Rector\Tests\Php56\Rector\FuncCall\PowToExpRector\Fixture;

function lowerPrecedenceOperands($a)
{
echo pow(~3, 4);
echo pow(!3, 4);
echo pow((int) "5x", 2);
echo pow($a += 4, 3);
echo pow($a ? 4 : 3, 2);
echo pow(2, $a ? 3 : 4);
echo pow(2, $a = 4);
}

?>
-----
<?php

namespace Rector\Tests\Php56\Rector\FuncCall\PowToExpRector\Fixture;

function lowerPrecedenceOperands($a)
{
echo (~3) ** 4;
echo (!3) ** 4;
echo ((int) "5x") ** 2;
echo ($a += 4) ** 3;
echo ($a ? 4 : 3) ** 2;
echo 2 ** ($a ? 3 : 4);
echo 2 ** ($a = 4);
}

?>
17 changes: 14 additions & 3 deletions rules/Php56/Rector/FuncCall/PowToExpRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use PhpParser\Node;
use PhpParser\Node\Expr\BinaryOp\Pow;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\UnaryMinus;
use Rector\NodeAnalyzer\PowOperandAnalyzer;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\Rector\AbstractRector;
use Rector\ValueObject\PhpVersionFeature;
Expand All @@ -20,6 +20,11 @@
*/
final class PowToExpRector extends AbstractRector implements MinPhpVersionInterface
{
public function __construct(
private readonly PowOperandAnalyzer $powOperandAnalyzer
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
Expand Down Expand Up @@ -54,8 +59,14 @@ public function refactor(Node $node): ?Node
$secondExpr = $node->getArgs()[1]
->value;

if ($firstExpr instanceof UnaryMinus) {
$firstExpr->setAttribute(AttributeKey::ORIGINAL_NODE, null);
// ** binds tighter than most operators, so operands with lower precedence must be
// wrapped in parentheses to keep the original semantics, e.g. pow(~3, 4) => (~3) ** 4
if ($this->powOperandAnalyzer->isLowerPrecedenceAsLeftOperand($firstExpr)) {
$firstExpr->setAttribute(AttributeKey::WRAPPED_IN_PARENTHESES, true);
}

if ($this->powOperandAnalyzer->isLowerPrecedenceAsRightOperand($secondExpr)) {
$secondExpr->setAttribute(AttributeKey::WRAPPED_IN_PARENTHESES, true);
}

return new Pow($firstExpr, $secondExpr);
Expand Down
70 changes: 70 additions & 0 deletions src/NodeAnalyzer/PowOperandAnalyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace Rector\NodeAnalyzer;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\AssignOp;
use PhpParser\Node\Expr\AssignRef;
use PhpParser\Node\Expr\BitwiseNot;
use PhpParser\Node\Expr\BooleanNot;
use PhpParser\Node\Expr\Cast;
use PhpParser\Node\Expr\ErrorSuppress;
use PhpParser\Node\Expr\Instanceof_;
use PhpParser\Node\Expr\Print_;
use PhpParser\Node\Expr\Ternary;
use PhpParser\Node\Expr\UnaryMinus;
use PhpParser\Node\Expr\UnaryPlus;
use PhpParser\Node\Expr\Yield_;
use PhpParser\Node\Expr\YieldFrom;

/**
* The ** operator binds tighter than most operators, so an operand that binds looser must be
* wrapped in parentheses when placed next to it, e.g. pow(~3, 4) must become (~3) ** 4.
* @see \Rector\Tests\NodeAnalyzer\PowOperandAnalyzerTest
*/
final class PowOperandAnalyzer
{
/**
* Operators that bind looser than ** on the left-hand side, so $operand ** $y would be
* misparsed without parentheses. Unary operators and casts are legal bare on the right-hand
* side of ** (2 ** -3), so they only need wrapping when used as the left operand.
*/
public function isLowerPrecedenceAsLeftOperand(Expr $expr): bool
{
// a plain Assign as left operand is already parenthesized by BetterStandardPrinter,
// wrapping it again here would produce a double set of parentheses
if ($expr instanceof Assign) {
return false;
}

if ($expr instanceof UnaryMinus
|| $expr instanceof UnaryPlus
|| $expr instanceof BitwiseNot
|| $expr instanceof BooleanNot
|| $expr instanceof ErrorSuppress
|| $expr instanceof Cast
|| $expr instanceof Instanceof_) {
return true;
}

return $this->isLowerPrecedenceAsRightOperand($expr);
}

/**
* Operators that would otherwise swallow the ** expression on either side, e.g.
* pow(2, $a ? 3 : 4) must become 2 ** ($a ? 3 : 4), not 2 ** $a ? 3 : 4.
*/
public function isLowerPrecedenceAsRightOperand(Expr $expr): bool
{
return $expr instanceof Ternary
|| $expr instanceof Assign
|| $expr instanceof AssignRef
|| $expr instanceof AssignOp
|| $expr instanceof Print_
|| $expr instanceof Yield_
|| $expr instanceof YieldFrom;
}
}
76 changes: 76 additions & 0 deletions tests/NodeAnalyzer/PowOperandAnalyzerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\NodeAnalyzer;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\AssignOp\Plus as AssignPlus;
use PhpParser\Node\Expr\BitwiseNot;
use PhpParser\Node\Expr\Cast\Int_ as CastInt;
use PhpParser\Node\Expr\Instanceof_;
use PhpParser\Node\Expr\Ternary;
use PhpParser\Node\Expr\UnaryMinus;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\Int_;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Rector\NodeAnalyzer\PowOperandAnalyzer;

final class PowOperandAnalyzerTest extends TestCase
{
private PowOperandAnalyzer $powOperandAnalyzer;

protected function setUp(): void
{
$this->powOperandAnalyzer = new PowOperandAnalyzer();
}

#[DataProvider('provideLeftOperand')]
public function testLeftOperand(Expr $expr, bool $expected): void
{
$this->assertSame($expected, $this->powOperandAnalyzer->isLowerPrecedenceAsLeftOperand($expr));
}

#[DataProvider('provideRightOperand')]
public function testRightOperand(Expr $expr, bool $expected): void
{
$this->assertSame($expected, $this->powOperandAnalyzer->isLowerPrecedenceAsRightOperand($expr));
}

/**
* @return iterable<array{Expr, bool}>
*/
public static function provideLeftOperand(): iterable
{
yield [new BitwiseNot(new Int_(3)), true];
yield [new UnaryMinus(new Int_(3)), true];
yield [new CastInt(new Variable('a')), true];
yield [new Instanceof_(new Variable('a'), new Name('DateTime')), true];
yield [new Ternary(new Variable('a'), new Int_(1), new Int_(2)), true];
yield [new AssignPlus(new Variable('a'), new Int_(4)), true];

// a plain Assign is already parenthesized by the printer on the left side
yield [new Assign(new Variable('a'), new Int_(4)), false];
yield [new Variable('a'), false];
yield [new Int_(3), false];
}

/**
* @return iterable<array{Expr, bool}>
*/
public static function provideRightOperand(): iterable
{
yield [new Ternary(new Variable('a'), new Int_(1), new Int_(2)), true];
yield [new Assign(new Variable('a'), new Int_(4)), true];
yield [new AssignPlus(new Variable('a'), new Int_(4)), true];

// unary operators and casts are legal bare on the right side of **
yield [new BitwiseNot(new Int_(3)), false];
yield [new UnaryMinus(new Int_(3)), false];
yield [new CastInt(new Variable('a')), false];
yield [new Variable('a'), false];
}
}
Loading