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
This commit is contained in:
dualfroz committed 2026-09-03 14:09:59 +02:00
1 parent 09376a3b68
commit 00e7c77eee
4 files changed
+86 -4

No files matched your search

+19 -4
View File
@@ -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<class-string, array{int, int, int}> */
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") {