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\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 } 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 830class_alias('Twig\ExpressionParser', 'Twig_ExpressionParser'); 831