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

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\Error\SyntaxError;
  13. use Twig\Node\Expression\AbstractExpression;
  14. use Twig\Node\Expression\ArrayExpression;
  15. use Twig\Node\Expression\ArrowFunctionExpression;
  16. use Twig\Node\Expression\AssignNameExpression;
  17. use Twig\Node\Expression\Binary\ConcatBinary;
  18. use Twig\Node\Expression\BlockReferenceExpression;
  19. use Twig\Node\Expression\ConditionalExpression;
  20. use Twig\Node\Expression\ConstantExpression;
  21. use Twig\Node\Expression\GetAttrExpression;
  22. use Twig\Node\Expression\MethodCallExpression;
  23. use Twig\Node\Expression\NameExpression;
  24. use Twig\Node\Expression\ParentExpression;
  25. use Twig\Node\Expression\TestExpression;
  26. use Twig\Node\Expression\Unary\NegUnary;
  27. use Twig\Node\Expression\Unary\NotUnary;
  28. use Twig\Node\Expression\Unary\PosUnary;
  29. use Twig\Node\Node;
  30. /**
  31.  * Parses expressions.
  32.  *
  33.  * This parser implements a "Precedence climbing" algorithm.
  34.  *
  35.  * @see https://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
  36.  * @see https://en.wikipedia.org/wiki/Operator-precedence_parser
  37.  *
  38.  * @author Fabien Potencier <fabien@symfony.com>
  39.  *
  40.  * @internal
  41.  */
  42. class ExpressionParser
  43. {
  44.     const OPERATOR_LEFT = 1;
  45.     const OPERATOR_RIGHT = 2;
  46.     private $parser;
  47.     private $env;
  48.     private $unaryOperators;
  49.     private $binaryOperators;
  50.     public function __construct(Parser $parser, Environment $env)
  51.     {
  52.         $this->parser = $parser;
  53.         $this->env = $env;
  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']);
  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 */ 12)) {
  93.             $line = $stream->getCurrent()->getLine();
  94.             $token = $stream->expect(/* Token::NAME_TYPE */ 5);
  95.             $names = [new AssignNameExpression($token->getValue(), $token->getLine())];
  96.             $stream->expect(/* Token::ARROW_TYPE */ 12);
  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 */ 9, '(')) {
  102.             return null;
  103.         }
  104.         ++$i;
  105.         while (true) {
  106.             // variable name
  107.             ++$i;
  108.             if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
  109.                 break;
  110.             }
  111.             ++$i;
  112.         }
  113.         if (!$stream->look($i)->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
  114.             return null;
  115.         }
  116.         ++$i;
  117.         if (!$stream->look($i)->test(/* Token::ARROW_TYPE */ 12)) {
  118.             return null;
  119.         }
  120.         // yes, let's parse it properly
  121.         $token = $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(');
  122.         $line = $token->getLine();
  123.         $names = [];
  124.         while (true) {
  125.             $token = $stream->expect(/* Token::NAME_TYPE */ 5);
  126.             $names[] = new AssignNameExpression($token->getValue(), $token->getLine());
  127.             if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
  128.                 break;
  129.             }
  130.         }
  131.         $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')');
  132.         $stream->expect(/* Token::ARROW_TYPE */ 12);
  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 */ 9, '(')) {
  145.             $this->parser->getStream()->next();
  146.             $expr = $this->parseExpression();
  147.             $this->parser->getStream()->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', '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 */ 9, '?')) {
  155.             if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
  156.                 $expr2 = $this->parseExpression();
  157.                 if ($this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
  158.                     $expr3 = $this->parseExpression();
  159.                 } else {
  160.                     $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine());
  161.                 }
  162.             } else {
  163.                 $expr2 = $expr;
  164.                 $expr3 = $this->parseExpression();
  165.             }
  166.             $expr = new ConditionalExpression($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
  167.         }
  168.         return $expr;
  169.     }
  170.     private function isUnary(Token $token): bool
  171.     {
  172.         return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->unaryOperators[$token->getValue()]);
  173.     }
  174.     private function isBinary(Token $token): bool
  175.     {
  176.         return $token->test(/* Token::OPERATOR_TYPE */ 8) && isset($this->binaryOperators[$token->getValue()]);
  177.     }
  178.     public function parsePrimaryExpression()
  179.     {
  180.         $token = $this->parser->getCurrentToken();
  181.         switch ($token->getType()) {
  182.             case /* Token::NAME_TYPE */ 5:
  183.                 $this->parser->getStream()->next();
  184.                 switch ($token->getValue()) {
  185.                     case 'true':
  186.                     case 'TRUE':
  187.                         $node = new ConstantExpression(true, $token->getLine());
  188.                         break;
  189.                     case 'false':
  190.                     case 'FALSE':
  191.                         $node = new ConstantExpression(false, $token->getLine());
  192.                         break;
  193.                     case 'none':
  194.                     case 'NONE':
  195.                     case 'null':
  196.                     case 'NULL':
  197.                         $node = new ConstantExpression(null, $token->getLine());
  198.                         break;
  199.                     default:
  200.                         if ('(' === $this->parser->getCurrentToken()->getValue()) {
  201.                             $node = $this->getFunctionNode($token->getValue(), $token->getLine());
  202.                         } else {
  203.                             $node = new NameExpression($token->getValue(), $token->getLine());
  204.                         }
  205.                 }
  206.                 break;
  207.             case /* Token::NUMBER_TYPE */ 6:
  208.                 $this->parser->getStream()->next();
  209.                 $node = new ConstantExpression($token->getValue(), $token->getLine());
  210.                 break;
  211.             case /* Token::STRING_TYPE */ 7:
  212.             case /* Token::INTERPOLATION_START_TYPE */ 10:
  213.                 $node = $this->parseStringExpression();
  214.                 break;
  215.             case /* Token::OPERATOR_TYPE */ 8:
  216.                 if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
  217.                     // in this context, string operators are variable names
  218.                     $this->parser->getStream()->next();
  219.                     $node = new NameExpression($token->getValue(), $token->getLine());
  220.                     break;
  221.                 } elseif (isset($this->unaryOperators[$token->getValue()])) {
  222.                     $class = $this->unaryOperators[$token->getValue()]['class'];
  223.                     $ref = new \ReflectionClass($class);
  224.                     if (!(\in_array($ref->getName(), [NegUnary::class, PosUnary::class, 'Twig_Node_Expression_Unary_Neg', 'Twig_Node_Expression_Unary_Pos'])
  225.                         || $ref->isSubclassOf(NegUnary::class) || $ref->isSubclassOf(PosUnary::class)
  226.                         || $ref->isSubclassOf('Twig_Node_Expression_Unary_Neg') || $ref->isSubclassOf('Twig_Node_Expression_Unary_Pos'))
  227.                     ) {
  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 */ 9, '[')) {
  238.                     $node = $this->parseArrayExpression();
  239.                 } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) {
  240.                     $node = $this->parseHashExpression();
  241.                 } elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $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 */ 7)) {
  257.                 $nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
  258.                 $nextCanBeString = false;
  259.             } elseif ($stream->nextIf(/* Token::INTERPOLATION_START_TYPE */ 10)) {
  260.                 $nodes[] = $this->parseExpression();
  261.                 $stream->expect(/* Token::INTERPOLATION_END_TYPE */ 11);
  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.     public function parseArrayExpression()
  274.     {
  275.         $stream = $this->parser->getStream();
  276.         $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'An array element was expected');
  277.         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
  278.         $first = true;
  279.         while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
  280.             if (!$first) {
  281.                 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'An array element must be followed by a comma');
  282.                 // trailing ,?
  283.                 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
  284.                     break;
  285.                 }
  286.             }
  287.             $first = false;
  288.             $node->addElement($this->parseExpression());
  289.         }
  290.         $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened array is not properly closed');
  291.         return $node;
  292.     }
  293.     public function parseHashExpression()
  294.     {
  295.         $stream = $this->parser->getStream();
  296.         $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A hash element was expected');
  297.         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
  298.         $first = true;
  299.         while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
  300.             if (!$first) {
  301.                 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A hash value must be followed by a comma');
  302.                 // trailing ,?
  303.                 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
  304.                     break;
  305.                 }
  306.             }
  307.             $first = false;
  308.             // a hash key can be:
  309.             //
  310.             //  * a number -- 12
  311.             //  * a string -- 'a'
  312.             //  * a name, which is equivalent to a string -- a
  313.             //  * an expression, which must be enclosed in parentheses -- (1 + 2)
  314.             if (($token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) || ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) || $token = $stream->nextIf(/* Token::NUMBER_TYPE */ 6)) {
  315.                 $key = new ConstantExpression($token->getValue(), $token->getLine());
  316.             } elseif ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
  317.                 $key = $this->parseExpression();
  318.             } else {
  319.                 $current = $stream->getCurrent();
  320.                 throw new SyntaxError(sprintf('A hash 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());
  321.             }
  322.             $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A hash key must be followed by a colon (:)');
  323.             $value = $this->parseExpression();
  324.             $node->addElement($value, $key);
  325.         }
  326.         $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened hash is not properly closed');
  327.         return $node;
  328.     }
  329.     public function parsePostfixExpression($node)
  330.     {
  331.         while (true) {
  332.             $token = $this->parser->getCurrentToken();
  333.             if (/* Token::PUNCTUATION_TYPE */ 9 == $token->getType()) {
  334.                 if ('.' == $token->getValue() || '[' == $token->getValue()) {
  335.                     $node = $this->parseSubscriptExpression($node);
  336.                 } elseif ('|' == $token->getValue()) {
  337.                     $node = $this->parseFilterExpression($node);
  338.                 } else {
  339.                     break;
  340.                 }
  341.             } else {
  342.                 break;
  343.             }
  344.         }
  345.         return $node;
  346.     }
  347.     public function getFunctionNode($name, $line)
  348.     {
  349.         switch ($name) {
  350.             case 'parent':
  351.                 $this->parseArguments();
  352.                 if (!\count($this->parser->getBlockStack())) {
  353.                     throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext());
  354.                 }
  355.                 if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
  356.                     throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext());
  357.                 }
  358.                 return new ParentExpression($this->parser->peekBlockStack(), $line);
  359.             case 'block':
  360.                 $args = $this->parseArguments();
  361.                 if (\count($args) < 1) {
  362.                     throw new SyntaxError('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext());
  363.                 }
  364.                 return new BlockReferenceExpression($args->getNode(0), \count($args) > 1 ? $args->getNode(1) : null, $line);
  365.             case 'attribute':
  366.                 $args = $this->parseArguments();
  367.                 if (\count($args) < 2) {
  368.                     throw new SyntaxError('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext());
  369.                 }
  370.                 return new GetAttrExpression($args->getNode(0), $args->getNode(1), \count($args) > 2 ? $args->getNode(2) : null, Template::ANY_CALL, $line);
  371.             default:
  372.                 if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
  373.                     $arguments = new ArrayExpression([], $line);
  374.                     foreach ($this->parseArguments() as $n) {
  375.                         $arguments->addElement($n);
  376.                     }
  377.                     $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
  378.                     $node->setAttribute('safe', true);
  379.                     return $node;
  380.                 }
  381.                 $args = $this->parseArguments(true);
  382.                 $class = $this->getFunctionNodeClass($name, $line);
  383.                 return new $class($name, $args, $line);
  384.         }
  385.     }
  386.     public function parseSubscriptExpression($node)
  387.     {
  388.         $stream = $this->parser->getStream();
  389.         $token = $stream->next();
  390.         $lineno = $token->getLine();
  391.         $arguments = new ArrayExpression([], $lineno);
  392.         $type = Template::ANY_CALL;
  393.         if ('.' == $token->getValue()) {
  394.             $token = $stream->next();
  395.             if (
  396.                 /* Token::NAME_TYPE */ 5 == $token->getType()
  397.                 ||
  398.                 /* Token::NUMBER_TYPE */ 6 == $token->getType()
  399.                 ||
  400.                 (/* Token::OPERATOR_TYPE */ 8 == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
  401.             ) {
  402.                 $arg = new ConstantExpression($token->getValue(), $lineno);
  403.                 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
  404.                     $type = Template::METHOD_CALL;
  405.                     foreach ($this->parseArguments() as $n) {
  406.                         $arguments->addElement($n);
  407.                     }
  408.                 }
  409.             } else {
  410.                 throw new SyntaxError('Expected name or number.', $lineno, $stream->getSourceContext());
  411.             }
  412.             if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
  413.                 if (!$arg instanceof ConstantExpression) {
  414.                     throw new SyntaxError(sprintf('Dynamic macro names are not supported (called on "%s").', $node->getAttribute('name')), $token->getLine(), $stream->getSourceContext());
  415.                 }
  416.                 $name = $arg->getAttribute('value');
  417.                 $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno);
  418.                 $node->setAttribute('safe', true);
  419.                 return $node;
  420.             }
  421.         } else {
  422.             $type = Template::ARRAY_CALL;
  423.             // slice?
  424.             $slice = false;
  425.             if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
  426.                 $slice = true;
  427.                 $arg = new ConstantExpression(0, $token->getLine());
  428.             } else {
  429.                 $arg = $this->parseExpression();
  430.             }
  431.             if ($stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
  432.                 $slice = true;
  433.             }
  434.             if ($slice) {
  435.                 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
  436.                     $length = new ConstantExpression(null, $token->getLine());
  437.                 } else {
  438.                     $length = $this->parseExpression();
  439.                 }
  440.                 $class = $this->getFilterNodeClass('slice', $token->getLine());
  441.                 $arguments = new Node([$arg, $length]);
  442.                 $filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine());
  443.                 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
  444.                 return $filter;
  445.             }
  446.             $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
  447.         }
  448.         return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
  449.     }
  450.     public function parseFilterExpression($node)
  451.     {
  452.         $this->parser->getStream()->next();
  453.         return $this->parseFilterExpressionRaw($node);
  454.     }
  455.     public function parseFilterExpressionRaw($node, $tag = null)
  456.     {
  457.         while (true) {
  458.             $token = $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5);
  459.             $name = new ConstantExpression($token->getValue(), $token->getLine());
  460.             if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
  461.                 $arguments = new Node();
  462.             } else {
  463.                 $arguments = $this->parseArguments(true, false, true);
  464.             }
  465.             $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
  466.             $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
  467.             if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '|')) {
  468.                 break;
  469.             }
  470.             $this->parser->getStream()->next();
  471.         }
  472.         return $node;
  473.     }
  474.     /**
  475.      * Parses arguments.
  476.      *
  477.      * @param bool $namedArguments Whether to allow named arguments or not
  478.      * @param bool $definition     Whether we are parsing arguments for a function definition
  479.      *
  480.      * @return Node
  481.      *
  482.      * @throws SyntaxError
  483.      */
  484.     public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false)
  485.     {
  486.         $args = [];
  487.         $stream = $this->parser->getStream();
  488.         $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(', 'A list of arguments must begin with an opening parenthesis');
  489.         while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
  490.             if (!empty($args)) {
  491.                 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'Arguments must be separated by a comma');
  492.             }
  493.             if ($definition) {
  494.                 $token = $stream->expect(/* Token::NAME_TYPE */ 5, null, 'An argument must be a name');
  495.                 $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
  496.             } else {
  497.                 $value = $this->parseExpression(0, $allowArrow);
  498.             }
  499.             $name = null;
  500.             if ($namedArguments && $token = $stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) {
  501.                 if (!$value instanceof NameExpression) {
  502.                     throw new SyntaxError(sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext());
  503.                 }
  504.                 $name = $value->getAttribute('name');
  505.                 if ($definition) {
  506.                     $value = $this->parsePrimaryExpression();
  507.                     if (!$this->checkConstantExpression($value)) {
  508.                         throw new SyntaxError(sprintf('A default value for an argument must be a constant (a boolean, a string, a number, or an array).'), $token->getLine(), $stream->getSourceContext());
  509.                     }
  510.                 } else {
  511.                     $value = $this->parseExpression(0, $allowArrow);
  512.                 }
  513.             }
  514.             if ($definition) {
  515.                 if (null === $name) {
  516.                     $name = $value->getAttribute('name');
  517.                     $value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());
  518.                 }
  519.                 $args[$name] = $value;
  520.             } else {
  521.                 if (null === $name) {
  522.                     $args[] = $value;
  523.                 } else {
  524.                     $args[$name] = $value;
  525.                 }
  526.             }
  527.         }
  528.         $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'A list of arguments must be closed by a parenthesis');
  529.         return new Node($args);
  530.     }
  531.     public function parseAssignmentExpression()
  532.     {
  533.         $stream = $this->parser->getStream();
  534.         $targets = [];
  535.         while (true) {
  536.             $token = $this->parser->getCurrentToken();
  537.             if ($stream->test(/* Token::OPERATOR_TYPE */ 8) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
  538.                 // in this context, string operators are variable names
  539.                 $this->parser->getStream()->next();
  540.             } else {
  541.                 $stream->expect(/* Token::NAME_TYPE */ 5, null, 'Only variables can be assigned to');
  542.             }
  543.             $value = $token->getValue();
  544.             if (\in_array(strtolower($value), ['true', 'false', 'none', 'null'])) {
  545.                 throw new SyntaxError(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
  546.             }
  547.             $targets[] = new AssignNameExpression($value, $token->getLine());
  548.             if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
  549.                 break;
  550.             }
  551.         }
  552.         return new Node($targets);
  553.     }
  554.     public function parseMultitargetExpression()
  555.     {
  556.         $targets = [];
  557.         while (true) {
  558.             $targets[] = $this->parseExpression();
  559.             if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
  560.                 break;
  561.             }
  562.         }
  563.         return new Node($targets);
  564.     }
  565.     private function parseNotTestExpression(Node $node): NotUnary
  566.     {
  567.         return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine());
  568.     }
  569.     private function parseTestExpression(Node $node): TestExpression
  570.     {
  571.         $stream = $this->parser->getStream();
  572.         list($name, $test) = $this->getTest($node->getTemplateLine());
  573.         $class = $this->getTestNodeClass($test);
  574.         $arguments = null;
  575.         if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
  576.             $arguments = $this->parseArguments(true);
  577.         }
  578.         if ('defined' === $name && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
  579.             $node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
  580.             $node->setAttribute('safe', true);
  581.         }
  582.         return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine());
  583.     }
  584.     private function getTest(int $line): array
  585.     {
  586.         $stream = $this->parser->getStream();
  587.         $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
  588.         if ($test = $this->env->getTest($name)) {
  589.             return [$name, $test];
  590.         }
  591.         if ($stream->test(/* Token::NAME_TYPE */ 5)) {
  592.             // try 2-words tests
  593.             $name = $name.' '.$this->parser->getCurrentToken()->getValue();
  594.             if ($test = $this->env->getTest($name)) {
  595.                 $stream->next();
  596.                 return [$name, $test];
  597.             }
  598.         }
  599.         $e = new SyntaxError(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
  600.         $e->addSuggestions($name, array_keys($this->env->getTests()));
  601.         throw $e;
  602.     }
  603.     private function getTestNodeClass(TwigTest $test): string
  604.     {
  605.         if ($test->isDeprecated()) {
  606.             $stream = $this->parser->getStream();
  607.             $message = sprintf('Twig Test "%s" is deprecated', $test->getName());
  608.             if (!\is_bool($test->getDeprecatedVersion())) {
  609.                 $message .= sprintf(' since version %s', $test->getDeprecatedVersion());
  610.             }
  611.             if ($test->getAlternative()) {
  612.                 $message .= sprintf('. Use "%s" instead', $test->getAlternative());
  613.             }
  614.             $src = $stream->getSourceContext();
  615.             $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
  616.             @trigger_error($message, E_USER_DEPRECATED);
  617.         }
  618.         return $test->getNodeClass();
  619.     }
  620.     private function getFunctionNodeClass(string $name, int $line): string
  621.     {
  622.         if (false === $function = $this->env->getFunction($name)) {
  623.             $e = new SyntaxError(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
  624.             $e->addSuggestions($name, array_keys($this->env->getFunctions()));
  625.             throw $e;
  626.         }
  627.         if ($function->isDeprecated()) {
  628.             $message = sprintf('Twig Function "%s" is deprecated', $function->getName());
  629.             if (!\is_bool($function->getDeprecatedVersion())) {
  630.                 $message .= sprintf(' since version %s', $function->getDeprecatedVersion());
  631.             }
  632.             if ($function->getAlternative()) {
  633.                 $message .= sprintf('. Use "%s" instead', $function->getAlternative());
  634.             }
  635.             $src = $this->parser->getStream()->getSourceContext();
  636.             $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
  637.             @trigger_error($message, E_USER_DEPRECATED);
  638.         }
  639.         return $function->getNodeClass();
  640.     }
  641.     private function getFilterNodeClass(string $name, int $line): string
  642.     {
  643.         if (false === $filter = $this->env->getFilter($name)) {
  644.             $e = new SyntaxError(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
  645.             $e->addSuggestions($name, array_keys($this->env->getFilters()));
  646.             throw $e;
  647.         }
  648.         if ($filter->isDeprecated()) {
  649.             $message = sprintf('Twig Filter "%s" is deprecated', $filter->getName());
  650.             if (!\is_bool($filter->getDeprecatedVersion())) {
  651.                 $message .= sprintf(' since version %s', $filter->getDeprecatedVersion());
  652.             }
  653.             if ($filter->getAlternative()) {
  654.                 $message .= sprintf('. Use "%s" instead', $filter->getAlternative());
  655.             }
  656.             $src = $this->parser->getStream()->getSourceContext();
  657.             $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
  658.             @trigger_error($message, E_USER_DEPRECATED);
  659.         }
  660.         return $filter->getNodeClass();
  661.     }
  662.     // checks that the node only contains "constant" elements
  663.     private function checkConstantExpression(Node $node): bool
  664.     {
  665.         if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
  666.             || $node instanceof NegUnary || $node instanceof PosUnary
  667.         )) {
  668.             return false;
  669.         }
  670.         foreach ($node as $n) {
  671.             if (!$this->checkConstantExpression($n)) {
  672.                 return false;
  673.             }
  674.         }
  675.         return true;
  676.     }
  677. }
  678. class_alias('Twig\ExpressionParser', 'Twig_ExpressionParser');