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