syncable_cli/analyzer/helmlint/rules/
hl3xxx.rs

1//! HL3xxx - Template Syntax Rules
2//!
3//! Rules for validating Go template syntax in Helm templates.
4
5use crate::analyzer::helmlint::rules::{LintContext, Rule};
6use crate::analyzer::helmlint::types::{CheckFailure, RuleCategory, Severity};
7
8/// Get all HL3xxx rules.
9pub fn rules() -> Vec<Box<dyn Rule>> {
10    vec![
11        Box::new(HL3001),
12        Box::new(HL3002),
13        Box::new(HL3004),
14        Box::new(HL3005),
15        Box::new(HL3006),
16        Box::new(HL3007),
17        Box::new(HL3008),
18        Box::new(HL3009),
19        Box::new(HL3010),
20        Box::new(HL3011),
21    ]
22}
23
24/// HL3001: Unclosed template action
25pub struct HL3001;
26
27impl Rule for HL3001 {
28    fn code(&self) -> &'static str {
29        "HL3001"
30    }
31
32    fn severity(&self) -> Severity {
33        Severity::Error
34    }
35
36    fn name(&self) -> &'static str {
37        "unclosed-action"
38    }
39
40    fn description(&self) -> &'static str {
41        "Template has unclosed action (missing }})"
42    }
43
44    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
45        let mut failures = Vec::new();
46
47        for template in ctx.templates {
48            for error in &template.errors {
49                if error.message.contains("Unclosed template action") {
50                    failures.push(CheckFailure::new(
51                        "HL3001",
52                        Severity::Error,
53                        "Unclosed template action (missing }})".to_string(),
54                        &template.path,
55                        error.line,
56                        RuleCategory::Template,
57                    ));
58                }
59            }
60        }
61
62        failures
63    }
64}
65
66/// HL3002: Unclosed range/if block
67pub struct HL3002;
68
69impl Rule for HL3002 {
70    fn code(&self) -> &'static str {
71        "HL3002"
72    }
73
74    fn severity(&self) -> Severity {
75        Severity::Error
76    }
77
78    fn name(&self) -> &'static str {
79        "unclosed-block"
80    }
81
82    fn description(&self) -> &'static str {
83        "Template has unclosed control block (if/range/with)"
84    }
85
86    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
87        let mut failures = Vec::new();
88
89        for template in ctx.templates {
90            for (structure, line) in &template.unclosed_blocks {
91                failures.push(CheckFailure::new(
92                    "HL3002",
93                    Severity::Error,
94                    format!("Unclosed {:?} block (missing {{{{- end }}}}))", structure),
95                    &template.path,
96                    *line,
97                    RuleCategory::Template,
98                ));
99            }
100        }
101
102        failures
103    }
104}
105
106/// HL3004: Missing 'end' for control structure
107pub struct HL3004;
108
109impl Rule for HL3004 {
110    fn code(&self) -> &'static str {
111        "HL3004"
112    }
113
114    fn severity(&self) -> Severity {
115        Severity::Error
116    }
117
118    fn name(&self) -> &'static str {
119        "missing-end"
120    }
121
122    fn description(&self) -> &'static str {
123        "Control structure is missing closing 'end'"
124    }
125
126    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
127        // This is covered by HL3002, but we check for specific error messages
128        let mut failures = Vec::new();
129
130        for template in ctx.templates {
131            for error in &template.errors {
132                if error.message.contains("Unclosed") && error.message.contains("block") {
133                    failures.push(CheckFailure::new(
134                        "HL3004",
135                        Severity::Error,
136                        error.message.clone(),
137                        &template.path,
138                        error.line,
139                        RuleCategory::Template,
140                    ));
141                }
142            }
143        }
144
145        failures
146    }
147}
148
149/// HL3005: Using deprecated function
150pub struct HL3005;
151
152impl Rule for HL3005 {
153    fn code(&self) -> &'static str {
154        "HL3005"
155    }
156
157    fn severity(&self) -> Severity {
158        Severity::Warning
159    }
160
161    fn name(&self) -> &'static str {
162        "deprecated-function"
163    }
164
165    fn description(&self) -> &'static str {
166        "Template uses deprecated function"
167    }
168
169    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
170        let deprecated_functions = [
171            ("dateInZone", "Use 'mustDateModify' instead"),
172            ("genCA", "Use 'genSelfSignedCert' for better control"),
173        ];
174
175        let mut failures = Vec::new();
176
177        for template in ctx.templates {
178            for (func, suggestion) in &deprecated_functions {
179                if template.calls_function(func) {
180                    failures.push(CheckFailure::new(
181                        "HL3005",
182                        Severity::Warning,
183                        format!("Function '{}' is deprecated. {}", func, suggestion),
184                        &template.path,
185                        1, // Can't determine exact line without deeper analysis
186                        RuleCategory::Template,
187                    ));
188                }
189            }
190        }
191
192        failures
193    }
194}
195
196/// HL3006: Potential nil pointer (missing 'default')
197pub struct HL3006;
198
199impl Rule for HL3006 {
200    fn code(&self) -> &'static str {
201        "HL3006"
202    }
203
204    fn severity(&self) -> Severity {
205        Severity::Warning
206    }
207
208    fn name(&self) -> &'static str {
209        "potential-nil"
210    }
211
212    fn description(&self) -> &'static str {
213        "Value access may fail if value is nil. Consider using 'default'"
214    }
215
216    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
217        // This is a heuristic check - look for deep value access without default
218        let failures = Vec::new();
219
220        for template in ctx.templates {
221            // Look for deep nested access patterns that might fail
222            for var in &template.variables_used {
223                if var.starts_with(".Values.") {
224                    let parts: Vec<&str> = var.split('.').collect();
225                    // Deep nesting (more than 3 levels) without apparent default is risky
226                    if parts.len() > 4 && !template.calls_function("default") {
227                        // This is a very rough heuristic
228                        // A more sophisticated check would track usage context
229                    }
230                }
231            }
232        }
233
234        failures
235    }
236}
237
238/// HL3007: Template file has invalid extension
239pub struct HL3007;
240
241impl Rule for HL3007 {
242    fn code(&self) -> &'static str {
243        "HL3007"
244    }
245
246    fn severity(&self) -> Severity {
247        Severity::Warning
248    }
249
250    fn name(&self) -> &'static str {
251        "invalid-template-extension"
252    }
253
254    fn description(&self) -> &'static str {
255        "Template file should have .yaml, .yml, or .tpl extension"
256    }
257
258    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
259        let valid_extensions = [".yaml", ".yml", ".tpl", ".txt"];
260        let mut failures = Vec::new();
261
262        for file in ctx.files {
263            if file.contains("templates/") && !file.contains("templates/tests/") {
264                let has_valid_ext = valid_extensions.iter().any(|ext| file.ends_with(ext));
265                let is_helper = file.contains("_helpers");
266                let is_notes = file.contains("NOTES.txt");
267
268                if !has_valid_ext && !is_helper && !is_notes && !file.ends_with('/') {
269                    failures.push(CheckFailure::new(
270                        "HL3007",
271                        Severity::Warning,
272                        format!("Template file '{}' has unexpected extension", file),
273                        file,
274                        1,
275                        RuleCategory::Template,
276                    ));
277                }
278            }
279        }
280
281        failures
282    }
283}
284
285/// HL3008: NOTES.txt missing
286pub struct HL3008;
287
288impl Rule for HL3008 {
289    fn code(&self) -> &'static str {
290        "HL3008"
291    }
292
293    fn severity(&self) -> Severity {
294        Severity::Info
295    }
296
297    fn name(&self) -> &'static str {
298        "missing-notes"
299    }
300
301    fn description(&self) -> &'static str {
302        "Chart should have a NOTES.txt for post-install instructions"
303    }
304
305    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
306        // Skip for library charts
307        if let Some(chart) = ctx.chart_metadata
308            && chart.is_library()
309        {
310            return vec![];
311        }
312
313        let has_notes = ctx.files.iter().any(|f| f.ends_with("NOTES.txt"));
314        if !has_notes {
315            return vec![CheckFailure::new(
316                "HL3008",
317                Severity::Info,
318                "Chart is missing templates/NOTES.txt for post-install instructions",
319                "templates/NOTES.txt",
320                1,
321                RuleCategory::Template,
322            )];
323        }
324
325        vec![]
326    }
327}
328
329/// HL3009: Helper without description comment
330pub struct HL3009;
331
332impl Rule for HL3009 {
333    fn code(&self) -> &'static str {
334        "HL3009"
335    }
336
337    fn severity(&self) -> Severity {
338        Severity::Info
339    }
340
341    fn name(&self) -> &'static str {
342        "helper-missing-comment"
343    }
344
345    fn description(&self) -> &'static str {
346        "Helper template should have a description comment"
347    }
348
349    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
350        let mut failures = Vec::new();
351
352        if let Some(helpers) = ctx.helpers {
353            for helper in &helpers.helpers {
354                if helper.doc_comment.is_none() {
355                    failures.push(CheckFailure::new(
356                        "HL3009",
357                        Severity::Info,
358                        format!("Helper '{}' is missing a description comment", helper.name),
359                        &helpers.path,
360                        helper.line,
361                        RuleCategory::Template,
362                    ));
363                }
364            }
365        }
366
367        failures
368    }
369}
370
371/// HL3010: Unused helper defined
372pub struct HL3010;
373
374impl Rule for HL3010 {
375    fn code(&self) -> &'static str {
376        "HL3010"
377    }
378
379    fn severity(&self) -> Severity {
380        Severity::Info
381    }
382
383    fn name(&self) -> &'static str {
384        "unused-helper"
385    }
386
387    fn description(&self) -> &'static str {
388        "Helper template is defined but never used"
389    }
390
391    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
392        let mut failures = Vec::new();
393
394        let helpers = match ctx.helpers {
395            Some(h) => h,
396            None => return failures,
397        };
398
399        let referenced = ctx.template_references();
400
401        for helper in &helpers.helpers {
402            if !referenced.contains(helper.name.as_str()) {
403                // Check if it's used via include in other helpers
404                let used_in_helpers = helpers
405                    .helpers
406                    .iter()
407                    .any(|h| h.name != helper.name && h.content.contains(&helper.name));
408
409                if !used_in_helpers {
410                    failures.push(CheckFailure::new(
411                        "HL3010",
412                        Severity::Info,
413                        format!("Helper '{}' is defined but never used", helper.name),
414                        &helpers.path,
415                        helper.line,
416                        RuleCategory::Template,
417                    ));
418                }
419            }
420        }
421
422        failures
423    }
424}
425
426/// HL3011: Include of non-existent template
427pub struct HL3011;
428
429impl Rule for HL3011 {
430    fn code(&self) -> &'static str {
431        "HL3011"
432    }
433
434    fn severity(&self) -> Severity {
435        Severity::Error
436    }
437
438    fn name(&self) -> &'static str {
439        "include-not-found"
440    }
441
442    fn description(&self) -> &'static str {
443        "Template includes a helper that is not defined"
444    }
445
446    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
447        let mut failures = Vec::new();
448
449        let defined_helpers: std::collections::HashSet<&str> =
450            ctx.helper_names().into_iter().collect();
451        let referenced = ctx.template_references();
452
453        for ref_name in referenced {
454            if !defined_helpers.contains(ref_name) {
455                // Find which template references this
456                for template in ctx.templates {
457                    if template.referenced_templates.contains(ref_name) {
458                        failures.push(CheckFailure::new(
459                            "HL3011",
460                            Severity::Error,
461                            format!("Template includes '{}' which is not defined", ref_name),
462                            &template.path,
463                            1,
464                            RuleCategory::Template,
465                        ));
466                        break;
467                    }
468                }
469            }
470        }
471
472        failures
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    // Tests would require setting up LintContext which needs parsed templates
481    // For now, we just verify the rules compile and have correct metadata
482
483    #[test]
484    fn test_rules_exist() {
485        let all_rules = rules();
486        assert!(!all_rules.is_empty());
487    }
488}