From 8ff19090f39fa6381314753b6f07a1dab4763b84 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 3 Feb 2025 22:22:16 +0100 Subject: [PATCH] Fix precedence rules --- .gitattributes | 1 + CHANGELOG | 2 + bin/generate_operators_precedence.php | 117 ++++++++------ doc/deprecated.rst | 18 +++ doc/filters/number_format.rst | 14 +- doc/operators_precedence.rst | 153 ++++++++++++------ doc/templates.rst | 28 +--- .../ExpressionParserDescriptionInterface.php | 17 ++ src/ExpressionParser/ExpressionParsers.php | 17 +- .../Infix/ArrowExpressionParser.php | 8 +- .../Infix/BinaryOperatorExpressionParser.php | 9 +- .../ConditionalTernaryExpressionParser.php | 8 +- .../Infix/DotExpressionParser.php | 10 +- .../Infix/FilterExpressionParser.php | 16 +- .../Infix/FunctionExpressionParser.php | 10 +- .../Infix/IsExpressionParser.php | 8 +- .../Infix/SquareBracketExpressionParser.php | 10 +- .../Prefix/GroupingExpressionParser.php | 8 +- .../Prefix/LiteralExpressionParser.php | 10 +- .../Prefix/UnaryOperatorExpressionParser.php | 9 +- src/Extension/CoreExtension.php | 22 ++- src/ExtensionSet.php | 2 +- src/Parser.php | 26 +-- tests/ExpressionParserTest.php | 80 +++++++++ tests/Fixtures/expressions/postfix.test | 4 +- .../operators/contat_vs_add_sub.legacy.test | 4 +- .../operators/minus_vs_pipe.legacy.test | 10 ++ .../operators/not_precedence.legacy.test | 2 +- .../Fixtures/tests/null_coalesce.legacy.test | 20 +-- 29 files changed, 457 insertions(+), 186 deletions(-) create mode 100644 src/ExpressionParser/ExpressionParserDescriptionInterface.php create mode 100644 tests/Fixtures/operators/minus_vs_pipe.legacy.test diff --git a/.gitattributes b/.gitattributes index 86b9ef413..c07b0dfb5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ /.github/ export-ignore +/bin/ export-ignore /doc/ export-ignore /extra/ export-ignore /tests/ export-ignore diff --git a/CHANGELOG b/CHANGELOG index df3f54bd3..cb70fd0c7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -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) diff --git a/bin/generate_operators_precedence.php b/bin/generate_operators_precedence.php index c22c81938..926989e66 100644 --- a/bin/generate_operators_precedence.php +++ b/bin/generate_operators_precedence.php @@ -1,65 +1,88 @@ 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); diff --git a/doc/deprecated.rst b/doc/deprecated.rst index e81eafaae..0b69f9139 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -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: diff --git a/doc/filters/number_format.rst b/doc/filters/number_format.rst index 047249d67..f9e9d718b 100644 --- a/doc/filters/number_format.rst +++ b/doc/filters/number_format.rst @@ -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 -`): +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 -`): + +.. 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 diff --git a/doc/operators_precedence.rst b/doc/operators_precedence.rst index 032582fbe..f603127f3 100644 --- a/doc/operators_precedence.rst +++ b/doc/operators_precedence.rst @@ -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) +=========== ============== ======= ============= =========== diff --git a/doc/templates.rst b/doc/templates.rst index 960093152..33a32e89e 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -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` 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: diff --git a/src/ExpressionParser/ExpressionParserDescriptionInterface.php b/src/ExpressionParser/ExpressionParserDescriptionInterface.php new file mode 100644 index 000000000..686f8a59f --- /dev/null +++ b/src/ExpressionParser/ExpressionParserDescriptionInterface.php @@ -0,0 +1,17 @@ +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; } } diff --git a/src/ExpressionParser/Infix/ArrowExpressionParser.php b/src/ExpressionParser/Infix/ArrowExpressionParser.php index 698497b0e..c8630da41 100644 --- a/src/ExpressionParser/Infix/ArrowExpressionParser.php +++ b/src/ExpressionParser/Infix/ArrowExpressionParser.php @@ -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; diff --git a/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php b/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php index ce650b424..4c66da73b 100644 --- a/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php +++ b/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php @@ -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 */ @@ -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; diff --git a/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php b/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php index 2bb5fc92c..9707c0a04 100644 --- a/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php +++ b/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php @@ -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; diff --git a/src/ExpressionParser/Infix/DotExpressionParser.php b/src/ExpressionParser/Infix/DotExpressionParser.php index d83f4bfbb..7d1cf5058 100644 --- a/src/ExpressionParser/Infix/DotExpressionParser.php +++ b/src/ExpressionParser/Infix/DotExpressionParser.php @@ -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 diff --git a/src/ExpressionParser/Infix/FilterExpressionParser.php b/src/ExpressionParser/Infix/FilterExpressionParser.php index 98e4b3b3a..e47d3fe67 100644 --- a/src/ExpressionParser/Infix/FilterExpressionParser.php +++ b/src/ExpressionParser/Infix/FilterExpressionParser.php @@ -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 diff --git a/src/ExpressionParser/Infix/FunctionExpressionParser.php b/src/ExpressionParser/Infix/FunctionExpressionParser.php index b1d627f7b..e9cd77517 100644 --- a/src/ExpressionParser/Infix/FunctionExpressionParser.php +++ b/src/ExpressionParser/Infix/FunctionExpressionParser.php @@ -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 diff --git a/src/ExpressionParser/Infix/IsExpressionParser.php b/src/ExpressionParser/Infix/IsExpressionParser.php index d63b495e2..1614c3cb9 100644 --- a/src/ExpressionParser/Infix/IsExpressionParser.php +++ b/src/ExpressionParser/Infix/IsExpressionParser.php @@ -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; diff --git a/src/ExpressionParser/Infix/SquareBracketExpressionParser.php b/src/ExpressionParser/Infix/SquareBracketExpressionParser.php index 1037dcb81..25fb153c7 100644 --- a/src/ExpressionParser/Infix/SquareBracketExpressionParser.php +++ b/src/ExpressionParser/Infix/SquareBracketExpressionParser.php @@ -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 diff --git a/src/ExpressionParser/Prefix/GroupingExpressionParser.php b/src/ExpressionParser/Prefix/GroupingExpressionParser.php index ac9f6c9db..5c6608da4 100644 --- a/src/ExpressionParser/Prefix/GroupingExpressionParser.php +++ b/src/ExpressionParser/Prefix/GroupingExpressionParser.php @@ -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; diff --git a/src/ExpressionParser/Prefix/LiteralExpressionParser.php b/src/ExpressionParser/Prefix/LiteralExpressionParser.php index 92540de75..e0e513273 100644 --- a/src/ExpressionParser/Prefix/LiteralExpressionParser.php +++ b/src/ExpressionParser/Prefix/LiteralExpressionParser.php @@ -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 diff --git a/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php b/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php index 4357d4ff6..35468940a 100644 --- a/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php +++ b/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php @@ -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 */ @@ -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; diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 2b6a9f058..89bc2cf6f 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -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(), ]; } diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 262d12625..c5e3321cc 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -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']) { diff --git a/src/Parser.php b/src/Parser.php index 1ddbae981..a1fd59276 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -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())); } } } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index 7d263fbf6..7808e6b62 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -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 diff --git a/tests/Fixtures/expressions/postfix.test b/tests/Fixtures/expressions/postfix.test index 276cbf197..6217a8410 100644 --- a/tests/Fixtures/expressions/postfix.test +++ b/tests/Fixtures/expressions/postfix.test @@ -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 diff --git a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test index 541e4f7cb..a1370d2ca 100644 --- a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test +++ b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test @@ -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 }} diff --git a/tests/Fixtures/operators/minus_vs_pipe.legacy.test b/tests/Fixtures/operators/minus_vs_pipe.legacy.test new file mode 100644 index 000000000..84eddeb21 --- /dev/null +++ b/tests/Fixtures/operators/minus_vs_pipe.legacy.test @@ -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 diff --git a/tests/Fixtures/operators/not_precedence.legacy.test b/tests/Fixtures/operators/not_precedence.legacy.test index 5178288e9..3a2f4a7ec 100644 --- a/tests/Fixtures/operators/not_precedence.legacy.test +++ b/tests/Fixtures/operators/not_precedence.legacy.test @@ -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-- diff --git a/tests/Fixtures/tests/null_coalesce.legacy.test b/tests/Fixtures/tests/null_coalesce.legacy.test index 2b2036660..4aaec8374 100644 --- a/tests/Fixtures/tests/null_coalesce.legacy.test +++ b/tests/Fixtures/tests/null_coalesce.legacy.test @@ -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 #}