syncable_cli/analyzer/helmlint/formatter/
stylish.rs1use crate::analyzer::helmlint::lint::LintResult;
6use crate::analyzer::helmlint::types::Severity;
7use std::collections::BTreeMap;
8
9mod colors {
11 pub const RESET: &str = "\x1b[0m";
12 pub const RED: &str = "\x1b[31m";
13 pub const YELLOW: &str = "\x1b[33m";
14 pub const BLUE: &str = "\x1b[34m";
15 pub const CYAN: &str = "\x1b[36m";
16 pub const DIM: &str = "\x1b[2m";
17 pub const BOLD: &str = "\x1b[1m";
18 pub const UNDERLINE: &str = "\x1b[4m";
19}
20
21pub fn format(result: &LintResult) -> String {
23 let mut output = String::new();
24
25 let mut by_file: BTreeMap<String, Vec<_>> = BTreeMap::new();
27 for failure in &result.failures {
28 let file = failure.file.display().to_string();
29 by_file.entry(file).or_default().push(failure);
30 }
31
32 if !result.parse_errors.is_empty() {
34 output.push_str(&format!(
35 "\n{}{}Parse Errors:{}\n",
36 colors::BOLD,
37 colors::RED,
38 colors::RESET
39 ));
40 for error in &result.parse_errors {
41 output.push_str(&format!(
42 " {}{}{} {}\n",
43 colors::RED,
44 "error",
45 colors::RESET,
46 error
47 ));
48 }
49 output.push('\n');
50 }
51
52 if by_file.is_empty() && result.parse_errors.is_empty() {
53 output.push_str(&format!(
54 "{}{}{} No issues found\n",
55 colors::BOLD,
56 result.chart_path,
57 colors::RESET
58 ));
59 return output;
60 }
61
62 for (file, failures) in by_file {
64 output.push_str(&format!(
65 "\n{}{}{}{}",
66 colors::UNDERLINE,
67 colors::BOLD,
68 file,
69 colors::RESET
70 ));
71 output.push('\n');
72
73 for failure in failures {
74 let severity_color = match failure.severity {
75 Severity::Error => colors::RED,
76 Severity::Warning => colors::YELLOW,
77 Severity::Info => colors::BLUE,
78 Severity::Style => colors::CYAN,
79 Severity::Ignore => colors::DIM,
80 };
81
82 let severity_text = match failure.severity {
83 Severity::Error => "error",
84 Severity::Warning => "warning",
85 Severity::Info => "info",
86 Severity::Style => "style",
87 Severity::Ignore => "ignore",
88 };
89
90 let location = match failure.column {
91 Some(col) => format!("{}:{}", failure.line, col),
92 None => format!("{}", failure.line),
93 };
94
95 output.push_str(&format!(
96 " {}{}:{:>8}{} {} {} {}",
97 colors::DIM,
98 location,
99 severity_color,
100 severity_text,
101 colors::RESET,
102 failure.message,
103 failure.code,
104 ));
105 output.push_str(colors::RESET);
106 output.push('\n');
107 }
108 }
109
110 output.push('\n');
112 let total = result.failures.len();
113 let errors = result.error_count;
114 let warnings = result.warning_count;
115 let infos = total - errors - warnings;
116
117 if total > 0 {
118 let status_color = if errors > 0 {
119 colors::RED
120 } else {
121 colors::YELLOW
122 };
123 output.push_str(&format!(
124 "{}{}✖ {} {} ({} {}, {} {}, {} info)\n{}",
125 colors::BOLD,
126 status_color,
127 total,
128 if total == 1 { "problem" } else { "problems" },
129 errors,
130 if errors == 1 { "error" } else { "errors" },
131 warnings,
132 if warnings == 1 { "warning" } else { "warnings" },
133 infos,
134 colors::RESET,
135 ));
136 }
137
138 output
139}
140
141pub fn format_no_color(result: &LintResult) -> String {
143 let mut output = String::new();
144
145 let mut by_file: BTreeMap<String, Vec<_>> = BTreeMap::new();
147 for failure in &result.failures {
148 let file = failure.file.display().to_string();
149 by_file.entry(file).or_default().push(failure);
150 }
151
152 if !result.parse_errors.is_empty() {
153 output.push_str("\nParse Errors:\n");
154 for error in &result.parse_errors {
155 output.push_str(&format!(" error {}\n", error));
156 }
157 output.push('\n');
158 }
159
160 if by_file.is_empty() && result.parse_errors.is_empty() {
161 output.push_str(&format!("{} No issues found\n", result.chart_path));
162 return output;
163 }
164
165 for (file, failures) in by_file {
166 output.push_str(&format!("\n{}\n", file));
167
168 for failure in failures {
169 let severity_text = match failure.severity {
170 Severity::Error => "error",
171 Severity::Warning => "warning",
172 Severity::Info => "info",
173 Severity::Style => "style",
174 Severity::Ignore => "ignore",
175 };
176
177 let location = match failure.column {
178 Some(col) => format!("{}:{}", failure.line, col),
179 None => format!("{}", failure.line),
180 };
181
182 output.push_str(&format!(
183 " {}: {} {} {}\n",
184 location, severity_text, failure.message, failure.code
185 ));
186 }
187 }
188
189 output.push('\n');
191 let total = result.failures.len();
192 let errors = result.error_count;
193 let warnings = result.warning_count;
194 let infos = total - errors - warnings;
195
196 if total > 0 {
197 output.push_str(&format!(
198 "✖ {} {} ({} {}, {} {}, {} info)\n",
199 total,
200 if total == 1 { "problem" } else { "problems" },
201 errors,
202 if errors == 1 { "error" } else { "errors" },
203 warnings,
204 if warnings == 1 { "warning" } else { "warnings" },
205 infos
206 ));
207 }
208
209 output
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::analyzer::helmlint::types::{CheckFailure, RuleCategory, Severity};
216
217 #[test]
218 fn test_stylish_format_empty() {
219 let result = LintResult::new("test-chart");
220 let output = format(&result);
221 assert!(output.contains("No issues found"));
222 }
223
224 #[test]
225 fn test_stylish_format_with_failures() {
226 let mut result = LintResult::new("test-chart");
227 result.failures.push(CheckFailure::new(
228 "HL1001",
229 Severity::Error,
230 "Missing Chart.yaml",
231 "Chart.yaml",
232 1,
233 RuleCategory::Structure,
234 ));
235 result.error_count = 1;
236
237 let output = format_no_color(&result);
238 assert!(output.contains("Chart.yaml"));
239 assert!(output.contains("error"));
240 assert!(output.contains("HL1001"));
241 }
242}