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

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.     public const OPERATOR_LEFT = 1;
  45.     public 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::NAME_TYPE */ 5)) {
  315.                 $key = new ConstantExpression($token->getValue(), $token->getLine());
  316.                 // {a} is a shortcut for {a:a}
  317.                 if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) {
  318.                     $value = new NameExpression($key->getAttribute('value'), $key->getTemplateLine());
  319.                     $node->addElement($value, $key);
  320.                     continue;
  321.                 }
  322.             } elseif (($token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) || $token = $stream->nextIf(/* Token::NUMBER_TYPE */ 6)) {
  323.                 $key = new ConstantExpression($token->getValue(), $token->getLine());
  324.             } elseif ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
  325.                 $key = $this->parseExpression();
  326.             } else {
  327.                 $current = $stream->getCurrent();
  328.                 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());
  329.             }
  330.             $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A hash key must be followed by a colon (:)');
  331.             $value = $this->parseExpression();
  332.             $node->addElement($value, $key);
  333.         }
  334.         $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened hash is not properly closed');
  335.         return $node;
  336.     }
  337.     public function parsePostfixExpression($node)
  338.     {
  339.         while (true) {
  340.             $token = $this->parser->getCurrentToken();
  341.             if (/* Token::PUNCTUATION_TYPE */ 9 == $token->getType()) {
  342.                 if ('.' == $token->getValue() || '[' == $token->getValue()) {
  343.                     $node = $this->parseSubscriptExpression($node);
  344.                 } elseif ('|' == $token->getValue()) {
  345.                     $node = $this->parseFilterExpression($node);
  346.                 } else {
  347.                     break;
  348.                 }
  349.             } else {
  350.                 break;
  351.             }
  352.         }
  353.         return $node;
  354.     }
  355.     public function getFunctionNode($name, $line)
  356.     {
  357.         switch ($name) {
  358.             case 'parent':
  359.                 $this->parseArguments();
  360.                 if (!\count($this->parser->getBlockStack())) {
  361.                     throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext());
  362.                 }
  363.                 if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
  364.                     throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext());
  365.                 }
  366.                 return new ParentExpression($this->parser->peekBlockStack(), $line);
  367.             case 'block':
  368.                 $args = $this->parseArguments();
  369.                 if (\count($args) < 1) {
  370.                     throw new SyntaxError('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext());
  371.                 }
  372.                 return new BlockReferenceExpression($args->getNode(0), \count($args) > 1 ? $args->getNode(1) : null, $line);
  373.             case 'attribute':
  374.                 $args = $this->parseArguments();
  375.                 if (\count($args) < 2) {
  376.                     throw new SyntaxError('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext());
  377.                 }
  378.                 return new GetAttrExpression($args->getNode(0), $args->getNode(1), \count($args) > 2 ? $args->getNode(2) : null, Template::ANY_CALL, $line);
  379.             default:
  380.                 if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
  381.                     $arguments = new ArrayExpression([], $line);
  382.                     foreach ($this->parseArguments() as $n) {
  383.                         $arguments->addElement($n);
  384.                     }
  385.                     $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
  386.                     $node->setAttribute('safe', true);
  387.                     return $node;
  388.                 }
  389.                 $args = $this->parseArguments(true);
  390.                 $class = $this->getFunctionNodeClass($name, $line);
  391.                 return new $class($name, $args, $line);
  392.         }
  393.     }
  394.     public function parseSubscriptExpression($node)
  395.     {
  396.         $stream = $this->parser->getStream();
  397.         $token = $stream->next();
  398.         $lineno = $token->getLine();
  399.         $arguments = new ArrayExpression([], $lineno);
  400.         $type = Template::ANY_CALL;
  401.         if ('.' == $token->getValue()) {
  402.             $token = $stream->next();
  403.             if (
  404.                 /* Token::NAME_TYPE */ 5 == $token->getType()
  405.                 ||
  406.                 /* Token::NUMBER_TYPE */ 6 == $token->getType()
  407.                 ||
  408.                 (/* Token::OPERATOR_TYPE */ 8 == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
  409.             ) {
  410.                 $arg = new ConstantExpression($token->getValue(), $lineno);
  411.                 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
  412.                     $type = Template::METHOD_CALL;
  413.                     foreach ($this->parseArguments() as $n) {
  414.                         $arguments->addElement($n);
  415.                     }
  416.                 }
  417.             } else {
  418.                 throw new SyntaxError(sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
  419.             }
  420.             if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
  421.                 if (!$arg instanceof ConstantExpression) {
  422.                     throw new SyntaxError(sprintf('Dynamic macro names are not supported (called on "%s").', $node->getAttribute('name')), $token->getLine(), $stream->getSourceContext());
  423.                 }
  424.                 $name = $arg->getAttribute('value');
  425.                 $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno);
  426.                 $node->setAttribute('safe', true);
  427.                 return $node;
  428.             }
  429.         } else {
  430.             $type = Template::ARRAY_CALL;
  431.             // slice?
  432.             $slice = false;
  433.             if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
  434.                 $slice = true;
  435.                 $arg = new ConstantExpression(0, $token->getLine());
  436.             } else {
  437.                 $arg = $this->parseExpression();
  438.             }
  439.             if ($stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
  440.                 $slice = true;
  441.             }
  442.             if ($slice) {
  443.                 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
  444.                     $length = new ConstantExpression(null, $token->getLine());
  445.                 } else {
  446.                     $length = $this->parseExpression();
  447.                 }
  448.                 $class = $this->getFilterNodeClass('slice', $token->getLine());
  449.                 $arguments = new Node([$arg, $length]);
  450.                 $filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine());
  451.                 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
  452.                 return $filter;
  453.             }
  454.             $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
  455.         }
  456.         return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
  457.     }
  458.     public function parseFilterExpression($node)
  459.     {
  460.         $this->parser->getStream()->next();
  461.         return $this->parseFilterExpressionRaw($node);
  462.     }
  463.     public function parseFilterExpressionRaw($node, $tag = null)
  464.     {
  465.         while (true) {
  466.             $token = $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5);
  467.             $name = new ConstantExpression($token->getValue(), $token->getLine());
  468.             if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
  469.                 $arguments = new Node();
  470.             } else {
  471.                 $arguments = $this->parseArguments(true, false, true);
  472.             }
  473.             $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
  474.             $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
  475.             if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '|')) {
  476.                 break;
  477.             }
  478.             $this->parser->getStream()->next();
  479.         }
  480.         return $node;
  481.     }
  482.     /**
  483.      * Parses arguments.
  484.      *
  485.      * @param bool $namedArguments Whether to allow named arguments or not
  486.      * @param bool $definition     Whether we are parsing arguments for a function definition
  487.      *
  488.      * @return Node
  489.      *
  490.      * @throws SyntaxError
  491.      */
  492.     public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false)
  493.     {
  494.         $args = [];
  495.         $stream = $this->parser->getStream();
  496.         $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(', 'A list of arguments must begin with an opening parenthesis');
  497.         while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
  498.             if (!empty($args)) {
  499.                 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'Arguments must be separated by a comma');
  500.                 // if the comma above was a trailing comma, early exit the argument parse loop
  501.                 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
  502.                     break;
  503.                 }
  504.             }
  505.             if ($definition) {
  506.                 $token = $stream->expect(/* Token::NAME_TYPE */ 5, null, 'An argument must be a name');
  507.                 $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
  508.             } else {
  509.                 $value = $this->parseExpression(0, $allowArrow);
  510.             }
  511.             $name = null;
  512.             if ($namedArguments && $token = $stream->nextIf(/* Token::OPERATOR_TYPE */ 8, '=')) {
  513.                 if (!$value instanceof NameExpression) {
  514.                     throw new SyntaxError(sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext());
  515.                 }
  516.                 $name = $value->getAttribute('name');
  517.                 if ($definition) {
  518.                     $value = $this->parsePrimaryExpression();
  519.                     if (!$this->checkConstantExpression($value)) {
  520.                         throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, or an array).', $token->getLine(), $stream->getSourceContext());
  521.                     }
  522.                 } else {
  523.                     $value = $this->parseExpression(0, $allowArrow);
  524.                 }
  525.             }
  526.             if ($definition) {
  527.                 if (null === $name) {
  528.                     $name = $value->getAttribute('name');
  529.                     $value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());
  530.                 }
  531.                 $args[$name] = $value;
  532.             } else {
  533.                 if (null === $name) {
  534.                     $args[] = $value;
  535.                 } else {
  536.                     $args[$name] = $value;
  537.                 }
  538.             }
  539.         }
  540.         $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ')', 'A list of arguments must be closed by a parenthesis');
  541.         return new Node($args);
  542.     }
  543.     public function parseAssignmentExpression()
  544.     {
  545.         $stream = $this->parser->getStream();
  546.         $targets = [];
  547.         while (true) {
  548.             $token = $this->parser->getCurrentToken();
  549.             if ($stream->test(/* Token::OPERATOR_TYPE */ 8) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
  550.                 // in this context, string operators are variable names
  551.                 $this->parser->getStream()->next();
  552.             } else {
  553.                 $stream->expect(/* Token::NAME_TYPE */ 5, null, 'Only variables can be assigned to');
  554.             }
  555.             $value = $token->getValue();
  556.             if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) {
  557.                 throw new SyntaxError(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
  558.             }
  559.             $targets[] = new AssignNameExpression($value, $token->getLine());
  560.             if (!$stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
  561.                 break;
  562.             }
  563.         }
  564.         return new Node($targets);
  565.     }
  566.     public function parseMultitargetExpression()
  567.     {
  568.         $targets = [];
  569.         while (true) {
  570.             $targets[] = $this->parseExpression();
  571.             if (!$this->parser->getStream()->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ',')) {
  572.                 break;
  573.             }
  574.         }
  575.         return new Node($targets);
  576.     }
  577.     private function parseNotTestExpression(Node $node): NotUnary
  578.     {
  579.         return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine());
  580.     }
  581.     private function parseTestExpression(Node $node): TestExpression
  582.     {
  583.         $stream = $this->parser->getStream();
  584.         list($name, $test) = $this->getTest($node->getTemplateLine());
  585.         $class = $this->getTestNodeClass($test);
  586.         $arguments = null;
  587.         if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
  588.             $arguments = $this->parseArguments(true);
  589.         } elseif ($test->hasOneMandatoryArgument()) {
  590.             $arguments = new Node([0 => $this->parsePrimaryExpression()]);
  591.         }
  592.         if ('defined' === $name && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
  593.             $node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
  594.             $node->setAttribute('safe', true);
  595.         }
  596.         return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine());
  597.     }
  598.     private function getTest(int $line): array
  599.     {
  600.         $stream = $this->parser->getStream();
  601.         $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
  602.         if ($test = $this->env->getTest($name)) {
  603.             return [$name, $test];
  604.         }
  605.         if ($stream->test(/* Token::NAME_TYPE */ 5)) {
  606.             // try 2-words tests
  607.             $name = $name.' '.$this->parser->getCurrentToken()->getValue();
  608.             if ($test = $this->env->getTest($name)) {
  609.                 $stream->next();
  610.                 return [$name, $test];
  611.             }
  612.         }
  613.         $e = new SyntaxError(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
  614.         $e->addSuggestions($name, array_keys($this->env->getTests()));
  615.         throw $e;
  616.     }
  617.     private function getTestNodeClass(TwigTest $test): string
  618.     {
  619.         if ($test->isDeprecated()) {
  620.             $stream = $this->parser->getStream();
  621.             $message = sprintf('Twig Test "%s" is deprecated', $test->getName());
  622.             if (!\is_bool($test->getDeprecatedVersion())) {
  623.                 $message .= sprintf(' since version %s', $test->getDeprecatedVersion());
  624.             }
  625.             if ($test->getAlternative()) {
  626.                 $message .= sprintf('. Use "%s" instead', $test->getAlternative());
  627.             }
  628.             $src = $stream->getSourceContext();
  629.             $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
  630.             @trigger_error($message, \E_USER_DEPRECATED);
  631.         }
  632.         return $test->getNodeClass();
  633.     }
  634.     private function getFunctionNodeClass(string $name, int $line): string
  635.     {
  636.         if (false === $function = $this->env->getFunction($name)) {
  637.             $e = new SyntaxError(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
  638.             $e->addSuggestions($name, array_keys($this->env->getFunctions()));
  639.             throw $e;
  640.         }
  641.         if ($function->isDeprecated()) {
  642.             $message = sprintf('Twig Function "%s" is deprecated', $function->getName());
  643.             if (!\is_bool($function->getDeprecatedVersion())) {
  644.                 $message .= sprintf(' since version %s', $function->getDeprecatedVersion());
  645.             }
  646.             if ($function->getAlternative()) {
  647.                 $message .= sprintf('. Use "%s" instead', $function->getAlternative());
  648.             }
  649.             $src = $this->parser->getStream()->getSourceContext();
  650.             $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
  651.             @trigger_error($message, \E_USER_DEPRECATED);
  652.         }
  653.         return $function->getNodeClass();
  654.     }
  655.     private function getFilterNodeClass(string $name, int $line): string
  656.     {
  657.         if (false === $filter = $this->env->getFilter($name)) {
  658.             $e = new SyntaxError(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
  659.             $e->addSuggestions($name, array_keys($this->env->getFilters()));
  660.             throw $e;
  661.         }
  662.         if ($filter->isDeprecated()) {
  663.             $message = sprintf('Twig Filter "%s" is deprecated', $filter->getName());
  664.             if (!\is_bool($filter->getDeprecatedVersion())) {
  665.                 $message .= sprintf(' since version %s', $filter->getDeprecatedVersion());
  666.             }
  667.             if ($filter->getAlternative()) {
  668.                 $message .= sprintf('. Use "%s" instead', $filter->getAlternative());
  669.             }
  670.             $src = $this->parser->getStream()->getSourceContext();
  671.             $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
  672.             @trigger_error($message, \E_USER_DEPRECATED);
  673.         }
  674.         return $filter->getNodeClass();
  675.     }
  676.     // checks that the node only contains "constant" elements
  677.     private function checkConstantExpression(Node $node): bool
  678.     {
  679.         if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
  680.             || $node instanceof NegUnary || $node instanceof PosUnary
  681.         )) {
  682.             return false;
  683.         }
  684.         foreach ($node as $n) {
  685.             if (!$this->checkConstantExpression($n)) {
  686.                 return false;
  687.             }
  688.         }
  689.         return true;
  690.     }
  691. }
  692. class_alias('Twig\ExpressionParser', 'Twig_ExpressionParser');