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