From 1b472cb95028084e2e6424a4339c825d5eb6be7e Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 19 Jan 2025 09:38:58 +0100 Subject: [PATCH 1/7] Introduce operator classes to describe operators provided by extensions instead of arrays --- CHANGELOG | 1 + doc/advanced.rst | 23 +-- doc/deprecated.rst | 23 +++ src/Environment.php | 19 +-- src/ExpressionParser.php | 100 ++++-------- src/Extension/AbstractExtension.php | 2 +- src/Extension/CoreExtension.php | 150 +++++++++--------- src/Extension/ExtensionInterface.php | 12 +- src/ExtensionSet.php | 141 ++++++++++++---- src/Lexer.php | 9 +- src/Operator/AbstractOperator.php | 32 ++++ .../Binary/AbstractBinaryOperator.php | 34 ++++ src/Operator/Binary/AddBinaryOperator.php | 32 ++++ src/Operator/Binary/AndBinaryOperator.php | 32 ++++ .../Binary/BitwiseAndBinaryOperator.php | 32 ++++ .../Binary/BitwiseOrBinaryOperator.php | 32 ++++ .../Binary/BitwiseXorBinaryOperator.php | 32 ++++ src/Operator/Binary/ConcatBinaryOperator.php | 38 +++++ src/Operator/Binary/DivBinaryOperator.php | 32 ++++ src/Operator/Binary/ElvisBinaryOperator.php | 43 +++++ .../Binary/EndsWithBinaryOperator.php | 32 ++++ src/Operator/Binary/EqualBinaryOperator.php | 32 ++++ .../Binary/FloorDivBinaryOperator.php | 32 ++++ src/Operator/Binary/GreaterBinaryOperator.php | 32 ++++ .../Binary/GreaterEqualBinaryOperator.php | 32 ++++ .../Binary/HasEveryBinaryOperator.php | 32 ++++ src/Operator/Binary/HasSomeBinaryOperator.php | 32 ++++ src/Operator/Binary/InBinaryOperator.php | 32 ++++ src/Operator/Binary/IsBinaryOperator.php | 30 ++++ src/Operator/Binary/IsNotBinaryOperator.php | 30 ++++ src/Operator/Binary/LessBinaryOperator.php | 32 ++++ .../Binary/LessEqualBinaryOperator.php | 32 ++++ src/Operator/Binary/MatchesBinaryOperator.php | 32 ++++ src/Operator/Binary/ModBinaryOperator.php | 32 ++++ src/Operator/Binary/MulBinaryOperator.php | 32 ++++ .../Binary/NotEqualBinaryOperator.php | 32 ++++ src/Operator/Binary/NotInBinaryOperator.php | 32 ++++ .../Binary/NullCoalesceBinaryOperator.php | 44 +++++ src/Operator/Binary/OrBinaryOperator.php | 32 ++++ src/Operator/Binary/PowerBinaryOperator.php | 38 +++++ src/Operator/Binary/RangeBinaryOperator.php | 32 ++++ .../Binary/SpaceshipBinaryOperator.php | 32 ++++ .../Binary/StartsWithBinaryOperator.php | 32 ++++ src/Operator/Binary/SubBinaryOperator.php | 32 ++++ src/Operator/Binary/XorBinaryOperator.php | 32 ++++ src/Operator/OperatorArity.php | 19 +++ src/Operator/OperatorAssociativity.php | 18 +++ src/Operator/OperatorInterface.php | 36 +++++ src/Operator/Operators.php | 93 +++++++++++ .../Ternary/AbstractTernaryOperator.php | 23 +++ src/Operator/Unary/AbstractUnaryOperator.php | 23 +++ src/Operator/Unary/NegUnaryOperator.php | 32 ++++ src/Operator/Unary/NotUnaryOperator.php | 38 +++++ src/Operator/Unary/PosUnaryOperator.php | 32 ++++ tests/CustomExtensionTest.php | 4 +- tests/EnvironmentTest.php | 40 ++++- tests/ExpressionParserTest.php | 34 +++- 57 files changed, 1789 insertions(+), 236 deletions(-) create mode 100644 src/Operator/AbstractOperator.php create mode 100644 src/Operator/Binary/AbstractBinaryOperator.php create mode 100644 src/Operator/Binary/AddBinaryOperator.php create mode 100644 src/Operator/Binary/AndBinaryOperator.php create mode 100644 src/Operator/Binary/BitwiseAndBinaryOperator.php create mode 100644 src/Operator/Binary/BitwiseOrBinaryOperator.php create mode 100644 src/Operator/Binary/BitwiseXorBinaryOperator.php create mode 100644 src/Operator/Binary/ConcatBinaryOperator.php create mode 100644 src/Operator/Binary/DivBinaryOperator.php create mode 100644 src/Operator/Binary/ElvisBinaryOperator.php create mode 100644 src/Operator/Binary/EndsWithBinaryOperator.php create mode 100644 src/Operator/Binary/EqualBinaryOperator.php create mode 100644 src/Operator/Binary/FloorDivBinaryOperator.php create mode 100644 src/Operator/Binary/GreaterBinaryOperator.php create mode 100644 src/Operator/Binary/GreaterEqualBinaryOperator.php create mode 100644 src/Operator/Binary/HasEveryBinaryOperator.php create mode 100644 src/Operator/Binary/HasSomeBinaryOperator.php create mode 100644 src/Operator/Binary/InBinaryOperator.php create mode 100644 src/Operator/Binary/IsBinaryOperator.php create mode 100644 src/Operator/Binary/IsNotBinaryOperator.php create mode 100644 src/Operator/Binary/LessBinaryOperator.php create mode 100644 src/Operator/Binary/LessEqualBinaryOperator.php create mode 100644 src/Operator/Binary/MatchesBinaryOperator.php create mode 100644 src/Operator/Binary/ModBinaryOperator.php create mode 100644 src/Operator/Binary/MulBinaryOperator.php create mode 100644 src/Operator/Binary/NotEqualBinaryOperator.php create mode 100644 src/Operator/Binary/NotInBinaryOperator.php create mode 100644 src/Operator/Binary/NullCoalesceBinaryOperator.php create mode 100644 src/Operator/Binary/OrBinaryOperator.php create mode 100644 src/Operator/Binary/PowerBinaryOperator.php create mode 100644 src/Operator/Binary/RangeBinaryOperator.php create mode 100644 src/Operator/Binary/SpaceshipBinaryOperator.php create mode 100644 src/Operator/Binary/StartsWithBinaryOperator.php create mode 100644 src/Operator/Binary/SubBinaryOperator.php create mode 100644 src/Operator/Binary/XorBinaryOperator.php create mode 100644 src/Operator/OperatorArity.php create mode 100644 src/Operator/OperatorAssociativity.php create mode 100644 src/Operator/OperatorInterface.php create mode 100644 src/Operator/Operators.php create mode 100644 src/Operator/Ternary/AbstractTernaryOperator.php create mode 100644 src/Operator/Unary/AbstractUnaryOperator.php create mode 100644 src/Operator/Unary/NegUnaryOperator.php create mode 100644 src/Operator/Unary/NotUnaryOperator.php create mode 100644 src/Operator/Unary/PosUnaryOperator.php diff --git a/CHANGELOG b/CHANGELOG index 3e9954190be..7fde125c9c5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.20.0 (2025-XX-XX) + * Introduce operator classes to describe operators provided by extensions instead of arrays * Fix support for ignoring syntax erros in an undefined handler in guard * Add configuration for Commonmark * Fix wrong array index diff --git a/doc/advanced.rst b/doc/advanced.rst index bc7e5d376f4..963aecf86d0 100644 --- a/doc/advanced.rst +++ b/doc/advanced.rst @@ -775,26 +775,9 @@ responsible for parsing the tag and compiling it to PHP. Operators ~~~~~~~~~ -The ``getOperators()`` methods lets you add new operators. Here is how to add -the ``!``, ``||``, and ``&&`` operators:: - - class CustomTwigExtension extends \Twig\Extension\AbstractExtension - { - public function getOperators() - { - return [ - [ - '!' => ['precedence' => 50, 'class' => \Twig\Node\Expression\Unary\NotUnary::class], - ], - [ - '||' => ['precedence' => 10, 'class' => \Twig\Node\Expression\Binary\OrBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT], - '&&' => ['precedence' => 15, 'class' => \Twig\Node\Expression\Binary\AndBinary::class, 'associativity' => \Twig\ExpressionParser::OPERATOR_LEFT], - ], - ]; - } - - // ... - } +The ``getOperators()`` methods lets you add new operators. To implement a new +one, have a look at the default operators provided by +``Twig\Extension\CoreExtension``. Tests ~~~~~ diff --git a/doc/deprecated.rst b/doc/deprecated.rst index b3397d8ddd7..74e9e695b43 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -418,3 +418,26 @@ Operators {# or #} {{ (not 1) * 2 }} {# this is equivalent to what Twig 4.x will do without the parentheses #} + +* Operators are now instances of ``Twig\Operator\OperatorInterface`` instead of + arrays. The ``ExtensionInterface::getOperators()`` method should now return an + array of ``Twig\Operator\OperatorInterface`` instances. + + Before: + + public function getOperators(): array { + return [ + 'not' => [ + 'precedence' => 10, + 'class' => NotUnaryOperator::class, + ], + ]; + } + + After: + + public function getOperators(): array { + return [ + new NotUnaryOperator(), + ]; + } diff --git a/src/Environment.php b/src/Environment.php index fddab050a8a..7792dca2855 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -27,11 +27,10 @@ use Twig\Loader\ArrayLoader; use Twig\Loader\ChainLoader; use Twig\Loader\LoaderInterface; -use Twig\Node\Expression\Binary\AbstractBinary; -use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\Operator\Operators; use Twig\Runtime\EscaperRuntime; use Twig\RuntimeLoader\FactoryRuntimeLoader; use Twig\RuntimeLoader\RuntimeLoaderInterface; @@ -925,22 +924,10 @@ public function mergeGlobals(array $context): array /** * @internal - * - * @return array}> - */ - public function getUnaryOperators(): array - { - return $this->extensionSet->getUnaryOperators(); - } - - /** - * @internal - * - * @return array, associativity: ExpressionParser::OPERATOR_*}> */ - public function getBinaryOperators(): array + public function getOperators(): Operators { - return $this->extensionSet->getBinaryOperators(); + return $this->extensionSet->getOperators(); } private function updateOptionsHash(): void diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 233139ee4ab..db0094948a9 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -18,14 +18,12 @@ use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ArrowFunctionExpression; -use Twig\Node\Expression\Binary\AbstractBinary; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\Ternary\ConditionalTernary; use Twig\Node\Expression\TestExpression; -use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; @@ -36,6 +34,9 @@ use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\Node; use Twig\Node\Nodes; +use Twig\Operator\OperatorArity; +use Twig\Operator\OperatorAssociativity; +use Twig\Operator\Operators; /** * Parses expressions. @@ -49,44 +50,19 @@ */ class ExpressionParser { + // deprecated, to be removed in 4.0 public const OPERATOR_LEFT = 1; public const OPERATOR_RIGHT = 2; - /** @var array}> */ - private $unaryOperators; - /** @var array, associativity: self::OPERATOR_*}> */ - private $binaryOperators; + private Operators $operators; private $readyNodes = []; - private array $precedenceChanges = []; private bool $deprecationCheck = true; public function __construct( private Parser $parser, private Environment $env, ) { - $this->unaryOperators = $env->getUnaryOperators(); - $this->binaryOperators = $env->getBinaryOperators(); - - $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; - } - $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[$c['type'].'_'.$c['name']][] = $name; - } - } - } + $this->operators = $env->getOperators(); } public function parseExpression($precedence = 0) @@ -101,28 +77,30 @@ public function parseExpression($precedence = 0) $expr = $this->getPrimary(); $token = $this->parser->getCurrentToken(); - while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) { - $op = $this->binaryOperators[$token->getValue()]; + while ($token->test(Token::OPERATOR_TYPE) && ($op = $this->operators->getBinary($token->getValue())) && $op->getPrecedence() >= $precedence) { $this->parser->getStream()->next(); if ('is not' === $token->getValue()) { $expr = $this->parseNotTestExpression($expr); } elseif ('is' === $token->getValue()) { $expr = $this->parseTestExpression($expr); - } elseif (isset($op['callable'])) { - $expr = $op['callable']($this->parser, $expr); + } elseif (null !== $op->getCallable()) { + $expr = $op->getCallable()($this->parser, $expr); } else { $previous = $this->setDeprecationCheck(true); try { - $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']); + $expr1 = $this->parseExpression(OperatorAssociativity::Left === $op->getAssociativity() ? $op->getPrecedence() + 1 : $op->getPrecedence()); } finally { $this->setDeprecationCheck($previous); } - $class = $op['class']; + $class = $op->getNodeClass(); + if (!$class) { + throw new \LogicException(\sprintf('Operator "%s" must have a Node class.', $op->getOperator())); + } $expr = new $class($expr, $expr1, $token->getLine()); } - $expr->setAttribute('operator', 'binary_'.$token->getValue()); + $expr->setAttribute('operator', $op); $this->triggerPrecedenceDeprecations($expr); @@ -138,35 +116,35 @@ public function parseExpression($precedence = 0) private function triggerPrecedenceDeprecations(AbstractExpression $expr): void { + $precedenceChanges = $this->operators->getPrecedenceChanges(); // Check that the all nodes that are between the 2 precedences have explicit parentheses - if (!$expr->hasAttribute('operator') || !isset($this->precedenceChanges[$expr->getAttribute('operator')])) { + if (!$expr->hasAttribute('operator') || !isset($precedenceChanges[$expr->getAttribute('operator')])) { return; } - if (str_starts_with($unaryOp = $expr->getAttribute('operator'), 'unary')) { + if (OperatorArity::Unary === $expr->getAttribute('operator')->getArity()) { if ($expr->hasExplicitParentheses()) { return; } - $target = explode('_', $unaryOp)[1]; + $operator = $expr->getAttribute('operator'); /** @var AbstractExpression $node */ $node = $expr->getNode('node'); - foreach ($this->precedenceChanges as $operatorName => $changes) { - if (!\in_array($unaryOp, $changes)) { + foreach ($precedenceChanges as $op => $changes) { + if (!\in_array($operator, $changes, true)) { continue; } - if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) { - $change = $this->unaryOperators[$target]['precedence_change']; - 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(), $node->getTemplateLine())); + if ($node->hasAttribute('operator') && $op === $node->getAttribute('operator')) { + $change = $operator->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.', $operator->getOperator(), $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); } } } else { - foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operatorName) { + foreach ($precedenceChanges[$expr->getAttribute('operator')] as $operator) { 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(), $node->getTemplateLine())); + if ($node->hasAttribute('operator') && $operator === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { + $change = $operator->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.', $operator->getOperator(), $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); } } } @@ -235,14 +213,16 @@ private function getPrimary(): AbstractExpression { $token = $this->parser->getCurrentToken(); - if ($this->isUnary($token)) { - $operator = $this->unaryOperators[$token->getValue()]; + if ($token->test(Token::OPERATOR_TYPE) && $operator = $this->operators->getUnary($token->getValue())) { $this->parser->getStream()->next(); - $expr = $this->parseExpression($operator['precedence']); - $class = $operator['class']; + $expr = $this->parseExpression($operator->getPrecedence()); + $class = $operator->getNodeClass(); + if (!$class) { + throw new \LogicException(\sprintf('Operator "%s" must have a Node class.', $operator->getOperator())); + } $expr = new $class($expr, $token->getLine()); - $expr->setAttribute('operator', 'unary_'.$token->getValue()); + $expr->setAttribute('operator', $operator); if ($this->deprecationCheck) { $this->triggerPrecedenceDeprecations($expr); @@ -283,16 +263,6 @@ private function parseConditionalExpression($expr): AbstractExpression return $expr; } - private function isUnary(Token $token): bool - { - return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]); - } - - private function isBinary(Token $token): bool - { - return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]); - } - public function parsePrimaryExpression() { $token = $this->parser->getCurrentToken(); diff --git a/src/Extension/AbstractExtension.php b/src/Extension/AbstractExtension.php index 26c00c68066..02767f7c37c 100644 --- a/src/Extension/AbstractExtension.php +++ b/src/Extension/AbstractExtension.php @@ -40,7 +40,7 @@ public function getFunctions() public function getOperators() { - return [[], []]; + return []; } public function getLastModified(): int diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index a351f570a18..53e04271074 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -16,40 +16,8 @@ use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; -use Twig\ExpressionParser; use Twig\Markup; use Twig\Node\Expression\AbstractExpression; -use Twig\Node\Expression\Binary\AddBinary; -use Twig\Node\Expression\Binary\AndBinary; -use Twig\Node\Expression\Binary\BitwiseAndBinary; -use Twig\Node\Expression\Binary\BitwiseOrBinary; -use Twig\Node\Expression\Binary\BitwiseXorBinary; -use Twig\Node\Expression\Binary\ConcatBinary; -use Twig\Node\Expression\Binary\DivBinary; -use Twig\Node\Expression\Binary\ElvisBinary; -use Twig\Node\Expression\Binary\EndsWithBinary; -use Twig\Node\Expression\Binary\EqualBinary; -use Twig\Node\Expression\Binary\FloorDivBinary; -use Twig\Node\Expression\Binary\GreaterBinary; -use Twig\Node\Expression\Binary\GreaterEqualBinary; -use Twig\Node\Expression\Binary\HasEveryBinary; -use Twig\Node\Expression\Binary\HasSomeBinary; -use Twig\Node\Expression\Binary\InBinary; -use Twig\Node\Expression\Binary\LessBinary; -use Twig\Node\Expression\Binary\LessEqualBinary; -use Twig\Node\Expression\Binary\MatchesBinary; -use Twig\Node\Expression\Binary\ModBinary; -use Twig\Node\Expression\Binary\MulBinary; -use Twig\Node\Expression\Binary\NotEqualBinary; -use Twig\Node\Expression\Binary\NotInBinary; -use Twig\Node\Expression\Binary\NullCoalesceBinary; -use Twig\Node\Expression\Binary\OrBinary; -use Twig\Node\Expression\Binary\PowerBinary; -use Twig\Node\Expression\Binary\RangeBinary; -use Twig\Node\Expression\Binary\SpaceshipBinary; -use Twig\Node\Expression\Binary\StartsWithBinary; -use Twig\Node\Expression\Binary\SubBinary; -use Twig\Node\Expression\Binary\XorBinary; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\Filter\DefaultFilter; use Twig\Node\Expression\FunctionNode\EnumCasesFunction; @@ -63,11 +31,43 @@ use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Test\OddTest; use Twig\Node\Expression\Test\SameasTest; -use Twig\Node\Expression\Unary\NegUnary; -use Twig\Node\Expression\Unary\NotUnary; -use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Node; -use Twig\OperatorPrecedenceChange; +use Twig\Operator\Binary\AddBinaryOperator; +use Twig\Operator\Binary\AndBinaryOperator; +use Twig\Operator\Binary\BitwiseAndBinaryOperator; +use Twig\Operator\Binary\BitwiseOrBinaryOperator; +use Twig\Operator\Binary\BitwiseXorBinaryOperator; +use Twig\Operator\Binary\ConcatBinaryOperator; +use Twig\Operator\Binary\DivBinaryOperator; +use Twig\Operator\Binary\ElvisBinaryOperator; +use Twig\Operator\Binary\EndsWithBinaryOperator; +use Twig\Operator\Binary\EqualBinaryOperator; +use Twig\Operator\Binary\FloorDivBinaryOperator; +use Twig\Operator\Binary\GreaterBinaryOperator; +use Twig\Operator\Binary\GreaterEqualBinaryOperator; +use Twig\Operator\Binary\HasEveryBinaryOperator; +use Twig\Operator\Binary\HasSomeBinaryOperator; +use Twig\Operator\Binary\InBinaryOperator; +use Twig\Operator\Binary\IsBinaryOperator; +use Twig\Operator\Binary\IsNotBinaryOperator; +use Twig\Operator\Binary\LessBinaryOperator; +use Twig\Operator\Binary\LessEqualBinaryOperator; +use Twig\Operator\Binary\MatchesBinaryOperator; +use Twig\Operator\Binary\ModBinaryOperator; +use Twig\Operator\Binary\MulBinaryOperator; +use Twig\Operator\Binary\NotEqualBinaryOperator; +use Twig\Operator\Binary\NotInBinaryOperator; +use Twig\Operator\Binary\NullCoalesceBinaryOperator; +use Twig\Operator\Binary\OrBinaryOperator; +use Twig\Operator\Binary\PowerBinaryOperator; +use Twig\Operator\Binary\RangeBinaryOperator; +use Twig\Operator\Binary\SpaceshipBinaryOperator; +use Twig\Operator\Binary\StartsWithBinaryOperator; +use Twig\Operator\Binary\SubBinaryOperator; +use Twig\Operator\Binary\XorBinaryOperator; +use Twig\Operator\Unary\NegUnaryOperator; +use Twig\Operator\Unary\NotUnaryOperator; +use Twig\Operator\Unary\PosUnaryOperator; use Twig\Parser; use Twig\Sandbox\SecurityNotAllowedMethodError; use Twig\Sandbox\SecurityNotAllowedPropertyError; @@ -316,47 +316,43 @@ public function getNodeVisitors(): array public function getOperators(): array { return [ - [ - '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], - ], - [ - '? :' => ['precedence' => 5, 'class' => ElvisBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - '?:' => ['precedence' => 5, 'class' => ElvisBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - '??' => ['precedence' => 300, 'precedence_change' => new OperatorPrecedenceChange('twig/twig', '3.15', 5), 'class' => NullCoalesceBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - 'or' => ['precedence' => 10, 'class' => OrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'xor' => ['precedence' => 12, 'class' => XorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'and' => ['precedence' => 15, 'class' => AndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'b-or' => ['precedence' => 16, 'class' => BitwiseOrBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'b-xor' => ['precedence' => 17, 'class' => BitwiseXorBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'b-and' => ['precedence' => 18, 'class' => BitwiseAndBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '==' => ['precedence' => 20, 'class' => EqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '!=' => ['precedence' => 20, 'class' => NotEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '<=>' => ['precedence' => 20, 'class' => SpaceshipBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '<' => ['precedence' => 20, 'class' => LessBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '>' => ['precedence' => 20, 'class' => GreaterBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '>=' => ['precedence' => 20, 'class' => GreaterEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '<=' => ['precedence' => 20, 'class' => LessEqualBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'not in' => ['precedence' => 20, 'class' => NotInBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'in' => ['precedence' => 20, 'class' => InBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'matches' => ['precedence' => 20, 'class' => MatchesBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'starts with' => ['precedence' => 20, 'class' => StartsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'ends with' => ['precedence' => 20, 'class' => EndsWithBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'has some' => ['precedence' => 20, 'class' => HasSomeBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'has every' => ['precedence' => 20, 'class' => HasEveryBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '..' => ['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, '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], - '%' => ['precedence' => 60, 'class' => ModBinary::class, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'is' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], - 'is not' => ['precedence' => 100, 'associativity' => ExpressionParser::OPERATOR_LEFT], - '**' => ['precedence' => 200, 'class' => PowerBinary::class, 'associativity' => ExpressionParser::OPERATOR_RIGHT], - ], + new NotUnaryOperator(), + new NegUnaryOperator(), + new PosUnaryOperator(), + + new ElvisBinaryOperator(), + new NullCoalesceBinaryOperator(), + new OrBinaryOperator(), + new XorBinaryOperator(), + new AndBinaryOperator(), + new BitwiseOrBinaryOperator(), + new BitwiseXorBinaryOperator(), + new BitwiseAndBinaryOperator(), + new EqualBinaryOperator(), + new NotEqualBinaryOperator(), + new SpaceshipBinaryOperator(), + new LessBinaryOperator(), + new GreaterBinaryOperator(), + new GreaterEqualBinaryOperator(), + new LessEqualBinaryOperator(), + new NotInBinaryOperator(), + new InBinaryOperator(), + new MatchesBinaryOperator(), + new StartsWithBinaryOperator(), + new EndsWithBinaryOperator(), + new HasSomeBinaryOperator(), + new HasEveryBinaryOperator(), + new RangeBinaryOperator(), + new AddBinaryOperator(), + new SubBinaryOperator(), + new ConcatBinaryOperator(), + new MulBinaryOperator(), + new DivBinaryOperator(), + new FloorDivBinaryOperator(), + new ModBinaryOperator(), + new IsBinaryOperator(), + new IsNotBinaryOperator(), + new PowerBinaryOperator(), ]; } diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index d51cd3ee2ff..6d5e4b5fa7f 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -11,11 +11,8 @@ namespace Twig\Extension; -use Twig\ExpressionParser; -use Twig\Node\Expression\Binary\AbstractBinary; -use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\OperatorPrecedenceChange; +use Twig\Operator\OperatorInterface; use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; use Twig\TwigFunction; @@ -66,12 +63,7 @@ public function getFunctions(); /** * Returns a list of operators to add to the existing list. * - * @return array First array of unary operators, second array of binary operators - * - * @psalm-return array{ - * array}>, - * array, associativity: ExpressionParser::OPERATOR_*}> - * } + * @return OperatorInterface[] */ public function getOperators(); } diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index b069232b44f..7a924f7b141 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -16,9 +16,12 @@ use Twig\Extension\GlobalsInterface; use Twig\Extension\LastModifiedExtensionInterface; use Twig\Extension\StagingExtension; -use Twig\Node\Expression\Binary\AbstractBinary; -use Twig\Node\Expression\Unary\AbstractUnary; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\Operator\Binary\AbstractBinaryOperator; +use Twig\Operator\OperatorAssociativity; +use Twig\Operator\OperatorInterface; +use Twig\Operator\Operators; +use Twig\Operator\Unary\AbstractUnaryOperator; use Twig\TokenParser\TokenParserInterface; /** @@ -46,10 +49,8 @@ final class ExtensionSet private $functions; /** @var array */ private $dynamicFunctions; - /** @var array}> */ - private $unaryOperators; - /** @var array, associativity: ExpressionParser::OPERATOR_*}> */ - private $binaryOperators; + /** @var Operators */ + private $operators; /** @var array|null */ private $globals; /** @var array */ @@ -406,28 +407,13 @@ public function getTest(string $name): ?TwigTest return null; } - /** - * @return array}> - */ - public function getUnaryOperators(): array + public function getOperators(): Operators { if (!$this->initialized) { $this->initExtensions(); } - return $this->unaryOperators; - } - - /** - * @return array, associativity: ExpressionParser::OPERATOR_*}> - */ - public function getBinaryOperators(): array - { - if (!$this->initialized) { - $this->initExtensions(); - } - - return $this->binaryOperators; + return $this->operators; } private function initExtensions(): void @@ -440,8 +426,7 @@ private function initExtensions(): void $this->dynamicFunctions = []; $this->dynamicTests = []; $this->visitors = []; - $this->unaryOperators = []; - $this->binaryOperators = []; + $this->operators = new Operators(); foreach ($this->extensions as $extension) { $this->initExtension($extension); @@ -497,12 +482,110 @@ private function initExtension(ExtensionInterface $extension): void throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators))); } - if (2 !== \count($operators)) { - throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); + // new signature? + $legacy = false; + foreach ($operators as $op) { + if (!$op instanceof OperatorInterface) { + $legacy = true; + + break; + } } - $this->unaryOperators = array_merge($this->unaryOperators, $operators[0]); - $this->binaryOperators = array_merge($this->binaryOperators, $operators[1]); + if ($legacy) { + if (2 !== \count($operators)) { + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); + } + + trigger_deprecation('twig/twig', '3.19.0', \sprintf('Extension "%s" uses the old signature for "getOperators()", please update it to return an array of "OperatorInterface" objects.', \get_class($extension))); + + $ops = []; + foreach ($operators[0] as $n => $op) { + $ops[] = $op instanceof OperatorInterface ? $op : $this->convertUnaryOperators($n, $op); + } + foreach ($operators[1] as $n => $op) { + $ops[] = $op instanceof OperatorInterface ? $op : $this->convertBinaryOperators($n, $op); + } + $this->operators->add($ops); + } else { + $this->operators->add($operators); + } } } + + private function convertUnaryOperators(string $n, array $op): OperatorInterface + { + trigger_deprecation('twig/twig', '3.19.0', \sprintf('Using a non-OperatorInterface object to define the "%s" unary operator is deprecated.', $n)); + + return new class($op, $n) extends AbstractUnaryOperator { + public function __construct(private array $op, private string $operator) + { + } + + public function getOperator(): string + { + return $this->operator; + } + + public function getPrecedence(): int + { + return $this->op['precedence']; + } + + public function getPrecedenceChange(): ?OperatorPrecedenceChange + { + return $this->op['precedence_change'] ?? null; + } + + public function getNodeClass(): ?string + { + return $this->op['class'] ?? null; + } + }; + } + + private function convertBinaryOperators(string $n, array $op): OperatorInterface + { + trigger_deprecation('twig/twig', '3.19.0', \sprintf('Using a non-OperatorInterface object to define the "%s" binary operator is deprecated.', $n)); + + return new class($op, $n) extends AbstractBinaryOperator { + public function __construct(private array $op, private string $operator) + { + } + + public function getOperator(): string + { + return $this->operator; + } + + public function getPrecedence(): int + { + return $this->op['precedence']; + } + + public function getPrecedenceChange(): ?OperatorPrecedenceChange + { + return $this->op['precedence_change'] ?? null; + } + + public function getNodeClass(): ?string + { + return $this->op['class'] ?? null; + } + + public function getAssociativity(): OperatorAssociativity + { + return match ($this->op['associativity']) { + 1 => OperatorAssociativity::Left, + 2 => OperatorAssociativity::Right, + default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".', $this->op['associativity'], $this->getOperator())), + }; + } + + public function getCallable(): ?callable + { + return $this->op['callable'] ?? null; + } + }; + } } diff --git a/src/Lexer.php b/src/Lexer.php index 929673c6082..215da8e9a5c 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -544,11 +544,10 @@ private function moveCursor($text): void private function getOperatorRegex(): string { - $operators = array_merge( - ['='], - array_keys($this->env->getUnaryOperators()), - array_keys($this->env->getBinaryOperators()) - ); + $operators = ['=']; + foreach ($this->env->getOperators() as $operator) { + $operators = array_merge($operators, [$operator->getOperator()], $operator->getAliases()); + } $operators = array_combine($operators, array_map('strlen', $operators)); arsort($operators); diff --git a/src/Operator/AbstractOperator.php b/src/Operator/AbstractOperator.php new file mode 100644 index 00000000000..c18904f35ae --- /dev/null +++ b/src/Operator/AbstractOperator.php @@ -0,0 +1,32 @@ +getArity()->value, $this->getOperator()); + } + + public function getPrecedenceChange(): ?OperatorPrecedenceChange + { + return null; + } + + public function getAliases(): array + { + return []; + } +} diff --git a/src/Operator/Binary/AbstractBinaryOperator.php b/src/Operator/Binary/AbstractBinaryOperator.php new file mode 100644 index 00000000000..2eec2537f3c --- /dev/null +++ b/src/Operator/Binary/AbstractBinaryOperator.php @@ -0,0 +1,34 @@ +'; + } + + public function getPrecedence(): int + { + return 20; + } + + public function getNodeClass(): ?string + { + return GreaterBinary::class; + } +} diff --git a/src/Operator/Binary/GreaterEqualBinaryOperator.php b/src/Operator/Binary/GreaterEqualBinaryOperator.php new file mode 100644 index 00000000000..aa709b55c39 --- /dev/null +++ b/src/Operator/Binary/GreaterEqualBinaryOperator.php @@ -0,0 +1,32 @@ +='; + } + + public function getPrecedence(): int + { + return 20; + } +} diff --git a/src/Operator/Binary/HasEveryBinaryOperator.php b/src/Operator/Binary/HasEveryBinaryOperator.php new file mode 100644 index 00000000000..98a74d32dbb --- /dev/null +++ b/src/Operator/Binary/HasEveryBinaryOperator.php @@ -0,0 +1,32 @@ +'; + } + + public function getPrecedence(): int + { + return 20; + } + + public function getNodeClass(): ?string + { + return SpaceshipBinary::class; + } +} diff --git a/src/Operator/Binary/StartsWithBinaryOperator.php b/src/Operator/Binary/StartsWithBinaryOperator.php new file mode 100644 index 00000000000..e28953b74a4 --- /dev/null +++ b/src/Operator/Binary/StartsWithBinaryOperator.php @@ -0,0 +1,32 @@ + + */ + public function getNodeClass(): ?string; + + public function getArity(): OperatorArity; + + public function getPrecedence(): int; + + public function getPrecedenceChange(): ?OperatorPrecedenceChange; + + /** + * @return array + */ + public function getAliases(): array; +} diff --git a/src/Operator/Operators.php b/src/Operator/Operators.php new file mode 100644 index 00000000000..19b92444224 --- /dev/null +++ b/src/Operator/Operators.php @@ -0,0 +1,93 @@ +add($operators); + } + + /** + * @param array $operators + * + * @return $this + */ + public function add(array $operators): self + { + $this->precedenceChanges = null; + foreach ($operators as $operator) { + $this->operators[$operator->getArity()->value][$operator->getOperator()] = $operator; + foreach ($operator->getAliases() as $alias) { + $this->aliases[$operator->getArity()->value][$alias] = $operator; + } + } + + return $this; + } + + public function getUnary(string $name): ?AbstractUnaryOperator + { + return $this->operators[OperatorArity::Unary->value][$name] ?? ($this->aliases[OperatorArity::Unary->value][$name] ?? null); + } + + public function getBinary(string $name): ?AbstractBinaryOperator + { + return $this->operators[OperatorArity::Binary->value][$name] ?? ($this->aliases[OperatorArity::Binary->value][$name] ?? null); + } + + public function getIterator(): \Traversable + { + foreach ($this->operators as $operators) { + // we don't yield the keys + yield from $operators; + } + } + + /** + * @internal + * + * @return \WeakMap> + */ + public function getPrecedenceChanges(): \WeakMap + { + if (null === $this->precedenceChanges) { + $this->precedenceChanges = new \WeakMap(); + foreach ($this as $op) { + if (!$op->getPrecedenceChange()) { + continue; + } + $min = min($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence()); + $max = max($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence()); + foreach ($this as $o) { + if ($o->getPrecedence() > $min && $o->getPrecedence() < $max) { + if (!isset($this->precedenceChanges[$o])) { + $this->precedenceChanges[$o] = []; + } + $this->precedenceChanges[$o][] = $op; + } + } + } + } + + return $this->precedenceChanges; + } +} diff --git a/src/Operator/Ternary/AbstractTernaryOperator.php b/src/Operator/Ternary/AbstractTernaryOperator.php new file mode 100644 index 00000000000..eceaa79a1e0 --- /dev/null +++ b/src/Operator/Ternary/AbstractTernaryOperator.php @@ -0,0 +1,23 @@ +expectException(\InvalidArgumentException::class); $this->expectExceptionMessage($expectedExceptionMessage); - $env->getUnaryOperators(); + $env->getOperators(); } public static function provideInvalidExtensions() diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 34774db1c5b..5bc90b58215 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -26,6 +26,8 @@ use Twig\Loader\LoaderInterface; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; +use Twig\Operator\Binary\AbstractBinaryOperator; +use Twig\Operator\Unary\AbstractUnaryOperator; use Twig\RuntimeLoader\RuntimeLoaderInterface; use Twig\Source; use Twig\Token; @@ -307,8 +309,8 @@ public function testAddExtension() $this->assertArrayHasKey('foo_filter', $twig->getFilters()); $this->assertArrayHasKey('foo_function', $twig->getFunctions()); $this->assertArrayHasKey('foo_test', $twig->getTests()); - $this->assertArrayHasKey('foo_unary', $twig->getUnaryOperators()); - $this->assertArrayHasKey('foo_binary', $twig->getBinaryOperators()); + $this->assertNotNull($twig->getOperators()->getUnary('foo_unary')); + $this->assertNotNull($twig->getOperators()->getBinary('foo_binary')); $this->assertArrayHasKey('foo_global', $twig->getGlobals()); $visitors = $twig->getNodeVisitors(); $found = false; @@ -597,8 +599,38 @@ public function getFunctions(): array public function getOperators(): array { return [ - ['foo_unary' => ['precedence' => 0]], - ['foo_binary' => ['precedence' => 0]], + new class extends AbstractUnaryOperator { + public function getOperator(): string + { + return 'foo_unary'; + } + + public function getPrecedence(): int + { + return 0; + } + + public function getNodeClass(): string + { + return ''; + } + }, + new class extends AbstractBinaryOperator { + public function getOperator(): string + { + return 'foo_binary'; + } + + public function getPrecedence(): int + { + return 0; + } + + public function getNodeClass(): string + { + return ''; + } + }, ]; } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index d3887e93880..f98bade0841 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -28,6 +28,7 @@ use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; +use Twig\Operator\Unary\AbstractUnaryOperator; use Twig\Parser; use Twig\Source; use Twig\TwigFilter; @@ -572,14 +573,31 @@ public function testUnaryPrecedenceChange() $env->addExtension(new class extends AbstractExtension { public function getOperators() { - $class = new class(new ConstantExpression('foo', 1), 1) extends AbstractUnary { - public function operator(Compiler $compiler): Compiler - { - return $compiler->raw('!'); - } - }; - - return [['!' => ['precedence' => 50, 'class' => $class::class]], []]; + return [ + new class extends AbstractUnaryOperator { + public function getOperator(): string + { + return '!'; + } + + public function getPrecedence(): int + { + return 50; + } + + public function getNodeClass(): string + { + $class = new class(new ConstantExpression('foo', 1), 1) extends AbstractUnary { + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('!'); + } + }; + + return $class::class; + } + }, + ]; } }); $parser = new Parser($env); From 1267d26e0951594e1ec3510593450ab595ddd25b Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 19 Jan 2025 21:21:19 +0100 Subject: [PATCH 2/7] Extract operators logic from ExpressionParser to their own classes --- src/ExpressionParser.php | 552 ++++-------------- src/Extension/CoreExtension.php | 15 + src/Extension/ExtensionInterface.php | 7 +- src/ExtensionSet.php | 29 +- src/Lexer.php | 53 +- .../Expression/ArrowFunctionExpression.php | 25 +- src/Node/Expression/ListExpression.php | 48 ++ .../Binary/AbstractBinaryOperator.php | 20 +- src/Operator/Binary/AddBinaryOperator.php | 2 +- src/Operator/Binary/AndBinaryOperator.php | 2 +- src/Operator/Binary/ArrowBinaryOperator.php | 49 ++ .../Binary/BinaryOperatorInterface.php | 25 + .../Binary/BitwiseAndBinaryOperator.php | 2 +- .../Binary/BitwiseOrBinaryOperator.php | 2 +- .../Binary/BitwiseXorBinaryOperator.php | 2 +- src/Operator/Binary/ConcatBinaryOperator.php | 2 +- src/Operator/Binary/DivBinaryOperator.php | 2 +- src/Operator/Binary/DotBinaryOperator.php | 93 +++ src/Operator/Binary/ElvisBinaryOperator.php | 2 +- .../Binary/EndsWithBinaryOperator.php | 2 +- src/Operator/Binary/EqualBinaryOperator.php | 2 +- src/Operator/Binary/FilterBinaryOperator.php | 73 +++ .../Binary/FloorDivBinaryOperator.php | 2 +- .../Binary/FunctionBinaryOperator.php | 84 +++ src/Operator/Binary/GreaterBinaryOperator.php | 2 +- .../Binary/GreaterEqualBinaryOperator.php | 2 +- .../Binary/HasEveryBinaryOperator.php | 2 +- src/Operator/Binary/HasSomeBinaryOperator.php | 2 +- src/Operator/Binary/InBinaryOperator.php | 2 +- src/Operator/Binary/IsBinaryOperator.php | 58 +- src/Operator/Binary/IsNotBinaryOperator.php | 16 +- src/Operator/Binary/LessBinaryOperator.php | 2 +- .../Binary/LessEqualBinaryOperator.php | 2 +- src/Operator/Binary/MatchesBinaryOperator.php | 2 +- src/Operator/Binary/ModBinaryOperator.php | 2 +- src/Operator/Binary/MulBinaryOperator.php | 2 +- .../Binary/NotEqualBinaryOperator.php | 2 +- src/Operator/Binary/NotInBinaryOperator.php | 2 +- .../Binary/NullCoalesceBinaryOperator.php | 2 +- src/Operator/Binary/OrBinaryOperator.php | 2 +- src/Operator/Binary/PowerBinaryOperator.php | 2 +- src/Operator/Binary/RangeBinaryOperator.php | 2 +- .../Binary/SpaceshipBinaryOperator.php | 2 +- .../Binary/SquareBracketBinaryOperator.php | 87 +++ .../Binary/StartsWithBinaryOperator.php | 2 +- src/Operator/Binary/SubBinaryOperator.php | 2 +- src/Operator/Binary/XorBinaryOperator.php | 2 +- src/Operator/OperatorInterface.php | 8 +- src/Operator/Operators.php | 37 +- .../Ternary/AbstractTernaryOperator.php | 8 +- .../Ternary/ConditionalTernaryOperator.php | 50 ++ .../Ternary/TernaryOperatorInterface.php | 25 + src/Operator/Unary/AbstractUnaryOperator.php | 15 +- src/Operator/Unary/NegUnaryOperator.php | 2 +- src/Operator/Unary/NotUnaryOperator.php | 2 +- .../Unary/ParenthesisUnaryOperator.php | 74 +++ src/Operator/Unary/PosUnaryOperator.php | 2 +- src/Operator/Unary/UnaryOperatorInterface.php | 22 + src/Parser.php | 92 +++ src/Token.php | 40 +- src/TokenParser/AbstractTokenParser.php | 31 + src/TokenParser/ApplyTokenParser.php | 10 +- src/TokenParser/ForTokenParser.php | 2 +- src/TokenParser/MacroTokenParser.php | 2 +- src/TokenParser/SetTokenParser.php | 18 +- src/TokenParser/TypesTokenParser.php | 2 +- .../filters/arrow_reserved_names.test | 2 +- 67 files changed, 1191 insertions(+), 549 deletions(-) create mode 100644 src/Node/Expression/ListExpression.php create mode 100644 src/Operator/Binary/ArrowBinaryOperator.php create mode 100644 src/Operator/Binary/BinaryOperatorInterface.php create mode 100644 src/Operator/Binary/DotBinaryOperator.php create mode 100644 src/Operator/Binary/FilterBinaryOperator.php create mode 100644 src/Operator/Binary/FunctionBinaryOperator.php create mode 100644 src/Operator/Binary/SquareBracketBinaryOperator.php create mode 100644 src/Operator/Ternary/ConditionalTernaryOperator.php create mode 100644 src/Operator/Ternary/TernaryOperatorInterface.php create mode 100644 src/Operator/Unary/ParenthesisUnaryOperator.php create mode 100644 src/Operator/Unary/UnaryOperatorInterface.php diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index db0094948a9..ad4a05257f6 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -12,30 +12,20 @@ namespace Twig; -use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Error\SyntaxError; -use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; -use Twig\Node\Expression\ArrowFunctionExpression; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\ConstantExpression; -use Twig\Node\Expression\GetAttrExpression; -use Twig\Node\Expression\MacroReferenceExpression; -use Twig\Node\Expression\Ternary\ConditionalTernary; -use Twig\Node\Expression\TestExpression; use Twig\Node\Expression\Unary\NegUnary; -use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Expression\Unary\SpreadUnary; use Twig\Node\Expression\Variable\AssignContextVariable; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Expression\Variable\LocalVariable; -use Twig\Node\Expression\Variable\TemplateVariable; use Twig\Node\Node; use Twig\Node\Nodes; use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; use Twig\Operator\Operators; /** @@ -50,12 +40,16 @@ */ class ExpressionParser { - // deprecated, to be removed in 4.0 + /** + * @deprecated since Twig 3.20 + */ public const OPERATOR_LEFT = 1; + /** + * @deprecated since Twig 3.20 + */ public const OPERATOR_RIGHT = 2; private Operators $operators; - private $readyNodes = []; private bool $deprecationCheck = true; public function __construct( @@ -65,52 +59,57 @@ public function __construct( $this->operators = $env->getOperators(); } + /** + * @internal + */ + public function getParser(): Parser + { + return $this->parser; + } + + /** + * @internal + */ + public function getStream(): TokenStream + { + return $this->parser->getStream(); + } + + /** + * @internal + */ + public function getImportedSymbol(string $type, string $name) + { + return $this->parser->getImportedSymbol($type, $name); + } + public function parseExpression($precedence = 0) { if (\func_num_args() > 1) { trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); } - if ($arrow = $this->parseArrow()) { - return $arrow; - } - - $expr = $this->getPrimary(); + $expr = $this->parsePrimary(); $token = $this->parser->getCurrentToken(); - while ($token->test(Token::OPERATOR_TYPE) && ($op = $this->operators->getBinary($token->getValue())) && $op->getPrecedence() >= $precedence) { + while ( + $token->test(Token::OPERATOR_TYPE) + && ( + ($op = $this->operators->getTernary($token->getValue())) && $op->getPrecedence() >= $precedence + || ($op = $this->operators->getBinary($token->getValue())) && $op->getPrecedence() >= $precedence + ) + ) { $this->parser->getStream()->next(); - - if ('is not' === $token->getValue()) { - $expr = $this->parseNotTestExpression($expr); - } elseif ('is' === $token->getValue()) { - $expr = $this->parseTestExpression($expr); - } elseif (null !== $op->getCallable()) { - $expr = $op->getCallable()($this->parser, $expr); - } else { - $previous = $this->setDeprecationCheck(true); - try { - $expr1 = $this->parseExpression(OperatorAssociativity::Left === $op->getAssociativity() ? $op->getPrecedence() + 1 : $op->getPrecedence()); - } finally { - $this->setDeprecationCheck($previous); - } - $class = $op->getNodeClass(); - if (!$class) { - throw new \LogicException(\sprintf('Operator "%s" must have a Node class.', $op->getOperator())); - } - $expr = new $class($expr, $expr1, $token->getLine()); + $previous = $this->setDeprecationCheck(true); + try { + $expr = $op->parse($this, $expr, $token); + } finally { + $this->setDeprecationCheck($previous); } - $expr->setAttribute('operator', $op); - $this->triggerPrecedenceDeprecations($expr); - $token = $this->parser->getCurrentToken(); } - if (0 === $precedence) { - return $this->parseConditionalExpression($expr); - } - return $expr; } @@ -152,115 +151,29 @@ private function triggerPrecedenceDeprecations(AbstractExpression $expr): void } /** - * @return ArrowFunctionExpression|null + * @internal */ - private function parseArrow() - { - $stream = $this->parser->getStream(); - - // short array syntax (one argument, no parentheses)? - if ($stream->look(1)->test(Token::ARROW_TYPE)) { - $line = $stream->getCurrent()->getLine(); - $token = $stream->expect(Token::NAME_TYPE); - $names = [new AssignContextVariable($token->getValue(), $token->getLine())]; - $stream->expect(Token::ARROW_TYPE); - - return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line); - } - - // first, determine if we are parsing an arrow function by finding => (long form) - $i = 0; - if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) { - return null; - } - ++$i; - while (true) { - // variable name - ++$i; - if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) { - break; - } - ++$i; - } - if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) { - return null; - } - ++$i; - if (!$stream->look($i)->test(Token::ARROW_TYPE)) { - return null; - } - - // yes, let's parse it properly - $token = $stream->expect(Token::PUNCTUATION_TYPE, '('); - $line = $token->getLine(); - - $names = []; - while (true) { - $token = $stream->expect(Token::NAME_TYPE); - $names[] = new AssignContextVariable($token->getValue(), $token->getLine()); - - if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { - break; - } - } - $stream->expect(Token::PUNCTUATION_TYPE, ')'); - $stream->expect(Token::ARROW_TYPE); - - return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line); - } - - private function getPrimary(): AbstractExpression + public function parsePrimary(): AbstractExpression { $token = $this->parser->getCurrentToken(); - if ($token->test(Token::OPERATOR_TYPE) && $operator = $this->operators->getUnary($token->getValue())) { - $this->parser->getStream()->next(); - $expr = $this->parseExpression($operator->getPrecedence()); - $class = $operator->getNodeClass(); - if (!$class) { - throw new \LogicException(\sprintf('Operator "%s" must have a Node class.', $operator->getOperator())); - } - - $expr = new $class($expr, $token->getLine()); - $expr->setAttribute('operator', $operator); - - if ($this->deprecationCheck) { - $this->triggerPrecedenceDeprecations($expr); - } - - return $this->parsePostfixExpression($expr); - } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) { $this->parser->getStream()->next(); $previous = $this->setDeprecationCheck(false); try { - $expr = $this->parseExpression()->setExplicitParentheses(); + $expr = $operator->parse($this, $token); } finally { $this->setDeprecationCheck($previous); } - $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); - - return $this->parsePostfixExpression($expr); - } - - return $this->parsePrimaryExpression(); - } + $expr->setAttribute('operator', $operator); - private function parseConditionalExpression($expr): AbstractExpression - { - while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) { - $expr2 = $this->parseExpression(); - if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { - // Ternary operator (expr ? expr2 : expr3) - $expr3 = $this->parseExpression(); - } else { - // Ternary without else (expr ? expr2) - $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine()); + if ($this->deprecationCheck) { + $this->triggerPrecedenceDeprecations($expr); } - $expr = new ConditionalTernary($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine()); + return $expr; } - return $expr; + return $this->parsePrimaryExpression(); } public function parsePrimaryExpression() @@ -272,54 +185,52 @@ public function parsePrimaryExpression() switch ($token->getValue()) { case 'true': case 'TRUE': - $node = new ConstantExpression(true, $token->getLine()); - break; + return new ConstantExpression(true, $token->getLine()); case 'false': case 'FALSE': - $node = new ConstantExpression(false, $token->getLine()); - break; + return new ConstantExpression(false, $token->getLine()); case 'none': case 'NONE': case 'null': case 'NULL': - $node = new ConstantExpression(null, $token->getLine()); - break; + return new ConstantExpression(null, $token->getLine()); default: - if ('(' === $this->parser->getCurrentToken()->getValue()) { - $node = $this->getFunctionNode($token->getValue(), $token->getLine()); - } else { - $node = new ContextVariable($token->getValue(), $token->getLine()); - } + return new ContextVariable($token->getValue(), $token->getLine()); } - break; + // no break case $token->test(Token::NUMBER_TYPE): $this->parser->getStream()->next(); - $node = new ConstantExpression($token->getValue(), $token->getLine()); - break; + + return new ConstantExpression($token->getValue(), $token->getLine()); case $token->test(Token::STRING_TYPE): case $token->test(Token::INTERPOLATION_START_TYPE): - $node = $this->parseStringExpression(); - break; + return $this->parseStringExpression(); case $token->test(Token::PUNCTUATION_TYPE): - $node = match ($token->getValue()) { - '[' => $this->parseSequenceExpression(), + // In 4.0, we should always return the node or throw an error for default + if ($node = match ($token->getValue()) { '{' => $this->parseMappingExpression(), - default => throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()), - }; - break; + default => null, + }) { + return $node; + } + // no break case $token->test(Token::OPERATOR_TYPE): + if ('[' === $token->getValue()) { + return $this->parseSequenceExpression(); + } + if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { // in this context, string operators are variable names $this->parser->getStream()->next(); - $node = new ContextVariable($token->getValue(), $token->getLine()); - break; + + return new ContextVariable($token->getValue(), $token->getLine()); } if ('=' === $token->getValue() && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { @@ -330,8 +241,6 @@ public function parsePrimaryExpression() default: throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); } - - return $this->parsePostfixExpression($node); } public function parseStringExpression() @@ -375,7 +284,7 @@ public function parseArrayExpression() public function parseSequenceExpression() { $stream = $this->parser->getStream(); - $stream->expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected'); + $stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected'); $node = new ArrayExpression([], $stream->getCurrent()->getLine()); $first = true; @@ -455,7 +364,7 @@ public function parseMappingExpression() } } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) { $key = new ConstantExpression($token->getValue(), $token->getLine()); - } elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) { + } elseif ($stream->test(Token::OPERATOR_TYPE, '(')) { $key = $this->parseExpression(); } else { $current = $stream->getCurrent(); @@ -473,8 +382,13 @@ public function parseMappingExpression() return $node; } + /** + * @deprecated since Twig 3.20 + */ public function parsePostfixExpression($node) { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + while (true) { $token = $this->parser->getCurrentToken(); if ($token->test(Token::PUNCTUATION_TYPE)) { @@ -493,81 +407,45 @@ public function parsePostfixExpression($node) return $node; } - public function getFunctionNode($name, $line) - { - if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) { - return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $this->createArguments($line), $line); - } - - $args = $this->parseNamedArguments(); - $function = $this->getFunction($name, $line); - - if ($function->getParserCallable()) { - $fakeNode = new EmptyNode($line); - $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext()); - - return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line); - } - - if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { - $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); - } - - if (!$ready = $this->readyNodes[$class]) { - trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); - } - - return new $class($ready ? $function : $function->getName(), $args, $line); - } - + /** + * @deprecated since Twig 3.20 + */ public function parseSubscriptExpression($node) { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + if ('.' === $this->parser->getStream()->next()->getValue()) { - return $this->parseSubscriptExpressionDot($node); + return $this->operators->getBinary('.')->parse($this, $node, $this->parser->getCurrentToken()); } - return $this->parseSubscriptExpressionArray($node); + return $this->operators->getBinary('[')->parse($this, $node, $this->parser->getCurrentToken()); } + /** + * @deprecated since Twig 3.20 + */ public function parseFilterExpression($node) { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + $this->parser->getStream()->next(); return $this->parseFilterExpressionRaw($node); } + /** + * @deprecated since Twig 3.20 + */ public function parseFilterExpressionRaw($node) { - if (\func_num_args() > 1) { - trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); - } + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + $op = $this->operators->getBinary('|'); while (true) { - $token = $this->parser->getStream()->expect(Token::NAME_TYPE); - - if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) { - $arguments = new EmptyNode(); - } else { - $arguments = $this->parseNamedArguments(); - } - - $filter = $this->getFilter($token->getValue(), $token->getLine()); - - $ready = true; - if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) { - $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); - } - - if (!$ready = $this->readyNodes[$class]) { - trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); - } - - $node = new $class($node, $ready ? $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments, $token->getLine()); - - if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) { + $node = $op->parse($this, $node, $this->parser->getCurrentToken()); + if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { break; } - $this->parser->getStream()->next(); } @@ -600,7 +478,7 @@ public function parseArguments() $args = []; $stream = $this->parser->getStream(); - $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); $hasSpread = false; while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { if ($args) { @@ -634,7 +512,7 @@ public function parseArguments() $name = $value->getAttribute('name'); if ($definition) { - $value = $this->getPrimary(); + $value = $this->parsePrimary(); if (!$this->checkConstantExpression($value)) { throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); @@ -664,8 +542,13 @@ public function parseArguments() return new Nodes($args); } + /** + * @deprecated since Twig 3.20, use "AbstractTokenParser::parseAssignmentExpression()" instead + */ public function parseAssignmentExpression() { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated, use "AbstractTokenParser::parseAssignmentExpression()" instead.', __METHOD__); + $stream = $this->parser->getStream(); $targets = []; while (true) { @@ -686,8 +569,13 @@ public function parseAssignmentExpression() return new Nodes($targets); } + /** + * @deprecated since Twig 3.20 + */ public function parseMultitargetExpression() { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + $targets = []; while (true) { $targets[] = $this->parseExpression(); @@ -699,131 +587,19 @@ public function parseMultitargetExpression() return new Nodes($targets); } - private function parseNotTestExpression(Node $node): NotUnary + public function getTest(int $line): TwigTest { - return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine()); + return $this->parser->getTest($line); } - private function parseTestExpression(Node $node): TestExpression + public function getFunction(string $name, int $line): TwigFunction { - $stream = $this->parser->getStream(); - $test = $this->getTest($node->getTemplateLine()); - - $arguments = null; - if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { - $arguments = $this->parseNamedArguments(); - } elseif ($test->hasOneMandatoryArgument()) { - $arguments = new Nodes([0 => $this->getPrimary()]); - } - - if ('defined' === $test->getName() && $node instanceof ContextVariable && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) { - $node = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine()); - } - - $ready = $test instanceof TwigTest; - if (!isset($this->readyNodes[$class = $test->getNodeClass()])) { - $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); - } - - if (!$ready = $this->readyNodes[$class]) { - trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); - } - - return new $class($node, $ready ? $test : $test->getName(), $arguments, $this->parser->getCurrentToken()->getLine()); + return $this->parser->getFunction($name, $line); } - private function getTest(int $line): TwigTest + public function getFilter(string $name, int $line): TwigFilter { - $stream = $this->parser->getStream(); - $name = $stream->expect(Token::NAME_TYPE)->getValue(); - - if ($stream->test(Token::NAME_TYPE)) { - // try 2-words tests - $name = $name.' '.$this->parser->getCurrentToken()->getValue(); - - if ($test = $this->env->getTest($name)) { - $stream->next(); - } - } else { - $test = $this->env->getTest($name); - } - - if (!$test) { - if ($this->parser->shouldIgnoreUnknownTwigCallables()) { - return new TwigTest($name, fn () => ''); - } - $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext()); - $e->addSuggestions($name, array_keys($this->env->getTests())); - - throw $e; - } - - if ($test->isDeprecated()) { - $stream = $this->parser->getStream(); - $src = $stream->getSourceContext(); - $test->triggerDeprecation($src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine()); - } - - return $test; - } - - private function getFunction(string $name, int $line): TwigFunction - { - try { - $function = $this->env->getFunction($name); - } catch (SyntaxError $e) { - if (!$this->parser->shouldIgnoreUnknownTwigCallables()) { - throw $e; - } - - $function = null; - } - - if (!$function) { - if ($this->parser->shouldIgnoreUnknownTwigCallables()) { - return new TwigFunction($name, fn () => ''); - } - $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext()); - $e->addSuggestions($name, array_keys($this->env->getFunctions())); - - throw $e; - } - - if ($function->isDeprecated()) { - $src = $this->parser->getStream()->getSourceContext(); - $function->triggerDeprecation($src->getPath() ?: $src->getName(), $line); - } - - return $function; - } - - private function getFilter(string $name, int $line): TwigFilter - { - try { - $filter = $this->env->getFilter($name); - } catch (SyntaxError $e) { - if (!$this->parser->shouldIgnoreUnknownTwigCallables()) { - throw $e; - } - - $filter = null; - } - if (!$filter) { - if ($this->parser->shouldIgnoreUnknownTwigCallables()) { - return new TwigFilter($name, fn () => ''); - } - $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext()); - $e->addSuggestions($name, array_keys($this->env->getFilters())); - - throw $e; - } - - if ($filter->isDeprecated()) { - $src = $this->parser->getStream()->getSourceContext(); - $filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line); - } - - return $filter; + return $this->parser->getFilter($name, $line); } // checks that the node only contains "constant" elements @@ -853,10 +629,13 @@ private function setDeprecationCheck(bool $deprecationCheck): bool return $current; } - private function createArguments(int $line): ArrayExpression + /** + * @internal + */ + public function parseCallableArguments(int $line, bool $parseOpenParenthesis = true): ArrayExpression { $arguments = new ArrayExpression([], $line); - foreach ($this->parseNamedArguments() as $k => $n) { + foreach ($this->parseNamedArguments($parseOpenParenthesis) as $k => $n) { $arguments->addElement($n, new LocalVariable($k, $line)); } @@ -873,11 +652,13 @@ public function parseOnlyArguments() return $this->parseNamedArguments(); } - public function parseNamedArguments(): Nodes + public function parseNamedArguments(bool $parseOpenParenthesis = true): Nodes { $args = []; $stream = $this->parser->getStream(); - $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + if ($parseOpenParenthesis) { + $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + } $hasSpread = false; while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { if ($args) { @@ -917,87 +698,4 @@ public function parseNamedArguments(): Nodes return new Nodes($args); } - - private function parseSubscriptExpressionDot(Node $node): AbstractExpression - { - $stream = $this->parser->getStream(); - $token = $stream->getCurrent(); - $lineno = $token->getLine(); - $arguments = new ArrayExpression([], $lineno); - $type = Template::ANY_CALL; - - if ($stream->nextIf(Token::PUNCTUATION_TYPE, '(')) { - $attribute = $this->parseExpression(); - $stream->expect(Token::PUNCTUATION_TYPE, ')'); - } else { - $token = $stream->next(); - if ( - $token->test(Token::NAME_TYPE) - || $token->test(Token::NUMBER_TYPE) - || ($token->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) - ) { - $attribute = new ConstantExpression($token->getValue(), $token->getLine()); - } else { - throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext()); - } - } - - if ($stream->test(Token::PUNCTUATION_TYPE, '(')) { - $type = Template::METHOD_CALL; - $arguments = $this->createArguments($token->getLine()); - } - - if ( - $node instanceof ContextVariable - && ( - null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name')) - || '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression - ) - ) { - return new MacroReferenceExpression(new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $node->getTemplateLine()); - } - - return new GetAttrExpression($node, $attribute, $arguments, $type, $lineno); - } - - private function parseSubscriptExpressionArray(Node $node): AbstractExpression - { - $stream = $this->parser->getStream(); - $token = $stream->getCurrent(); - $lineno = $token->getLine(); - $arguments = new ArrayExpression([], $lineno); - - // slice? - $slice = false; - if ($stream->test(Token::PUNCTUATION_TYPE, ':')) { - $slice = true; - $attribute = new ConstantExpression(0, $token->getLine()); - } else { - $attribute = $this->parseExpression(); - } - - if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) { - $slice = true; - } - - if ($slice) { - if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { - $length = new ConstantExpression(null, $token->getLine()); - } else { - $length = $this->parseExpression(); - } - - $filter = $this->getFilter('slice', $token->getLine()); - $arguments = new Nodes([$attribute, $length]); - $filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine()); - - $stream->expect(Token::PUNCTUATION_TYPE, ']'); - - return $filter; - } - - $stream->expect(Token::PUNCTUATION_TYPE, ']'); - - return new GetAttrExpression($node, $attribute, $arguments, Template::ARRAY_CALL, $lineno); - } } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 53e04271074..074a965f749 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -34,15 +34,19 @@ use Twig\Node\Node; use Twig\Operator\Binary\AddBinaryOperator; use Twig\Operator\Binary\AndBinaryOperator; +use Twig\Operator\Binary\ArrowBinaryOperator; use Twig\Operator\Binary\BitwiseAndBinaryOperator; use Twig\Operator\Binary\BitwiseOrBinaryOperator; use Twig\Operator\Binary\BitwiseXorBinaryOperator; use Twig\Operator\Binary\ConcatBinaryOperator; use Twig\Operator\Binary\DivBinaryOperator; +use Twig\Operator\Binary\DotBinaryOperator; use Twig\Operator\Binary\ElvisBinaryOperator; use Twig\Operator\Binary\EndsWithBinaryOperator; use Twig\Operator\Binary\EqualBinaryOperator; +use Twig\Operator\Binary\FilterBinaryOperator; use Twig\Operator\Binary\FloorDivBinaryOperator; +use Twig\Operator\Binary\FunctionBinaryOperator; use Twig\Operator\Binary\GreaterBinaryOperator; use Twig\Operator\Binary\GreaterEqualBinaryOperator; use Twig\Operator\Binary\HasEveryBinaryOperator; @@ -62,11 +66,14 @@ use Twig\Operator\Binary\PowerBinaryOperator; use Twig\Operator\Binary\RangeBinaryOperator; use Twig\Operator\Binary\SpaceshipBinaryOperator; +use Twig\Operator\Binary\SquareBracketBinaryOperator; use Twig\Operator\Binary\StartsWithBinaryOperator; use Twig\Operator\Binary\SubBinaryOperator; use Twig\Operator\Binary\XorBinaryOperator; +use Twig\Operator\Ternary\ConditionalTernaryOperator; use Twig\Operator\Unary\NegUnaryOperator; use Twig\Operator\Unary\NotUnaryOperator; +use Twig\Operator\Unary\ParenthesisUnaryOperator; use Twig\Operator\Unary\PosUnaryOperator; use Twig\Parser; use Twig\Sandbox\SecurityNotAllowedMethodError; @@ -319,6 +326,7 @@ public function getOperators(): array new NotUnaryOperator(), new NegUnaryOperator(), new PosUnaryOperator(), + new ParenthesisUnaryOperator(), new ElvisBinaryOperator(), new NullCoalesceBinaryOperator(), @@ -353,6 +361,13 @@ public function getOperators(): array new IsBinaryOperator(), new IsNotBinaryOperator(), new PowerBinaryOperator(), + new FilterBinaryOperator(), + new DotBinaryOperator(), + new SquareBracketBinaryOperator(), + new FunctionBinaryOperator(), + new ArrowBinaryOperator(), + + new ConditionalTernaryOperator(), ]; } diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index 6d5e4b5fa7f..7eef100f904 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -63,7 +63,12 @@ public function getFunctions(); /** * Returns a list of operators to add to the existing list. * - * @return OperatorInterface[] + * @return OperatorInterface[]|array + * + * @psalm-return OperatorInterface[]|array{ + * array}>, + * array, associativity: ExpressionParser::OPERATOR_*}> + * } */ public function getOperators(); } diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index 7a924f7b141..ad6ee7d0713 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -16,12 +16,15 @@ use Twig\Extension\GlobalsInterface; use Twig\Extension\LastModifiedExtensionInterface; use Twig\Extension\StagingExtension; +use Twig\Node\Expression\AbstractExpression; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\Operator\Binary\AbstractBinaryOperator; +use Twig\Operator\Binary\BinaryOperatorInterface; use Twig\Operator\OperatorAssociativity; use Twig\Operator\OperatorInterface; use Twig\Operator\Operators; use Twig\Operator\Unary\AbstractUnaryOperator; +use Twig\Operator\Unary\UnaryOperatorInterface; use Twig\TokenParser\TokenParserInterface; /** @@ -497,7 +500,7 @@ private function initExtension(ExtensionInterface $extension): void throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); } - trigger_deprecation('twig/twig', '3.19.0', \sprintf('Extension "%s" uses the old signature for "getOperators()", please update it to return an array of "OperatorInterface" objects.', \get_class($extension))); + trigger_deprecation('twig/twig', '3.20', \sprintf('Extension "%s" uses the old signature for "getOperators()", please update it to return an array of "OperatorInterface" objects.', \get_class($extension))); $ops = []; foreach ($operators[0] as $n => $op) { @@ -515,9 +518,9 @@ private function initExtension(ExtensionInterface $extension): void private function convertUnaryOperators(string $n, array $op): OperatorInterface { - trigger_deprecation('twig/twig', '3.19.0', \sprintf('Using a non-OperatorInterface object to define the "%s" unary operator is deprecated.', $n)); + trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-OperatorInterface object to define the "%s" unary operator is deprecated.', $n)); - return new class($op, $n) extends AbstractUnaryOperator { + return new class($op, $n) extends AbstractUnaryOperator implements UnaryOperatorInterface { public function __construct(private array $op, private string $operator) { } @@ -537,18 +540,18 @@ public function getPrecedenceChange(): ?OperatorPrecedenceChange return $this->op['precedence_change'] ?? null; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { - return $this->op['class'] ?? null; + return $this->op['class'] ?? ''; } }; } private function convertBinaryOperators(string $n, array $op): OperatorInterface { - trigger_deprecation('twig/twig', '3.19.0', \sprintf('Using a non-OperatorInterface object to define the "%s" binary operator is deprecated.', $n)); + trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-OperatorInterface object to define the "%s" binary operator is deprecated.', $n)); - return new class($op, $n) extends AbstractBinaryOperator { + return new class($op, $n) extends AbstractBinaryOperator implements BinaryOperatorInterface { public function __construct(private array $op, private string $operator) { } @@ -568,9 +571,9 @@ public function getPrecedenceChange(): ?OperatorPrecedenceChange return $this->op['precedence_change'] ?? null; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { - return $this->op['class'] ?? null; + return $this->op['class'] ?? ''; } public function getAssociativity(): OperatorAssociativity @@ -582,9 +585,13 @@ public function getAssociativity(): OperatorAssociativity }; } - public function getCallable(): ?callable + public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression { - return $this->op['callable'] ?? null; + if ($this->op['callable']) { + return $this->op['callable']($parser, $expr); + } + + return parent::parse($parser, $expr, $token); } }; } diff --git a/src/Lexer.php b/src/Lexer.php index 215da8e9a5c..84b29de32a7 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -36,6 +36,8 @@ class Lexer private $position; private $positions; private $currentVarBlockLine; + private array $openingBrackets = ['{', '(', '[']; + private array $closingBrackets = ['}', ')', ']']; public const STATE_DATA = 0; public const STATE_BLOCK = 1; @@ -337,14 +339,18 @@ private function lexExpression(): void $this->pushToken(Token::SPREAD_TYPE, '...'); $this->moveCursor('...'); } - // arrow function - elseif ('=' === $this->code[$this->cursor] && ($this->cursor + 1 < $this->end) && '>' === $this->code[$this->cursor + 1]) { - $this->pushToken(Token::ARROW_TYPE, '=>'); - $this->moveCursor('=>'); - } // operators elseif (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) { - $this->pushToken(Token::OPERATOR_TYPE, preg_replace('/\s+/', ' ', $match[0])); + $operator = preg_replace('/\s+/', ' ', $match[0]); + $type = Token::OPERATOR_TYPE; + // to be removed in 4.0 + if (str_contains(self::PUNCTUATION, $operator)) { + $type = Token::PUNCTUATION_TYPE; + } + if (in_array($operator, $this->openingBrackets)) { + $this->checkBrackets($operator); + } + $this->pushToken($type, $operator); $this->moveCursor($match[0]); } // names @@ -359,22 +365,7 @@ private function lexExpression(): void } // punctuation elseif (str_contains(self::PUNCTUATION, $this->code[$this->cursor])) { - // opening bracket - if (str_contains('([{', $this->code[$this->cursor])) { - $this->brackets[] = [$this->code[$this->cursor], $this->lineno]; - } - // closing bracket - elseif (str_contains(')]}', $this->code[$this->cursor])) { - if (!$this->brackets) { - throw new SyntaxError(\sprintf('Unexpected "%s".', $this->code[$this->cursor]), $this->lineno, $this->source); - } - - [$expect, $lineno] = array_pop($this->brackets); - if ($this->code[$this->cursor] != strtr($expect, '([{', ')]}')) { - throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); - } - } - + $this->checkBrackets($this->code[$this->cursor]); $this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]); ++$this->cursor; } @@ -589,4 +580,22 @@ private function popState(): void $this->state = array_pop($this->states); } + + private function checkBrackets(string $code): void + { + // opening bracket + if (in_array($code, $this->openingBrackets)) { + $this->brackets[] = [$code, $this->lineno]; + } elseif (in_array($code, $this->closingBrackets)) { + // closing bracket + if (!$this->brackets) { + throw new SyntaxError(\sprintf('Unexpected "%s".', $code), $this->lineno, $this->source); + } + + [$expect, $lineno] = array_pop($this->brackets); + if ($code !== str_replace($this->openingBrackets, $this->closingBrackets, $expect)) { + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + } + } + } } diff --git a/src/Node/Expression/ArrowFunctionExpression.php b/src/Node/Expression/ArrowFunctionExpression.php index 2bae4edd75f..552b8fe9115 100644 --- a/src/Node/Expression/ArrowFunctionExpression.php +++ b/src/Node/Expression/ArrowFunctionExpression.php @@ -12,6 +12,9 @@ namespace Twig\Node\Expression; use Twig\Compiler; +use Twig\Error\SyntaxError; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; /** @@ -23,6 +26,14 @@ class ArrowFunctionExpression extends AbstractExpression { public function __construct(AbstractExpression $expr, Node $names, $lineno) { + if (!$names instanceof ListExpression && !$names instanceof ContextVariable) { + throw new SyntaxError('The arrow function argument must be a list of variables or a single variable.', $names->getTemplateLine(), $names->getSourceContext()); + } + + if ($names instanceof ContextVariable) { + $names = new ListExpression([new AssignContextVariable($names->getAttribute('name'), $names->getTemplateLine())], $lineno); + } + parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno); } @@ -31,19 +42,7 @@ public function compile(Compiler $compiler): void $compiler ->addDebugInfo($this) ->raw('function (') - ; - foreach ($this->getNode('names') as $i => $name) { - if ($i) { - $compiler->raw(', '); - } - - $compiler - ->raw('$__') - ->raw($name->getAttribute('name')) - ->raw('__') - ; - } - $compiler + ->subcompile($this->getNode('names')) ->raw(') use ($context, $macros) { ') ; foreach ($this->getNode('names') as $name) { diff --git a/src/Node/Expression/ListExpression.php b/src/Node/Expression/ListExpression.php new file mode 100644 index 00000000000..8a774a19581 --- /dev/null +++ b/src/Node/Expression/ListExpression.php @@ -0,0 +1,48 @@ + $items + */ + public function __construct(array $items, int $lineno) + { + foreach ($items as $item) { + if (!$item instanceof ContextVariable) { + throw new SyntaxError('All elements of a list expression must be variable names.'.get_class($item), $item->getTemplateLine(), $item->getSourceContext()); + } + } + + parent::__construct($items, [], $lineno); + } + + public function compile(Compiler $compiler): void + { + foreach ($this as $i => $name) { + if ($i) { + $compiler->raw(', '); + } + + $compiler + ->raw('$__') + ->raw($name->getAttribute('name')) + ->raw('__') + ; + } + } +} diff --git a/src/Operator/Binary/AbstractBinaryOperator.php b/src/Operator/Binary/AbstractBinaryOperator.php index 2eec2537f3c..64fc9033991 100644 --- a/src/Operator/Binary/AbstractBinaryOperator.php +++ b/src/Operator/Binary/AbstractBinaryOperator.php @@ -11,12 +11,22 @@ namespace Twig\Operator\Binary; +use Twig\ExpressionParser; +use Twig\Node\Expression\AbstractExpression; use Twig\Operator\AbstractOperator; use Twig\Operator\OperatorArity; use Twig\Operator\OperatorAssociativity; +use Twig\Token; -abstract class AbstractBinaryOperator extends AbstractOperator +abstract class AbstractBinaryOperator extends AbstractOperator implements BinaryOperatorInterface { + public function parse(ExpressionParser $parser, AbstractExpression $left, Token $token): AbstractExpression + { + $right = $parser->parseExpression(OperatorAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence()); + + return new ($this->getNodeClass())($left, $right, $token->getLine()); + } + public function getArity(): OperatorArity { return OperatorArity::Binary; @@ -27,8 +37,8 @@ public function getAssociativity(): OperatorAssociativity return OperatorAssociativity::Left; } - public function getCallable(): ?callable - { - return null; - } + /** + * @return class-string + */ + abstract protected function getNodeClass(): string; } diff --git a/src/Operator/Binary/AddBinaryOperator.php b/src/Operator/Binary/AddBinaryOperator.php index f840f2cffbf..7e708c38004 100644 --- a/src/Operator/Binary/AddBinaryOperator.php +++ b/src/Operator/Binary/AddBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 30; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return AddBinary::class; } diff --git a/src/Operator/Binary/AndBinaryOperator.php b/src/Operator/Binary/AndBinaryOperator.php index 28a06721f5f..78a79d18324 100644 --- a/src/Operator/Binary/AndBinaryOperator.php +++ b/src/Operator/Binary/AndBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 15; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return AndBinary::class; } diff --git a/src/Operator/Binary/ArrowBinaryOperator.php b/src/Operator/Binary/ArrowBinaryOperator.php new file mode 100644 index 00000000000..253b00fc78c --- /dev/null +++ b/src/Operator/Binary/ArrowBinaryOperator.php @@ -0,0 +1,49 @@ +parseExpression(), $expr, $token->getLine()); + } + + public function getOperator(): string + { + return '=>'; + } + + public function getPrecedence(): int + { + return 250; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } +} diff --git a/src/Operator/Binary/BinaryOperatorInterface.php b/src/Operator/Binary/BinaryOperatorInterface.php new file mode 100644 index 00000000000..5ff2c4b9a70 --- /dev/null +++ b/src/Operator/Binary/BinaryOperatorInterface.php @@ -0,0 +1,25 @@ +getStream(); + $token = $stream->getCurrent(); + $lineno = $token->getLine(); + $arguments = new ArrayExpression([], $lineno); + $type = Template::ANY_CALL; + + if ($stream->nextIf(Token::OPERATOR_TYPE, '(')) { + $attribute = $parser->parseExpression(); + $stream->expect(Token::PUNCTUATION_TYPE, ')'); + } else { + $token = $stream->next(); + if ( + $token->test(Token::NAME_TYPE) + || $token->test(Token::NUMBER_TYPE) + || ($token->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) + ) { + $attribute = new ConstantExpression($token->getValue(), $token->getLine()); + } else { + throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext()); + } + } + + if ($stream->test(Token::OPERATOR_TYPE, '(')) { + $type = Template::METHOD_CALL; + $arguments = $parser->parseCallableArguments($token->getLine()); + } + + if ( + $expr instanceof NameExpression + && ( + null !== $parser->getImportedSymbol('template', $expr->getAttribute('name')) + || '_self' === $expr->getAttribute('name') && $attribute instanceof ConstantExpression + ) + ) { + return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $expr->getTemplateLine()); + } + + return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno); + } + + public function getOperator(): string + { + return '.'; + } + + public function getPrecedence(): int + { + return 300; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } +} diff --git a/src/Operator/Binary/ElvisBinaryOperator.php b/src/Operator/Binary/ElvisBinaryOperator.php index c1dac1ec9ac..dacc53819a0 100644 --- a/src/Operator/Binary/ElvisBinaryOperator.php +++ b/src/Operator/Binary/ElvisBinaryOperator.php @@ -26,7 +26,7 @@ public function getAliases(): array return ['? :']; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return ElvisBinary::class; } diff --git a/src/Operator/Binary/EndsWithBinaryOperator.php b/src/Operator/Binary/EndsWithBinaryOperator.php index 430448b2f9a..1073d3cb15b 100644 --- a/src/Operator/Binary/EndsWithBinaryOperator.php +++ b/src/Operator/Binary/EndsWithBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return EndsWithBinary::class; } diff --git a/src/Operator/Binary/EqualBinaryOperator.php b/src/Operator/Binary/EqualBinaryOperator.php index c1d46c6777b..7bcaeb4e1fb 100644 --- a/src/Operator/Binary/EqualBinaryOperator.php +++ b/src/Operator/Binary/EqualBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return EqualBinary::class; } diff --git a/src/Operator/Binary/FilterBinaryOperator.php b/src/Operator/Binary/FilterBinaryOperator.php new file mode 100644 index 00000000000..9dc6333898f --- /dev/null +++ b/src/Operator/Binary/FilterBinaryOperator.php @@ -0,0 +1,73 @@ +getStream(); + $token = $stream->expect(Token::NAME_TYPE); + $line = $token->getLine(); + + if (!$stream->test(Token::OPERATOR_TYPE, '(')) { + $arguments = new EmptyNode(); + } else { + $arguments = $parser->parseNamedArguments(); + } + + $filter = $parser->getFilter($token->getValue(), $line); + + $ready = true; + if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($expr, $ready ? $filter : new ConstantExpression($filter->getName(), $line), $arguments, $line); + } + + public function getOperator(): string + { + return '|'; + } + + public function getPrecedence(): int + { + return 300; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } +} diff --git a/src/Operator/Binary/FloorDivBinaryOperator.php b/src/Operator/Binary/FloorDivBinaryOperator.php index 62d5e0a4e67..05ef8480902 100644 --- a/src/Operator/Binary/FloorDivBinaryOperator.php +++ b/src/Operator/Binary/FloorDivBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 60; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return FloorDivBinary::class; } diff --git a/src/Operator/Binary/FunctionBinaryOperator.php b/src/Operator/Binary/FunctionBinaryOperator.php new file mode 100644 index 00000000000..740c1ce5948 --- /dev/null +++ b/src/Operator/Binary/FunctionBinaryOperator.php @@ -0,0 +1,84 @@ +getLine(); + if (!$expr instanceof NameExpression) { + throw new SyntaxError('Function name must be an identifier.', $line, $parser->getStream()->getSourceContext()); + } + + $name = $expr->getAttribute('name'); + + if (null !== $alias = $parser->getImportedSymbol('function', $name)) { + return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $parser->parseCallableArguments($line, false), $line); + } + + $args = $parser->parseNamedArguments(false); + + $function = $parser->getFunction($name, $line); + + if ($function->getParserCallable()) { + $fakeNode = new EmptyNode($line); + $fakeNode->setSourceContext($parser->getStream()->getSourceContext()); + + return ($function->getParserCallable())($parser->getParser(), $fakeNode, $args, $line); + } + + if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($ready ? $function : $function->getName(), $args, $line); + } + + public function getOperator(): string + { + return '('; + } + + public function getPrecedence(): int + { + return 300; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } +} diff --git a/src/Operator/Binary/GreaterBinaryOperator.php b/src/Operator/Binary/GreaterBinaryOperator.php index 9a25be4adae..b6ef2349572 100644 --- a/src/Operator/Binary/GreaterBinaryOperator.php +++ b/src/Operator/Binary/GreaterBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return GreaterBinary::class; } diff --git a/src/Operator/Binary/GreaterEqualBinaryOperator.php b/src/Operator/Binary/GreaterEqualBinaryOperator.php index aa709b55c39..69a5ad203bb 100644 --- a/src/Operator/Binary/GreaterEqualBinaryOperator.php +++ b/src/Operator/Binary/GreaterEqualBinaryOperator.php @@ -15,7 +15,7 @@ class GreaterEqualBinaryOperator extends AbstractBinaryOperator { - public function getNodeClass(): ?string + protected function getNodeClass(): string { return GreaterEqualBinary::class; } diff --git a/src/Operator/Binary/HasEveryBinaryOperator.php b/src/Operator/Binary/HasEveryBinaryOperator.php index 98a74d32dbb..1312640aed0 100644 --- a/src/Operator/Binary/HasEveryBinaryOperator.php +++ b/src/Operator/Binary/HasEveryBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return HasEveryBinary::class; } diff --git a/src/Operator/Binary/HasSomeBinaryOperator.php b/src/Operator/Binary/HasSomeBinaryOperator.php index 4aa4150d8be..13dd25c2964 100644 --- a/src/Operator/Binary/HasSomeBinaryOperator.php +++ b/src/Operator/Binary/HasSomeBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return HasSomeBinary::class; } diff --git a/src/Operator/Binary/InBinaryOperator.php b/src/Operator/Binary/InBinaryOperator.php index 70058c86b1a..b103e147ac7 100644 --- a/src/Operator/Binary/InBinaryOperator.php +++ b/src/Operator/Binary/InBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return InBinary::class; } diff --git a/src/Operator/Binary/IsBinaryOperator.php b/src/Operator/Binary/IsBinaryOperator.php index 160c6d07847..4236b769a44 100644 --- a/src/Operator/Binary/IsBinaryOperator.php +++ b/src/Operator/Binary/IsBinaryOperator.php @@ -11,20 +11,68 @@ namespace Twig\Operator\Binary; -class IsBinaryOperator extends AbstractBinaryOperator +use Twig\Attribute\FirstClassTwigCallableReady; +use Twig\ExpressionParser; +use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\ArrayExpression; +use Twig\Node\Expression\MacroReferenceExpression; +use Twig\Node\Expression\NameExpression; +use Twig\Node\Nodes; +use Twig\Operator\AbstractOperator; +use Twig\Operator\OperatorArity; +use Twig\Operator\OperatorAssociativity; +use Twig\Token; +use Twig\TwigTest; + +class IsBinaryOperator extends AbstractOperator implements BinaryOperatorInterface { - public function getPrecedence(): int + private $readyNodes = []; + + public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression { - return 100; + $stream = $parser->getStream(); + $test = $parser->getTest($token->getLine()); + + $arguments = null; + if ($stream->test(Token::OPERATOR_TYPE, '(')) { + $arguments = $parser->parseNamedArguments(); + } elseif ($test->hasOneMandatoryArgument()) { + $arguments = new Nodes([0 => $parser->parseExpression($this->getPrecedence())]); + } + + if ('defined' === $test->getName() && $expr instanceof NameExpression && null !== $alias = $parser->getImportedSymbol('function', $expr->getAttribute('name'))) { + $expr = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $expr->getTemplateLine()), $expr->getTemplateLine()); + } + + $ready = $test instanceof TwigTest; + if (!isset($this->readyNodes[$class = $test->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($expr, $ready ? $test : $test->getName(), $arguments, $stream->getCurrent()->getLine()); } - public function getNodeClass(): ?string + public function getPrecedence(): int { - return null; + return 100; } public function getOperator(): string { return 'is'; } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } } diff --git a/src/Operator/Binary/IsNotBinaryOperator.php b/src/Operator/Binary/IsNotBinaryOperator.php index 4e1c8e7a58d..2455f738714 100644 --- a/src/Operator/Binary/IsNotBinaryOperator.php +++ b/src/Operator/Binary/IsNotBinaryOperator.php @@ -11,16 +11,16 @@ namespace Twig\Operator\Binary; -class IsNotBinaryOperator extends AbstractBinaryOperator -{ - public function getPrecedence(): int - { - return 100; - } +use Twig\ExpressionParser; +use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\Unary\NotUnary; +use Twig\Token; - public function getNodeClass(): ?string +class IsNotBinaryOperator extends IsBinaryOperator +{ + public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression { - return null; + return new NotUnary(parent::parse($parser, $expr, $token), $token->getLine()); } public function getOperator(): string diff --git a/src/Operator/Binary/LessBinaryOperator.php b/src/Operator/Binary/LessBinaryOperator.php index 56961dc94e9..f6e13095694 100644 --- a/src/Operator/Binary/LessBinaryOperator.php +++ b/src/Operator/Binary/LessBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return LessBinary::class; } diff --git a/src/Operator/Binary/LessEqualBinaryOperator.php b/src/Operator/Binary/LessEqualBinaryOperator.php index 374449356ae..0eb43d6ff2f 100644 --- a/src/Operator/Binary/LessEqualBinaryOperator.php +++ b/src/Operator/Binary/LessEqualBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return LessEqualBinary::class; } diff --git a/src/Operator/Binary/MatchesBinaryOperator.php b/src/Operator/Binary/MatchesBinaryOperator.php index 12b04ceafe7..2e7828f5ec0 100644 --- a/src/Operator/Binary/MatchesBinaryOperator.php +++ b/src/Operator/Binary/MatchesBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return MatchesBinary::class; } diff --git a/src/Operator/Binary/ModBinaryOperator.php b/src/Operator/Binary/ModBinaryOperator.php index 42efbed088c..9aaa239a9b0 100644 --- a/src/Operator/Binary/ModBinaryOperator.php +++ b/src/Operator/Binary/ModBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 60; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return ModBinary::class; } diff --git a/src/Operator/Binary/MulBinaryOperator.php b/src/Operator/Binary/MulBinaryOperator.php index 19e784ee211..d9077b662f0 100644 --- a/src/Operator/Binary/MulBinaryOperator.php +++ b/src/Operator/Binary/MulBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 60; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return MulBinary::class; } diff --git a/src/Operator/Binary/NotEqualBinaryOperator.php b/src/Operator/Binary/NotEqualBinaryOperator.php index d3b474bcf69..1636ca2adc5 100644 --- a/src/Operator/Binary/NotEqualBinaryOperator.php +++ b/src/Operator/Binary/NotEqualBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return NotEqualBinary::class; } diff --git a/src/Operator/Binary/NotInBinaryOperator.php b/src/Operator/Binary/NotInBinaryOperator.php index e8d24469b4e..6e35b77b819 100644 --- a/src/Operator/Binary/NotInBinaryOperator.php +++ b/src/Operator/Binary/NotInBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return NotInBinary::class; } diff --git a/src/Operator/Binary/NullCoalesceBinaryOperator.php b/src/Operator/Binary/NullCoalesceBinaryOperator.php index c8603d7879b..f89c0d3862f 100644 --- a/src/Operator/Binary/NullCoalesceBinaryOperator.php +++ b/src/Operator/Binary/NullCoalesceBinaryOperator.php @@ -32,7 +32,7 @@ public function getPrecedenceChange(): ?OperatorPrecedenceChange return new OperatorPrecedenceChange('twig/twig', '3.15', 5); } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return NullCoalesceBinary::class; } diff --git a/src/Operator/Binary/OrBinaryOperator.php b/src/Operator/Binary/OrBinaryOperator.php index 2a4565b1264..59fcd3067c2 100644 --- a/src/Operator/Binary/OrBinaryOperator.php +++ b/src/Operator/Binary/OrBinaryOperator.php @@ -20,7 +20,7 @@ public function getOperator(): string return 'or'; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return OrBinary::class; } diff --git a/src/Operator/Binary/PowerBinaryOperator.php b/src/Operator/Binary/PowerBinaryOperator.php index be26723b75c..98797b75bc3 100644 --- a/src/Operator/Binary/PowerBinaryOperator.php +++ b/src/Operator/Binary/PowerBinaryOperator.php @@ -26,7 +26,7 @@ public function getPrecedence(): int return 200; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return PowerBinary::class; } diff --git a/src/Operator/Binary/RangeBinaryOperator.php b/src/Operator/Binary/RangeBinaryOperator.php index 63082263135..84c77d60c5c 100644 --- a/src/Operator/Binary/RangeBinaryOperator.php +++ b/src/Operator/Binary/RangeBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 25; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return RangeBinary::class; } diff --git a/src/Operator/Binary/SpaceshipBinaryOperator.php b/src/Operator/Binary/SpaceshipBinaryOperator.php index eead28ff921..ef67c9e3f09 100644 --- a/src/Operator/Binary/SpaceshipBinaryOperator.php +++ b/src/Operator/Binary/SpaceshipBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return SpaceshipBinary::class; } diff --git a/src/Operator/Binary/SquareBracketBinaryOperator.php b/src/Operator/Binary/SquareBracketBinaryOperator.php new file mode 100644 index 00000000000..c2e8b500652 --- /dev/null +++ b/src/Operator/Binary/SquareBracketBinaryOperator.php @@ -0,0 +1,87 @@ +getStream(); + $lineno = $token->getLine(); + $arguments = new ArrayExpression([], $lineno); + + // slice? + $slice = false; + if ($stream->test(Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + $attribute = new ConstantExpression(0, $token->getLine()); + } else { + $attribute = $parser->parseExpression(); + } + + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + } + + if ($slice) { + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { + $length = new ConstantExpression(null, $token->getLine()); + } else { + $length = $parser->parseExpression(); + } + + $filter = $parser->getFilter('slice', $token->getLine()); + $arguments = new Nodes([$attribute, $length]); + $filter = new ($filter->getNodeClass())($expr, $filter, $arguments, $token->getLine()); + + $stream->expect(Token::PUNCTUATION_TYPE, ']'); + + return $filter; + } + + $stream->expect(Token::PUNCTUATION_TYPE, ']'); + + return new GetAttrExpression($expr, $attribute, $arguments, Template::ARRAY_CALL, $lineno); + } + + public function getOperator(): string + { + return '['; + } + + public function getPrecedence(): int + { + return 300; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Binary; + } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } +} diff --git a/src/Operator/Binary/StartsWithBinaryOperator.php b/src/Operator/Binary/StartsWithBinaryOperator.php index e28953b74a4..4d543454d0a 100644 --- a/src/Operator/Binary/StartsWithBinaryOperator.php +++ b/src/Operator/Binary/StartsWithBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 20; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return StartsWithBinary::class; } diff --git a/src/Operator/Binary/SubBinaryOperator.php b/src/Operator/Binary/SubBinaryOperator.php index 4fb413c5de6..c36c90707f8 100644 --- a/src/Operator/Binary/SubBinaryOperator.php +++ b/src/Operator/Binary/SubBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 30; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return SubBinary::class; } diff --git a/src/Operator/Binary/XorBinaryOperator.php b/src/Operator/Binary/XorBinaryOperator.php index 1ea03413262..72a3b96429f 100644 --- a/src/Operator/Binary/XorBinaryOperator.php +++ b/src/Operator/Binary/XorBinaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 12; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return XorBinary::class; } diff --git a/src/Operator/OperatorInterface.php b/src/Operator/OperatorInterface.php index f7fcfb735e0..8512bdeade6 100644 --- a/src/Operator/OperatorInterface.php +++ b/src/Operator/OperatorInterface.php @@ -11,17 +11,13 @@ namespace Twig\Operator; -use Twig\Node\Expression\AbstractExpression; use Twig\OperatorPrecedenceChange; interface OperatorInterface { - public function getOperator(): string; + public function __toString(): string; - /** - * @return class-string - */ - public function getNodeClass(): ?string; + public function getOperator(): string; public function getArity(): OperatorArity; diff --git a/src/Operator/Operators.php b/src/Operator/Operators.php index 19b92444224..65b7934b658 100644 --- a/src/Operator/Operators.php +++ b/src/Operator/Operators.php @@ -11,15 +11,33 @@ namespace Twig\Operator; -use Twig\Operator\Binary\AbstractBinaryOperator; -use Twig\Operator\Unary\AbstractUnaryOperator; +use Twig\Operator\Binary\BinaryOperatorInterface; +use Twig\Operator\Ternary\TernaryOperatorInterface; +use Twig\Operator\Unary\UnaryOperatorInterface; -class Operators implements \IteratorAggregate +/** + * @template-implements \IteratorAggregate + */ +final class Operators implements \IteratorAggregate { + /** + * @var array, array> + */ private array $operators = []; + + /** + * @var array, array> + */ private array $aliases = []; + + /** + * @var \WeakMap>|null + */ private ?\WeakMap $precedenceChanges = null; + /** + * @param array $operators + */ public function __construct( array $operators = [], ) { @@ -27,7 +45,7 @@ public function __construct( } /** - * @param array $operators + * @param array $operators * * @return $this */ @@ -44,16 +62,21 @@ public function add(array $operators): self return $this; } - public function getUnary(string $name): ?AbstractUnaryOperator + public function getUnary(string $name): ?UnaryOperatorInterface { return $this->operators[OperatorArity::Unary->value][$name] ?? ($this->aliases[OperatorArity::Unary->value][$name] ?? null); } - public function getBinary(string $name): ?AbstractBinaryOperator + public function getBinary(string $name): ?BinaryOperatorInterface { return $this->operators[OperatorArity::Binary->value][$name] ?? ($this->aliases[OperatorArity::Binary->value][$name] ?? null); } + public function getTernary(string $name): ?TernaryOperatorInterface + { + return $this->operators[OperatorArity::Ternary->value][$name] ?? ($this->aliases[OperatorArity::Ternary->value][$name] ?? null); + } + public function getIterator(): \Traversable { foreach ($this->operators as $operators) { @@ -65,7 +88,7 @@ public function getIterator(): \Traversable /** * @internal * - * @return \WeakMap> + * @return \WeakMap> */ public function getPrecedenceChanges(): \WeakMap { diff --git a/src/Operator/Ternary/AbstractTernaryOperator.php b/src/Operator/Ternary/AbstractTernaryOperator.php index eceaa79a1e0..3a88247ff30 100644 --- a/src/Operator/Ternary/AbstractTernaryOperator.php +++ b/src/Operator/Ternary/AbstractTernaryOperator.php @@ -13,11 +13,17 @@ use Twig\Operator\AbstractOperator; use Twig\Operator\OperatorArity; +use Twig\Operator\OperatorAssociativity; -abstract class AbstractTernaryOperator extends AbstractOperator +abstract class AbstractTernaryOperator extends AbstractOperator implements TernaryOperatorInterface { public function getArity(): OperatorArity { return OperatorArity::Ternary; } + + public function getAssociativity(): OperatorAssociativity + { + return OperatorAssociativity::Left; + } } diff --git a/src/Operator/Ternary/ConditionalTernaryOperator.php b/src/Operator/Ternary/ConditionalTernaryOperator.php new file mode 100644 index 00000000000..7c20963db57 --- /dev/null +++ b/src/Operator/Ternary/ConditionalTernaryOperator.php @@ -0,0 +1,50 @@ +parseExpression($this->getPrecedence()); + if ($parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, $this->getElseOperator())) { + // Ternary operator (expr ? expr2 : expr3) + $else = $parser->parseExpression($this->getPrecedence()); + } else { + // Ternary without else (expr ? expr2) + $else = new ConstantExpression('', $token->getLine()); + } + + return new ConditionalTernary($left, $then, $else, $token->getLine()); + } + + public function getOperator(): string + { + return '?'; + } + + public function getPrecedence(): int + { + return 0; + } + + private function getElseOperator(): string + { + return ':'; + } +} diff --git a/src/Operator/Ternary/TernaryOperatorInterface.php b/src/Operator/Ternary/TernaryOperatorInterface.php new file mode 100644 index 00000000000..86628203e03 --- /dev/null +++ b/src/Operator/Ternary/TernaryOperatorInterface.php @@ -0,0 +1,25 @@ +getNodeClass())($parser->parseExpression($this->getPrecedence()), $token->getLine()); + } + public function getArity(): OperatorArity { return OperatorArity::Unary; } + + /** + * @return class-string + */ + abstract protected function getNodeClass(): string; } diff --git a/src/Operator/Unary/NegUnaryOperator.php b/src/Operator/Unary/NegUnaryOperator.php index eb72d1b6f82..de01a3b5176 100644 --- a/src/Operator/Unary/NegUnaryOperator.php +++ b/src/Operator/Unary/NegUnaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 500; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return NegUnary::class; } diff --git a/src/Operator/Unary/NotUnaryOperator.php b/src/Operator/Unary/NotUnaryOperator.php index b0bb4cc10dd..6ee1a0c32e8 100644 --- a/src/Operator/Unary/NotUnaryOperator.php +++ b/src/Operator/Unary/NotUnaryOperator.php @@ -31,7 +31,7 @@ public function getPrecedenceChange(): ?OperatorPrecedenceChange return new OperatorPrecedenceChange('twig/twig', '3.15', 70); } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return NotUnary::class; } diff --git a/src/Operator/Unary/ParenthesisUnaryOperator.php b/src/Operator/Unary/ParenthesisUnaryOperator.php new file mode 100644 index 00000000000..8191cb82045 --- /dev/null +++ b/src/Operator/Unary/ParenthesisUnaryOperator.php @@ -0,0 +1,74 @@ +getStream(); + $expr = $parser->parseExpression($this->getPrecedence()); + + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ')')) { + if (!$stream->test(Token::OPERATOR_TYPE, '=>')) { + return $expr->setExplicitParentheses(); + } + + return new ListExpression([$expr], $token->getLine()); + } + + // determine if we are parsing an arrow function arguments + if (!$stream->test(Token::PUNCTUATION_TYPE, ',')) { + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); + } + + $names = [$expr]; + while (true) { + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ')')) { + break; + } + $stream->expect(Token::PUNCTUATION_TYPE, ','); + $token = $stream->expect(Token::NAME_TYPE); + $names[] = new ContextVariable($token->getValue(), $token->getLine()); + } + + if (!$stream->test(Token::OPERATOR_TYPE, '=>')) { + throw new SyntaxError('A list of variables must be followed by an arrow.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + + return new ListExpression($names, $token->getLine()); + } + + public function getOperator(): string + { + return '('; + } + + public function getPrecedence(): int + { + return 0; + } + + public function getArity(): OperatorArity + { + return OperatorArity::Unary; + } +} diff --git a/src/Operator/Unary/PosUnaryOperator.php b/src/Operator/Unary/PosUnaryOperator.php index 255085182a9..88059530534 100644 --- a/src/Operator/Unary/PosUnaryOperator.php +++ b/src/Operator/Unary/PosUnaryOperator.php @@ -25,7 +25,7 @@ public function getPrecedence(): int return 500; } - public function getNodeClass(): ?string + protected function getNodeClass(): string { return PosUnary::class; } diff --git a/src/Operator/Unary/UnaryOperatorInterface.php b/src/Operator/Unary/UnaryOperatorInterface.php new file mode 100644 index 00000000000..71c599acf65 --- /dev/null +++ b/src/Operator/Unary/UnaryOperatorInterface.php @@ -0,0 +1,22 @@ +stream->getCurrent(); } + public function getFunction(string $name, int $line): TwigFunction + { + try { + $function = $this->env->getFunction($name); + } catch (SyntaxError $e) { + if (!$this->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $function = null; + } + + if (!$function) { + if ($this->shouldIgnoreUnknownTwigCallables()) { + return new TwigFunction($name, fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getFunctions())); + + throw $e; + } + + if ($function->isDeprecated()) { + $src = $this->stream->getSourceContext(); + $function->triggerDeprecation($src->getPath() ?: $src->getName(), $line); + } + + return $function; + } + + public function getFilter(string $name, int $line): TwigFilter + { + try { + $filter = $this->env->getFilter($name); + } catch (SyntaxError $e) { + if (!$this->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $filter = null; + } + if (!$filter) { + if ($this->shouldIgnoreUnknownTwigCallables()) { + return new TwigFilter($name, fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getFilters())); + + throw $e; + } + + if ($filter->isDeprecated()) { + $src = $this->stream->getSourceContext(); + $filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line); + } + + return $filter; + } + + public function getTest(int $line): TwigTest + { + $name = $this->stream->expect(Token::NAME_TYPE)->getValue(); + + if ($this->stream->test(Token::NAME_TYPE)) { + // try 2-words tests + $name = $name.' '.$this->getCurrentToken()->getValue(); + + if ($test = $this->env->getTest($name)) { + $this->stream->next(); + } + } else { + $test = $this->env->getTest($name); + } + + if (!$test) { + if ($this->shouldIgnoreUnknownTwigCallables()) { + return new TwigTest($name, fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $this->stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getTests())); + + throw $e; + } + + if ($test->isDeprecated()) { + $src = $this->stream->getSourceContext(); + $test->triggerDeprecation($src->getPath() ?: $src->getName(), $this->stream->getCurrent()->getLine()); + } + + return $test; + } + private function filterBodyNodes(Node $node, bool $nested = false): ?Node { // check that the body does not contain non-empty output nodes diff --git a/src/Token.php b/src/Token.php index a4da548cbf2..6390472e026 100644 --- a/src/Token.php +++ b/src/Token.php @@ -30,6 +30,9 @@ final class Token public const PUNCTUATION_TYPE = 9; public const INTERPOLATION_START_TYPE = 10; public const INTERPOLATION_END_TYPE = 11; + /** + * @deprecated since Twig 3.20, "arrow" is now an operator + */ public const ARROW_TYPE = 12; public const SPREAD_TYPE = 13; @@ -38,6 +41,9 @@ public function __construct( private $value, private int $lineno, ) { + if (self::ARROW_TYPE === $type) { + trigger_deprecation('twig/twig', '3.20', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::ARROW_TYPE); + } } public function __toString(): string @@ -63,7 +69,39 @@ public function test($type, $values = null): bool $type = self::NAME_TYPE; } - return ($this->type === $type) && ( + if (self::ARROW_TYPE === $type) { + trigger_deprecation('twig/twig', '3.20', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::typeToEnglish(self::ARROW_TYPE)); + + return self::OPERATOR_TYPE === $this->type && '=>' === $this->value; + } + + $typeMatches = $this->type === $type; + if ($typeMatches && self::PUNCTUATION_TYPE === $type && \in_array($this->value, ['(', '[', '|', '.', '?', '?:']) && $values) { + foreach ((array) $values as $value) { + if (\in_array($value, ['(', '[', '|', '.', '?', '?:'])) { + trigger_deprecation('twig/twig', '3.20', 'The "%s" token is now an "%s" token instead of a "%s" one.', $this->value, self::typeToEnglish(self::OPERATOR_TYPE), $this->toEnglish()); + + break; + } + } + } + if (!$typeMatches) { + if (self::OPERATOR_TYPE === $type && self::PUNCTUATION_TYPE === $this->type) { + if ($values) { + foreach ((array) $values as $value) { + if (\in_array($value, ['(', '[', '|', '.', '?', '?:'])) { + $typeMatches = true; + + break; + } + } + } else { + $typeMatches = true; + } + } + } + + return $typeMatches && ( null === $values || (\is_array($values) && \in_array($this->value, $values)) || $this->value == $values diff --git a/src/TokenParser/AbstractTokenParser.php b/src/TokenParser/AbstractTokenParser.php index 720ea676283..30bef15a340 100644 --- a/src/TokenParser/AbstractTokenParser.php +++ b/src/TokenParser/AbstractTokenParser.php @@ -11,7 +11,11 @@ namespace Twig\TokenParser; +use Twig\Lexer; +use Twig\Node\Expression\Variable\AssignContextVariable; +use Twig\Node\Nodes; use Twig\Parser; +use Twig\Token; /** * Base class for all token parsers. @@ -29,4 +33,31 @@ public function setParser(Parser $parser): void { $this->parser = $parser; } + + /** + * Parses an assignment expression like "a, b". + * + * @return Nodes + */ + protected function parseAssignmentExpression(): Nodes + { + $stream = $this->parser->getStream(); + $targets = []; + while (true) { + $token = $stream->getCurrent(); + if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { + // in this context, string operators are variable names + $stream->next(); + } else { + $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); + } + $targets[] = new AssignContextVariable($token->getValue(), $token->getLine()); + + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + return new Nodes($targets); + } } diff --git a/src/TokenParser/ApplyTokenParser.php b/src/TokenParser/ApplyTokenParser.php index 0c95074828a..68ef7c17e6b 100644 --- a/src/TokenParser/ApplyTokenParser.php +++ b/src/TokenParser/ApplyTokenParser.php @@ -33,7 +33,15 @@ public function parse(Token $token): Node { $lineno = $token->getLine(); $ref = new LocalVariable(null, $lineno); - $filter = $this->parser->getExpressionParser()->parseFilterExpressionRaw($ref); + $filter = $ref; + $op = $this->parser->getEnvironment()->getOperators()->getBinary('|'); + while (true) { + $filter = $op->parse($this->parser->getExpressionParser(), $filter, $this->parser->getCurrentToken()); + if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { + break; + } + $this->parser->getStream()->next(); + } $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideApplyEnd'], true); diff --git a/src/TokenParser/ForTokenParser.php b/src/TokenParser/ForTokenParser.php index 3e08b22fa8a..b098737fa6f 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -35,7 +35,7 @@ public function parse(Token $token): Node { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $targets = $this->parser->getExpressionParser()->parseAssignmentExpression(); + $targets = $this->parseAssignmentExpression(); $stream->expect(Token::OPERATOR_TYPE, 'in'); $seq = $this->parser->getExpressionParser()->parseExpression(); diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index 33379be0319..1d857730011 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -73,7 +73,7 @@ private function parseDefinition(): ArrayExpression { $arguments = new ArrayExpression([], $this->parser->getCurrentToken()->getLine()); $stream = $this->parser->getStream(); - $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { if (\count($arguments)) { $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); diff --git a/src/TokenParser/SetTokenParser.php b/src/TokenParser/SetTokenParser.php index bb43907bd24..c9ebceb0bf8 100644 --- a/src/TokenParser/SetTokenParser.php +++ b/src/TokenParser/SetTokenParser.php @@ -13,6 +13,7 @@ use Twig\Error\SyntaxError; use Twig\Node\Node; +use Twig\Node\Nodes; use Twig\Node\SetNode; use Twig\Token; @@ -34,11 +35,11 @@ public function parse(Token $token): Node { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - $names = $this->parser->getExpressionParser()->parseAssignmentExpression(); + $names = $this->parseAssignmentExpression(); $capture = false; if ($stream->nextIf(Token::OPERATOR_TYPE, '=')) { - $values = $this->parser->getExpressionParser()->parseMultitargetExpression(); + $values = $this->parseMultitargetExpression(); $stream->expect(Token::BLOCK_END_TYPE); @@ -70,4 +71,17 @@ public function getTag(): string { return 'set'; } + + private function parseMultitargetExpression() + { + $targets = []; + while (true) { + $targets[] = $this->parser->getExpressionParser()->parseExpression(); + if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + return new Nodes($targets); + } } diff --git a/src/TokenParser/TypesTokenParser.php b/src/TokenParser/TypesTokenParser.php index a7da0f5ecf4..2c7b77c024b 100644 --- a/src/TokenParser/TypesTokenParser.php +++ b/src/TokenParser/TypesTokenParser.php @@ -63,7 +63,7 @@ private function parseSimpleMappingExpression(TokenStream $stream): array if ($stream->nextIf(Token::OPERATOR_TYPE, '?:')) { $isOptional = true; } else { - $isOptional = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '?'); + $isOptional = null !== $stream->nextIf(Token::OPERATOR_TYPE, '?'); $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); } diff --git a/tests/Fixtures/filters/arrow_reserved_names.test b/tests/Fixtures/filters/arrow_reserved_names.test index 3e5d0722b1a..188373feee5 100644 --- a/tests/Fixtures/filters/arrow_reserved_names.test +++ b/tests/Fixtures/filters/arrow_reserved_names.test @@ -5,4 +5,4 @@ --DATA-- return [] --EXCEPTION-- -Twig\Error\SyntaxError: You cannot assign a value to "true" in "index.twig" at line 2. +Twig\Error\SyntaxError: The arrow function argument must be a list of variables or a single variable in "index.twig" at line 2. From 272fbe4c9d1342fe00ad15f2df6f32ad29195608 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 26 Jan 2025 15:19:24 +0100 Subject: [PATCH 3/7] Add a script to update operator precedence documentation --- bin/generate_operators_precedence.php | 64 +++++++++++++++++++++++++++ doc/operators_precedence.rst | 56 +++++++++++++++++++++++ doc/templates.rst | 32 ++------------ 3 files changed, 123 insertions(+), 29 deletions(-) create mode 100644 bin/generate_operators_precedence.php create mode 100644 doc/operators_precedence.rst diff --git a/bin/generate_operators_precedence.php b/bin/generate_operators_precedence.php new file mode 100644 index 00000000000..97cdf016d09 --- /dev/null +++ b/bin/generate_operators_precedence.php @@ -0,0 +1,64 @@ +getPrecedenceChange() ? $a->getPrecedenceChange()->getNewPrecedence() : $a->getPrecedence(); + $bPrecedence = $b->getPrecedenceChange() ? $b->getPrecedenceChange()->getNewPrecedence() : $b->getPrecedence(); + return $bPrecedence - $aPrecedence; + }); + + $current = \PHP_INT_MAX; + foreach ($operators as $operator) { + $precedence = $operator->getPrecedenceChange() ? $operator->getPrecedenceChange()->getNewPrecedence() : $operator->getPrecedence(); + if ($precedence !== $current) { + $current = $precedence; + if ($withAssociativity) { + fwrite($output, \sprintf("\n%-11d %-11s %s", $precedence, $operator->getOperator(), OperatorAssociativity::Left === $operator->getAssociativity() ? 'Left' : 'Right')); + } else { + fwrite($output, \sprintf("\n%-11d %s", $precedence, $operator->getOperator())); + } + } else { + fwrite($output, "\n".str_repeat(' ', 12).$operator->getOperator()); + } + } + fwrite($output, "\n"); +} + +$output = fopen(dirname(__DIR__).'/doc/operators_precedence.rst', 'w'); + +$twig = new Environment(new ArrayLoader([])); +$unaryOperators = []; +$notUnaryOperators = []; +foreach ($twig->getOperators() as $operator) { + if ($operator->getArity()->value == OperatorArity::Unary->value) { + $unaryOperators[] = $operator; + } else { + $notUnaryOperators[] = $operator; + } +} + +fwrite($output, "Unary operators precedence:\n"); +printOperators($output, $unaryOperators); + +fwrite($output, "\nBinary and Ternary operators precedence:\n"); +printOperators($output, $notUnaryOperators, true); + +fclose($output); diff --git a/doc/operators_precedence.rst b/doc/operators_precedence.rst new file mode 100644 index 00000000000..6ce1b521bbd --- /dev/null +++ b/doc/operators_precedence.rst @@ -0,0 +1,56 @@ +Unary operators precedence: + +=========== =========== +Precedence Operator +=========== =========== + +500 - + + +70 not +0 ( + +Binary and Ternary operators precedence: + +=========== =========== ============= +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 diff --git a/doc/templates.rst b/doc/templates.rst index 7bf2d15f591..960093152b7 100644 --- a/doc/templates.rst +++ b/doc/templates.rst @@ -1033,35 +1033,9 @@ Understanding the precedence of these operators is crucial for writing correct and efficient Twig templates. The operator precedence rules are as follows, with the lowest-precedence -operators listed first: - -============================= =================================== ===================================================== -Operator Score of precedence Description -============================= =================================== ===================================================== -``?:`` 0 Ternary operator, conditional statement -``or`` 10 Logical OR operation between two boolean expressions -``xor`` 12 Logical XOR operation between two boolean expressions -``and`` 15 Logical AND operation between two boolean expressions -``b-or`` 16 Bitwise OR operation on integers -``b-xor`` 17 Bitwise XOR operation on integers -``b-and`` 18 Bitwise AND operation on integers -``==``, ``!=``, ``<=>``, 20 Comparison operators -``<``, ``>``, ``>=``, -``<=``, ``not in``, ``in``, -``matches``, ``starts with``, -``ends with``, ``has some``, -``has every`` -``..`` 25 Range of values -``+``, ``-`` 30 Addition and subtraction on numbers -``~`` 40 String concatenation -``not`` 50 Negates a statement -``*``, ``/``, ``//``, ``%`` 60 Arithmetic operations on numbers -``is``, ``is not`` 100 Tests -``**`` 200 Raises a number to the power of another -``??`` 300 Default value when a variable is null -``+``, ``-`` 500 Unary operations on numbers -``|``,``[]``,``.`` - Filters, sequence, mapping, and attribute access -============================= =================================== ===================================================== +operators listed first. + +.. include:: operators_precedence.rst Without using any parentheses, the operator precedence rules are used to determine how to convert the code to PHP: From ea266a13679c471a95fa9ea34c9be91b78dda9f0 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 3 Feb 2025 08:24:29 +0100 Subject: [PATCH 4/7] Move Operators to ExpressionParsers, deprecate ExpressionParser --- bin/generate_operators_precedence.php | 37 +- doc/operators_precedence.rst | 5 +- .../TokenParser/CacheTokenParser.php | 5 +- extra/cache-extra/composer.json | 2 +- src/Environment.php | 6 +- src/ExpressionParser.php | 428 ++---------------- .../AbstractExpressionParser.php} | 10 +- .../ExpressionParserInterface.php} | 12 +- src/ExpressionParser/ExpressionParserType.php | 33 ++ src/ExpressionParser/ExpressionParsers.php | 140 ++++++ src/ExpressionParser/Infix/ArgumentsTrait.php | 81 ++++ .../Infix/ArrowExpressionParser.php} | 28 +- .../Infix/BinaryOperatorExpressionParser.php | 73 +++ .../ConditionalTernaryExpressionParser.php} | 22 +- .../Infix/DotExpressionParser.php} | 32 +- .../Infix/FilterExpressionParser.php} | 32 +- .../Infix/FunctionExpressionParser.php} | 36 +- .../Infix/IsExpressionParser.php} | 32 +- .../Infix/IsNotExpressionParser.php} | 13 +- .../Infix/SquareBracketExpressionParser.php} | 28 +- .../InfixAssociativity.php} | 4 +- .../InfixExpressionParserInterface.php | 23 + src/ExpressionParser/PrecedenceChange.php | 42 ++ .../Prefix/GroupingExpressionParser.php} | 22 +- .../Prefix/LiteralExpressionParser.php | 243 ++++++++++ .../Prefix/UnaryOperatorExpressionParser.php | 64 +++ .../PrefixExpressionParserInterface.php} | 9 +- src/Extension/AbstractExtension.php | 5 + src/Extension/CoreExtension.php | 187 ++++---- src/Extension/ExtensionInterface.php | 13 +- src/ExtensionSet.php | 162 +++---- src/Lexer.php | 18 +- src/Node/Expression/ListExpression.php | 7 - .../Binary/AbstractBinaryOperator.php | 44 -- src/Operator/Binary/AddBinaryOperator.php | 32 -- src/Operator/Binary/AndBinaryOperator.php | 32 -- .../Binary/BinaryOperatorInterface.php | 25 - .../Binary/BitwiseAndBinaryOperator.php | 32 -- .../Binary/BitwiseOrBinaryOperator.php | 32 -- .../Binary/BitwiseXorBinaryOperator.php | 32 -- src/Operator/Binary/ConcatBinaryOperator.php | 38 -- src/Operator/Binary/DivBinaryOperator.php | 32 -- src/Operator/Binary/ElvisBinaryOperator.php | 43 -- .../Binary/EndsWithBinaryOperator.php | 32 -- src/Operator/Binary/EqualBinaryOperator.php | 32 -- .../Binary/FloorDivBinaryOperator.php | 32 -- src/Operator/Binary/GreaterBinaryOperator.php | 32 -- .../Binary/GreaterEqualBinaryOperator.php | 32 -- .../Binary/HasEveryBinaryOperator.php | 32 -- src/Operator/Binary/HasSomeBinaryOperator.php | 32 -- src/Operator/Binary/InBinaryOperator.php | 32 -- src/Operator/Binary/LessBinaryOperator.php | 32 -- .../Binary/LessEqualBinaryOperator.php | 32 -- src/Operator/Binary/MatchesBinaryOperator.php | 32 -- src/Operator/Binary/ModBinaryOperator.php | 32 -- src/Operator/Binary/MulBinaryOperator.php | 32 -- .../Binary/NotEqualBinaryOperator.php | 32 -- src/Operator/Binary/NotInBinaryOperator.php | 32 -- .../Binary/NullCoalesceBinaryOperator.php | 44 -- src/Operator/Binary/OrBinaryOperator.php | 32 -- src/Operator/Binary/PowerBinaryOperator.php | 38 -- src/Operator/Binary/RangeBinaryOperator.php | 32 -- .../Binary/SpaceshipBinaryOperator.php | 32 -- .../Binary/StartsWithBinaryOperator.php | 32 -- src/Operator/Binary/SubBinaryOperator.php | 32 -- src/Operator/Binary/XorBinaryOperator.php | 32 -- src/Operator/OperatorArity.php | 19 - src/Operator/Operators.php | 116 ----- .../Ternary/AbstractTernaryOperator.php | 29 -- .../Ternary/TernaryOperatorInterface.php | 25 - src/Operator/Unary/AbstractUnaryOperator.php | 36 -- src/Operator/Unary/NegUnaryOperator.php | 32 -- src/Operator/Unary/NotUnaryOperator.php | 38 -- src/Operator/Unary/PosUnaryOperator.php | 32 -- src/OperatorPrecedenceChange.php | 24 +- src/Parser.php | 81 +++- src/TokenParser/AbstractTokenParser.php | 2 - src/TokenParser/ApplyTokenParser.php | 5 +- src/TokenParser/AutoEscapeTokenParser.php | 2 +- src/TokenParser/BlockTokenParser.php | 2 +- src/TokenParser/DeprecatedTokenParser.php | 7 +- src/TokenParser/DoTokenParser.php | 2 +- src/TokenParser/EmbedTokenParser.php | 2 +- src/TokenParser/ExtendsTokenParser.php | 2 +- src/TokenParser/ForTokenParser.php | 2 +- src/TokenParser/FromTokenParser.php | 2 +- src/TokenParser/IfTokenParser.php | 4 +- src/TokenParser/ImportTokenParser.php | 2 +- src/TokenParser/IncludeTokenParser.php | 4 +- src/TokenParser/MacroTokenParser.php | 2 +- src/TokenParser/SetTokenParser.php | 4 +- src/TokenParser/UseTokenParser.php | 2 +- src/TokenParser/WithTokenParser.php | 2 +- tests/CustomExtensionTest.php | 2 +- tests/EnvironmentTest.php | 44 +- tests/ExpressionParserTest.php | 35 +- tests/Fixtures/operators/not_precedence.test | 2 +- 97 files changed, 1212 insertions(+), 2301 deletions(-) rename src/{Operator/AbstractOperator.php => ExpressionParser/AbstractExpressionParser.php} (57%) rename src/{Operator/OperatorInterface.php => ExpressionParser/ExpressionParserInterface.php} (60%) create mode 100644 src/ExpressionParser/ExpressionParserType.php create mode 100644 src/ExpressionParser/ExpressionParsers.php create mode 100644 src/ExpressionParser/Infix/ArgumentsTrait.php rename src/{Operator/Binary/ArrowBinaryOperator.php => ExpressionParser/Infix/ArrowExpressionParser.php} (52%) create mode 100644 src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php rename src/{Operator/Ternary/ConditionalTernaryOperator.php => ExpressionParser/Infix/ConditionalTernaryExpressionParser.php} (61%) rename src/{Operator/Binary/DotBinaryOperator.php => ExpressionParser/Infix/DotExpressionParser.php} (78%) rename src/{Operator/Binary/FilterBinaryOperator.php => ExpressionParser/Infix/FilterExpressionParser.php} (70%) rename src/{Operator/Binary/FunctionBinaryOperator.php => ExpressionParser/Infix/FunctionExpressionParser.php} (69%) rename src/{Operator/Binary/IsBinaryOperator.php => ExpressionParser/Infix/IsExpressionParser.php} (75%) rename src/{Operator/Binary/IsNotBinaryOperator.php => ExpressionParser/Infix/IsNotExpressionParser.php} (61%) rename src/{Operator/Binary/SquareBracketBinaryOperator.php => ExpressionParser/Infix/SquareBracketExpressionParser.php} (74%) rename src/{Operator/OperatorAssociativity.php => ExpressionParser/InfixAssociativity.php} (80%) create mode 100644 src/ExpressionParser/InfixExpressionParserInterface.php create mode 100644 src/ExpressionParser/PrecedenceChange.php rename src/{Operator/Unary/ParenthesisUnaryOperator.php => ExpressionParser/Prefix/GroupingExpressionParser.php} (80%) create mode 100644 src/ExpressionParser/Prefix/LiteralExpressionParser.php create mode 100644 src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php rename src/{Operator/Unary/UnaryOperatorInterface.php => ExpressionParser/PrefixExpressionParserInterface.php} (52%) delete mode 100644 src/Operator/Binary/AbstractBinaryOperator.php delete mode 100644 src/Operator/Binary/AddBinaryOperator.php delete mode 100644 src/Operator/Binary/AndBinaryOperator.php delete mode 100644 src/Operator/Binary/BinaryOperatorInterface.php delete mode 100644 src/Operator/Binary/BitwiseAndBinaryOperator.php delete mode 100644 src/Operator/Binary/BitwiseOrBinaryOperator.php delete mode 100644 src/Operator/Binary/BitwiseXorBinaryOperator.php delete mode 100644 src/Operator/Binary/ConcatBinaryOperator.php delete mode 100644 src/Operator/Binary/DivBinaryOperator.php delete mode 100644 src/Operator/Binary/ElvisBinaryOperator.php delete mode 100644 src/Operator/Binary/EndsWithBinaryOperator.php delete mode 100644 src/Operator/Binary/EqualBinaryOperator.php delete mode 100644 src/Operator/Binary/FloorDivBinaryOperator.php delete mode 100644 src/Operator/Binary/GreaterBinaryOperator.php delete mode 100644 src/Operator/Binary/GreaterEqualBinaryOperator.php delete mode 100644 src/Operator/Binary/HasEveryBinaryOperator.php delete mode 100644 src/Operator/Binary/HasSomeBinaryOperator.php delete mode 100644 src/Operator/Binary/InBinaryOperator.php delete mode 100644 src/Operator/Binary/LessBinaryOperator.php delete mode 100644 src/Operator/Binary/LessEqualBinaryOperator.php delete mode 100644 src/Operator/Binary/MatchesBinaryOperator.php delete mode 100644 src/Operator/Binary/ModBinaryOperator.php delete mode 100644 src/Operator/Binary/MulBinaryOperator.php delete mode 100644 src/Operator/Binary/NotEqualBinaryOperator.php delete mode 100644 src/Operator/Binary/NotInBinaryOperator.php delete mode 100644 src/Operator/Binary/NullCoalesceBinaryOperator.php delete mode 100644 src/Operator/Binary/OrBinaryOperator.php delete mode 100644 src/Operator/Binary/PowerBinaryOperator.php delete mode 100644 src/Operator/Binary/RangeBinaryOperator.php delete mode 100644 src/Operator/Binary/SpaceshipBinaryOperator.php delete mode 100644 src/Operator/Binary/StartsWithBinaryOperator.php delete mode 100644 src/Operator/Binary/SubBinaryOperator.php delete mode 100644 src/Operator/Binary/XorBinaryOperator.php delete mode 100644 src/Operator/OperatorArity.php delete mode 100644 src/Operator/Operators.php delete mode 100644 src/Operator/Ternary/AbstractTernaryOperator.php delete mode 100644 src/Operator/Ternary/TernaryOperatorInterface.php delete mode 100644 src/Operator/Unary/AbstractUnaryOperator.php delete mode 100644 src/Operator/Unary/NegUnaryOperator.php delete mode 100644 src/Operator/Unary/NotUnaryOperator.php delete mode 100644 src/Operator/Unary/PosUnaryOperator.php diff --git a/bin/generate_operators_precedence.php b/bin/generate_operators_precedence.php index 97cdf016d09..c22c81938d7 100644 --- a/bin/generate_operators_precedence.php +++ b/bin/generate_operators_precedence.php @@ -1,13 +1,14 @@ getPrecedenceChange() ? $a->getPrecedenceChange()->getNewPrecedence() : $a->getPrecedence(); $bPrecedence = $b->getPrecedenceChange() ? $b->getPrecedenceChange()->getNewPrecedence() : $b->getPrecedence(); return $bPrecedence - $aPrecedence; }); $current = \PHP_INT_MAX; - foreach ($operators as $operator) { - $precedence = $operator->getPrecedenceChange() ? $operator->getPrecedenceChange()->getNewPrecedence() : $operator->getPrecedence(); + 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, $operator->getOperator(), OperatorAssociativity::Left === $operator->getAssociativity() ? 'Left' : 'Right')); + fwrite($output, \sprintf("\n%-11d %-11s %s", $precedence, $expressionParser->getName(), InfixAssociativity::Left === $expressionParser->getAssociativity() ? 'Left' : 'Right')); } else { - fwrite($output, \sprintf("\n%-11d %s", $precedence, $operator->getOperator())); + fwrite($output, \sprintf("\n%-11d %s", $precedence, $expressionParser->getName())); } } else { - fwrite($output, "\n".str_repeat(' ', 12).$operator->getOperator()); + fwrite($output, "\n".str_repeat(' ', 12).$expressionParser->getName()); } } fwrite($output, "\n"); @@ -45,20 +46,20 @@ function printOperators($output, array $operators, bool $withAssociativity = fal $output = fopen(dirname(__DIR__).'/doc/operators_precedence.rst', 'w'); $twig = new Environment(new ArrayLoader([])); -$unaryOperators = []; -$notUnaryOperators = []; -foreach ($twig->getOperators() as $operator) { - if ($operator->getArity()->value == OperatorArity::Unary->value) { - $unaryOperators[] = $operator; - } else { - $notUnaryOperators[] = $operator; +$prefixExpressionParsers = []; +$infixExpressionParsers = []; +foreach ($twig->getExpressionParsers() as $expressionParser) { + if ($expressionParser instanceof PrefixExpressionParserInterface) { + $prefixExpressionParsers[] = $expressionParser; + } elseif ($expressionParser instanceof InfixExpressionParserInterface) { + $infixExpressionParsers[] = $expressionParser; } } fwrite($output, "Unary operators precedence:\n"); -printOperators($output, $unaryOperators); +printExpressionParsers($output, $prefixExpressionParsers); fwrite($output, "\nBinary and Ternary operators precedence:\n"); -printOperators($output, $notUnaryOperators, true); +printExpressionParsers($output, $infixExpressionParsers, true); fclose($output); diff --git a/doc/operators_precedence.rst b/doc/operators_precedence.rst index 6ce1b521bbd..032582fbe5e 100644 --- a/doc/operators_precedence.rst +++ b/doc/operators_precedence.rst @@ -8,6 +8,7 @@ Precedence Operator + 70 not 0 ( + literal Binary and Ternary operators precedence: @@ -15,9 +16,9 @@ Binary and Ternary operators precedence: Precedence Operator Associativity =========== =========== ============= -300 | Left - . +300 . Left [ + | ( 250 => Left 200 ** Right diff --git a/extra/cache-extra/TokenParser/CacheTokenParser.php b/extra/cache-extra/TokenParser/CacheTokenParser.php index dcc2ddd288f..086fad88eb6 100644 --- a/extra/cache-extra/TokenParser/CacheTokenParser.php +++ b/extra/cache-extra/TokenParser/CacheTokenParser.php @@ -24,8 +24,7 @@ class CacheTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $expressionParser = $this->parser->getExpressionParser(); - $key = $expressionParser->parseExpression(); + $key = $this->parser->parseExpression(); $ttl = null; $tags = null; @@ -41,7 +40,7 @@ public function parse(Token $token): Node if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { throw new SyntaxError(\sprintf('The "%s" modifier takes exactly one argument (0 given).', $k), $line, $stream->getSourceContext()); } - $arg = $expressionParser->parseExpression(); + $arg = $this->parser->parseExpression(); if ($stream->test(Token::PUNCTUATION_TYPE, ',')) { throw new SyntaxError(\sprintf('The "%s" modifier takes exactly one argument (2 given).', $k), $line, $stream->getSourceContext()); } diff --git a/extra/cache-extra/composer.json b/extra/cache-extra/composer.json index 4ae0621cd4d..cd7919eddb0 100644 --- a/extra/cache-extra/composer.json +++ b/extra/cache-extra/composer.json @@ -17,7 +17,7 @@ "require": { "php": ">=8.1.0", "symfony/cache": "^5.4|^6.4|^7.0", - "twig/twig": "^3.19|^4.0" + "twig/twig": "^3.20|^4.0" }, "require-dev": { "symfony/phpunit-bridge": "^6.4|^7.0" diff --git a/src/Environment.php b/src/Environment.php index 7792dca2855..a32dcbf0491 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -19,6 +19,7 @@ use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; +use Twig\ExpressionParser\ExpressionParsers; use Twig\Extension\CoreExtension; use Twig\Extension\EscaperExtension; use Twig\Extension\ExtensionInterface; @@ -30,7 +31,6 @@ use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\Operator\Operators; use Twig\Runtime\EscaperRuntime; use Twig\RuntimeLoader\FactoryRuntimeLoader; use Twig\RuntimeLoader\RuntimeLoaderInterface; @@ -925,9 +925,9 @@ public function mergeGlobals(array $context): array /** * @internal */ - public function getOperators(): Operators + public function getExpressionParsers(): ExpressionParsers { - return $this->extensionSet->getOperators(); + return $this->extensionSet->getExpressionParsers(); } private function updateOptionsHash(): void diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index ad4a05257f6..9922d11ec9b 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -13,20 +13,18 @@ namespace Twig; use Twig\Error\SyntaxError; -use Twig\Node\Expression\AbstractExpression; +use Twig\ExpressionParser\Infix\DotExpressionParser; +use Twig\ExpressionParser\Infix\FilterExpressionParser; +use Twig\ExpressionParser\Infix\SquareBracketExpressionParser; use Twig\Node\Expression\ArrayExpression; -use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\Unary\NegUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Expression\Unary\SpreadUnary; use Twig\Node\Expression\Variable\AssignContextVariable; use Twig\Node\Expression\Variable\ContextVariable; -use Twig\Node\Expression\Variable\LocalVariable; use Twig\Node\Node; use Twig\Node\Nodes; -use Twig\Operator\OperatorArity; -use Twig\Operator\Operators; /** * Parses expressions. @@ -37,6 +35,8 @@ * @see https://en.wikipedia.org/wiki/Operator-precedence_parser * * @author Fabien Potencier + * + * @deprecated since Twig 3.20 */ class ExpressionParser { @@ -49,38 +49,11 @@ class ExpressionParser */ public const OPERATOR_RIGHT = 2; - private Operators $operators; - private bool $deprecationCheck = true; - public function __construct( private Parser $parser, private Environment $env, ) { - $this->operators = $env->getOperators(); - } - - /** - * @internal - */ - public function getParser(): Parser - { - return $this->parser; - } - - /** - * @internal - */ - public function getStream(): TokenStream - { - return $this->parser->getStream(); - } - - /** - * @internal - */ - public function getImportedSymbol(string $type, string $name) - { - return $this->parser->getImportedSymbol($type, $name); + trigger_deprecation('twig/twig', '3.20', 'Class "%s" is deprecated, use "Parser::parseExpression()" instead.', __CLASS__); } public function parseExpression($precedence = 0) @@ -89,297 +62,69 @@ public function parseExpression($precedence = 0) trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); } - $expr = $this->parsePrimary(); - $token = $this->parser->getCurrentToken(); - while ( - $token->test(Token::OPERATOR_TYPE) - && ( - ($op = $this->operators->getTernary($token->getValue())) && $op->getPrecedence() >= $precedence - || ($op = $this->operators->getBinary($token->getValue())) && $op->getPrecedence() >= $precedence - ) - ) { - $this->parser->getStream()->next(); - $previous = $this->setDeprecationCheck(true); - try { - $expr = $op->parse($this, $expr, $token); - } finally { - $this->setDeprecationCheck($previous); - } - $expr->setAttribute('operator', $op); - $this->triggerPrecedenceDeprecations($expr); - $token = $this->parser->getCurrentToken(); - } - - return $expr; - } - - private function triggerPrecedenceDeprecations(AbstractExpression $expr): void - { - $precedenceChanges = $this->operators->getPrecedenceChanges(); - // Check that the all nodes that are between the 2 precedences have explicit parentheses - if (!$expr->hasAttribute('operator') || !isset($precedenceChanges[$expr->getAttribute('operator')])) { - return; - } + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated, use "Parser::parseExpression()" instead.', __METHOD__); - if (OperatorArity::Unary === $expr->getAttribute('operator')->getArity()) { - if ($expr->hasExplicitParentheses()) { - return; - } - $operator = $expr->getAttribute('operator'); - /** @var AbstractExpression $node */ - $node = $expr->getNode('node'); - foreach ($precedenceChanges as $op => $changes) { - if (!\in_array($operator, $changes, true)) { - continue; - } - if ($node->hasAttribute('operator') && $op === $node->getAttribute('operator')) { - $change = $operator->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.', $operator->getOperator(), $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); - } - } - } else { - foreach ($precedenceChanges[$expr->getAttribute('operator')] as $operator) { - foreach ($expr as $node) { - /** @var AbstractExpression $node */ - if ($node->hasAttribute('operator') && $operator === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) { - $change = $operator->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.', $operator->getOperator(), $this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); - } - } - } - } + return $this->parser->parseExpression((int) $precedence); } /** - * @internal + * @deprecated since Twig 3.20 */ - public function parsePrimary(): AbstractExpression - { - $token = $this->parser->getCurrentToken(); - if ($token->test(Token::OPERATOR_TYPE) && $operator = $this->operators->getUnary($token->getValue())) { - $this->parser->getStream()->next(); - $previous = $this->setDeprecationCheck(false); - try { - $expr = $operator->parse($this, $token); - } finally { - $this->setDeprecationCheck($previous); - } - $expr->setAttribute('operator', $operator); - - if ($this->deprecationCheck) { - $this->triggerPrecedenceDeprecations($expr); - } - - return $expr; - } - - return $this->parsePrimaryExpression(); - } - public function parsePrimaryExpression() { - $token = $this->parser->getCurrentToken(); - switch (true) { - case $token->test(Token::NAME_TYPE): - $this->parser->getStream()->next(); - switch ($token->getValue()) { - case 'true': - case 'TRUE': - return new ConstantExpression(true, $token->getLine()); - - case 'false': - case 'FALSE': - return new ConstantExpression(false, $token->getLine()); - - case 'none': - case 'NONE': - case 'null': - case 'NULL': - return new ConstantExpression(null, $token->getLine()); - - default: - return new ContextVariable($token->getValue(), $token->getLine()); - } - - // no break - case $token->test(Token::NUMBER_TYPE): - $this->parser->getStream()->next(); - - return new ConstantExpression($token->getValue(), $token->getLine()); - - case $token->test(Token::STRING_TYPE): - case $token->test(Token::INTERPOLATION_START_TYPE): - return $this->parseStringExpression(); - - case $token->test(Token::PUNCTUATION_TYPE): - // In 4.0, we should always return the node or throw an error for default - if ($node = match ($token->getValue()) { - '{' => $this->parseMappingExpression(), - default => null, - }) { - return $node; - } - - // no break - case $token->test(Token::OPERATOR_TYPE): - if ('[' === $token->getValue()) { - return $this->parseSequenceExpression(); - } - - if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { - // in this context, string operators are variable names - $this->parser->getStream()->next(); - - return new ContextVariable($token->getValue(), $token->getLine()); - } - - if ('=' === $token->getValue() && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) { - throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); - } + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - // no break - default: - throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); - } + return $this->parseExpression(); } + /** + * @deprecated since Twig 3.20 + */ public function parseStringExpression() { - $stream = $this->parser->getStream(); - - $nodes = []; - // a string cannot be followed by another string in a single expression - $nextCanBeString = true; - while (true) { - if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) { - $nodes[] = new ConstantExpression($token->getValue(), $token->getLine()); - $nextCanBeString = false; - } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) { - $nodes[] = $this->parseExpression(); - $stream->expect(Token::INTERPOLATION_END_TYPE); - $nextCanBeString = true; - } else { - break; - } - } - - $expr = array_shift($nodes); - foreach ($nodes as $node) { - $expr = new ConcatBinary($expr, $node, $node->getTemplateLine()); - } + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - return $expr; + return $this->parseExpression(); } /** - * @deprecated since Twig 3.11, use parseSequenceExpression() instead + * @deprecated since Twig 3.11, use parseExpression() instead */ public function parseArrayExpression() { - trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__); + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); - return $this->parseSequenceExpression(); + return $this->parseExpression(); } + /** + * @deprecated since Twig 3.20 + */ public function parseSequenceExpression() { - $stream = $this->parser->getStream(); - $stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected'); - - $node = new ArrayExpression([], $stream->getCurrent()->getLine()); - $first = true; - while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) { - if (!$first) { - $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma'); - - // trailing ,? - if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { - break; - } - } - $first = false; - - if ($stream->nextIf(Token::SPREAD_TYPE)) { - $expr = $this->parseExpression(); - $expr->setAttribute('spread', true); - $node->addElement($expr); - } else { - $node->addElement($this->parseExpression()); - } - } - $stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed'); + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - return $node; + return $this->parseExpression(); } /** - * @deprecated since Twig 3.11, use parseMappingExpression() instead + * @deprecated since Twig 3.11, use parseExpression() instead */ public function parseHashExpression() { - trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.', __METHOD__); + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); - return $this->parseMappingExpression(); + return $this->parseExpression(); } + /** + * @deprecated since Twig 3.20 + */ public function parseMappingExpression() { - $stream = $this->parser->getStream(); - $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); - - $node = new ArrayExpression([], $stream->getCurrent()->getLine()); - $first = true; - while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { - if (!$first) { - $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma'); - - // trailing ,? - if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { - break; - } - } - $first = false; - - if ($stream->nextIf(Token::SPREAD_TYPE)) { - $value = $this->parseExpression(); - $value->setAttribute('spread', true); - $node->addElement($value); - continue; - } - - // a mapping key can be: - // - // * a number -- 12 - // * a string -- 'a' - // * a name, which is equivalent to a string -- a - // * an expression, which must be enclosed in parentheses -- (1 + 2) - if ($token = $stream->nextIf(Token::NAME_TYPE)) { - $key = new ConstantExpression($token->getValue(), $token->getLine()); - - // {a} is a shortcut for {a:a} - if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) { - $value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine()); - $node->addElement($value, $key); - continue; - } - } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) { - $key = new ConstantExpression($token->getValue(), $token->getLine()); - } elseif ($stream->test(Token::OPERATOR_TYPE, '(')) { - $key = $this->parseExpression(); - } else { - $current = $stream->getCurrent(); - - throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->toEnglish(), $current->getValue()), $current->getLine(), $stream->getSourceContext()); - } - - $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)'); - $value = $this->parseExpression(); - - $node->addElement($value, $key); - } - $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - return $node; + return $this->parseExpression(); } /** @@ -414,11 +159,13 @@ public function parseSubscriptExpression($node) { trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + $parsers = new \ReflectionProperty($this->parser, 'parsers'); + if ('.' === $this->parser->getStream()->next()->getValue()) { - return $this->operators->getBinary('.')->parse($this, $node, $this->parser->getCurrentToken()); + return $parsers->getValue($this->parser)->getInfixByClass(DotExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); } - return $this->operators->getBinary('[')->parse($this, $node, $this->parser->getCurrentToken()); + return $parsers->getValue($this->parser)->getInfixByClass(SquareBracketExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); } /** @@ -440,9 +187,11 @@ public function parseFilterExpressionRaw($node) { trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - $op = $this->operators->getBinary('|'); + $parsers = new \ReflectionProperty($this->parser, 'parsers'); + + $op = $parsers->getValue($this->parser)->getInfixByClass(FilterExpressionParser::class); while (true) { - $node = $op->parse($this, $node, $this->parser->getCurrentToken()); + $node = $op->parse($this->parser, $node, $this->parser->getCurrentToken()); if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { break; } @@ -459,11 +208,13 @@ public function parseFilterExpressionRaw($node) * * @throws SyntaxError * - * @deprecated since Twig 3.19 Use parseNamedArguments() instead + * @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead */ public function parseArguments() { - trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "%s::parseNamedArguments()" instead.', __METHOD__, __CLASS__)); + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__)); + + $parsePrimary = new \ReflectionMethod($this->parser, 'parsePrimary'); $namedArguments = false; $definition = false; @@ -512,7 +263,7 @@ public function parseArguments() $name = $value->getAttribute('name'); if ($definition) { - $value = $this->parsePrimary(); + $value = $parsePrimary->invoke($this->parser); if (!$this->checkConstantExpression($value)) { throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); @@ -587,21 +338,6 @@ public function parseMultitargetExpression() return new Nodes($targets); } - public function getTest(int $line): TwigTest - { - return $this->parser->getTest($line); - } - - public function getFunction(string $name, int $line): TwigFunction - { - return $this->parser->getFunction($name, $line); - } - - public function getFilter(string $name, int $line): TwigFilter - { - return $this->parser->getFilter($name, $line); - } - // checks that the node only contains "constant" elements // to be removed in 4.0 private function checkConstantExpression(Node $node): bool @@ -621,81 +357,13 @@ private function checkConstantExpression(Node $node): bool return true; } - private function setDeprecationCheck(bool $deprecationCheck): bool - { - $current = $this->deprecationCheck; - $this->deprecationCheck = $deprecationCheck; - - return $current; - } - /** - * @internal - */ - public function parseCallableArguments(int $line, bool $parseOpenParenthesis = true): ArrayExpression - { - $arguments = new ArrayExpression([], $line); - foreach ($this->parseNamedArguments($parseOpenParenthesis) as $k => $n) { - $arguments->addElement($n, new LocalVariable($k, $line)); - } - - return $arguments; - } - - /** - * @deprecated since Twig 3.19 Use parseNamedArguments() instead + * @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead */ public function parseOnlyArguments() { - trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "%s::parseNamedArguments()" instead.', __METHOD__, __CLASS__)); - - return $this->parseNamedArguments(); - } - - public function parseNamedArguments(bool $parseOpenParenthesis = true): Nodes - { - $args = []; - $stream = $this->parser->getStream(); - if ($parseOpenParenthesis) { - $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); - } - $hasSpread = false; - while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { - if ($args) { - $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); - - // if the comma above was a trailing comma, early exit the argument parse loop - if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { - break; - } - } + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__)); - if ($stream->nextIf(Token::SPREAD_TYPE)) { - $hasSpread = true; - $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine()); - } elseif ($hasSpread) { - throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); - } else { - $value = $this->parseExpression(); - } - - $name = null; - if (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) { - if (!$value instanceof ContextVariable) { - throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); - } - $name = $value->getAttribute('name'); - $value = $this->parseExpression(); - } - - if (null === $name) { - $args[] = $value; - } else { - $args[$name] = $value; - } - } - $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); - - return new Nodes($args); + return $this->parseArguments(); } } diff --git a/src/Operator/AbstractOperator.php b/src/ExpressionParser/AbstractExpressionParser.php similarity index 57% rename from src/Operator/AbstractOperator.php rename to src/ExpressionParser/AbstractExpressionParser.php index c18904f35ae..bc05bfa051e 100644 --- a/src/Operator/AbstractOperator.php +++ b/src/ExpressionParser/AbstractExpressionParser.php @@ -9,18 +9,16 @@ * file that was distributed with this source code. */ -namespace Twig\Operator; +namespace Twig\ExpressionParser; -use Twig\OperatorPrecedenceChange; - -abstract class AbstractOperator implements OperatorInterface +abstract class AbstractExpressionParser implements ExpressionParserInterface { public function __toString(): string { - return \sprintf('%s(%s)', $this->getArity()->value, $this->getOperator()); + return \sprintf('%s(%s)', ExpressionParserType::getType($this)->value, $this->getName()); } - public function getPrecedenceChange(): ?OperatorPrecedenceChange + public function getPrecedenceChange(): ?PrecedenceChange { return null; } diff --git a/src/Operator/OperatorInterface.php b/src/ExpressionParser/ExpressionParserInterface.php similarity index 60% rename from src/Operator/OperatorInterface.php rename to src/ExpressionParser/ExpressionParserInterface.php index 8512bdeade6..86576aec49a 100644 --- a/src/Operator/OperatorInterface.php +++ b/src/ExpressionParser/ExpressionParserInterface.php @@ -9,21 +9,17 @@ * file that was distributed with this source code. */ -namespace Twig\Operator; +namespace Twig\ExpressionParser; -use Twig\OperatorPrecedenceChange; - -interface OperatorInterface +interface ExpressionParserInterface { public function __toString(): string; - public function getOperator(): string; - - public function getArity(): OperatorArity; + public function getName(): string; public function getPrecedence(): int; - public function getPrecedenceChange(): ?OperatorPrecedenceChange; + public function getPrecedenceChange(): ?PrecedenceChange; /** * @return array diff --git a/src/ExpressionParser/ExpressionParserType.php b/src/ExpressionParser/ExpressionParserType.php new file mode 100644 index 00000000000..0a980a8ec40 --- /dev/null +++ b/src/ExpressionParser/ExpressionParserType.php @@ -0,0 +1,33 @@ + + * + * @internal + */ +final class ExpressionParsers implements \IteratorAggregate +{ + /** + * @var array, array> + */ + private array $parsers = []; + + /** + * @var array, array, ExpressionParserInterface>> + */ + private array $parsersByClass = []; + + /** + * @var array, array> + */ + private array $aliases = []; + + /** + * @var \WeakMap>|null + */ + private ?\WeakMap $precedenceChanges = null; + + /** + * @param array $parsers + */ + public function __construct( + array $parsers = [], + ) { + $this->precedenceChanges = null; + $this->add($parsers); + } + + /** + * @param array $parsers + * + * @return $this + */ + 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; + } + } + + return $this; + } + + /** + * @param class-string $name + */ + public function getPrefixByClass(string $name): ?PrefixExpressionParserInterface + { + return $this->parsersByClass[ExpressionParserType::Prefix->value][$name] ?? null; + } + + public function getPrefix(string $name): ?PrefixExpressionParserInterface + { + return + $this->parsers[ExpressionParserType::Prefix->value][$name] + ?? $this->aliases[ExpressionParserType::Prefix->value][$name] + ?? null + ; + } + + /** + * @param class-string $name + */ + public function getInfixByClass(string $name): ?InfixExpressionParserInterface + { + return $this->parsersByClass[ExpressionParserType::Infix->value][$name] ?? null; + } + + public function getInfix(string $name): ?InfixExpressionParserInterface + { + return + $this->parsers[ExpressionParserType::Infix->value][$name] + ?? $this->aliases[ExpressionParserType::Infix->value][$name] + ?? null + ; + } + + public function getIterator(): \Traversable + { + foreach ($this->parsers as $parsers) { + // we don't yield the keys + yield from $parsers; + } + } + + /** + * @internal + * + * @return \WeakMap> + */ + public function getPrecedenceChanges(): \WeakMap + { + if (null === $this->precedenceChanges) { + $this->precedenceChanges = new \WeakMap(); + foreach ($this as $ep) { + if (!$ep->getPrecedenceChange()) { + continue; + } + $min = min($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence()); + $max = max($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence()); + foreach ($this as $e) { + if ($e->getPrecedence() > $min && $e->getPrecedence() < $max) { + if (!isset($this->precedenceChanges[$e])) { + $this->precedenceChanges[$e] = []; + } + $this->precedenceChanges[$e][] = $ep; + } + } + } + } + + return $this->precedenceChanges; + } +} diff --git a/src/ExpressionParser/Infix/ArgumentsTrait.php b/src/ExpressionParser/Infix/ArgumentsTrait.php new file mode 100644 index 00000000000..b60a8481053 --- /dev/null +++ b/src/ExpressionParser/Infix/ArgumentsTrait.php @@ -0,0 +1,81 @@ +parseNamedArguments($parser, $parseOpenParenthesis) as $k => $n) { + $arguments->addElement($n, new LocalVariable($k, $line)); + } + + return $arguments; + } + + private function parseNamedArguments(Parser $parser, bool $parseOpenParenthesis = true): Nodes + { + $args = []; + $stream = $parser->getStream(); + if ($parseOpenParenthesis) { + $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + } + $hasSpread = false; + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { + if ($args) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + + // if the comma above was a trailing comma, early exit the argument parse loop + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { + break; + } + } + + if ($stream->nextIf(Token::SPREAD_TYPE)) { + $hasSpread = true; + $value = new SpreadUnary($parser->parseExpression(), $stream->getCurrent()->getLine()); + } elseif ($hasSpread) { + throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } else { + $value = $parser->parseExpression(); + } + + $name = null; + if (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) { + if (!$value instanceof ContextVariable) { + throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext()); + } + $name = $value->getAttribute('name'); + $value = $parser->parseExpression(); + } + + if (null === $name) { + $args[] = $value; + } else { + $args[$name] = $value; + } + } + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return new Nodes($args); + } +} diff --git a/src/Operator/Binary/ArrowBinaryOperator.php b/src/ExpressionParser/Infix/ArrowExpressionParser.php similarity index 52% rename from src/Operator/Binary/ArrowBinaryOperator.php rename to src/ExpressionParser/Infix/ArrowExpressionParser.php index 253b00fc78c..698497b0e8d 100644 --- a/src/Operator/Binary/ArrowBinaryOperator.php +++ b/src/ExpressionParser/Infix/ArrowExpressionParser.php @@ -9,25 +9,28 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; +namespace Twig\ExpressionParser\Infix; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrowFunctionExpression; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Token; -class ArrowBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +final class ArrowExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { // As the expression of the arrow function is independent from the current precedence, we want a precedence of 0 return new ArrowFunctionExpression($parser->parseExpression(), $expr, $token->getLine()); } - public function getOperator(): string + public function getName(): string { return '=>'; } @@ -37,13 +40,8 @@ public function getPrecedence(): int return 250; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php b/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php new file mode 100644 index 00000000000..ce650b424ea --- /dev/null +++ b/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php @@ -0,0 +1,73 @@ + */ + private string $nodeClass, + private string $name, + private int $precedence, + private InfixAssociativity $associativity = InfixAssociativity::Left, + private ?PrecedenceChange $precedenceChange = null, + private array $aliases = [], + ) { + } + + /** + * @return AbstractBinary + */ + public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression + { + $right = $parser->parseExpression(InfixAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence()); + + return new ($this->nodeClass)($left, $right, $token->getLine()); + } + + public function getAssociativity(): InfixAssociativity + { + return $this->associativity; + } + + public function getName(): string + { + return $this->name; + } + + public function getPrecedence(): int + { + return $this->precedence; + } + + public function getPrecedenceChange(): ?PrecedenceChange + { + return $this->precedenceChange; + } + + public function getAliases(): array + { + return $this->aliases; + } +} diff --git a/src/Operator/Ternary/ConditionalTernaryOperator.php b/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php similarity index 61% rename from src/Operator/Ternary/ConditionalTernaryOperator.php rename to src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php index 7c20963db57..2bb5fc92c79 100644 --- a/src/Operator/Ternary/ConditionalTernaryOperator.php +++ b/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php @@ -9,20 +9,26 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Ternary; +namespace Twig\ExpressionParser\Infix; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\Ternary\ConditionalTernary; +use Twig\Parser; use Twig\Token; -class ConditionalTernaryOperator extends AbstractTernaryOperator +/** + * @internal + */ +final class ConditionalTernaryExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { - public function parse(ExpressionParser $parser, AbstractExpression $left, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression { $then = $parser->parseExpression($this->getPrecedence()); - if ($parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, $this->getElseOperator())) { + if ($parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { // Ternary operator (expr ? expr2 : expr3) $else = $parser->parseExpression($this->getPrecedence()); } else { @@ -33,7 +39,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $left, Token return new ConditionalTernary($left, $then, $else, $token->getLine()); } - public function getOperator(): string + public function getName(): string { return '?'; } @@ -43,8 +49,8 @@ public function getPrecedence(): int return 0; } - private function getElseOperator(): string + public function getAssociativity(): InfixAssociativity { - return ':'; + return InfixAssociativity::Left; } } diff --git a/src/Operator/Binary/DotBinaryOperator.php b/src/ExpressionParser/Infix/DotExpressionParser.php similarity index 78% rename from src/Operator/Binary/DotBinaryOperator.php rename to src/ExpressionParser/Infix/DotExpressionParser.php index 2aecf0b10cc..d83f4bfbbfb 100644 --- a/src/Operator/Binary/DotBinaryOperator.php +++ b/src/ExpressionParser/Infix/DotExpressionParser.php @@ -9,10 +9,12 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; +namespace Twig\ExpressionParser\Infix; use Twig\Error\SyntaxError; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Lexer; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; @@ -21,15 +23,18 @@ use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Expression\Variable\TemplateVariable; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Template; use Twig\Token; -class DotBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +final class DotExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + use ArgumentsTrait; + + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { $stream = $parser->getStream(); $token = $stream->getCurrent(); @@ -55,7 +60,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token if ($stream->test(Token::OPERATOR_TYPE, '(')) { $type = Template::METHOD_CALL; - $arguments = $parser->parseCallableArguments($token->getLine()); + $arguments = $this->parseCallableArguments($parser, $token->getLine()); } if ( @@ -71,7 +76,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno); } - public function getOperator(): string + public function getName(): string { return '.'; } @@ -81,13 +86,8 @@ public function getPrecedence(): int return 300; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/Operator/Binary/FilterBinaryOperator.php b/src/ExpressionParser/Infix/FilterExpressionParser.php similarity index 70% rename from src/Operator/Binary/FilterBinaryOperator.php rename to src/ExpressionParser/Infix/FilterExpressionParser.php index 9dc6333898f..98e4b3b3a90 100644 --- a/src/Operator/Binary/FilterBinaryOperator.php +++ b/src/ExpressionParser/Infix/FilterExpressionParser.php @@ -9,23 +9,28 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; +namespace Twig\ExpressionParser\Infix; use Twig\Attribute\FirstClassTwigCallableReady; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ConstantExpression; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Token; -class FilterBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +final class FilterExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { + use ArgumentsTrait; + private $readyNodes = []; - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { $stream = $parser->getStream(); $token = $stream->expect(Token::NAME_TYPE); @@ -34,7 +39,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token if (!$stream->test(Token::OPERATOR_TYPE, '(')) { $arguments = new EmptyNode(); } else { - $arguments = $parser->parseNamedArguments(); + $arguments = $this->parseNamedArguments($parser); } $filter = $parser->getFilter($token->getValue(), $line); @@ -51,7 +56,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token return new $class($expr, $ready ? $filter : new ConstantExpression($filter->getName(), $line), $arguments, $line); } - public function getOperator(): string + public function getName(): string { return '|'; } @@ -61,13 +66,8 @@ public function getPrecedence(): int return 300; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/Operator/Binary/FunctionBinaryOperator.php b/src/ExpressionParser/Infix/FunctionExpressionParser.php similarity index 69% rename from src/Operator/Binary/FunctionBinaryOperator.php rename to src/ExpressionParser/Infix/FunctionExpressionParser.php index 740c1ce5948..b1d627f7b9b 100644 --- a/src/Operator/Binary/FunctionBinaryOperator.php +++ b/src/ExpressionParser/Infix/FunctionExpressionParser.php @@ -9,25 +9,30 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; +namespace Twig\ExpressionParser\Infix; use Twig\Attribute\FirstClassTwigCallableReady; use Twig\Error\SyntaxError; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\EmptyNode; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\NameExpression; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Token; -class FunctionBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +final class FunctionExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { + use ArgumentsTrait; + private $readyNodes = []; - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { $line = $token->getLine(); if (!$expr instanceof NameExpression) { @@ -37,10 +42,10 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $name = $expr->getAttribute('name'); if (null !== $alias = $parser->getImportedSymbol('function', $name)) { - return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $parser->parseCallableArguments($line, false), $line); + return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $this->parseCallableArguments($parser, $line, false), $line); } - $args = $parser->parseNamedArguments(false); + $args = $this->parseNamedArguments($parser, false); $function = $parser->getFunction($name, $line); @@ -48,7 +53,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $fakeNode = new EmptyNode($line); $fakeNode->setSourceContext($parser->getStream()->getSourceContext()); - return ($function->getParserCallable())($parser->getParser(), $fakeNode, $args, $line); + return ($function->getParserCallable())($parser, $fakeNode, $args, $line); } if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { @@ -62,7 +67,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token return new $class($ready ? $function : $function->getName(), $args, $line); } - public function getOperator(): string + public function getName(): string { return '('; } @@ -72,13 +77,8 @@ public function getPrecedence(): int return 300; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/Operator/Binary/IsBinaryOperator.php b/src/ExpressionParser/Infix/IsExpressionParser.php similarity index 75% rename from src/Operator/Binary/IsBinaryOperator.php rename to src/ExpressionParser/Infix/IsExpressionParser.php index 4236b769a44..d63b495e24e 100644 --- a/src/Operator/Binary/IsBinaryOperator.php +++ b/src/ExpressionParser/Infix/IsExpressionParser.php @@ -9,33 +9,38 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; + namespace Twig\ExpressionParser\Infix; use Twig\Attribute\FirstClassTwigCallableReady; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\MacroReferenceExpression; use Twig\Node\Expression\NameExpression; use Twig\Node\Nodes; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Token; use Twig\TwigTest; -class IsBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +class IsExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { + use ArgumentsTrait; + private $readyNodes = []; - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { $stream = $parser->getStream(); $test = $parser->getTest($token->getLine()); $arguments = null; if ($stream->test(Token::OPERATOR_TYPE, '(')) { - $arguments = $parser->parseNamedArguments(); + $arguments = $this->parseNamedArguments($parser); } elseif ($test->hasOneMandatoryArgument()) { $arguments = new Nodes([0 => $parser->parseExpression($this->getPrecedence())]); } @@ -61,18 +66,13 @@ public function getPrecedence(): int return 100; } - public function getOperator(): string + public function getName(): string { return 'is'; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/Operator/Binary/IsNotBinaryOperator.php b/src/ExpressionParser/Infix/IsNotExpressionParser.php similarity index 61% rename from src/Operator/Binary/IsNotBinaryOperator.php rename to src/ExpressionParser/Infix/IsNotExpressionParser.php index 2455f738714..55c0844ced7 100644 --- a/src/Operator/Binary/IsNotBinaryOperator.php +++ b/src/ExpressionParser/Infix/IsNotExpressionParser.php @@ -9,21 +9,24 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; + namespace Twig\ExpressionParser\Infix; -use Twig\ExpressionParser; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\Unary\NotUnary; +use Twig\Parser; use Twig\Token; -class IsNotBinaryOperator extends IsBinaryOperator +/** + * @internal + */ +final class IsNotExpressionParser extends IsExpressionParser { - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { return new NotUnary(parent::parse($parser, $expr, $token), $token->getLine()); } - public function getOperator(): string + public function getName(): string { return 'is not'; } diff --git a/src/Operator/Binary/SquareBracketBinaryOperator.php b/src/ExpressionParser/Infix/SquareBracketExpressionParser.php similarity index 74% rename from src/Operator/Binary/SquareBracketBinaryOperator.php rename to src/ExpressionParser/Infix/SquareBracketExpressionParser.php index c2e8b500652..1037dcb8193 100644 --- a/src/Operator/Binary/SquareBracketBinaryOperator.php +++ b/src/ExpressionParser/Infix/SquareBracketExpressionParser.php @@ -9,23 +9,26 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Binary; + namespace Twig\ExpressionParser\Infix; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Nodes; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; -use Twig\Operator\OperatorAssociativity; +use Twig\Parser; use Twig\Template; use Twig\Token; -class SquareBracketBinaryOperator extends AbstractOperator implements BinaryOperatorInterface +/** + * @internal + */ +final class SquareBracketExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface { - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { $stream = $parser->getStream(); $lineno = $token->getLine(); @@ -65,7 +68,7 @@ public function parse(ExpressionParser $parser, AbstractExpression $expr, Token return new GetAttrExpression($expr, $attribute, $arguments, Template::ARRAY_CALL, $lineno); } - public function getOperator(): string + public function getName(): string { return '['; } @@ -75,13 +78,8 @@ public function getPrecedence(): int return 300; } - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity + public function getAssociativity(): InfixAssociativity { - return OperatorAssociativity::Left; + return InfixAssociativity::Left; } } diff --git a/src/Operator/OperatorAssociativity.php b/src/ExpressionParser/InfixAssociativity.php similarity index 80% rename from src/Operator/OperatorAssociativity.php rename to src/ExpressionParser/InfixAssociativity.php index 638cdda1576..3aeccce4565 100644 --- a/src/Operator/OperatorAssociativity.php +++ b/src/ExpressionParser/InfixAssociativity.php @@ -9,9 +9,9 @@ * file that was distributed with this source code. */ -namespace Twig\Operator; +namespace Twig\ExpressionParser; -enum OperatorAssociativity +enum InfixAssociativity { case Left; case Right; diff --git a/src/ExpressionParser/InfixExpressionParserInterface.php b/src/ExpressionParser/InfixExpressionParserInterface.php new file mode 100644 index 00000000000..8d0ac674c83 --- /dev/null +++ b/src/ExpressionParser/InfixExpressionParserInterface.php @@ -0,0 +1,23 @@ + + */ +class PrecedenceChange +{ + 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(): int + { + return $this->newPrecedence; + } +} diff --git a/src/Operator/Unary/ParenthesisUnaryOperator.php b/src/ExpressionParser/Prefix/GroupingExpressionParser.php similarity index 80% rename from src/Operator/Unary/ParenthesisUnaryOperator.php rename to src/ExpressionParser/Prefix/GroupingExpressionParser.php index 8191cb82045..ac9f6c9dbe3 100644 --- a/src/Operator/Unary/ParenthesisUnaryOperator.php +++ b/src/ExpressionParser/Prefix/GroupingExpressionParser.php @@ -9,20 +9,23 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Unary; +namespace Twig\ExpressionParser\Prefix; use Twig\Error\SyntaxError; -use Twig\ExpressionParser; +use Twig\ExpressionParser\AbstractExpressionParser; +use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Node\Expression\AbstractExpression; use Twig\Node\Expression\ListExpression; use Twig\Node\Expression\Variable\ContextVariable; -use Twig\Operator\AbstractOperator; -use Twig\Operator\OperatorArity; +use Twig\Parser; use Twig\Token; -class ParenthesisUnaryOperator extends AbstractOperator implements UnaryOperatorInterface +/** + * @internal + */ +final class GroupingExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface { - public function parse(ExpressionParser $parser, Token $token): AbstractExpression + public function parse(Parser $parser, Token $token): AbstractExpression { $stream = $parser->getStream(); $expr = $parser->parseExpression($this->getPrecedence()); @@ -57,7 +60,7 @@ public function parse(ExpressionParser $parser, Token $token): AbstractExpressio return new ListExpression($names, $token->getLine()); } - public function getOperator(): string + public function getName(): string { return '('; } @@ -66,9 +69,4 @@ public function getPrecedence(): int { return 0; } - - public function getArity(): OperatorArity - { - return OperatorArity::Unary; - } } diff --git a/src/ExpressionParser/Prefix/LiteralExpressionParser.php b/src/ExpressionParser/Prefix/LiteralExpressionParser.php new file mode 100644 index 00000000000..92540de75fd --- /dev/null +++ b/src/ExpressionParser/Prefix/LiteralExpressionParser.php @@ -0,0 +1,243 @@ +getStream(); + switch (true) { + case $token->test(Token::NAME_TYPE): + $stream->next(); + switch ($token->getValue()) { + case 'true': + case 'TRUE': + $this->type = 'constant'; + return new ConstantExpression(true, $token->getLine()); + + case 'false': + case 'FALSE': + $this->type = 'constant'; + return new ConstantExpression(false, $token->getLine()); + + case 'none': + case 'NONE': + case 'null': + case 'NULL': + $this->type = 'constant'; + return new ConstantExpression(null, $token->getLine()); + + default: + $this->type = 'variable'; + return new ContextVariable($token->getValue(), $token->getLine()); + } + + // no break + case $token->test(Token::NUMBER_TYPE): + $stream->next(); + $this->type = 'constant'; + + return new ConstantExpression($token->getValue(), $token->getLine()); + + case $token->test(Token::STRING_TYPE): + case $token->test(Token::INTERPOLATION_START_TYPE): + $this->type = 'string'; + + return $this->parseStringExpression($parser); + + case $token->test(Token::PUNCTUATION_TYPE): + // In 4.0, we should always return the node or throw an error for default + if ($node = match ($token->getValue()) { + '{' => $this->parseMappingExpression($parser), + default => null, + }) { + return $node; + } + + // no break + case $token->test(Token::OPERATOR_TYPE): + if ('[' === $token->getValue()) { + return $this->parseSequenceExpression($parser); + } + + if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { + // in this context, string operators are variable names + $stream->next(); + $this->type = 'variable'; + + return new ContextVariable($token->getValue(), $token->getLine()); + } + + if ('=' === $token->getValue() && ('==' === $stream->look(-1)->getValue() || '!=' === $stream->look(-1)->getValue())) { + throw new SyntaxError(\sprintf('Unexpected operator of value "%s". Did you try to use "===" or "!==" for strict comparison? Use "is same as(value)" instead.', $token->getValue()), $token->getLine(), $stream->getSourceContext()); + } + + // no break + default: + throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $stream->getSourceContext()); + } + } + + public function getName(): string + { + return $this->type; + } + + public function getPrecedence(): int + { + // not used + return 0; + } + + private function parseStringExpression(Parser $parser) + { + $stream = $parser->getStream(); + + $nodes = []; + // a string cannot be followed by another string in a single expression + $nextCanBeString = true; + while (true) { + if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) { + $nodes[] = new ConstantExpression($token->getValue(), $token->getLine()); + $nextCanBeString = false; + } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) { + $nodes[] = $parser->parseExpression(); + $stream->expect(Token::INTERPOLATION_END_TYPE); + $nextCanBeString = true; + } else { + break; + } + } + + $expr = array_shift($nodes); + foreach ($nodes as $node) { + $expr = new ConcatBinary($expr, $node, $node->getTemplateLine()); + } + + return $expr; + } + + private function parseSequenceExpression(Parser $parser) + { + $this->type = 'sequence'; + + $stream = $parser->getStream(); + $stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected'); + + $node = new ArrayExpression([], $stream->getCurrent()->getLine()); + $first = true; + while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { + break; + } + } + $first = false; + + if ($stream->nextIf(Token::SPREAD_TYPE)) { + $expr = $parser->parseExpression(); + $expr->setAttribute('spread', true); + $node->addElement($expr); + } else { + $node->addElement($parser->parseExpression()); + } + } + $stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed'); + + return $node; + } + + private function parseMappingExpression(Parser $parser) + { + $this->type = 'mapping'; + + $stream = $parser->getStream(); + $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); + + $node = new ArrayExpression([], $stream->getCurrent()->getLine()); + $first = true; + while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { + break; + } + } + $first = false; + + if ($stream->nextIf(Token::SPREAD_TYPE)) { + $value = $parser->parseExpression(); + $value->setAttribute('spread', true); + $node->addElement($value); + continue; + } + + // a mapping key can be: + // + // * a number -- 12 + // * a string -- 'a' + // * a name, which is equivalent to a string -- a + // * an expression, which must be enclosed in parentheses -- (1 + 2) + if ($token = $stream->nextIf(Token::NAME_TYPE)) { + $key = new ConstantExpression($token->getValue(), $token->getLine()); + + // {a} is a shortcut for {a:a} + if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) { + $value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine()); + $node->addElement($value, $key); + continue; + } + } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) { + $key = new ConstantExpression($token->getValue(), $token->getLine()); + } elseif ($stream->test(Token::OPERATOR_TYPE, '(')) { + $key = $parser->parseExpression(); + } else { + $current = $stream->getCurrent(); + + throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->toEnglish(), $current->getValue()), $current->getLine(), $stream->getSourceContext()); + } + + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)'); + $value = $parser->parseExpression(); + + $node->addElement($value, $key); + } + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + + return $node; + } +} diff --git a/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php b/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php new file mode 100644 index 00000000000..4357d4ff6ab --- /dev/null +++ b/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php @@ -0,0 +1,64 @@ + */ + private string $nodeClass, + private string $name, + private int $precedence, + private ?PrecedenceChange $precedenceChange = null, + private array $aliases = [], + ) { + } + + /** + * @return AbstractUnary + */ + public function parse(Parser $parser, Token $token): AbstractExpression + { + return new ($this->nodeClass)($parser->parseExpression($this->precedence), $token->getLine()); + } + + public function getName(): string + { + return $this->name; + } + + public function getPrecedence(): int + { + return $this->precedence; + } + + public function getPrecedenceChange(): ?PrecedenceChange + { + return $this->precedenceChange; + } + + public function getAliases(): array + { + return $this->aliases; + } +} diff --git a/src/Operator/Unary/UnaryOperatorInterface.php b/src/ExpressionParser/PrefixExpressionParserInterface.php similarity index 52% rename from src/Operator/Unary/UnaryOperatorInterface.php rename to src/ExpressionParser/PrefixExpressionParserInterface.php index 71c599acf65..587997c51a2 100644 --- a/src/Operator/Unary/UnaryOperatorInterface.php +++ b/src/ExpressionParser/PrefixExpressionParserInterface.php @@ -9,14 +9,13 @@ * file that was distributed with this source code. */ -namespace Twig\Operator\Unary; +namespace Twig\ExpressionParser; -use Twig\ExpressionParser; use Twig\Node\Expression\AbstractExpression; -use Twig\Operator\OperatorInterface; +use Twig\Parser; use Twig\Token; -interface UnaryOperatorInterface extends OperatorInterface +interface PrefixExpressionParserInterface extends ExpressionParserInterface { - public function parse(ExpressionParser $parser, Token $token): AbstractExpression; + public function parse(Parser $parser, Token $token): AbstractExpression; } diff --git a/src/Extension/AbstractExtension.php b/src/Extension/AbstractExtension.php index 02767f7c37c..351fb0698df 100644 --- a/src/Extension/AbstractExtension.php +++ b/src/Extension/AbstractExtension.php @@ -39,6 +39,11 @@ public function getFunctions() } public function getOperators() + { + return [[], []]; + } + + public function getExpressionParsers(): array { return []; } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 074a965f749..2b6a9f05878 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -16,8 +16,53 @@ use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; +use Twig\ExpressionParser\Infix\ArrowExpressionParser; +use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser; +use Twig\ExpressionParser\Infix\ConditionalTernaryExpressionParser; +use Twig\ExpressionParser\Infix\DotExpressionParser; +use Twig\ExpressionParser\Infix\FilterExpressionParser; +use Twig\ExpressionParser\Infix\FunctionExpressionParser; +use Twig\ExpressionParser\Infix\IsExpressionParser; +use Twig\ExpressionParser\Infix\IsNotExpressionParser; +use Twig\ExpressionParser\Infix\SquareBracketExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\PrecedenceChange; +use Twig\ExpressionParser\Prefix\GroupingExpressionParser; +use Twig\ExpressionParser\Prefix\LiteralExpressionParser; +use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser; use Twig\Markup; use Twig\Node\Expression\AbstractExpression; +use Twig\Node\Expression\Binary\AddBinary; +use Twig\Node\Expression\Binary\AndBinary; +use Twig\Node\Expression\Binary\BitwiseAndBinary; +use Twig\Node\Expression\Binary\BitwiseOrBinary; +use Twig\Node\Expression\Binary\BitwiseXorBinary; +use Twig\Node\Expression\Binary\ConcatBinary; +use Twig\Node\Expression\Binary\DivBinary; +use Twig\Node\Expression\Binary\ElvisBinary; +use Twig\Node\Expression\Binary\EndsWithBinary; +use Twig\Node\Expression\Binary\EqualBinary; +use Twig\Node\Expression\Binary\FloorDivBinary; +use Twig\Node\Expression\Binary\GreaterBinary; +use Twig\Node\Expression\Binary\GreaterEqualBinary; +use Twig\Node\Expression\Binary\HasEveryBinary; +use Twig\Node\Expression\Binary\HasSomeBinary; +use Twig\Node\Expression\Binary\InBinary; +use Twig\Node\Expression\Binary\LessBinary; +use Twig\Node\Expression\Binary\LessEqualBinary; +use Twig\Node\Expression\Binary\MatchesBinary; +use Twig\Node\Expression\Binary\ModBinary; +use Twig\Node\Expression\Binary\MulBinary; +use Twig\Node\Expression\Binary\NotEqualBinary; +use Twig\Node\Expression\Binary\NotInBinary; +use Twig\Node\Expression\Binary\NullCoalesceBinary; +use Twig\Node\Expression\Binary\OrBinary; +use Twig\Node\Expression\Binary\PowerBinary; +use Twig\Node\Expression\Binary\RangeBinary; +use Twig\Node\Expression\Binary\SpaceshipBinary; +use Twig\Node\Expression\Binary\StartsWithBinary; +use Twig\Node\Expression\Binary\SubBinary; +use Twig\Node\Expression\Binary\XorBinary; use Twig\Node\Expression\BlockReferenceExpression; use Twig\Node\Expression\Filter\DefaultFilter; use Twig\Node\Expression\FunctionNode\EnumCasesFunction; @@ -31,50 +76,10 @@ use Twig\Node\Expression\Test\NullTest; use Twig\Node\Expression\Test\OddTest; use Twig\Node\Expression\Test\SameasTest; +use Twig\Node\Expression\Unary\NegUnary; +use Twig\Node\Expression\Unary\NotUnary; +use Twig\Node\Expression\Unary\PosUnary; use Twig\Node\Node; -use Twig\Operator\Binary\AddBinaryOperator; -use Twig\Operator\Binary\AndBinaryOperator; -use Twig\Operator\Binary\ArrowBinaryOperator; -use Twig\Operator\Binary\BitwiseAndBinaryOperator; -use Twig\Operator\Binary\BitwiseOrBinaryOperator; -use Twig\Operator\Binary\BitwiseXorBinaryOperator; -use Twig\Operator\Binary\ConcatBinaryOperator; -use Twig\Operator\Binary\DivBinaryOperator; -use Twig\Operator\Binary\DotBinaryOperator; -use Twig\Operator\Binary\ElvisBinaryOperator; -use Twig\Operator\Binary\EndsWithBinaryOperator; -use Twig\Operator\Binary\EqualBinaryOperator; -use Twig\Operator\Binary\FilterBinaryOperator; -use Twig\Operator\Binary\FloorDivBinaryOperator; -use Twig\Operator\Binary\FunctionBinaryOperator; -use Twig\Operator\Binary\GreaterBinaryOperator; -use Twig\Operator\Binary\GreaterEqualBinaryOperator; -use Twig\Operator\Binary\HasEveryBinaryOperator; -use Twig\Operator\Binary\HasSomeBinaryOperator; -use Twig\Operator\Binary\InBinaryOperator; -use Twig\Operator\Binary\IsBinaryOperator; -use Twig\Operator\Binary\IsNotBinaryOperator; -use Twig\Operator\Binary\LessBinaryOperator; -use Twig\Operator\Binary\LessEqualBinaryOperator; -use Twig\Operator\Binary\MatchesBinaryOperator; -use Twig\Operator\Binary\ModBinaryOperator; -use Twig\Operator\Binary\MulBinaryOperator; -use Twig\Operator\Binary\NotEqualBinaryOperator; -use Twig\Operator\Binary\NotInBinaryOperator; -use Twig\Operator\Binary\NullCoalesceBinaryOperator; -use Twig\Operator\Binary\OrBinaryOperator; -use Twig\Operator\Binary\PowerBinaryOperator; -use Twig\Operator\Binary\RangeBinaryOperator; -use Twig\Operator\Binary\SpaceshipBinaryOperator; -use Twig\Operator\Binary\SquareBracketBinaryOperator; -use Twig\Operator\Binary\StartsWithBinaryOperator; -use Twig\Operator\Binary\SubBinaryOperator; -use Twig\Operator\Binary\XorBinaryOperator; -use Twig\Operator\Ternary\ConditionalTernaryOperator; -use Twig\Operator\Unary\NegUnaryOperator; -use Twig\Operator\Unary\NotUnaryOperator; -use Twig\Operator\Unary\ParenthesisUnaryOperator; -use Twig\Operator\Unary\PosUnaryOperator; use Twig\Parser; use Twig\Sandbox\SecurityNotAllowedMethodError; use Twig\Sandbox\SecurityNotAllowedPropertyError; @@ -320,54 +325,58 @@ public function getNodeVisitors(): array return []; } - public function getOperators(): array + public function getExpressionParsers(): array { return [ - new NotUnaryOperator(), - new NegUnaryOperator(), - new PosUnaryOperator(), - new ParenthesisUnaryOperator(), - - new ElvisBinaryOperator(), - new NullCoalesceBinaryOperator(), - new OrBinaryOperator(), - new XorBinaryOperator(), - new AndBinaryOperator(), - new BitwiseOrBinaryOperator(), - new BitwiseXorBinaryOperator(), - new BitwiseAndBinaryOperator(), - new EqualBinaryOperator(), - new NotEqualBinaryOperator(), - new SpaceshipBinaryOperator(), - new LessBinaryOperator(), - new GreaterBinaryOperator(), - new GreaterEqualBinaryOperator(), - new LessEqualBinaryOperator(), - new NotInBinaryOperator(), - new InBinaryOperator(), - new MatchesBinaryOperator(), - new StartsWithBinaryOperator(), - new EndsWithBinaryOperator(), - new HasSomeBinaryOperator(), - new HasEveryBinaryOperator(), - new RangeBinaryOperator(), - new AddBinaryOperator(), - new SubBinaryOperator(), - new ConcatBinaryOperator(), - new MulBinaryOperator(), - new DivBinaryOperator(), - new FloorDivBinaryOperator(), - new ModBinaryOperator(), - new IsBinaryOperator(), - new IsNotBinaryOperator(), - new PowerBinaryOperator(), - new FilterBinaryOperator(), - new DotBinaryOperator(), - new SquareBracketBinaryOperator(), - new FunctionBinaryOperator(), - new ArrowBinaryOperator(), - - new ConditionalTernaryOperator(), + 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)), + new BinaryOperatorExpressionParser(OrBinary::class, 'or', 10), + new BinaryOperatorExpressionParser(XorBinary::class, 'xor', 12), + new BinaryOperatorExpressionParser(AndBinary::class, 'and', 15), + new BinaryOperatorExpressionParser(BitwiseOrBinary::class, 'b-or', 16), + new BinaryOperatorExpressionParser(BitwiseXorBinary::class, 'b-xor', 17), + new BinaryOperatorExpressionParser(BitwiseAndBinary::class, 'b-and', 18), + new BinaryOperatorExpressionParser(EqualBinary::class, '==', 20), + new BinaryOperatorExpressionParser(NotEqualBinary::class, '!=', 20), + new BinaryOperatorExpressionParser(SpaceshipBinary::class, '<=>', 20), + new BinaryOperatorExpressionParser(LessBinary::class, '<', 20), + new BinaryOperatorExpressionParser(GreaterBinary::class, '>', 20), + new BinaryOperatorExpressionParser(GreaterEqualBinary::class, '>=', 20), + new BinaryOperatorExpressionParser(LessEqualBinary::class, '<=', 20), + new BinaryOperatorExpressionParser(NotInBinary::class, 'not in', 20), + new BinaryOperatorExpressionParser(InBinary::class, 'in', 20), + new BinaryOperatorExpressionParser(MatchesBinary::class, 'matches', 20), + new BinaryOperatorExpressionParser(StartsWithBinary::class, 'starts with', 20), + new BinaryOperatorExpressionParser(EndsWithBinary::class, 'ends with', 20), + new BinaryOperatorExpressionParser(HasSomeBinary::class, 'has some', 20), + new BinaryOperatorExpressionParser(HasEveryBinary::class, 'has every', 20), + new BinaryOperatorExpressionParser(RangeBinary::class, '..', 25), + new BinaryOperatorExpressionParser(AddBinary::class, '+', 30), + new BinaryOperatorExpressionParser(SubBinary::class, '-', 30), + 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(ModBinary::class, '%', 60), + new BinaryOperatorExpressionParser(PowerBinary::class, '**', 200, InfixAssociativity::Right), + + new ConditionalTernaryExpressionParser(), + + new IsExpressionParser(), + new IsNotExpressionParser(), + new DotExpressionParser(), + new SquareBracketExpressionParser(), + + new GroupingExpressionParser(), + new FilterExpressionParser(), + new FunctionExpressionParser(), + new ArrowExpressionParser(), + + new LiteralExpressionParser(), ]; } diff --git a/src/Extension/ExtensionInterface.php b/src/Extension/ExtensionInterface.php index 7eef100f904..44356f62769 100644 --- a/src/Extension/ExtensionInterface.php +++ b/src/Extension/ExtensionInterface.php @@ -11,8 +11,9 @@ namespace Twig\Extension; +use Twig\ExpressionParser\ExpressionParserInterface; +use Twig\ExpressionParser\PrecedenceChange; use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\Operator\OperatorInterface; use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; use Twig\TwigFunction; @@ -22,6 +23,8 @@ * Interface implemented by extension classes. * * @author Fabien Potencier + * + * @method array getExpressionParsers() */ interface ExtensionInterface { @@ -63,11 +66,11 @@ public function getFunctions(); /** * Returns a list of operators to add to the existing list. * - * @return OperatorInterface[]|array + * @return array * - * @psalm-return OperatorInterface[]|array{ - * array}>, - * array, associativity: ExpressionParser::OPERATOR_*}> + * @psalm-return array{ + * array}>, + * array, associativity: ExpressionParser::OPERATOR_*}> * } */ public function getOperators(); diff --git a/src/ExtensionSet.php b/src/ExtensionSet.php index ad6ee7d0713..262d1262579 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -12,19 +12,18 @@ namespace Twig; use Twig\Error\RuntimeError; +use Twig\ExpressionParser\ExpressionParsers; +use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser; +use Twig\ExpressionParser\InfixAssociativity; +use Twig\ExpressionParser\InfixExpressionParserInterface; +use Twig\ExpressionParser\PrecedenceChange; +use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser; use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; use Twig\Extension\LastModifiedExtensionInterface; use Twig\Extension\StagingExtension; use Twig\Node\Expression\AbstractExpression; use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\Operator\Binary\AbstractBinaryOperator; -use Twig\Operator\Binary\BinaryOperatorInterface; -use Twig\Operator\OperatorAssociativity; -use Twig\Operator\OperatorInterface; -use Twig\Operator\Operators; -use Twig\Operator\Unary\AbstractUnaryOperator; -use Twig\Operator\Unary\UnaryOperatorInterface; use Twig\TokenParser\TokenParserInterface; /** @@ -52,8 +51,7 @@ final class ExtensionSet private $functions; /** @var array */ private $dynamicFunctions; - /** @var Operators */ - private $operators; + private ExpressionParsers $expressionParsers; /** @var array|null */ private $globals; /** @var array */ @@ -410,13 +408,13 @@ public function getTest(string $name): ?TwigTest return null; } - public function getOperators(): Operators + public function getExpressionParsers(): ExpressionParsers { if (!$this->initialized) { $this->initExtensions(); } - return $this->operators; + return $this->expressionParsers; } private function initExtensions(): void @@ -429,7 +427,7 @@ private function initExtensions(): void $this->dynamicFunctions = []; $this->dynamicTests = []; $this->visitors = []; - $this->operators = new Operators(); + $this->expressionParsers = new ExpressionParsers(); foreach ($this->extensions as $extension) { $this->initExtension($extension); @@ -479,119 +477,65 @@ private function initExtension(ExtensionInterface $extension): void $this->visitors[] = $visitor; } - // operators - if ($operators = $extension->getOperators()) { - if (!\is_array($operators)) { - throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators))); - } - - // new signature? - $legacy = false; - foreach ($operators as $op) { - if (!$op instanceof OperatorInterface) { - $legacy = true; + // expression parsers + if (method_exists($extension, 'getExpressionParsers')) { + $this->expressionParsers->add($extension->getExpressionParsers()); + } - break; - } - } + $operators = $extension->getOperators(); + if (!\is_array($operators)) { + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', \get_class($extension), get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators))); + } - if ($legacy) { - if (2 !== \count($operators)) { - throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); - } + if (2 !== \count($operators)) { + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', \get_class($extension), \count($operators))); + } - trigger_deprecation('twig/twig', '3.20', \sprintf('Extension "%s" uses the old signature for "getOperators()", please update it to return an array of "OperatorInterface" objects.', \get_class($extension))); + $expressionParsers = []; + foreach ($operators[0] as $operator => $op) { + $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']) { + 1 => InfixAssociativity::Left, + 2 => InfixAssociativity::Right, + default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".', $op['associativity'], $operator)), + }; - $ops = []; - foreach ($operators[0] as $n => $op) { - $ops[] = $op instanceof OperatorInterface ? $op : $this->convertUnaryOperators($n, $op); - } - foreach ($operators[1] as $n => $op) { - $ops[] = $op instanceof OperatorInterface ? $op : $this->convertBinaryOperators($n, $op); - } - $this->operators->add($ops); + if ($op['callable']) { + $expressionParsers[] = $this->convertInfixExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, $op['aliases'] ?? [], $op['callable']); } else { - $this->operators->add($operators); + $expressionParsers[] = new BinaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, $op['aliases'] ?? []); } } - } - private function convertUnaryOperators(string $n, array $op): OperatorInterface - { - trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-OperatorInterface object to define the "%s" unary operator is deprecated.', $n)); - - return new class($op, $n) extends AbstractUnaryOperator implements UnaryOperatorInterface { - public function __construct(private array $op, private string $operator) - { - } - - public function getOperator(): string - { - return $this->operator; - } - - public function getPrecedence(): int - { - return $this->op['precedence']; - } + if (count($expressionParsers)) { + trigger_deprecation('twig/twig', '3.20', \sprintf('Extension "%s" uses the old signature for "getOperators()", please implement "getExpressionParsers()" instead.', \get_class($extension))); - public function getPrecedenceChange(): ?OperatorPrecedenceChange - { - return $this->op['precedence_change'] ?? null; - } - - protected function getNodeClass(): string - { - return $this->op['class'] ?? ''; - } - }; + $this->expressionParsers->add($expressionParsers); + } } - private function convertBinaryOperators(string $n, array $op): OperatorInterface + private function convertInfixExpressionParser(string $nodeClass, string $operator, int $precedence, InfixAssociativity $associativity, ?PrecedenceChange $precedenceChange, array $aliases, callable $callable): InfixExpressionParserInterface { - trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-OperatorInterface object to define the "%s" binary operator is deprecated.', $n)); - - return new class($op, $n) extends AbstractBinaryOperator implements BinaryOperatorInterface { - public function __construct(private array $op, private string $operator) - { - } - - public function getOperator(): string - { - return $this->operator; - } + trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-ExpressionParserInterface object to define the "%s" binary operator is deprecated.', $operator)); - public function getPrecedence(): int - { - return $this->op['precedence']; + return new class($nodeClass, $operator, $precedence, $associativity, $precedenceChange, $aliases, $callable) extends BinaryOperatorExpressionParser { + public function __construct( + string $nodeClass, + string $operator, + int $precedence, + InfixAssociativity $associativity = InfixAssociativity::Left, + ?PrecedenceChange $precedenceChange = null, + array $aliases = [], + private $callable = null, + ) { + parent::__construct($nodeClass, $operator, $precedence, $associativity, $precedenceChange, $aliases); } - public function getPrecedenceChange(): ?OperatorPrecedenceChange + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { - return $this->op['precedence_change'] ?? null; - } - - protected function getNodeClass(): string - { - return $this->op['class'] ?? ''; - } - - public function getAssociativity(): OperatorAssociativity - { - return match ($this->op['associativity']) { - 1 => OperatorAssociativity::Left, - 2 => OperatorAssociativity::Right, - default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".', $this->op['associativity'], $this->getOperator())), - }; - } - - public function parse(ExpressionParser $parser, AbstractExpression $expr, Token $token): AbstractExpression - { - if ($this->op['callable']) { - return $this->op['callable']($parser, $expr); - } - - return parent::parse($parser, $expr, $token); + return ($this->callable)($parser, $expr); } }; } diff --git a/src/Lexer.php b/src/Lexer.php index 84b29de32a7..26d8fa42437 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -535,25 +535,25 @@ private function moveCursor($text): void private function getOperatorRegex(): string { - $operators = ['=']; - foreach ($this->env->getOperators() as $operator) { - $operators = array_merge($operators, [$operator->getOperator()], $operator->getAliases()); + $expressionParsers = ['=']; + foreach ($this->env->getExpressionParsers() as $expressionParser) { + $expressionParsers = array_merge($expressionParsers, [$expressionParser->getName()], $expressionParser->getAliases()); } - $operators = array_combine($operators, array_map('strlen', $operators)); - arsort($operators); + $expressionParsers = array_combine($expressionParsers, array_map('strlen', $expressionParsers)); + arsort($expressionParsers); $regex = []; - foreach ($operators as $operator => $length) { + foreach ($expressionParsers as $expressionParser => $length) { // an operator that ends with a character must be followed by // a whitespace, a parenthesis, an opening map [ or sequence { - $r = preg_quote($operator, '/'); - if (ctype_alpha($operator[$length - 1])) { + $r = preg_quote($expressionParser, '/'); + if (ctype_alpha($expressionParser[$length - 1])) { $r .= '(?=[\s()\[{])'; } // an operator that begins with a character must not have a dot or pipe before - if (ctype_alpha($operator[0])) { + if (ctype_alpha($expressionParser[0])) { $r = '(?getTemplateLine(), $item->getSourceContext()); - } - } - parent::__construct($items, [], $lineno); } diff --git a/src/Operator/Binary/AbstractBinaryOperator.php b/src/Operator/Binary/AbstractBinaryOperator.php deleted file mode 100644 index 64fc9033991..00000000000 --- a/src/Operator/Binary/AbstractBinaryOperator.php +++ /dev/null @@ -1,44 +0,0 @@ -parseExpression(OperatorAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence()); - - return new ($this->getNodeClass())($left, $right, $token->getLine()); - } - - public function getArity(): OperatorArity - { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity - { - return OperatorAssociativity::Left; - } - - /** - * @return class-string - */ - abstract protected function getNodeClass(): string; -} diff --git a/src/Operator/Binary/AddBinaryOperator.php b/src/Operator/Binary/AddBinaryOperator.php deleted file mode 100644 index 7e708c38004..00000000000 --- a/src/Operator/Binary/AddBinaryOperator.php +++ /dev/null @@ -1,32 +0,0 @@ -'; - } - - public function getPrecedence(): int - { - return 20; - } - - protected function getNodeClass(): string - { - return GreaterBinary::class; - } -} diff --git a/src/Operator/Binary/GreaterEqualBinaryOperator.php b/src/Operator/Binary/GreaterEqualBinaryOperator.php deleted file mode 100644 index 69a5ad203bb..00000000000 --- a/src/Operator/Binary/GreaterEqualBinaryOperator.php +++ /dev/null @@ -1,32 +0,0 @@ -='; - } - - public function getPrecedence(): int - { - return 20; - } -} diff --git a/src/Operator/Binary/HasEveryBinaryOperator.php b/src/Operator/Binary/HasEveryBinaryOperator.php deleted file mode 100644 index 1312640aed0..00000000000 --- a/src/Operator/Binary/HasEveryBinaryOperator.php +++ /dev/null @@ -1,32 +0,0 @@ -'; - } - - public function getPrecedence(): int - { - return 20; - } - - protected function getNodeClass(): string - { - return SpaceshipBinary::class; - } -} diff --git a/src/Operator/Binary/StartsWithBinaryOperator.php b/src/Operator/Binary/StartsWithBinaryOperator.php deleted file mode 100644 index 4d543454d0a..00000000000 --- a/src/Operator/Binary/StartsWithBinaryOperator.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -final class Operators implements \IteratorAggregate -{ - /** - * @var array, array> - */ - private array $operators = []; - - /** - * @var array, array> - */ - private array $aliases = []; - - /** - * @var \WeakMap>|null - */ - private ?\WeakMap $precedenceChanges = null; - - /** - * @param array $operators - */ - public function __construct( - array $operators = [], - ) { - $this->add($operators); - } - - /** - * @param array $operators - * - * @return $this - */ - public function add(array $operators): self - { - $this->precedenceChanges = null; - foreach ($operators as $operator) { - $this->operators[$operator->getArity()->value][$operator->getOperator()] = $operator; - foreach ($operator->getAliases() as $alias) { - $this->aliases[$operator->getArity()->value][$alias] = $operator; - } - } - - return $this; - } - - public function getUnary(string $name): ?UnaryOperatorInterface - { - return $this->operators[OperatorArity::Unary->value][$name] ?? ($this->aliases[OperatorArity::Unary->value][$name] ?? null); - } - - public function getBinary(string $name): ?BinaryOperatorInterface - { - return $this->operators[OperatorArity::Binary->value][$name] ?? ($this->aliases[OperatorArity::Binary->value][$name] ?? null); - } - - public function getTernary(string $name): ?TernaryOperatorInterface - { - return $this->operators[OperatorArity::Ternary->value][$name] ?? ($this->aliases[OperatorArity::Ternary->value][$name] ?? null); - } - - public function getIterator(): \Traversable - { - foreach ($this->operators as $operators) { - // we don't yield the keys - yield from $operators; - } - } - - /** - * @internal - * - * @return \WeakMap> - */ - public function getPrecedenceChanges(): \WeakMap - { - if (null === $this->precedenceChanges) { - $this->precedenceChanges = new \WeakMap(); - foreach ($this as $op) { - if (!$op->getPrecedenceChange()) { - continue; - } - $min = min($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence()); - $max = max($op->getPrecedenceChange()->getNewPrecedence(), $op->getPrecedence()); - foreach ($this as $o) { - if ($o->getPrecedence() > $min && $o->getPrecedence() < $max) { - if (!isset($this->precedenceChanges[$o])) { - $this->precedenceChanges[$o] = []; - } - $this->precedenceChanges[$o][] = $op; - } - } - } - } - - return $this->precedenceChanges; - } -} diff --git a/src/Operator/Ternary/AbstractTernaryOperator.php b/src/Operator/Ternary/AbstractTernaryOperator.php deleted file mode 100644 index 3a88247ff30..00000000000 --- a/src/Operator/Ternary/AbstractTernaryOperator.php +++ /dev/null @@ -1,29 +0,0 @@ -getNodeClass())($parser->parseExpression($this->getPrecedence()), $token->getLine()); - } - - public function getArity(): OperatorArity - { - return OperatorArity::Unary; - } - - /** - * @return class-string - */ - abstract protected function getNodeClass(): string; -} diff --git a/src/Operator/Unary/NegUnaryOperator.php b/src/Operator/Unary/NegUnaryOperator.php deleted file mode 100644 index de01a3b5176..00000000000 --- a/src/Operator/Unary/NegUnaryOperator.php +++ /dev/null @@ -1,32 +0,0 @@ - + * + * @deprecated since Twig 1.20 Use Twig\ExpressionParser\PrecedenceChange instead */ -class OperatorPrecedenceChange +class OperatorPrecedenceChange extends PrecedenceChange { 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; - } + trigger_deprecation('twig/twig', '3.20', 'The "%s" class is deprecated since Twig 3.20. Use "%s" instead.', self::class, PrecedenceChange::class); - public function getNewPrecedence(): int - { - return $this->newPrecedence; + parent::__construct($package, $version, $newPrecedence); } } diff --git a/src/Parser.php b/src/Parser.php index c2468cffe91..1ddbae9813f 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -13,6 +13,10 @@ namespace Twig; use Twig\Error\SyntaxError; +use Twig\ExpressionParser\ExpressionParserInterface; +use Twig\ExpressionParser\ExpressionParsers; +use Twig\ExpressionParser\Prefix\LiteralExpressionParser; +use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Node\BlockNode; use Twig\Node\BlockReferenceNode; use Twig\Node\BodyNode; @@ -49,10 +53,12 @@ class Parser private $embeddedTemplates = []; private $varNameSalt = 0; private $ignoreUnknownTwigCallables = false; + private ExpressionParsers $parsers; public function __construct( private Environment $env, ) { + $this->parsers = $env->getExpressionParsers(); } public function getEnvironment(): Environment @@ -78,10 +84,6 @@ public function parse(TokenStream $stream, $test = null, bool $dropNeedle = fals $this->visitors = $this->env->getNodeVisitors(); } - if (null === $this->expressionParser) { - $this->expressionParser = new ExpressionParser($this, $this->env); - } - $this->stream = $stream; $this->parent = null; $this->blocks = []; @@ -155,7 +157,7 @@ public function subparse($test, bool $dropNeedle = false): Node case $this->stream->getCurrent()->test(Token::VAR_START_TYPE): $token = $this->stream->next(); - $expr = $this->expressionParser->parseExpression(); + $expr = $this->parseExpression(); $this->stream->expect(Token::VAR_END_TYPE); $rv[] = new PrintNode($expr, $token->getLine()); break; @@ -337,11 +339,42 @@ public function popLocalScope(): void array_shift($this->importedSymbols); } + /** + * @deprecated since Twig 3.20 + */ public function getExpressionParser(): ExpressionParser { + trigger_deprecation('twig/twig', '3.20', 'Method "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); + + if (null === $this->expressionParser) { + $this->expressionParser = new ExpressionParser($this, $this->env); + } + return $this->expressionParser; } + public function parseExpression(int $precedence = 0): AbstractExpression + { + $token = $this->getCurrentToken(); + if ($token->test(Token::OPERATOR_TYPE) && $ep = $this->parsers->getPrefix($token->getValue())) { + $this->getStream()->next(); + $expr = $ep->parse($this, $token); + $this->checkPrecedenceDeprecations($ep, $expr); + } else { + $expr = $this->parsers->getPrefixByClass(LiteralExpressionParser::class)->parse($this, $token); + } + + $token = $this->getCurrentToken(); + while ($token->test(Token::OPERATOR_TYPE) && ($ep = $this->parsers->getInfix($token->getValue())) && $ep->getPrecedence() >= $precedence) { + $this->getStream()->next(); + $expr = $ep->parse($this, $expr, $token); + $this->checkPrecedenceDeprecations($ep, $expr); + $token = $this->getCurrentToken(); + } + + return $expr; + } + public function getParent(): ?Node { trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); @@ -519,4 +552,42 @@ private function filterBodyNodes(Node $node, bool $nested = false): ?Node return $node; } + + private function checkPrecedenceDeprecations(ExpressionParserInterface $expressionParser, AbstractExpression $expr) + { + $expr->setAttribute('expression_parser', $expressionParser); + $precedenceChanges = $this->parsers->getPrecedenceChanges(); + + // Check that the all nodes that are between the 2 precedences have explicit parentheses + if (!isset($precedenceChanges[$expressionParser])) { + return; + } + + if ($expressionParser instanceof PrefixExpressionParserInterface) { + if ($expr->hasExplicitParentheses()) { + return; + } + /** @var AbstractExpression $node */ + $node = $expr->getNode('node'); + foreach ($precedenceChanges as $ep => $changes) { + if (!\in_array($expressionParser, $changes, true)) { + continue; + } + 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())); + } + } + } 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())); + } + } + } + } + } } diff --git a/src/TokenParser/AbstractTokenParser.php b/src/TokenParser/AbstractTokenParser.php index 30bef15a340..8acaa6f56e9 100644 --- a/src/TokenParser/AbstractTokenParser.php +++ b/src/TokenParser/AbstractTokenParser.php @@ -36,8 +36,6 @@ public function setParser(Parser $parser): void /** * Parses an assignment expression like "a, b". - * - * @return Nodes */ protected function parseAssignmentExpression(): Nodes { diff --git a/src/TokenParser/ApplyTokenParser.php b/src/TokenParser/ApplyTokenParser.php index 68ef7c17e6b..e4e3cfaebf0 100644 --- a/src/TokenParser/ApplyTokenParser.php +++ b/src/TokenParser/ApplyTokenParser.php @@ -11,6 +11,7 @@ namespace Twig\TokenParser; +use Twig\ExpressionParser\Infix\FilterExpressionParser; use Twig\Node\Expression\Variable\LocalVariable; use Twig\Node\Node; use Twig\Node\Nodes; @@ -34,9 +35,9 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $ref = new LocalVariable(null, $lineno); $filter = $ref; - $op = $this->parser->getEnvironment()->getOperators()->getBinary('|'); + $op = $this->parser->getEnvironment()->getExpressionParsers()->getInfixByClass(FilterExpressionParser::class); while (true) { - $filter = $op->parse($this->parser->getExpressionParser(), $filter, $this->parser->getCurrentToken()); + $filter = $op->parse($this->parser, $filter, $this->parser->getCurrentToken()); if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { break; } diff --git a/src/TokenParser/AutoEscapeTokenParser.php b/src/TokenParser/AutoEscapeTokenParser.php index b50b29e659e..86feb27e621 100644 --- a/src/TokenParser/AutoEscapeTokenParser.php +++ b/src/TokenParser/AutoEscapeTokenParser.php @@ -32,7 +32,7 @@ public function parse(Token $token): Node if ($stream->test(Token::BLOCK_END_TYPE)) { $value = 'html'; } else { - $expr = $this->parser->getExpressionParser()->parseExpression(); + $expr = $this->parser->parseExpression(); if (!$expr instanceof ConstantExpression) { throw new SyntaxError('An escaping strategy must be a string or false.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); } diff --git a/src/TokenParser/BlockTokenParser.php b/src/TokenParser/BlockTokenParser.php index 3561b99cdd7..452b323e533 100644 --- a/src/TokenParser/BlockTokenParser.php +++ b/src/TokenParser/BlockTokenParser.php @@ -53,7 +53,7 @@ public function parse(Token $token): Node } } else { $body = new Nodes([ - new PrintNode($this->parser->getExpressionParser()->parseExpression(), $lineno), + new PrintNode($this->parser->parseExpression(), $lineno), ]); } $stream->expect(Token::BLOCK_END_TYPE); diff --git a/src/TokenParser/DeprecatedTokenParser.php b/src/TokenParser/DeprecatedTokenParser.php index 164ef26eec3..df1ba381f44 100644 --- a/src/TokenParser/DeprecatedTokenParser.php +++ b/src/TokenParser/DeprecatedTokenParser.php @@ -33,8 +33,7 @@ final class DeprecatedTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $expressionParser = $this->parser->getExpressionParser(); - $expr = $expressionParser->parseExpression(); + $expr = $this->parser->parseExpression(); $node = new DeprecatedNode($expr, $token->getLine()); while ($stream->test(Token::NAME_TYPE)) { @@ -44,10 +43,10 @@ public function parse(Token $token): Node switch ($k) { case 'package': - $node->setNode('package', $expressionParser->parseExpression()); + $node->setNode('package', $this->parser->parseExpression()); break; case 'version': - $node->setNode('version', $expressionParser->parseExpression()); + $node->setNode('version', $this->parser->parseExpression()); break; default: throw new SyntaxError(\sprintf('Unknown "%s" option.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); diff --git a/src/TokenParser/DoTokenParser.php b/src/TokenParser/DoTokenParser.php index 8afd4855937..ca9d03d454f 100644 --- a/src/TokenParser/DoTokenParser.php +++ b/src/TokenParser/DoTokenParser.php @@ -24,7 +24,7 @@ final class DoTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $expr = $this->parser->getExpressionParser()->parseExpression(); + $expr = $this->parser->parseExpression(); $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); diff --git a/src/TokenParser/EmbedTokenParser.php b/src/TokenParser/EmbedTokenParser.php index f1acbf1ef00..fa279104614 100644 --- a/src/TokenParser/EmbedTokenParser.php +++ b/src/TokenParser/EmbedTokenParser.php @@ -28,7 +28,7 @@ public function parse(Token $token): Node { $stream = $this->parser->getStream(); - $parent = $this->parser->getExpressionParser()->parseExpression(); + $parent = $this->parser->parseExpression(); [$variables, $only, $ignoreMissing] = $this->parseArguments(); diff --git a/src/TokenParser/ExtendsTokenParser.php b/src/TokenParser/ExtendsTokenParser.php index a93afe8cd59..8f64698187d 100644 --- a/src/TokenParser/ExtendsTokenParser.php +++ b/src/TokenParser/ExtendsTokenParser.php @@ -36,7 +36,7 @@ public function parse(Token $token): Node throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext()); } - $this->parser->setParent($this->parser->getExpressionParser()->parseExpression()); + $this->parser->setParent($this->parser->parseExpression()); $stream->expect(Token::BLOCK_END_TYPE); diff --git a/src/TokenParser/ForTokenParser.php b/src/TokenParser/ForTokenParser.php index b098737fa6f..21166fc1fab 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -37,7 +37,7 @@ public function parse(Token $token): Node $stream = $this->parser->getStream(); $targets = $this->parseAssignmentExpression(); $stream->expect(Token::OPERATOR_TYPE, 'in'); - $seq = $this->parser->getExpressionParser()->parseExpression(); + $seq = $this->parser->parseExpression(); $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideForFork']); diff --git a/src/TokenParser/FromTokenParser.php b/src/TokenParser/FromTokenParser.php index c8732df29f6..1c80a171777 100644 --- a/src/TokenParser/FromTokenParser.php +++ b/src/TokenParser/FromTokenParser.php @@ -29,7 +29,7 @@ final class FromTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $macro = $this->parser->getExpressionParser()->parseExpression(); + $macro = $this->parser->parseExpression(); $stream = $this->parser->getStream(); $stream->expect(Token::NAME_TYPE, 'import'); diff --git a/src/TokenParser/IfTokenParser.php b/src/TokenParser/IfTokenParser.php index 6b90105633b..4e3588e5be5 100644 --- a/src/TokenParser/IfTokenParser.php +++ b/src/TokenParser/IfTokenParser.php @@ -36,7 +36,7 @@ final class IfTokenParser extends AbstractTokenParser public function parse(Token $token): Node { $lineno = $token->getLine(); - $expr = $this->parser->getExpressionParser()->parseExpression(); + $expr = $this->parser->parseExpression(); $stream = $this->parser->getStream(); $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideIfFork']); @@ -52,7 +52,7 @@ public function parse(Token $token): Node break; case 'elseif': - $expr = $this->parser->getExpressionParser()->parseExpression(); + $expr = $this->parser->parseExpression(); $stream->expect(Token::BLOCK_END_TYPE); $body = $this->parser->subparse([$this, 'decideIfFork']); $tests[] = $expr; diff --git a/src/TokenParser/ImportTokenParser.php b/src/TokenParser/ImportTokenParser.php index f23584a5a42..6dcb7662cbf 100644 --- a/src/TokenParser/ImportTokenParser.php +++ b/src/TokenParser/ImportTokenParser.php @@ -28,7 +28,7 @@ final class ImportTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $macro = $this->parser->getExpressionParser()->parseExpression(); + $macro = $this->parser->parseExpression(); $this->parser->getStream()->expect(Token::NAME_TYPE, 'as'); $name = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(); $var = new AssignTemplateVariable(new TemplateVariable($name, $token->getLine()), $this->parser->isMainScope()); diff --git a/src/TokenParser/IncludeTokenParser.php b/src/TokenParser/IncludeTokenParser.php index c5ce180ad2a..55ac1516c4e 100644 --- a/src/TokenParser/IncludeTokenParser.php +++ b/src/TokenParser/IncludeTokenParser.php @@ -30,7 +30,7 @@ class IncludeTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $expr = $this->parser->getExpressionParser()->parseExpression(); + $expr = $this->parser->parseExpression(); [$variables, $only, $ignoreMissing] = $this->parseArguments(); @@ -53,7 +53,7 @@ protected function parseArguments() $variables = null; if ($stream->nextIf(Token::NAME_TYPE, 'with')) { - $variables = $this->parser->getExpressionParser()->parseExpression(); + $variables = $this->parser->parseExpression(); } $only = false; diff --git a/src/TokenParser/MacroTokenParser.php b/src/TokenParser/MacroTokenParser.php index 1d857730011..38e66c81073 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -87,7 +87,7 @@ private function parseDefinition(): ArrayExpression $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); $name = new LocalVariable($token->getValue(), $this->parser->getCurrentToken()->getLine()); if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) { - $default = $this->parser->getExpressionParser()->parseExpression(); + $default = $this->parser->parseExpression(); } else { $default = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine()); $default->setAttribute('is_implicit', true); diff --git a/src/TokenParser/SetTokenParser.php b/src/TokenParser/SetTokenParser.php index c9ebceb0bf8..1aabbf582b1 100644 --- a/src/TokenParser/SetTokenParser.php +++ b/src/TokenParser/SetTokenParser.php @@ -72,11 +72,11 @@ public function getTag(): string return 'set'; } - private function parseMultitargetExpression() + private function parseMultitargetExpression(): Nodes { $targets = []; while (true) { - $targets[] = $this->parser->getExpressionParser()->parseExpression(); + $targets[] = $this->parser->parseExpression(); if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { break; } diff --git a/src/TokenParser/UseTokenParser.php b/src/TokenParser/UseTokenParser.php index ebd95aa317f..41386c8b479 100644 --- a/src/TokenParser/UseTokenParser.php +++ b/src/TokenParser/UseTokenParser.php @@ -36,7 +36,7 @@ final class UseTokenParser extends AbstractTokenParser { public function parse(Token $token): Node { - $template = $this->parser->getExpressionParser()->parseExpression(); + $template = $this->parser->parseExpression(); $stream = $this->parser->getStream(); if (!$template instanceof ConstantExpression) { diff --git a/src/TokenParser/WithTokenParser.php b/src/TokenParser/WithTokenParser.php index 8ce4f02b2c5..83470d8651f 100644 --- a/src/TokenParser/WithTokenParser.php +++ b/src/TokenParser/WithTokenParser.php @@ -31,7 +31,7 @@ public function parse(Token $token): Node $variables = null; $only = false; if (!$stream->test(Token::BLOCK_END_TYPE)) { - $variables = $this->parser->getExpressionParser()->parseExpression(); + $variables = $this->parser->parseExpression(); $only = (bool) $stream->nextIf(Token::NAME_TYPE, 'only'); } diff --git a/tests/CustomExtensionTest.php b/tests/CustomExtensionTest.php index f89a900df52..174ad5e73f4 100644 --- a/tests/CustomExtensionTest.php +++ b/tests/CustomExtensionTest.php @@ -31,7 +31,7 @@ public function testGetInvalidOperators(ExtensionInterface $extension, $expected $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage($expectedExceptionMessage); - $env->getOperators(); + $env->getExpressionParsers(); } public static function provideInvalidExtensions() diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 5bc90b58215..5ddf07009bd 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -18,6 +18,8 @@ use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; +use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser; +use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser; use Twig\Extension\AbstractExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; @@ -26,8 +28,6 @@ use Twig\Loader\LoaderInterface; use Twig\Node\Node; use Twig\NodeVisitor\NodeVisitorInterface; -use Twig\Operator\Binary\AbstractBinaryOperator; -use Twig\Operator\Unary\AbstractUnaryOperator; use Twig\RuntimeLoader\RuntimeLoaderInterface; use Twig\Source; use Twig\Token; @@ -309,8 +309,8 @@ public function testAddExtension() $this->assertArrayHasKey('foo_filter', $twig->getFilters()); $this->assertArrayHasKey('foo_function', $twig->getFunctions()); $this->assertArrayHasKey('foo_test', $twig->getTests()); - $this->assertNotNull($twig->getOperators()->getUnary('foo_unary')); - $this->assertNotNull($twig->getOperators()->getBinary('foo_binary')); + $this->assertNotNull($twig->getExpressionParsers()->getPrefix('foo_unary')); + $this->assertNotNull($twig->getExpressionParsers()->getInfix('foo_binary')); $this->assertArrayHasKey('foo_global', $twig->getGlobals()); $visitors = $twig->getNodeVisitors(); $found = false; @@ -596,41 +596,11 @@ public function getFunctions(): array ]; } - public function getOperators(): array + public function getExpressionParsers(): array { return [ - new class extends AbstractUnaryOperator { - public function getOperator(): string - { - return 'foo_unary'; - } - - public function getPrecedence(): int - { - return 0; - } - - public function getNodeClass(): string - { - return ''; - } - }, - new class extends AbstractBinaryOperator { - public function getOperator(): string - { - return 'foo_binary'; - } - - public function getPrecedence(): int - { - return 0; - } - - public function getNodeClass(): string - { - return ''; - } - }, + new UnaryOperatorExpressionParser('', 'foo_unary', 0), + new BinaryOperatorExpressionParser('', 'foo_binary', 0), ]; } diff --git a/tests/ExpressionParserTest.php b/tests/ExpressionParserTest.php index f98bade0841..7d263fbf66a 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -17,6 +17,7 @@ use Twig\Compiler; use Twig\Environment; use Twig\Error\SyntaxError; +use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser; use Twig\Extension\AbstractExtension; use Twig\Loader\ArrayLoader; use Twig\Node\Expression\ArrayExpression; @@ -28,7 +29,6 @@ use Twig\Node\Expression\Unary\AbstractUnary; use Twig\Node\Expression\Variable\ContextVariable; use Twig\Node\Node; -use Twig\Operator\Unary\AbstractUnaryOperator; use Twig\Parser; use Twig\Source; use Twig\TwigFilter; @@ -571,32 +571,17 @@ public function testUnaryPrecedenceChange() { $env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]); $env->addExtension(new class extends AbstractExtension { - public function getOperators() + public function getExpressionParsers(): array { + $class = new class(new ConstantExpression('foo', 1), 1) extends AbstractUnary { + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('!'); + } + }; + return [ - new class extends AbstractUnaryOperator { - public function getOperator(): string - { - return '!'; - } - - public function getPrecedence(): int - { - return 50; - } - - public function getNodeClass(): string - { - $class = new class(new ConstantExpression('foo', 1), 1) extends AbstractUnary { - public function operator(Compiler $compiler): Compiler - { - return $compiler->raw('!'); - } - }; - - return $class::class; - } - }, + new UnaryOperatorExpressionParser($class::class, '!', 50), ]; } }); diff --git a/tests/Fixtures/operators/not_precedence.test b/tests/Fixtures/operators/not_precedence.test index 592b1c33440..f21a0861653 100644 --- a/tests/Fixtures/operators/not_precedence.test +++ b/tests/Fixtures/operators/not_precedence.test @@ -2,7 +2,7 @@ *, /, //, and % will have a higher precedence over not in Twig 4.0 --TEMPLATE-- {{ (not 1) * 2 }} -{{ (not 1 * 2) }} +{{ not (1 * 2) }} --DATA-- return [] --EXPECT-- From 51197d9a8229fdfb4b3e8b203e489030cbaff2bb Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 26 Jan 2025 20:53:23 +0100 Subject: [PATCH 5/7] Add deprecation notices in CHANGELOG and docs --- CHANGELOG | 5 +++- doc/deprecated.rst | 57 ++++++++++++++++++++++++++++++---------------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7fde125c9c5..7e473f84911 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ # 3.20.0 (2025-XX-XX) - * Introduce operator classes to describe operators provided by extensions instead of arrays + * 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) + * Deprecate the `Twig\ExpressionParser`, and `Twig\OperatorPrecedenceChange` classes * Fix support for ignoring syntax erros in an undefined handler in guard * Add configuration for Commonmark * Fix wrong array index diff --git a/doc/deprecated.rst b/doc/deprecated.rst index 74e9e695b43..e81eafaae57 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -210,27 +210,35 @@ Node Visitors Parser ------ -* Passing a second argument to ``ExpressionParser::parseFilterExpressionRaw()`` - is deprecated as of Twig 3.12. - * The following methods from ``Twig\Parser`` are deprecated as of Twig 3.12: ``getBlockStack()``, ``hasBlock()``, ``getBlock()``, ``hasMacro()``, ``hasTraits()``, ``getParent()``. -* The ``Twig\ExpressionParser::parseHashExpression()`` method is deprecated, use - ``Twig\ExpressionParser::parseMappingExpression()`` instead. - -* The ``Twig\ExpressionParser::parseArrayExpression()`` method is deprecated, use - ``Twig\ExpressionParser::parseSequenceExpression()`` instead. - * Passing ``null`` to ``Twig\Parser::setParent()`` is deprecated as of Twig 3.12. -* The ``Twig\ExpressionParser::parseOnlyArguments()`` and - ``Twig\ExpressionParser::parseArguments()`` methods are deprecated, use - ``Twig\ExpressionParser::parseNamedArguments()`` instead. - -Lexer +* The ``Twig\Parser::getExpressionParser()`` method is deprecated as of Twig + 3.20, use ``Twig\Parser::parseExpression()`` instead. + +* The ``Twig\ExpressionParser`` class is deprecated as of Twig 3.20: + + * ``parseExpression()``, use ``Parser::parseExpression()`` + * ``parsePrimaryExpression()``, use ``Parser::parseExpression()`` + * ``parseStringExpression()``, use ``Parser::parseExpression()`` + * ``parseHashExpression()``, use ``Parser::parseExpression()`` + * ``parseMappingExpression()``, use ``Parser::parseExpression()`` + * ``parseArrayExpression()``, use ``Parser::parseExpression()`` + * ``parseSequenceExpression()``, use ``Parser::parseExpression()`` + * ``parsePostfixExpression`` + * ``parseSubscriptExpression`` + * ``parseFilterExpression`` + * ``parseFilterExpressionRaw`` + * ``parseArguments()``, use ``Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()`` + * ``parseAssignmentExpression``, use ``AbstractTokenParser::parseAssignmentExpression`` + * ``parseMultitargetExpression`` + * ``parseOnlyArguments()``, use ``Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()`` + +Token ----- * Not passing a ``Source`` instance to ``Twig\TokenStream`` constructor is @@ -239,6 +247,12 @@ Lexer * The ``Token::getType()`` method is deprecated as of Twig 3.19, use ``Token::test()`` instead. +* The ``Token::ARROW_TYPE`` constant is deprecated as of Twig 3.20, the arrow + ``=>`` is now an operator (``Token::OPERATOR_TYPE``). + +* The ``Token::PUNCTUATION_TYPE`` with values ``(``, ``[``, ``|``, ``.``, + ``?``, or ``?:`` are now of the ``Token::OPERATOR_TYPE`` type. + Templates --------- @@ -419,9 +433,9 @@ Operators {{ (not 1) * 2 }} {# this is equivalent to what Twig 4.x will do without the parentheses #} -* Operators are now instances of ``Twig\Operator\OperatorInterface`` instead of - arrays. The ``ExtensionInterface::getOperators()`` method should now return an - array of ``Twig\Operator\OperatorInterface`` instances. +* The ``Twig\Extension\ExtensionInterface::getOperators()`` method is deprecated + as of Twig 3.20, use ``Twig\Extension\ExtensionInterface::getExpressionParsers()`` + instead: Before: @@ -429,15 +443,18 @@ Operators return [ 'not' => [ 'precedence' => 10, - 'class' => NotUnaryOperator::class, + 'class' => NotUnary::class, ], ]; } After: - public function getOperators(): array { + public function getExpressionParsers(): array { return [ - new NotUnaryOperator(), + new UnaryOperatorExpressionParser(NotUnary::class, 'not', 10), ]; } + +* The ``Twig\OperatorPrecedenceChange`` class is deprecated as of Twig 3.20, + use ``Twig\ExpressionParser\PrecedenceChange`` instead. From 0387ba2e045e361d9b33296b4b352802c102122f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Mon, 3 Feb 2025 22:22:16 +0100 Subject: [PATCH 6/7] Fix precedence rules --- .gitattributes | 1 + CHANGELOG | 2 + bin/generate_operators_precedence.php | 106 ++++++------ 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, 447 insertions(+), 185 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 86b9ef413df..c07b0dfb56e 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 7e473f84911..93d0386667d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ # 3.20.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 [-512, 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 c22c81938d7..0e00f6db39c 100644 --- a/bin/generate_operators_precedence.php +++ b/bin/generate_operators_precedence.php @@ -1,65 +1,79 @@ 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, "\n=========== ================ ======= ============= ===========\n"); +fwrite($output, "Precedence Operator Type Associativity Description\n"); +fwrite($output, "=========== ================ ======= ============= ==========="); + +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, "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, 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 e81eafaae57..12b3a738379 100644 --- a/doc/deprecated.rst +++ b/doc/deprecated.rst @@ -378,6 +378,8 @@ Node Operators --------- +* An operator precedence must be part of the [-512, 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 047249d6718..f9e9d718b25 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 032582fbe5e..f603127f3dd 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 960093152b7..33a32e89e1a 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 00000000000..686f8a59f1e --- /dev/null +++ b/src/ExpressionParser/ExpressionParserDescriptionInterface.php @@ -0,0 +1,17 @@ +precedenceChanges = null; $this->add($parsers); } @@ -55,12 +54,16 @@ public function __construct( */ 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() < -512) { + trigger_deprecation('twig/twig', '3.20', 'Precedence for "%s" must be between -512 and 512, got %d.', $parser->getName(), $parser->getPrecedence()); + // throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between -1024 and 1024, 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 698497b0e8d..c8630da41e7 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 @@ /** * @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 @@ public function getName(): string 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 ce650b424ea..4c66da73bc1 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 @@ /** * @internal */ -class BinaryOperatorExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +class BinaryOperatorExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { public function __construct( /** @var class-string */ @@ -32,6 +33,7 @@ public function __construct( private int $precedence, private InfixAssociativity $associativity = InfixAssociativity::Left, private ?PrecedenceChange $precedenceChange = null, + private ?string $description = null, private array $aliases = [], ) { } @@ -56,6 +58,11 @@ public function getName(): string 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 2bb5fc92c79..9707c0a04bd 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 @@ /** * @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 @@ public function getName(): string 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 d83f4bfbbfb..7d1cf505827 100644 --- a/src/ExpressionParser/Infix/DotExpressionParser.php +++ b/src/ExpressionParser/Infix/DotExpressionParser.php @@ -13,6 +13,7 @@ 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 @@ /** * @internal */ -final class DotExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +final class DotExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { use ArgumentsTrait; @@ -81,9 +82,14 @@ public function getName(): string 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 98e4b3b3a90..e47d3fe6773 100644 --- a/src/ExpressionParser/Infix/FilterExpressionParser.php +++ b/src/ExpressionParser/Infix/FilterExpressionParser.php @@ -13,8 +13,10 @@ 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 @@ /** * @internal */ -final class FilterExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +final class FilterExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { use ArgumentsTrait; @@ -61,9 +63,19 @@ public function getName(): string 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 b1d627f7b9b..e9cd7751793 100644 --- a/src/ExpressionParser/Infix/FunctionExpressionParser.php +++ b/src/ExpressionParser/Infix/FunctionExpressionParser.php @@ -14,6 +14,7 @@ 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 @@ /** * @internal */ -final class FunctionExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +final class FunctionExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { use ArgumentsTrait; @@ -72,9 +73,14 @@ public function getName(): string 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 d63b495e24e..1614c3cb991 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 @@ /** * @internal */ -class IsExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface +class IsExpressionParser extends AbstractExpressionParser implements InfixExpressionParserInterface, ExpressionParserDescriptionInterface { use ArgumentsTrait; @@ -71,6 +72,11 @@ public function getName(): string 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 1037dcb8193..25fb153c7cf 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 @@ /** * @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 @@ public function getName(): string 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 ac9f6c9dbe3..5c6608da401 100644 --- a/src/ExpressionParser/Prefix/GroupingExpressionParser.php +++ b/src/ExpressionParser/Prefix/GroupingExpressionParser.php @@ -13,6 +13,7 @@ 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 @@ /** * @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 @@ public function getName(): string 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 92540de75fd..e0e513273fb 100644 --- a/src/ExpressionParser/Prefix/LiteralExpressionParser.php +++ b/src/ExpressionParser/Prefix/LiteralExpressionParser.php @@ -13,8 +13,7 @@ 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 @@ /** * @internal */ -final class LiteralExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface +final class LiteralExpressionParser extends AbstractExpressionParser implements PrefixExpressionParserInterface, ExpressionParserDescriptionInterface { private string $type = 'literal'; @@ -112,6 +111,11 @@ public function getName(): string 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 4357d4ff6ab..35468940a14 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 @@ /** * @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 @@ public function __construct( private string $name, private int $precedence, private ?PrecedenceChange $precedenceChange = null, + private ?string $description = null, private array $aliases = [], ) { } @@ -47,6 +49,11 @@ public function getName(): string 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 2b6a9f05878..89bc2cf6f0d 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -328,12 +328,14 @@ public function getNodeVisitors(): array 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 @@ public function getExpressionParsers(): array 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 262d1262579..c5e3321cc10 100644 --- a/src/ExtensionSet.php +++ b/src/ExtensionSet.php @@ -493,7 +493,7 @@ private function initExtension(ExtensionInterface $extension): void $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 1ddbae9813f..a1fd5927647 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -15,6 +15,7 @@ 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 @@ private function checkPrecedenceDeprecations(ExpressionParserInterface $expressi 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 @@ private function checkPrecedenceDeprecations(ExpressionParserInterface $expressi } 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 7d263fbf66a..7808e6b6223 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -600,6 +600,86 @@ private static function createContextVariable(string $name, array $attributes): 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 276cbf197d1..6217a8410a5 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 541e4f7cb8a..a1370d2caed 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 00000000000..84eddeb21aa --- /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 5178288e950..3a2f4a7ec3f 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 2b2036660c9..4aaec83743a 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 #} From 14e6bc90c2449aba1d9287de101ea5aa402c394f Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 6 Feb 2025 08:26:34 +0100 Subject: [PATCH 7/7] Use generics in ExpressionParsers --- src/ExpressionParser.php | 6 +-- src/ExpressionParser/ExpressionParsers.php | 59 ++++++++-------------- src/Parser.php | 7 +-- src/TokenParser/ApplyTokenParser.php | 2 +- tests/EnvironmentTest.php | 6 ++- 5 files changed, 34 insertions(+), 46 deletions(-) diff --git a/src/ExpressionParser.php b/src/ExpressionParser.php index 9922d11ec9b..d90d5f90bfd 100644 --- a/src/ExpressionParser.php +++ b/src/ExpressionParser.php @@ -162,10 +162,10 @@ public function parseSubscriptExpression($node) $parsers = new \ReflectionProperty($this->parser, 'parsers'); if ('.' === $this->parser->getStream()->next()->getValue()) { - return $parsers->getValue($this->parser)->getInfixByClass(DotExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); + return $parsers->getValue($this->parser)->getByClass(DotExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); } - return $parsers->getValue($this->parser)->getInfixByClass(SquareBracketExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); + return $parsers->getValue($this->parser)->getByClass(SquareBracketExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); } /** @@ -189,7 +189,7 @@ public function parseFilterExpressionRaw($node) $parsers = new \ReflectionProperty($this->parser, 'parsers'); - $op = $parsers->getValue($this->parser)->getInfixByClass(FilterExpressionParser::class); + $op = $parsers->getValue($this->parser)->getByClass(FilterExpressionParser::class); while (true) { $node = $op->parse($this->parser, $node, $this->parser->getCurrentToken()); if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { diff --git a/src/ExpressionParser/ExpressionParsers.php b/src/ExpressionParser/ExpressionParsers.php index e973465b233..5dd9e89eb50 100644 --- a/src/ExpressionParser/ExpressionParsers.php +++ b/src/ExpressionParser/ExpressionParsers.php @@ -19,20 +19,15 @@ final class ExpressionParsers implements \IteratorAggregate { /** - * @var array, array> + * @var array, array> */ - private array $parsers = []; + private array $parsersByName = []; /** - * @var array, array, ExpressionParserInterface>> + * @var array, ExpressionParserInterface> */ private array $parsersByClass = []; - /** - * @var array, array> - */ - private array $aliases = []; - /** * @var \WeakMap>|null */ @@ -59,11 +54,11 @@ public function add(array $parsers): self trigger_deprecation('twig/twig', '3.20', 'Precedence for "%s" must be between -512 and 512, got %d.', $parser->getName(), $parser->getPrecedence()); // throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between -1024 and 1024, 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; + $interface = $parser instanceof PrefixExpressionParserInterface ? PrefixExpressionParserInterface::class : InfixExpressionParserInterface::class; + $this->parsersByName[$interface][$parser->getName()] = $parser; + $this->parsersByClass[get_class($parser)] = $parser; foreach ($parser->getAliases() as $alias) { - $this->aliases[$type->value][$alias] = $parser; + $this->parsersByName[$interface][$alias] = $parser; } } @@ -71,42 +66,32 @@ public function add(array $parsers): self } /** - * @param class-string $name + * @template T of ExpressionParserInterface + * + * @param class-string $class + * + * @return T|null */ - public function getPrefixByClass(string $name): ?PrefixExpressionParserInterface + public function getByClass(string $class): ?ExpressionParserInterface { - return $this->parsersByClass[ExpressionParserType::Prefix->value][$name] ?? null; - } - - public function getPrefix(string $name): ?PrefixExpressionParserInterface - { - return - $this->parsers[ExpressionParserType::Prefix->value][$name] - ?? $this->aliases[ExpressionParserType::Prefix->value][$name] - ?? null - ; + return $this->parsersByClass[$class] ?? null; } /** - * @param class-string $name + * @template T of ExpressionParserInterface + * + * @param class-string $interface + * + * @return T|null */ - public function getInfixByClass(string $name): ?InfixExpressionParserInterface - { - return $this->parsersByClass[ExpressionParserType::Infix->value][$name] ?? null; - } - - public function getInfix(string $name): ?InfixExpressionParserInterface + public function getByName(string $interface, string $name): ?ExpressionParserInterface { - return - $this->parsers[ExpressionParserType::Infix->value][$name] - ?? $this->aliases[ExpressionParserType::Infix->value][$name] - ?? null - ; + return $this->parsersByName[$interface][$name] ?? null; } public function getIterator(): \Traversable { - foreach ($this->parsers as $parsers) { + foreach ($this->parsersByName as $parsers) { // we don't yield the keys yield from $parsers; } diff --git a/src/Parser.php b/src/Parser.php index a1fd5927647..01c49b8d856 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -16,6 +16,7 @@ use Twig\ExpressionParser\ExpressionParserInterface; use Twig\ExpressionParser\ExpressionParsers; use Twig\ExpressionParser\ExpressionParserType; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\ExpressionParser\Prefix\LiteralExpressionParser; use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Node\BlockNode; @@ -357,16 +358,16 @@ public function getExpressionParser(): ExpressionParser public function parseExpression(int $precedence = 0): AbstractExpression { $token = $this->getCurrentToken(); - if ($token->test(Token::OPERATOR_TYPE) && $ep = $this->parsers->getPrefix($token->getValue())) { + if ($token->test(Token::OPERATOR_TYPE) && $ep = $this->parsers->getByName(PrefixExpressionParserInterface::class, $token->getValue())) { $this->getStream()->next(); $expr = $ep->parse($this, $token); $this->checkPrecedenceDeprecations($ep, $expr); } else { - $expr = $this->parsers->getPrefixByClass(LiteralExpressionParser::class)->parse($this, $token); + $expr = $this->parsers->getByClass(LiteralExpressionParser::class)->parse($this, $token); } $token = $this->getCurrentToken(); - while ($token->test(Token::OPERATOR_TYPE) && ($ep = $this->parsers->getInfix($token->getValue())) && $ep->getPrecedence() >= $precedence) { + while ($token->test(Token::OPERATOR_TYPE) && ($ep = $this->parsers->getByName(InfixExpressionParserInterface::class, $token->getValue())) && $ep->getPrecedence() >= $precedence) { $this->getStream()->next(); $expr = $ep->parse($this, $expr, $token); $this->checkPrecedenceDeprecations($ep, $expr); diff --git a/src/TokenParser/ApplyTokenParser.php b/src/TokenParser/ApplyTokenParser.php index e4e3cfaebf0..5b560e74916 100644 --- a/src/TokenParser/ApplyTokenParser.php +++ b/src/TokenParser/ApplyTokenParser.php @@ -35,7 +35,7 @@ public function parse(Token $token): Node $lineno = $token->getLine(); $ref = new LocalVariable(null, $lineno); $filter = $ref; - $op = $this->parser->getEnvironment()->getExpressionParsers()->getInfixByClass(FilterExpressionParser::class); + $op = $this->parser->getEnvironment()->getExpressionParsers()->getByClass(FilterExpressionParser::class); while (true) { $filter = $op->parse($this->parser, $filter, $this->parser->getCurrentToken()); if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { diff --git a/tests/EnvironmentTest.php b/tests/EnvironmentTest.php index 5ddf07009bd..19f2fce4677 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -19,7 +19,9 @@ use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; use Twig\ExpressionParser\Infix\BinaryOperatorExpressionParser; +use Twig\ExpressionParser\InfixExpressionParserInterface; use Twig\ExpressionParser\Prefix\UnaryOperatorExpressionParser; +use Twig\ExpressionParser\PrefixExpressionParserInterface; use Twig\Extension\AbstractExtension; use Twig\Extension\ExtensionInterface; use Twig\Extension\GlobalsInterface; @@ -309,8 +311,8 @@ public function testAddExtension() $this->assertArrayHasKey('foo_filter', $twig->getFilters()); $this->assertArrayHasKey('foo_function', $twig->getFunctions()); $this->assertArrayHasKey('foo_test', $twig->getTests()); - $this->assertNotNull($twig->getExpressionParsers()->getPrefix('foo_unary')); - $this->assertNotNull($twig->getExpressionParsers()->getInfix('foo_binary')); + $this->assertNotNull($twig->getExpressionParsers()->getByName(PrefixExpressionParserInterface::class, 'foo_unary')); + $this->assertNotNull($twig->getExpressionParsers()->getByName(InfixExpressionParserInterface::class, 'foo_binary')); $this->assertArrayHasKey('foo_global', $twig->getGlobals()); $visitors = $twig->getNodeVisitors(); $found = false;