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