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 +----- +