vendor/twig/twig/src/Parser.php line 644

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\ExpressionParser\ExpressionParserInterface;
  14. use Twig\ExpressionParser\ExpressionParsers;
  15. use Twig\ExpressionParser\ExpressionParserType;
  16. use Twig\ExpressionParser\InfixExpressionParserInterface;
  17. use Twig\ExpressionParser\Prefix\LiteralExpressionParser;
  18. use Twig\ExpressionParser\PrefixExpressionParserInterface;
  19. use Twig\Node\BlockNode;
  20. use Twig\Node\BlockReferenceNode;
  21. use Twig\Node\BodyNode;
  22. use Twig\Node\EmptyNode;
  23. use Twig\Node\Expression\AbstractExpression;
  24. use Twig\Node\Expression\Variable\AssignMacroVariable;
  25. use Twig\Node\Expression\Variable\MacroVariable;
  26. use Twig\Node\MacroNode;
  27. use Twig\Node\MacrosNode;
  28. use Twig\Node\ModuleNode;
  29. use Twig\Node\Node;
  30. use Twig\Node\NodeDocumentation;
  31. use Twig\Node\Nodes;
  32. use Twig\Node\PrintNode;
  33. use Twig\Node\TextNode;
  34. use Twig\TokenParser\TokenParserInterface;
  35. use Twig\Util\ReflectionCallable;
  36. /**
  37. * @author Fabien Potencier <fabien@symfony.com>
  38. */
  39. class Parser
  40. {
  41. private $stack = [];
  42. private ?\WeakMap $expressionRefs = null;
  43. private $stream;
  44. private $parent;
  45. private $visitors;
  46. private $expressionParser;
  47. private $blocks;
  48. private $blockStack;
  49. /** @var list<Node|null> */
  50. private array $documentationTargets = [];
  51. private $macros;
  52. private $importedSymbols;
  53. private $traits;
  54. private $embeddedTemplates = [];
  55. private int $lastEmbedIndex = 0;
  56. private $varNameSalt = 0;
  57. private $ignoreUnknownTwigCallables = false;
  58. private ExpressionParsers $parsers;
  59. public function __construct(
  60. private Environment $env,
  61. ) {
  62. $this->parsers = $env->getExpressionParsers();
  63. }
  64. public function getEnvironment(): Environment
  65. {
  66. return $this->env;
  67. }
  68. public function getVarName(): string
  69. {
  70. trigger_deprecation('twig/twig', '3.15', 'The "%s()" method is deprecated.', __METHOD__);
  71. return \sprintf('__internal_parse_%d', $this->varNameSalt++);
  72. }
  73. /**
  74. * @throws SyntaxError
  75. */
  76. public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode
  77. {
  78. // reset on root parse() calls only, so the counter spans nested/reentrant parses
  79. if (!$this->stack) {
  80. $this->lastEmbedIndex = 0;
  81. }
  82. $vars = get_object_vars($this);
  83. unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['lastEmbedIndex'], $vars['varNameSalt']);
  84. $this->stack[] = $vars;
  85. // node visitors
  86. if (null === $this->visitors) {
  87. $this->visitors = $this->env->getNodeVisitors();
  88. }
  89. $this->stream = $stream;
  90. $this->parent = null;
  91. $this->blocks = [];
  92. $this->documentationTargets = [];
  93. $this->macros = [];
  94. $this->traits = [];
  95. $this->blockStack = [];
  96. $this->importedSymbols = [[]];
  97. $this->embeddedTemplates = [];
  98. $this->expressionRefs = new \WeakMap();
  99. try {
  100. $body = $this->subparse($test, $dropNeedle);
  101. } catch (SyntaxError $e) {
  102. if (!$e->getSourceContext()) {
  103. $e->setSourceContext($this->stream->getSourceContext());
  104. }
  105. if (!$e->getTemplateLine()) {
  106. $e->setTemplateLine($this->getCurrentToken()->getLine());
  107. }
  108. throw $e;
  109. } finally {
  110. $this->expressionRefs = null;
  111. }
  112. if ($this->parent) {
  113. $body = $this->cleanupBodyForChildTemplates($body);
  114. }
  115. $node = new ModuleNode(
  116. new BodyNode([$body]),
  117. $this->parent,
  118. $this->blocks ? new Nodes($this->blocks) : new EmptyNode(),
  119. new MacrosNode($this->macros),
  120. $this->traits ? new Nodes($this->traits) : new EmptyNode(),
  121. $this->embeddedTemplates ? new Nodes($this->embeddedTemplates) : new EmptyNode(),
  122. $stream->getSourceContext(),
  123. );
  124. $traverser = new NodeTraverser($this->env, $this->visitors);
  125. /**
  126. * @var ModuleNode $node
  127. */
  128. $node = $traverser->traverse($node);
  129. // restore previous stack so previous parse() call can resume working
  130. foreach (array_pop($this->stack) as $key => $val) {
  131. $this->$key = $val;
  132. }
  133. return $node;
  134. }
  135. public function shouldIgnoreUnknownTwigCallables(): bool
  136. {
  137. return $this->ignoreUnknownTwigCallables;
  138. }
  139. public function subparseIgnoreUnknownTwigCallables($test, bool $dropNeedle = false): void
  140. {
  141. $previous = $this->ignoreUnknownTwigCallables;
  142. $this->ignoreUnknownTwigCallables = true;
  143. try {
  144. $this->subparse($test, $dropNeedle);
  145. } finally {
  146. $this->ignoreUnknownTwigCallables = $previous;
  147. }
  148. }
  149. /**
  150. * @throws SyntaxError
  151. */
  152. public function subparse($test, bool $dropNeedle = false): Node
  153. {
  154. $lineno = $this->getCurrentToken()->getLine();
  155. $rv = [];
  156. while (!$this->stream->isEOF()) {
  157. switch (true) {
  158. case $this->stream->getCurrent()->test(Token::TEXT_TYPE):
  159. $token = $this->stream->next();
  160. $rv[] = new TextNode($token->getValue(), $token->getLine());
  161. break;
  162. case $this->stream->getCurrent()->test(Token::VAR_START_TYPE):
  163. $token = $this->stream->next();
  164. $expr = $this->parseExpression();
  165. $this->stream->expect(Token::VAR_END_TYPE);
  166. $node = new PrintNode($expr, $token->getLine());
  167. NodeDocumentation::add($node, $token);
  168. $rv[] = $node;
  169. break;
  170. case $this->stream->getCurrent()->test(Token::BLOCK_START_TYPE):
  171. $startToken = $this->stream->next();
  172. $token = $this->getCurrentToken();
  173. if (!$token->test(Token::NAME_TYPE)) {
  174. throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext());
  175. }
  176. if (null !== $test && $test($token)) {
  177. if ($dropNeedle) {
  178. $this->stream->next();
  179. }
  180. if (1 === \count($rv)) {
  181. return $rv[0];
  182. }
  183. return new Nodes($rv, $lineno);
  184. }
  185. if (!$subparser = $this->env->getTokenParser($token->getValue())) {
  186. if (null !== $test) {
  187. $e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
  188. $callable = (new ReflectionCallable(new TwigTest('decision', $test)))->getCallable();
  189. if (\is_array($callable) && $callable[0] instanceof TokenParserInterface) {
  190. $e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $callable[0]->getTag(), $lineno));
  191. }
  192. } else {
  193. $e = new SyntaxError(\sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
  194. $e->addSuggestions($token->getValue(), array_keys($this->env->getTokenParsers()));
  195. }
  196. throw $e;
  197. }
  198. $this->stream->next();
  199. $subparser->setParser($this);
  200. $documentationTargetIndex = \count($this->documentationTargets);
  201. $this->documentationTargets[] = null;
  202. try {
  203. $node = $subparser->parse($token);
  204. $documentationTarget = $this->documentationTargets[$documentationTargetIndex];
  205. } finally {
  206. array_pop($this->documentationTargets);
  207. }
  208. if (!$node) {
  209. trigger_deprecation('twig/twig', '3.12', 'Returning "null" from "%s" is deprecated and forbidden by "TokenParserInterface".', $subparser::class);
  210. } else {
  211. $node->setNodeTag($subparser->getTag());
  212. NodeDocumentation::add($node, $startToken);
  213. if (null !== $documentationTarget && $node !== $documentationTarget) {
  214. NodeDocumentation::move($node, $documentationTarget);
  215. }
  216. $rv[] = $node;
  217. }
  218. break;
  219. default:
  220. throw new SyntaxError('The lexer or the parser ended up in an unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext());
  221. }
  222. }
  223. if (1 === \count($rv)) {
  224. return $rv[0];
  225. }
  226. return new Nodes($rv, $lineno);
  227. }
  228. public function getBlockStack(): array
  229. {
  230. trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
  231. return $this->blockStack;
  232. }
  233. /**
  234. * @return string|null
  235. */
  236. public function peekBlockStack()
  237. {
  238. return $this->blockStack[\count($this->blockStack) - 1] ?? null;
  239. }
  240. public function popBlockStack(): void
  241. {
  242. array_pop($this->blockStack);
  243. }
  244. public function pushBlockStack($name): void
  245. {
  246. $this->blockStack[] = $name;
  247. }
  248. public function hasBlock(string $name): bool
  249. {
  250. trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
  251. return isset($this->blocks[$name]);
  252. }
  253. public function getBlock(string $name): Node
  254. {
  255. trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
  256. return $this->blocks[$name];
  257. }
  258. public function setBlock(string $name, BlockNode $value): void
  259. {
  260. if (isset($this->blocks[$name])) {
  261. throw new SyntaxError(\sprintf("The block '%s' has already been defined line %d.", $name, $this->blocks[$name]->getTemplateLine()), $this->getCurrentToken()->getLine(), $this->blocks[$name]->getSourceContext());
  262. }
  263. $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine());
  264. }
  265. public function hasMacro(string $name): bool
  266. {
  267. trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
  268. return isset($this->macros[$name]);
  269. }
  270. public function setDocumentationTarget(Node $node): void
  271. {
  272. if (null === $index = array_key_last($this->documentationTargets)) {
  273. throw new \LogicException('A documentation target can only be set while parsing a tag.');
  274. }
  275. if (null !== $this->documentationTargets[$index]) {
  276. throw new \LogicException('The documentation target for a tag can only be set once.');
  277. }
  278. $this->documentationTargets[$index] = $node;
  279. }
  280. public function setMacro(string $name, MacroNode $node): void
  281. {
  282. if (isset($this->macros[$name])) {
  283. trigger_deprecation('twig/twig', '3.29', 'Defining the macro "%s" more than once in "%s" is deprecated and will throw a SyntaxError in Twig 4.0 (previous definition at line %d, new definition at line %d). The last definition is used in Twig 3.', $name, $this->stream->getSourceContext()->getName(), $this->macros[$name]->getTemplateLine(), $node->getTemplateLine());
  284. }
  285. $this->macros[$name] = $node;
  286. }
  287. public function addTrait($trait): void
  288. {
  289. $this->traits[] = $trait;
  290. }
  291. public function hasTraits(): bool
  292. {
  293. trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
  294. return \count($this->traits) > 0;
  295. }
  296. /**
  297. * @return void
  298. */
  299. public function embedTemplate(ModuleNode $template)
  300. {
  301. $template->setIndex(++$this->lastEmbedIndex);
  302. $this->embeddedTemplates[] = $template;
  303. }
  304. public function addImportedSymbol(string $type, string $alias, ?string $name = null, AbstractExpression|AssignMacroVariable|null $internalRef = null): void
  305. {
  306. if ($internalRef && !$internalRef instanceof AssignMacroVariable) {
  307. trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance as an internal reference is deprecated ("%s" given).', __METHOD__, AssignMacroVariable::class, $internalRef::class);
  308. $internalRef = new AssignMacroVariable(new MacroVariable($internalRef->getAttribute('name'), $internalRef->getTemplateLine()), $internalRef->getAttribute('global'));
  309. }
  310. $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $internalRef];
  311. }
  312. /**
  313. * @return array{name: string, node: AssignMacroVariable|null}|null
  314. */
  315. public function getImportedSymbol(string $type, string $alias)
  316. {
  317. // if the symbol does not exist in the current scope (0), try in the main/global scope (last index)
  318. return $this->importedSymbols[0][$type][$alias] ?? ($this->importedSymbols[\count($this->importedSymbols) - 1][$type][$alias] ?? null);
  319. }
  320. public function isMainScope(): bool
  321. {
  322. return 1 === \count($this->importedSymbols);
  323. }
  324. public function pushLocalScope(): void
  325. {
  326. array_unshift($this->importedSymbols, []);
  327. }
  328. public function popLocalScope(): void
  329. {
  330. array_shift($this->importedSymbols);
  331. }
  332. /**
  333. * @deprecated since Twig 3.21
  334. */
  335. public function getExpressionParser(): ExpressionParser
  336. {
  337. trigger_deprecation('twig/twig', '3.21', 'Method "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__);
  338. if (null === $this->expressionParser) {
  339. $this->expressionParser = new ExpressionParser($this, $this->env);
  340. }
  341. return $this->expressionParser;
  342. }
  343. public function parseExpression(int $precedence = 0): AbstractExpression
  344. {
  345. $token = $this->getCurrentToken();
  346. if ($token->test(Token::OPERATOR_TYPE) && $ep = $this->parsers->getByName(PrefixExpressionParserInterface::class, $token->getValue())) {
  347. $this->getStream()->next();
  348. $expr = $ep->parse($this, $token);
  349. $this->checkPrecedenceDeprecations($ep, $expr);
  350. } else {
  351. $expr = $this->parsers->getByClass(LiteralExpressionParser::class)->parse($this, $token);
  352. }
  353. $token = $this->getCurrentToken();
  354. while ($token->test(Token::OPERATOR_TYPE) && ($ep = $this->parsers->getByName(InfixExpressionParserInterface::class, $token->getValue())) && $ep->getPrecedence() >= $precedence) {
  355. $this->getStream()->next();
  356. $expr = $ep->parse($this, $expr, $token);
  357. $this->checkPrecedenceDeprecations($ep, $expr);
  358. $token = $this->getCurrentToken();
  359. }
  360. return $expr;
  361. }
  362. public function getParent(): ?Node
  363. {
  364. trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);
  365. return $this->parent;
  366. }
  367. /**
  368. * @return bool
  369. */
  370. public function hasInheritance()
  371. {
  372. return $this->parent || 0 < \count($this->traits);
  373. }
  374. public function setParent(?Node $parent, bool $throwOnMultiple = true): void
  375. {
  376. if (null === $parent) {
  377. trigger_deprecation('twig/twig', '3.12', 'Passing "null" to "%s()" is deprecated.', __METHOD__);
  378. }
  379. if (null !== $parent && !$parent instanceof AbstractExpression) {
  380. trigger_deprecation('twig/twig', '3.24', 'Passing a "%s" instance to "%s()" is deprecated, pass an "AbstractExpression" instance instead.', $parent::class, __METHOD__);
  381. }
  382. if (null !== $this->parent) {
  383. if (!$throwOnMultiple) {
  384. return;
  385. }
  386. throw new SyntaxError('Multiple extends tags are forbidden.', $parent->getTemplateLine(), $parent->getSourceContext());
  387. }
  388. $this->parent = $parent;
  389. }
  390. public function getStream(): TokenStream
  391. {
  392. return $this->stream;
  393. }
  394. public function getCurrentToken(): Token
  395. {
  396. return $this->stream->getCurrent();
  397. }
  398. public function getFunction(string $name, int $line): TwigFunction
  399. {
  400. try {
  401. $function = $this->env->getFunction($name);
  402. } catch (SyntaxError $e) {
  403. if (!$this->shouldIgnoreUnknownTwigCallables()) {
  404. throw $e;
  405. }
  406. $function = null;
  407. }
  408. if (!$function) {
  409. if ($this->shouldIgnoreUnknownTwigCallables()) {
  410. return new TwigFunction($name, static fn () => '');
  411. }
  412. $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->stream->getSourceContext());
  413. $e->addSuggestions($name, array_keys($this->env->getFunctions()));
  414. throw $e;
  415. }
  416. if ($function->isDeprecated()) {
  417. $src = $this->stream->getSourceContext();
  418. $function->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
  419. }
  420. return $function;
  421. }
  422. public function getFilter(string $name, int $line): TwigFilter
  423. {
  424. try {
  425. $filter = $this->env->getFilter($name);
  426. } catch (SyntaxError $e) {
  427. if (!$this->shouldIgnoreUnknownTwigCallables()) {
  428. throw $e;
  429. }
  430. $filter = null;
  431. }
  432. if (!$filter) {
  433. if ($this->shouldIgnoreUnknownTwigCallables()) {
  434. return new TwigFilter($name, static fn () => '');
  435. }
  436. $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->stream->getSourceContext());
  437. $e->addSuggestions($name, array_keys($this->env->getFilters()));
  438. throw $e;
  439. }
  440. if ($filter->isDeprecated()) {
  441. $src = $this->stream->getSourceContext();
  442. $filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line);
  443. }
  444. return $filter;
  445. }
  446. public function getTest(int $line): TwigTest
  447. {
  448. $name = $this->stream->expect(Token::NAME_TYPE)->getValue();
  449. if ($this->stream->test(Token::NAME_TYPE)) {
  450. // try 2-words tests
  451. $name = $name.' '.$this->getCurrentToken()->getValue();
  452. try {
  453. $test = $this->env->getTest($name);
  454. } catch (SyntaxError $e) {
  455. if (!$this->shouldIgnoreUnknownTwigCallables()) {
  456. throw $e;
  457. }
  458. $test = null;
  459. }
  460. $this->stream->next();
  461. } else {
  462. try {
  463. $test = $this->env->getTest($name);
  464. } catch (SyntaxError $e) {
  465. if (!$this->shouldIgnoreUnknownTwigCallables()) {
  466. throw $e;
  467. }
  468. $test = null;
  469. }
  470. }
  471. if (!$test) {
  472. if ($this->shouldIgnoreUnknownTwigCallables()) {
  473. return new TwigTest($name, static fn () => '');
  474. }
  475. $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $this->stream->getSourceContext());
  476. $e->addSuggestions($name, array_keys($this->env->getTests()));
  477. throw $e;
  478. }
  479. if ($test->isDeprecated()) {
  480. $src = $this->stream->getSourceContext();
  481. $test->triggerDeprecation($src->getPath() ?: $src->getName(), $this->stream->getCurrent()->getLine());
  482. }
  483. return $test;
  484. }
  485. private function cleanupBodyForChildTemplates(Node $body): Node
  486. {
  487. if ($body instanceof BlockReferenceNode) {
  488. return new EmptyNode();
  489. }
  490. if ($body instanceof TextNode && $body->isBlank()) {
  491. return new EmptyNode();
  492. }
  493. foreach ($body as $k => $node) {
  494. if ($node instanceof BlockReferenceNode) {
  495. // as it has a parent, the block reference won't be used
  496. $body->removeNode($k);
  497. } elseif ($node instanceof TextNode && $node->isBlank()) {
  498. // remove nodes considered as "empty"
  499. $body->removeNode($k);
  500. }
  501. }
  502. return $body;
  503. }
  504. private function checkPrecedenceDeprecations(ExpressionParserInterface $expressionParser, AbstractExpression $expr): void
  505. {
  506. $this->expressionRefs[$expr] = $expressionParser;
  507. $precedenceChanges = $this->parsers->getPrecedenceChanges();
  508. // Check that the all nodes that are between the 2 precedences have explicit parentheses
  509. if (!isset($precedenceChanges[$expressionParser])) {
  510. return;
  511. }
  512. if ($expr->hasExplicitParentheses()) {
  513. return;
  514. }
  515. if ($expressionParser instanceof PrefixExpressionParserInterface) {
  516. /** @var AbstractExpression $node */
  517. $node = $expr->getNode('node');
  518. foreach ($precedenceChanges as $ep => $changes) {
  519. if (!\in_array($expressionParser, $changes, true)) {
  520. continue;
  521. }
  522. if (isset($this->expressionRefs[$node]) && $ep === $this->expressionRefs[$node]) {
  523. $change = $expressionParser->getPrecedenceChange();
  524. trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('As the "%s" %s operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "%s" at line %d.', $expressionParser->getName(), ExpressionParserType::getType($expressionParser)->value, $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
  525. }
  526. }
  527. }
  528. foreach ($precedenceChanges[$expressionParser] as $ep) {
  529. foreach ($expr as $node) {
  530. /** @var AbstractExpression $node */
  531. if (isset($this->expressionRefs[$node]) && $ep === $this->expressionRefs[$node] && !$node->hasExplicitParentheses()) {
  532. $change = $ep->getPrecedenceChange();
  533. trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('As the "%s" %s operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "%s" at line %d.', $ep->getName(), ExpressionParserType::getType($ep)->value, $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine()));
  534. }
  535. }
  536. }
  537. }
  538. }