Repository: mitsuhiko/minijinja (language: rust)

Issue:
jinja2.meta.find_undeclared_variables equivalent
http://code.nabla.net/doc/jinja2/api/jinja2/meta/jinja2.meta.find_undeclared_variables.html

I see that it was implemented but removed in https://github.com/mitsuhiko/minijinja/pull/105

With the custom delimiters feature in place, maybe you could consider PRs adding that feature again?


Discussion / hints:
Very possible to bring it back. I think the bigger question is what you want to use it for so I better understand the motivation.

Fix patch:
diff --git a/minijinja/src/compiler/meta.rs b/minijinja/src/compiler/meta.rs
index 1f70d2a5..001b3e9b 100644
--- a/minijinja/src/compiler/meta.rs
+++ b/minijinja/src/compiler/meta.rs
@@ -26,166 +26,181 @@ impl<'a> AssignmentTracker<'a> {
 }
 
 /// Finds all variables that need to be captured as closure for a macro.
+#[cfg(feature = "macros")]
 pub fn find_macro_closure<'a>(m: &ast::Macro<'a>) -> HashSet<&'a str> {
-    fn visit_expr_opt<'a>(expr: &Option<ast::Expr<'a>>, state: &mut AssignmentTracker<'a>) {
-        if let Some(expr) = expr {
-            visit_expr(expr, state);
-        }
+    let mut state = AssignmentTracker {
+        out: HashSet::new(),
+        assigned: vec![Default::default()],
+    };
+    m.args.iter().for_each(|arg| track_assign(arg, &mut state));
+    m.body.iter().for_each(|node| track_walk(node, &mut state));
+    state.out
+}
+
+/// Finds all variables that are undeclared in a template.
+pub fn find_undeclared<'a>(t: &ast::Stmt<'a>) -> HashSet<&'a str> {
+    let mut state = AssignmentTracker {
+        out: HashSet::new(),
+        assigned: vec![Default::default()],
+    };
+    track_walk(t, &mut state);
+    state.out
+}
+
+fn tracker_visit_expr_opt<'a>(expr: &Option<ast::Expr<'a>>, state: &mut AssignmentTracker<'a>) {
+    if let Some(expr) = expr {
+        tracker_visit_expr(expr, state);
     }
+}
 
