Verzeichnisstruktur phpBB-3.3.16


Veröffentlicht
27.04.2026

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.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

      (Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

ExpressionParser.php

Zuletzt modifiziert: 01.05.2026, 11:26 - Dateigröße: 32.50 KiB


 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