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\ConstantExpression;
  21. use Twig\Node\Expression\GetAttrExpression;
  22. use Twig\Node\Expression\MacroReferenceExpression;
  23. use Twig\Node\Expression\NameExpression;
  24. use Twig\Node\Expression\Ternary\ConditionalTernary;
  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, precedence_change?: OperatorPrecedenceChange, class: class-string<AbstractUnary>}> */
  52.     private $unaryOperators;
  53.     /** @var array<string, array{precedence: int, precedence_change?: OperatorPrecedenceChange, 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);
  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);
  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.             $expr2 $this->parseExpression();
  239.             if ($this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE':')) {
  240.                 // Ternary operator (expr ? expr2 : expr3)
  241.                 $expr3 $this->parseExpression();
  242.             } else {
  243.                 // Ternary without else (expr ? expr2)
  244.                 $expr3 = new ConstantExpression(''$this->parser->getCurrentToken()->getLine());
  245.             }
  246.             $expr = new ConditionalTernary($expr$expr2$expr3$this->parser->getCurrentToken()->getLine());
  247.         }
  248.         return $expr;
  249.     }
  250.     private function isUnary(Token $token): bool
  251.     {
  252.         return $token->test(Token::OPERATOR_TYPE) && isset($this->unaryOperators[$token->getValue()]);
  253.     }
  254.     private function isBinary(Token $token): bool
  255.     {
  256.         return $token->test(Token::OPERATOR_TYPE) && isset($this->binaryOperators[$token->getValue()]);
  257.     }
  258.     public function parsePrimaryExpression()
  259.     {
  260.         $token $this->parser->getCurrentToken();
  261.         switch ($token->getType()) {
  262.             case Token::NAME_TYPE:
  263.                 $this->parser->getStream()->next();
  264.                 switch ($token->getValue()) {
  265.                     case 'true':
  266.                     case 'TRUE':
  267.                         $node = new ConstantExpression(true$token->getLine());
  268.                         break;
  269.                     case 'false':
  270.                     case 'FALSE':
  271.                         $node = new ConstantExpression(false$token->getLine());
  272.                         break;
  273.                     case 'none':
  274.                     case 'NONE':
  275.                     case 'null':
  276.                     case 'NULL':
  277.                         $node = new ConstantExpression(null$token->getLine());
  278.                         break;
  279.                     default:
  280.                         if ('(' === $this->parser->getCurrentToken()->getValue()) {
  281.                             $node $this->getFunctionNode($token->getValue(), $token->getLine());
  282.                         } else {
  283.                             $node = new ContextVariable($token->getValue(), $token->getLine());
  284.                         }
  285.                 }
  286.                 break;
  287.             case Token::NUMBER_TYPE:
  288.                 $this->parser->getStream()->next();
  289.                 $node = new ConstantExpression($token->getValue(), $token->getLine());
  290.                 break;
  291.             case Token::STRING_TYPE:
  292.             case Token::INTERPOLATION_START_TYPE:
  293.                 $node $this->parseStringExpression();
  294.                 break;
  295.             case Token::PUNCTUATION_TYPE:
  296.                 $node = match ($token->getValue()) {
  297.                     '[' => $this->parseSequenceExpression(),
  298.                     '{' => $this->parseMappingExpression(),
  299.                     default => throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".'Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext()),
  300.                 };
  301.                 break;
  302.             case Token::OPERATOR_TYPE:
  303.                 if (preg_match(Lexer::REGEX_NAME$token->getValue(), $matches) && $matches[0] == $token->getValue()) {
  304.                     // in this context, string operators are variable names
  305.                     $this->parser->getStream()->next();
  306.                     $node = new ContextVariable($token->getValue(), $token->getLine());
  307.                     break;
  308.                 }
  309.                 if ('=' === $token->getValue() && ('==' === $this->parser->getStream()->look(-1)->getValue() || '!=' === $this->parser->getStream()->look(-1)->getValue())) {
  310.                     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());
  311.                 }
  312.                 // no break
  313.             default:
  314.                 throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".'Token::typeToEnglish($token->getType()), $token->getValue()), $token->getLine(), $this->parser->getStream()->getSourceContext());
  315.         }
  316.         return $this->parsePostfixExpression($node);
  317.     }
  318.     public function parseStringExpression()
  319.     {
  320.         $stream $this->parser->getStream();
  321.         $nodes = [];
  322.         // a string cannot be followed by another string in a single expression
  323.         $nextCanBeString true;
  324.         while (true) {
  325.             if ($nextCanBeString && $token $stream->nextIf(Token::STRING_TYPE)) {
  326.                 $nodes[] = new ConstantExpression($token->getValue(), $token->getLine());
  327.                 $nextCanBeString false;
  328.             } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) {
  329.                 $nodes[] = $this->parseExpression();
  330.                 $stream->expect(Token::INTERPOLATION_END_TYPE);
  331.                 $nextCanBeString true;
  332.             } else {
  333.                 break;
  334.             }
  335.         }
  336.         $expr array_shift($nodes);
  337.         foreach ($nodes as $node) {
  338.             $expr = new ConcatBinary($expr$node$node->getTemplateLine());
  339.         }
  340.         return $expr;
  341.     }
  342.     /**
  343.      * @deprecated since Twig 3.11, use parseSequenceExpression() instead
  344.      */
  345.     public function parseArrayExpression()
  346.     {
  347.         trigger_deprecation('twig/twig''3.11''Calling "%s()" is deprecated, use "parseSequenceExpression()" instead.'__METHOD__);
  348.         return $this->parseSequenceExpression();
  349.     }
  350.     public function parseSequenceExpression()
  351.     {
  352.         $stream $this->parser->getStream();
  353.         $stream->expect(Token::PUNCTUATION_TYPE'[''A sequence element was expected');
  354.         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
  355.         $first true;
  356.         while (!$stream->test(Token::PUNCTUATION_TYPE']')) {
  357.             if (!$first) {
  358.                 $stream->expect(Token::PUNCTUATION_TYPE',''A sequence element must be followed by a comma');
  359.                 // trailing ,?
  360.                 if ($stream->test(Token::PUNCTUATION_TYPE']')) {
  361.                     break;
  362.                 }
  363.             }
  364.             $first false;
  365.             if ($stream->nextIf(Token::SPREAD_TYPE)) {
  366.                 $expr $this->parseExpression();
  367.                 $expr->setAttribute('spread'true);
  368.                 $node->addElement($expr);
  369.             } else {
  370.                 $node->addElement($this->parseExpression());
  371.             }
  372.         }
  373.         $stream->expect(Token::PUNCTUATION_TYPE']''An opened sequence is not properly closed');
  374.         return $node;
  375.     }
  376.     /**
  377.      * @deprecated since Twig 3.11, use parseMappingExpression() instead
  378.      */
  379.     public function parseHashExpression()
  380.     {
  381.         trigger_deprecation('twig/twig''3.11''Calling "%s()" is deprecated, use "parseMappingExpression()" instead.'__METHOD__);
  382.         return $this->parseMappingExpression();
  383.     }
  384.     public function parseMappingExpression()
  385.     {
  386.         $stream $this->parser->getStream();
  387.         $stream->expect(Token::PUNCTUATION_TYPE'{''A mapping element was expected');
  388.         $node = new ArrayExpression([], $stream->getCurrent()->getLine());
  389.         $first true;
  390.         while (!$stream->test(Token::PUNCTUATION_TYPE'}')) {
  391.             if (!$first) {
  392.                 $stream->expect(Token::PUNCTUATION_TYPE',''A mapping value must be followed by a comma');
  393.                 // trailing ,?
  394.                 if ($stream->test(Token::PUNCTUATION_TYPE'}')) {
  395.                     break;
  396.                 }
  397.             }
  398.             $first false;
  399.             if ($stream->nextIf(Token::SPREAD_TYPE)) {
  400.                 $value $this->parseExpression();
  401.                 $value->setAttribute('spread'true);
  402.                 $node->addElement($value);
  403.                 continue;
  404.             }
  405.             // a mapping key can be:
  406.             //
  407.             //  * a number -- 12
  408.             //  * a string -- 'a'
  409.             //  * a name, which is equivalent to a string -- a
  410.             //  * an expression, which must be enclosed in parentheses -- (1 + 2)
  411.             if ($token $stream->nextIf(Token::NAME_TYPE)) {
  412.                 $key = new ConstantExpression($token->getValue(), $token->getLine());
  413.                 // {a} is a shortcut for {a:a}
  414.                 if ($stream->test(Token::PUNCTUATION_TYPE, [',''}'])) {
  415.                     $value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine());
  416.                     $node->addElement($value$key);
  417.                     continue;
  418.                 }
  419.             } elseif (($token $stream->nextIf(Token::STRING_TYPE)) || $token $stream->nextIf(Token::NUMBER_TYPE)) {
  420.                 $key = new ConstantExpression($token->getValue(), $token->getLine());
  421.             } elseif ($stream->test(Token::PUNCTUATION_TYPE'(')) {
  422.                 $key $this->parseExpression();
  423.             } else {
  424.                 $current $stream->getCurrent();
  425.                 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());
  426.             }
  427.             $stream->expect(Token::PUNCTUATION_TYPE':''A mapping key must be followed by a colon (:)');
  428.             $value $this->parseExpression();
  429.             $node->addElement($value$key);
  430.         }
  431.         $stream->expect(Token::PUNCTUATION_TYPE'}''An opened mapping is not properly closed');
  432.         return $node;
  433.     }
  434.     public function parsePostfixExpression($node)
  435.     {
  436.         while (true) {
  437.             $token $this->parser->getCurrentToken();
  438.             if (Token::PUNCTUATION_TYPE == $token->getType()) {
  439.                 if ('.' == $token->getValue() || '[' == $token->getValue()) {
  440.                     $node $this->parseSubscriptExpression($node);
  441.                 } elseif ('|' == $token->getValue()) {
  442.                     $node $this->parseFilterExpression($node);
  443.                 } else {
  444.                     break;
  445.                 }
  446.             } else {
  447.                 break;
  448.             }
  449.         }
  450.         return $node;
  451.     }
  452.     public function getFunctionNode($name$line)
  453.     {
  454.         if (null !== $alias $this->parser->getImportedSymbol('function'$name)) {
  455.             return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $this->createArguments($line), $line);
  456.         }
  457.         $args $this->parseOnlyArguments();
  458.         $function $this->getFunction($name$line);
  459.         if ($function->getParserCallable()) {
  460.             $fakeNode = new EmptyNode($line);
  461.             $fakeNode->setSourceContext($this->parser->getStream()->getSourceContext());
  462.             return ($function->getParserCallable())($this->parser$fakeNode$args$line);
  463.         }
  464.         if (!isset($this->readyNodes[$class $function->getNodeClass()])) {
  465.             $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
  466.         }
  467.         if (!$ready $this->readyNodes[$class]) {
  468.             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);
  469.         }
  470.         return new $class($ready $function $function->getName(), $args$line);
  471.     }
  472.     public function parseSubscriptExpression($node)
  473.     {
  474.         if ('.' === $this->parser->getStream()->next()->getValue()) {
  475.             return $this->parseSubscriptExpressionDot($node);
  476.         }
  477.         return $this->parseSubscriptExpressionArray($node);
  478.     }
  479.     public function parseFilterExpression($node)
  480.     {
  481.         $this->parser->getStream()->next();
  482.         return $this->parseFilterExpressionRaw($node);
  483.     }
  484.     public function parseFilterExpressionRaw($node)
  485.     {
  486.         if (\func_num_args() > 1) {
  487.             trigger_deprecation('twig/twig''3.12''Passing a second argument to "%s()" is deprecated.'__METHOD__);
  488.         }
  489.         while (true) {
  490.             $token $this->parser->getStream()->expect(Token::NAME_TYPE);
  491.             if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE'(')) {
  492.                 $arguments = new EmptyNode();
  493.             } else {
  494.                 $arguments $this->parseOnlyArguments();
  495.             }
  496.             $filter $this->getFilter($token->getValue(), $token->getLine());
  497.             $ready true;
  498.             if (!isset($this->readyNodes[$class $filter->getNodeClass()])) {
  499.                 $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
  500.             }
  501.             if (!$ready $this->readyNodes[$class]) {
  502.                 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);
  503.             }
  504.             $node = new $class($node$ready $filter : new ConstantExpression($filter->getName(), $token->getLine()), $arguments$token->getLine());
  505.             if (!$this->parser->getStream()->test(Token::PUNCTUATION_TYPE'|')) {
  506.                 break;
  507.             }
  508.             $this->parser->getStream()->next();
  509.         }
  510.         return $node;
  511.     }
  512.     /**
  513.      * Parses arguments.
  514.      *
  515.      * @return Node
  516.      *
  517.      * @throws SyntaxError
  518.      */
  519.     public function parseArguments()
  520.     {
  521.         $namedArguments false;
  522.         $definition false;
  523.         if (\func_num_args() > 1) {
  524.             $definition func_get_arg(1);
  525.         }
  526.         if (\func_num_args() > 0) {
  527.             trigger_deprecation('twig/twig''3.15''Passing arguments to "%s()" is deprecated.'__METHOD__);
  528.             $namedArguments func_get_arg(0);
  529.         }
  530.         $args = [];
  531.         $stream $this->parser->getStream();
  532.         $stream->expect(Token::PUNCTUATION_TYPE'(''A list of arguments must begin with an opening parenthesis');
  533.         $hasSpread false;
  534.         while (!$stream->test(Token::PUNCTUATION_TYPE')')) {
  535.             if ($args) {
  536.                 $stream->expect(Token::PUNCTUATION_TYPE',''Arguments must be separated by a comma');
  537.                 // if the comma above was a trailing comma, early exit the argument parse loop
  538.                 if ($stream->test(Token::PUNCTUATION_TYPE')')) {
  539.                     break;
  540.                 }
  541.             }
  542.             if ($definition) {
  543.                 $token $stream->expect(Token::NAME_TYPEnull'An argument must be a name');
  544.                 $value = new ContextVariable($token->getValue(), $this->parser->getCurrentToken()->getLine());
  545.             } else {
  546.                 if ($stream->nextIf(Token::SPREAD_TYPE)) {
  547.                     $hasSpread true;
  548.                     $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine());
  549.                 } elseif ($hasSpread) {
  550.                     throw new SyntaxError('Normal arguments must be placed before argument unpacking.'$stream->getCurrent()->getLine(), $stream->getSourceContext());
  551.                 } else {
  552.                     $value $this->parseExpression();
  553.                 }
  554.             }
  555.             $name null;
  556.             if ($namedArguments && (($token $stream->nextIf(Token::OPERATOR_TYPE'=')) || (!$definition && $token $stream->nextIf(Token::PUNCTUATION_TYPE':')))) {
  557.                 if (!$value instanceof NameExpression) {
  558.                     throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.'\get_class($value)), $token->getLine(), $stream->getSourceContext());
  559.                 }
  560.                 $name $value->getAttribute('name');
  561.                 if ($definition) {
  562.                     $value $this->getPrimary();
  563.                     if (!$this->checkConstantExpression($value)) {
  564.                         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());
  565.                     }
  566.                 } else {
  567.                     $value $this->parseExpression();
  568.                 }
  569.             }
  570.             if ($definition) {
  571.                 if (null === $name) {
  572.                     $name $value->getAttribute('name');
  573.                     $value = new ConstantExpression(null$this->parser->getCurrentToken()->getLine());
  574.                     $value->setAttribute('is_implicit'true);
  575.                 }
  576.                 $args[$name] = $value;
  577.             } else {
  578.                 if (null === $name) {
  579.                     $args[] = $value;
  580.                 } else {
  581.                     $args[$name] = $value;
  582.                 }
  583.             }
  584.         }
  585.         $stream->expect(Token::PUNCTUATION_TYPE')''A list of arguments must be closed by a parenthesis');
  586.         return new Nodes($args);
  587.     }
  588.     public function parseAssignmentExpression()
  589.     {
  590.         $stream $this->parser->getStream();
  591.         $targets = [];
  592.         while (true) {
  593.             $token $this->parser->getCurrentToken();
  594.             if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME$token->getValue())) {
  595.                 // in this context, string operators are variable names
  596.                 $this->parser->getStream()->next();
  597.             } else {
  598.                 $stream->expect(Token::NAME_TYPEnull'Only variables can be assigned to');
  599.             }
  600.             $targets[] = new AssignContextVariable($token->getValue(), $token->getLine());
  601.             if (!$stream->nextIf(Token::PUNCTUATION_TYPE',')) {
  602.                 break;
  603.             }
  604.         }
  605.         return new Nodes($targets);
  606.     }
  607.     public function parseMultitargetExpression()
  608.     {
  609.         $targets = [];
  610.         while (true) {
  611.             $targets[] = $this->parseExpression();
  612.             if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE',')) {
  613.                 break;
  614.             }
  615.         }
  616.         return new Nodes($targets);
  617.     }
  618.     private function parseNotTestExpression(Node $node): NotUnary
  619.     {
  620.         return new NotUnary($this->parseTestExpression($node), $this->parser->getCurrentToken()->getLine());
  621.     }
  622.     private function parseTestExpression(Node $node): TestExpression
  623.     {
  624.         $stream $this->parser->getStream();
  625.         $test $this->getTest($node->getTemplateLine());
  626.         $arguments null;
  627.         if ($stream->test(Token::PUNCTUATION_TYPE'(')) {
  628.             $arguments $this->parseOnlyArguments();
  629.         } elseif ($test->hasOneMandatoryArgument()) {
  630.             $arguments = new Nodes([=> $this->getPrimary()]);
  631.         }
  632.         if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias $this->parser->getImportedSymbol('function'$node->getAttribute('name'))) {
  633.             $node = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
  634.         }
  635.         $ready $test instanceof TwigTest;
  636.         if (!isset($this->readyNodes[$class $test->getNodeClass()])) {
  637.             $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class);
  638.         }
  639.         if (!$ready $this->readyNodes[$class]) {
  640.             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);
  641.         }
  642.         return new $class($node$ready $test $test->getName(), $arguments$this->parser->getCurrentToken()->getLine());
  643.     }
  644.     private function getTest(int $line): TwigTest
  645.     {
  646.         $stream $this->parser->getStream();
  647.         $name $stream->expect(Token::NAME_TYPE)->getValue();
  648.         if ($stream->test(Token::NAME_TYPE)) {
  649.             // try 2-words tests
  650.             $name $name.' '.$this->parser->getCurrentToken()->getValue();
  651.             if ($test $this->env->getTest($name)) {
  652.                 $stream->next();
  653.             }
  654.         } else {
  655.             $test $this->env->getTest($name);
  656.         }
  657.         if (!$test) {
  658.             if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
  659.                 return new TwigTest($name, fn () => '');
  660.             }
  661.             $e = new SyntaxError(\sprintf('Unknown "%s" test.'$name), $line$stream->getSourceContext());
  662.             $e->addSuggestions($namearray_keys($this->env->getTests()));
  663.             throw $e;
  664.         }
  665.         if ($test->isDeprecated()) {
  666.             $stream $this->parser->getStream();
  667.             $src $stream->getSourceContext();
  668.             $test->triggerDeprecation($src->getPath() ?: $src->getName(), $stream->getCurrent()->getLine());
  669.         }
  670.         return $test;
  671.     }
  672.     private function getFunction(string $nameint $line): TwigFunction
  673.     {
  674.         if (!$function $this->env->getFunction($name)) {
  675.             if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
  676.                 return new TwigFunction($name, fn () => '');
  677.             }
  678.             $e = new SyntaxError(\sprintf('Unknown "%s" function.'$name), $line$this->parser->getStream()->getSourceContext());
  679.             $e->addSuggestions($namearray_keys($this->env->getFunctions()));
  680.             throw $e;
  681.         }
  682.         if ($function->isDeprecated()) {
  683.             $src $this->parser->getStream()->getSourceContext();
  684.             $function->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
  685.         }
  686.         return $function;
  687.     }
  688.     private function getFilter(string $nameint $line): TwigFilter
  689.     {
  690.         if (!$filter $this->env->getFilter($name)) {
  691.             if ($this->parser->shouldIgnoreUnknownTwigCallables()) {
  692.                 return new TwigFilter($name, fn () => '');
  693.             }
  694.             $e = new SyntaxError(\sprintf('Unknown "%s" filter.'$name), $line$this->parser->getStream()->getSourceContext());
  695.             $e->addSuggestions($namearray_keys($this->env->getFilters()));
  696.             throw $e;
  697.         }
  698.         if ($filter->isDeprecated()) {
  699.             $src $this->parser->getStream()->getSourceContext();
  700.             $filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
  701.         }
  702.         return $filter;
  703.     }
  704.     // checks that the node only contains "constant" elements
  705.     // to be removed in 4.0
  706.     private function checkConstantExpression(Node $node): bool
  707.     {
  708.         if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression
  709.             || $node instanceof NegUnary || $node instanceof PosUnary
  710.         )) {
  711.             return false;
  712.         }
  713.         foreach ($node as $n) {
  714.             if (!$this->checkConstantExpression($n)) {
  715.                 return false;
  716.             }
  717.         }
  718.         return true;
  719.     }
  720.     private function setDeprecationCheck(bool $deprecationCheck): bool
  721.     {
  722.         $current $this->deprecationCheck;
  723.         $this->deprecationCheck $deprecationCheck;
  724.         return $current;
  725.     }
  726.     private function createArguments(int $line): ArrayExpression
  727.     {
  728.         $arguments = new ArrayExpression([], $line);
  729.         foreach ($this->parseOnlyArguments() as $k => $n) {
  730.             $arguments->addElement($n, new LocalVariable($k$line));
  731.         }
  732.         return $arguments;
  733.     }
  734.     public function parseOnlyArguments()
  735.     {
  736.         $args = [];
  737.         $stream $this->parser->getStream();
  738.         $stream->expect(Token::PUNCTUATION_TYPE'(''A list of arguments must begin with an opening parenthesis');
  739.         $hasSpread false;
  740.         while (!$stream->test(Token::PUNCTUATION_TYPE')')) {
  741.             if ($args) {
  742.                 $stream->expect(Token::PUNCTUATION_TYPE',''Arguments must be separated by a comma');
  743.                 // if the comma above was a trailing comma, early exit the argument parse loop
  744.                 if ($stream->test(Token::PUNCTUATION_TYPE')')) {
  745.                     break;
  746.                 }
  747.             }
  748.             if ($stream->nextIf(Token::SPREAD_TYPE)) {
  749.                 $hasSpread true;
  750.                 $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine());
  751.             } elseif ($hasSpread) {
  752.                 throw new SyntaxError('Normal arguments must be placed before argument unpacking.'$stream->getCurrent()->getLine(), $stream->getSourceContext());
  753.             } else {
  754.                 $value $this->parseExpression();
  755.             }
  756.             $name null;
  757.             if (($token $stream->nextIf(Token::OPERATOR_TYPE'=')) || ($token $stream->nextIf(Token::PUNCTUATION_TYPE':'))) {
  758.                 if (!$value instanceof NameExpression) {
  759.                     throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.'\get_class($value)), $token->getLine(), $stream->getSourceContext());
  760.                 }
  761.                 $name $value->getAttribute('name');
  762.                 $value $this->parseExpression();
  763.             }
  764.             if (null === $name) {
  765.                 $args[] = $value;
  766.             } else {
  767.                 $args[$name] = $value;
  768.             }
  769.         }
  770.         $stream->expect(Token::PUNCTUATION_TYPE')''A list of arguments must be closed by a parenthesis');
  771.         return new Nodes($args);
  772.     }
  773.     private function parseSubscriptExpressionDot(Node $node): AbstractExpression
  774.     {
  775.         $stream $this->parser->getStream();
  776.         $token $stream->getCurrent();
  777.         $lineno $token->getLine();
  778.         $arguments = new ArrayExpression([], $lineno);
  779.         $type Template::ANY_CALL;
  780.         if ($stream->nextIf(Token::PUNCTUATION_TYPE'(')) {
  781.             $attribute $this->parseExpression();
  782.             $stream->expect(Token::PUNCTUATION_TYPE')');
  783.         } else {
  784.             $token $stream->next();
  785.             if (
  786.                 Token::NAME_TYPE == $token->getType()
  787.                 || Token::NUMBER_TYPE == $token->getType()
  788.                 || (Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME$token->getValue()))
  789.             ) {
  790.                 $attribute = new ConstantExpression($token->getValue(), $token->getLine());
  791.             } else {
  792.                 throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.'$token->getValue(), Token::typeToEnglish($token->getType())), $token->getLine(), $stream->getSourceContext());
  793.             }
  794.         }
  795.         if ($stream->test(Token::PUNCTUATION_TYPE'(')) {
  796.             $type Template::METHOD_CALL;
  797.             $arguments $this->createArguments($token->getLine());
  798.         }
  799.         if (
  800.             $node instanceof NameExpression
  801.             && (
  802.                 null !== $this->parser->getImportedSymbol('template'$node->getAttribute('name'))
  803.                 || '_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression
  804.             )
  805.         ) {
  806.             return new MacroReferenceExpression(new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments$node->getTemplateLine());
  807.         }
  808.         return new GetAttrExpression($node$attribute$arguments$type$lineno);
  809.     }
  810.     private function parseSubscriptExpressionArray(Node $node): AbstractExpression
  811.     {
  812.         $stream $this->parser->getStream();
  813.         $token $stream->getCurrent();
  814.         $lineno $token->getLine();
  815.         $arguments = new ArrayExpression([], $lineno);
  816.         // slice?
  817.         $slice false;
  818.         if ($stream->test(Token::PUNCTUATION_TYPE':')) {
  819.             $slice true;
  820.             $attribute = new ConstantExpression(0$token->getLine());
  821.         } else {
  822.             $attribute $this->parseExpression();
  823.         }
  824.         if ($stream->nextIf(Token::PUNCTUATION_TYPE':')) {
  825.             $slice true;
  826.         }
  827.         if ($slice) {
  828.             if ($stream->test(Token::PUNCTUATION_TYPE']')) {
  829.                 $length = new ConstantExpression(null$token->getLine());
  830.             } else {
  831.                 $length $this->parseExpression();
  832.             }
  833.             $filter $this->getFilter('slice'$token->getLine());
  834.             $arguments = new Nodes([$attribute$length]);
  835.             $filter = new ($filter->getNodeClass())($node$filter$arguments$token->getLine());
  836.             $stream->expect(Token::PUNCTUATION_TYPE']');
  837.             return $filter;
  838.         }
  839.         $stream->expect(Token::PUNCTUATION_TYPE']');
  840.         return new GetAttrExpression($node$attribute$argumentsTemplate::ARRAY_CALL$lineno);
  841.     }
  842. }