mirror of
https://github.com/twigphp/Twig.git
synced 2026-11-11 12:56:56 +00:00
Fix precedence rules
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
/.github/ export-ignore
|
||||
/bin/ export-ignore
|
||||
/doc/ export-ignore
|
||||
/extra/ export-ignore
|
||||
/tests/ export-ignore
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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--
|
||||
|
||||
@@ -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 #}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user