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