vendor/twig/twig/src/ExpressionParser.php line 830

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