Fix the loop variable of a for tag being undefined in the sequence of a nested loop

This commit is contained in:
Fabien Potencier
2026-09-26 10:54:52 +02:00
parent 4241bb73fe
commit 0c90971a98
4 changed files with 34 additions and 2 deletions
+1
View File
@@ -1,5 +1,6 @@
# 3.30.1 (2026-XX-XX)
* Fix the `loop` variable being undefined when used in the sequence of a nested `for` tag
* 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
+19 -2
View File
@@ -47,6 +47,8 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
private $loops = [];
private $loopsTargets = [];
/** @var list<array{Node, ForNode, string, string}> */
private array $suspendedLoops = [];
/**
* @param int $optimizers The optimizer mode
@@ -129,7 +131,16 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
array_unshift($this->loops, $node);
array_unshift($this->loopsTargets, $node->getNode('value_target')->getAttribute('name'));
array_unshift($this->loopsTargets, $node->getNode('key_target')->getAttribute('name'));
} elseif (!$this->loops) {
return;
}
// the sequence of a loop is evaluated in the scope enclosing the loop
if ($this->loops && $node === $this->loops[0]->getNode('seq')) {
array_unshift($this->suspendedLoops, [$node, array_shift($this->loops), array_shift($this->loopsTargets), array_shift($this->loopsTargets)]);
}
if (!$this->loops) {
// we are outside a loop
return;
}
@@ -137,7 +148,7 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
// when do we need to add the loop variable back?
// the loop variable is referenced for the current loop
elseif ($node instanceof ContextVariable && 'loop' === $node->getAttribute('name')) {
if ($node instanceof ContextVariable && 'loop' === $node->getAttribute('name')) {
$node->setAttribute('always_defined', true);
$this->addLoopToCurrent();
}
@@ -187,6 +198,12 @@ final class OptimizerNodeVisitor implements NodeVisitorInterface
*/
private function leaveOptimizeFor(Node $node): void
{
if ($this->suspendedLoops && $node === $this->suspendedLoops[0][0]) {
[, $loop, $keyTarget, $valueTarget] = array_shift($this->suspendedLoops);
array_unshift($this->loops, $loop);
array_unshift($this->loopsTargets, $keyTarget, $valueTarget);
}
if ($node instanceof ForNode) {
array_shift($this->loops);
array_shift($this->loopsTargets);
@@ -0,0 +1,10 @@
--TEST--
"for" tag gives access to the loop variable of the enclosing loop in the sequence of a nested loop
--TEMPLATE--
{% for i in ['a', 'b'] %}{% for j in [loop.index] %}{{ i }}{{ j }}{% endfor %}{% endfor %}
--DATA--
return []
--CONFIG--
return ['strict_variables' => true]
--EXPECT--
a1b2
+4
View File
@@ -124,6 +124,10 @@ class OptimizerTest extends TestCase
['{% for i in foo %}{% for j in foo %}{{ loop.parent.loop.index }}{% endfor %}{% endfor %}', ['i' => true, 'j' => true]],
['{% for i in foo %}{% for j in [loop.index] %}{% endfor %}{% endfor %}', ['i' => true, 'j' => false]],
['{% for i in foo %}{% for j in loop %}{% endfor %}{% endfor %}', ['i' => true, 'j' => false]],
['{% for i in foo %}{% set l = loop %}{% for j in foo %}{{ l.index }}{% endfor %}{% endfor %}', ['i' => true, 'j' => false]],
['{% for i in foo %}{% for j in foo %}{{ foo.parent.loop.index }}{% endfor %}{% endfor %}', ['i' => false, 'j' => false]],