-    fn visit_expr<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a>) {
-        match expr {
-            ast::Expr::Var(var) => {
-                if !state.is_assigned(var.id) {
-                    state.out.insert(var.id);
-                    state.assign(var.id);
-                }
+fn tracker_visit_expr<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a>) {
+    match expr {
+        ast::Expr::Var(var) => {
+            if !state.is_assigned(var.id) {
+                state.out.insert(var.id);
+                state.assign(var.id);
             }
-            ast::Expr::Const(_) => {}
-            ast::Expr::UnaryOp(expr) => visit_expr(&expr.expr, state),
-            ast::Expr::BinOp(expr) => {
-                visit_expr(&expr.left, state);
-                visit_expr(&expr.right, state);
-            }
-            ast::Expr::IfExpr(expr) => {
-                visit_expr(&expr.test_expr, state);
-                visit_expr(&expr.true_expr, state);
-                visit_expr_opt(&expr.false_expr, state);
-            }
-            ast::Expr::Filter(expr) => {
-                visit_expr_opt(&expr.expr, state);
-                expr.args.iter().for_each(|x| visit_expr(x, state));
-            }
-            ast::Expr::Test(expr) => {
-                visit_expr(&expr.expr, state);
-                expr.args.iter().for_each(|x| visit_expr(x, state));
-            }
-            ast::Expr::GetAttr(expr) => visit_expr(&expr.expr, state),
-            ast::Expr::GetItem(expr) => {
-                visit_expr(&expr.expr, state);
-                visit_expr(&expr.subscript_expr, state);
-            }
-            ast::Expr::Slice(slice) => {
-                visit_expr_opt(&slice.start, state);
-                visit_expr_opt(&slice.stop, state);
-                visit_expr_opt(&slice.step, state);
-            }
-            ast::Expr::Call(expr) => {
-                visit_expr(&expr.expr, state);
-                expr.args.iter().for_each(|x| visit_expr(x, state));
-            }
-            ast::Expr::List(expr) => expr.items.iter().for_each(|x| visit_expr(x, state)),
-            ast::Expr::Map(expr) => expr.keys.iter().zip(expr.values.iter()).for_each(|(k, v)| {
-                visit_expr(k, state);
-                visit_expr(v, state);
-            }),
-            ast::Expr::Kwargs(expr) => expr.pairs.iter().for_each(|(_, v)| visit_expr(v, state)),
         }
+        ast::Expr::Const(_) => {}
+        ast::Expr::UnaryOp(expr) => tracker_visit_expr(&expr.expr, state),
+        ast::Expr::BinOp(expr) => {
+            tracker_visit_expr(&expr.left, state);
+            tracker_visit_expr(&expr.right, state);
+        }
+        ast::Expr::IfExpr(expr) => {
+            tracker_visit_expr(&expr.test_expr, state);
+            tracker_visit_expr(&expr.true_expr, state);
+            tracker_visit_expr_opt(&expr.false_expr, state);
+        }
+        ast::Expr::Filter(expr) => {
+            tracker_visit_expr_opt(&expr.expr, state);
+            expr.args.iter().for_each(|x| tracker_visit_expr(x, state));
+        }
+        ast::Expr::Test(expr) => {
+            tracker_visit_expr(&expr.expr, state);
+            expr.args.iter().for_each(|x| tracker_visit_expr(x, state));
+        }
+        ast::Expr::GetAttr(expr) => tracker_visit_expr(&expr.expr, state),
+        ast::Expr::GetItem(expr) => {
+            tracker_visit_expr(&expr.expr, state);
+            tracker_visit_expr(&expr.subscript_expr, state);
+        }
+        ast::Expr::Slice(slice) => {
+            tracker_visit_expr_opt(&slice.start, state);
+            tracker_visit_expr_opt(&slice.stop, state);
+            tracker_visit_expr_opt(&slice.step, state);
+        }
+        ast::Expr::Call(expr) => {
+            tracker_visit_expr(&expr.expr, state);
+            expr.args.iter().for_each(|x| tracker_visit_expr(x, state));
+        }
+        ast::Expr::List(expr) => expr.items.iter().for_each(|x| tracker_visit_expr(x, state)),
+        ast::Expr::Map(expr) => expr.keys.iter().zip(expr.values.iter()).for_each(|(k, v)| {
+            tracker_visit_expr(k, state);
+            tracker_visit_expr(v, state);
+        }),
+        ast::Expr::Kwargs(expr) => expr
+            .pairs
+            .iter()
+            .for_each(|(_, v)| tracker_visit_expr(v, state)),
     }
+}
 
-    fn assign_nested<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a>) {
-        match expr {
-            ast::Expr::Var(var) => state.assign(var.id),
-            ast::Expr::List(list) => list.items.iter().for_each(|x| assign_nested(x, state)),
-            _ => {}
-        }
+fn track_assign<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a>) {
+    match expr {
+        ast::Expr::Var(var) => state.assign(var.id),
+        ast::Expr::List(list) => list.items.iter().for_each(|x| track_assign(x, state)),
+        _ => {}
     }
+}
 
-    fn walk<'a>(node: &ast::Stmt<'a>, state: &mut AssignmentTracker<'a>) {
-        match node {
-            ast::Stmt::Template(stmt) => {
-                state.assign("self");
-                stmt.children.iter().for_each(|x| walk(x, state));
-            }
-            ast::Stmt::EmitExpr(expr) => visit_expr(&expr.expr, state),
-            ast::Stmt::EmitRaw(_) => {}
-            ast::Stmt::ForLoop(stmt) => {
-                state.push();
-                state.assign("loop");
-                visit_expr(&stmt.iter, state);
-                assign_nested(&stmt.target, state);
-                visit_expr_opt(&stmt.filter_expr, state);
-                stmt.body.iter().for_each(|x| walk(x, state));
-                state.pop();
-                state.push();
-                stmt.else_body.iter().for_each(|x| walk(x, state));
-                state.pop();
-            }
-            ast::Stmt::IfCond(stmt) => {
-                visit_expr(&stmt.expr, state);
-                state.push();
-                stmt.true_body.iter().for_each(|x| walk(x, state));
-                state.pop();
-                state.push();
-                stmt.false_body.iter().for_each(|x| walk(x, state));
-                state.pop();
-            }
-            ast::Stmt::WithBlock(stmt) => {
-                state.push();
-                for (target, expr) in &stmt.assignments {
-                    assign_nested(target, state);
-                    visit_expr(expr, state);
-                }
-                stmt.body.iter().for_each(|x| walk(x, state));
-                state.pop();
-            }
-            ast::Stmt::Set(stmt) => {
-                assign_nested(&stmt.target, state);
-                visit_expr(&stmt.expr, state);
-            }
-            ast::Stmt::AutoEscape(stmt) => {
-                state.push();
-                stmt.body.iter().for_each(|x| walk(x, state));
-                state.pop();
-            }
-            ast::Stmt::FilterBlock(stmt) => {
-                state.push();
-                stmt.body.iter().for_each(|x| walk(x, state));
-                state.pop();
-            }
-            ast::Stmt::SetBlock(stmt) => {
-                assign_nested(&stmt.target, state);
-                state.push();
-                stmt.body.iter().for_each(|x| walk(x, state));
-                state.pop();
-            }
-            #[cfg(feature = "multi_template")]
-            ast::Stmt::Block(stmt) => {
-                state.push();
-                state.assign("super");
-                stmt.body.iter().for_each(|x| walk(x, state));
-                state.pop();
-            }
-            #[cfg(feature = "multi_template")]
-            ast::Stmt::Extends(_) | ast::Stmt::Include(_) => {}
-            #[cfg(feature = "multi_template")]
-            ast::Stmt::Import(stmt) => {
-                assign_nested(&stmt.name, state);
-            }
-            #[cfg(feature = "multi_template")]
-            ast::Stmt::FromImport(stmt) => stmt.names.iter().for_each(|(arg, alias)| {
-                assign_nested(alias.as_ref().unwrap_or(arg), state);
-            }),
-            #[cfg(feature = "macros")]
-            ast::Stmt::Macro(stmt) => {
-                state.assign(stmt.name);
-            }
-            #[cfg(feature = "macros")]
-            ast::Stmt::CallBlock(_) => {}
-            ast::Stmt::Do(stmt) => {
-                visit_expr(&stmt.call.expr, state);
-                stmt.call.args.iter().for_each(|x| visit_expr(x, state));
-            }
+fn track_walk<'a>(node: &ast::Stmt<'a>, state: &mut AssignmentTracker<'a>) {
+    match node {
+        ast::Stmt::Template(stmt) => {
+            state.assign("self");
+            stmt.children.iter().for_each(|x| track_walk(x, state));
+        }
+        ast::Stmt::EmitExpr(expr) => tracker_visit_expr(&expr.expr, state),
+        ast::Stmt::EmitRaw(_) => {}
+        ast::Stmt::ForLoop(stmt) => {
+            state.push();
+            state.assign("loop");
+            tracker_visit_expr(&stmt.iter, state);
+            track_assign(&stmt.target, state);
+            tracker_visit_expr_opt(&stmt.filter_expr, state);
+            stmt.body.iter().for_each(|x| track_walk(x, state));
+            state.pop();
+            state.push();
+            stmt.else_body.iter().for_each(|x| track_walk(x, state));
+            state.pop();
+        }
+        ast::Stmt::IfCond(stmt) => {
+            tracker_visit_expr(&stmt.expr, state);
+            state.push();
+            stmt.true_body.iter().for_each(|x| track_walk(x, state));
+            state.pop();
+            state.push();
+            stmt.false_body.iter().for_each(|x| track_walk(x, state));
+            state.pop();
+        }
+        ast::Stmt::WithBlock(stmt) => {
+            state.push();
+            for (target, expr) in &stmt.assignments {
+                track_assign(target, state);
+                tracker_visit_expr(expr, state);
+            }
+            stmt.body.iter().for_each(|x| track_walk(x, state));
+            state.pop();
+        }
+        ast::Stmt::Set(stmt) => {
+            track_assign(&stmt.target, state);
+            tracker_visit_expr(&stmt.expr, state);
+        }
+        ast::Stmt::AutoEscape(stmt) => {
+            state.push();
+            stmt.body.iter().for_each(|x| track_walk(x, state));
+            state.pop();
+        }
+        ast::Stmt::FilterBlock(stmt) => {
+            state.push();
+            stmt.body.iter().for_each(|x| track_walk(x, state));
+            state.pop();
+        }
+        ast::Stmt::SetBlock(stmt) => {
+            track_assign(&stmt.target, state);
+            state.push();
+            stmt.body.iter().for_each(|x| track_walk(x, state));
+            state.pop();
+        }
+        #[cfg(feature = "multi_template")]
+        ast::Stmt::Block(stmt) => {
+            state.push();
+            state.assign("super");
+            stmt.body.iter().for_each(|x| track_walk(x, state));
+            state.pop();
+        }
+        #[cfg(feature = "multi_template")]
+        ast::Stmt::Extends(_) | ast::Stmt::Include(_) => {}
+        #[cfg(feature = "multi_template")]
+        ast::Stmt::Import(stmt) => {
+            track_assign(&stmt.name, state);
+        }
+        #[cfg(feature = "multi_template")]
+        ast::Stmt::FromImport(stmt) => stmt.names.iter().for_each(|(arg, alias)| {
+            track_assign(alias.as_ref().unwrap_or(arg), state);
+        }),
+        #[cfg(feature = "macros")]
+        ast::Stmt::Macro(stmt) => {
+            state.assign(stmt.name);
+        }
+        #[cfg(feature = "macros")]
+        ast::Stmt::CallBlock(_) => {}
+        ast::Stmt::Do(stmt) => {
+            tracker_visit_expr(&stmt.call.expr, state);
+            stmt.call
+                .args
+                .iter()
+                .for_each(|x| tracker_visit_expr(x, state));
         }
     }
-
-    let mut state = AssignmentTracker {
-        out: HashSet::new(),
-        assigned: vec![Default::default()],
-    };
-
-    m.args.iter().for_each(|arg| assign_nested(arg, &mut state));
-    m.body.iter().for_each(|node| walk(node, &mut state));
-
-    state.out
 }
diff --git a/minijinja/src/compiler/mod.rs b/minijinja/src/compiler/mod.rs
index 5ddd00bd..33ca1155 100644
--- a/minijinja/src/compiler/mod.rs
+++ b/minijinja/src/compiler/mod.rs
@@ -4,7 +4,6 @@ pub mod ast;
 pub mod codegen;
 pub mod instructions;
 pub mod lexer;
-#[cfg(feature = "macros")]
 pub mod meta;
 pub mod parser;
 pub mod tokens;
diff --git a/minijinja/src/expression.rs b/minijinja/src/expression.rs
index 798db266..7506e618 100644
--- a/minijinja/src/expression.rs
+++ b/minijinja/src/expression.rs
@@ -1,9 +1,13 @@
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashSet};
 use std::fmt;
 
 use serde::Serialize;
 
