"Fossies" - the Fresh Open Source Software Archive

Member "grav/vendor/twig/twig/src/ExpressionParser.php" (1 Sep 2020, 32759 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "ExpressionParser.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 
    3 /*
    4  * This file is part of Twig.
    5  *
    6  * (c) Fabien Potencier
    7  * (c) Armin Ronacher
    8  *
    9  * For the full copyright and license information, please view the LICENSE
   10  * file that was distributed with this source code.
   11  */
   12 
   13 namespace Twig;
   14 
   15 use Twig\Error\SyntaxError;
   16 use Twig\Node\Expression\ArrayExpression;
   17 use Twig\Node\Expression\ArrowFunctionExpression;
   18 use Twig\Node\Expression\AssignNameExpression;
   19 use Twig\Node\Expression\Binary\ConcatBinary;
   20 use Twig\Node\Expression\BlockReferenceExpression;
   21 use Twig\Node\Expression\ConditionalExpression;
   22 use Twig\Node\Expression\ConstantExpression;
   23 use Twig\Node\Expression\GetAttrExpression;
   24 use Twig\Node\Expression\MethodCallExpression;
   25 use Twig\Node\Expression\NameExpression;
   26 use Twig\Node\Expression\ParentExpression;
   27 use Twig\Node\Expression\Unary\NegUnary;
   28 use Twig\Node\Expression\Unary\NotUnary;
   29 use Twig\Node\Expression\Unary\PosUnary;
   30 use Twig\Node\Node;
   31 
   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  * @internal
   43  */
   44 class ExpressionParser
   45 {
   46     const OPERATOR_LEFT = 1;
   47     const OPERATOR_RIGHT = 2;
   48 
   49     protected $parser;
   50     protected $unaryOperators;
   51     protected $binaryOperators;
   52 
   53     private $env;
   54 
   55     public function __construct(Parser $parser, $env = null)
   56     {
   57         $this->parser = $parser;
   58 
   59         if ($env instanceof Environment) {
   60             $this->env = $env;
   61             $this->unaryOperators = $env->getUnaryOperators();
   62             $this->binaryOperators = $env->getBinaryOperators();
   63         } else {
   64             @trigger_error('Passing the operators as constructor arguments to '.__METHOD__.' is deprecated since version 1.27. Pass the environment instead.', E_USER_DEPRECATED);
   65 
   66             $this->env = $parser->getEnvironment();
   67             $this->unaryOperators = func_get_arg(1);
   68             $this->binaryOperators = func_get_arg(2);
   69         }
   70     }
   71 
   72     public function parseExpression($precedence = 0, $allowArrow = false)
   73     {
   74         if ($allowArrow && $arrow = $this->parseArrow()) {
   75             return $arrow;
   76         }
   77 
   78         $expr = $this->getPrimary();
   79         $token = $this->parser->getCurrentToken();
   80         while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) {
   81             $op = $this->binaryOperators[$token->getValue()];
   82             $this->parser->getStream()->next();
   83 
   84             if ('is not' === $token->getValue()) {
   85                 $expr = $this->parseNotTestExpression($expr);
   86             } elseif ('is' === $token->getValue()) {
   87                 $expr = $this->parseTestExpression($expr);
   88             } elseif (isset($op['callable'])) {
   89                 $expr = \call_user_func($op['callable'], $this->parser, $expr);
   90             } else {
   91                 $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']);
   92                 $class = $op['class'];
   93                 $expr = new $class($expr, $expr1, $token->getLine());
   94             }
   95 
   96             $token = $this->parser->getCurrentToken();
   97         }
   98 
   99         if (0 === $precedence) {
  100             return $this->parseConditionalExpression($expr);
  101         }
  102 
  103         return $expr;
  104     }
  105 
  106     /**
  107      * @return ArrowFunctionExpression|null
  108      */
  109     private function parseArrow()
  110     {
  111         $stream = $this->parser->getStream();
  112 
  113         // short array syntax (one argument, no parentheses)?
  114         if ($stream->look(1)->test(Token::ARROW_TYPE)) {
  115             $line = $stream->getCurrent()->getLine();
  116             $token = $stream->expect(Token::NAME_TYPE);
  117             $names = [new AssignNameExpression($token->getValue(), $token->getLine())];
  118             $stream->expect(Token::ARROW_TYPE);
  119 
  120             return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
  121         }
  122 
  123         // first, determine if we are parsing an arrow function by finding => (long form)
  124         $i = 0;
  125         if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, '(')) {
  126             return null;
  127         }
  128         ++$i;
  129         while (true) {
  130             // variable name
  131             ++$i;
  132             if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ',')) {
  133                 break;
  134             }
  135             ++$i;
  136         }
  137         if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE, ')')) {
  138             return null;
  139         }
  140         ++$i;
  141         if (!$stream->look($i)->test(Token::ARROW_TYPE)) {
  142             return null;
  143         }
  144 
  145         // yes, let's parse it properly
  146         $token = $stream->expect(Token::PUNCTUATION_TYPE, '(');
  147         $line = $token->getLine();
  148 
  149         $names = [];
  150         while (true) {
  151             $token = $stream->expect(Token::NAME_TYPE);
  152             $names[] = new AssignNameExpression($token->getValue(), $token->getLine());
  153 
  154             if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
  155                 break;
  156             }
  157         }
  158         $stream->expect(Token::PUNCTUATION_TYPE, ')');
  159         $stream->expect(Token::ARROW_TYPE);
  160 
  161         return new ArrowFunctionExpression($this->parseExpression(0), new Node($names), $line);
  162     }
  163 
  164     protected function getPrimary()
  165     {
  166         $token = $this->parser->getCurrentToken();
  167 
  168         if ($this->isUnary($token)) {
  169             $operator = $this->unaryOperators[$token->getValue()];
  170             $this->parser->getStream()->next();
  171             $expr = $this->parseExpression($operator['precedence']);
  172             $class = $operator['class'];
  173 
  174             return $this->parsePostfixExpression(new $class($expr, $token->getLine()));
  175         } elseif ($token->test(Token::PUNCTUATION_TYPE, '(')) {
  176             $this->parser->getStream()->next();
  177             $expr = $this->parseExpression();
  178             $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed');
  179 
  180             return $this->parsePostfixExpression($expr);
  181         }
  182 
  183         return $this->parsePrimaryExpression();
  184     }
  185 
  186     protected function parseConditionalExpression($expr)
  187     {
  188         while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, '?')) {
  189             if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
  190                 $expr2 = $this->parseExpression();
  191                 if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) {
  192                     $expr3 = $this->parseExpression();
  193                 } else {
  194                     $expr3 = new ConstantExpression('', $this->parser->getCurrentToken()->getLine());
  195                 }
  196             } else {
  197                 $expr2 = $expr;
  198                 $expr3 = $this->parseExpression();
  199             }
  200 
  201             $expr = new ConditionalExpression($expr, $expr2, $expr3, $this->parser->getCurrentToken()->getLine());
  202         }
  203 
  204         return $expr;
  205     }
  206 
  207     protected function isUnary(Token $token)
  208     {
  209         return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
  210     }
  211 
  212     protected function isBinary(Token $token)
  213     {
  214         return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]);
  215     }
  216 
  217     public function parsePrimaryExpression()
  218     {
  219         $token = $this->parser->getCurrentToken();
  220         switch ($token->getType()) {
  221             case Token::NAME_TYPE:
  222                 $this->parser->getStream()->next();
  223                 switch ($token->getValue()) {
  224                     case 'true':
  225                     case 'TRUE':
  226                         $node = new ConstantExpression(true, $token->getLine());
  227                         break;
  228 
  229                     case 'false':
  230                     case 'FALSE':
  231                         $node = new ConstantExpression(false, $token->getLine());
  232                         break;
  233 
  234                     case 'none':
  235                     case 'NONE':
  236                     case 'null':
  237                     case 'NULL':
  238                         $node = new ConstantExpression(null, $token->getLine());
  239                         break;
  240 
  241                     default:
  242                         if ('(' === $this->parser->getCurrentToken()->getValue()) {
  243                             $node = $this->getFunctionNode($token->getValue(), $token->getLine());
  244                         } else {
  245                             $node = new NameExpression($token->getValue(), $token->getLine());
  246                         }
  247                 }
  248                 break;
  249 
  250             case Token::NUMBER_TYPE:
  251                 $this->parser->getStream()->next();
  252                 $node = new ConstantExpression($token->getValue(), $token->getLine());
  253                 break;
  254 
  255             case Token::STRING_TYPE:
  256             case Token::INTERPOLATION_START_TYPE:
  257                 $node = $this->parseStringExpression();
  258                 break;
  259 
  260             case Token::OPERATOR_TYPE:
  261                 if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) {
  262                     // in this context, string operators are variable names
  263                     $this->parser->getStream()->next();
  264                     $node = new NameExpression($token->getValue(), $token->getLine());
  265                     break;
  266                 } elseif (isset($this->unaryOperators[$token->getValue()])) {
  267                     $class = $this->unaryOperators[$token->getValue()]['class'];
  268 
  269                     $ref = new \ReflectionClass($class);
  270                     $negClass = 'Twig\Node\Expression\Unary\NegUnary';
  271                     $posClass = 'Twig\Node\Expression\Unary\PosUnary';
  272                     if (!(\in_array($ref->getName(), [$negClass, $posClass, 'Twig_Node_Expression_Unary_Neg', 'Twig_Node_Expression_Unary_Pos'])
  273                         || $ref->isSubclassOf($negClass) || $ref->isSubclassOf($posClass)
  274                         || $ref->isSubclassOf('Twig_Node_Expression_Unary_Neg') || $ref->isSubclassOf('Twig_Node_Expression_Unary_Pos'))
  275                     ) {
  276                         throw new SyntaxError(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
  277                     }
  278 
  279                     $this->parser->getStream()->next();
  280                     $expr = $this->parsePrimaryExpression();
  281 
  282                     $node = new $class($expr, $token->getLine());
  283                     break;
  284                 }
  285 
  286                 // no break
  287             default:
  288                 if ($token->test(Token::PUNCTUATION_TYPE, '[')) {
  289                     $node = $this->parseArrayExpression();
  290                 } elseif ($token->test(Token::PUNCTUATION_TYPE, '{')) {
  291                     $node = $this->parseHashExpression();
  292                 } elseif ($token->test(Token::OPERATOR_TYPE, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
  293                     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());
  294                 } else {
  295                     throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
  296                 }
  297         }
  298 
  299         return $this->parsePostfixExpression($node);
  300     }
  301 
  302     public function parseStringExpression()
  303     {
  304         $stream = $this->parser->getStream();
  305 
  306         $nodes = [];
  307         // a string cannot be followed by another string in a single expression
  308         $nextCanBeString = true;
  309         while (true) {
  310             if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) {
  311                 $nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
  312                 $nextCanBeString = false;
  313             } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) {
  314                 $nodes[] = $this->parseExpression();
  315                 $stream->expect(Token::INTERPOLATION_END_TYPE);
  316                 $nextCanBeString = true;
  317             } else {
  318                 break;
  319             }
  320         }
  321 
  322         $expr = array_shift($nodes);
  323         foreach ($nodes as $node) {
  324             $expr = new ConcatBinary($expr, $node, $node->getTemplateLine());
  325         }
  326 
  327         return $expr;
  328     }
  329 
  330     public function parseArrayExpression()
  331     {
  332         $stream = $this->parser->getStream();
  333         $stream->expect(Token::PUNCTUATION_TYPE, '[', 'An array element was expected');
  334 
  335         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
  336         $first = true;
  337         while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) {
  338             if (!$first) {
  339                 $stream->expect(Token::PUNCTUATION_TYPE, ',', 'An array element must be followed by a comma');
  340 
  341                 // trailing ,?
  342                 if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
  343                     break;
  344                 }
  345             }
  346             $first = false;
  347 
  348             $node->addElement($this->parseExpression());
  349         }
  350         $stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened array is not properly closed');
  351 
  352         return $node;
  353     }
  354 
  355     public function parseHashExpression()
  356     {
  357         $stream = $this->parser->getStream();
  358         $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A hash element was expected');
  359 
  360         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
  361         $first = true;
  362         while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) {
  363             if (!$first) {
  364                 $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A hash value must be followed by a comma');
  365 
  366                 // trailing ,?
  367                 if ($stream->test(Token::PUNCTUATION_TYPE, '}')) {
  368                     break;
  369                 }
  370             }
  371             $first = false;
  372 
  373             // a hash key can be:
  374             //
  375             //  * a number -- 12
  376             //  * a string -- 'a'
  377             //  * a name, which is equivalent to a string -- a
  378             //  * an expression, which must be enclosed in parentheses -- (1 + 2)
  379             if (($token = $stream->nextIf(Token::STRING_TYPE)) || ($token = $stream->nextIf(Token::NAME_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) {
  380                 $key = new ConstantExpression($token->getValue(), $token->getLine());
  381             } elseif ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
  382                 $key = $this->parseExpression();
  383             } else {
  384                 $current = $stream->getCurrent();
  385 
  386                 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());
  387             }
  388 
  389             $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A hash key must be followed by a colon (:)');
  390             $value = $this->parseExpression();
  391 
  392             $node->addElement($value, $key);
  393         }
  394         $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened hash is not properly closed');
  395 
  396         return $node;
  397     }
  398 
  399     public function parsePostfixExpression($node)
  400     {
  401         while (true) {
  402             $token = $this->parser->getCurrentToken();
  403             if (Token::PUNCTUATION_TYPE == $token->getType()) {
  404                 if ('.' == $token->getValue() || '[' == $token->getValue()) {
  405                     $node = $this->parseSubscriptExpression($node);
  406                 } elseif ('|' == $token->getValue()) {
  407                     $node = $this->parseFilterExpression($node);
  408                 } else {
  409                     break;
  410                 }
  411             } else {
  412                 break;
  413             }
  414         }
  415 
  416         return $node;
  417     }
  418 
  419     public function getFunctionNode($name, $line)
  420     {
  421         switch ($name) {
  422             case 'parent':
  423                 $this->parseArguments();
  424                 if (!\count($this->parser->getBlockStack())) {
  425                     throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext());
  426                 }
  427 
  428                 if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
  429                     throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext());
  430                 }
  431 
  432                 return new ParentExpression($this->parser->peekBlockStack(), $line);
  433             case 'block':
  434                 $args = $this->parseArguments();
  435                 if (\count($args) < 1) {
  436                     throw new SyntaxError('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext());
  437                 }
  438 
  439                 return new BlockReferenceExpression($args->getNode(0), \count($args) > 1 ? $args->getNode(1) : null, $line);
  440             case 'attribute':
  441                 $args = $this->parseArguments();
  442                 if (\count($args) < 2) {
  443                     throw new SyntaxError('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext());
  444                 }
  445 
  446                 return new GetAttrExpression($args->getNode(0), $args->getNode(1), \count($args) > 2 ? $args->getNode(2) : null, Template::ANY_CALL, $line);
  447             default:
  448                 if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
  449                     $arguments = new ArrayExpression([], $line);
  450                     foreach ($this->parseArguments() as $n) {
  451                         $arguments->addElement($n);
  452                     }
  453 
  454                     $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
  455                     $node->setAttribute('safe', true);
  456 
  457                     return $node;
  458                 }
  459 
  460                 $args = $this->parseArguments(true);
  461                 $class = $this->getFunctionNodeClass($name, $line);
  462 
  463                 return new $class($name, $args, $line);
  464         }
  465     }
  466 
  467     public function parseSubscriptExpression($node)
  468     {
  469         $stream = $this->parser->getStream();
  470         $token = $stream->next();
  471         $lineno = $token->getLine();
  472         $arguments = new ArrayExpression([], $lineno);
  473         $type = Template::ANY_CALL;
  474         if ('.' == $token->getValue()) {
  475             $token = $stream->next();
  476             if (
  477                 Token::NAME_TYPE == $token->getType()
  478                 ||
  479                 Token::NUMBER_TYPE == $token->getType()
  480                 ||
  481                 (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
  482             ) {
  483                 $arg = new ConstantExpression($token->getValue(), $lineno);
  484 
  485                 if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
  486                     $type = Template::METHOD_CALL;
  487                     foreach ($this->parseArguments() as $n) {
  488                         $arguments->addElement($n);
  489                     }
  490                 }
  491             } else {
  492                 throw new SyntaxError('Expected name or number.', $lineno, $stream->getSourceContext());
  493             }
  494 
  495             if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
  496                 if (!$arg instanceof ConstantExpression) {
  497                     throw new SyntaxError(sprintf('Dynamic macro names are not supported (called on "%s").', $node->getAttribute('name')), $token->getLine(), $stream->getSourceContext());
  498                 }
  499 
  500                 $name = $arg->getAttribute('value');
  501 
  502                 if ($this->parser->isReservedMacroName($name)) {
  503                     throw new SyntaxError(sprintf('"%s" cannot be called as macro as it is a reserved keyword.', $name), $token->getLine(), $stream->getSourceContext());
  504                 }
  505 
  506                 $node = new MethodCallExpression($node, 'get'.$name, $arguments, $lineno);
  507                 $node->setAttribute('safe', true);
  508 
  509                 return $node;
  510             }
  511         } else {
  512             $type = Template::ARRAY_CALL;
  513 
  514             // slice?
  515             $slice = false;
  516             if ($stream->test(Token::PUNCTUATION_TYPE, ':')) {
  517                 $slice = true;
  518                 $arg = new ConstantExpression(0, $token->getLine());
  519             } else {
  520                 $arg = $this->parseExpression();
  521             }
  522 
  523             if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) {
  524                 $slice = true;
  525             }
  526 
  527             if ($slice) {
  528                 if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
  529                     $length = new ConstantExpression(null, $token->getLine());
  530                 } else {
  531                     $length = $this->parseExpression();
  532                 }
  533 
  534                 $class = $this->getFilterNodeClass('slice', $token->getLine());
  535                 $arguments = new Node([$arg, $length]);
  536                 $filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine());
  537 
  538                 $stream->expect(Token::PUNCTUATION_TYPE, ']');
  539 
  540                 return $filter;
  541             }
  542 
  543             $stream->expect(Token::PUNCTUATION_TYPE, ']');
  544         }
  545 
  546         return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
  547     }
  548 
  549     public function parseFilterExpression($node)
  550     {
  551         $this->parser->getStream()->next();
  552 
  553         return $this->parseFilterExpressionRaw($node);
  554     }
  555 
  556     public function parseFilterExpressionRaw($node, $tag = null)
  557     {
  558         while (true) {
  559             $token = $this->parser->getStream()->expect(Token::NAME_TYPE);
  560 
  561             $name = new ConstantExpression($token->getValue(), $token->getLine());
  562             if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '(')) {
  563                 $arguments = new Node();
  564             } else {
  565                 $arguments = $this->parseArguments(true, false, true);
  566             }
  567 
  568             $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
  569 
  570             $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
  571 
  572             if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE, '|')) {
  573                 break;
  574             }
  575 
  576             $this->parser->getStream()->next();
  577         }
  578 
  579         return $node;
  580     }
  581 
  582     /**
  583      * Parses arguments.
  584      *
  585      * @param bool $namedArguments Whether to allow named arguments or not
  586      * @param bool $definition     Whether we are parsing arguments for a function definition
  587      *
  588      * @return Node
  589      *
  590      * @throws SyntaxError
  591      */
  592     public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false)
  593     {
  594         $args = [];
  595         $stream = $this->parser->getStream();
  596 
  597         $stream->expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis');
  598         while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) {
  599             if (!empty($args)) {
  600                 $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');
  601             }
  602 
  603             if ($definition) {
  604                 $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');
  605                 $value = new NameExpression($token->getValue(), $this->parser->getCurrentToken()->getLine());
  606             } else {
  607                 $value = $this->parseExpression(0, $allowArrow);
  608             }
  609 
  610             $name = null;
  611             if ($namedArguments && $token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) {
  612                 if (!$value instanceof NameExpression) {
  613                     throw new SyntaxError(sprintf('A parameter name must be a string, "%s" given.', \get_class($value)), $token->getLine(), $stream->getSourceContext());
  614                 }
  615                 $name = $value->getAttribute('name');
  616 
  617                 if ($definition) {
  618                     $value = $this->parsePrimaryExpression();
  619 
  620                     if (!$this->checkConstantExpression($value)) {
  621                         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());
  622                     }
  623                 } else {
  624                     $value = $this->parseExpression(0, $allowArrow);
  625                 }
  626             }
  627 
  628             if ($definition) {
  629                 if (null === $name) {
  630                     $name = $value->getAttribute('name');
  631                     $value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());
  632                 }
  633                 $args[$name] = $value;
  634             } else {
  635                 if (null === $name) {
  636                     $args[] = $value;
  637                 } else {
  638                     $args[$name] = $value;
  639                 }
  640             }
  641         }
  642         $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis');
  643 
  644         return new Node($args);
  645     }
  646 
  647     public function parseAssignmentExpression()
  648     {
  649         $stream = $this->parser->getStream();
  650         $targets = [];
  651         while (true) {
  652             $token = $this->parser->getCurrentToken();
  653             if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) {
  654                 // in this context, string operators are variable names
  655                 $this->parser->getStream()->next();
  656             } else {
  657                 $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to');
  658             }
  659             $value = $token->getValue();
  660             if (\in_array(strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), ['true', 'false', 'none', 'null'])) {
  661                 throw new SyntaxError(sprintf('You cannot assign a value to "%s".', $value), $token->getLine(), $stream->getSourceContext());
  662             }
  663             $targets[] = new AssignNameExpression($value, $token->getLine());
  664 
  665             if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) {
  666                 break;
  667             }
  668         }
  669 
  670         return new Node($targets);
  671     }
  672 
  673     public function parseMultitargetExpression()
  674     {
  675         $targets = [];
  676         while (true) {
  677             $targets[] = $this->parseExpression();
  678             if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) {
  679                 break;
  680             }
  681         }
  682 
  683         return new Node($targets);
  684     }
  685 
  686     private function parseNotTestExpression(\Twig_NodeInterface $node)
  687     {
  688         return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine());
  689     }
  690 
  691     private function parseTestExpression(\Twig_NodeInterface $node)
  692     {
  693         $stream = $this->parser->getStream();
  694         list($name, $test) = $this->getTest($node->getTemplateLine());
  695 
  696         $class = $this->getTestNodeClass($test);
  697         $arguments = null;
  698         if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
  699             $arguments = $this->parseArguments(true);
  700         }
  701 
  702         return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine());
  703     }
  704 
  705     private function getTest($line)
  706     {
  707         $stream = $this->parser->getStream();
  708         $name = $stream->expect(Token::NAME_TYPE)->getValue();
  709 
  710         if ($test = $this->env->getTest($name)) {
  711             return [$name, $test];
  712         }
  713 
  714         if ($stream->test(Token::NAME_TYPE)) {
  715             // try 2-words tests
  716             $name = $name.' '.$this->parser->getCurrentToken()->getValue();
  717 
  718             if ($test = $this->env->getTest($name)) {
  719                 $stream->next();
  720 
  721                 return [$name, $test];
  722             }
  723         }
  724 
  725         $e = new SyntaxError(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
  726         $e->addSuggestions($name, array_keys($this->env->getTests()));
  727 
  728         throw $e;
  729     }
  730 
  731     private function getTestNodeClass($test)
  732     {
  733         if ($test instanceof TwigTest && $test->isDeprecated()) {
  734             $stream = $this->parser->getStream();
  735             $message = sprintf('Twig Test "%s" is deprecated', $test->getName());
  736             if (!\is_bool($test->getDeprecatedVersion())) {
  737                 $message .= sprintf(' since version %s', $test->getDeprecatedVersion());
  738             }
  739             if ($test->getAlternative()) {
  740                 $message .= sprintf('. Use "%s" instead', $test->getAlternative());
  741             }
  742             $src = $stream->getSourceContext();
  743             $message .= sprintf(' in %s at line %d.', $src->getPath() ? $src->getPath() : $src->getName(), $stream->getCurrent()->getLine());
  744 
  745             @trigger_error($message, E_USER_DEPRECATED);
  746         }
  747 
  748         if ($test instanceof TwigTest) {
  749             return $test->getNodeClass();
  750         }
  751 
  752         return $test instanceof \Twig_Test_Node ? $test->getClass() : 'Twig\Node\Expression\TestExpression';
  753     }
  754 
  755     protected function getFunctionNodeClass($name, $line)
  756     {
  757         if (false === $function = $this->env->getFunction($name)) {
  758             $e = new SyntaxError(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
  759             $e->addSuggestions($name, array_keys($this->env->getFunctions()));
  760 
  761             throw $e;
  762         }
  763 
  764         if ($function instanceof TwigFunction && $function->isDeprecated()) {
  765             $message = sprintf('Twig Function "%s" is deprecated', $function->getName());
  766             if (!\is_bool($function->getDeprecatedVersion())) {
  767                 $message .= sprintf(' since version %s', $function->getDeprecatedVersion());
  768             }
  769             if ($function->getAlternative()) {
  770                 $message .= sprintf('. Use "%s" instead', $function->getAlternative());
  771             }
  772             $src = $this->parser->getStream()->getSourceContext();
  773             $message .= sprintf(' in %s at line %d.', $src->getPath() ? $src->getPath() : $src->getName(), $line);
  774 
  775             @trigger_error($message, E_USER_DEPRECATED);
  776         }
  777 
  778         if ($function instanceof TwigFunction) {
  779             return $function->getNodeClass();
  780         }
  781 
  782         return $function instanceof \Twig_Function_Node ? $function->getClass() : 'Twig\Node\Expression\FunctionExpression';
  783     }
  784 
  785     protected function getFilterNodeClass($name, $line)
  786     {
  787         if (false === $filter = $this->env->getFilter($name)) {
  788             $e = new SyntaxError(sprintf('Unknown "%s" filter.', $name), $line, $this->parser->getStream()->getSourceContext());
  789             $e->addSuggestions($name, array_keys($this->env->getFilters()));
  790 
  791             throw $e;
  792         }
  793 
  794         if ($filter instanceof TwigFilter && $filter->isDeprecated()) {
  795             $message = sprintf('Twig Filter "%s" is deprecated', $filter->getName());
  796             if (!\is_bool($filter->getDeprecatedVersion())) {
  797                 $message .= sprintf(' since version %s', $filter->getDeprecatedVersion());
  798             }
  799             if ($filter->getAlternative()) {
  800                 $message .= sprintf('. Use "%s" instead', $filter->getAlternative());
  801             }
  802             $src = $this->parser->getStream()->getSourceContext();
  803             $message .= sprintf(' in %s at line %d.', $src->getPath() ? $src->getPath() : $src->getName(), $line);
  804 
  805             @trigger_error($message, E_USER_DEPRECATED);
  806         }
  807 
  808         if ($filter instanceof TwigFilter) {
  809             return $filter->getNodeClass();
  810         }
  811 
  812         return $filter instanceof \Twig_Filter_Node ? $filter->getClass() : 'Twig\Node\Expression\FilterExpression';
  813     }
  814 
  815     // checks that the node only contains "constant" elements
  816     protected function checkConstantExpression(\Twig_NodeInterface $node)
  817     {
  818         if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
  819             || $node instanceof NegUnary || $node instanceof PosUnary
  820         )) {
  821             return false;
  822         }
  823 
  824         foreach ($node as $n) {
  825             if (!$this->checkConstantExpression($n)) {
  826                 return false;
  827             }
  828         }
  829 
  830         return true;
  831     }
  832 }
  833 
  834 class_alias('Twig\ExpressionParser', 'Twig_ExpressionParser');