From 00e7c77eee801f8818d2fd3ce80a7c56ecdd7b8d Mon Sep 17 00:00:00 2001 From: dualfroz Date: Thu, 3 Sep 2026 14:09:59 +0200 Subject: [PATCH] fix: use PHP 8 concatenation precedence in the pretty printer The precedence table gave "." the same precedence as "+" and "-", which is the PHP 7 ordering. PHP 8.0 lowered "." below the arithmetic and shift operators, so printing ('abc' . 1) + 2 produced 'abc' . 1 + 2, which PHP 8 reparses as 'abc' . (1 + 2). The same applied to "-", "<<" and ">>". The printer already targets a specific PHP version and already varies parenthesization by version, so select the concatenation precedence the same way: for a target of PHP 8.0 or newer, place "." between the shift operators and the pipe operator, matching the PHP8 block of grammar/php.y. Targets below 8.0 keep the PHP 7 table, which is correct for them and produces unchanged output. Fixes #1041 --- doc/component/Pretty_printing.markdown | 4 ++ lib/PhpParser/PhpVersion.php | 8 +++ lib/PhpParser/PrettyPrinterAbstract.php | 23 ++++++-- .../prettyPrinter/expr/concatPrecedence.test | 55 +++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 test/code/prettyPrinter/expr/concatPrecedence.test diff --git a/doc/component/Pretty_printing.markdown b/doc/component/Pretty_printing.markdown index 35c3681..71c832a 100644 --- a/doc/component/Pretty_printing.markdown +++ b/doc/component/Pretty_printing.markdown @@ -67,6 +67,10 @@ The behaviors controlled by `phpVersion` (defaults to PHP 7.4) are: for this has been removed with the introduction of flexible heredoc/nowdoc strings. * For PHP >= 7.3, heredoc/nowdoc strings are indented just like regular code. This was allowed with the introduction of flexible heredoc/nowdoc strings. +* For PHP >= 8.0, the concatenation operator is treated as binding less tightly than the arithmetic + and shift operators, matching the precedence change in PHP 8.0. For older versions `.` keeps the + same precedence as `+` and `-`. This determines where parentheses are required, so the printed + code only reparses into the same AST under the targeted version. The default pretty printer does not provide functionality for fine-grained customization of code formatting. diff --git a/lib/PhpParser/PhpVersion.php b/lib/PhpParser/PhpVersion.php index 9517d72..56fde97 100644 --- a/lib/PhpParser/PhpVersion.php +++ b/lib/PhpParser/PhpVersion.php @@ -172,4 +172,12 @@ class PhpVersion { public function supportsNewDereferenceWithoutParentheses(): bool { return $this->id >= 80400; } + + /** + * Whether the concatenation operator has lower precedence than the arithmetic and shift + * operators. Prior to PHP 8.0, "." had the same precedence as "+" and "-". + */ + public function hasLowerConcatPrecedence(): bool { + return $this->id >= 80000; + } } diff --git a/lib/PhpParser/PrettyPrinterAbstract.php b/lib/PhpParser/PrettyPrinterAbstract.php index 7a77be4..2feb51e 100644 --- a/lib/PhpParser/PrettyPrinterAbstract.php +++ b/lib/PhpParser/PrettyPrinterAbstract.php @@ -33,6 +33,13 @@ abstract class PrettyPrinterAbstract implements PrettyPrinter { protected const MAX_PRECEDENCE = 1000; + /** + * @var array{int, int, int} Precedence of the concatenation operator on PHP >= 8.0, where it + * binds less tightly than the arithmetic and shift operators, but + * more tightly than the pipe operator. + */ + protected const PHP8_CONCAT_PRECEDENCE = [62, 63, 62]; + /** @var array */ protected array $precedenceMap = [ // [precedence, precedenceLHS, precedenceRHS] @@ -59,7 +66,8 @@ abstract class PrettyPrinterAbstract implements PrettyPrinter { BinaryOp\Mod::class => [ 40, 41, 40], BinaryOp\Plus::class => [ 50, 51, 50], BinaryOp\Minus::class => [ 50, 51, 50], - // FIXME: This precedence is incorrect for PHP 8. + // This is the PHP 7 precedence. For PHP >= 8.0 the constructor replaces it with + // PHP8_CONCAT_PRECEDENCE. BinaryOp\Concat::class => [ 50, 51, 50], BinaryOp\ShiftLeft::class => [ 60, 61, 60], BinaryOp\ShiftRight::class => [ 60, 61, 60], @@ -179,9 +187,13 @@ abstract class PrettyPrinterAbstract implements PrettyPrinter { * * PhpVersion $phpVersion: The PHP version to target (default to PHP 7.4). This option * controls compatibility of the generated code with older PHP * versions in cases where a simple stylistic choice exists (e.g. - * array() vs []). It is safe to pretty-print an AST for a newer - * PHP version while specifying an older target (but the result will - * of course not be compatible with the older version in that case). + * array() vs []). It also selects the operator precedence table, + * which changed for the concatenation operator in PHP 8.0, so the + * printed code is only guaranteed to parse back into the same AST + * under the targeted version. It is safe to pretty-print an AST for + * a newer PHP version while specifying an older target (but the + * result will of course not be compatible with the older version in + * that case). * * string $newline: The newline style to use. Should be "\n" (default) or "\r\n". * * string $indent: The indentation to use. Should either be all spaces or a single * tab. Defaults to four spaces (" "). @@ -195,6 +207,9 @@ abstract class PrettyPrinterAbstract implements PrettyPrinter { */ public function __construct(array $options = []) { $this->phpVersion = $options['phpVersion'] ?? PhpVersion::fromComponents(7, 4); + if ($this->phpVersion->hasLowerConcatPrecedence()) { + $this->precedenceMap[BinaryOp\Concat::class] = self::PHP8_CONCAT_PRECEDENCE; + } $this->newline = $options['newline'] ?? "\n"; if ($this->newline !== "\n" && $this->newline != "\r\n") { diff --git a/test/code/prettyPrinter/expr/concatPrecedence.test b/test/code/prettyPrinter/expr/concatPrecedence.test new file mode 100644 index 0000000..a16a05d --- /dev/null +++ b/test/code/prettyPrinter/expr/concatPrecedence.test @@ -0,0 +1,55 @@ +Precedence of the concatenation operator +----- +