1use std::sync::LazyLock;
5
6use regex::Regex;
7
8pub const RUN_ATTRS: &[&str] = &[
10 "report",
11 "set",
12 "target",
13 "schedule",
14 "date",
15 "date_format",
16 "now",
17 "scheduled_at",
18 "timezone",
19];
20pub use crate::dates::DATE_ATTRS;
22
23static SEGMENT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?s)\{\{.*?\}\}|\{%.*?%\}").unwrap());
24static VAR: LazyLock<Regex> =
25 LazyLock::new(|| Regex::new(r#"(?:^|[^\w.])(env_var|var)\s*\(\s*(['"])([^'"]+)['"]\s*([,)])"#).unwrap());
26static RUN: LazyLock<Regex> =
27 LazyLock::new(|| Regex::new(r"(?:^|[^\w.])run\.([A-Za-z_]\w*)(?:\.([A-Za-z_]\w*))?").unwrap());
28
29static MACRO: LazyLock<Regex> = LazyLock::new(|| {
30 Regex::new(r"(?s)\{%-?\s*macro\s+([A-Za-z_]\w*)\s*\(.*?%\}(.*?)\{%-?\s*endmacro\s*-?%\}").unwrap()
31});
32static CALL: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?:^|[^\w.])([A-Za-z_]\w*)\s*\(").unwrap());
33
34static REF: LazyLock<Regex> =
35 LazyLock::new(|| Regex::new(r#"(?:^|[^\w.])ref\s*\(\s*['"]([^'"]+)['"]\s*\)"#).unwrap());
36
37static TARGET: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?:^|[^\w.])target\.([A-Za-z_]\w*)").unwrap());
38static SOURCE_ROLE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"role\s*=\s*['"]source['"]"#).unwrap());
39
40pub fn removed_names(src: &str) -> Vec<(String, String, usize)> {
42 let mut out = Vec::new();
43 for seg in SEGMENT.find_iter(src) {
44 for c in TARGET.captures_iter(seg.as_str()) {
45 let field = &c[1];
46 if field == "name" {
47 continue;
48 }
49 let line = line_at(src, seg.start() + c.get(0).unwrap().start());
50 let instead = match field {
51 "type" => "`connection.type` (the query's connection)".to_string(),
52 "profile" => "`connection.name` (the query's connection)".to_string(),
53 f => format!(
54 "`connection.{f}` (the query's connection) or `profile('<name>').{f}`; `target` is now only the environment, `target.name`"
55 ),
56 };
57 out.push((format!("target.{field}"), instead, line));
58 }
59 for c in RUN.captures_iter(seg.as_str()) {
60 let instead = match &c[1] {
61 "profile" => "`connection.name` (the query's connection)",
62 "source_type" => "`connection.type` (the query's connection)",
63 _ => continue,
64 };
65 let line = line_at(src, seg.start() + c.get(0).unwrap().start());
66 out.push((format!("run.{}", &c[1]), instead.to_string(), line));
67 }
68 for m in SOURCE_ROLE.find_iter(seg.as_str()) {
69 out.push((
70 "role='source'".into(),
71 "`role='connection'`".into(),
72 line_at(src, seg.start() + m.start()),
73 ));
74 }
75 }
76 out
77}
78
79pub fn refs(src: &str) -> Vec<(String, usize)> {
81 let mut out = Vec::new();
82 for seg in SEGMENT.find_iter(src) {
83 for c in REF.captures_iter(seg.as_str()) {
84 out.push((
85 c[1].to_string(),
86 line_at(src, seg.start() + c.get(1).unwrap().start()),
87 ));
88 }
89 }
90 out
91}
92
93#[derive(Debug, Clone)]
95pub struct MacroDef {
96 pub name: String,
97 pub body: String,
99 pub line_offset: usize,
101}
102
103pub fn macro_defs(src: &str) -> Vec<MacroDef> {
105 MACRO
106 .captures_iter(src)
107 .map(|c| {
108 let body = c.get(2).unwrap();
109 MacroDef {
110 name: c[1].to_string(),
111 body: body.as_str().to_string(),
112 line_offset: line_at(src, body.start()) - 1,
113 }
114 })
115 .collect()
116}
117
118pub fn called_names(src: &str) -> Vec<String> {
120 let mut out = Vec::new();
121 for seg in SEGMENT.find_iter(src) {
122 for c in CALL.captures_iter(seg.as_str()) {
123 out.push(c[1].to_string());
124 }
125 }
126 out
127}
128
129pub fn is_templated(s: &str) -> bool {
131 s.contains("{{") || s.contains("{%")
132}
133
134pub fn check_syntax(name: &str, src: &str) -> Result<(), (Option<usize>, String)> {
136 let env = minijinja::Environment::new();
137 match env.template_from_named_str(name, src) {
138 Ok(_) => Ok(()),
139 Err(e) => Err((e.line(), e.detail().unwrap_or("syntax error").to_string())),
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct Call {
145 pub func: &'static str,
147 pub name: String,
148 pub has_default: bool,
149 pub line: usize,
151}
152
153fn line_at(src: &str, offset: usize) -> usize {
154 src[..offset].matches('\n').count() + 1
155}
156
157pub fn calls(src: &str) -> Vec<Call> {
159 let mut out = Vec::new();
160 for seg in SEGMENT.find_iter(src) {
161 for c in VAR.captures_iter(seg.as_str()) {
162 let m = c.get(1).unwrap();
163 out.push(Call {
164 func: if m.as_str() == "var" { "var" } else { "env_var" },
165 name: c[3].to_string(),
166 has_default: &c[4] == ",",
167 line: line_at(src, seg.start() + m.start()),
168 });
169 }
170 }
171 out
172}
173
174pub fn unknown_run_refs(src: &str) -> Vec<(String, usize)> {
176 let mut out = Vec::new();
177 for seg in SEGMENT.find_iter(src) {
178 for c in RUN.captures_iter(seg.as_str()) {
179 let attr = &c[1];
180 let line = line_at(src, seg.start() + c.get(0).unwrap().start());
181 if matches!(attr, "profile" | "source_type") {
182 continue;
184 }
185 if !RUN_ATTRS.contains(&attr) {
186 out.push((format!("run.{attr}"), line));
187 } else if attr == "date"
188 && let Some(sub) = c.get(2)
189 && !DATE_ATTRS.contains(&sub.as_str())
190 {
191 out.push((format!("run.date.{}", sub.as_str()), line));
192 }
193 }
194 }
195 out
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn finds_var_calls_with_and_without_defaults() {
204 let src = "select {{ var('a') }},\n {{ var(\"b\", 1) }}, {{ env_var('HOME') }} from t";
205 let c = calls(src);
206 assert_eq!(c.len(), 3);
207 assert_eq!((c[0].name.as_str(), c[0].has_default, c[0].line), ("a", false, 1));
208 assert_eq!((c[1].name.as_str(), c[1].has_default, c[1].line), ("b", true, 2));
209 assert_eq!((c[2].func, c[2].name.as_str()), ("env_var", "HOME"));
210 }
211
212 #[test]
213 fn ignores_sql_outside_jinja_and_method_like_names() {
214 assert!(calls("select var('x') from t").is_empty());
215 assert!(calls("{{ my.var('x') }} {{ env_var_x('y') }}").is_empty());
216 assert!(unknown_run_refs("select run.dat from runs run").is_empty());
217 }
218
219 #[test]
220 fn flags_unknown_run_attributes() {
221 let src = "{{ run.report }} {{ run.dat }}\n{{ run.date.yyyymmdd }} {{ run.date.yymm }}";
222 assert_eq!(
223 unknown_run_refs(src),
224 vec![("run.dat".to_string(), 1), ("run.date.yymm".to_string(), 2)]
225 );
226 }
227}