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

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