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
13namespace Twig;
14
15use Twig\Error\SyntaxError;
16use Twig\Node\Expression\ArrayExpression;
17use Twig\Node\Expression\ArrowFunctionExpression;
18use Twig\Node\Expression\AssignNameExpression;
19use Twig\Node\Expression\Binary\ConcatBinary;
20use Twig\Node\Expression\BlockReferenceExpression;
21use Twig\Node\Expression\ConditionalExpression;
22use Twig\Node\Expression\ConstantExpression;
23use Twig\Node\Expression\GetAttrExpression;
24use Twig\Node\Expression\MethodCallExpression;
25use Twig\Node\Expression\NameExpression;
26use Twig\Node\Expression\ParentExpression;
27use Twig\Node\Expression\Unary\NegUnary;
28use Twig\Node\Expression\Unary\NotUnary;
29use Twig\Node\Expression\Unary\PosUnary;
30use 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 */
44class 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
834class_alias('Twig\ExpressionParser', 'Twig_ExpressionParser');
835