+use crate::compiler::ast;
 use crate::compiler::instructions::Instructions;
+use crate::compiler::lexer::SyntaxConfig;
+use crate::compiler::meta::find_undeclared;
+use crate::compiler::parser::parse_expr;
 use crate::environment::Environment;
 use crate::error::Error;
 use crate::output::Output;
@@ -60,6 +64,20 @@ impl<'env, 'source> Expression<'env, 'source> {
         self._eval(Value::from_serializable(&ctx))
     }
 
+    /// Returns a set of all undeclared variables in the expression.
+    ///
+    /// This works the same as
+    /// [`Template::undeclared_variables`](crate::Template::undeclared_variables).
+    pub fn undeclared_variables(&self) -> HashSet<&str> {
+        match parse_expr(self.instructions.source(), SyntaxConfig::default()) {
+            Ok(expr) => find_undeclared(&ast::Stmt::EmitExpr(ast::Spanned::new(
+                ast::EmitExpr { expr },
+                Default::default(),
+            ))),
+            Err(_) => HashSet::new(),
+        }
+    }
+
     fn _eval(&self, root: Value) -> Result<Value, Error> {
         Ok(ok!(Vm::new(self.env).eval(
             &self.instructions,
diff --git a/minijinja/src/template.rs b/minijinja/src/template.rs
index f14831b5..b070d5e1 100644
--- a/minijinja/src/template.rs
+++ b/minijinja/src/template.rs
@@ -1,4 +1,4 @@
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashSet};
 use std::{fmt, io};
 
 use serde::Serialize;
@@ -6,6 +6,7 @@ use serde::Serialize;
 use crate::compiler::codegen::CodeGenerator;
 use crate::compiler::instructions::Instructions;
 use crate::compiler::lexer::SyntaxConfig;
+use crate::compiler::meta::find_undeclared;
 use crate::compiler::parser::parse_with_syntax;
 use crate::environment::Environment;
 use crate::error::{attach_basic_debug_info, Error, ErrorKind};
@@ -140,6 +141,33 @@ impl<'env> Template<'env> {
         )
     }
 
+    /// Returns a set of all undeclared variables in the template.
+    ///
+    /// This returns a set of all variables that might be looked up
+    /// at runtime by the template.  Since this is runs a static
+    /// analysis, the actual control flow is not considered.  This
+    /// also cannot take into account what happens due to includes,
+    /// imports or extending.
+    ///
+    /// ```rust
+    /// # use minijinja::Environment;
+    /// let mut env = Environment::new();
+    /// env.add_template("x", "{% set x = foo %}{{ x }}{{ bar }}").unwrap();
+    /// let tmpl = env.get_template("x").unwrap();
+    /// let undeclared = tmpl.undeclared_variables();
+    /// assert_eq!(undeclared, ["foo", "bar"].into_iter().collect());
+    /// ```
+    pub fn undeclared_variables(&self) -> HashSet<&str> {
+        match parse_with_syntax(
+            self.compiled.instructions.source(),
+            self.name(),
+            self.compiled.syntax.clone(),
+        ) {
+            Ok(ast) => find_undeclared(&ast),
+            Err(_) => HashSet::new(),
+        }
+    }
+
     /// Returns the root instructions.
     #[cfg(feature = "multi_template")]
     pub(crate) fn instructions(&self) -> &'env Instructions<'env> {
@@ -167,6 +195,8 @@ pub struct CompiledTemplate<'source> {
     pub blocks: BTreeMap<&'source str, Instructions<'source>>,
     /// Optional size hint for string rendering.
     pub buffer_size_hint: usize,
+    /// The syntax config that created it.
+    pub syntax: SyntaxConfig,
 }
 
 impl<'env> fmt::Debug for CompiledTemplate<'env> {
@@ -211,7 +241,7 @@ impl<'source> CompiledTemplate<'source> {
         // the parser/compiler combination can create constants in which case
         // we can probably benefit from the value optimization a bit.
         let _guard = value::value_optimization();
-        let ast = ok!(parse_with_syntax(source, name, syntax));
+        let ast = ok!(parse_with_syntax(source, name, syntax.clone()));
         let mut gen = CodeGenerator::new(name, source);
         gen.compile_stmt(&ast);
         let buffer_size_hint = gen.buffer_size_hint();
@@ -220,6 +250,7 @@ impl<'source> CompiledTemplate<'source> {
             instructions,
             blocks,
             buffer_size_hint,
+            syntax,
         })
     }
 }


Test patch:
diff --git a/minijinja/tests/test_environment.rs b/minijinja/tests/test_environment.rs
index 429d95f4..4990fc16 100644
--- a/minijinja/tests/test_environment.rs
+++ b/minijinja/tests/test_environment.rs
@@ -45,6 +45,14 @@ fn test_expression_lifetimes() {
     }
 }
 
+#[test]
+fn test_expression_undeclared_variables() {
+    let env = Environment::new();
+    let expr = env.compile_expression("[foo, bar]").unwrap();
+    let undeclared = expr.undeclared_variables();
+    assert_eq!(undeclared, ["foo", "bar"].into_iter().collect());
+}
+
 #[test]
 fn test_clone() {
     let mut env = Environment::new();
diff --git a/minijinja/tests/test_templates.rs b/minijinja/tests/test_templates.rs
index c4ff1b12..ee6e3c01 100644
--- a/minijinja/tests/test_templates.rs
+++ b/minijinja/tests/test_templates.rs
@@ -445,3 +445,13 @@ fn test_custom_syntax() {
         .unwrap();
     assert_eq!(value, r"012");
 }
+
+#[test]
+fn test_undeclared_variables() {
+    let mut env = Environment::new();
+    env.add_template("demo", "{% set x = foo %}{{ x }}{{ bar }}")
+        .unwrap();
+    let tmpl = env.get_template("demo").unwrap();
+    let undeclared = tmpl.undeclared_variables();
+    assert_eq!(undeclared, ["foo", "bar"].into_iter().collect());
+}