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

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