minijinja/compiler/
meta.rs

1use std::collections::HashSet;
2use std::fmt::Write;
3
4use crate::compiler::ast;
5
6struct AssignmentTracker<'a> {
7    out: HashSet<&'a str>,
8    nested_out: Option<HashSet<String>>,
9    assigned: Vec<HashSet<&'a str>>,
10}
11
12impl<'a> AssignmentTracker<'a> {
13    fn is_assigned(&self, name: &str) -> bool {
14        self.assigned.iter().any(|x| x.contains(name))
15    }
16
17    fn assign(&mut self, name: &'a str) {
18        self.assigned.last_mut().unwrap().insert(name);
19    }
20
21    fn assign_nested(&mut self, name: String) {
22        if let Some(ref mut nested_out) = self.nested_out {
23            if !nested_out.contains(&name) {
24                nested_out.insert(name);
25            }
26        }
27    }
28
29    fn push(&mut self) {
30        self.assigned.push(Default::default());
31    }
32
33    fn pop(&mut self) {
34        self.assigned.pop();
35    }
36}
37
38/// Finds all variables that need to be captured as closure for a macro.
39#[cfg(feature = "macros")]
40pub fn find_macro_closure<'a>(m: &ast::Macro<'a>) -> HashSet<&'a str> {
41    let mut state = AssignmentTracker {
42        out: HashSet::new(),
43        nested_out: None,
44        assigned: vec![Default::default()],
45    };
46    tracker_visit_macro(m, &mut state, false);
47    state.out
48}
49
50/// Finds all variables that are undeclared in a template.
51pub fn find_undeclared(t: &ast::Stmt<'_>, track_nested: bool) -> HashSet<String> {
52    let mut state = AssignmentTracker {
53        out: HashSet::new(),
54        nested_out: if track_nested {
55            Some(HashSet::new())
56        } else {
57            None
58        },
59        assigned: vec![Default::default()],
60    };
61    track_walk(t, &mut state);
62    if let Some(nested) = state.nested_out {
63        nested
64    } else {
65        state.out.into_iter().map(|x| x.to_string()).collect()
66    }
67}
68
69fn tracker_visit_expr_opt<'a>(expr: &Option<ast::Expr<'a>>, state: &mut AssignmentTracker<'a>) {
70    if let Some(expr) = expr {
71        tracker_visit_expr(expr, state);
72    }
73}
74
75#[cfg(feature = "macros")]
76fn tracker_visit_macro<'a>(
77    m: &ast::Macro<'a>,
78    state: &mut AssignmentTracker<'a>,
79    declare_caller: bool,
80) {
81    if declare_caller {
82        // this is not completely correct as caller is actually only defined
83        // if the macro was used in the context of a call block.  However it
84        // is impossible to determine this at compile time so we err on the
85        // side of assuming caller is there.
86        state.assign("caller");
87    }
88    m.args.iter().for_each(|arg| track_assign(arg, state));
89    m.defaults
90        .iter()
91        .for_each(|expr| tracker_visit_expr(expr, state));
92    m.body.iter().for_each(|node| track_walk(node, state));
93}
94
95fn tracker_visit_callarg<'a>(callarg: &ast::CallArg<'a>, state: &mut AssignmentTracker<'a>) {
96    match callarg {
97        ast::CallArg::Pos(expr)
98        | ast::CallArg::Kwarg(_, expr)
99        | ast::CallArg::PosSplat(expr)
100        | ast::CallArg::KwargSplat(expr) => tracker_visit_expr(expr, state),
101    }
102}
103
104fn tracker_visit_expr<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a>) {
105    match expr {
106        ast::Expr::Var(var) => {
107            if !state.is_assigned(var.id) {
108                state.out.insert(var.id);
109                // if we are not tracking nested assignments, we can consider a variable
110                // to be assigned the first time we perform a lookup.
111                if state.nested_out.is_none() {
112                    state.assign(var.id);
113                } else {
114                    state.assign_nested(var.id.to_string());
115                }
116            }
117        }
118        ast::Expr::Const(_) => {}
119        ast::Expr::UnaryOp(expr) => tracker_visit_expr(&expr.expr, state),
120        ast::Expr::BinOp(expr) => {
121            tracker_visit_expr(&expr.left, state);
122            tracker_visit_expr(&expr.right, state);
123        }
124        ast::Expr::IfExpr(expr) => {
125            tracker_visit_expr(&expr.test_expr, state);
126            tracker_visit_expr(&expr.true_expr, state);
127            tracker_visit_expr_opt(&expr.false_expr, state);
128        }
129        ast::Expr::Filter(expr) => {
130            tracker_visit_expr_opt(&expr.expr, state);
131            expr.args
132                .iter()
133                .for_each(|x| tracker_visit_callarg(x, state));
134        }
135        ast::Expr::Test(expr) => {
136            tracker_visit_expr(&expr.expr, state);
137            expr.args
138                .iter()
139                .for_each(|x| tracker_visit_callarg(x, state));
140        }
141        ast::Expr::GetAttr(expr) => {
142            // if we are tracking nested, we check if we have a chain of attribute
143            // lookups that terminate in a variable lookup.  In that case we can
144            // assign the nested lookup.
145            if state.nested_out.is_some() {
146                let mut attrs = vec![expr.name];
147                let mut ptr = &expr.expr;
148                loop {
149                    match ptr {
150                        ast::Expr::Var(var) => {
151                            if !state.is_assigned(var.id) {
152                                let mut rv = var.id.to_string();
153                                for attr in attrs.iter().rev() {
154                                    write!(rv, ".{}", attr).ok();
155                                }
156                                state.assign_nested(rv);
157                                return;
158                            } else {
159                                break;
160                            }
161                        }
162                        ast::Expr::GetAttr(expr) => {
163                            attrs.push(expr.name);
164                            ptr = &expr.expr;
165                            continue;
166                        }
167                        _ => break,
168                    }
169                }
170            }
171            tracker_visit_expr(&expr.expr, state)
172        }
173        ast::Expr::GetItem(expr) => {
174            tracker_visit_expr(&expr.expr, state);
175            tracker_visit_expr(&expr.subscript_expr, state);
176        }
177        ast::Expr::Slice(slice) => {
178            tracker_visit_expr_opt(&slice.start, state);
179            tracker_visit_expr_opt(&slice.stop, state);
180            tracker_visit_expr_opt(&slice.step, state);
181        }
182        ast::Expr::Call(expr) => {
183            tracker_visit_expr(&expr.expr, state);
184            expr.args
185                .iter()
186                .for_each(|x| tracker_visit_callarg(x, state));
187        }
188        ast::Expr::List(expr) => expr.items.iter().for_each(|x| tracker_visit_expr(x, state)),
189        ast::Expr::Map(expr) => expr.keys.iter().zip(expr.values.iter()).for_each(|(k, v)| {
190            tracker_visit_expr(k, state);
191            tracker_visit_expr(v, state);
192        }),
193    }
194}
195
196fn track_assign<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a>) {
197    match expr {
198        ast::Expr::Var(var) => state.assign(var.id),
199        ast::Expr::List(list) => list.items.iter().for_each(|x| track_assign(x, state)),
200        _ => {}
201    }
202}
203
204fn track_walk<'a>(node: &ast::Stmt<'a>, state: &mut AssignmentTracker<'a>) {
205    match node {
206        ast::Stmt::Template(stmt) => {
207            state.assign("self");
208            stmt.children.iter().for_each(|x| track_walk(x, state));
209        }
210        ast::Stmt::EmitExpr(expr) => tracker_visit_expr(&expr.expr, state),
211        ast::Stmt::EmitRaw(_) => {}
212        ast::Stmt::ForLoop(stmt) => {
213            state.push();
214            state.assign("loop");
215            tracker_visit_expr(&stmt.iter, state);
216            track_assign(&stmt.target, state);
217            tracker_visit_expr_opt(&stmt.filter_expr, state);
218            stmt.body.iter().for_each(|x| track_walk(x, state));
219            state.pop();
220            state.push();
221            stmt.else_body.iter().for_each(|x| track_walk(x, state));
222            state.pop();
223        }
224        ast::Stmt::IfCond(stmt) => {
225            tracker_visit_expr(&stmt.expr, state);
226            state.push();
227            stmt.true_body.iter().for_each(|x| track_walk(x, state));
228            state.pop();
229            state.push();
230            stmt.false_body.iter().for_each(|x| track_walk(x, state));
231            state.pop();
232        }
233        ast::Stmt::WithBlock(stmt) => {
234            state.push();
235            for (target, expr) in &stmt.assignments {
236                track_assign(target, state);
237                tracker_visit_expr(expr, state);
238            }
239            stmt.body.iter().for_each(|x| track_walk(x, state));
240            state.pop();
241        }
242        ast::Stmt::Set(stmt) => {
243            track_assign(&stmt.target, state);
244            tracker_visit_expr(&stmt.expr, state);
245        }
246        ast::Stmt::AutoEscape(stmt) => {
247            state.push();
248            stmt.body.iter().for_each(|x| track_walk(x, state));
249            state.pop();
250        }
251        ast::Stmt::FilterBlock(stmt) => {
252            state.push();
253            stmt.body.iter().for_each(|x| track_walk(x, state));
254            state.pop();
255        }
256        ast::Stmt::SetBlock(stmt) => {
257            track_assign(&stmt.target, state);
258            state.push();
259            stmt.body.iter().for_each(|x| track_walk(x, state));
260            state.pop();
261        }
262        #[cfg(feature = "multi_template")]
263        ast::Stmt::Block(stmt) => {
264            state.push();
265            state.assign("super");
266            stmt.body.iter().for_each(|x| track_walk(x, state));
267            state.pop();
268        }
269        #[cfg(feature = "multi_template")]
270        ast::Stmt::Extends(_) | ast::Stmt::Include(_) => {}
271        #[cfg(feature = "multi_template")]
272        ast::Stmt::Import(stmt) => {
273            track_assign(&stmt.name, state);
274        }
275        #[cfg(feature = "multi_template")]
276        ast::Stmt::FromImport(stmt) => stmt.names.iter().for_each(|(arg, alias)| {
277            track_assign(alias.as_ref().unwrap_or(arg), state);
278        }),
279        #[cfg(feature = "macros")]
280        ast::Stmt::Macro(stmt) => {
281            state.assign(stmt.name);
282            state.push();
283            tracker_visit_macro(stmt, state, true);
284            state.pop();
285        }
286        #[cfg(feature = "macros")]
287        ast::Stmt::CallBlock(stmt) => {
288            tracker_visit_expr(&stmt.call.expr, state);
289            stmt.call
290                .args
291                .iter()
292                .for_each(|x| tracker_visit_callarg(x, state));
293            state.push();
294            tracker_visit_macro(&stmt.macro_decl, state, true);
295            state.pop();
296        }
297        #[cfg(feature = "loop_controls")]
298        ast::Stmt::Continue(_) | ast::Stmt::Break(_) => {}
299        ast::Stmt::Do(stmt) => {
300            tracker_visit_expr(&stmt.call.expr, state);
301            stmt.call
302                .args
303                .iter()
304                .for_each(|x| tracker_visit_callarg(x, state));
305        }
306    }
307}