mirror of
https://github.com/twigphp/Twig.git
synced 2026-10-02 09:56:49 +00:00
Fix the loop variable of a for tag being undefined in the sequence of a nested loop
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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]],
|
||||
|
||||
Reference in New Issue
Block a user