vendor/twig/twig/src/ExpressionParser.php line 830

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Twig.
  4.  *
  5.  * (c) Fabien Potencier
  6.  * (c) Armin Ronacher
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace Twig;
  12. use Twig\Attribute\FirstClassTwigCallableReady;
  13. use Twig\Error\SyntaxError;
  14. use Twig\Node\Expression\AbstractExpression;
  15. use Twig\Node\Expression\ArrayExpression;
  16. use Twig\Node\Expression\ArrowFunctionExpression;
  17. use Twig\Node\Expression\AssignNameExpression;
  18. use Twig\Node\Expression\Binary\AbstractBinary;
  19. use Twig\Node\Expression\Binary\ConcatBinary;
  20. use Twig\Node\Expression\ConditionalExpression;
  21. use Twig\Node\Expression\ConstantExpression;
  22. use Twig\Node\Expression\GetAttrExpression;
  23. use Twig\Node\Expression\MethodCallExpression;
  24. use Twig\Node\Expression\NameExpression;
  25. use Twig\Node\Expression\TestExpression;
  26. use Twig\Node\Expression\Unary\AbstractUnary;
  27. use Twig\Node\Expression\Unary\NegUnary;
  28. use Twig\Node\Expression\Unary\NotUnary;
  29. use Twig\Node\Expression\Unary\PosUnary;
  30. use Twig\Node\Node;
  31. /**
  32.  * Parses expressions.
  33.  *
  34.  * This parser implements a "Precedence climbing" algorithm.
  35.  *
  36.  * @see https://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
  37.  * @see https://en.wikipedia.org/wiki/Operator-precedence_parser
  38.  *
  39.  * @author Fabien Potencier <fabien@symfony.com>
  40.  */
  41. class ExpressionParser
  42. {
  43.     public const OPERATOR_LEFT = 1;
  44.     public const OPERATOR_RIGHT = 2;
  45.     /** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
  46.     private $unaryOperators;
  47.     /** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: self::OPERATOR_*}> */
  48.     private $binaryOperators;
  49.     private $readyNodes = [];
  50.     public function __construct(
  51.         private Parser $parser,
  52.         private Environment $env,
  53.     ) {
  54.         $this->unaryOperators = $env->getUnaryOperators();
  55.         $this->binaryOperators = $env->getBinaryOperators();
  56.     }
  57.     public function parseExpression($precedence = 0, $allowArrow = false)
  58.     {
  59.         if ($allowArrow && $arrow = $this->parseArrow()) {
  60.             return $arrow;
  61.         }
  62.         $expr = $this->getPrimary();
  63.         $token = $this->parser->getCurrentToken();
  64.         while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) {
  65.             $op = $this->binaryOperators[$token->getValue()];
  66.             $this->parser->getStream()->next();
  67.             if ('is not' === $token->getValue()) {
  68.                 $expr = $this->parseNotTestExpression($expr);
  69.             } elseif ('is' === $token->getValue()) {
  70.                 $expr = $this->parseTestExpression($expr);
  71.             } elseif (isset($op['callable'])) {
  72.                 $expr = $op['callable']($this->parser, $expr);
  73.             } else {
  74.                 $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence'], true);
  75.                 $class = $op['class'];
  76.                 $expr = new $class($expr, $expr1, $token->getLine());
  77.             }
  78.             $token = $this->parser->getCurrentToken();
  79.         }
  80.         if (0 === $precedence) {
  81.             return $this->parseConditionalExpression($expr);
  82.         }
  83.         return $expr;
  84.     }
  85.     /**
  86.      * @return ArrowFunctionExpression|null
  87.      */
  88.     private function parseArrow()
  89.     {
  90.         $stream = $this->parser->getStream();
  91.         // short array syntax (one argument, no parentheses)?
  92.         if ($stream->look(1)->test(Token::ARROW_TYPE)) {
  93.             $line = $stream->getCurrent()->getLine();
  94.             $token = $stream->expect(Token::NAME_TYPE);
  95.             $names = [new AssignNameExpression($token->getValue(), $token->getLine())];
  96.             $stream->expect(Token::ARROW_TYPE);
  97.             return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
  98.         }
  99.         // first, determine if we are parsing an arrow function by finding => (long form)
  100.         $i = 0;
  101.         if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) {
  102.             return null;
  103.         }
  104.         ++$i;
  105.         while (true) {
  106.             // variable name
  107.             ++$i;
  108.             if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) {
  109.                 break;
  110.             }
  111.             ++$i;
  112.         }
  113.         if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) {
  114.             return null;
  115.         }
  116.         ++$i;
  117.         if (!$stream->look($i)->test(Token::ARROW_TYPE)) {
  118.             return null;
  119.         }
  120.         // yes, let's parse it properly
  121.         $token = $stream->expect(Token::PUNCTUATION_TYPE, '(');
  122.         $line = $token->getLine();
  123.         $names = [];
  124.         while (true) {
  125.             $token = $stream->expect(Token::NAME_TYPE);
  126.             $names[] = new AssignNameExpression($token->getValue(), $token->getLine());
  127.             if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
  128.                 break;
  129.             }
  130.         }
  131.         $stream->expect(Token::PUNCTUATION_TYPE, ')');
  132.         $stream->expect(Token::ARROW_TYPE);
  133.         return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
  134.     }
  135.     private function getPrimary(): AbstractExpression
  136.     {
  137.         $token = $this->parser->getCurrentToken();
  138.         if ($this->isUnary($token)) {
  139.             $operator = $this->unaryOperators[$token->getValue()];
  140.             $this->parser->getStream()->next();
  141.             $expr = $this->parseExpression($operator['precedence']);
  142.             $class = $operator['class'];
  143.             return $this->parsePostfixExpression(new $class($expr, $token->getLine()));
  144.         } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) {
  145.             $this->parser->getStream()->next();
  146.             $expr = $this->parseExpression();
  147.             $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');
  148.             return $this->parsePostfixExpression($expr);
  149.         }
  150.         return $this->parsePrimaryExpression();
  151.     }
  152.     private function parseConditionalExpression($expr): AbstractExpression
  153.     {
  154.         while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) {
  155.             if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
  156.                 $expr2 = $this->parseExpression();
  157.                 if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
  158.                     // Ternary operator (expr ? expr2 : expr3)
  159.                     $expr3 = $this->parseExpression();
  160.                 } else {
  161.                     // Ternary without else (expr ? expr2)
  162.                     $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine());
  163.                 }
  164.             } else {
  165.                 // Ternary without then (expr ?: expr3)
  166.                 $expr2 = $expr;
  167.                 $expr3 = $this->parseExpression();
  168.             }
  169.             $expr = new ConditionalExpression($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
  170.         }
  171.         return $expr;
  172.     }
  173.     private function isUnary(Token $token): bool
  174.     {
  175.         return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
  176.     }
  177.     private function isBinary(Token $token): bool
  178.     {
  179.         return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]);
  180.     }
  181.     public function parsePrimaryExpression()
  182.     {
  183.         $token = $this->parser->getCurrentToken();
  184.         switch ($token->getType()) {
  185.             case Token::NAME_TYPE:
  186.                 $this->parser->getStream()->next();
  187.                 switch ($token->getValue()) {
  188.                     case 'true':
  189.                     case 'TRUE':
  190.                         $node = new ConstantExpression(true, $token->getLine());
  191.                         break;
  192.                     case 'false':
  193.                     case 'FALSE':
  194.                         $node = new ConstantExpression(false, $token->getLine());
  195.                         break;
  196.                     case 'none':
  197.                     case 'NONE':
  198.                     case 'null':
  199.                     case 'NULL':
  200.                         $node = new ConstantExpression(null, $token->getLine());
  201.                         break;
  202.                     default:
  203.                         if ('(' === $this->parser->getCurrentToken()->getValue()) {
  204.                             $node = $this->getFunctionNode($token->getValue(), $token->getLine());
  205.                         } else {
  206.                             $node = new NameExpression($token->getValue(), $token->getLine());
  207.                         }
  208.                 }
  209.                 break;
  210.             case Token::NUMBER_TYPE:
  211.                 $this->parser->getStream()->next();
  212.                 $node = new ConstantExpression($token->getValue(), $token->getLine());
  213.                 break;
  214.             case Token::STRING_TYPE:
  215.             case Token::INTERPOLATION_START_TYPE:
  216.                 $node = $this->parseStringExpression();
  217.                 break;
  218.             case Token::OPERATOR_TYPE:
  219.                 if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
  220.                     // in this context, string operators are variable names
  221.                     $this->parser->getStream()->next();
  222.                     $node = new NameExpression($token->getValue(), $token->getLine());
  223.                     break;
  224.                 }
  225.                 if (isset($this->unaryOperators[$token->getValue()])) {
  226.                     $class = $this->unaryOperators[$token->getValue()]['class'];
  227.                     if (!\in_array($class, [NegUnary::class, PosUnary::class])) {
  228.                         throw new SyntaxError(\sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
  229.                     }
  230.                     $this->parser->getStream()->next();
  231.                     $expr = $this->parsePrimaryExpression();
  232.                     $node = new $class($expr, $token->getLine());
  233.                     break;
  234.                 }
  235.                 // no break
  236.             default:
  237.                 if ($token->test(Token::PUNCTUATION_TYPE, '[')) {
  238.                     $node = $this->parseSequenceExpression();
  239.                 } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) {
  240.                     $node = $this->parseMappingExpression();
  241.                 } elseif ($token->test(Token::OPERATOR_TYPE, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
  242.                     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());
  243.                 } else {
  244.                     throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
  245.                 }
  246.         }
  247.         return $this->parsePostfixExpression($node);
  248.     }
  249.     public function parseStringExpression()
  250.     {
  251.         $stream = $this->parser->getStream();
  252.         $nodes = [];
  253.         // a string cannot be followed by another string in a single expression
  254.         $nextCanBeString = true;
  255.         while (true) {
  256.             if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) {
  257.                 $nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
  258.                 $nextCanBeString = false;
  259.             } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) {
  260.                 $nodes[] = $this->parseExpression();
  261.                 $stream->expect(Token::INTERPOLATION_END_TYPE);
  262.                 $nextCanBeString = true;
  263.             } else {
  264.                 break;
  265.             }
  266.         }
  267.         $expr = array_shift($nodes);
  268.         foreach ($nodes as $node) {
  269.             $expr = new ConcatBinary($expr, $node, $node->getTemplateLine());
  270.         }
  271.         return $expr;
  272.     }
  273.     /**
  274.      * @deprecated since 3.11, use parseSequenceExpression() instead
  275.      */
  276.     public function parseArrayExpression()
  277.     {
  278.         trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.', __METHOD__);
  279.         return $this->parseSequenceExpression();
  280.     }
  281.     public function parseSequenceExpression()
  282.     {
  283.         $stream = $this->parser->getStream();
  284.         $stream->expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected');
  285.         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
  286.         $first = true;
  287.         while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) {
  288.             if (!$first) {
  289.                 $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma');
  290.                 // trailing ,?
  291.                 if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
  292.                     break;
  293.                 }
  294.             }
  295.             $first = false;
  296.             if ($stream->test(Token::SPREAD_TYPE)) {
  297.                 $stream->next();
  298.                 $expr = $this->parseExpression();
  299.                 $expr->setAttribute('spread', true);
  300.                 $node->addElement($expr);
  301.             } else {
  302.                 $node->addElement($this->parseExpression());
  303.             }
  304.         }
  305.         $stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed');
  306.         return $node;
  307.     }
  308.     /**
  309.      * @deprecated since 3.11, use parseMappingExpression() instead
  310.      */
  311.     public function parseHashExpression()
  312.     {
  313.         trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseMappingExpression()" instead.', __METHOD__);
  314.         return $this->parseMappingExpression();
  315.     }
  316.     public function parseMappingExpression()
  317.     {
  318.         $stream = $this->parser->getStream();
  319.         $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected');
  320.         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
  321.         $first = true;
  322.         while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) {
  323.             if (!$first) {
  324.                 $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma');
  325.                 // trailing ,?
  326.                 if ($stream->test(Token::PUNCTUATION_TYPE, '}')) {
  327.                     break;
  328.                 }
  329.             }
  330.             $first = false;
  331.             if ($stream->test(Token::SPREAD_TYPE)) {
  332.                 $stream->next();
  333.                 $value = $this->parseExpression();
  334.                 $value->setAttribute('spread', true);
  335.                 $node->addElement($value);
  336.                 continue;
  337.             }
  338.             // a mapping key can be:
  339.             //
  340.             //  * a number -- 12
  341.             //  * a string -- 'a'
  342.             //  * a name, which is equivalent to a string -- a
  343.             //  * an expression, which must be enclosed in parentheses -- (1 + 2)
  344.             if ($token = $stream->nextIf(Token::NAME_TYPE)) {
  345.                 $key = new ConstantExpression($token->getValue(), $token->getLine());
  346.                 // {a} is a shortcut for {a:a}
  347.                 if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) {
  348.                     $value = new NameExpression($key->getAttribute('value'), $key->getTemplateLine());
  349.                     $node->addElement($value, $key);
  350.                     continue;
  351.                 }
  352.             } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) {
  353.                 $key = new ConstantExpression($token->getValue(), $token->getLine());
  354.             } elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
  355.                 $key = $this->parseExpression();
  356.             } else {
  357.                 $current = $stream->getCurrent();
  358.                 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".', Token::typeToEnglish($current->getType()), $current->getValue()), $current->getLine(), $stream->getSourceContext());
  359.             }
  360.             $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)');
  361.             $value = $this->parseExpression();
  362.             $node->addElement($value, $key);
  363.         }
  364.         $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed');
  365.         return $node;
  366.     }
  367.     public function parsePostfixExpression($node)
  368.     {
  369.         while (true) {
  370.             $token = $this->parser->getCurrentToken();
  371.             if (Token::PUNCTUATION_TYPE == $token->getType()) {
  372.                 if ('.' == $token->getValue() || '[' == $token->getValue()) {
  373.                     $node = $this->parseSubscriptExpression($node);
  374.                 } elseif ('|' == $token->getValue()) {
  375.                     $node = $this->parseFilterExpression($node);
  376.                 } else {
  377.                     break;
  378.                 }
  379.             } else {
  380.                 break;
  381.             }
  382.         }
  383.         return $node;
  384.     }
  385.     public function getFunctionNode($name, $line)
  386.     {
  387.         if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
  388.             $arguments = new ArrayExpression([], $line);
  389.             foreach ($this->parseArguments() as $n) {
  390.                 $arguments->addElement($n);
  391.             }
  392.             $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
  393.             $node->setAttribute('safe', true);
  394.             return $node;
  395.         }
  396.         $args = $this->parseArguments(true);
  397.         $function = $this->getFunction($name, $line);
  398.         if ($function->getParserCallable()) {
  399.             $fakeNode = new Node(lineno: $line);
  400.             $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext());
  401.             return ($function->getParserCallable())($this->parser, $fakeNode, $args, $line);
  402.         }
  403.         if (!isset($this->readyNodes[$class = $function->getNodeClass()])) {
  404.             $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
  405.         }
  406.         if (!$ready = $this->readyNodes[$class]) {
  407.             trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
  408.         }
  409.         return new $class($ready ? $function : $function->getName(), $args, $line);
  410.     }
  411.     public function parseSubscriptExpression($node)
  412.     {
  413.         $stream = $this->parser->getStream();
  414.         $token = $stream->next();
  415.         $lineno = $token->getLine();
  416.         $arguments = new ArrayExpression([], $lineno);
  417.         $type = Template::ANY_CALL;
  418.         if ('.' == $token->getValue()) {
  419.             $token = $stream->next();
  420.             if (
  421.                 Token::NAME_TYPE == $token->getType()
  422.                 ||
  423.                 Token::NUMBER_TYPE == $token->getType()
  424.                 ||
  425.                 (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
  426.             ) {
  427.                 $arg = new ConstantExpression($token->getValue(), $lineno);
  428.                 if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
  429.                     $type = Template::METHOD_CALL;
  430.                     foreach ($this->parseArguments() as $n) {
  431.                         $arguments->addElement($n);
  432.                     }
  433.                 }
  434.             } else {
  435.                 throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
  436.             }
  437.             if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
  438.                 $name = $arg->getAttribute('value');
  439.                 $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno);
  440.                 $node->setAttribute('safe', true);
  441.                 return $node;
  442.             }
  443.         } else {
  444.             $type = Template::ARRAY_CALL;
  445.             // slice?
  446.             $slice = false;
  447.             if ($stream->test(Token::PUNCTUATION_TYPE, ':')) {
  448.                 $slice = true;
  449.                 $arg = new ConstantExpression(0, $token->getLine());
  450.             } else {
  451.                 $arg = $this->parseExpression();
  452.             }
  453.             if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) {
  454.                 $slice = true;
  455.             }
  456.             if ($slice) {
  457.                 if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
  458.                     $length = new ConstantExpression(null, $token->getLine());
  459.                 } else {
  460.                     $length = $this->parseExpression();
  461.                 }
  462.                 $filter = $this->getFilter('slice', $token->getLine());
  463.                 $arguments = new Node([$arg, $length]);
  464.                 $filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine());
  465.                 $stream->expect(Token::PUNCTUATION_TYPE, ']');
  466.                 return $filter;
  467.             }
  468.             $stream->expect(Token::PUNCTUATION_TYPE, ']');
  469.         }
  470.         return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
  471.     }
  472.     public function parseFilterExpression($node)
  473.     {
  474.         $this->parser->getStream()->next();
  475.         return $this->parseFilterExpressionRaw($node);
  476.     }
  477.     public function parseFilterExpressionRaw($node)
  478.     {
  479.         if (\func_num_args() > 1) {
  480.             trigger_deprecation('twig/twig', '3.12', 'Passing a second argument to "%s()" is deprecated.', __METHOD__);
  481.         }
  482.         while (true) {
  483.             $token = $this->parser->getStream()->expect(Token::NAME_TYPE);
  484.             if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
  485.                 $arguments = new Node();
  486.             } else {
  487.                 $arguments = $this->parseArguments(true, false, true);
  488.             }
  489.             $filter = $this->getFilter($token->getValue(), $token->getLine());
  490.             $ready = true;
  491.             if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) {
  492.                 $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
  493.             }
  494.             if (!$ready = $this->readyNodes[$class]) {
  495.                 trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
  496.             }
  497.             $node = new $class($node, $ready ? $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments, $token->getLine());
  498.             if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) {
  499.                 break;
  500.             }
  501.             $this->parser->getStream()->next();
  502.         }
  503.         return $node;
  504.     }
  505.     /**
  506.      * Parses arguments.
  507.      *
  508.      * @param bool $namedArguments Whether to allow named arguments or not
  509.      * @param bool $definition     Whether we are parsing arguments for a function (or macro) definition
  510.      *
  511.      * @return Node
  512.      *
  513.      * @throws SyntaxError
  514.      */
  515.     public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false)
  516.     {
  517.         $args = [];
  518.         $stream = $this->parser->getStream();
  519.         $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
  520.         while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
  521.             if (!empty($args)) {
  522.                 $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
  523.                 // if the comma above was a trailing comma, early exit the argument parse loop
  524.                 if ($stream->test(Token::PUNCTUATION_TYPE, ')')) {
  525.                     break;
  526.                 }
  527.             }
  528.             if ($definition) {
  529.                 $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
  530.                 $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
  531.             } else {
  532.                 $value = $this->parseExpression(0, $allowArrow);
  533.             }
  534.             $name = null;
  535.             if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) {
  536.                 if (!$value instanceof NameExpression) {
  537.                     throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext());
  538.                 }
  539.                 $name = $value->getAttribute('name');
  540.                 if ($definition) {
  541.                     $value = $this->parsePrimaryExpression();
  542.                     if (!$this->checkConstantExpression($value)) {
  543.                         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());
  544.                     }
  545.                 } else {
  546.                     $value = $this->parseExpression(0, $allowArrow);
  547.                 }
  548.             }
  549.             if ($definition) {
  550.                 if (null === $name) {
  551.                     $name = $value->getAttribute('name');
  552.                     $value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());
  553.                     $value->setAttribute('is_implicit', true);
  554.                 }
  555.                 $args[$name] = $value;
  556.             } else {
  557.                 if (null === $name) {
  558.                     $args[] = $value;
  559.                 } else {
  560.                     $args[$name] = $value;
  561.                 }
  562.             }
  563.         }
  564.         $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
  565.         return new Node($args);
  566.     }
  567.     public function parseAssignmentExpression()
  568.     {
  569.         $stream = $this->parser->getStream();
  570.         $targets = [];
  571.         while (true) {
  572.             $token = $this->parser->getCurrentToken();
  573.             if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
  574.                 // in this context, string operators are variable names
  575.                 $this->parser->getStream()->next();
  576.             } else {
  577.                 $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to');
  578.             }
  579.             $value = $token->getValue();
  580.             if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) {
  581.                 throw new SyntaxError(\sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
  582.             }
  583.             $targets[] = new AssignNameExpression($value, $token->getLine());
  584.             if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
  585.                 break;
  586.             }
  587.         }
  588.         return new Node($targets);
  589.     }
  590.     public function parseMultitargetExpression()
  591.     {
  592.         $targets = [];
  593.         while (true) {
  594.             $targets[] = $this->parseExpression();
  595.             if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) {
  596.                 break;
  597.             }
  598.         }
  599.         return new Node($targets);
  600.     }
  601.     private function parseNotTestExpression(Node $node): NotUnary
  602.     {
  603.         return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine());
  604.     }
  605.     private function parseTestExpression(Node $node): TestExpression
  606.     {
  607.         $stream = $this->parser->getStream();
  608.         $test = $this->getTest($node->getTemplateLine());
  609.         $arguments = null;
  610.         if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
  611.             $arguments = $this->parseArguments(true);
  612.         } elseif ($test->hasOneMandatoryArgument()) {
  613.             $arguments = new Node([0 => $this->parsePrimaryExpression()]);
  614.         }
  615.         if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
  616.             $node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
  617.             $node->setAttribute('safe', true);
  618.         }
  619.         $ready = $test instanceof TwigTest;
  620.         if (!isset($this->readyNodes[$class = $test->getNodeClass()])) {
  621.             $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
  622.         }
  623.         if (!$ready = $this->readyNodes[$class]) {
  624.             trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class);
  625.         }
  626.         return new $class($node, $ready ? $test : $test->getName(), $arguments, $this->parser->getCurrentToken()->getLine());
  627.     }
  628.     private function getTest(int $line): TwigTest
  629.     {
  630.         $stream = $this->parser->getStream();
  631.         $name = $stream->expect(Token::NAME_TYPE)->getValue();
  632.         if ($stream->test(Token::NAME_TYPE)) {
  633.             // try 2-words tests
  634.             $name = $name.' '.$this->parser->getCurrentToken()->getValue();
  635.             if ($test = $this->env->getTest($name)) {
  636.                 $stream->next();
  637.             }
  638.         } else {
  639.             $test = $this->env->getTest($name);
  640.         }
  641.         if (!$test) {
  642.             $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
  643.             $e->addSuggestions($name, array_keys($this->env->getTests()));
  644.             throw $e;
  645.         }
  646.         if ($test->isDeprecated()) {
  647.             $stream = $this->parser->getStream();
  648.             $message = \sprintf('Twig Test "%s" is deprecated', $test->getName());
  649.             if ($test->getAlternative()) {
  650.                 $message .= \sprintf('. Use "%s" instead', $test->getAlternative());
  651.             }
  652.             $src = $stream->getSourceContext();
  653.             $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
  654.             trigger_deprecation($test->getDeprecatingPackage(), $test->getDeprecatedVersion(), $message);
  655.         }
  656.         return $test;
  657.     }
  658.     private function getFunction(string $name, int $line): TwigFunction
  659.     {
  660.         if (!$function = $this->env->getFunction($name)) {
  661.             $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
  662.             $e->addSuggestions($name, array_keys($this->env->getFunctions()));
  663.             throw $e;
  664.         }
  665.         if ($function->isDeprecated()) {
  666.             $message = \sprintf('Twig Function "%s" is deprecated', $function->getName());
  667.             if ($function->getAlternative()) {
  668.                 $message .= \sprintf('. Use "%s" instead', $function->getAlternative());
  669.             }
  670.             $src = $this->parser->getStream()->getSourceContext();
  671.             $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
  672.             trigger_deprecation($function->getDeprecatingPackage(), $function->getDeprecatedVersion(), $message);
  673.         }
  674.         return $function;
  675.     }
  676.     private function getFilter(string $name, int $line): TwigFilter
  677.     {
  678.         if (!$filter = $this->env->getFilter($name)) {
  679.             $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
  680.             $e->addSuggestions($name, array_keys($this->env->getFilters()));
  681.             throw $e;
  682.         }
  683.         if ($filter->isDeprecated()) {
  684.             $message = \sprintf('Twig Filter "%s" is deprecated', $filter->getName());
  685.             if ($filter->getAlternative()) {
  686.                 $message .= \sprintf('. Use "%s" instead', $filter->getAlternative());
  687.             }
  688.             $src = $this->parser->getStream()->getSourceContext();
  689.             $message .= \sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
  690.             trigger_deprecation($filter->getDeprecatingPackage(), $filter->getDeprecatedVersion(), $message);
  691.         }
  692.         return $filter;
  693.     }
  694.     // checks that the node only contains "constant" elements
  695.     private function checkConstantExpression(Node $node): bool
  696.     {
  697.         if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
  698.             || $node instanceof NegUnary || $node instanceof PosUnary
  699.         )) {
  700.             return false;
  701.         }
  702.         foreach ($node as $n) {
  703.             if (!$this->checkConstantExpression($n)) {
  704.                 return false;
  705.             }
  706.         }
  707.         return true;
  708.     }
  709. }