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'] + 1 : $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 (0 === $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_TYPE, null, '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_TYPE, null, '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([0 => $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($name, array_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 $name, int $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($name, array_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 $name, int $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($name, array_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, $arguments, Template::ARRAY_CALL, $lineno);
  851. }
  852. }