From d5011f4654042f03d8986b5b3ba12a4d85f976f8 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 26 Sep 2026 09:07:40 +0200 Subject: [PATCH] Discard what the non-selected branch of a guard tag registers on the parser --- CHANGELOG | 1 + src/Parser.php | 5 +++++ src/TokenParser/GuardTokenParser.php | 6 +++++- tests/Fixtures/tags/guard/discarded_branch.test | 9 +++++++++ .../tags/guard/discarded_parent.legacy.test | 15 +++++++++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/tags/guard/discarded_branch.test create mode 100644 tests/Fixtures/tags/guard/discarded_parent.legacy.test diff --git a/CHANGELOG b/CHANGELOG index 8f423332e..d40bf0913 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ * Reject a deprecated `Template` instance created by another environment in `Environment::resolveTemplate()` * Fix a compilation error inside an `autoescape` tag leaking its escaping strategy into the next compiled template + * Fix the non-selected branch of a `guard` tag registering its parent, blocks, and macros * Speed up reading object attributes backed by getters or class constants * Fix the sandbox not reporting the line of a rejected `guard` tag diff --git a/src/Parser.php b/src/Parser.php index ddd7fe2cd..8a44deb83 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -159,14 +159,19 @@ class Parser return $this->ignoreUnknownTwigCallables; } + /** + * Parses a discarded part of the template: what it registers (parent, blocks, macros, ...) is discarded as well. + */ public function subparseIgnoreUnknownTwigCallables($test, bool $dropNeedle = false): void { $previous = $this->ignoreUnknownTwigCallables; + $state = [$this->parent, $this->blocks, $this->documentationTargets, $this->macros, $this->importedSymbols, $this->traits, $this->embeddedTemplates]; $this->ignoreUnknownTwigCallables = true; try { $this->subparse($test, $dropNeedle); } finally { $this->ignoreUnknownTwigCallables = $previous; + [$this->parent, $this->blocks, $this->documentationTargets, $this->macros, $this->importedSymbols, $this->traits, $this->embeddedTemplates] = $state; } } diff --git a/src/TokenParser/GuardTokenParser.php b/src/TokenParser/GuardTokenParser.php index 7aba0f0d9..e5d93ac75 100644 --- a/src/TokenParser/GuardTokenParser.php +++ b/src/TokenParser/GuardTokenParser.php @@ -55,7 +55,11 @@ final class GuardTokenParser extends AbstractTokenParser $else = new EmptyNode(); if ('else' === $stream->next()->getValue()) { $stream->expect(Token::BLOCK_END_TYPE); - $else = $this->parser->subparse([$this, 'decideGuardEnd'], true); + if ($exists) { + $this->parser->subparseIgnoreUnknownTwigCallables([$this, 'decideGuardEnd'], true); + } else { + $else = $this->parser->subparse([$this, 'decideGuardEnd'], true); + } } $stream->expect(Token::BLOCK_END_TYPE); diff --git a/tests/Fixtures/tags/guard/discarded_branch.test b/tests/Fixtures/tags/guard/discarded_branch.test new file mode 100644 index 000000000..7ccfd5bdd --- /dev/null +++ b/tests/Fixtures/tags/guard/discarded_branch.test @@ -0,0 +1,9 @@ +--TEST-- +"guard" ignores the blocks and macros defined in the discarded branch +--TEMPLATE-- +{% guard function constant %}{% else %}{% block discarded %}{% endblock %}{% macro discarded() %}{% endmacro %}{% endguard %} +{{ block('discarded') is defined ? 'NEVER' : 'no block' }} {{ _self.discarded is defined ? 'NEVER' : 'no macro' }} +--DATA-- +return [] +--EXPECT-- +no block no macro diff --git a/tests/Fixtures/tags/guard/discarded_parent.legacy.test b/tests/Fixtures/tags/guard/discarded_parent.legacy.test new file mode 100644 index 000000000..52a29c555 --- /dev/null +++ b/tests/Fixtures/tags/guard/discarded_parent.legacy.test @@ -0,0 +1,15 @@ +--TEST-- +"guard" ignores the parent defined in the discarded branch +--DEPRECATION-- +Since twig/twig 3.28: Using the "extends" tag outside the root of a template is deprecated in index.twig at line 2. +--TEMPLATE-- +{% guard function foobar %}{% extends "never.twig" %}{% else %}{% extends "layout.twig" %}{% endguard %} +{% block content %}content{% endblock %} +--TEMPLATE(layout.twig)-- +layout {{ block('content') }} +--TEMPLATE(never.twig)-- +NEVER +--DATA-- +return [] +--EXPECT-- +layout content