syncable_cli/analyzer/helmlint/parser/
helpers.rs1use std::collections::HashSet;
6use std::path::Path;
7
8use crate::analyzer::helmlint::parser::template::{ParsedTemplate, TemplateToken, parse_template};
9
10#[derive(Debug, Clone)]
12pub struct HelperDefinition {
13 pub name: String,
15 pub line: u32,
17 pub content: String,
19 pub doc_comment: Option<String>,
21}
22
23#[derive(Debug, Clone)]
25pub struct ParsedHelpers {
26 pub path: String,
28 pub helpers: Vec<HelperDefinition>,
30 pub helper_names: HashSet<String>,
32 pub template: ParsedTemplate,
34}
35
36impl ParsedHelpers {
37 pub fn has_helper(&self, name: &str) -> bool {
39 self.helper_names.contains(name)
40 }
41
42 pub fn get_helper(&self, name: &str) -> Option<&HelperDefinition> {
44 self.helpers.iter().find(|h| h.name == name)
45 }
46
47 pub fn names(&self) -> impl Iterator<Item = &str> {
49 self.helper_names.iter().map(|s| s.as_str())
50 }
51}
52
53pub fn parse_helpers(content: &str, path: &str) -> ParsedHelpers {
55 let template = parse_template(content, path);
56 let mut helpers = Vec::new();
57 let mut helper_names = HashSet::new();
58
59 let mut last_comment: Option<(String, u32)> = None;
61
62 let mut i = 0;
64 while i < template.tokens.len() {
65 let token = &template.tokens[i];
66
67 match token {
68 TemplateToken::Comment { content, line } => {
69 last_comment = Some((content.clone(), *line));
71 }
72 TemplateToken::Action { content, line, .. } => {
73 let trimmed = content.trim();
74 if trimmed.starts_with("define ") {
75 if let Some(name) = extract_define_name(trimmed) {
77 let mut helper_content = String::new();
79 let mut depth = 1;
80 let mut j = i + 1;
81
82 while j < template.tokens.len() && depth > 0 {
83 match &template.tokens[j] {
84 TemplateToken::Action {
85 content: inner_content,
86 ..
87 } => {
88 let inner_trimmed = inner_content.trim();
89 if inner_trimmed.starts_with("define ")
90 || inner_trimmed.starts_with("if ")
91 || inner_trimmed.starts_with("range ")
92 || inner_trimmed.starts_with("with ")
93 || inner_trimmed.starts_with("block ")
94 {
95 depth += 1;
96 } else if inner_trimmed == "end" {
97 depth -= 1;
98 if depth == 0 {
99 break;
100 }
101 }
102 if depth > 0 {
103 helper_content
104 .push_str(&format!("{{{{ {} }}}}", inner_content));
105 }
106 }
107 TemplateToken::Text {
108 content: text_content,
109 ..
110 } => {
111 helper_content.push_str(text_content);
112 }
113 TemplateToken::Comment {
114 content: comment_content,
115 ..
116 } => {
117 helper_content
118 .push_str(&format!("{{{{/* {} */}}}}", comment_content));
119 }
120 }
121 j += 1;
122 }
123
124 let doc_comment = last_comment
128 .take()
129 .filter(|(_, comment_line)| {
130 *line > *comment_line && *line - *comment_line <= 5
131 })
132 .map(|(c, _)| c);
133
134 helpers.push(HelperDefinition {
135 name: name.clone(),
136 line: *line,
137 content: helper_content.trim().to_string(),
138 doc_comment,
139 });
140 helper_names.insert(name);
141 }
142 }
143
144 if !content.trim().starts_with("define ") {
146 last_comment = None;
147 }
148 }
149 TemplateToken::Text { .. } => {
150 if !token.content().trim().is_empty() {
152 last_comment = None;
153 }
154 }
155 }
156 i += 1;
157 }
158
159 ParsedHelpers {
160 path: path.to_string(),
161 helpers,
162 helper_names,
163 template,
164 }
165}
166
167pub fn parse_helpers_file(path: &Path) -> Result<ParsedHelpers, std::io::Error> {
169 let content = std::fs::read_to_string(path)?;
170 Ok(parse_helpers(&content, &path.display().to_string()))
171}
172
173fn extract_define_name(content: &str) -> Option<String> {
175 let parts: Vec<&str> = content.split('"').collect();
177 if parts.len() >= 2 {
178 let name = parts[1].trim();
179 if !name.is_empty() {
180 return Some(name.to_string());
181 }
182 }
183 None
184}
185
186pub const COMMON_HELPERS: &[&str] = &[
188 "chart",
189 "name",
190 "fullname",
191 "labels",
192 "selectorLabels",
193 "serviceAccountName",
194 "image",
195];
196
197pub fn is_valid_helper_name(name: &str) -> bool {
199 if name.is_empty() {
201 return false;
202 }
203
204 name.chars()
206 .all(|c| c.is_alphanumeric() || c == '.' || c == '-' || c == '_')
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_parse_helpers() {
215 let content = r#"
216{{/*
217Get the name of the chart.
218*/}}
219{{- define "mychart.name" -}}
220{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
221{{- end }}
222
223{{- define "mychart.fullname" -}}
224{{- if .Values.fullnameOverride }}
225{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
226{{- else }}
227{{- $name := default .Chart.Name .Values.nameOverride }}
228{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
229{{- end }}
230{{- end }}
231
232{{- define "mychart.labels" -}}
233app.kubernetes.io/name: {{ include "mychart.name" . }}
234app.kubernetes.io/instance: {{ .Release.Name }}
235{{- end }}
236"#;
237 let parsed = parse_helpers(content, "_helpers.tpl");
238
239 assert!(parsed.has_helper("mychart.name"));
240 assert!(parsed.has_helper("mychart.fullname"));
241 assert!(parsed.has_helper("mychart.labels"));
242 assert_eq!(parsed.helpers.len(), 3);
243
244 let name_helper = parsed.get_helper("mychart.name").unwrap();
246 assert!(name_helper.doc_comment.is_some());
247 assert!(
248 name_helper
249 .doc_comment
250 .as_ref()
251 .unwrap()
252 .contains("Get the name")
253 );
254 }
255
256 #[test]
257 fn test_parse_empty_helpers() {
258 let content = "";
259 let parsed = parse_helpers(content, "_helpers.tpl");
260 assert!(parsed.helpers.is_empty());
261 }
262
263 #[test]
264 fn test_valid_helper_name() {
265 assert!(is_valid_helper_name("mychart.name"));
266 assert!(is_valid_helper_name("my-chart.full_name"));
267 assert!(is_valid_helper_name("common.labels"));
268 assert!(!is_valid_helper_name(""));
269 assert!(!is_valid_helper_name("has space"));
270 assert!(!is_valid_helper_name("has:colon"));
271 }
272
273 #[test]
274 fn test_helper_content() {
275 let content = r#"
276{{- define "simple.helper" -}}
277hello world
278{{- end }}
279"#;
280 let parsed = parse_helpers(content, "_helpers.tpl");
281 let helper = parsed.get_helper("simple.helper").unwrap();
282 assert!(helper.content.contains("hello world"));
283 }
284
285 #[test]
286 fn test_nested_structures() {
287 let content = r#"
288{{- define "mychart.conditional" -}}
289{{- if .Values.enabled }}
290enabled
291{{- else }}
292disabled
293{{- end }}
294{{- end }}
295"#;
296 let parsed = parse_helpers(content, "_helpers.tpl");
297 assert!(parsed.has_helper("mychart.conditional"));
298 assert!(parsed.template.errors.is_empty());
299 }
300}