diff --git a/common/jinja/runtime.cpp b/common/jinja/runtime.cpp index f81d98d9..4a228f91 100644 --- a/common/jinja/runtime.cpp +++ b/common/jinja/runtime.cpp @@ -596,11 +596,18 @@ value for_statement::execute_impl(context & ctx) { loop_obj->insert("length", mk_val(filtered_items.size())); loop_obj->insert("previtem", i > 0 ? filtered_items[i - 1] : mk_val("previtem")); loop_obj->insert("nextitem", i < filtered_items.size() - 1 ? filtered_items[i + 1] : mk_val("nextitem")); - scope.set_val("loop", loop_obj); - scope_update_fns[i](scope); + // Use a fresh scope for each iteration so that {% set %} variables + // (including ones assigned only conditionally inside the body) do not + // leak across iterations. This matches standard Jinja2 semantics, where + // each loop iteration starts with a clean scope. State that must + // accumulate across iterations has to use namespace(), whose mutations + // are applied to the shared object referenced from the enclosing scope. + context iter_scope(scope); + iter_scope.set_val("loop", loop_obj); + scope_update_fns[i](iter_scope); try { for (auto & stmt : body) { - value val = stmt->execute(scope); + value val = stmt->execute(iter_scope); result->push_back(val); } } catch (const continue_statement::signal &) { diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 4b490dc5..6be42ea3 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -360,6 +360,18 @@ static void test_loops(testing & t) { json::object(), "012" ); + + // {% set %} of a non-loop variable inside a loop body must not leak across + // iterations. When a variable is only assigned on some iterations (here via + // a conditional branch), the engine must still treat it as undefined on the + // iterations where the branch is not taken, rather than reusing the value + // from a previous iteration. This matches standard Jinja2 semantics, where + // each loop iteration starts with a fresh scope. + test_template(t, "conditional set does not leak across iterations", + "{%- for m in msgs %}{% if m.x %}{% set r = m.x %}{% endif %}[{{ r|default('-') }}]{% endfor %}", + {{"msgs", json::array({json{{"x", "a"}}, json::object(), json{{"x", "c"}}})}}, + "[a][-][c]" + ); } static void test_expressions(testing & t) {