tera_introspection/
introspection.rs1use 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
92fn 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}