syncable_cli/analyzer/helmlint/
lint.rs1use std::collections::HashSet;
7use std::path::Path;
8
9use crate::analyzer::helmlint::config::HelmlintConfig;
10use crate::analyzer::helmlint::parser::chart::parse_chart_yaml;
11use crate::analyzer::helmlint::parser::helpers::{ParsedHelpers, parse_helpers};
12use crate::analyzer::helmlint::parser::template::parse_template;
13use crate::analyzer::helmlint::parser::values::parse_values_yaml;
14use crate::analyzer::helmlint::pragma::{
15 PragmaState, extract_template_pragmas, extract_yaml_pragmas,
16};
17use crate::analyzer::helmlint::rules::{LintContext, all_rules};
18use crate::analyzer::helmlint::types::{CheckFailure, Severity};
19
20#[derive(Debug, Clone)]
22pub struct LintResult {
23 pub chart_path: String,
25 pub failures: Vec<CheckFailure>,
27 pub parse_errors: Vec<String>,
29 pub files_checked: usize,
31 pub error_count: usize,
33 pub warning_count: usize,
35}
36
37impl LintResult {
38 pub fn new(chart_path: impl Into<String>) -> Self {
40 Self {
41 chart_path: chart_path.into(),
42 failures: Vec::new(),
43 parse_errors: Vec::new(),
44 files_checked: 0,
45 error_count: 0,
46 warning_count: 0,
47 }
48 }
49
50 fn update_counts(&mut self) {
52 self.error_count = self
53 .failures
54 .iter()
55 .filter(|f| f.severity == Severity::Error)
56 .count();
57 self.warning_count = self
58 .failures
59 .iter()
60 .filter(|f| f.severity == Severity::Warning)
61 .count();
62 }
63
64 pub fn has_failures(&self) -> bool {
66 !self.failures.is_empty()
67 }
68
69 pub fn has_errors(&self) -> bool {
71 self.error_count > 0
72 }
73
74 pub fn has_warnings(&self) -> bool {
76 self.warning_count > 0
77 }
78
79 pub fn max_severity(&self) -> Option<Severity> {
81 self.failures.iter().map(|f| f.severity).max()
82 }
83
84 pub fn should_fail(&self, config: &HelmlintConfig) -> bool {
86 if config.no_fail {
87 return false;
88 }
89
90 if let Some(max) = self.max_severity() {
91 max >= config.failure_threshold
92 } else {
93 false
94 }
95 }
96
97 pub fn sort(&mut self) {
99 self.failures.sort();
100 }
101}
102
103pub fn lint_chart(path: &Path, config: &HelmlintConfig) -> LintResult {
105 let chart_path_str = path.display().to_string();
106 let mut result = LintResult::new(&chart_path_str);
107
108 if !path.exists() {
110 result
111 .parse_errors
112 .push(format!("Chart path does not exist: {}", chart_path_str));
113 return result;
114 }
115
116 if !path.is_dir() {
117 result
118 .parse_errors
119 .push(format!("Chart path is not a directory: {}", chart_path_str));
120 return result;
121 }
122
123 let files = collect_chart_files(path);
125 result.files_checked = files.len();
126
127 let chart_yaml_path = path.join("Chart.yaml");
129 let chart_metadata = if chart_yaml_path.exists() {
130 match std::fs::read_to_string(&chart_yaml_path) {
131 Ok(content) => match parse_chart_yaml(&content) {
132 Ok(metadata) => Some(metadata),
133 Err(e) => {
134 result.parse_errors.push(format!("Chart.yaml: {}", e));
135 None
136 }
137 },
138 Err(e) => {
139 result
140 .parse_errors
141 .push(format!("Failed to read Chart.yaml: {}", e));
142 None
143 }
144 }
145 } else {
146 None
147 };
148
149 let values_yaml_path = path.join("values.yaml");
151 let values = if values_yaml_path.exists() {
152 match std::fs::read_to_string(&values_yaml_path) {
153 Ok(content) => match parse_values_yaml(&content) {
154 Ok(v) => Some(v),
155 Err(e) => {
156 result.parse_errors.push(format!("values.yaml: {}", e));
157 None
158 }
159 },
160 Err(e) => {
161 result
162 .parse_errors
163 .push(format!("Failed to read values.yaml: {}", e));
164 None
165 }
166 }
167 } else {
168 None
169 };
170
171 let templates_dir = path.join("templates");
173 let mut templates = Vec::new();
174 let mut helpers: Option<ParsedHelpers> = None;
175
176 if templates_dir.exists() && templates_dir.is_dir() {
177 for entry in walkdir::WalkDir::new(&templates_dir)
178 .into_iter()
179 .filter_map(|e| e.ok())
180 {
181 let file_path = entry.path();
182 if file_path.is_file() {
183 let relative_path = file_path
184 .strip_prefix(path)
185 .unwrap_or(file_path)
186 .display()
187 .to_string();
188
189 if config.is_excluded(&relative_path) {
191 continue;
192 }
193
194 let extension = file_path.extension().and_then(|e| e.to_str());
195 match extension {
196 Some("yaml") | Some("yml") | Some("tpl") | Some("txt") => {
197 match std::fs::read_to_string(file_path) {
198 Ok(content) => {
199 let parsed = parse_template(&content, &relative_path);
200
201 if relative_path.contains("_helpers") {
203 helpers = Some(parse_helpers(&content, &relative_path));
204 }
205
206 templates.push(parsed);
207 }
208 Err(e) => {
209 result
210 .parse_errors
211 .push(format!("Failed to read {}: {}", relative_path, e));
212 }
213 }
214 }
215 _ => {}
216 }
217 }
218 }
219 }
220
221 let mut all_pragmas = PragmaState::new();
223
224 if let Ok(content) = std::fs::read_to_string(&chart_yaml_path) {
226 let pragmas = extract_yaml_pragmas(&content);
227 merge_pragmas(&mut all_pragmas, pragmas);
228 }
229
230 if let Ok(content) = std::fs::read_to_string(&values_yaml_path) {
232 let pragmas = extract_yaml_pragmas(&content);
233 merge_pragmas(&mut all_pragmas, pragmas);
234 }
235
236 for template in &templates {
238 let content = template
239 .tokens
240 .iter()
241 .map(|t| t.content())
242 .collect::<Vec<_>>()
243 .join("");
244 let pragmas = extract_template_pragmas(&content);
245 merge_pragmas(&mut all_pragmas, pragmas);
246 }
247
248 let ctx = LintContext::new(
250 path,
251 chart_metadata.as_ref(),
252 values.as_ref(),
253 helpers.as_ref(),
254 &templates,
255 &files,
256 );
257
258 let rules = all_rules();
260 let mut all_failures = Vec::new();
261
262 for rule in rules {
263 if config.is_rule_ignored(rule.code()) {
265 continue;
266 }
267
268 let failures = rule.check(&ctx);
269 all_failures.extend(failures);
270 }
271
272 result.failures = all_failures
274 .into_iter()
275 .filter(|f| {
276 let effective_severity = config.effective_severity(f.code.as_str(), f.severity);
278 config.should_report(effective_severity)
279 })
280 .filter(|f| !config.is_rule_ignored(f.code.as_str()))
281 .filter(|f| {
282 if config.disable_ignore_pragma {
283 true
284 } else {
285 !all_pragmas.is_ignored(&f.code, f.line)
286 }
287 })
288 .filter(|f| if config.fixable_only { f.fixable } else { true })
289 .map(|mut f| {
290 f.severity = config.effective_severity(f.code.as_str(), f.severity);
292 f
293 })
294 .collect();
295
296 result.sort();
298 result.update_counts();
299
300 result
301}
302
303pub fn lint_chart_file(path: &Path, config: &HelmlintConfig) -> LintResult {
305 let chart_root = path.parent().unwrap_or(path);
307 lint_chart(chart_root, config)
308}
309
310fn collect_chart_files(path: &Path) -> HashSet<String> {
312 let mut files = HashSet::new();
313
314 for entry in walkdir::WalkDir::new(path)
315 .into_iter()
316 .filter_map(|e| e.ok())
317 {
318 if entry.path().is_file()
319 && let Ok(relative) = entry.path().strip_prefix(path)
320 {
321 files.insert(relative.display().to_string());
322 }
323 }
324
325 files
326}
327
328fn merge_pragmas(target: &mut PragmaState, source: PragmaState) {
330 if source.file_disabled {
331 target.file_disabled = true;
332 }
333
334 for code in source.file_ignores {
335 target.file_ignores.insert(code);
336 }
337
338 for (line, codes) in source.line_ignores {
339 target.line_ignores.entry(line).or_default().extend(codes);
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use std::fs;
347 use tempfile::TempDir;
348
349 fn create_test_chart(dir: &Path) {
350 fs::create_dir_all(dir.join("templates")).unwrap();
351
352 fs::write(
353 dir.join("Chart.yaml"),
354 r#"apiVersion: v2
355name: test-chart
356version: 1.0.0
357description: A test chart
358"#,
359 )
360 .unwrap();
361
362 fs::write(
363 dir.join("values.yaml"),
364 r#"replicaCount: 1
365image:
366 repository: nginx
367 tag: "1.25"
368"#,
369 )
370 .unwrap();
371
372 fs::write(
373 dir.join("templates/deployment.yaml"),
374 r#"apiVersion: apps/v1
375kind: Deployment
376metadata:
377 name: {{ .Release.Name }}
378spec:
379 replicas: {{ .Values.replicaCount }}
380"#,
381 )
382 .unwrap();
383 }
384
385 #[test]
386 fn test_lint_valid_chart() {
387 let temp_dir = TempDir::new().unwrap();
388 create_test_chart(temp_dir.path());
389
390 let config = HelmlintConfig::default();
391 let result = lint_chart(temp_dir.path(), &config);
392
393 assert!(result.parse_errors.is_empty());
394 }
395
396 #[test]
397 fn test_lint_nonexistent_path() {
398 let config = HelmlintConfig::default();
399 let result = lint_chart(Path::new("/nonexistent/path"), &config);
400
401 assert!(!result.parse_errors.is_empty());
402 }
403
404 #[test]
405 fn test_lint_with_ignored_rules() {
406 let temp_dir = TempDir::new().unwrap();
407 create_test_chart(temp_dir.path());
408
409 let config = HelmlintConfig::default()
410 .ignore("HL1007") .ignore("HL5001"); let result = lint_chart(temp_dir.path(), &config);
414
415 assert!(!result.failures.iter().any(|f| f.code.as_str() == "HL1007"));
416 assert!(!result.failures.iter().any(|f| f.code.as_str() == "HL5001"));
417 }
418
419 #[test]
420 fn test_result_counts() {
421 let mut result = LintResult::new("test");
422 result.failures.push(CheckFailure::new(
423 "HL1001",
424 Severity::Error,
425 "test",
426 "Chart.yaml",
427 1,
428 crate::analyzer::helmlint::types::RuleCategory::Structure,
429 ));
430 result.failures.push(CheckFailure::new(
431 "HL1002",
432 Severity::Warning,
433 "test",
434 "Chart.yaml",
435 2,
436 crate::analyzer::helmlint::types::RuleCategory::Structure,
437 ));
438 result.update_counts();
439
440 assert_eq!(result.error_count, 1);
441 assert_eq!(result.warning_count, 1);
442 assert!(result.has_errors());
443 assert!(result.has_warnings());
444 }
445}