From baa407cfba4985932a65cb8c6de27b952418cc7b Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Thu, 17 Nov 2022 20:49:55 -0800 Subject: [PATCH 1/3] Expose the name of the currently called item --- minijinja/src/vm/mod.rs | 26 ++++++++++++++----- minijinja/src/vm/state.rs | 9 +++++++ .../test_templates__vm@debug.txt.snap | 3 +++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/minijinja/src/vm/mod.rs b/minijinja/src/vm/mod.rs index 68e71684..305ba23e 100644 --- a/minijinja/src/vm/mod.rs +++ b/minijinja/src/vm/mod.rs @@ -83,8 +83,9 @@ impl<'env> Vm<'env> { env: self.env, ctx: Context::new(Frame::new(root)), current_block: None, - instructions, + current_call: None, auto_escape, + instructions, blocks: prepare_blocks(blocks), loaded_templates: BTreeSet::new(), #[cfg(feature = "macros")] @@ -115,8 +116,9 @@ impl<'env> Vm<'env> { env: self.env, ctx, current_block: None, - instructions, + current_call: None, auto_escape: state.auto_escape(), + instructions, blocks: BTreeMap::default(), loaded_templates: BTreeSet::new(), #[cfg(feature = "macros")] @@ -421,6 +423,7 @@ impl<'env> Vm<'env> { stack.push(out.end_capture(state.auto_escape)); } Instruction::ApplyFilter(name, arg_count, local_id) => { + state.current_call = Some(name); let filter = ctx_ok!(get_or_lookup_local(&mut loaded_filters, *local_id, || { state.env.get_filter(name) @@ -435,8 +438,10 @@ impl<'env> Vm<'env> { a = ctx_ok!(filter.apply_to(state, args)); stack.drop_top(*arg_count); stack.push(a); + state.current_call = Some(name); } Instruction::PerformTest(name, arg_count, local_id) => { + state.current_call = Some(name); let test = ctx_ok!(get_or_lookup_local(&mut loaded_tests, *local_id, || { state.env.get_test(name) }) @@ -447,10 +452,13 @@ impl<'env> Vm<'env> { let rv = ctx_ok!(test.perform(state, args)); stack.drop_top(*arg_count); stack.push(Value::from(rv)); + state.current_call = None; } - Instruction::CallFunction(function_name, arg_count) => { + Instruction::CallFunction(name, arg_count) => { + state.current_call = Some(name); + // super is a special function reserved for super-ing into blocks. - if *function_name == "super" { + if *name == "super" { if *arg_count != 0 { bail!(Error::new( ErrorKind::InvalidOperation, @@ -459,7 +467,7 @@ impl<'env> Vm<'env> { } stack.push(ctx_ok!(self.perform_super(state, out, true))); // loop is a special name which when called recurses the current loop. - } else if *function_name == "loop" { + } else if *name == "loop" { if *arg_count != 1 { bail!(Error::new( ErrorKind::InvalidOperation, @@ -468,7 +476,7 @@ impl<'env> Vm<'env> { } // leave the one argument on the stack for the recursion recurse_loop!(true); - } else if let Some(func) = state.ctx.load(self.env, function_name) { + } else if let Some(func) = state.ctx.load(self.env, name) { let args = stack.slice_top(*arg_count); a = ctx_ok!(func.call(state, args)); stack.drop_top(*arg_count); @@ -476,15 +484,19 @@ impl<'env> Vm<'env> { } else { bail!(Error::new( ErrorKind::UnknownFunction, - format!("{} is unknown", function_name), + format!("{} is unknown", name), )); } + + state.current_call = None; } Instruction::CallMethod(name, arg_count) => { + state.current_call = Some(name); let args = stack.slice_top(*arg_count); a = ctx_ok!(args[0].call_method(state, name, &args[1..])); stack.drop_top(*arg_count); stack.push(a); + state.current_call = None; } Instruction::CallObject(arg_count) => { let args = stack.slice_top(*arg_count); diff --git a/minijinja/src/vm/state.rs b/minijinja/src/vm/state.rs index 29f80d13..263f7130 100644 --- a/minijinja/src/vm/state.rs +++ b/minijinja/src/vm/state.rs @@ -24,6 +24,7 @@ pub struct State<'vm, 'env> { pub(crate) env: &'env Environment<'env>, pub(crate) ctx: Context<'env>, pub(crate) current_block: Option<&'env str>, + pub(crate) current_call: Option<&'env str>, pub(crate) auto_escape: AutoEscape, pub(crate) instructions: &'vm Instructions<'env>, pub(crate) blocks: BTreeMap<&'env str, BlockStack<'vm, 'env>>, @@ -38,6 +39,7 @@ impl<'vm, 'env> fmt::Debug for State<'vm, 'env> { let mut ds = f.debug_struct("State"); ds.field("name", &self.instructions.name()); ds.field("current_block", &self.current_block); + ds.field("current_call", &self.current_call); ds.field("auto_escape", &self.auto_escape); ds.field("ctx", &self.ctx); ds.field("env", &self.env); @@ -66,6 +68,12 @@ impl<'vm, 'env> State<'vm, 'env> { self.current_block } + /// Returns the name of the item (filter, function, test, method) currently + /// being called. + pub fn current_call(&self) -> Option<&str> { + self.current_call + } + /// Looks up a variable by name in the context. pub fn lookup(&self, name: &str) -> Option { self.ctx.load(self.env(), name) @@ -82,6 +90,7 @@ impl<'vm, 'env> State<'vm, 'env> { blocks: BTreeMap::new(), loaded_templates: BTreeSet::new(), macros: Default::default(), + current_call: None, }) } diff --git a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap index 0a6efd62..7d773f69 100644 --- a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap +++ b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap @@ -8,6 +8,9 @@ input_file: minijinja/tests/inputs/debug.txt State { name: "debug.txt", current_block: None, + current_call: Some( + "debug", + ), auto_escape: None, ctx: { "x": 0, From 407fe7535eba6f6ef5f0af09b592f8f60b678569 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 18 Nov 2022 17:13:30 -0800 Subject: [PATCH 2/3] Add test for 'State::current_call()' --- minijinja/tests/test_templates.rs | 101 ++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/minijinja/tests/test_templates.rs b/minijinja/tests/test_templates.rs index 10c4e720..75233f95 100644 --- a/minijinja/tests/test_templates.rs +++ b/minijinja/tests/test_templates.rs @@ -146,3 +146,104 @@ fn test_loop_changed() { ); assert_eq!(rv, "12345"); } + +#[test] +fn test_current_call_state() { + use minijinja::value::{Object, Value}; + use std::fmt; + + #[derive(Debug)] + struct MethodAndFunc; + + impl fmt::Display for MethodAndFunc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self:?}") + } + } + + impl Object for MethodAndFunc { + fn call_method(&self, state: &State, name: &str, args: &[Value]) -> Result { + assert_eq!(name, state.current_call().unwrap()); + let args = args + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(", "); + + Ok(format!("{}({args})", state.current_call().unwrap()).into()) + } + + fn call(&self, state: &State, args: &[Value]) -> Result { + let args = args + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(", "); + + Ok(format!("{}({args})", state.current_call().unwrap()).into()) + } + } + + fn current_call(state: &State, value: Option<&str>) -> String { + format!("{}({})", state.current_call().unwrap(), value.unwrap_or("")) + } + + fn check_test(state: &State, value: &str) -> bool { + state.current_call() == Some(value) + } + + let mut env = Environment::new(); + env.add_function("fn_call_a", current_call); + env.add_function("fn_call_b", current_call); + env.add_filter("filter_call", current_call); + env.add_test("my_test", check_test); + env.add_test("another_test", check_test); + env.add_global("object", Value::from_object(MethodAndFunc)); + + env.add_template( + "test", + r#" + {{ fn_call_a() }} + {{ "foo" | filter_call }} + {{ fn_call_a() | filter_call }} + {{ fn_call_b() | filter_call }} + {{ fn_call_a(fn_call_b()) }} + {{ fn_call_a(fn_call_b()) | filter_call }} + + {{ "my_test" is my_test }} + {{ "another_test" is my_test }} + {{ "another_test" is another_test }} + + {{ object.foo() }} + {{ object.bar() }} + {{ object.foo(object.bar(object.baz())) }} + {{ object(object.bar()) }} + {{ object.baz(object()) }} + "#, + ) + .unwrap(); + + let tmpl = env.get_template("test").unwrap(); + let rv = tmpl.render(context!()).unwrap(); + assert_eq!( + rv, + r#" + fn_call_a() + filter_call(foo) + filter_call(fn_call_a()) + filter_call(fn_call_b()) + fn_call_a(fn_call_b()) + filter_call(fn_call_a(fn_call_b())) + + true + false + true + + foo() + bar() + foo(bar(baz())) + object(bar()) + baz(object()) + "# + ); +} From b531dd265a94ad781a4cf65d977966904bd65a5e Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 18 Nov 2022 17:39:57 -0800 Subject: [PATCH 3/3] Leave a note where 'current_call' isn't updated --- minijinja/src/vm/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/minijinja/src/vm/mod.rs b/minijinja/src/vm/mod.rs index 305ba23e..c4e8cb6a 100644 --- a/minijinja/src/vm/mod.rs +++ b/minijinja/src/vm/mod.rs @@ -511,9 +511,13 @@ impl<'env> Vm<'env> { stack.pop(); } Instruction::FastSuper => { + // Note that we don't store 'current_call' here since it + // would only be visible (and unused) internally. ctx_ok!(self.perform_super(state, out, false)); } Instruction::FastRecurse => { + // Note that we don't store 'current_call' here since it + // would only be visible (and unused) internally. recurse_loop!(false); } // Explanation on the behavior of `LoadBlocks` and `RenderParent`.