diff --git a/bin/generate_operators_precedence.php b/bin/generate_operators_precedence.php index 97cdf016d..c22c81938 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 6ce1b521b..032582fbe 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 dcc2ddd28..086fad88e 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 @@ class CacheTokenParser extends AbstractTokenParser 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 4ae0621cd..cd7919edd 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 e367835ac..46e0f3da9 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -19,6 +19,7 @@ use Twig\Error\Error; 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\Loader\LoaderInterface; 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 @@ class Environment /** * @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 ad4a05257..9922d11ec 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 @@ use Twig\Operator\Operators; * @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 @@ class ExpressionParser 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(); - } + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated, use "Parser::parseExpression()" instead.', __METHOD__); - 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; - } - - 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()); + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - 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()); - } - - // no break - default: - throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()); - } - } - - 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()); - } - - return $expr; + return $this->parseExpression(); } /** - * @deprecated since Twig 3.11, use parseSequenceExpression() instead + * @deprecated since Twig 3.20 + */ + public function parseStringExpression() + { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + + return $this->parseExpression(); + } + + /** + * @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(); - } - - 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'); - - return $node; + return $this->parseExpression(); } /** - * @deprecated since Twig 3.11, use parseMappingExpression() instead + * @deprecated since Twig 3.20 + */ + public function parseSequenceExpression() + { + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); + + return $this->parseExpression(); + } + + /** + * @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'); + trigger_deprecation('twig/twig', '3.20', 'The "%s()" method is deprecated.', __METHOD__); - $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'); - - return $node; + return $this->parseExpression(); } /** @@ -414,11 +159,13 @@ class ExpressionParser { 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 @@ class ExpressionParser { 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 @@ class ExpressionParser * * @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 @@ class ExpressionParser $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 @@ class ExpressionParser 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 @@ class ExpressionParser 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__)); + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__)); - 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; - } - } - - 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 c18904f35..bc05bfa05 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 8512bdead..86576aec4 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 000000000..0a980a8ec --- /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 000000000..b60a84810 --- /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 253b00fc7..698497b0e 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 @@ class ArrowBinaryOperator extends AbstractOperator implements BinaryOperatorInte return 250; } - public function getArity(): OperatorArity + public function getAssociativity(): InfixAssociativity { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity - { - 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 000000000..ce650b424 --- /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 7c20963db..2bb5fc92c 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 @@ class ConditionalTernaryOperator extends AbstractTernaryOperator return new ConditionalTernary($left, $then, $else, $token->getLine()); } - public function getOperator(): string + public function getName(): string { return '?'; } @@ -43,8 +49,8 @@ class ConditionalTernaryOperator extends AbstractTernaryOperator 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 2aecf0b10..d83f4bfbb 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\GetAttrExpression; 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 @@ class DotBinaryOperator extends AbstractOperator implements BinaryOperatorInterf 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 @@ class DotBinaryOperator extends AbstractOperator implements BinaryOperatorInterf return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno); } - public function getOperator(): string + public function getName(): string { return '.'; } @@ -81,13 +86,8 @@ class DotBinaryOperator extends AbstractOperator implements BinaryOperatorInterf return 300; } - public function getArity(): OperatorArity + public function getAssociativity(): InfixAssociativity { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity - { - 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 9dc633389..98e4b3b3a 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 @@ class FilterBinaryOperator extends AbstractOperator implements BinaryOperatorInt 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 @@ class FilterBinaryOperator extends AbstractOperator implements BinaryOperatorInt 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 @@ class FilterBinaryOperator extends AbstractOperator implements BinaryOperatorInt return 300; } - public function getArity(): OperatorArity + public function getAssociativity(): InfixAssociativity { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity - { - 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 740c1ce59..b1d627f7b 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 @@ class FunctionBinaryOperator extends AbstractOperator implements BinaryOperatorI $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 @@ class FunctionBinaryOperator extends AbstractOperator implements BinaryOperatorI $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 @@ class FunctionBinaryOperator extends AbstractOperator implements BinaryOperatorI return new $class($ready ? $function : $function->getName(), $args, $line); } - public function getOperator(): string + public function getName(): string { return '('; } @@ -72,13 +77,8 @@ class FunctionBinaryOperator extends AbstractOperator implements BinaryOperatorI return 300; } - public function getArity(): OperatorArity + public function getAssociativity(): InfixAssociativity { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity - { - 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 4236b769a..d63b495e2 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 @@ class IsBinaryOperator extends AbstractOperator implements BinaryOperatorInterfa return 100; } - public function getOperator(): string + public function getName(): string { return 'is'; } - public function getArity(): OperatorArity + public function getAssociativity(): InfixAssociativity { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity - { - 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 2455f7387..55c0844ce 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 c2e8b5006..1037dcb81 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 @@ class SquareBracketBinaryOperator extends AbstractOperator implements BinaryOper return new GetAttrExpression($expr, $attribute, $arguments, Template::ARRAY_CALL, $lineno); } - public function getOperator(): string + public function getName(): string { return '['; } @@ -75,13 +78,8 @@ class SquareBracketBinaryOperator extends AbstractOperator implements BinaryOper return 300; } - public function getArity(): OperatorArity + public function getAssociativity(): InfixAssociativity { - return OperatorArity::Binary; - } - - public function getAssociativity(): OperatorAssociativity - { - 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 638cdda15..3aeccce45 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 000000000..8d0ac674c --- /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 8191cb820..ac9f6c9db 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 @@ class ParenthesisUnaryOperator extends AbstractOperator implements UnaryOperator return new ListExpression($names, $token->getLine()); } - public function getOperator(): string + public function getName(): string { return '('; } @@ -66,9 +69,4 @@ class ParenthesisUnaryOperator extends AbstractOperator implements UnaryOperator { 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 000000000..92540de75 --- /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 000000000..4357d4ff6 --- /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 71c599acf..587997c51 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 02767f7c3..351fb0698 100644 --- a/src/Extension/AbstractExtension.php +++ b/src/Extension/AbstractExtension.php @@ -39,6 +39,11 @@ abstract class AbstractExtension implements LastModifiedExtensionInterface } public function getOperators() + { + return [[], []]; + } + + public function getExpressionParsers(): array { return []; } diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index 074a965f7..2b6a9f058 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -16,8 +16,53 @@ use Twig\Environment; 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\EvenTest; 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 @@ final class CoreExtension extends AbstractExtension return []; } - public function getOperators(): array + public function getExpressionParsers(): array { return [ - new NotUnaryOperator(), - new NegUnaryOperator(), - new PosUnaryOperator(), - new ParenthesisUnaryOperator(), + new UnaryOperatorExpressionParser(NotUnary::class, 'not', 50, new PrecedenceChange('twig/twig', '3.15', 70)), + new UnaryOperatorExpressionParser(NegUnary::class, '-', 500), + new UnaryOperatorExpressionParser(PosUnary::class, '+', 500), - 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 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 ConditionalTernaryOperator(), + 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 7eef100f9..44356f627 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 @@ use Twig\TwigTest; * Interface implemented by extension classes. * * @author Fabien Potencier + * + * @method array getExpressionParsers() */ interface ExtensionInterface { @@ -63,11 +66,11 @@ interface ExtensionInterface /** * 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 ad6ee7d07..262d12625 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 @@ final class ExtensionSet 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 @@ final class ExtensionSet $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 @@ final class ExtensionSet $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))); - } + // expression parsers + if (method_exists($extension, 'getExpressionParsers')) { + $this->expressionParsers->add($extension->getExpressionParsers()); + } - // new signature? - $legacy = false; - foreach ($operators as $op) { - if (!$op instanceof OperatorInterface) { - $legacy = true; + $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))); + } - break; - } - } + 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 ($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))); - } + $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)), + }; - 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) { - $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'] ?? []); } } + + 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))); + + $this->expressionParsers->add($expressionParsers); + } } - private function convertUnaryOperators(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" unary operator is deprecated.', $n)); + trigger_deprecation('twig/twig', '3.20', \sprintf('Using a non-ExpressionParserInterface object to define the "%s" binary operator is deprecated.', $operator)); - return new class($op, $n) extends AbstractUnaryOperator implements UnaryOperatorInterface { - public function __construct(private array $op, private string $operator) - { + 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 getOperator(): string + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression { - return $this->operator; - } - - public function getPrecedence(): int - { - return $this->op['precedence']; - } - - public function getPrecedenceChange(): ?OperatorPrecedenceChange - { - return $this->op['precedence_change'] ?? null; - } - - protected function getNodeClass(): string - { - return $this->op['class'] ?? ''; - } - }; - } - - private function convertBinaryOperators(string $n, array $op): OperatorInterface - { - 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; - } - - public function getPrecedence(): int - { - return $this->op['precedence']; - } - - public function getPrecedenceChange(): ?OperatorPrecedenceChange - { - 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 84b29de32..26d8fa424 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -535,25 +535,25 @@ class Lexer 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 64fc90339..000000000 --- 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 7e708c380..000000000 --- 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 69a5ad203..000000000 --- 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 1312640ae..000000000 --- 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 4d543454d..000000000 --- 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 3a88247ff..000000000 --- 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 de01a3b51..000000000 --- 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, ) { - } + trigger_deprecation('twig/twig', '3.20', 'The "%s" class is deprecated since Twig 3.20. Use "%s" instead.', self::class, PrecedenceChange::class); - public function getPackage(): string - { - return $this->package; - } - - public function getVersion(): string - { - return $this->version; - } - - public function getNewPrecedence(): int - { - return $this->newPrecedence; + parent::__construct($package, $version, $newPrecedence); } } diff --git a/src/Parser.php b/src/Parser.php index c2468cffe..1ddbae981 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 @@ class Parser $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 @@ class Parser 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 @@ class Parser 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 @@ class Parser 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 30bef15a3..8acaa6f56 100644 --- a/src/TokenParser/AbstractTokenParser.php +++ b/src/TokenParser/AbstractTokenParser.php @@ -36,8 +36,6 @@ abstract class AbstractTokenParser implements TokenParserInterface /** * 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 68ef7c17e..e4e3cfaeb 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 @@ final class ApplyTokenParser extends AbstractTokenParser $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 b50b29e65..86feb27e6 100644 --- a/src/TokenParser/AutoEscapeTokenParser.php +++ b/src/TokenParser/AutoEscapeTokenParser.php @@ -32,7 +32,7 @@ final class AutoEscapeTokenParser extends AbstractTokenParser 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 3561b99cd..452b323e5 100644 --- a/src/TokenParser/BlockTokenParser.php +++ b/src/TokenParser/BlockTokenParser.php @@ -53,7 +53,7 @@ final class BlockTokenParser extends AbstractTokenParser } } 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 164ef26ee..df1ba381f 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 @@ final class DeprecatedTokenParser extends AbstractTokenParser 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 8afd48559..ca9d03d45 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 f1acbf1ef..fa2791046 100644 --- a/src/TokenParser/EmbedTokenParser.php +++ b/src/TokenParser/EmbedTokenParser.php @@ -28,7 +28,7 @@ final class EmbedTokenParser extends IncludeTokenParser { $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 a93afe8cd..8f6469818 100644 --- a/src/TokenParser/ExtendsTokenParser.php +++ b/src/TokenParser/ExtendsTokenParser.php @@ -36,7 +36,7 @@ final class ExtendsTokenParser extends AbstractTokenParser 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 b098737fa..21166fc1f 100644 --- a/src/TokenParser/ForTokenParser.php +++ b/src/TokenParser/ForTokenParser.php @@ -37,7 +37,7 @@ final class ForTokenParser extends AbstractTokenParser $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 c8732df29..1c80a1717 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 6b9010563..4e3588e5b 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 @@ final class IfTokenParser extends AbstractTokenParser 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 f23584a5a..6dcb7662c 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 c5ce180ad..55ac1516c 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 @@ class IncludeTokenParser extends AbstractTokenParser $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 1d8577300..38e66c810 100644 --- a/src/TokenParser/MacroTokenParser.php +++ b/src/TokenParser/MacroTokenParser.php @@ -87,7 +87,7 @@ final class MacroTokenParser extends AbstractTokenParser $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 c9ebceb0b..1aabbf582 100644 --- a/src/TokenParser/SetTokenParser.php +++ b/src/TokenParser/SetTokenParser.php @@ -72,11 +72,11 @@ final class SetTokenParser extends AbstractTokenParser 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 ebd95aa31..41386c8b4 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 8ce4f02b2..83470d865 100644 --- a/src/TokenParser/WithTokenParser.php +++ b/src/TokenParser/WithTokenParser.php @@ -31,7 +31,7 @@ final class WithTokenParser extends AbstractTokenParser $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 f89a900df..174ad5e73 100644 --- a/tests/CustomExtensionTest.php +++ b/tests/CustomExtensionTest.php @@ -31,7 +31,7 @@ class CustomExtensionTest extends TestCase $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 5bc90b582..5ddf07009 100644 --- a/tests/EnvironmentTest.php +++ b/tests/EnvironmentTest.php @@ -18,6 +18,8 @@ use Twig\Cache\FilesystemCache; 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\FilesystemLoader; 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 @@ class EnvironmentTest extends TestCase $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 @@ class EnvironmentTest_Extension extends AbstractExtension implements GlobalsInte ]; } - 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 f98bade08..7d263fbf6 100644 --- a/tests/ExpressionParserTest.php +++ b/tests/ExpressionParserTest.php @@ -17,6 +17,7 @@ use Twig\Attribute\FirstClassTwigCallableReady; 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\TestExpression; 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 @@ class ExpressionParserTest extends TestCase { $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 592b1c334..f21a08616 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--