1use std::sync::LazyLock;
5
6use regex::Regex;
7
8pub const RUN_ATTRS: &[&str] = &[
10 "report",
11 "set",
12 "target",
13 "profile",
14 "source_type",
15 "schedule",
16 "date",
17 "date_format",
18 "now",
19 "timezone",
20];
21pub use crate::dates::DATE_ATTRS;
23
24static SEGMENT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)\{\{.*?\}\}|\{%.*?%\}").unwrap());
25static VAR: LazyLock<Regex> =
26 LazyLock::new(|| Regex::new(r#"(?:^|[^\w.])(env_var|var)\s*\(\s*(['"])([^'"]+)['"]\s*([,)])"#).unwrap());
27static RUN: LazyLock<Regex> =
28 LazyLock::new(|| Regex::new(r"(?:^|[^\w.])run\.([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?").unwrap());
29
30static MACRO: LazyLock<Regex> = LazyLock::new(|| {
31 Regex::new(r"(?s)\{%-?\s*macro\s+([A-Za-z_]\w*)\s*\(.*?%\}(.*?)\{%-?\s*endmacro\s*-?%\}").unwrap()
32});
33static CALL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?:^|[^\w.])([A-Za-z_]\w*)\s*\(").unwrap());
34
35static REF: LazyLock<Regex> =
36 LazyLock::new(|| Regex::new(r#"(?:^|[^\w.])ref\s*\(\s*['"]([^'"]+)['"]\s*\)"#).unwrap());
37
38pub fn refs(src: &str) -> Vec<(String, usize)> {
40 let mut out = Vec::new();
41 for seg in SEGMENT.find_iter(src) {
42 for c in REF.captures_iter(seg.as_str()) {
43 out.push((
44 c[1].to_string(),
45 line_at(src, seg.start() + c.get(1).unwrap().start()),
46 ));
47 }
48 }
49 out
50}
51
52#[derive(Debug, Clone)]
54pub struct MacroDef {
55 pub name: String,
56 pub body: String,
58 pub line_offset: usize,
60}
61
62pub fn macro_defs(src: &str) -> Vec<MacroDef> {
64 MACRO
65 .captures_iter(src)
66 .map(|c| {
67 let body = c.get(2).unwrap();
68 MacroDef {
69 name: c[1].to_string(),
70 body: body.as_str().to_string(),
71 line_offset: line_at(src, body.start()) - 1,
72 }
73 })
74 .collect()
75}
76
77pub fn called_names(src: &str) -> Vec<String> {
79 let mut out = Vec::new();
80 for seg in SEGMENT.find_iter(src) {
81 for c in CALL.captures_iter(seg.as_str()) {
82 out.push(c[1].to_string());
83 }
84 }
85 out
86}
87
88pub fn is_templated(s: &str) -> bool {
90 s.contains("{{") || s.contains("{%")
91}
92
93pub fn check_syntax(name: &str, src: &str) -> Result<(), (Option<usize>, String)> {
95 let env = minijinja::Environment::new();
96 match env.template_from_named_str(name, src) {
97 Ok(_) => Ok(()),
98 Err(e) => Err((e.line(), e.detail().unwrap_or("syntax error").to_string())),
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct Call {
104 pub func: &'static str,
106 pub name: String,
107 pub has_default: bool,
108 pub line: usize,
110}
111
112fn line_at(src: &str, offset: usize) -> usize {
113 src[..offset].matches('\n').count() + 1
114}
115
116pub fn calls(src: &str) -> Vec<Call> {
118 let mut out = Vec::new();
119 for seg in SEGMENT.find_iter(src) {
120 for c in VAR.captures_iter(seg.as_str()) {
121 let m = c.get(1).unwrap();
122 out.push(Call {
123 func: if m.as_str() == "var" { "var" } else { "env_var" },
124 name: c[3].to_string(),
125 has_default: &c[4] == ",",
126 line: line_at(src, seg.start() + m.start()),
127 });
128 }
129 }
130 out
131}
132
133pub fn unknown_run_refs(src: &str) -> Vec<(String, usize)> {
135 let mut out = Vec::new();
136 for seg in SEGMENT.find_iter(src) {
137 for c in RUN.captures_iter(seg.as_str()) {
138 let attr = &c[1];
139 let line = line_at(src, seg.start() + c.get(0).unwrap().start());
140 if !RUN_ATTRS.contains(&attr) {
141 out.push((format!("run.{attr}"), line));
142 } else if attr == "date"
143 && let Some(sub) = c.get(2)
144 && !DATE_ATTRS.contains(&sub.as_str())
145 {
146 out.push((format!("run.date.{}", sub.as_str()), line));
147 }
148 }
149 }
150 out
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn finds_var_calls_with_and_without_defaults() {
159 let src = "select {{ var('a') }},\n {{ var(\"b\", 1) }}, {{ env_var('HOME') }} from t";
160 let c = calls(src);
161 assert_eq!(c.len(), 3);
162 assert_eq!((c[0].name.as_str(), c[0].has_default, c[0].line), ("a", false, 1));
163 assert_eq!((c[1].name.as_str(), c[1].has_default, c[1].line), ("b", true, 2));
164 assert_eq!((c[2].func, c[2].name.as_str()), ("env_var", "HOME"));
165 }
166
167 #[test]
168 fn ignores_sql_outside_jinja_and_method_like_names() {
169 assert!(calls("select var('x') from t").is_empty());
170 assert!(calls("{{ my.var('x') }} {{ env_var_x('y') }}").is_empty());
171 assert!(unknown_run_refs("select run.dat from runs run").is_empty());
172 }
173
174 #[test]
175 fn flags_unknown_run_attributes() {
176 let src = "{{ run.report }} {{ run.dat }}\n{{ run.date.yyyymmdd }} {{ run.date.yymm }}";
177 assert_eq!(
178 unknown_run_refs(src),
179 vec![("run.dat".to_string(), 1), ("run.date.yymm".to_string(), 2)]
180 );
181 }
182}