diff --git a/rules-tests/Php56/Rector/FuncCall/PowToExpRector/Fixture/keep_bare_right_operand.php.inc b/rules-tests/Php56/Rector/FuncCall/PowToExpRector/Fixture/keep_bare_right_operand.php.inc new file mode 100644 index 00000000000..9221e549a72 --- /dev/null +++ b/rules-tests/Php56/Rector/FuncCall/PowToExpRector/Fixture/keep_bare_right_operand.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/rules-tests/Php56/Rector/FuncCall/PowToExpRector/Fixture/lower_precedence_operands.php.inc b/rules-tests/Php56/Rector/FuncCall/PowToExpRector/Fixture/lower_precedence_operands.php.inc new file mode 100644 index 00000000000..a78172663d0 --- /dev/null +++ b/rules-tests/Php56/Rector/FuncCall/PowToExpRector/Fixture/lower_precedence_operands.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/rules/Php56/Rector/FuncCall/PowToExpRector.php b/rules/Php56/Rector/FuncCall/PowToExpRector.php index de81227758e..27b2f40bb32 100644 --- a/rules/Php56/Rector/FuncCall/PowToExpRector.php +++ b/rules/Php56/Rector/FuncCall/PowToExpRector.php @@ -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; @@ -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( @@ -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); diff --git a/src/NodeAnalyzer/PowOperandAnalyzer.php b/src/NodeAnalyzer/PowOperandAnalyzer.php new file mode 100644 index 00000000000..3c8a194f02c --- /dev/null +++ b/src/NodeAnalyzer/PowOperandAnalyzer.php @@ -0,0 +1,70 @@ +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; + } +} diff --git a/tests/NodeAnalyzer/PowOperandAnalyzerTest.php b/tests/NodeAnalyzer/PowOperandAnalyzerTest.php new file mode 100644 index 00000000000..f5a918e4503 --- /dev/null +++ b/tests/NodeAnalyzer/PowOperandAnalyzerTest.php @@ -0,0 +1,76 @@ +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 + */ + 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 + */ + 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]; + } +}