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

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