Verzeichnisstruktur phpBB-3.3.15
- Veröffentlicht
- 28.08.2024
So funktioniert es
|
Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück |
Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis. |
|
(Beispiel Datei-Icons)
|
Auf das Icon klicken um den Quellcode anzuzeigen |
ExpressionParser.php
001 <?php
002
003 /*
004 * This file is part of Twig.
005 *
006 * (c) Fabien Potencier
007 * (c) Armin Ronacher
008 *
009 * For the full copyright and license information, please view the LICENSE
010 * file that was distributed with this source code.
011 */
012
013 namespace Twig;
014
015 use Twig\Error\SyntaxError;
016 use Twig\Node\Expression\AbstractExpression;
017 use Twig\Node\Expression\ArrayExpression;
018 use Twig\Node\Expression\ArrowFunctionExpression;
019 use Twig\Node\Expression\AssignNameExpression;
020 use Twig\Node\Expression\Binary\ConcatBinary;
021 use Twig\Node\Expression\BlockReferenceExpression;
022 use Twig\Node\Expression\ConditionalExpression;
023 use Twig\Node\Expression\ConstantExpression;
024 use Twig\Node\Expression\GetAttrExpression;
025 use Twig\Node\Expression\MethodCallExpression;
026 use Twig\Node\Expression\NameExpression;
027 use Twig\Node\Expression\ParentExpression;
028 use Twig\Node\Expression\TestExpression;
029 use Twig\Node\Expression\Unary\NegUnary;
030 use Twig\Node\Expression\Unary\NotUnary;
031 use Twig\Node\Expression\Unary\PosUnary;
032 use Twig\Node\Node;
033
034 /**
035 * Parses expressions.
036 *
037 * This parser implements a "Precedence climbing" algorithm.
038 *
039 * @see https://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
040 * @see https://en.wikipedia.org/wiki/Operator-precedence_parser
041 *
042 * @author Fabien Potencier <fabien@symfony.com>
043 *
044 * @internal
045 */
046 class ExpressionParser
047 {
048 public const OPERATOR_LEFT = 1;
049 public const OPERATOR_RIGHT = 2;
050
051 private $parser;
052 private $env;
053 private $unaryOperators;
054 private $binaryOperators;
055
056 public function __construct(Parser $parser, Environment $env)
057 {
058 $this->parser = $parser;
059 $this->env = $env;
060 $this->unaryOperators = $env->getUnaryOperators();
061 $this->binaryOperators = $env->getBinaryOperators();
062 }
063
064 public function parseExpression($precedence = 0, $allowArrow = false)
065 {
066 if ($allowArrow && $arrow = $this->parseArrow()) {
067 return $arrow;
068 }
069
070 $expr = $this->getPrimary();
071 $token = $this->parser->getCurrentToken();
072 while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) {
073 $op = $this->binaryOperators[$token->getValue()];
074 $this->parser->getStream()->next();
075
076 if ('is not' === $token->getValue()) {
077 $expr = $this->parseNotTestExpression($expr);
078 } elseif ('is' === $token->getValue()) {
079 $expr = $this->parseTestExpression($expr);
080 } elseif (isset($op['callable'])) {
081 $expr = $op['callable']($this->parser, $expr);
082 } else {
083 $expr1 = $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + 1 : $op['precedence']);
084 $class = $op['class'];
085 $expr = new $class($expr, $expr1, $token->getLine());
086 }
087
088 $token = $this->parser->getCurrentToken();
089 }
090
091 if (0 === $precedence) {
092 return $this->parseConditionalExpression($expr);
093 }
094
095 return $expr;
096 }
097
098 /**
099 * @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 } elseif (isset($this->unaryOperators[$token->getValue()])) {
259 $class = $this->unaryOperators[$token->getValue()]['class'];
260
261 $ref = new \ReflectionClass($class);
262 if (!(\in_array($ref->getName(), [NegUnary::class, PosUnary::class, 'Twig_Node_Expression_Unary_Neg', 'Twig_Node_Expression_Unary_Pos'])
263 || $ref->isSubclassOf(NegUnary::class) || $ref->isSubclassOf(PosUnary::class)
264 || $ref->isSubclassOf('Twig_Node_Expression_Unary_Neg') || $ref->isSubclassOf('Twig_Node_Expression_Unary_Pos'))
265 ) {
266 throw new SyntaxError(sprintf('Unexpected unary operator "%s".', $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
267 }
268
269 $this->parser->getStream()->next();
270 $expr = $this->parsePrimaryExpression();
271
272 $node = new $class($expr, $token->getLine());
273 break;
274 }
275
276 // no break
277 default:
278 if ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '[')) {
279 $node = $this->parseArrayExpression();
280 } elseif ($token->test(/* Token::PUNCTUATION_TYPE */ 9, '{')) {
281 $node = $this->parseHashExpression();
282 } elseif ($token->test(/* Token::OPERATOR_TYPE */ 8, '=') && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
283 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());
284 } else {
285 throw new SyntaxError(sprintf('Unexpected token "%s" of value "%s".', Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
286 }
287 }
288
289 return $this->parsePostfixExpression($node);
290 }
291
292 public function parseStringExpression()
293 {
294 $stream = $this->parser->getStream();
295
296 $nodes = [];
297 // a string cannot be followed by another string in a single expression
298 $nextCanBeString = true;
299 while (true) {
300 if ($nextCanBeString && $token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) {
301 $nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
302 $nextCanBeString = false;
303 } elseif ($stream->nextIf(/* Token::INTERPOLATION_START_TYPE */ 10)) {
304 $nodes[] = $this->parseExpression();
305 $stream->expect(/* Token::INTERPOLATION_END_TYPE */ 11);
306 $nextCanBeString = true;
307 } else {
308 break;
309 }
310 }
311
312 $expr = array_shift($nodes);
313 foreach ($nodes as $node) {
314 $expr = new ConcatBinary($expr, $node, $node->getTemplateLine());
315 }
316
317 return $expr;
318 }
319
320 public function parseArrayExpression()
321 {
322 $stream = $this->parser->getStream();
323 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '[', 'An array element was expected');
324
325 $node = new ArrayExpression([], $stream->getCurrent()->getLine());
326 $first = true;
327 while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
328 if (!$first) {
329 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'An array element must be followed by a comma');
330
331 // trailing ,?
332 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
333 break;
334 }
335 }
336 $first = false;
337
338 $node->addElement($this->parseExpression());
339 }
340 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']', 'An opened array is not properly closed');
341
342 return $node;
343 }
344
345 public function parseHashExpression()
346 {
347 $stream = $this->parser->getStream();
348 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '{', 'A hash element was expected');
349
350 $node = new ArrayExpression([], $stream->getCurrent()->getLine());
351 $first = true;
352 while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
353 if (!$first) {
354 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'A hash value must be followed by a comma');
355
356 // trailing ,?
357 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '}')) {
358 break;
359 }
360 }
361 $first = false;
362
363 // a hash key can be:
364 //
365 // * a number -- 12
366 // * a string -- 'a'
367 // * a name, which is equivalent to a string -- a
368 // * an expression, which must be enclosed in parentheses -- (1 + 2)
369 if ($token = $stream->nextIf(/* Token::NAME_TYPE */ 5)) {
370 $key = new ConstantExpression($token->getValue(), $token->getLine());
371
372 // {a} is a shortcut for {a:a}
373 if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) {
374 $value = new NameExpression($key->getAttribute('value'), $key->getTemplateLine());
375 $node->addElement($value, $key);
376 continue;
377 }
378 } elseif (($token = $stream->nextIf(/* Token::STRING_TYPE */ 7)) || $token = $stream->nextIf(/* Token::NUMBER_TYPE */ 6)) {
379 $key = new ConstantExpression($token->getValue(), $token->getLine());
380 } elseif ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
381 $key = $this->parseExpression();
382 } else {
383 $current = $stream->getCurrent();
384
385 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());
386 }
387
388 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ':', 'A hash key must be followed by a colon (:)');
389 $value = $this->parseExpression();
390
391 $node->addElement($value, $key);
392 }
393 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '}', 'An opened hash is not properly closed');
394
395 return $node;
396 }
397
398 public function parsePostfixExpression($node)
399 {
400 while (true) {
401 $token = $this->parser->getCurrentToken();
402 if (/* Token::PUNCTUATION_TYPE */ 9 == $token->getType()) {
403 if ('.' == $token->getValue() || '[' == $token->getValue()) {
404 $node = $this->parseSubscriptExpression($node);
405 } elseif ('|' == $token->getValue()) {
406 $node = $this->parseFilterExpression($node);
407 } else {
408 break;
409 }
410 } else {
411 break;
412 }
413 }
414
415 return $node;
416 }
417
418 public function getFunctionNode($name, $line)
419 {
420 switch ($name) {
421 case 'parent':
422 $this->parseArguments();
423 if (!\count($this->parser->getBlockStack())) {
424 throw new SyntaxError('Calling "parent" outside a block is forbidden.', $line, $this->parser->getStream()->getSourceContext());
425 }
426
427 if (!$this->parser->getParent() && !$this->parser->hasTraits()) {
428 throw new SyntaxError('Calling "parent" on a template that does not extend nor "use" another template is forbidden.', $line, $this->parser->getStream()->getSourceContext());
429 }
430
431 return new ParentExpression($this->parser->peekBlockStack(), $line);
432 case 'block':
433 $args = $this->parseArguments();
434 if (\count($args) < 1) {
435 throw new SyntaxError('The "block" function takes one argument (the block name).', $line, $this->parser->getStream()->getSourceContext());
436 }
437
438 return new BlockReferenceExpression($args->getNode(0), \count($args) > 1 ? $args->getNode(1) : null, $line);
439 case 'attribute':
440 $args = $this->parseArguments();
441 if (\count($args) < 2) {
442 throw new SyntaxError('The "attribute" function takes at least two arguments (the variable and the attributes).', $line, $this->parser->getStream()->getSourceContext());
443 }
444
445 return new GetAttrExpression($args->getNode(0), $args->getNode(1), \count($args) > 2 ? $args->getNode(2) : null, Template::ANY_CALL, $line);
446 default:
447 if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
448 $arguments = new ArrayExpression([], $line);
449 foreach ($this->parseArguments() as $n) {
450 $arguments->addElement($n);
451 }
452
453 $node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
454 $node->setAttribute('safe', true);
455
456 return $node;
457 }
458
459 $args = $this->parseArguments(true);
460 $class = $this->getFunctionNodeClass($name, $line);
461
462 return new $class($name, $args, $line);
463 }
464 }
465
466 public function parseSubscriptExpression($node)
467 {
468 $stream = $this->parser->getStream();
469 $token = $stream->next();
470 $lineno = $token->getLine();
471 $arguments = new ArrayExpression([], $lineno);
472 $type = Template::ANY_CALL;
473 if ('.' == $token->getValue()) {
474 $token = $stream->next();
475 if (
476 /* Token::NAME_TYPE */ 5 == $token->getType()
477 ||
478 /* Token::NUMBER_TYPE */ 6 == $token->getType()
479 ||
480 (/* Token::OPERATOR_TYPE */ 8 == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
481 ) {
482 $arg = new ConstantExpression($token->getValue(), $lineno);
483
484 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
485 $type = Template::METHOD_CALL;
486 foreach ($this->parseArguments() as $n) {
487 $arguments->addElement($n);
488 }
489 }
490 } else {
491 throw new SyntaxError(sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
492 }
493
494 if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
495 if (!$arg instanceof ConstantExpression) {
496 throw new SyntaxError(sprintf('Dynamic macro names are not supported (called on "%s").', $node->getAttribute('name')), $token->getLine(), $stream->getSourceContext());
497 }
498
499 $name = $arg->getAttribute('value');
500
501 $node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno);
502 $node->setAttribute('safe', true);
503
504 return $node;
505 }
506 } else {
507 $type = Template::ARRAY_CALL;
508
509 // slice?
510 $slice = false;
511 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
512 $slice = true;
513 $arg = new ConstantExpression(0, $token->getLine());
514 } else {
515 $arg = $this->parseExpression();
516 }
517
518 if ($stream->nextIf(/* Token::PUNCTUATION_TYPE */ 9, ':')) {
519 $slice = true;
520 }
521
522 if ($slice) {
523 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ']')) {
524 $length = new ConstantExpression(null, $token->getLine());
525 } else {
526 $length = $this->parseExpression();
527 }
528
529 $class = $this->getFilterNodeClass('slice', $token->getLine());
530 $arguments = new Node([$arg, $length]);
531 $filter = new $class($node, new ConstantExpression('slice', $token->getLine()), $arguments, $token->getLine());
532
533 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
534
535 return $filter;
536 }
537
538 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ']');
539 }
540
541 return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
542 }
543
544 public function parseFilterExpression($node)
545 {
546 $this->parser->getStream()->next();
547
548 return $this->parseFilterExpressionRaw($node);
549 }
550
551 public function parseFilterExpressionRaw($node, $tag = null)
552 {
553 while (true) {
554 $token = $this->parser->getStream()->expect(/* Token::NAME_TYPE */ 5);
555
556 $name = new ConstantExpression($token->getValue(), $token->getLine());
557 if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '(')) {
558 $arguments = new Node();
559 } else {
560 $arguments = $this->parseArguments(true, false, true);
561 }
562
563 $class = $this->getFilterNodeClass($name->getAttribute('value'), $token->getLine());
564
565 $node = new $class($node, $name, $arguments, $token->getLine(), $tag);
566
567 if (!$this->parser->getStream()->test(/* Token::PUNCTUATION_TYPE */ 9, '|')) {
568 break;
569 }
570
571 $this->parser->getStream()->next();
572 }
573
574 return $node;
575 }
576
577 /**
578 * Parses arguments.
579 *
580 * @param bool $namedArguments Whether to allow named arguments or not
581 * @param bool $definition Whether we are parsing arguments for a function definition
582 *
583 * @return Node
584 *
585 * @throws SyntaxError
586 */
587 public function parseArguments($namedArguments = false, $definition = false, $allowArrow = false)
588 {
589 $args = [];
590 $stream = $this->parser->getStream();
591
592 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, '(', 'A list of arguments must begin with an opening parenthesis');
593 while (!$stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
594 if (!empty($args)) {
595 $stream->expect(/* Token::PUNCTUATION_TYPE */ 9, ',', 'Arguments must be separated by a comma');
596
597 // if the comma above was a trailing comma, early exit the argument parse loop
598 if ($stream->test(/* Token::PUNCTUATION_TYPE */ 9, ')')) {
599 break;
600 }
601 }
602
603 if ($definition) {
604 $token = $stream->expect(/* Token::NAME_TYPE */ 5, 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 */ 8, '=')) {
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('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 */ 9, ')', '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 */ 8) && 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 */ 5, 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 */ 9, ',')) {
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 */ 9, ',')) {
679 break;
680 }
681 }
682
683 return new Node($targets);
684 }
685
686 private function parseNotTestExpression(Node $node): NotUnary
687 {
688 return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine());
689 }
690
691 private function parseTestExpression(Node $node): TestExpression
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 */ 9, '(')) {
699 $arguments = $this->parseArguments(true);
700 } elseif ($test->hasOneMandatoryArgument()) {
701 $arguments = new Node([0 => $this->parsePrimaryExpression()]);
702 }
703
704 if ('defined' === $name && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
705 $node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
706 $node->setAttribute('safe', true);
707 }
708
709 return new $class($node, $name, $arguments, $this->parser->getCurrentToken()->getLine());
710 }
711
712 private function getTest(int $line): array
713 {
714 $stream = $this->parser->getStream();
715 $name = $stream->expect(/* Token::NAME_TYPE */ 5)->getValue();
716
717 if ($test = $this->env->getTest($name)) {
718 return [$name, $test];
719 }
720
721 if ($stream->test(/* Token::NAME_TYPE */ 5)) {
722 // try 2-words tests
723 $name = $name.' '.$this->parser->getCurrentToken()->getValue();
724
725 if ($test = $this->env->getTest($name)) {
726 $stream->next();
727
728 return [$name, $test];
729 }
730 }
731
732 $e = new SyntaxError(sprintf('Unknown "%s" test.', $name), $line, $stream->getSourceContext());
733 $e->addSuggestions($name, array_keys($this->env->getTests()));
734
735 throw $e;
736 }
737
738 private function getTestNodeClass(TwigTest $test): string
739 {
740 if ($test->isDeprecated()) {
741 $stream = $this->parser->getStream();
742 $message = sprintf('Twig Test "%s" is deprecated', $test->getName());
743
744 if (!\is_bool($test->getDeprecatedVersion())) {
745 $message .= sprintf(' since version %s', $test->getDeprecatedVersion());
746 }
747 if ($test->getAlternative()) {
748 $message .= sprintf('. Use "%s" instead', $test->getAlternative());
749 }
750 $src = $stream->getSourceContext();
751 $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
752
753 @trigger_error($message, \E_USER_DEPRECATED);
754 }
755
756 return $test->getNodeClass();
757 }
758
759 private function getFunctionNodeClass(string $name, int $line): string
760 {
761 if (false === $function = $this->env->getFunction($name)) {
762 $e = new SyntaxError(sprintf('Unknown "%s" function.', $name), $line, $this->parser->getStream()->getSourceContext());
763 $e->addSuggestions($name, array_keys($this->env->getFunctions()));
764
765 throw $e;
766 }
767
768 if ($function->isDeprecated()) {
769 $message = sprintf('Twig Function "%s" is deprecated', $function->getName());
770 if (!\is_bool($function->getDeprecatedVersion())) {
771 $message .= sprintf(' since version %s', $function->getDeprecatedVersion());
772 }
773 if ($function->getAlternative()) {
774 $message .= sprintf('. Use "%s" instead', $function->getAlternative());
775 }
776 $src = $this->parser->getStream()->getSourceContext();
777 $message .= sprintf(' in %s at line %d.', $src->getPath() ?: $src->getName(), $line);
778
779 @trigger_error($message, \E_USER_DEPRECATED);
780 }
781
782 return $function->getNodeClass();
783 }
784
785 private function getFilterNodeClass(string $name, int $line): string
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->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->getName(), $line);
804
805 @trigger_error($message, \E_USER_DEPRECATED);
806 }
807
808 return $filter->getNodeClass();
809 }
810
811 // checks that the node only contains "constant" elements
812 private function checkConstantExpression(Node $node): bool
813 {
814 if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
815 || $node instanceof NegUnary || $node instanceof PosUnary
816 )) {
817 return false;
818 }
819
820 foreach ($node as $n) {
821 if (!$this->checkConstantExpression($n)) {
822 return false;
823 }
824 }
825
826 return true;
827 }
828 }
829
830 class_alias('Twig\ExpressionParser', 'Twig_ExpressionParser');
831