syncable_cli/analyzer/helmlint/
types.rs

1//! Core types for the helmlint linter.
2//!
3//! These types provide the foundation for rule violations and severity levels:
4//! - `Severity` - Rule violation severity levels
5//! - `RuleCode` - Rule identifiers (e.g., "HL1001")
6//! - `CheckFailure` - A single rule violation
7//! - `RuleCategory` - Categories of rules
8
9use std::cmp::Ordering;
10use std::fmt;
11use std::path::PathBuf;
12
13/// Severity levels for rule violations.
14///
15/// Ordered from most severe to least severe:
16/// `Error > Warning > Info > Style > Ignore`
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
18pub enum Severity {
19    /// Critical issues that should always be fixed
20    Error,
21    /// Important issues that should usually be fixed
22    #[default]
23    Warning,
24    /// Informational suggestions for improvement
25    Info,
26    /// Style recommendations
27    Style,
28    /// Ignored (rule disabled)
29    Ignore,
30}
31
32impl Severity {
33    /// Parse a severity from a string (case-insensitive).
34    pub fn parse(s: &str) -> Option<Self> {
35        match s.to_lowercase().as_str() {
36            "error" => Some(Self::Error),
37            "warning" => Some(Self::Warning),
38            "info" => Some(Self::Info),
39            "style" => Some(Self::Style),
40            "ignore" | "none" | "off" => Some(Self::Ignore),
41            _ => None,
42        }
43    }
44
45    /// Get the string representation.
46    pub fn as_str(&self) -> &'static str {
47        match self {
48            Self::Error => "error",
49            Self::Warning => "warning",
50            Self::Info => "info",
51            Self::Style => "style",
52            Self::Ignore => "ignore",
53        }
54    }
55}
56
57impl fmt::Display for Severity {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        write!(f, "{}", self.as_str())
60    }
61}
62
63impl Ord for Severity {
64    fn cmp(&self, other: &Self) -> Ordering {
65        // Higher severity = lower numeric value for Ord
66        let self_val = match self {
67            Self::Error => 0,
68            Self::Warning => 1,
69            Self::Info => 2,
70            Self::Style => 3,
71            Self::Ignore => 4,
72        };
73        let other_val = match other {
74            Self::Error => 0,
75            Self::Warning => 1,
76            Self::Info => 2,
77            Self::Style => 3,
78            Self::Ignore => 4,
79        };
80        // Reverse so Error > Warning > Info > Style > Ignore
81        other_val.cmp(&self_val)
82    }
83}
84
85impl PartialOrd for Severity {
86    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
87        Some(self.cmp(other))
88    }
89}
90
91/// Rule categories for organizing lint rules.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
93pub enum RuleCategory {
94    /// Chart structure rules (HL1xxx)
95    Structure,
96    /// Values validation rules (HL2xxx)
97    Values,
98    /// Template syntax rules (HL3xxx)
99    Template,
100    /// Security rules (HL4xxx)
101    Security,
102    /// Best practice rules (HL5xxx)
103    BestPractice,
104}
105
106impl RuleCategory {
107    /// Get the code prefix for this category.
108    pub fn prefix(&self) -> &'static str {
109        match self {
110            Self::Structure => "HL1",
111            Self::Values => "HL2",
112            Self::Template => "HL3",
113            Self::Security => "HL4",
114            Self::BestPractice => "HL5",
115        }
116    }
117
118    /// Get the display name for this category.
119    pub fn display_name(&self) -> &'static str {
120        match self {
121            Self::Structure => "Chart Structure",
122            Self::Values => "Values Validation",
123            Self::Template => "Template Syntax",
124            Self::Security => "Security",
125            Self::BestPractice => "Best Practice",
126        }
127    }
128
129    /// Determine category from rule code.
130    pub fn from_code(code: &str) -> Option<Self> {
131        if code.starts_with("HL1") {
132            Some(Self::Structure)
133        } else if code.starts_with("HL2") {
134            Some(Self::Values)
135        } else if code.starts_with("HL3") {
136            Some(Self::Template)
137        } else if code.starts_with("HL4") {
138            Some(Self::Security)
139        } else if code.starts_with("HL5") {
140            Some(Self::BestPractice)
141        } else {
142            None
143        }
144    }
145}
146
147impl fmt::Display for RuleCategory {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "{}", self.display_name())
150    }
151}
152
153/// A rule code identifier (e.g., "HL1001", "HL4002").
154#[derive(Debug, Clone, PartialEq, Eq, Hash)]
155pub struct RuleCode(pub String);
156
157impl RuleCode {
158    /// Create a new rule code.
159    pub fn new(code: impl Into<String>) -> Self {
160        Self(code.into())
161    }
162
163    /// Get the code as a string slice.
164    pub fn as_str(&self) -> &str {
165        &self.0
166    }
167
168    /// Get the category for this rule.
169    pub fn category(&self) -> Option<RuleCategory> {
170        RuleCategory::from_code(&self.0)
171    }
172
173    /// Check if this is a structure rule (HL1xxx).
174    pub fn is_structure_rule(&self) -> bool {
175        self.0.starts_with("HL1")
176    }
177
178    /// Check if this is a values rule (HL2xxx).
179    pub fn is_values_rule(&self) -> bool {
180        self.0.starts_with("HL2")
181    }
182
183    /// Check if this is a template rule (HL3xxx).
184    pub fn is_template_rule(&self) -> bool {
185        self.0.starts_with("HL3")
186    }
187
188    /// Check if this is a security rule (HL4xxx).
189    pub fn is_security_rule(&self) -> bool {
190        self.0.starts_with("HL4")
191    }
192
193    /// Check if this is a best practice rule (HL5xxx).
194    pub fn is_best_practice_rule(&self) -> bool {
195        self.0.starts_with("HL5")
196    }
197}
198
199impl fmt::Display for RuleCode {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        write!(f, "{}", self.0)
202    }
203}
204
205impl From<&str> for RuleCode {
206    fn from(s: &str) -> Self {
207        Self::new(s)
208    }
209}
210
211impl From<String> for RuleCode {
212    fn from(s: String) -> Self {
213        Self(s)
214    }
215}
216
217/// Metadata about a lint rule.
218#[derive(Debug, Clone)]
219pub struct RuleMeta {
220    /// The rule code (e.g., "HL1001").
221    pub code: RuleCode,
222    /// Short name for the rule.
223    pub name: &'static str,
224    /// Human-readable description.
225    pub description: &'static str,
226    /// Default severity level.
227    pub severity: Severity,
228    /// Rule category.
229    pub category: RuleCategory,
230    /// Whether this rule can be auto-fixed.
231    pub fixable: bool,
232}
233
234impl RuleMeta {
235    /// Create new rule metadata.
236    pub const fn new(
237        _code: &'static str,
238        name: &'static str,
239        description: &'static str,
240        severity: Severity,
241        category: RuleCategory,
242        fixable: bool,
243    ) -> Self {
244        Self {
245            code: RuleCode(String::new()), // Will be set properly at runtime
246            name,
247            description,
248            severity,
249            category,
250            fixable,
251        }
252    }
253}
254
255/// A check failure (rule violation) found during linting.
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct CheckFailure {
258    /// The rule code that was violated.
259    pub code: RuleCode,
260    /// The severity of the violation.
261    pub severity: Severity,
262    /// A human-readable message describing the violation.
263    pub message: String,
264    /// The file where the violation occurred (relative to chart root).
265    pub file: PathBuf,
266    /// The line number where the violation occurred (1-indexed).
267    pub line: u32,
268    /// Optional column number (1-indexed).
269    pub column: Option<u32>,
270    /// Whether this violation can be auto-fixed.
271    pub fixable: bool,
272    /// The rule category.
273    pub category: RuleCategory,
274}
275
276impl CheckFailure {
277    /// Create a new check failure.
278    pub fn new(
279        code: impl Into<RuleCode>,
280        severity: Severity,
281        message: impl Into<String>,
282        file: impl Into<PathBuf>,
283        line: u32,
284        category: RuleCategory,
285    ) -> Self {
286        Self {
287            code: code.into(),
288            severity,
289            message: message.into(),
290            file: file.into(),
291            line,
292            column: None,
293            fixable: false,
294            category,
295        }
296    }
297
298    /// Create a check failure with column information.
299    pub fn with_column(
300        code: impl Into<RuleCode>,
301        severity: Severity,
302        message: impl Into<String>,
303        file: impl Into<PathBuf>,
304        line: u32,
305        column: u32,
306        category: RuleCategory,
307    ) -> Self {
308        Self {
309            code: code.into(),
310            severity,
311            message: message.into(),
312            file: file.into(),
313            line,
314            column: Some(column),
315            fixable: false,
316            category,
317        }
318    }
319
320    /// Set whether this failure is fixable.
321    pub fn set_fixable(mut self, fixable: bool) -> Self {
322        self.fixable = fixable;
323        self
324    }
325}
326
327impl Ord for CheckFailure {
328    fn cmp(&self, other: &Self) -> Ordering {
329        // Sort by file first, then line number
330        match self.file.cmp(&other.file) {
331            Ordering::Equal => self.line.cmp(&other.line),
332            other => other,
333        }
334    }
335}
336
337impl PartialOrd for CheckFailure {
338    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
339        Some(self.cmp(other))
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[test]
348    fn test_severity_ordering() {
349        assert!(Severity::Error > Severity::Warning);
350        assert!(Severity::Warning > Severity::Info);
351        assert!(Severity::Info > Severity::Style);
352        assert!(Severity::Style > Severity::Ignore);
353    }
354
355    #[test]
356    fn test_severity_from_str() {
357        assert_eq!(Severity::parse("error"), Some(Severity::Error));
358        assert_eq!(Severity::parse("WARNING"), Some(Severity::Warning));
359        assert_eq!(Severity::parse("Info"), Some(Severity::Info));
360        assert_eq!(Severity::parse("style"), Some(Severity::Style));
361        assert_eq!(Severity::parse("ignore"), Some(Severity::Ignore));
362        assert_eq!(Severity::parse("off"), Some(Severity::Ignore));
363        assert_eq!(Severity::parse("invalid"), None);
364    }
365
366    #[test]
367    fn test_rule_code_category() {
368        assert!(RuleCode::new("HL1001").is_structure_rule());
369        assert!(RuleCode::new("HL2001").is_values_rule());
370        assert!(RuleCode::new("HL3001").is_template_rule());
371        assert!(RuleCode::new("HL4001").is_security_rule());
372        assert!(RuleCode::new("HL5001").is_best_practice_rule());
373    }
374
375    #[test]
376    fn test_rule_category_from_code() {
377        assert_eq!(
378            RuleCategory::from_code("HL1001"),
379            Some(RuleCategory::Structure)
380        );
381        assert_eq!(
382            RuleCategory::from_code("HL2001"),
383            Some(RuleCategory::Values)
384        );
385        assert_eq!(
386            RuleCategory::from_code("HL3001"),
387            Some(RuleCategory::Template)
388        );
389        assert_eq!(
390            RuleCategory::from_code("HL4001"),
391            Some(RuleCategory::Security)
392        );
393        assert_eq!(
394            RuleCategory::from_code("HL5001"),
395            Some(RuleCategory::BestPractice)
396        );
397        assert_eq!(RuleCategory::from_code("XX1001"), None);
398    }
399
400    #[test]
401    fn test_check_failure_ordering() {
402        let f1 = CheckFailure::new(
403            "HL1001",
404            Severity::Warning,
405            "msg1",
406            "Chart.yaml",
407            5,
408            RuleCategory::Structure,
409        );
410        let f2 = CheckFailure::new(
411            "HL1002",
412            Severity::Info,
413            "msg2",
414            "Chart.yaml",
415            10,
416            RuleCategory::Structure,
417        );
418        let f3 = CheckFailure::new(
419            "HL1003",
420            Severity::Error,
421            "msg3",
422            "Chart.yaml",
423            3,
424            RuleCategory::Structure,
425        );
426        let f4 = CheckFailure::new(
427            "HL3001",
428            Severity::Error,
429            "msg4",
430            "templates/deployment.yaml",
431            1,
432            RuleCategory::Template,
433        );
434
435        let mut failures = vec![f1.clone(), f2.clone(), f3.clone(), f4.clone()];
436        failures.sort();
437
438        assert_eq!(failures[0].line, 3);
439        assert_eq!(failures[1].line, 5);
440        assert_eq!(failures[2].line, 10);
441        assert_eq!(
442            failures[3].file.to_str().unwrap(),
443            "templates/deployment.yaml"
444        );
445    }
446}