1use std::collections::HashSet;
3
4use crate::ast::{ActionNode, Ast, Command, Expression, IfNode, Node, RangeNode, Span, WithNode};
5use crate::lexer::{Token, TokenKind};
6use crate::runtime::FunctionRegistry;
7
8pub fn analyze_template(ast: &Ast, registry: Option<&FunctionRegistry>) -> TemplateAnalysis {
9 let mut analyzer = Analyzer::new(registry);
10 analyzer.walk_block(&ast.root);
11 analyzer.finish()
12}
13
14#[derive(Debug, Clone)]
15pub struct TemplateAnalysis {
16 pub version: &'static str,
17 pub precision: Precision,
18 pub has_template_invocation: bool,
19 pub variables: Vec<VariableAccess>,
20 pub functions: Vec<FunctionCall>,
21 pub templates: Vec<TemplateCall>,
22 pub controls: Vec<ControlUsage>,
23 pub issues: Vec<AnalysisIssue>,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Precision {
28 Precise,
29 Conservative,
30}
31
32#[derive(Debug, Clone)]
33pub struct VariableAccess {
34 pub path: String,
35 pub span: Span,
36 pub kind: VariableKind,
37 pub certainty: Certainty,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum VariableKind {
42 Dot,
43 Dollar,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Certainty {
48 Certain,
49 Uncertain,
50}
51
52#[derive(Debug, Clone)]
53pub struct FunctionCall {
54 pub name: String,
55 pub span: Span,
56 pub source: FunctionSource,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum FunctionSource {
61 Registered,
62 Unknown,
63}
64
65#[derive(Debug, Clone)]
66pub struct TemplateCall {
67 pub span: Span,
68 pub name: Option<String>,
69 pub indirect: bool,
70}
71
72#[derive(Debug, Clone)]
73pub struct ControlUsage {
74 pub kind: ControlKind,
75 pub span: Span,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ControlKind {
80 If,
81 Range,
82 With,
83 Block,
84 Define,
85 Else,
86 End,
87}
88
89#[derive(Debug, Clone)]
90pub struct AnalysisIssue {
91 pub message: String,
92 pub span: Option<Span>,
93}
94
95struct Analyzer<'a> {
96 registry: Option<&'a FunctionRegistry>,
97 variables: Vec<VariableAccess>,
98 functions: Vec<FunctionCall>,
99 templates: Vec<TemplateCall>,
100 controls: Vec<ControlUsage>,
101 issues: Vec<AnalysisIssue>,
102 has_template: bool,
103 conservative: bool,
104 seen_vars: HashSet<(String, Span)>,
105}
106
107impl<'a> Analyzer<'a> {
108 fn new(registry: Option<&'a FunctionRegistry>) -> Self {
109 Self {
110 registry,
111 variables: Vec::new(),
112 functions: Vec::new(),
113 templates: Vec::new(),
114 controls: Vec::new(),
115 issues: Vec::new(),
116 has_template: false,
117 conservative: false,
118 seen_vars: HashSet::new(),
119 }
120 }
121
122 fn finish(self) -> TemplateAnalysis {
123 TemplateAnalysis {
124 version: env!("CARGO_PKG_VERSION"),
125 precision: if self.conservative {
126 Precision::Conservative
127 } else {
128 Precision::Precise
129 },
130 has_template_invocation: self.has_template,
131 variables: self.variables,
132 functions: self.functions,
133 templates: self.templates,
134 controls: self.controls,
135 issues: self.issues,
136 }
137 }
138
139 fn walk_block(&mut self, block: &crate::ast::Block) {
140 for node in &block.nodes {
141 match node {
142 Node::Action(action) => self.visit_action(action),
143 Node::If(if_node) => self.visit_if(if_node),
144 Node::Range(range_node) => self.visit_range(range_node),
145 Node::With(with_node) => self.visit_with(with_node),
146 Node::Text(_) | Node::Comment(_) => {}
147 }
148 }
149 }
150
151 fn visit_action(&mut self, action: &ActionNode) {
152 self.inspect_tokens(&action.tokens);
153 self.visit_pipeline(&action.pipeline, action.span);
154 }
155
156 fn visit_if(&mut self, node: &IfNode) {
157 self.inspect_tokens(&node.tokens);
158 self.controls.push(ControlUsage {
159 kind: ControlKind::If,
160 span: node.span,
161 });
162 self.visit_pipeline(&node.pipeline, node.span);
163 self.walk_block(&node.then_block);
164 if let Some(else_block) = &node.else_block {
165 self.walk_block(else_block);
166 }
167 }
168
169 fn visit_range(&mut self, node: &RangeNode) {
170 self.inspect_tokens(&node.tokens);
171 self.controls.push(ControlUsage {
172 kind: ControlKind::Range,
173 span: node.span,
174 });
175 self.visit_pipeline(&node.pipeline, node.span);
176 self.walk_block(&node.then_block);
177 if let Some(else_block) = &node.else_block {
178 self.walk_block(else_block);
179 }
180 }
181
182 fn visit_with(&mut self, node: &WithNode) {
183 self.inspect_tokens(&node.tokens);
184 self.controls.push(ControlUsage {
185 kind: ControlKind::With,
186 span: node.span,
187 });
188 self.visit_pipeline(&node.pipeline, node.span);
189 self.walk_block(&node.then_block);
190 if let Some(else_block) = &node.else_block {
191 self.walk_block(else_block);
192 }
193 }
194
195 fn inspect_tokens(&mut self, tokens: &[Token]) {
196 for token in tokens {
197 match token.kind {
198 TokenKind::LeftBracket
199 | TokenKind::RightBracket
200 | TokenKind::Declare
201 | TokenKind::Assign => {
202 self.mark_conservative(
203 "indexing or assignments are not fully analysed",
204 Some(token.span),
205 );
206 }
207 _ => {}
208 }
209 }
210 }
211
212 fn visit_pipeline(&mut self, pipeline: &crate::ast::Pipeline, span: Span) {
213 for command in &pipeline.commands {
214 self.visit_command(command, span);
215 }
216 }
217
218 fn visit_command(&mut self, command: &Command, span: Span) {
219 self.collect_expr(&command.target, span);
220
221 match &command.target {
222 Expression::Identifier(name) => {
223 let lowered = name.as_str();
224 if lowered == "template" || lowered == "block" {
225 self.record_template(command, span, lowered == "block");
226 } else if let Some(control) = control_kind(lowered) {
227 self.controls.push(ControlUsage {
228 kind: control,
229 span,
230 });
231 } else {
232 self.record_function(name.clone(), span);
233 }
234 }
235 Expression::Variable(name) => {
236 self.record_variable(name.clone(), span, VariableKind::Dollar, Certainty::Certain);
237 }
238 _ => {}
239 }
240
241 for arg in &command.args {
242 self.collect_expr(arg, span);
243 }
244 }
245
246 fn collect_expr(&mut self, expr: &Expression, span: Span) {
247 match expr {
248 Expression::Field(parts) => {
249 let (path, certainty) = normalize_field(parts);
250 self.record_variable(path, span, VariableKind::Dot, certainty);
251 }
252 Expression::Identifier(name) if name.starts_with('$') => {
253 self.record_variable(name.clone(), span, VariableKind::Dollar, Certainty::Certain);
254 }
255 Expression::PipelineExpr(pipeline) => {
256 self.visit_pipeline(pipeline, span);
257 }
258 Expression::Variable(name) => {
259 self.record_variable(name.clone(), span, VariableKind::Dollar, Certainty::Certain);
260 }
261 _ => {}
262 }
263 }
264
265 fn record_variable(
266 &mut self,
267 path: String,
268 span: Span,
269 kind: VariableKind,
270 certainty: Certainty,
271 ) {
272 let key = (path.clone(), span);
273 if self.seen_vars.insert(key) {
274 self.variables.push(VariableAccess {
275 path,
276 span,
277 kind,
278 certainty,
279 });
280 }
281 }
282
283 fn record_function(&mut self, name: String, span: Span) {
284 let source = if self
285 .registry
286 .map(|reg| reg.get(&name).is_some())
287 .unwrap_or(false)
288 {
289 FunctionSource::Registered
290 } else {
291 FunctionSource::Unknown
292 };
293 self.functions.push(FunctionCall { name, span, source });
294 }
295
296 fn record_template(&mut self, command: &Command, span: Span, is_block: bool) {
297 self.has_template = true;
298 let template_name = command.args.first();
299 let (name, indirect) = match template_name {
300 Some(Expression::StringLiteral(lit)) => (Some(lit.clone()), false),
301 Some(_) => (None, true),
302 None => (None, true),
303 };
304 if indirect {
305 self.mark_conservative("dynamic template invocation", Some(span));
306 }
307 self.templates.push(TemplateCall {
308 span,
309 name,
310 indirect,
311 });
312 if is_block {
313 self.controls.push(ControlUsage {
314 kind: ControlKind::Block,
315 span,
316 });
317 }
318 }
319
320 fn mark_conservative(&mut self, message: impl Into<String>, span: Option<Span>) {
321 self.conservative = true;
322 self.issues.push(AnalysisIssue {
323 message: message.into(),
324 span,
325 });
326 }
327}
328
329fn control_kind(name: &str) -> Option<ControlKind> {
330 match name {
331 "if" => Some(ControlKind::If),
332 "range" => Some(ControlKind::Range),
333 "with" => Some(ControlKind::With),
334 "block" => Some(ControlKind::Block),
335 "define" => Some(ControlKind::Define),
336 "else" => Some(ControlKind::Else),
337 "end" => Some(ControlKind::End),
338 _ => None,
339 }
340}
341
342fn normalize_field(parts: &[String]) -> (String, Certainty) {
343 if parts.is_empty() {
344 return (".".to_string(), Certainty::Certain);
345 }
346 let mut certainty = Certainty::Certain;
347 let mut normalized_parts = Vec::with_capacity(parts.len());
348 for part in parts {
349 if part.chars().all(|c| c.is_alphanumeric() || c == '_') {
350 normalized_parts.push(part.clone());
351 } else {
352 certainty = Certainty::Uncertain;
353 normalized_parts.push(part.clone());
354 }
355 }
356 (format!(".{}", normalized_parts.join(".")), certainty)
357}