Repository: mitsuhiko/minijinja (rust)

Issue:
Filter in child template replaces other filter in parent template
## Description

It's probably best to just see the reproduction. As far as I can tell, using `{% set foo = "value" | <filter> %}` in a template that extends another affects filters in the base template.

## Reproduction steps

Rust minimum working example with `minijinja = "=2.1.1"` in `Cargo.toml`:

```rust
fn main() {
    let mut env = minijinja::Environment::empty();

    env.add_filter("reverse", minijinja::filters::reverse);
    env.add_filter("nop", |value: String| value);

    const ONE: &str = r#"{{ "foobar" | nop }}"#;
    const TWO: &str = r#"{% extends "one.html" %}{% set foo = "" | reverse %}"#;

    env.add_template("one.html", ONE).unwrap();
    env.add_template("two.html", TWO).unwrap();

    let template = env.get_template("one.html").unwrap();
    println!("{}", template.render(minijinja::context! {}).unwrap());

    let template = env.get_template("two.html").unwrap();
    println!("{}", template.render(minijinja::context! {}).unwrap());
}
```

This prints "foobar" and "raboof" (!).

It does not matter where the set statement is, so this also produces the same result:

```rust
const TWO: &str = r#"{% set foo = "" | reverse %}{% extends "one.html" %}"#;
```

For completeness, Python/Jinja2 3.1.4 minimum working example:

```python
from jinja2 import Environment, DictLoader
loader = DictLoader({
    "one.html": '{{ "foobar" | nop }}',
    "two.html": '{% extends "one.html" %}{% set foo = "" | reverse %}',
})
env = Environment(loader=loader)
env.filters["nop"] = lambda v: v
print(env.get_template("one.html").render())
print(env.get_template("two.html").render())
```

Prints `foobar` and `foobar` as expected.

Additional helpful information:

- Version of minijinja: 2.1.1 (f4bf71af278c578cbcc91d0b1ff092910208bc86f7b3750364642bd424e3dcd3), 2.1.0, 2.0.0, 1.0.21, 0.30.0, 0.26.0
- Version of rustc: 1.80.0 (051478957 2024-07-21), 1.79.0 (129f3b996 2024-06-10), 1.76.0 (07dca489a 2024-02-04)
- Operating system and version: macOS Sonoma 14.5 (23F79)

As far as I can see, 0.25.0 works as expected, and 0.26.0 introduced this behaviour.

## What did you expect

I'd expect the example to print "foobar" in both cases, as the Python example and version 0.25.0 does.



Pull request description:
Flush local filter and test cache when processing `parent_instructions`
This resolves #551 . Please let me know if you want any changes made. This fix is the least disruptive one I could find. There may be a slight performance hit until the filters/tests are re-cached.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2b9f17..de8083d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
 
 All notable changes to MiniJinja are documented here.
 
+## 2.1.2
+
+- Flush filter and test cache when processing extended template.
+  This fixes a bug that caused the wrong filters to be used in some
+  cases. #551
+
 ## 2.1.1
 
 - Added `indent` parameter to `tojson` filter.  #546
diff --git a/minijinja/src/vm/mod.rs b/minijinja/src/vm/mod.rs
index bebe4ca..06bb4d0 100644
--- a/minijinja/src/vm/mod.rs
+++ b/minijinja/src/vm/mod.rs
@@ -211,7 +211,7 @@ impl<'env> Vm<'env> {
             }};
         }
 
-        // looks nicer nice way
+        // looks nicer this way
         #[allow(clippy::while_let_loop)]
         loop {
             let instr = match state.instructions.get(pc) {
@@ -231,6 +231,11 @@ impl<'env> Vm<'env> {
                     };
                     out.end_capture(AutoEscape::None);
                     pc = 0;
+                    // because we swap out the instructions we also need to unload all
+                    // the filters and tests to ensure that we are not accidentally
+                    // reusing the local_ids for completely different filters.
+                    loaded_filters = [None; MAX_LOCALS];
+                    loaded_tests = [None; MAX_LOCALS];
                     continue;
                 }
             };


Test patch:
diff --git a/minijinja/tests/test_templates.rs b/minijinja/tests/test_templates.rs
index 6a03949..f3fe5d7 100644
--- a/minijinja/tests/test_templates.rs
+++ b/minijinja/tests/test_templates.rs
@@ -629,3 +629,32 @@ fn test_multiple_extended_includes_in_loop() {
     let rv = env.get_template("main.txt").unwrap().render(()).unwrap();
     assert_eq!(rv, "012");
 }
+
+/// See https://github.com/mitsuhiko/minijinja/issues/551
+#[test]
+fn test_filter_caching() {
+    let mut env = Environment::new();
+    env.add_template("parent.txt", "{{ 'Hello Foo' | lower }}")
+        .unwrap();
+    env.add_template(
+        "child.txt",
+        "{% extends 'parent.txt' %}{% set dummy = 'a' | upper %}",
+    )
+    .unwrap();
+    let rv = env.get_template("child.txt").unwrap().render(()).unwrap();
+    assert_eq!(rv, "hello foo");
+}
+
+/// See https://github.com/mitsuhiko/minijinja/issues/551
+#[test]
+fn test_test_caching() {
+    let mut env = Environment::new();
+    env.add_template("parent.txt", "{{ 42 is odd }}").unwrap();
+    env.add_template(
+        "child.txt",
+        "{% extends 'parent.txt' %}{% set dummy = 23 is even %}",
+    )
+    .unwrap();
+    let rv = env.get_template("child.txt").unwrap().render(()).unwrap();
+    assert_eq!(rv, "false");
+}