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

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 $parserEnvironment $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'] + $op['precedence']);
  75.                 $class $op['class'];
  76.                 $expr = new $class($expr$expr1$token->getLine());
  77.             }
  78.             $token $this->parser->getCurrentToken();
  79.         }
  80.         if (=== $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 */ == $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) > $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) > $args->getNode(2) : nullTemplate::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 */ == $token->getType()
  405.                 ||
  406.                 /* Token::NUMBER_TYPE */ == $token->getType()
  407.                 ||
  408.                 (/* Token::OPERATOR_TYPE */ == $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('Expected name or number.'$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(truefalsetrue);
  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 */ 5null'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(sprintf('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 */ 5null'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([=> $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($namearray_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($messageE_USER_DEPRECATED);
  631.         }
  632.         return $test->getNodeClass();
  633.     }
  634.     private function getFunctionNodeClass(string $nameint $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($namearray_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($messageE_USER_DEPRECATED);
  652.         }
  653.         return $function->getNodeClass();
  654.     }
  655.     private function getFilterNodeClass(string $nameint $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($namearray_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($messageE_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');