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

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\EmptyNode;
  15. use Twig\Node\Expression\AbstractExpression;
  16. use Twig\Node\Expression\ArrayExpression;
  17. use Twig\Node\Expression\ArrowFunctionExpression;
  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\MacroReferenceExpression;
  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\Expression\Unary\SpreadUnary;
  31. use Twig\Node\Expression\Variable\AssignContextVariable;
  32. use Twig\Node\Expression\Variable\ContextVariable;
  33. use Twig\Node\Expression\Variable\LocalVariable;
  34. use Twig\Node\Expression\Variable\TemplateVariable;
  35. use Twig\Node\Node;
  36. use Twig\Node\Nodes;
  37. /**
  38.  * Parses expressions.
  39.  *
  40.  * This parser implements a "Precedence climbing" algorithm.
  41.  *
  42.  * @see https://www.engr.mun.ca/~theo/Misc/exp_parsing.htm
  43.  * @see https://en.wikipedia.org/wiki/Operator-precedence_parser
  44.  *
  45.  * @author Fabien Potencier <fabien@symfony.com>
  46.  */
  47. class ExpressionParser
  48. {
  49.     public const OPERATOR_LEFT 1;
  50.     public const OPERATOR_RIGHT 2;
  51.     /** @var array<string, array{precedence: int, class: class-string<AbstractUnary>}> */
  52.     private $unaryOperators;
  53.     /** @var array<string, array{precedence: int, class: class-string<AbstractBinary>, associativity: self::OPERATOR_*}> */
  54.     private $binaryOperators;
  55.     private $readyNodes = [];
  56.     private array $precedenceChanges = [];
  57.     private bool $deprecationCheck true;
  58.     public function __construct(
  59.         private Parser $parser,
  60.         private Environment $env,
  61.     ) {
  62.         $this->unaryOperators $env->getUnaryOperators();
  63.         $this->binaryOperators $env->getBinaryOperators();
  64.         $ops = [];
  65.         foreach ($this->unaryOperators as $n => $c) {
  66.             $ops[] = $c + ['name' => $n'type' => 'unary'];
  67.         }
  68.         foreach ($this->binaryOperators as $n => $c) {
  69.             $ops[] = $c + ['name' => $n'type' => 'binary'];
  70.         }
  71.         foreach ($ops as $config) {
  72.             if (!isset($config['precedence_change'])) {
  73.                 continue;
  74.             }
  75.             $name $config['type'].'_'.$config['name'];
  76.             $min min($config['precedence_change']->getNewPrecedence(), $config['precedence']);
  77.             $max max($config['precedence_change']->getNewPrecedence(), $config['precedence']);
  78.             foreach ($ops as $c) {
  79.                 if ($c['precedence'] > $min && $c['precedence'] < $max) {
  80.                     $this->precedenceChanges[$c['type'].'_'.$c['name']][] = $name;
  81.                 }
  82.             }
  83.         }
  84.     }
  85.     public function parseExpression($precedence 0)
  86.     {
  87.         if (func_num_args() > 1) {
  88.             trigger_deprecation('twig/twig''3.15''Passing a second argument ($allowArrow) to "%s()" is deprecated.'__METHOD__);
  89.         }
  90.         if ($arrow $this->parseArrow()) {
  91.             return $arrow;
  92.         }
  93.         $expr $this->getPrimary();
  94.         $token $this->parser->getCurrentToken();
  95.         while ($this->isBinary($token) && $this->binaryOperators[$token->getValue()]['precedence'] >= $precedence) {
  96.             $op $this->binaryOperators[$token->getValue()];
  97.             $this->parser->getStream()->next();
  98.             if ('is not' === $token->getValue()) {
  99.                 $expr $this->parseNotTestExpression($expr);
  100.             } elseif ('is' === $token->getValue()) {
  101.                 $expr $this->parseTestExpression($expr);
  102.             } elseif (isset($op['callable'])) {
  103.                 $expr $op['callable']($this->parser$expr);
  104.             } else {
  105.                 $previous $this->setDeprecationCheck(true);
  106.                 try {
  107.                     $expr1 $this->parseExpression(self::OPERATOR_LEFT === $op['associativity'] ? $op['precedence'] + $op['precedence']);
  108.                 } finally {
  109.                     $this->setDeprecationCheck($previous);
  110.                 }
  111.                 $class $op['class'];
  112.                 $expr = new $class($expr$expr1$token->getLine());
  113.             }
  114.             $expr->setAttribute('operator''binary_'.$token->getValue());
  115.             $this->triggerPrecedenceDeprecations($expr$token);
  116.             $token $this->parser->getCurrentToken();
  117.         }
  118.         if (=== $precedence) {
  119.             return $this->parseConditionalExpression($expr);
  120.         }
  121.         return $expr;
  122.     }
  123.     private function triggerPrecedenceDeprecations(AbstractExpression $expr): void
  124.     {
  125.         // Check that the all nodes that are between the 2 precedences have explicit parentheses
  126.         if (!$expr->hasAttribute('operator') || !isset($this->precedenceChanges[$expr->getAttribute('operator')])) {
  127.             return;
  128.         }
  129.         if (str_starts_with($unaryOp $expr->getAttribute('operator'), 'unary')) {
  130.             if ($expr->hasExplicitParentheses()) {
  131.                 return;
  132.             }
  133.             $target explode('_'$unaryOp)[1];
  134.             $change $this->unaryOperators[$target]['precedence_change'];
  135.             /** @var AbstractExpression $node */
  136.             $node $expr->getNode('node');
  137.             foreach ($this->precedenceChanges as $operatorName => $changes) {
  138.                 if (!in_array($unaryOp$changes)) {
  139.                     continue;
  140.                 }
  141.                 if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator')) {
  142.                     trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" unary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.'$target$this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
  143.                 }
  144.             }
  145.         } else {
  146.             foreach ($this->precedenceChanges[$expr->getAttribute('operator')] as $operatorName) {
  147.                 foreach ($expr as $node) {
  148.                     /** @var AbstractExpression $node */
  149.                     if ($node->hasAttribute('operator') && $operatorName === $node->getAttribute('operator') && !$node->hasExplicitParentheses()) {
  150.                         $op explode('_'$operatorName)[1];
  151.                         $change $this->binaryOperators[$op]['precedence_change'];
  152.                         trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('Add explicit parentheses around the "%s" binary operator to avoid behavior change in the next major version as its precedence will change in "%s" at line %d.'$op$this->parser->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
  153.                     }
  154.                 }
  155.             }
  156.         }
  157.     }
  158.     /**
  159.      * @return ArrowFunctionExpression|null
  160.      */
  161.     private function parseArrow()
  162.     {
  163.         $stream $this->parser->getStream();
  164.         // short array syntax (one argument, no parentheses)?
  165.         if ($stream->look(1)->test(Token::ARROW_TYPE)) {
  166.             $line $stream->getCurrent()->getLine();
  167.             $token $stream->expect(Token::NAME_TYPE);
  168.             $names = [new AssignContextVariable($token->getValue(), $token->getLine())];
  169.             $stream->expect(Token::ARROW_TYPE);
  170.             return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line);
  171.         }
  172.         // first, determine if we are parsing an arrow function by finding => (long form)
  173.         $i 0;
  174.         if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE'(')) {
  175.             return null;
  176.         }
  177.         ++$i;
  178.         while (true) {
  179.             // variable name
  180.             ++$i;
  181.             if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE',')) {
  182.                 break;
  183.             }
  184.             ++$i;
  185.         }
  186.         if (!$stream->look($i)->test(Token::PUNCTUATION_TYPE')')) {
  187.             return null;
  188.         }
  189.         ++$i;
  190.         if (!$stream->look($i)->test(Token::ARROW_TYPE)) {
  191.             return null;
  192.         }
  193.         // yes, let's parse it properly
  194.         $token $stream->expect(Token::PUNCTUATION_TYPE'(');
  195.         $line $token->getLine();
  196.         $names = [];
  197.         while (true) {
  198.             $token $stream->expect(Token::NAME_TYPE);
  199.             $names[] = new AssignContextVariable($token->getValue(), $token->getLine());
  200.             if (!$stream->nextIf(Token::PUNCTUATION_TYPE',')) {
  201.                 break;
  202.             }
  203.         }
  204.         $stream->expect(Token::PUNCTUATION_TYPE')');
  205.         $stream->expect(Token::ARROW_TYPE);
  206.         return new ArrowFunctionExpression($this->parseExpression(), new Nodes($names), $line);
  207.     }
  208.     private function getPrimary(): AbstractExpression
  209.     {
  210.         $token $this->parser->getCurrentToken();
  211.         if ($this->isUnary($token)) {
  212.             $operator $this->unaryOperators[$token->getValue()];
  213.             $this->parser->getStream()->next();
  214.             $expr $this->parseExpression($operator['precedence']);
  215.             $class $operator['class'];
  216.             $expr = new $class($expr$token->getLine());
  217.             $expr->setAttribute('operator''unary_'.$token->getValue());
  218.             if ($this->deprecationCheck) {
  219.                 $this->triggerPrecedenceDeprecations($expr$token);
  220.             }
  221.             return $this->parsePostfixExpression($expr);
  222.         } elseif ($token->test(Token::PUNCTUATION_TYPE'(')) {
  223.             $this->parser->getStream()->next();
  224.             $previous $this->setDeprecationCheck(false);
  225.             try {
  226.                 $expr $this->parseExpression()->setExplicitParentheses();
  227.             } finally {
  228.                 $this->setDeprecationCheck($previous);
  229.             }
  230.             $this->parser->getStream()->expect(Token::PUNCTUATION_TYPE')''An opened parenthesis is not properly closed');
  231.             return $this->parsePostfixExpression($expr);
  232.         }
  233.         return $this->parsePrimaryExpression();
  234.     }
  235.     private function parseConditionalExpression($expr): AbstractExpression
  236.     {
  237.         while ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE'?')) {
  238.             if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE':')) {
  239.                 $expr2 $this->parseExpression();
  240.                 if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE':')) {
  241.                     // Ternary operator (expr ? expr2 : expr3)
  242.                     $expr3 $this->parseExpression();
  243.                 } else {
  244.                     // Ternary without else (expr ? expr2)
  245.                     $expr3 = new ConstantExpression(''$this->parser->getCurrentToken()->getLine());
  246.                 }
  247.             } else {
  248.                 // Ternary without then (expr ?: expr3)
  249.                 $expr2 $expr;
  250.                 $expr3 $this->parseExpression();
  251.             }
  252.             $expr = new ConditionalExpression($expr$expr2$expr3$this->parser->getCurrentToken()->getLine());
  253.         }
  254.         return $expr;
  255.     }
  256.     private function isUnary(Token $token): bool
  257.     {
  258.         return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
  259.     }
  260.     private function isBinary(Token $token): bool
  261.     {
  262.         return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]);
  263.     }
  264.     public function parsePrimaryExpression()
  265.     {
  266.         $token $this->parser->getCurrentToken();
  267.         switch ($token->getType()) {
  268.             case Token::NAME_TYPE:
  269.                 $this->parser->getStream()->next();
  270.                 switch ($token->getValue()) {
  271.                     case 'true':
  272.                     case 'TRUE':
  273.                         $node = new ConstantExpression(true$token->getLine());
  274.                         break;
  275.                     case 'false':
  276.                     case 'FALSE':
  277.                         $node = new ConstantExpression(false$token->getLine());
  278.                         break;
  279.                     case 'none':
  280.                     case 'NONE':
  281.                     case 'null':
  282.                     case 'NULL':
  283.                         $node = new ConstantExpression(null$token->getLine());
  284.                         break;
  285.                     default:
  286.                         if ('(' === $this->parser->getCurrentToken()->getValue()) {
  287.                             $node $this->getFunctionNode($token->getValue(), $token->getLine());
  288.                         } else {
  289.                             $node = new ContextVariable($token->getValue(), $token->getLine());
  290.                         }
  291.                 }
  292.                 break;
  293.             case Token::NUMBER_TYPE:
  294.                 $this->parser->getStream()->next();
  295.                 $node = new ConstantExpression($token->getValue(), $token->getLine());
  296.                 break;
  297.             case Token::STRING_TYPE:
  298.             case Token::INTERPOLATION_START_TYPE:
  299.                 $node $this->parseStringExpression();
  300.                 break;
  301.             case Token::PUNCTUATION_TYPE:
  302.                 $node = match ($token->getValue()) {
  303.                     '[' => $this->parseSequenceExpression(),
  304.                     '{' => $this->parseMappingExpression(),
  305.                     default => throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".'Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()),
  306.                 };
  307.                 break;
  308.             case Token::OPERATOR_TYPE:
  309.                 if (preg_match(Lexer::REGEX_NAME$token->getValue(), $matches) && $matches[0] == $token->getValue()) {
  310.                     // in this context, string operators are variable names
  311.                     $this->parser->getStream()->next();
  312.                     $node = new ContextVariable($token->getValue(), $token->getLine());
  313.                     break;
  314.                 }
  315.                 if ('=' === $token->getValue() && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
  316.                     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());
  317.                 }
  318.                 // no break
  319.             default:
  320.                 throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".'Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
  321.         }
  322.         return $this->parsePostfixExpression($node);
  323.     }
  324.     public function parseStringExpression()
  325.     {
  326.         $stream $this->parser->getStream();
  327.         $nodes = [];
  328.         // a string cannot be followed by another string in a single expression
  329.         $nextCanBeString true;
  330.         while (true) {
  331.             if ($nextCanBeString && $token $stream->nextIf(Token::STRING_TYPE)) {
  332.                 $nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
  333.                 $nextCanBeString false;
  334.             } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) {
  335.                 $nodes[] = $this->parseExpression();
  336.                 $stream->expect(Token::INTERPOLATION_END_TYPE);
  337.                 $nextCanBeString true;
  338.             } else {
  339.                 break;
  340.             }
  341.         }
  342.         $expr array_shift($nodes);
  343.         foreach ($nodes as $node) {
  344.             $expr = new ConcatBinary($expr$node$node->getTemplateLine());
  345.         }
  346.         return $expr;
  347.     }
  348.     /**
  349.      * @deprecated since Twig 3.11, use parseSequenceExpression() instead
  350.      */
  351.     public function parseArrayExpression()
  352.     {
  353.         trigger_deprecation('twig/twig''3.11''Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.'__METHOD__);
  354.         return $this->parseSequenceExpression();
  355.     }
  356.     public function parseSequenceExpression()
  357.     {
  358.         $stream $this->parser->getStream();
  359.         $stream->expect(Token::PUNCTUATION_TYPE'[''A sequence element was expected');
  360.         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
  361.         $first true;
  362.         while (!$stream->test(Token::PUNCTUATION_TYPE']')) {
  363.             if (!$first) {
  364.                 $stream->expect(Token::PUNCTUATION_TYPE',''A sequence element must be followed by a comma');
  365.                 // trailing ,?
  366.                 if ($stream->test(Token::PUNCTUATION_TYPE']')) {
  367.                     break;
  368.                 }
  369.             }
  370.             $first false;
  371.             if ($stream->nextIf(Token::SPREAD_TYPE)) {
  372.                 $expr $this->parseExpression();
  373.                 $expr->setAttribute('spread'true);
  374.                 $node->addElement($expr);
  375.             } else {
  376.                 $node->addElement($this->parseExpression());
  377.             }
  378.         }
  379.         $stream->expect(Token::PUNCTUATION_TYPE']''An opened sequence is not properly closed');
  380.         return $node;
  381.     }
  382.     /**
  383.      * @deprecated since Twig 3.11, use parseMappingExpression() instead
  384.      */
  385.     public function parseHashExpression()
  386.     {
  387.         trigger_deprecation('twig/twig''3.11''Calling "%s()" is deprecated, use "parseMappingExpression()" instead.'__METHOD__);
  388.         return $this->parseMappingExpression();
  389.     }
  390.     public function parseMappingExpression()
  391.     {
  392.         $stream $this->parser->getStream();
  393.         $stream->expect(Token::PUNCTUATION_TYPE'{''A mapping element was expected');
  394.         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
  395.         $first true;
  396.         while (!$stream->test(Token::PUNCTUATION_TYPE'}')) {
  397.             if (!$first) {
  398.                 $stream->expect(Token::PUNCTUATION_TYPE',''A mapping value must be followed by a comma');
  399.                 // trailing ,?
  400.                 if ($stream->test(Token::PUNCTUATION_TYPE'}')) {
  401.                     break;
  402.                 }
  403.             }
  404.             $first false;
  405.             if ($stream->nextIf(Token::SPREAD_TYPE)) {
  406.                 $value $this->parseExpression();
  407.                 $value->setAttribute('spread'true);
  408.                 $node->addElement($value);
  409.                 continue;
  410.             }
  411.             // a mapping key can be:
  412.             //
  413.             //  * a number -- 12
  414.             //  * a string -- 'a'
  415.             //  * a name, which is equivalent to a string -- a
  416.             //  * an expression, which must be enclosed in parentheses -- (1 + 2)
  417.             if ($token $stream->nextIf(Token::NAME_TYPE)) {
  418.                 $key = new ConstantExpression($token->getValue(), $token->getLine());
  419.                 // {a} is a shortcut for {a:a}
  420.                 if ($stream->test(Token::PUNCTUATION_TYPE, [',''}'])) {
  421.                     $value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine());
  422.                     $node->addElement($value$key);
  423.                     continue;
  424.                 }
  425.             } elseif (($token $stream->nextIf(Token::STRING_TYPE)) || $token $stream->nextIf(Token::NUMBER_TYPE)) {
  426.                 $key = new ConstantExpression($token->getValue(), $token->getLine());
  427.             } elseif ($stream->test(Token::PUNCTUATION_TYPE'(')) {
  428.                 $key $this->parseExpression();
  429.             } else {
  430.                 $current $stream->getCurrent();
  431.                 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());
  432.             }
  433.             $stream->expect(Token::PUNCTUATION_TYPE':''A mapping key must be followed by a colon (:)');
  434.             $value $this->parseExpression();
  435.             $node->addElement($value$key);
  436.         }
  437.         $stream->expect(Token::PUNCTUATION_TYPE'}''An opened mapping is not properly closed');
  438.         return $node;
  439.     }
  440.     public function parsePostfixExpression($node)
  441.     {
  442.         while (true) {
  443.             $token $this->parser->getCurrentToken();
  444.             if (Token::PUNCTUATION_TYPE == $token->getType()) {
  445.                 if ('.' == $token->getValue() || '[' == $token->getValue()) {
  446.                     $node $this->parseSubscriptExpression($node);
  447.                 } elseif ('|' == $token->getValue()) {
  448.                     $node $this->parseFilterExpression($node);
  449.                 } else {
  450.                     break;
  451.                 }
  452.             } else {
  453.                 break;
  454.             }
  455.         }
  456.         return $node;
  457.     }
  458.     public function getFunctionNode($name$line)
  459.     {
  460.         if (null !== $alias $this->parser->getImportedSymbol('function'$name)) {
  461.             return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $this->createArguments($line), $line);
  462.         }
  463.         $args $this->parseOnlyArguments();
  464.         $function $this->getFunction($name$line);
  465.         if ($function->getParserCallable()) {
  466.             $fakeNode = new EmptyNode($line);
  467.             $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext());
  468.             return ($function->getParserCallable())($this->parser$fakeNode$args$line);
  469.         }
  470.         if (!isset($this->readyNodes[$class $function->getNodeClass()])) {
  471.             $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
  472.         }
  473.         if (!$ready $this->readyNodes[$class]) {
  474.             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);
  475.         }
  476.         return new $class($ready $function $function->getName(), $args$line);
  477.     }
  478.     public function parseSubscriptExpression($node)
  479.     {
  480.         if ('.' === $this->parser->getStream()->next()->getValue()) {
  481.             return $this->parseSubscriptExpressionDot($node);
  482.         }
  483.         return $this->parseSubscriptExpressionArray($node);
  484.     }
  485.     public function parseFilterExpression($node)
  486.     {
  487.         $this->parser->getStream()->next();
  488.         return $this->parseFilterExpressionRaw($node);
  489.     }
  490.     public function parseFilterExpressionRaw($node)
  491.     {
  492.         if (\func_num_args() > 1) {
  493.             trigger_deprecation('twig/twig''3.12''Passing a second argument to "%s()" is deprecated.'__METHOD__);
  494.         }
  495.         while (true) {
  496.             $token $this->parser->getStream()->expect(Token::NAME_TYPE);
  497.             if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE'(')) {
  498.                 $arguments = new EmptyNode();
  499.             } else {
  500.                 $arguments $this->parseOnlyArguments();
  501.             }
  502.             $filter $this->getFilter($token->getValue(), $token->getLine());
  503.             $ready true;
  504.             if (!isset($this->readyNodes[$class $filter->getNodeClass()])) {
  505.                 $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
  506.             }
  507.             if (!$ready $this->readyNodes[$class]) {
  508.                 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);
  509.             }
  510.             $node = new $class($node$ready $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments$token->getLine());
  511.             if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE'|')) {
  512.                 break;
  513.             }
  514.             $this->parser->getStream()->next();
  515.         }
  516.         return $node;
  517.     }
  518.     /**
  519.      * Parses arguments.
  520.      *
  521.      * @return Node
  522.      *
  523.      * @throws SyntaxError
  524.      */
  525.     public function parseArguments()
  526.     {
  527.         $namedArguments false;
  528.         $definition false;
  529.         if (func_num_args() > 1) {
  530.             $definition func_get_arg(1);
  531.         }
  532.         if (func_num_args() > 0) {
  533.             trigger_deprecation('twig/twig''3.15''Passing arguments to "%s()" is deprecated.'__METHOD__);
  534.             $namedArguments func_get_arg(0);
  535.         }
  536.         $args = [];
  537.         $stream $this->parser->getStream();
  538.         $stream->expect(Token::PUNCTUATION_TYPE'(''A list of arguments must begin with an opening parenthesis');
  539.         $hasSpread false;
  540.         while (!$stream->test(Token::PUNCTUATION_TYPE')')) {
  541.             if ($args) {
  542.                 $stream->expect(Token::PUNCTUATION_TYPE',''Arguments must be separated by a comma');
  543.                 // if the comma above was a trailing comma, early exit the argument parse loop
  544.                 if ($stream->test(Token::PUNCTUATION_TYPE')')) {
  545.                     break;
  546.                 }
  547.             }
  548.             if ($definition) {
  549.                 $token $stream->expect(Token::NAME_TYPEnull'An argument must be a name');
  550.                 $value = new ContextVariable($token->getValue(), $this->parser->getCurrentToken()->getLine());
  551.             } else {
  552.                 if ($stream->nextIf(Token::SPREAD_TYPE)) {
  553.                     $hasSpread true;
  554.                     $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine());
  555.                 } elseif ($hasSpread) {
  556.                     throw new SyntaxError('Normal arguments must be placed before argument unpacking.'$stream->getCurrent()->getLine(), $stream->getSourceContext());
  557.                 } else {
  558.                     $value $this->parseExpression();
  559.                 }
  560.             }
  561.             $name null;
  562.             if ($namedArguments && (($token $stream->nextIf(Token::OPERATOR_TYPE'=')) || (!$definition && $token $stream->nextIf(Token::PUNCTUATION_TYPE':')))) {
  563.                 if (!$value instanceof NameExpression) {
  564.                     throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.'\get_class($value)), $token->getLine(), $stream->getSourceContext());
  565.                 }
  566.                 $name $value->getAttribute('name');
  567.                 if ($definition) {
  568.                     $value $this->getPrimary();
  569.                     if (!$this->checkConstantExpression($value)) {
  570.                         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());
  571.                     }
  572.                 } else {
  573.                     $value $this->parseExpression();
  574.                 }
  575.             }
  576.             if ($definition) {
  577.                 if (null === $name) {
  578.                     $name $value->getAttribute('name');
  579.                     $value = new ConstantExpression(null$this->parser->getCurrentToken()->getLine());
  580.                     $value->setAttribute('is_implicit'true);
  581.                 }
  582.                 $args[$name] = $value;
  583.             } else {
  584.                 if (null === $name) {
  585.                     $args[] = $value;
  586.                 } else {
  587.                     $args[$name] = $value;
  588.                 }
  589.             }
  590.         }
  591.         $stream->expect(Token::PUNCTUATION_TYPE')''A list of arguments must be closed by a parenthesis');
  592.         return new Nodes($args);
  593.     }
  594.     public function parseAssignmentExpression()
  595.     {
  596.         $stream $this->parser->getStream();
  597.         $targets = [];
  598.         while (true) {
  599.             $token $this->parser->getCurrentToken();
  600.             if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME$token->getValue())) {
  601.                 // in this context, string operators are variable names
  602.                 $this->parser->getStream()->next();
  603.             } else {
  604.                 $stream->expect(Token::NAME_TYPEnull'Only variables can be assigned to');
  605.             }
  606.             $targets[] = new AssignContextVariable($token->getValue(), $token->getLine());
  607.             if (!$stream->nextIf(Token::PUNCTUATION_TYPE',')) {
  608.                 break;
  609.             }
  610.         }
  611.         return new Nodes($targets);
  612.     }
  613.     public function parseMultitargetExpression()
  614.     {
  615.         $targets = [];
  616.         while (true) {
  617.             $targets[] = $this->parseExpression();
  618.             if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE',')) {
  619.                 break;
  620.             }
  621.         }
  622.         return new Nodes($targets);
  623.     }
  624.     private function parseNotTestExpression(Node $node): NotUnary
  625.     {
  626.         return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine());
  627.     }
  628.     private function parseTestExpression(Node $node): TestExpression
  629.     {
  630.         $stream $this->parser->getStream();
  631.         $test $this->getTest($node->getTemplateLine());
  632.         $arguments null;
  633.         if ($stream->test(Token::PUNCTUATION_TYPE'(')) {
  634.             $arguments $this->parseOnlyArguments();
  635.         } elseif ($test->hasOneMandatoryArgument()) {
  636.             $arguments = new Nodes([=> $this->getPrimary()]);
  637.         }
  638.         if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias $this->parser->getImportedSymbol('function'$node->getAttribute('name'))) {
  639.             $node = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
  640.         }
  641.         $ready $test instanceof TwigTest;
  642.         if (!isset($this->readyNodes[$class $test->getNodeClass()])) {
  643.             $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
  644.         }
  645.         if (!$ready $this->readyNodes[$class]) {
  646.             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);
  647.         }
  648.         return new $class($node$ready $test $test->getName(), $arguments$this->parser->getCurrentToken()->getLine());
  649.     }
  650.     private function getTest(int $line): TwigTest
  651.     {
  652.         $stream $this->parser->getStream();
  653.         $name $stream->expect(Token::NAME_TYPE)->getValue();
  654.         if ($stream->test(Token::NAME_TYPE)) {
  655.             // try 2-words tests
  656.             $name $name.' '.$this->parser->getCurrentToken()->getValue();
  657.             if ($test $this->env->getTest($name)) {
  658.                 $stream->next();
  659.             }
  660.         } else {
  661.             $test $this->env->getTest($name);
  662.         }
  663.         if (!$test) {
  664.             if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
  665.                 return new TwigTest($name, fn () => '');
  666.             }
  667.             $e = new SyntaxError(\sprintf('Unknown "%s" test.'$name), $line$stream->getSourceContext());
  668.             $e->addSuggestions($namearray_keys($this->env->getTests()));
  669.             throw $e;
  670.         }
  671.         if ($test->isDeprecated()) {
  672.             $stream $this->parser->getStream();
  673.             $src $stream->getSourceContext();
  674.             $test->triggerDeprecation($src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
  675.         }
  676.         return $test;
  677.     }
  678.     private function getFunction(string $nameint $line): TwigFunction
  679.     {
  680.         if (!$function $this->env->getFunction($name)) {
  681.             if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
  682.                 return new TwigFunction($name, fn () => '');
  683.             }
  684.             $e = new SyntaxError(\sprintf('Unknown "%s" function.'$name), $line$this->parser->getStream()->getSourceContext());
  685.             $e->addSuggestions($namearray_keys($this->env->getFunctions()));
  686.             throw $e;
  687.         }
  688.         if ($function->isDeprecated()) {
  689.             $src $this->parser->getStream()->getSourceContext();
  690.             $function->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
  691.         }
  692.         return $function;
  693.     }
  694.     private function getFilter(string $nameint $line): TwigFilter
  695.     {
  696.         if (!$filter $this->env->getFilter($name)) {
  697.             if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
  698.                 return new TwigFilter($name, fn () => '');
  699.             }
  700.             $e = new SyntaxError(\sprintf('Unknown "%s" filter.'$name), $line$this->parser->getStream()->getSourceContext());
  701.             $e->addSuggestions($namearray_keys($this->env->getFilters()));
  702.             throw $e;
  703.         }
  704.         if ($filter->isDeprecated()) {
  705.             $src $this->parser->getStream()->getSourceContext();
  706.             $filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
  707.         }
  708.         return $filter;
  709.     }
  710.     // checks that the node only contains "constant" elements
  711.     // to be removed in 4.0
  712.     private function checkConstantExpression(Node $node): bool
  713.     {
  714.         if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
  715.             || $node instanceof NegUnary || $node instanceof PosUnary
  716.         )) {
  717.             return false;
  718.         }
  719.         foreach ($node as $n) {
  720.             if (!$this->checkConstantExpression($n)) {
  721.                 return false;
  722.             }
  723.         }
  724.         return true;
  725.     }
  726.     private function setDeprecationCheck(bool $deprecationCheck): bool
  727.     {
  728.         $current $this->deprecationCheck;
  729.         $this->deprecationCheck $deprecationCheck;
  730.         return $current;
  731.     }
  732.     private function createArguments(int $line): ArrayExpression
  733.     {
  734.         $arguments = new ArrayExpression([], $line);
  735.         foreach ($this->parseOnlyArguments() as $k => $n) {
  736.             $arguments->addElement($n, new LocalVariable($k$line));
  737.         }
  738.         return $arguments;
  739.     }
  740.     public function parseOnlyArguments()
  741.     {
  742.         $args = [];
  743.         $stream $this->parser->getStream();
  744.         $stream->expect(Token::PUNCTUATION_TYPE'(''A list of arguments must begin with an opening parenthesis');
  745.         $hasSpread false;
  746.         while (!$stream->test(Token::PUNCTUATION_TYPE')')) {
  747.             if ($args) {
  748.                 $stream->expect(Token::PUNCTUATION_TYPE',''Arguments must be separated by a comma');
  749.                 // if the comma above was a trailing comma, early exit the argument parse loop
  750.                 if ($stream->test(Token::PUNCTUATION_TYPE')')) {
  751.                     break;
  752.                 }
  753.             }
  754.             if ($stream->nextIf(Token::SPREAD_TYPE)) {
  755.                 $hasSpread true;
  756.                 $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine());
  757.             } elseif ($hasSpread) {
  758.                 throw new SyntaxError('Normal arguments must be placed before argument unpacking.'$stream->getCurrent()->getLine(), $stream->getSourceContext());
  759.             } else {
  760.                 $value $this->parseExpression();
  761.             }
  762.             $name null;
  763.             if (($token $stream->nextIf(Token::OPERATOR_TYPE'=')) || ($token $stream->nextIf(Token::PUNCTUATION_TYPE':'))) {
  764.                 if (!$value instanceof NameExpression) {
  765.                     throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.'\get_class($value)), $token->getLine(), $stream->getSourceContext());
  766.                 }
  767.                 $name $value->getAttribute('name');
  768.                 $value $this->parseExpression();
  769.             }
  770.             if (null === $name) {
  771.                 $args[] = $value;
  772.             } else {
  773.                 $args[$name] = $value;
  774.             }
  775.         }
  776.         $stream->expect(Token::PUNCTUATION_TYPE')''A list of arguments must be closed by a parenthesis');
  777.         return new Nodes($args);
  778.     }
  779.     private function parseSubscriptExpressionDot(Node $node): AbstractExpression
  780.     {
  781.         $stream $this->parser->getStream();
  782.         $token $stream->getCurrent();
  783.         $lineno $token->getLine();
  784.         $arguments = new ArrayExpression([], $lineno);
  785.         $type Template::ANY_CALL;
  786.         if ($stream->nextIf(Token::PUNCTUATION_TYPE'(')) {
  787.             $attribute $this->parseExpression();
  788.             $stream->expect(Token::PUNCTUATION_TYPE')');
  789.         } else {
  790.             $token $stream->next();
  791.             if (
  792.                 Token::NAME_TYPE == $token->getType()
  793.                 ||
  794.                 Token::NUMBER_TYPE == $token->getType()
  795.                 ||
  796.                 (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME$token->getValue()))
  797.             ) {
  798.                 $attribute = new ConstantExpression($token->getValue(), $token->getLine());
  799.             } else {
  800.                 throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.'$token->getValue(), Token::typeToEnglish($token->getType())), $token->getLine(), $stream->getSourceContext());
  801.             }
  802.         }
  803.         if ($stream->test(Token::PUNCTUATION_TYPE'(')) {
  804.             $type Template::METHOD_CALL;
  805.             $arguments $this->createArguments($token->getLine());
  806.         }
  807.         if (
  808.             $node instanceof NameExpression
  809.             &&
  810.             (
  811.                 null !== $this->parser->getImportedSymbol('template'$node->getAttribute('name'))
  812.                 ||
  813.                 '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression
  814.             )
  815.         ) {
  816.             return new MacroReferenceExpression(new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments$node->getTemplateLine());
  817.         }
  818.         return new GetAttrExpression($node$attribute$arguments$type$lineno);
  819.     }
  820.     private function parseSubscriptExpressionArray(Node $node): AbstractExpression
  821.     {
  822.         $stream $this->parser->getStream();
  823.         $token $stream->getCurrent();
  824.         $lineno $token->getLine();
  825.         $arguments = new ArrayExpression([], $lineno);
  826.         // slice?
  827.         $slice false;
  828.         if ($stream->test(Token::PUNCTUATION_TYPE':')) {
  829.             $slice true;
  830.             $attribute = new ConstantExpression(0$token->getLine());
  831.         } else {
  832.             $attribute $this->parseExpression();
  833.         }
  834.         if ($stream->nextIf(Token::PUNCTUATION_TYPE':')) {
  835.             $slice true;
  836.         }
  837.         if ($slice) {
  838.             if ($stream->test(Token::PUNCTUATION_TYPE']')) {
  839.                 $length = new ConstantExpression(null$token->getLine());
  840.             } else {
  841.                 $length $this->parseExpression();
  842.             }
  843.             $filter $this->getFilter('slice'$token->getLine());
  844.             $arguments = new Nodes([$attribute$length]);
  845.             $filter = new ($filter->getNodeClass())($node$filter$arguments$token->getLine());
  846.             $stream->expect(Token::PUNCTUATION_TYPE']');
  847.             return $filter;
  848.         }
  849.         $stream->expect(Token::PUNCTUATION_TYPE']');
  850.         return new GetAttrExpression($node$attribute$argumentsTemplate::ARRAY_CALL$lineno);
  851.     }
  852. }