From f4aacafd78505186ee2c6aa8f35eade954420a9b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 30 Sep 2024 11:51:58 +0200 Subject: [PATCH 1/2] Deprecate using ?? without explicit parentheses --- CHANGELOG | 1 + doc/deprecated.rst | 23 +++++++-- src/ExpressionParser.php | 49 +++++++++++-------- src/Extension/CoreExtension.php | 5 +- tests/EnvironmentTest.php | 2 +- .../operators/contat_vs_add_sub.legacy.test | 4 +- .../Fixtures/tests/null_coalesce.legacy.test | 16 ++++++ tests/Fixtures/tests/null_coalesce.test | 6 +-- 8 files changed, 73 insertions(+), 33 deletions(-) create mode 100644 tests/Fixtures/tests/null_coalesce.legacy.test diff --git a/CHANGELOG b/CHANGELOG index 6100fefe9..05c8d5615 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.15.0 (2024-XX-XX) + * Deprecate using `??` without explicit parentheses * Deprecate using `~` with `+` or `-` in an expression without using parentheses to clarify precedence * Deprecate not passing `AbstractExpression` args to most constructor arguments for classes extending `AbstractExpression` * Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`) diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 10cc1214c..7bcf5d249 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -315,9 +315,9 @@ Operators --------- * Using ``~`` with ``+`` or ``-`` in an expression without using parentheses to - clarify precedence is deprecated as of Twig 3.15 (in Twig 4.0, parentheses - won't be needed anymore as ``+`` / ``-`` will have a higher precedence than - ``~``). + clarify precedence triggers a deprecation as of Twig 3.15 (in Twig 4.0, + parentheses won't be needed anymore as ``+`` / ``-`` will have a higher + precedence than ``~``). For example, the following expression will trigger a deprecation in Twig 3.15:: @@ -331,3 +331,20 @@ Operators {# or #} {{ '42' ~ (1 + 41) }} {# this is equivalent to what Twig 4.x will do without the parentheses #} + +* Using ``??`` without explicit parentheses to clarify precedence triggers a + deprecation as of Twig 3.15 (in Twig 4.0, parentheses won't be needed anymore + as ``??`` will have the lowest precedence). + + For example, the following expression will trigger a deprecation in Twig 3.15:: + + {{ 'notnull' ?? 'foo' ~ '_bar' }} + + To avoid the deprecation, wrap the ``??`` expressionin parentheses to clarify + the precedence:: + + {{ ('notnull' ?? 'foo') ~ '_bar' }} {# this is equivalent to what Twig 3.x does without the parentheses #} + + {# or #} + + {{ 'notnull' ?? ('foo' ~ '_bar') }} {# this is equivalent to what Twig 4.x will do without the parentheses #} diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index ed61dcc2a..f7cf488ba 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -20,9 +20,7 @@ use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ArrowFunctionExpression; use Twig\Node\Expression\AssignNameExpression; use Twig\Node\Expression\Binary\AbstractBinary; -use Twig\Node\Expression\Binary\AddBinary; use Twig\Node\Expression\Binary\ConcatBinary; -use Twig\Node\Expression\Binary\SubBinary; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; @@ -57,6 +55,7 @@ class ExpressionParser /** @var array, associativity: self::OPERATOR_*}> */ private $binaryOperators; private $readyNodes = []; + private array $precedenceChanges = []; public function __construct( private Parser $parser, @@ -64,6 +63,20 @@ class ExpressionParser ) { $this->unaryOperators = $env->getUnaryOperators(); $this->binaryOperators = $env->getBinaryOperators(); + + foreach ($this->binaryOperators as $name => $config) { + if (!isset($config['future_precedence'])) { + continue; + } + + $min = min($config['future_precedence'], $config['precedence']); + $max = max($config['future_precedence'], $config['precedence']); + foreach ($this->binaryOperators as $n => $c) { + if ($c['precedence'] > $min && $c['precedence'] < $max) { + $this->precedenceChanges[$n][] = $name; + } + } + } } public function parseExpression($precedence = 0, $allowArrow = false) @@ -90,10 +103,22 @@ class ExpressionParser $expr = new $class($expr, $expr1, $token->getLine()); } + $expr->setAttribute('operator', $token->getValue()); + $token = $this->parser->getCurrentToken(); } - $this->triggerPrecedenceDeprecations($expr, $token); + // Check that the all nodes that are between the 2 precedences have explicit parentheses + if ($expr->hasAttribute('operator') && isset($this->precedenceChanges[$expr->getAttribute('operator')])) { + foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operatorName) { + foreach ($expr as $node) { + /** @var AbstractExpression $node */ + if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Add explicit parentheses around the "%s" operator to avoid behavior change in Twig 4.0 as its precedence will change in "%s" at line %d.', $operatorName, $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); + } + } + } + } if (0 === $precedence) { return $this->parseConditionalExpression($expr); @@ -102,24 +127,6 @@ class ExpressionParser return $expr; } - private function triggerPrecedenceDeprecations(AbstractExpression $expr, Token $token): void - { - // Precedence of the ~ operator will be lower than + and - in Twig 4.0 - if ($expr instanceof AddBinary || $expr instanceof SubBinary) { - /** @var AbstractExpression $left */ - $left = $expr->getNode('left'); - /** @var AbstractExpression $right */ - $right = $expr->getNode('right'); - if ( - ($left instanceof ConcatBinary && !$left->hasExplicitParentheses()) - || - ($right instanceof ConcatBinary && !$right->hasExplicitParentheses()) - ) { - trigger_deprecation('twig/twig', '3.15', \sprintf('As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "%s" at line %d.', $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); - } - } - } - /** * @return ArrowFunctionExpression|null */ diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 1dc051d8b..489c2c266 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -323,8 +323,7 @@ final class CoreExtension extends AbstractExtension '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - // Precedence of the ~ operator will change to 27 in Twig 4.0 - '~' => ['precedence' => 40, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + '~' => ['precedence' => 40, 'future_precedence' => 27, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '//' => ['precedence' => 60, 'class' => FloorDivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], @@ -332,7 +331,7 @@ final class CoreExtension extends AbstractExtension 'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - '??' => ['precedence' => 300, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], + '??' => ['precedence' => 300, 'future_precedence' => 5, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], ], ]; } diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 7bbc61cef..d5aee8d1b 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -598,7 +598,7 @@ class EnvironmentTest_Extension extends AbstractExtension implements GlobalsInte { return [ ['foo_unary' => []], - ['foo_binary' => []], + ['foo_binary' => ['precedence' => 0]], ]; } diff --git a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test index dc4a1ef50..c032cf290 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: As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "index.twig" at line 2. -Since twig/twig 3.15: As "+" / "-" will have a higher precedence than "~" in Twig 4.0, please add parentheses to keep the current behavior in "index.twig" at line 3. +Since twig/twig 3.15: Add explicit parentheses around the "~" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 2. +Since twig/twig 3.15: Add explicit parentheses around the "~" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 3. --TEMPLATE-- {{ '42' ~ 1 + 41 }} {{ '42' ~ 43 - 1 }} diff --git a/tests/Fixtures/tests/null_coalesce.legacy.test b/tests/Fixtures/tests/null_coalesce.legacy.test new file mode 100644 index 000000000..b9e3a8775 --- /dev/null +++ b/tests/Fixtures/tests/null_coalesce.legacy.test @@ -0,0 +1,16 @@ +--TEST-- +Twig supports the ?? operator +--DEPRECATION-- +Since twig/twig 3.15: Add explicit parentheses around the "??" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 4. +Since twig/twig 3.15: Add explicit parentheses around the "??" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 5. +--TEMPLATE-- +{{ nope ?? nada ?? 'OK' -}} {# no deprecation as the operators have the same precedence #} + +{{ 1 + nope ?? nada ?? 2 }} +{{ 1 + nope ?? 3 + nada ?? 2 }} +--DATA-- +return [] +--EXPECT-- +OK +3 +6 diff --git a/tests/Fixtures/tests/null_coalesce.test b/tests/Fixtures/tests/null_coalesce.test index 7af3255d6..f80b90717 100644 --- a/tests/Fixtures/tests/null_coalesce.test +++ b/tests/Fixtures/tests/null_coalesce.test @@ -10,9 +10,9 @@ Twig supports the ?? operator {{ foo.bar.baz.missing ?? 'OK' }} {{ foo['bar'] ?? 'KO' }} {{ foo['missing'] ?? 'OK' }} -{{ nope ?? nada ?? 'OK' }} -{{ 1 + nope ?? nada ?? 2 }} -{{ 1 + nope ?? 3 + nada ?? 2 }} +{{ nope ?? (nada ?? 'OK') }} +{{ 1 + (nope ?? (nada ?? 2)) }} +{{ 1 + (nope ?? 3) + (nada ?? 2) }} --DATA-- return ['bar' => 'OK', 'foo' => ['bar' => 'OK']] --EXPECT-- From 2081b1ff74aeb1ee3a5732487c8de7fe1420284e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 1 Oct 2024 22:30:54 +0200 Subject: [PATCH 2/2] Deprecate using the not unary operator without parenthesis --- CHANGELOG | 6 +- doc/deprecated.rst | 31 +++++-- src/ExpressionParser.php | 84 ++++++++++++++----- src/Extension/CoreExtension.php | 7 +- src/Extension/ExtensionInterface.php | 4 +- src/OperatorPrecedenceChange.php | 42 ++++++++++ tests/EnvironmentTest.php | 2 +- .../operators/contat_vs_add_sub.legacy.test | 4 +- .../operators/not_precedence.legacy.test | 9 ++ tests/Fixtures/operators/not_precedence.test | 9 ++ .../Fixtures/tests/null_coalesce.legacy.test | 4 +- 11 files changed, 161 insertions(+), 41 deletions(-) create mode 100644 src/OperatorPrecedenceChange.php create mode 100644 tests/Fixtures/operators/not_precedence.legacy.test create mode 100644 tests/Fixtures/operators/not_precedence.test diff --git a/CHANGELOG b/CHANGELOG index 05c8d5615..c4a17f735 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,9 @@ # 3.15.0 (2024-XX-XX) - * Deprecate using `??` without explicit parentheses - * Deprecate using `~` with `+` or `-` in an expression without using parentheses to clarify precedence + * Add support for triggering deprecations for future operator precedence changes + * Deprecate using the `not` unary operator in an expression with ``*``, ``/``, ``//``, or ``%`` without using explicit parentheses to clarify precedence + * Deprecate using the `??` binary operator without explicit parentheses + * Deprecate using the `~` binary operator in an expression with `+` or `-` without using parentheses to clarify precedence * Deprecate not passing `AbstractExpression` args to most constructor arguments for classes extending `AbstractExpression` * Fix `power` expressions with a negative number in parenthesis (`(-1) ** 2`) * Deprecate instantiating `Node` directly. Use `EmptyNode` or `Nodes` instead. diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 7bcf5d249..e1967adda 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -314,10 +314,9 @@ Node Operators --------- -* Using ``~`` with ``+`` or ``-`` in an expression without using parentheses to - clarify precedence triggers a deprecation as of Twig 3.15 (in Twig 4.0, - parentheses won't be needed anymore as ``+`` / ``-`` will have a higher - precedence than ``~``). +* Using ``~`` in an expression with the ``+`` or ``-`` operators without using + parentheses to clarify precedence triggers a deprecation as of Twig 3.15 (in + Twig 4.0, ``+`` / ``-`` will have a higher precedence than ``~``). For example, the following expression will trigger a deprecation in Twig 3.15:: @@ -333,14 +332,14 @@ Operators {{ '42' ~ (1 + 41) }} {# this is equivalent to what Twig 4.x will do without the parentheses #} * Using ``??`` without explicit parentheses to clarify precedence triggers a - deprecation as of Twig 3.15 (in Twig 4.0, parentheses won't be needed anymore - as ``??`` will have the lowest precedence). + deprecation as of Twig 3.15 (in Twig 4.0, ``??`` will have the lowest + precedence). For example, the following expression will trigger a deprecation in Twig 3.15:: {{ 'notnull' ?? 'foo' ~ '_bar' }} - To avoid the deprecation, wrap the ``??`` expressionin parentheses to clarify + To avoid the deprecation, wrap the ``??`` expression in parentheses to clarify the precedence:: {{ ('notnull' ?? 'foo') ~ '_bar' }} {# this is equivalent to what Twig 3.x does without the parentheses #} @@ -348,3 +347,21 @@ Operators {# or #} {{ 'notnull' ?? ('foo' ~ '_bar') }} {# this is equivalent to what Twig 4.x will do without the parentheses #} + +* Using the ``not`` unary operator in an expression with ``*``, ``/``, ``//``, + or ``%`` operators without explicit parentheses to clarify precedence + triggers a deprecation as of Twig 3.15 (in Twig 4.0, ``not`` will have a + higher precedence than ``*``, ``/``, ``//``, and ``%``). + + For example, the following expression will trigger a deprecation in Twig 3.15:: + + {{ not 1 * 2 }} + + To avoid the deprecation, wrap the concatenation in parentheses to clarify + the precedence:: + + {{ (not 1 * 2) }} {# this is equivalent to what Twig 3.x does without the parentheses #} + + {# or #} + + {{ (not 1) * 2 }} {# this is equivalent to what Twig 4.x will do without the parentheses #} diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index f7cf488ba..01cb9b934 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -64,29 +64,36 @@ class ExpressionParser $this->unaryOperators = $env->getUnaryOperators(); $this->binaryOperators = $env->getBinaryOperators(); - foreach ($this->binaryOperators as $name => $config) { - if (!isset($config['future_precedence'])) { + $ops = []; + foreach ($this->unaryOperators as $n => $c) { + $ops[] = $c + ['name' => $n, 'type' => 'unary']; + } + foreach ($this->binaryOperators as $n => $c) { + $ops[] = $c + ['name' => $n, 'type' => 'binary']; + } + foreach ($ops as $config) { + if (!isset($config['precedence_change'])) { continue; } - - $min = min($config['future_precedence'], $config['precedence']); - $max = max($config['future_precedence'], $config['precedence']); - foreach ($this->binaryOperators as $n => $c) { + $name = $config['type'].'_'.$config['name']; + $min = min($config['precedence_change']->getNewPrecedence(), $config['precedence']); + $max = max($config['precedence_change']->getNewPrecedence(), $config['precedence']); + foreach ($ops as $c) { if ($c['precedence'] > $min && $c['precedence'] < $max) { - $this->precedenceChanges[$n][] = $name; + $this->precedenceChanges[$c['type'].'_'.$c['name']][] = $name; } } } } - public function parseExpression($precedence = 0, $allowArrow = false) + public function parseExpression($precedence = 0, $allowArrow = false, bool $deprecationCheck = true) { if ($allowArrow && $arrow = $this->parseArrow()) { return $arrow; } $expr = $this->getPrimary(); - $token = $this->parser->getCurrentToken(); + $previousToken = $token = $this->parser->getCurrentToken(); while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) { $op = $this->binaryOperators[$token->getValue()]; $this->parser->getStream()->next(); @@ -103,21 +110,14 @@ class ExpressionParser $expr = new $class($expr, $expr1, $token->getLine()); } - $expr->setAttribute('operator', $token->getValue()); + $expr->setAttribute('operator', 'binary_'.$token->getValue()); + $previousToken = $token; $token = $this->parser->getCurrentToken(); } - // Check that the all nodes that are between the 2 precedences have explicit parentheses - if ($expr->hasAttribute('operator') && isset($this->precedenceChanges[$expr->getAttribute('operator')])) { - foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operatorName) { - foreach ($expr as $node) { - /** @var AbstractExpression $node */ - if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { - trigger_deprecation('twig/twig', '3.15', \sprintf('Add explicit parentheses around the "%s" operator to avoid behavior change in Twig 4.0 as its precedence will change in "%s" at line %d.', $operatorName, $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); - } - } - } + if ($deprecationCheck) { + $this->triggerPrecedenceDeprecations($expr, $previousToken); } if (0 === $precedence) { @@ -127,6 +127,43 @@ class ExpressionParser return $expr; } + private function triggerPrecedenceDeprecations(AbstractExpression $expr, Token $token): void + { + // Check that the all nodes that are between the 2 precedences have explicit parentheses + if (!$expr->hasAttribute('operator') || !isset($this->precedenceChanges[$expr->getAttribute('operator')])) { + return; + } + + if (str_starts_with($unaryOp = $expr->getAttribute('operator'), 'unary')) { + if ($expr->hasExplicitParentheses()) { + return; + } + $target = explode('_', $unaryOp)[1]; + $change = $this->unaryOperators[$target]['precedence_change']; + /** @var AbstractExpression $node */ + $node = $expr->getNode('node'); + foreach ($this->precedenceChanges as $operatorName => $changes) { + if (!in_array($unaryOp, $changes)) { + continue; + } + if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) { + 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.', $target, $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); + } + } + } else { + foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operatorName) { + foreach ($expr as $node) { + /** @var AbstractExpression $node */ + if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { + $op = explode('_', $operatorName)[1]; + $change = $this->binaryOperators[$op]['precedence_change']; + 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.', $op, $this->parser->getStream()->getSourceContext()->getName(), $token->getLine())); + } + } + } + } + } + /** * @return ArrowFunctionExpression|null */ @@ -195,10 +232,13 @@ class ExpressionParser $expr = $this->parseExpression($operator['precedence']); $class = $operator['class']; - return $this->parsePostfixExpression(new $class($expr, $token->getLine())); + $expr = new $class($expr, $token->getLine()); + $expr->setAttribute('operator', 'unary_'.$token->getValue()); + + return $this->parsePostfixExpression($expr); } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) { $this->parser->getStream()->next(); - $expr = $this->parseExpression()->setExplicitParentheses(); + $expr = $this->parseExpression(deprecationCheck: false)->setExplicitParentheses(); $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); return $this->parsePostfixExpression($expr); diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 489c2c266..6f27e9acd 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -66,6 +66,7 @@ use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Node; use Twig\NodeVisitor\MacroAutoImportNodeVisitor; +use Twig\OperatorPrecedenceChange; use Twig\Parser; use Twig\Source; use Twig\Template; @@ -296,7 +297,7 @@ final class CoreExtension extends AbstractExtension { return [ [ - 'not' => ['precedence' => 50, 'class' => NotUnary::class], + 'not' => ['precedence' => 50, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 70), 'class' => NotUnary::class], '-' => ['precedence' => 500, 'class' => NegUnary::class], '+' => ['precedence' => 500, 'class' => PosUnary::class], ], @@ -323,7 +324,7 @@ final class CoreExtension extends AbstractExtension '..' => ['precedence' => 25, 'class' => RangeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '+' => ['precedence' => 30, 'class' => AddBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '-' => ['precedence' => 30, 'class' => SubBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '~' => ['precedence' => 40, 'future_precedence' => 27, 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], + '~' => ['precedence' => 40, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 27), 'class' => ConcatBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '*' => ['precedence' => 60, 'class' => MulBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '/' => ['precedence' => 60, 'class' => DivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], '//' => ['precedence' => 60, 'class' => FloorDivBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], @@ -331,7 +332,7 @@ final class CoreExtension extends AbstractExtension 'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], 'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - '??' => ['precedence' => 300, 'future_precedence' => 5, 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], + '??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 5), 'class' => NullCoalesceExpression::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], ], ]; } diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index 10a42b6b1..1b7be44c1 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -67,8 +67,8 @@ interface ExtensionInterface * @return array First array of unary operators, second array of binary operators * * @psalm-return array{ - * array}>, - * array, associativity: ExpressionParser::OPERATOR_*}> + * array}>, + * array, associativity: ExpressionParser::OPERATOR_*}> * } */ public function getOperators(); diff --git a/src/OperatorPrecedenceChange.php b/src/OperatorPrecedenceChange.php new file mode 100644 index 000000000..12fd98c8d --- /dev/null +++ b/src/OperatorPrecedenceChange.php @@ -0,0 +1,42 @@ + + */ +class OperatorPrecedenceChange +{ + public function __construct( + private string $package, + private string $version, + private int $newPrecedence, + ) { + } + + public function getPackage(): string + { + return $this->package; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getNewPrecedence(): string + { + return $this->newPrecedence; + } +} diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index d5aee8d1b..802f9dd3a 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -597,7 +597,7 @@ class EnvironmentTest_Extension extends AbstractExtension implements GlobalsInte public function getOperators(): array { return [ - ['foo_unary' => []], + ['foo_unary' => ['precedence' => 0]], ['foo_binary' => ['precedence' => 0]], ]; } diff --git a/tests/Fixtures/operators/contat_vs_add_sub.legacy.test b/tests/Fixtures/operators/contat_vs_add_sub.legacy.test index c032cf290..541e4f7cb 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 "~" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 2. -Since twig/twig 3.15: Add explicit parentheses around the "~" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 3. +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. --TEMPLATE-- {{ '42' ~ 1 + 41 }} {{ '42' ~ 43 - 1 }} diff --git a/tests/Fixtures/operators/not_precedence.legacy.test b/tests/Fixtures/operators/not_precedence.legacy.test new file mode 100644 index 000000000..5178288e9 --- /dev/null +++ b/tests/Fixtures/operators/not_precedence.legacy.test @@ -0,0 +1,9 @@ +--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. +--TEMPLATE-- +{{ not 1 * 2 }} +--DATA-- +return [] +--EXPECT-- diff --git a/tests/Fixtures/operators/not_precedence.test b/tests/Fixtures/operators/not_precedence.test new file mode 100644 index 000000000..592b1c334 --- /dev/null +++ b/tests/Fixtures/operators/not_precedence.test @@ -0,0 +1,9 @@ +--TEST-- +*, /, //, and % will have a higher precedence over not in Twig 4.0 +--TEMPLATE-- +{{ (not 1) * 2 }} +{{ (not 1 * 2) }} +--DATA-- +return [] +--EXPECT-- +0 diff --git a/tests/Fixtures/tests/null_coalesce.legacy.test b/tests/Fixtures/tests/null_coalesce.legacy.test index b9e3a8775..3d33d0a79 100644 --- a/tests/Fixtures/tests/null_coalesce.legacy.test +++ b/tests/Fixtures/tests/null_coalesce.legacy.test @@ -1,8 +1,8 @@ --TEST-- Twig supports the ?? operator --DEPRECATION-- -Since twig/twig 3.15: Add explicit parentheses around the "??" operator to avoid behavior change in Twig 4.0 as its precedence will change in "index.twig" at line 4. -Since twig/twig 3.15: Add explicit parentheses around the "??" operator to avoid behavior change in Twig 4.0 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 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. --TEMPLATE-- {{ nope ?? nada ?? 'OK' -}} {# no deprecation as the operators have the same precedence #}