Repository: mitsuhiko/minijinja
Pull request #542 (2024-07-21 14:08:40)

Allow macro state to resolve to the initial template context
This changes how macros are resolving variables.  Previously if a macro did not enclose a value from the render scope it would only resolve down to the true global state passed to the environment.  This changes it so that the initial state passed to the context can also be used.

Refs #535


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b18af7e..aba597c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,9 @@ All notable changes to MiniJinja are documented here.
 - Bumped the minimum version of `self_cell` to 1.0.4.  #540
 - MiniJinja will now warn if the `serde` feature is disabled.  This is in
   anticipation of removing the serde dependency in the future.  #541
+- Improved an edge case with `State::resolve`.  It now can resolve the
+  initial template context in macro calls even if no closure has been
+  created.  #542
 
 ## 2.0.3
 
diff --git a/minijinja/src/vm/context.rs b/minijinja/src/vm/context.rs
index 1fbfcd8..5902b75 100644
--- a/minijinja/src/vm/context.rs
+++ b/minijinja/src/vm/context.rs
@@ -256,6 +256,15 @@ impl<'env> Context<'env> {
         self.stack.last_mut().unwrap().closure = closure;
     }
 
+    /// Return the base context value
+    #[cfg(feature = "macros")]
+    pub fn clone_base(&self) -> Value {
+        self.stack
+            .first()
+            .map(|x| x.ctx.clone())
+            .unwrap_or_default()
+    }
+
     /// Looks up a variable in the context.
     pub fn load(&self, env: &Environment, key: &str) -> Option<Value> {
         for frame in self.stack.iter().rev() {
diff --git a/minijinja/src/vm/macro_object.rs b/minijinja/src/vm/macro_object.rs
index d49c0a7..8fc66dc 100644
--- a/minijinja/src/vm/macro_object.rs
+++ b/minijinja/src/vm/macro_object.rs
@@ -137,6 +137,7 @@ impl Object for Macro {
             instructions,
             *offset,
             closure,
+            state.ctx.clone_base(),
             caller,
             &mut out,
             state,
diff --git a/minijinja/src/vm/mod.rs b/minijinja/src/vm/mod.rs
index e82213b..bebe4ca 100644
--- a/minijinja/src/vm/mod.rs
+++ b/minijinja/src/vm/mod.rs
@@ -38,7 +38,7 @@ const INCLUDE_RECURSION_COST: usize = 10;
 
 // the cost of a single macro call against the stack limit.
 #[cfg(feature = "macros")]
-const MACRO_RECURSION_COST: usize = 5;
+const MACRO_RECURSION_COST: usize = 4;
 
 /// Helps to evaluate something.
 #[cfg_attr(feature = "internal_debug", derive(Debug))]
@@ -108,12 +108,14 @@ impl<'env> Vm<'env> {
         instructions: &Instructions<'env>,
         pc: usize,
         closure: Value,
+        context_base: Value,
         caller: Option<Value>,
         out: &mut Output,
         state: &State,
         args: Vec<Value>,
     ) -> Result<Option<Value>, Error> {
-        let mut ctx = Context::new_with_frame(Frame::new(closure), self.env.recursion_limit());
+        let mut ctx = Context::new_with_frame(Frame::new(context_base), self.env.recursion_limit());
+        ok!(ctx.push_frame(Frame::new(closure)));
         if let Some(caller) = caller {
             ctx.store("caller", caller);
         }


Test patch:
diff --git a/minijinja/tests/test_macros.rs b/minijinja/tests/test_macros.rs
index 9e69691..6d11dc1 100644
--- a/minijinja/tests/test_macros.rs
+++ b/minijinja/tests/test_macros.rs
@@ -214,3 +214,36 @@ fn test_caller_bug() {
     );
     assert_snapshot!(rv.trim(), @"42|23");
 }
+
+/// https://github.com/mitsuhiko/minijinja/issues/535
+#[test]
+fn test_unenclosed_resolve() {
+    // the current intended logic here is that a the state can
+    // observe real globals and the initial template context, but
+    // no other modifications.  Normally the call block can only
+    // see what it encloses explicitly, but since it does not
+    // refer to anything here it in fact has an empty closure.
+
+    fn resolve(state: &minijinja::State, var: &str) -> Value {
+        state.lookup(var).unwrap_or_default()
+    }
+
+    let mut env = Environment::new();
+    env.add_global("ctx_global", "ctx global");
+    env.add_function("resolve", resolve);
+    let rv = env
+        .render_str(
+            r#"
+    {%- set template_global = 'template global' %}
+    {%- macro wrapper() %}{{ caller() }}{% endmacro %}
+    {%- call wrapper() %}
+        {{- resolve('render_global') }}|
+        {{- resolve('ctx_global') }}|
+        {{- resolve('template_global') }}
+    {%- endcall -%}
+    "#,
+            context! { render_global => "render global" },
+        )
+        .unwrap();
+    assert_snapshot!(rv, @"render global|ctx global|");
+}