diff --git a/lib/PhpParser/PrettyPrinterAbstract.php b/lib/PhpParser/PrettyPrinterAbstract.php index 4e4ba9a844..8a38ac5603 100644 --- a/lib/PhpParser/PrettyPrinterAbstract.php +++ b/lib/PhpParser/PrettyPrinterAbstract.php @@ -605,6 +605,13 @@ protected function p( Node $node, int $precedence = self::MAX_PRECEDENCE, int $lhsPrecedence = self::MAX_PRECEDENCE, bool $parentFormatPreserved = false ): string { + if ($lhsPrecedence <= $this->precedenceMap[Expr\UnaryMinus::class][0] + && ($node instanceof Scalar\Int_ || $node instanceof Scalar\Float_) + ) { + $result = $this->p($node); + return strpos($result, '-') === 0 ? '(' . $result . ')' : $result; + } + // No orig tokens means this is a normal pretty print without preservation of formatting if (!$this->origTokens) { return $this->{'p' . $node->getType()}($node, $precedence, $lhsPrecedence); @@ -744,11 +751,11 @@ protected function p( $origIndentLevel = $this->indentLevel; $this->setIndentLevel(max($this->origTokens->getIndentationBefore($subStartPos) + $indentAdjustment, 0)); - // If it's the same node that was previously in this position, it certainly doesn't - // need fixup. It's important to check this here, because our fixup checks are more - // conservative than strictly necessary. + // Numeric scalars may change sign and need fixup even if they remain the same node. + // Other nodes in the same position do not need our conservative fixup checks. if (isset($fixupInfo[$subNodeName]) - && $subNode->getAttribute('origNode') !== $origSubNode + && ($subNode->getAttribute('origNode') !== $origSubNode + || $subNode instanceof Scalar\Int_ || $subNode instanceof Scalar\Float_) ) { $fixup = $fixupInfo[$subNodeName]; $res = $this->pFixup($fixup, $subNode, $class, $subStartPos, $subEndPos); diff --git a/test/PhpParser/PrettyPrinterTest.php b/test/PhpParser/PrettyPrinterTest.php index aa956377ad..985948a438 100644 --- a/test/PhpParser/PrettyPrinterTest.php +++ b/test/PhpParser/PrettyPrinterTest.php @@ -165,6 +165,60 @@ public static function provideTestUnnaturalLiterals() { ]; } + public function testNegativeNumbersInPow(): void { + $prettyPrinter = new Standard(); + $factory = new BuilderFactory(); + foreach ([ + [new Int_(-5), '(-5) ** 2'], + [$factory->val(-5), '(-5) ** 2'], + [new Float_(-1.5), '(-1.5) ** 2'], + [$factory->val(-1.5), '(-1.5) ** 2'], + [new Int_(-5, ['kind' => Int_::KIND_BIN]), '(-0b101) ** 2'], + [new Int_(-5, ['kind' => Int_::KIND_OCT]), '(-05) ** 2'], + [new Int_(-5, ['kind' => Int_::KIND_HEX]), '(-0x5) ** 2'], + [new Int_(PHP_INT_MIN), '(-' . PHP_INT_MAX . '-1) ** 2'], + [new Float_(-0.0), '(-0.0) ** 2'], + [new Float_(-INF), '(-1.0E+1000) ** 2'], + [new Int_(5), '5 ** 2'], + [new Float_(1.5), '1.5 ** 2'], + ] as [$base, $expected]) { + $expr = new Expr\BinaryOp\Pow($base, new Int_(2)); + $result = $prettyPrinter->prettyPrintExpr($expr); + $this->assertSame($expected, $result); + $this->assertSame($base->value ** 2, eval('return ' . $result . ';')); + } + + $expr = new Expr\BinaryOp\Pow(new Int_(5), new Int_(-2)); + $this->assertSame('5 ** -2', $prettyPrinter->prettyPrintExpr($expr)); + $expr = new Expr\BinaryOp\Mul( + new Expr\BinaryOp\Pow(new Int_(-5), new Int_(2)), new Int_(3) + ); + $this->assertSame('(-5) ** 2 * 3', $prettyPrinter->prettyPrintExpr($expr)); + } + + public function testFormatPreservingNegativeNumbersInPow(): void { + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $prettyPrinter = new Standard(); + $traverser = new NodeTraverser(new NodeVisitor\CloningVisitor()); + foreach ([ + ['5', new Int_(-5), '(-5)'], + ['1.5', new Float_(-1.5), '(-1.5)'], + ] as [$literal, $base, $expected]) { + $oldStmts = $parser->parse('getTokens(); + foreach ([true, false] as $replaceNode) { + $newStmts = $traverser->traverse($oldStmts); + if ($replaceNode) { + $newStmts[0]->expr->left = $base; + } else { + $newStmts[0]->expr->left->value = $base->value; + } + $result = $prettyPrinter->printFormatPreserving($newStmts, $oldStmts, $oldTokens); + $this->assertSame('