1use crate::agent::ui::colors::{ansi, icons};
7use std::io::{self, Write};
8
9pub struct HadolintDisplay;
11
12impl HadolintDisplay {
13 pub fn print_result(json_result: &str) {
15 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json_result) {
16 Self::print_formatted(&parsed);
17 } else {
18 println!("{}", json_result);
20 }
21 }
22
23 fn print_formatted(result: &serde_json::Value) {
25 let stdout = io::stdout();
26 let mut handle = stdout.lock();
27
28 let file = result["file"].as_str().unwrap_or("Dockerfile");
30 let _ = writeln!(
31 handle,
32 "\n{}{}━━━ {} Hadolint: {} ━━━{}",
33 ansi::DOCKER_BLUE,
34 ansi::BOLD,
35 icons::DOCKER,
36 file,
37 ansi::RESET
38 );
39
40 if let Some(context) = result["decision_context"].as_str() {
42 let context_color = if context.contains("Critical") {
43 ansi::CRITICAL
44 } else if context.contains("High") {
45 ansi::HIGH
46 } else if context.contains("Medium") || context.contains("improvements") {
47 ansi::MEDIUM
48 } else {
49 ansi::LOW
50 };
51 let _ = writeln!(
52 handle,
53 "{} {} {}{}",
54 context_color,
55 icons::ARROW,
56 context,
57 ansi::RESET
58 );
59 }
60
61 if let Some(summary) = result.get("summary") {
63 let total = summary["total"].as_u64().unwrap_or(0);
64 if total == 0 {
65 let _ = writeln!(
66 handle,
67 "\n{} {} No issues found!{}",
68 ansi::SUCCESS,
69 icons::SUCCESS,
70 ansi::RESET
71 );
72 } else {
73 let _ = writeln!(handle);
74
75 if let Some(by_priority) = summary.get("by_priority") {
77 let critical = by_priority["critical"].as_u64().unwrap_or(0);
78 let high = by_priority["high"].as_u64().unwrap_or(0);
79 let medium = by_priority["medium"].as_u64().unwrap_or(0);
80 let low = by_priority["low"].as_u64().unwrap_or(0);
81
82 let _ = write!(handle, " ");
83 if critical > 0 {
84 let _ = write!(
85 handle,
86 "{}{} {} critical{} ",
87 ansi::CRITICAL,
88 icons::CRITICAL,
89 critical,
90 ansi::RESET
91 );
92 }
93 if high > 0 {
94 let _ = write!(
95 handle,
96 "{}{} {} high{} ",
97 ansi::HIGH,
98 icons::HIGH,
99 high,
100 ansi::RESET
101 );
102 }
103 if medium > 0 {
104 let _ = write!(
105 handle,
106 "{}{} {} medium{} ",
107 ansi::MEDIUM,
108 icons::MEDIUM,
109 medium,
110 ansi::RESET
111 );
112 }
113 if low > 0 {
114 let _ = write!(
115 handle,
116 "{}{} {} low{}",
117 ansi::LOW,
118 icons::LOW,
119 low,
120 ansi::RESET
121 );
122 }
123 let _ = writeln!(handle);
124 }
125 }
126 }
127
128 if let Some(quick_fixes) = result.get("quick_fixes").and_then(|f| f.as_array())
130 && !quick_fixes.is_empty()
131 {
132 let _ = writeln!(
133 handle,
134 "\n{}{} Quick Fixes:{}",
135 ansi::DOCKER_BLUE,
136 icons::FIX,
137 ansi::RESET
138 );
139 for fix in quick_fixes.iter().take(5) {
140 if let Some(fix_str) = fix.as_str() {
141 let _ = writeln!(
142 handle,
143 "{} {} {}{}",
144 ansi::INFO_BLUE,
145 icons::ARROW,
146 fix_str,
147 ansi::RESET
148 );
149 }
150 }
151 }
152
153 Self::print_priority_section(
155 &mut handle,
156 result,
157 "critical",
158 "Critical Issues",
159 ansi::CRITICAL,
160 );
161 Self::print_priority_section(&mut handle, result, "high", "High Priority", ansi::HIGH);
162
163 if let Some(medium_issues) = result["action_plan"]["medium"].as_array()
165 && !medium_issues.is_empty()
166 {
167 let _ = writeln!(
168 handle,
169 "\n{} {} {} medium priority issue{} (run with --verbose to see all){}",
170 ansi::MEDIUM,
171 icons::MEDIUM,
172 medium_issues.len(),
173 if medium_issues.len() == 1 { "" } else { "s" },
174 ansi::RESET
175 );
176 }
177
178 let _ = writeln!(
180 handle,
181 "{}{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}",
182 ansi::DOCKER_BLUE,
183 ansi::DIM,
184 ansi::RESET
185 );
186
187 let _ = handle.flush();
188 }
189
190 fn print_priority_section(
192 handle: &mut io::StdoutLock,
193 result: &serde_json::Value,
194 priority: &str,
195 title: &str,
196 color: &str,
197 ) {
198 if let Some(issues) = result["action_plan"][priority].as_array() {
199 if issues.is_empty() {
200 return;
201 }
202
203 let _ = writeln!(handle, "\n{} {}:{}", color, title, ansi::RESET);
204
205 for issue in issues.iter().take(10) {
206 let code = issue["code"].as_str().unwrap_or("???");
207 let line = issue["line"].as_u64().unwrap_or(0);
208 let message = issue["message"].as_str().unwrap_or("");
209 let category = issue["category"].as_str().unwrap_or("");
210
211 let category_badge = match category {
213 "security" => format!("{}[SEC]{}", ansi::CRITICAL, ansi::RESET),
214 "best-practice" => format!("{}[BP]{}", ansi::INFO_BLUE, ansi::RESET),
215 "deprecated" => format!("{}[DEP]{}", ansi::MEDIUM, ansi::RESET),
216 "performance" => format!("{}[PERF]{}", ansi::CYAN, ansi::RESET),
217 "maintainability" => format!("{}[MAINT]{}", ansi::GRAY, ansi::RESET),
218 _ => String::new(),
219 };
220
221 let _ = writeln!(
222 handle,
223 " {}{}:{}{} {}{}{} {} {}",
224 ansi::DIM,
225 line,
226 ansi::RESET,
227 ansi::DOCKER_BLUE,
228 code,
229 ansi::RESET,
230 category_badge,
231 ansi::GRAY,
232 message,
233 );
234
235 if let Some(fix) = issue["fix"].as_str() {
237 let _ = writeln!(handle, " {}→ {}{}", ansi::INFO_BLUE, fix, ansi::RESET);
238 }
239 }
240
241 if issues.len() > 10 {
242 let _ = writeln!(
243 handle,
244 " {}... and {} more{}",
245 ansi::DIM,
246 issues.len() - 10,
247 ansi::RESET
248 );
249 }
250 }
251 }
252
253 pub fn format_summary(json_result: &str) -> String {
255 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(json_result) {
256 let success = parsed["success"].as_bool().unwrap_or(false);
257 let total = parsed["summary"]["total"].as_u64().unwrap_or(0);
258
259 if success && total == 0 {
260 format!(
261 "{}{} {} Dockerfile OK - no issues{}",
262 ansi::SUCCESS,
263 icons::SUCCESS,
264 icons::DOCKER,
265 ansi::RESET
266 )
267 } else {
268 let critical = parsed["summary"]["by_priority"]["critical"]
269 .as_u64()
270 .unwrap_or(0);
271 let high = parsed["summary"]["by_priority"]["high"]
272 .as_u64()
273 .unwrap_or(0);
274
275 if critical > 0 {
276 format!(
277 "{}{} {} {} critical, {} high priority issues{}",
278 ansi::CRITICAL,
279 icons::ERROR,
280 icons::DOCKER,
281 critical,
282 high,
283 ansi::RESET
284 )
285 } else if high > 0 {
286 format!(
287 "{}{} {} {} high priority issues{}",
288 ansi::HIGH,
289 icons::WARNING,
290 icons::DOCKER,
291 high,
292 ansi::RESET
293 )
294 } else {
295 format!(
296 "{}{} {} {} issues (medium/low){}",
297 ansi::MEDIUM,
298 icons::WARNING,
299 icons::DOCKER,
300 total,
301 ansi::RESET
302 )
303 }
304 }
305 } else {
306 format!("{} Hadolint analysis complete", icons::DOCKER)
307 }
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_format_summary_success() {
317 let json = r#"{"success": true, "summary": {"total": 0, "by_priority": {"critical": 0, "high": 0, "medium": 0, "low": 0}}}"#;
318 let summary = HadolintDisplay::format_summary(json);
319 assert!(summary.contains("OK"));
320 }
321
322 #[test]
323 fn test_format_summary_critical() {
324 let json = r#"{"success": false, "summary": {"total": 3, "by_priority": {"critical": 1, "high": 2, "medium": 0, "low": 0}}}"#;
325 let summary = HadolintDisplay::format_summary(json);
326 assert!(summary.contains("critical"));
327 }
328}