Fix precedence rules

This commit is contained in:
Fabien Potencier
2025-02-03 22:22:16 +01:00
parent 0cb5c0e8d0
commit 8ff19090f3
29 changed files with 457 additions and 186 deletions
+1
View File
@@ -1,4 +1,5 @@
/.github/ export-ignore
/bin/ export-ignore
/doc/ export-ignore
/extra/ export-ignore
/tests/ export-ignore
+2
View File
@@ -1,5 +1,7 @@
# 3.21.0 (2025-XX-XX)
* Deprecate using the `|` operator in an expression with `+` or `-` without using parentheses to clarify precedence
* Deprecate operator precedence outside of the [0, 512] range
* Introduce expression parser classes to describe operators and operands provided by extensions
instead of arrays (it comes with many deprecations that are documented in
the ``deprecated`` documentation chapter)
+70 -47
View File
@@ -1,65 +1,88 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Twig\Environment;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\ExpressionParserType;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\ExpressionParser\PrefixExpressionParserInterface;
use Twig\Loader\ArrayLoader;
require_once dirname(__DIR__).'/vendor/autoload.php';
function printExpressionParsers($output, array $expressionParsers, bool $withAssociativity = false)
{
if ($withAssociativity) {
fwrite($output, "\n=========== =========== =============\n");
fwrite($output, "Precedence Operator Associativity\n");
fwrite($output, "=========== =========== =============\n");
} else {
fwrite($output, "\n=========== ===========\n");
fwrite($output, "Precedence Operator\n");
fwrite($output, "=========== ===========\n");
}
usort($expressionParsers, function($a, $b) {
$aPrecedence = $a->getPrecedenceChange() ? $a->getPrecedenceChange()->getNewPrecedence() : $a->getPrecedence();
$bPrecedence = $b->getPrecedenceChange() ? $b->getPrecedenceChange()->getNewPrecedence() : $b->getPrecedence();
return $bPrecedence - $aPrecedence;
});
$current = \PHP_INT_MAX;
foreach ($expressionParsers as $expressionParser) {
$precedence = $expressionParser->getPrecedenceChange() ? $expressionParser->getPrecedenceChange()->getNewPrecedence() : $expressionParser->getPrecedence();
if ($precedence !== $current) {
$current = $precedence;
if ($withAssociativity) {
fwrite($output, \sprintf("\n%-11d %-11s %s", $precedence, $expressionParser->getName(), InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right'));
} else {
fwrite($output, \sprintf("\n%-11d %s", $precedence, $expressionParser->getName()));
}
} else {
fwrite($output, "\n".str_repeat(' ', 12).$expressionParser->getName());
}
}
fwrite($output, "\n");
}
$output = fopen(dirname(__DIR__).'/doc/operators_precedence.rst', 'w');
$twig = new Environment(new ArrayLoader([]));
$prefixExpressionParsers = [];
$infixExpressionParsers = [];
$expressionParsers = [];
foreach ($twig->getExpressionParsers() as $expressionParser) {
if ($expressionParser instanceof PrefixExpressionParserInterface) {
$prefixExpressionParsers[] = $expressionParser;
} elseif ($expressionParser instanceof InfixExpressionParserInterface) {
$infixExpressionParsers[] = $expressionParser;
}
$expressionParsers[] = $expressionParser;
}
fwrite($output, "Unary operators precedence:\n");
printExpressionParsers($output, $prefixExpressionParsers);
fwrite($output, "\n=========== ================ ======= ============= ===========\n");
fwrite($output, "Precedence Operator Type Associativity Description\n");
fwrite($output, "=========== ================ ======= ============= ===========");
fwrite($output, "\nBinary and Ternary operators precedence:\n");
printExpressionParsers($output, $infixExpressionParsers, true);
usort($expressionParsers, fn ($a, $b) => $b->getPrecedence() <=> $a->getPrecedence());
$previous = null;
foreach ($expressionParsers as $expressionParser) {
$precedence = $expressionParser->getPrecedence();
$previousPrecedence = $previous ? $previous->getPrecedence() : \PHP_INT_MAX;
$associativity = $expressionParser instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right') : 'n/a';
$previousAssociativity = $previous ? ($previous instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $previous->getAssociativity() ? 'Left' : 'Right') : 'n/a') : 'n/a';
if ($previousPrecedence !== $precedence) {
$previous = null;
}
fwrite($output, rtrim(\sprintf("\n%-11s %-16s %-7s %-13s %s\n",
(!$previous || $previousPrecedence !== $precedence ? $precedence : '').($expressionParser->getPrecedenceChange() ? ' => '.$expressionParser->getPrecedenceChange()->getNewPrecedence() : ''),
'``'.$expressionParser->getName().'``',
!$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '',
!$previous || $previousAssociativity !== $associativity ? $associativity : '',
$expressionParser instanceof ExpressionParserDescriptionInterface ? $expressionParser->getDescription() : '',
)));
$previous = $expressionParser;
}
fwrite($output, "\n=========== ================ ======= ============= ===========\n");
fwrite($output, "\nWhen a precedence will change in 4.0, the new precedence is indicated by the arrow ``=>``.\n");
fwrite($output, "\nHere is the same table for Twig 4.0 with adjusted precedences:\n");
fwrite($output, "\n=========== ================ ======= ============= ===========\n");
fwrite($output, "Precedence Operator Type Associativity Description\n");
fwrite($output, "=========== ================ ======= ============= ===========");
usort($expressionParsers, function($a, $b) {
$aPrecedence = $a->getPrecedenceChange() ? $a->getPrecedenceChange()->getNewPrecedence() : $a->getPrecedence();
$bPrecedence = $b->getPrecedenceChange() ? $b->getPrecedenceChange()->getNewPrecedence() : $b->getPrecedence();
return $bPrecedence - $aPrecedence;
});
$previous = null;
foreach ($expressionParsers as $expressionParser) {
$precedence = $expressionParser->getPrecedenceChange() ? $expressionParser->getPrecedenceChange()->getNewPrecedence() : $expressionParser->getPrecedence();
$previousPrecedence = $previous ? ($previous->getPrecedenceChange() ? $previous->getPrecedenceChange()->getNewPrecedence() : $previous->getPrecedence()) : \PHP_INT_MAX;
$associativity = $expressionParser instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right') : 'n/a';
$previousAssociativity = $previous ? ($previous instanceof InfixExpressionParserInterface ? (InfixAssociativity::Left === $previous->getAssociativity() ? 'Left' : 'Right') : 'n/a') : 'n/a';
if ($previousPrecedence !== $precedence) {
$previous = null;
}
fwrite($output, rtrim(\sprintf("\n%-11s %-16s %-7s %-13s %s\n",
!$previous || $previousPrecedence !== $precedence ? $precedence : '',
'``'.$expressionParser->getName().'``',
!$previous || ExpressionParserType::getType($previous) !== ExpressionParserType::getType($expressionParser) ? ExpressionParserType::getType($expressionParser)->value : '',
!$previous || $previousAssociativity !== $associativity ? $associativity : '',
$expressionParser instanceof ExpressionParserDescriptionInterface ? $expressionParser->getDescription() : '',
)));
$previous = $expressionParser;
}
fwrite($output, "\n=========== ================ ======= ============= ===========\n");
fclose($output);
+18
View File
@@ -378,6 +378,8 @@ Node
Operators
---------
* An operator precedence must be part of the [0, 512] range as of Twig 3.20.
* The ``.`` operator allows accessing class constants as of Twig 3.15.
This can be a BC break if you don't use UPPERCASE constant names.
@@ -433,6 +435,22 @@ Operators
{{ (not 1) * 2 }} {# this is equivalent to what Twig 4.x will do without the parentheses #}
* Using the ``|`` operator in an expression with ``+`` or ``-`` without explicit
parentheses to clarify precedence triggers a deprecation as of Twig 3.20 (in
Twig 4.0, ``|`` will have a higher precedence than ``+`` and ``-``).
For example, the following expression will trigger a deprecation in Twig 3.20::
{{ -1|abs }}
To avoid the deprecation, add parentheses to clarify the precedence::
{{ -(1|abs) }} {# this is equivalent to what Twig 3.x does without the parentheses #}
{# or #}
{{ (-1)|abs }} {# this is equivalent to what Twig 4.x will do without the parentheses #}
* The ``Twig\Extension\ExtensionInterface::getOperators()`` method is deprecated
as of Twig 3.20, use ``Twig\Extension\ExtensionInterface::getExpressionParsers()``
instead:
+10 -4
View File
@@ -15,15 +15,21 @@ separator using the additional arguments:
{{ 9800.333|number_format(2, '.', ',') }}
To format negative numbers or math calculation, wrap the previous statement
with parentheses (needed because of Twig's :ref:`precedence of operators
<twig-expressions>`):
To format negative numbers, wrap the previous statement with parentheses (note
that as of Twig 3.20, not using parentheses is deprecated as the filter
operator will change precedence in Twig 4.0):
.. code-block:: twig
{{ -9800.333|number_format(2, '.', ',') }} {# outputs : -9 #}
{{ (-9800.333)|number_format(2, '.', ',') }} {# outputs : -9,800.33 #}
{{ 1 + 0.2|number_format(2) }} {# outputs : 1.2 #}
To format math calculation, wrap the previous statement with parentheses
(needed because of Twig's :ref:`precedence of operators -<twig-expressions>`):
.. code-block:: twig
{{ 1 + 0.2|number_format(2) }} {# outputs : 1.2 #}
{{ (1 + 0.2)|number_format(2) }} {# outputs : 1.20 #}
If no formatting options are provided then Twig will use the default formatting
+100 -53
View File
@@ -1,57 +1,104 @@
Unary operators precedence:
=========== ===========
Precedence Operator
=========== ===========
=========== ================ ======= ============= ===========
Precedence Operator Type Associativity Description
=========== ================ ======= ============= ===========
512 => 300 ``|`` infix Left Twig filter call
``(`` Twig function call
``.`` Get an attribute on a variable
``[`` Array access
500 ``-`` prefix n/a
``+``
300 => 5 ``??`` infix Right Null coalescing operator (a ?? b)
250 ``=>`` infix Left Arrow function (x => expr)
200 ``**`` infix Right Exponentiation operator
100 ``is`` infix Left Twig tests
``is not`` Twig tests
60 ``*`` infix Left
``/``
``//`` Floor division
``%``
50 => 70 ``not`` prefix n/a
40 => 27 ``~`` infix Left
30 ``+`` infix Left
``-``
25 ``..`` infix Left
20 ``==`` infix Left
``!=``
``<=>``
``<``
``>``
``>=``
``<=``
``not in``
``in``
``matches``
``starts with``
``ends with``
``has some``
``has every``
18 ``b-and`` infix Left
17 ``b-xor`` infix Left
16 ``b-or`` infix Left
15 ``and`` infix Left
12 ``xor`` infix Left
10 ``or`` infix Left
5 ``?:`` infix Right Elvis operator (a ?: b)
``?:`` Elvis operator (a ?: b)
0 ``(`` prefix n/a Explicit group expression (a)
``literal`` A literal value (boolean, string, number, sequence, mapping, ...)
``?`` infix Left Conditional operator (a ? b : c)
=========== ================ ======= ============= ===========
500 -
+
70 not
0 (
literal
When a precedence will change in 4.0, the new precedence is indicated by the arrow ``=>``.
Binary and Ternary operators precedence:
Here is the same table for Twig 4.0 with adjusted precedences:
=========== =========== =============
Precedence Operator Associativity
=========== =========== =============
300 . Left
[
|
(
250 => Left
200 ** Right
100 is Left
is not
60 * Left
/
//
%
30 + Left
-
27 ~ Left
25 .. Left
20 == Left
!=
<=>
<
>
>=
<=
not in
in
matches
starts with
ends with
has some
has every
18 b-and Left
17 b-xor Left
16 b-or Left
15 and Left
12 xor Left
10 or Left
5 ?: Right
??
0 ? Left
=========== ============== ======= ============= ===========
Precedence Operator Type Associativity Description
=========== ============== ======= ============= ===========
512 `(` infix Left Twig function call
`.` Get an attribute on a variable
`[` Array access
500 `-` prefix n/a
`+`
300 `|` infix Left Twig filter call
250 `=>` infix Left Arrow function (x => expr)
200 `**` infix Right Exponentiation operator
100 `is` infix Left Twig tests
`is not` Twig tests
70 `not` prefix n/a
60 `*` infix Left
`/`
`//` Floor division
`%`
30 `+` infix Left
`-`
27 `~` infix Left
25 `..` infix Left
20 `==` infix Left
`!=`
`<=>`
`<`
`>`
`>=`
`<=`
`not in`
`in`
`matches`
`starts with`
`ends with`
`has some`
`has every`
18 `b-and` infix Left
17 `b-xor` infix Left
16 `b-or` infix Left
15 `and` infix Left
12 `xor` infix Left
10 `or` infix Left
5 `??` infix Right Null coalescing operator (a ?? b)
`?:` Elvis operator (a ?: b)
`?:` Elvis operator (a ?: b)
0 `(` prefix n/a Explicit group expression (a)
`literal` A literal value (boolean, string, number, sequence, mapping, ...)
`?` infix Left Conditional operator (a ? b : c)
=========== ============== ======= ============= ===========
+4 -24
View File
@@ -186,28 +186,6 @@ filters.
{{ ('HELLO' ~ 'FABIEN')|lower }}
A common mistake is to forget using parentheses for filters on negative
numbers as a negative number in Twig is represented by the ``-`` operator
followed by a positive number. As the ``-`` operator has a lower precedence
than the filter operator, it can lead to confusion:
.. code-block:: twig
{{ -1|abs }} {# returns -1 #}
{{ -1**0 }} {# returns -1 #}
{# as it is equivalent to #}
{{ -(1|abs) }}
{{ -(1**0) }}
For such cases, use parentheses to force the precedence:
.. code-block:: twig
{{ (-1)|abs }} {# returns 1 as expected #}
{{ (-1)**0 }} {# returns 1 as expected #}
Functions
---------
@@ -703,14 +681,16 @@ Twig allows you to do math in templates; the following operators are supported:
``4``.
* ``//``: Divides two numbers and returns the floored integer result. ``{{ 20
// 7 }}`` is ``2``, ``{{ -20 // 7 }}`` is ``-3`` (this is just syntactic
// 7 }}`` is ``2``, ``{{ -20 // 7 }}`` is ``-3`` (this is just syntactic
sugar for the :doc:`round<filters/round>` filter).
* ``*``: Multiplies the left operand with the right one. ``{{ 2 * 2 }}`` would
return ``4``.
* ``**``: Raises the left operand to the power of the right operand. ``{{ 2 **
3 }}`` would return ``8``.
3 }}`` would return ``8``. Be careful as the ``**`` operator is right
associative, which means that ``{{ -1**0 }}`` is equivalent to ``{{ -(1**0)
}}`` and not ``{{ (-1)**0 }}``.
.. _template_logic:
@@ -0,0 +1,17 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\ExpressionParser;
interface ExpressionParserDescriptionInterface
{
public function getDescription(): string;
}
+10 -7
View File
@@ -44,7 +44,6 @@ final class ExpressionParsers implements \IteratorAggregate
public function __construct(
array $parsers = [],
) {
$this->precedenceChanges = null;
$this->add($parsers);
}
@@ -55,12 +54,16 @@ final class ExpressionParsers implements \IteratorAggregate
*/
public function add(array $parsers): self
{
foreach ($parsers as $operator) {
$type = ExpressionParserType::getType($operator);
$this->parsers[$type->value][$operator->getName()] = $operator;
$this->parsersByClass[$type->value][get_class($operator)] = $operator;
foreach ($operator->getAliases() as $alias) {
$this->aliases[$type->value][$alias] = $operator;
foreach ($parsers as $parser) {
if ($parser->getPrecedence() > 512 || $parser->getPrecedence() < 0) {
trigger_deprecation('twig/twig', '3.20', 'Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence());
// throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence()));
}
$type = ExpressionParserType::getType($parser);
$this->parsers[$type->value][$parser->getName()] = $parser;
$this->parsersByClass[$type->value][get_class($parser)] = $parser;
foreach ($parser->getAliases() as $alias) {
$this->aliases[$type->value][$alias] = $parser;
}
}
@@ -12,6 +12,7 @@
namespace Twig\ExpressionParser\Infix;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Node\Expression\AbstractExpression;
@@ -22,7 +23,7 @@ use Twig\Token;
/**
* @internal
*/
final class ArrowExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface
final class ArrowExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
{
@@ -35,6 +36,11 @@ final class ArrowExpressionParser extends AbstractExpressionParser implements In
return '=>';
}
public function getDescription(): string
{
return 'Arrow function (x => expr)';
}
public function getPrecedence(): int
{
return 250;
@@ -12,6 +12,7 @@
namespace Twig\ExpressionParser\Infix;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\ExpressionParser\PrecedenceChange;
@@ -23,7 +24,7 @@ use Twig\Token;
/**
* @internal
*/
class BinaryOperatorExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface
class BinaryOperatorExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function __construct(
/** @var class-string<AbstractBinary> */
@@ -32,6 +33,7 @@ class BinaryOperatorExpressionParser extends AbstractExpressionParser implements
private int $precedence,
private InfixAssociativity $associativity = InfixAssociativity::Left,
private ?PrecedenceChange $precedenceChange = null,
private ?string $description = null,
private array $aliases = [],
) {
}
@@ -56,6 +58,11 @@ class BinaryOperatorExpressionParser extends AbstractExpressionParser implements
return $this->name;
}
public function getDescription(): string
{
return $this->description ?? '';
}
public function getPrecedence(): int
{
return $this->precedence;
@@ -12,6 +12,7 @@
namespace Twig\ExpressionParser\Infix;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Node\Expression\AbstractExpression;
@@ -23,7 +24,7 @@ use Twig\Token;
/**
* @internal
*/
final class ConditionalTernaryExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface
final class ConditionalTernaryExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression
{
@@ -44,6 +45,11 @@ final class ConditionalTernaryExpressionParser extends AbstractExpressionParser
return '?';
}
public function getDescription(): string
{
return 'Conditional operator (a ? b : c)';
}
public function getPrecedence(): int
{
return 0;
@@ -13,6 +13,7 @@ namespace Twig\ExpressionParser\Infix;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Lexer;
@@ -30,7 +31,7 @@ use Twig\Token;
/**
* @internal
*/
final class DotExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface
final class DotExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
use ArgumentsTrait;
@@ -81,9 +82,14 @@ final class DotExpressionParser extends AbstractExpressionParser implements Infi
return '.';
}
public function getDescription(): string
{
return 'Get an attribute on a variable';
}
public function getPrecedence(): int
{
return 300;
return 512;
}
public function getAssociativity(): InfixAssociativity
@@ -13,8 +13,10 @@ namespace Twig\ExpressionParser\Infix;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\ExpressionParser\PrecedenceChange;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ConstantExpression;
@@ -24,7 +26,7 @@ use Twig\Token;
/**
* @internal
*/
final class FilterExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface
final class FilterExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
use ArgumentsTrait;
@@ -61,9 +63,19 @@ final class FilterExpressionParser extends AbstractExpressionParser implements I
return '|';
}
public function getDescription(): string
{
return 'Twig filter call';
}
public function getPrecedence(): int
{
return 300;
return 512;
}
public function getPrecedenceChange(): ?PrecedenceChange
{
return new PrecedenceChange('twig/twig', '3.20', 300);
}
public function getAssociativity(): InfixAssociativity
@@ -14,6 +14,7 @@ namespace Twig\ExpressionParser\Infix;
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Node\EmptyNode;
@@ -26,7 +27,7 @@ use Twig\Token;
/**
* @internal
*/
final class FunctionExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface
final class FunctionExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
use ArgumentsTrait;
@@ -72,9 +73,14 @@ final class FunctionExpressionParser extends AbstractExpressionParser implements
return '(';
}
public function getDescription(): string
{
return 'Twig function call';
}
public function getPrecedence(): int
{
return 300;
return 512;
}
public function getAssociativity(): InfixAssociativity
@@ -13,6 +13,7 @@
use Twig\Attribute\FirstClassTwigCallableReady;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Node\Expression\AbstractExpression;
@@ -27,7 +28,7 @@ use Twig\TwigTest;
/**
* @internal
*/
class IsExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface
class IsExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
use ArgumentsTrait;
@@ -71,6 +72,11 @@ class IsExpressionParser extends AbstractExpressionParser implements InfixExpres
return 'is';
}
public function getDescription(): string
{
return 'Twig tests';
}
public function getAssociativity(): InfixAssociativity
{
return InfixAssociativity::Left;
@@ -12,6 +12,7 @@
namespace Twig\ExpressionParser\Infix;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\InfixAssociativity;
use Twig\ExpressionParser\InfixExpressionParserInterface;
use Twig\Node\Expression\AbstractExpression;
@@ -26,7 +27,7 @@ use Twig\Token;
/**
* @internal
*/
final class SquareBracketExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface
final class SquareBracketExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
{
@@ -73,9 +74,14 @@ final class SquareBracketExpressionParser extends AbstractExpressionParser imple
return '[';
}
public function getDescription(): string
{
return 'Array access';
}
public function getPrecedence(): int
{
return 300;
return 512;
}
public function getAssociativity(): InfixAssociativity
@@ -13,6 +13,7 @@ namespace Twig\ExpressionParser\Prefix;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\PrefixExpressionParserInterface;
use Twig\Node\Expression\AbstractExpression;
use Twig\Node\Expression\ListExpression;
@@ -23,7 +24,7 @@ use Twig\Token;
/**
* @internal
*/
final class GroupingExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface
final class GroupingExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function parse(Parser $parser, Token $token): AbstractExpression
{
@@ -65,6 +66,11 @@ final class GroupingExpressionParser extends AbstractExpressionParser implements
return '(';
}
public function getDescription(): string
{
return 'Explicit group expression (a)';
}
public function getPrecedence(): int
{
return 0;
@@ -13,8 +13,7 @@ namespace Twig\ExpressionParser\Prefix;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserType;
use Twig\ExpressionParser\PrecedenceChange;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\PrefixExpressionParserInterface;
use Twig\Lexer;
use Twig\Node\Expression\AbstractExpression;
@@ -28,7 +27,7 @@ use Twig\Token;
/**
* @internal
*/
final class LiteralExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface
final class LiteralExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
{
private string $type = 'literal';
@@ -112,6 +111,11 @@ final class LiteralExpressionParser extends AbstractExpressionParser implements
return $this->type;
}
public function getDescription(): string
{
return 'A literal value (boolean, string, number, sequence, mapping, ...)';
}
public function getPrecedence(): int
{
// not used
@@ -12,6 +12,7 @@
namespace Twig\ExpressionParser\Prefix;
use Twig\ExpressionParser\AbstractExpressionParser;
use Twig\ExpressionParser\ExpressionParserDescriptionInterface;
use Twig\ExpressionParser\PrecedenceChange;
use Twig\ExpressionParser\PrefixExpressionParserInterface;
use Twig\Node\Expression\AbstractExpression;
@@ -22,7 +23,7 @@ use Twig\Token;
/**
* @internal
*/
final class UnaryOperatorExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface
final class UnaryOperatorExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface
{
public function __construct(
/** @var class-string<AbstractUnary> */
@@ -30,6 +31,7 @@ final class UnaryOperatorExpressionParser extends AbstractExpressionParser imple
private string $name,
private int $precedence,
private ?PrecedenceChange $precedenceChange = null,
private ?string $description = null,
private array $aliases = [],
) {
}
@@ -47,6 +49,11 @@ final class UnaryOperatorExpressionParser extends AbstractExpressionParser imple
return $this->name;
}
public function getDescription(): string
{
return $this->description ?? '';
}
public function getPrecedence(): int
{
return $this->precedence;
+16 -6
View File
@@ -328,12 +328,14 @@ final class CoreExtension extends AbstractExtension
public function getExpressionParsers(): array
{
return [
// unary operators
new UnaryOperatorExpressionParser(NotUnary::class, 'not', 50, new PrecedenceChange('twig/twig', '3.15', 70)),
new UnaryOperatorExpressionParser(NegUnary::class, '-', 500),
new UnaryOperatorExpressionParser(PosUnary::class, '+', 500),
new BinaryOperatorExpressionParser(ElvisBinary::class, '?:', 5, InfixAssociativity::Right, aliases: ['? :']),
new BinaryOperatorExpressionParser(NullCoalesceBinary::class, '??', 300, InfixAssociativity::Right, new PrecedenceChange('twig/twig', '3.15', 5)),
// binary operators
new BinaryOperatorExpressionParser(ElvisBinary::class, '?:', 5, InfixAssociativity::Right, description: 'Elvis operator (a ?: b)', aliases: ['? :']),
new BinaryOperatorExpressionParser(NullCoalesceBinary::class, '??', 300, InfixAssociativity::Right, new PrecedenceChange('twig/twig', '3.15', 5), description: 'Null coalescing operator (a ?? b)'),
new BinaryOperatorExpressionParser(OrBinary::class, 'or', 10),
new BinaryOperatorExpressionParser(XorBinary::class, 'xor', 12),
new BinaryOperatorExpressionParser(AndBinary::class, 'and', 15),
@@ -360,22 +362,30 @@ final class CoreExtension extends AbstractExtension
new BinaryOperatorExpressionParser(ConcatBinary::class, '~', 40, precedenceChange: new PrecedenceChange('twig/twig', '3.15', 27)),
new BinaryOperatorExpressionParser(MulBinary::class, '*', 60),
new BinaryOperatorExpressionParser(DivBinary::class, '/', 60),
new BinaryOperatorExpressionParser(FloorDivBinary::class, '//', 60),
new BinaryOperatorExpressionParser(FloorDivBinary::class, '//', 60, description: 'Floor division'),
new BinaryOperatorExpressionParser(ModBinary::class, '%', 60),
new BinaryOperatorExpressionParser(PowerBinary::class, '**', 200, InfixAssociativity::Right),
new BinaryOperatorExpressionParser(PowerBinary::class, '**', 200, InfixAssociativity::Right, description: 'Exponentiation operator'),
// ternary operator
new ConditionalTernaryExpressionParser(),
// Twig callables
new IsExpressionParser(),
new IsNotExpressionParser(),
new FilterExpressionParser(),
new FunctionExpressionParser(),
// get attribute operators
new DotExpressionParser(),
new SquareBracketExpressionParser(),
// group expression
new GroupingExpressionParser(),
new FilterExpressionParser(),
new FunctionExpressionParser(),
// arrow function
new ArrowExpressionParser(),
// all literals
new LiteralExpressionParser(),
];
}
+1 -1
View File
@@ -493,7 +493,7 @@ final class ExtensionSet
$expressionParsers = [];
foreach ($operators[0] as $operator => $op) {
$expressionParsers[] = new UnaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['precedence_change'] ?? null, $op['aliases'] ?? []);
$expressionParsers[] = new UnaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []);
}
foreach ($operators[1] as $operator => $op) {
$op['associativity'] = match ($op['associativity']) {
+14 -12
View File
@@ -15,6 +15,7 @@ namespace Twig;
use Twig\Error\SyntaxError;
use Twig\ExpressionParser\ExpressionParserInterface;
use Twig\ExpressionParser\ExpressionParsers;
use Twig\ExpressionParser\ExpressionParserType;
use Twig\ExpressionParser\Prefix\LiteralExpressionParser;
use Twig\ExpressionParser\PrefixExpressionParserInterface;
use Twig\Node\BlockNode;
@@ -563,10 +564,11 @@ class Parser
return;
}
if ($expr->hasExplicitParentheses()) {
return;
}
if ($expressionParser instanceof PrefixExpressionParserInterface) {
if ($expr->hasExplicitParentheses()) {
return;
}
/** @var AbstractExpression $node */
$node = $expr->getNode('node');
foreach ($precedenceChanges as $ep => $changes) {
@@ -575,17 +577,17 @@ class Parser
}
if ($node->hasAttribute('expression_parser') && $ep === $node->getAttribute('expression_parser')) {
$change = $expressionParser->getPrecedenceChange();
trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $expressionParser->getName(), $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('As the "%s" %s operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "%s" at line %d.', $expressionParser->getName(), ExpressionParserType::getType($expressionParser)->value, $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
}
}
} else {
foreach ($precedenceChanges[$expressionParser] as $ep) {
foreach ($expr as $node) {
/** @var AbstractExpression $node */
if ($node->hasAttribute('expression_parser') && $ep === $node->getAttribute('expression_parser') && !$node->hasExplicitParentheses()) {
$change = $ep->getPrecedenceChange();
trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.', $ep->getName(), $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
}
}
foreach ($precedenceChanges[$expressionParser] as $ep) {
foreach ($expr as $node) {
/** @var AbstractExpression $node */
if ($node->hasAttribute('expression_parser') && $ep === $node->getAttribute('expression_parser') && !$node->hasExplicitParentheses()) {
$change = $ep->getPrecedenceChange();
trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('As the "%s" %s operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "%s" at line %d.', $ep->getName(), ExpressionParserType::getType($ep)->value, $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
}
}
}
+80
View File
@@ -600,6 +600,86 @@ class ExpressionParserTest extends TestCase
return $expression;
}
/**
* @dataProvider getBindingPowerTests
*/
public function testBindingPower(string $expression, string $expectedExpression, mixed $expectedResult, array $context = [])
{
$env = new Environment(new ArrayLoader([
'expression' => $expression,
'expected' => $expectedExpression,
]));
$this->assertSame($env->render('expected', $context), $env->render('expression', $context));
$this->assertEquals($expectedResult, $env->render('expression', $context));
}
public static function getBindingPowerTests(): iterable
{
// * / // % stronger than + -
foreach (['*', '/', '//', '%'] as $op1) {
foreach (['+', '-'] as $op2) {
$e = "12 $op1 6 $op2 3";
if ('//' === $op1) {
$php = eval("return (int) floor(12 / 6) $op2 3;");
} else {
$php = eval("return $e;");
}
yield "$op1 vs $op2" => ["{{ $e }}", "{{ (12 $op1 6) $op2 3 }}", $php];
$e = "12 $op2 6 $op1 3";
if ('//' === $op1) {
$php = eval("return 12 $op2 (int) floor(6 / 3);");
} else {
$php = eval("return $e;");
}
yield "$op2 vs $op1" => ["{{ $e }}", "{{ 12 $op2 (6 $op1 3) }}", $php];
}
}
// + - * / // % stronger than == != <=> < > >= <= `not in` `in` `matches` `starts with` `ends with` `has some` `has every`
foreach (['+', '-', '*', '/', '//', '%'] as $op1) {
foreach (['==', '!=', '<=>', '<', '>', '>=', '<='] as $op2) {
$e = "12 $op1 6 $op2 3";
if ('//' === $op1) {
$php = eval("return (int) floor(12 / 6) $op2 3;");
} else {
$php = eval("return $e;");
}
yield "$op1 vs $op2" => ["{{ $e }}", "{{ (12 $op1 6) $op2 3 }}", $php];
}
}
yield '+ vs not in' => ['{{ 1 + 2 not in [3, 4] }}', '{{ (1 + 2) not in [3, 4] }}', eval("return !in_array(1 + 2, [3, 4]);")];
yield '+ vs in' => ['{{ 1 + 2 in [3, 4] }}', '{{ (1 + 2) in [3, 4] }}', eval("return in_array(1 + 2, [3, 4]);")];
yield '+ vs matches' => ['{{ 1 + 2 matches "/^3$/" }}', '{{ (1 + 2) matches "/^3$/" }}', eval("return preg_match('/^3$/', 1 + 2);")];
// ~ stronger than `starts with` `ends with`
yield '~ vs starts with' => ['{{ "a" ~ "b" starts with "a" }}', '{{ ("a" ~ "b") starts with "a" }}', eval("return str_starts_with('ab', 'a');")];
yield '~ vs ends with' => ['{{ "a" ~ "b" ends with "b" }}', '{{ ("a" ~ "b") ends with "b" }}', eval("return str_ends_with('ab', 'b');")];
// [] . stronger than anything else
$context = ['a' => ['b' => 1, 'c' => ['d' => 2]]];
yield '[] vs unary -' => ['{{ -a["b"] + 3 }}', '{{ -(a["b"]) + 3 }}', eval("\$a = ['b' => 1]; return -\$a['b'] + 3;"), $context];
yield '[] vs unary - (multiple levels)' => ['{{ -a["c"]["d"] }}', '{{ -((a["c"])["d"]) }}', eval("\$a = ['c' => ['d' => 2]]; return -\$a['c']['d'];"), $context];
yield '. vs unary -' => ['{{ -a.b }}', '{{ -(a.b) }}', eval("\$a = ['b' => 1]; return -\$a['b'];"), $context];
yield '. vs unary - (multiple levels)' => ['{{ -a.c.d }}', '{{ -((a.c).d) }}', eval("\$a = ['c' => ['d' => 2]]; return -\$a['c']['d'];"), $context];
yield '. [] vs unary -' => ['{{ -a.c["d"] }}', '{{ -((a.c)["d"]) }}', eval("\$a = ['c' => ['d' => 2]]; return -\$a['c']['d'];"), $context];
yield '[] . vs unary -' => ['{{ -a["c"].d }}', '{{ -((a["c"]).d) }}', eval("\$a = ['c' => ['d' => 2]]; return -\$a['c']['d'];"), $context];
// () stronger than anything else
yield '() vs unary -' => ['{{ -random(1, 1) + 3 }}', '{{ -(random(1, 1)) + 3 }}', eval("return -rand(1, 1) + 3;")];
// + - stronger than |
yield '+ vs |' => ['{{ 10 + 2|length }}', '{{ 10 + (2|length) }}', eval("return 10 + strlen(2);"), $context];
// - unary stronger than |
// To be uncomment in Twig 4.0
//yield '- vs |' => ['{{ -1|abs }}', '{{ (-1)|abs }}', eval("return abs(-1);"), $context];
// ?? stronger than ()
//yield '?? vs ()' => ['{{ (1 ?? "a") }}', '{{ ((1 ?? "a")) }}', eval("return 1;")];
}
}
class NotReadyFunctionExpression extends FunctionExpression
+2 -2
View File
@@ -8,7 +8,7 @@ Twig parses postfix expressions
{{ 'a' }}
{{ 'a'|upper }}
{{ ('a')|upper }}
{{ -1|upper }}
{{ (-1)|abs }}
{{ macros.foo() }}
{{ (macros).foo() }}
--DATA--
@@ -17,6 +17,6 @@ return []
a
A
A
-1
1
foo
foo
@@ -1,8 +1,8 @@
--TEST--
+/- will have a higher precedence over ~ in Twig 4.0
--DEPRECATION--
Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 2.
Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 3.
Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 2.
Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 3.
--TEMPLATE--
{{ '42' ~ 1 + 41 }}
{{ '42' ~ 43 - 1 }}
@@ -0,0 +1,10 @@
--TEST--
| will have a higher precedence over + and - in Twig 4.0
--DEPRECATION--
Since twig/twig 3.20: As the "|" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 2.
--TEMPLATE--
{{ -1|abs }}
--DATA--
return []
--EXPECT--
-1
@@ -1,7 +1,7 @@
--TEST--
*, /, //, and % will have a higher precedence over not in Twig 4.0
--DEPRECATION--
Since twig/twig 3.15: Add explicit parentheses around the "not" unary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 2.
Since twig/twig 3.15: As the "not" prefix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 2.
--TEMPLATE--
{{ not 1 * 2 }}
--DATA--
+10 -10
View File
@@ -1,16 +1,16 @@
--TEST--
Twig supports the ?? operator
--DEPRECATION--
Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 4.
Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 5.
Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 6.
Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 7.
Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 10.
Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 9.
Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 11.
Since twig/twig 3.15: Add explicit parentheses around the "??" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 16.
Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 15.
Since twig/twig 3.15: Add explicit parentheses around the "~" binary operator to avoid behavior change in the next major version as its precedence will change in "index.twig" at line 17.
Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 4.
Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 5.
Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 6.
Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 7.
Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 10.
Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 9.
Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 11.
Since twig/twig 3.15: As the "??" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 16.
Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 15.
Since twig/twig 3.15: As the "~" infix operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "index.twig" at line 17.
--TEMPLATE--
{{ nope ?? nada ?? 'OK' -}} {# no deprecation as the operators have the same precedence #}