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

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