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

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