Move extends validation into correctness visitor

This commit is contained in:
Fabien Potencier
2026-06-04 13:19:27 +02:00
parent d96eac3895
commit ffcae61b15
4 changed files with 29 additions and 13 deletions
+12 -3
View File
@@ -42,6 +42,7 @@ final class CorrectnessNodeVisitor implements NodeVisitorInterface
private bool $hasParent = false;
private int $blockDepth = 0;
private int $macroDepth = 0;
private ?ConfigNode $extendsNode = null;
public function enterNode(Node $node, Environment $env): Node
{
@@ -52,6 +53,7 @@ final class CorrectnessNodeVisitor implements NodeVisitorInterface
$this->tagStack = [];
$this->blockDepth = 0;
$this->macroDepth = 0;
$this->extendsNode = null;
$body = $node->getNode('body')->getNode('0');
// Parser::subparse() does not wrap the parsed nodes when there is only one,
@@ -77,15 +79,21 @@ final class CorrectnessNodeVisitor implements NodeVisitorInterface
$this->tagStack[] = $node;
}
if ($node instanceof ConfigNode && !isset($this->rootNodes[$node])) {
if ($node instanceof ConfigNode && 'extends' === $node->getNodeTag()) {
// "extends" inside a "block" or a "macro" has always been a hard error; keep it
if ('extends' === $node->getNodeTag() && $this->blockDepth) {
if ($this->blockDepth) {
throw new SyntaxError('Cannot use "extend" in a block.', $node->getTemplateLine(), $node->getSourceContext());
}
if ('extends' === $node->getNodeTag() && $this->macroDepth) {
if ($this->macroDepth) {
throw new SyntaxError('Cannot use "extend" in a macro.', $node->getTemplateLine(), $node->getSourceContext());
}
if ($this->extendsNode) {
throw new SyntaxError('Multiple extends tags are forbidden.', $node->getTemplateLine(), $node->getSourceContext());
}
$this->extendsNode = $node;
}
if ($node instanceof ConfigNode && !isset($this->rootNodes[$node])) {
trigger_deprecation('twig/twig', '3.27', 'Using the "%s" tag outside the root of a template is deprecated in %s at line %d.', $node->getNodeTag(), $node->getSourceContext()->getName(), $node->getTemplateLine());
}
@@ -112,6 +120,7 @@ final class CorrectnessNodeVisitor implements NodeVisitorInterface
if ($node instanceof ModuleNode) {
$this->rootNodes = null;
$this->hasParent = false;
$this->extendsNode = null;
return $node;
}
+5 -1
View File
@@ -414,7 +414,7 @@ class Parser
return $this->parent || 0 < \count($this->traits);
}
public function setParent(?Node $parent): void
public function setParent(?Node $parent, bool $throwOnMultiple = true): void
{
if (null === $parent) {
trigger_deprecation('twig/twig', '3.12', 'Passing "null" to "%s()" is deprecated.', __METHOD__);
@@ -425,6 +425,10 @@ class Parser
}
if (null !== $this->parent) {
if (!$throwOnMultiple) {
return;
}
throw new SyntaxError('Multiple extends tags are forbidden.', $parent->getTemplateLine(), $parent->getSourceContext());
}
+1 -9
View File
@@ -12,7 +12,6 @@
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\ConfigNode;
use Twig\Node\Node;
use Twig\Token;
@@ -29,14 +28,7 @@ final class ExtendsTokenParser extends AbstractTokenParser
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
if ($this->parser->peekBlockStack()) {
throw new SyntaxError('Cannot use "extend" in a block.', $token->getLine(), $stream->getSourceContext());
} elseif (!$this->parser->isMainScope()) {
throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext());
}
$this->parser->setParent($this->parser->parseExpression());
$this->parser->setParent($this->parser->parseExpression(), false);
$stream->expect(Token::BLOCK_END_TYPE);
return new ConfigNode($token->getLine());
@@ -0,0 +1,11 @@
--TEST--
Multiple "extends" tags are forbidden
--TEMPLATE--
{% extends "base.twig" %}
{% extends "other.twig" %}
--TEMPLATE(base.twig)--
--TEMPLATE(other.twig)--
--DATA--
return []
--EXCEPTION--
Twig\Error\SyntaxError: Multiple extends tags are forbidden in "index.twig" at line 3.