Repository: mitsuhiko/minijinja (rust)

Issue:
Macro variables' declared-ness is leaking to outer scope
## Description

Variables that share a name with a macro's variables are not considered undeclared, despite them being unbound.

## Reproduction steps

```python
from minijinja import Environment

env = Environment()

vars = env.undeclared_variables_in_str("""
{%- macro bar(a) -%}
    -{{a}}-
{%- endmacro- %}
{{a}}
""")

print(vars)
--> set()  # Expected {'a'}
```

Additional helpful information:

- Version of minijinja: 2.7.0
- Version of rustc: N/A
- Operating system and version: MacOS M1 Max Seqouia

## What did you expect

`a` is unbound, and is indeed a free variable. It's expected that `undeclared_variables_in_str` will return a as an undeclared variable




Pull request description:
Fix a bug with macros in undeclared_variables
Fixes #712

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e09d83..8062b70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,8 @@ All notable changes to MiniJinja are documented here.
 - Added `Object::custom_cmp` to allow objects to influence how they
   compare against themselves.  This also fixes Python objects in the
   Python binding not to compare correctly.  #707
+- Fixed a bug where `undeclared_variables` would incorrectly handle
+  variables referenced by macros.  #714
 
 ## 2.8.0
 
diff --git a/minijinja/src/compiler/meta.rs b/minijinja/src/compiler/meta.rs
index 2566159..7a97686 100644
--- a/minijinja/src/compiler/meta.rs
+++ b/minijinja/src/compiler/meta.rs
@@ -268,7 +268,9 @@ fn track_walk<'a>(node: &ast::Stmt<'a>, state: &mut AssignmentTracker<'a>) {
         #[cfg(feature = "macros")]
         ast::Stmt::Macro(stmt) => {
             state.assign(stmt.name);
+            state.push();
             tracker_visit_macro(stmt, state);
+            state.pop();
         }
         #[cfg(feature = "macros")]
         ast::Stmt::CallBlock(stmt) => {
@@ -277,7 +279,9 @@ fn track_walk<'a>(node: &ast::Stmt<'a>, state: &mut AssignmentTracker<'a>) {
                 .args
                 .iter()
                 .for_each(|x| tracker_visit_callarg(x, state));
+            state.push();
             tracker_visit_macro(&stmt.macro_decl, state);
+            state.pop();
         }
         #[cfg(feature = "loop_controls")]
         ast::Stmt::Continue(_) | ast::Stmt::Break(_) => {}


Test patch:
diff --git a/minijinja/tests/test_templates.rs b/minijinja/tests/test_templates.rs
index 4cac3a7..9de80c0 100644
--- a/minijinja/tests/test_templates.rs
+++ b/minijinja/tests/test_templates.rs
@@ -535,6 +535,31 @@ fn test_undeclared_variables_bug() {
     );
 }
 
+#[test]
+fn test_undeclared_variables_macro_bug() {
+    // https://github.com/mitsuhiko/minijinja/issues/712
+    let mut env = Environment::new();
+    env.add_template(
+        "demo",
+        r#"
+        {% macro bar(a) %}
+            {{ a }}
+        {% endmacro %}
+        {% call (b) bar() %}
+            {{ b }}
+        {% endcall %}
+        {{ a }} {{ b }}
+    "#,
+    )
+    .unwrap();
+    let tmpl = env.get_template("demo").unwrap();
+    let undeclared = tmpl.undeclared_variables(true);
+    assert_eq!(
+        undeclared,
+        ["a", "b"].into_iter().map(|x| x.to_string()).collect()
+    );
+}
+
 #[test]
 fn test_block_fragments() {
     let mut env = Environment::new();