From ffcae61b1581fc08dc78fd021cbf7be0e3d3ecdd Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Thu, 4 Jun 2026 13:19:27 +0200 Subject: [PATCH] Move extends validation into correctness visitor --- src/NodeVisitor/CorrectnessNodeVisitor.php | 15 ++++++++++++--- src/Parser.php | 6 +++++- src/TokenParser/ExtendsTokenParser.php | 10 +--------- .../tags/inheritance/multiple_extends.test | 11 +++++++++++ 4 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 tests/Fixtures/tags/inheritance/multiple_extends.test diff --git a/src/NodeVisitor/CorrectnessNodeVisitor.php b/src/NodeVisitor/CorrectnessNodeVisitor.php index e0afd4d60..e74c799be 100644 --- a/src/NodeVisitor/CorrectnessNodeVisitor.php +++ b/src/NodeVisitor/CorrectnessNodeVisitor.php @@ -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; } diff --git a/src/Parser.php b/src/Parser.php index 3daf0cad9..9b30f1ba6 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -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()); } diff --git a/src/TokenParser/ExtendsTokenParser.php b/src/TokenParser/ExtendsTokenParser.php index f6d9d0289..cba2927f9 100644 --- a/src/TokenParser/ExtendsTokenParser.php +++ b/src/TokenParser/ExtendsTokenParser.php @@ -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()); diff --git a/tests/Fixtures/tags/inheritance/multiple_extends.test b/tests/Fixtures/tags/inheritance/multiple_extends.test new file mode 100644 index 000000000..4f7d4d0c0 --- /dev/null +++ b/tests/Fixtures/tags/inheritance/multiple_extends.test @@ -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.