tera_introspection/
introspection.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::{
4    parser::ast::{Expr, ExprVal},
5    Node,
6};
7use itertools::Itertools;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Serialize, Deserialize)]
11pub struct TeraIntrospection {
12    pub extends: HashSet<String>,
13    pub includes: HashSet<String>,
14    pub macros: HashSet<String>,
15    pub idents: HashSet<String>,
16}
17
18impl TeraIntrospection {
19    pub fn new(nodes: &[Node], mapping: &mut HashMap<String, Vec<String>>) -> Self {
20        let mut extends = HashSet::new();
21        let mut includes = HashSet::new();
22        let mut macros = HashSet::new();
23        let mut idents = HashSet::new();
24        let mut items = vec![];
25
26        for node in nodes {
27            match node {
28                Node::Extends(_, name) => {
29                    extends.insert(name.to_string());
30                }
31                Node::Include(_, template, _) => {
32                    includes.insert(template.iter().join(""));
33                }
34                Node::ImportMacro(_, name, _) => {
35                    macros.insert(name.to_string());
36                }
37                Node::Block(_, block, _) => {
38                    items.push(TeraIntrospection::new(block.body.as_ref(), mapping))
39                }
40                Node::Forloop(_, for_loop, _) => {
41                    let value = for_loop.value.clone();
42                    add_mapping(mapping, &value, &for_loop.container, true);
43                    add_ident(&mut idents, &for_loop.container, mapping);
44                    items.push(TeraIntrospection::new(for_loop.body.as_ref(), mapping))
45                }
46                Node::If(if_cond, _) => {
47                    for (_, expr, body) in &if_cond.conditions {
48                        add_ident(&mut idents, expr, mapping);
49                        items.push(TeraIntrospection::new(body.as_ref(), mapping))
50                    }
51                    if let Some(else_cond) = &if_cond.otherwise {
52                        items.push(TeraIntrospection::new(else_cond.1.as_ref(), mapping))
53                    }
54                }
55                Node::VariableBlock(_, expr) => {
56                    add_ident(&mut idents, expr, mapping);
57                }
58                _ => (),
59            }
60        }
61
62        let mut ins = Self {
63            extends,
64            includes,
65            macros,
66            idents,
67        };
68
69        for item in items {
70            ins.merge(item);
71        }
72
73        ins
74    }
75
76    fn merge(&mut self, other: TeraIntrospection) {
77        self.extends.extend(other.extends);
78        self.includes.extend(other.includes);
79        self.macros.extend(other.macros);
80        self.idents.extend(other.idents);
81    }
82}
83
84fn add_ident(idents: &mut HashSet<String>, expr: &Expr, mapping: &HashMap<String, Vec<String>>) {
85    if let ExprVal::Ident(ref v) = expr.val {
86        let names = split_ident(v);
87        let name = expand_names(&names, mapping).into_iter().join(".");
88        idents.insert(name);
89    }
90}
91
92// use nom to parse a.b[c] or a[b][c] or a.b.c to ["a", "b", "c"]
93fn split_ident(ident: &str) -> Vec<String> {
94    ident
95        .split(&['.', '['])
96        .map(|v| v.trim_end_matches(']').to_string())
97        .collect()
98}
99
100fn expand_names(names: &[String], mapping: &HashMap<String, Vec<String>>) -> Vec<String> {
101    let first = names.first().expect("should exits");
102    if let Some(v) = mapping.get(first) {
103        let mut ret = v.clone();
104        ret.extend_from_slice(&names[1..]);
105        expand_names(&ret, mapping)
106    } else {
107        names.to_vec()
108    }
109}
110
111fn add_mapping(
112    mapping: &mut HashMap<String, Vec<String>>,
113    value: &str,
114    expr: &Expr,
115    is_loop: bool,
116) {
117    if let ExprVal::Ident(ref v) = expr.val {
118        let mut names = split_ident(v);
119        if is_loop {
120            if let Some(v) = names.last_mut() {
121                v.push_str("()")
122            }
123        }
124        mapping.insert(value.to_owned(), names);
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use crate::parse;
131
132    use super::*;
133
134    #[test]
135    fn tera_introspection_should_work() {
136        let extend = include_str!("../fixtures/table.j2");
137        let nodes = parse(extend).unwrap();
138        let mut mapping = HashMap::new();
139        let ins = TeraIntrospection::new(nodes.as_ref(), &mut mapping);
140        assert!(ins
141            .extends
142            .contains("crn:cws:cella:us-west2::view:todo/main"));
143        assert!(ins
144            .includes
145            .contains("crn:cws:cella:us-west2::view:todo/table"));
146        assert!(ins.macros.is_empty());
147        assert_eq!(
148            ins.idents,
149            [
150                "data.items().col",
151                "data.items",
152                "data.names",
153                "config.edit_get",
154                "loop.first",
155                "data.items().values",
156                "data.items().values().abc",
157                "config.edit_title"
158            ]
159            .iter()
160            .map(|v| v.to_string())
161            .collect()
162        );
163    }
164}