syncable_cli/analyzer/helmlint/
pragma.rs1use std::collections::{HashMap, HashSet};
10
11use crate::analyzer::helmlint::types::RuleCode;
12
13#[derive(Debug, Clone, Default)]
15pub struct PragmaState {
16 pub file_ignores: HashSet<String>,
18 pub line_ignores: HashMap<u32, HashSet<String>>,
20 pub file_disabled: bool,
22}
23
24impl PragmaState {
25 pub fn new() -> Self {
27 Self::default()
28 }
29
30 pub fn is_ignored(&self, code: &RuleCode, line: u32) -> bool {
32 if self.file_disabled {
33 return true;
34 }
35
36 if self.file_ignores.contains(code.as_str()) {
37 return true;
38 }
39
40 if let Some(ignores) = self.line_ignores.get(&line)
42 && ignores.contains(code.as_str())
43 {
44 return true;
45 }
46
47 if line > 1
49 && let Some(ignores) = self.line_ignores.get(&(line - 1))
50 && ignores.contains(code.as_str())
51 {
52 return true;
53 }
54
55 false
56 }
57
58 pub fn add_file_ignore(&mut self, code: impl Into<String>) {
60 self.file_ignores.insert(code.into());
61 }
62
63 pub fn add_line_ignore(&mut self, line: u32, code: impl Into<String>) {
65 self.line_ignores
66 .entry(line)
67 .or_default()
68 .insert(code.into());
69 }
70
71 pub fn disable_file(&mut self) {
73 self.file_disabled = true;
74 }
75}
76
77pub fn extract_yaml_pragmas(content: &str) -> PragmaState {
79 let mut state = PragmaState::new();
80
81 for (line_num, line) in content.lines().enumerate() {
82 let line_number = (line_num + 1) as u32;
83 let trimmed = line.trim();
84
85 if let Some(comment) = trimmed.strip_prefix('#') {
87 process_comment(comment.trim(), line_number, &mut state);
88 }
89 }
90
91 state
92}
93
94pub fn extract_template_pragmas(content: &str) -> PragmaState {
96 let mut state = PragmaState::new();
97
98 for (line_num, line) in content.lines().enumerate() {
100 let line_number = (line_num + 1) as u32;
101 let trimmed = line.trim();
102
103 if let Some(comment) = trimmed.strip_prefix('#') {
105 if !line.contains("{{") || line.find('#') < line.find("{{") {
107 process_comment(comment.trim(), line_number, &mut state);
108 }
109 }
110 }
111
112 let mut line_num: u32 = 1;
114 let mut i = 0;
115 let chars: Vec<char> = content.chars().collect();
116
117 while i < chars.len() {
118 if chars[i] == '\n' {
119 line_num += 1;
120 i += 1;
121 continue;
122 }
123
124 if i + 4 < chars.len()
126 && chars[i] == '{'
127 && chars[i + 1] == '{'
128 && (chars[i + 2] == '/'
129 || (chars[i + 2] == '-' && i + 5 < chars.len() && chars[i + 3] == '/'))
130 {
131 let _comment_start = i;
132 let comment_line = line_num;
133
134 i += 2;
136 if chars[i] == '-' {
137 i += 1;
138 }
139 i += 2; let mut comment_content = String::new();
143 while i + 3 < chars.len() {
144 if chars[i] == '\n' {
145 line_num += 1;
146 }
147 if chars[i] == '*' && chars[i + 1] == '/' {
148 i += 2;
149 if i < chars.len() && chars[i] == '-' {
151 i += 1;
152 }
153 if i + 1 < chars.len() && chars[i] == '}' && chars[i + 1] == '}' {
154 i += 2;
155 }
156 break;
157 }
158 comment_content.push(chars[i]);
159 i += 1;
160 }
161
162 process_comment(comment_content.trim(), comment_line, &mut state);
164 continue;
165 }
166
167 i += 1;
168 }
169
170 state
171}
172
173fn process_comment(comment: &str, line: u32, state: &mut PragmaState) {
175 let lower = comment.to_lowercase();
176
177 if lower.starts_with("helmlint-ignore-file") || lower.starts_with("helmlint-disable-file") {
179 let rest = comment
180 .strip_prefix("helmlint-ignore-file")
181 .or_else(|| comment.strip_prefix("helmlint-disable-file"))
182 .unwrap_or("")
183 .trim();
184
185 if rest.is_empty() {
186 state.disable_file();
187 } else {
188 for code in parse_rule_list(rest) {
190 state.add_file_ignore(code);
191 }
192 }
193 return;
194 }
195
196 if lower.starts_with("helmlint-ignore") || lower.starts_with("helmlint-disable") {
198 let rest = comment
199 .strip_prefix("helmlint-ignore")
200 .or_else(|| comment.strip_prefix("helmlint-disable"))
201 .unwrap_or("")
202 .trim();
203
204 if rest.is_empty() {
205 state.add_line_ignore(line, "*");
207 } else {
208 for code in parse_rule_list(rest) {
209 state.add_line_ignore(line, code);
210 }
211 }
212 }
213}
214
215fn parse_rule_list(input: &str) -> Vec<String> {
217 input
218 .split([',', ' '])
219 .map(|s| s.trim())
220 .filter(|s| !s.is_empty() && s.starts_with("HL"))
221 .map(|s| s.to_string())
222 .collect()
223}
224
225pub fn starts_with_disable_file_comment(content: &str) -> bool {
227 for line in content.lines().take(10) {
228 let trimmed = line.trim();
229 if trimmed.is_empty() {
230 continue;
231 }
232 if let Some(comment) = trimmed.strip_prefix('#') {
233 let comment_lower = comment.trim().to_lowercase();
234 if comment_lower.starts_with("helmlint-ignore-file")
235 || comment_lower.starts_with("helmlint-disable-file")
236 {
237 let rest = comment
239 .trim()
240 .strip_prefix("helmlint-ignore-file")
241 .or_else(|| comment.trim().strip_prefix("helmlint-disable-file"))
242 .unwrap_or("")
243 .trim();
244 if rest.is_empty() {
245 return true;
246 }
247 }
248 }
249 if !trimmed.starts_with('#') {
251 break;
252 }
253 }
254 false
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn test_yaml_pragma_ignore() {
263 let content = r#"
264# helmlint-ignore HL1001
265name: test-chart
266version: 1.0.0
267"#;
268 let state = extract_yaml_pragmas(content);
269 assert!(state.is_ignored(&RuleCode::new("HL1001"), 3));
270 assert!(!state.is_ignored(&RuleCode::new("HL1002"), 3));
271 }
272
273 #[test]
274 fn test_yaml_pragma_file_ignore() {
275 let content = r#"
276# helmlint-ignore-file HL1001,HL1002
277name: test-chart
278"#;
279 let state = extract_yaml_pragmas(content);
280 assert!(state.is_ignored(&RuleCode::new("HL1001"), 3));
281 assert!(state.is_ignored(&RuleCode::new("HL1002"), 10));
282 assert!(!state.is_ignored(&RuleCode::new("HL1003"), 3));
283 }
284
285 #[test]
286 fn test_yaml_pragma_disable_file() {
287 let content = r#"
288# helmlint-ignore-file
289name: test-chart
290"#;
291 let state = extract_yaml_pragmas(content);
292 assert!(state.file_disabled);
293 assert!(state.is_ignored(&RuleCode::new("HL1001"), 3));
294 assert!(state.is_ignored(&RuleCode::new("HL9999"), 100));
295 }
296
297 #[test]
298 fn test_template_pragma() {
299 let content = r#"
300{{/* helmlint-ignore HL3001 */}}
301{{ .Values.name }}
302"#;
303 let state = extract_template_pragmas(content);
304 assert!(state.is_ignored(&RuleCode::new("HL3001"), 3));
305 }
306
307 #[test]
308 fn test_template_pragma_file_ignore() {
309 let content = r#"
310{{/* helmlint-ignore-file HL3001 */}}
311apiVersion: v1
312kind: ConfigMap
313"#;
314 let state = extract_template_pragmas(content);
315 assert!(state.is_ignored(&RuleCode::new("HL3001"), 3));
316 assert!(state.is_ignored(&RuleCode::new("HL3001"), 4));
317 }
318
319 #[test]
320 fn test_multiple_rules() {
321 let content = r#"
322# helmlint-ignore HL1001, HL1002, HL1003
323apiVersion: v2
324"#;
325 let state = extract_yaml_pragmas(content);
326 assert!(state.is_ignored(&RuleCode::new("HL1001"), 3));
327 assert!(state.is_ignored(&RuleCode::new("HL1002"), 3));
328 assert!(state.is_ignored(&RuleCode::new("HL1003"), 3));
329 }
330
331 #[test]
332 fn test_starts_with_disable_file() {
333 let content = r#"# helmlint-ignore-file
334apiVersion: v2
335"#;
336 assert!(starts_with_disable_file_comment(content));
337
338 let content_with_rules = r#"# helmlint-ignore-file HL1001
339apiVersion: v2
340"#;
341 assert!(!starts_with_disable_file_comment(content_with_rules));
342
343 let content_normal = r#"apiVersion: v2
344name: test
345"#;
346 assert!(!starts_with_disable_file_comment(content_normal));
347 }
348
349 #[test]
350 fn test_disable_alias() {
351 let content = r#"
352# helmlint-disable HL1001
353apiVersion: v2
354"#;
355 let state = extract_yaml_pragmas(content);
356 assert!(state.is_ignored(&RuleCode::new("HL1001"), 3));
357 }
358}