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:
1 parent
09376a3b68
commit
00e7c77eee
4 files changed
+86
-4
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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") {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
Precedence of the concatenation operator
|
||||
-----
|
||||
<?php
|
||||
|
||||
echo ('abc' . 1) + 2;
|
||||
echo ('abc' . 1) - 2;
|
||||
echo 2 + ('abc' . 1);
|
||||
echo 'abc' . (1 + 2);
|
||||
echo (1 + 2) . 'abc';
|
||||
echo ('a' . 'b') << 1;
|
||||
echo 'a' . (1 << 2);
|
||||
echo 'a' . 'b' . 'c';
|
||||
echo 'a' . ('b' . 'c');
|
||||
echo ('a' . 'b') == 'c';
|
||||
echo 'a' . ('b' == 'c');
|
||||
-----
|
||||
!!version=8.0
|
||||
echo ('abc' . 1) + 2;
|
||||
echo ('abc' . 1) - 2;
|
||||
echo 2 + ('abc' . 1);
|
||||
echo 'abc' . 1 + 2;
|
||||
echo 1 + 2 . 'abc';
|
||||
echo ('a' . 'b') << 1;
|
||||
echo 'a' . 1 << 2;
|
||||
echo 'a' . 'b' . 'c';
|
||||
echo 'a' . ('b' . 'c');
|
||||
echo 'a' . 'b' == 'c';
|
||||
echo 'a' . ('b' == 'c');
|
||||
-----
|
||||
<?php
|
||||
|
||||
echo ('abc' . 1) + 2;
|
||||
echo ('abc' . 1) - 2;
|
||||
echo 2 + ('abc' . 1);
|
||||
echo 'abc' . (1 + 2);
|
||||
echo (1 + 2) . 'abc';
|
||||
echo ('a' . 'b') << 1;
|
||||
echo 'a' . (1 << 2);
|
||||
echo 'a' . 'b' . 'c';
|
||||
echo 'a' . ('b' . 'c');
|
||||
echo ('a' . 'b') == 'c';
|
||||
echo 'a' . ('b' == 'c');
|
||||
-----
|
||||
!!version=7.4
|
||||
echo 'abc' . 1 + 2;
|
||||
echo 'abc' . 1 - 2;
|
||||
echo 2 + ('abc' . 1);
|
||||
echo 'abc' . (1 + 2);
|
||||
echo 1 + 2 . 'abc';
|
||||
echo 'a' . 'b' << 1;
|
||||
echo 'a' . (1 << 2);
|
||||
echo 'a' . 'b' . 'c';
|
||||
echo 'a' . ('b' . 'c');
|
||||
echo 'a' . 'b' == 'c';
|
||||
echo 'a' . ('b' == 'c');
|
||||
Reference in new issue
Block a user