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 if !quick_fixes.is_empty() {
131 let _ = writeln!(
132 handle,
133 "\n{}{} Quick Fixes:{}",
134 ansi::DOCKER_BLUE,
135 icons::FIX,
136 ansi::RESET
137 );
138 for fix in quick_fixes.iter().take(5) {
139 if let Some(fix_str) = fix.as_str() {
140 let _ = writeln!(
141 handle,
142 "{} {} {}{}",
143 ansi::INFO_BLUE,
144 icons::ARROW,
145 fix_str,
146 ansi::RESET
147 );
148 }
149 }
150 }
151 }
152
153 Self::print_priority_section(&mut handle, result, "critical", "Critical Issues", ansi::CRITICAL);
155 Self::print_priority_section(&mut handle, result, "high", "High Priority", ansi::HIGH);
156
157 if let Some(medium_issues) = result["action_plan"]["medium"].as_array() {
159 if !medium_issues.is_empty() {
160 let _ = writeln!(
161 handle,
162 "\n{} {} {} medium priority issue{} (run with --verbose to see all){}",
163 ansi::MEDIUM,
164 icons::MEDIUM,
165 medium_issues.len(),
166 if medium_issues.len() == 1 { "" } else { "s" },
167 ansi::RESET
168 );
169 }
170 }
171
172 let _ = writeln!(
174 handle,
175 "{}{}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{}",
176 ansi::DOCKER_BLUE,
177 ansi::DIM,
178 ansi::RESET
179 );
180
181 let _ = handle.flush();
182 }
183
184 fn print_priority_section(
186 handle: &mut io::StdoutLock,
187 result: &serde_json::Value,
188 priority: &str,
189 title: &str,
190 color: &str,
191 ) {
192 if let Some(issues) = result["action_plan"][priority].as_array() {
193 if issues.is_empty() {
194 return;
195 }
196
197 let _ = writeln!(handle, "\n{} {}:{}", color, title, ansi::RESET);
198
199 for issue in issues.iter().take(10) {
200 let code = issue["code"].as_str().unwrap_or("???");
201 let line = issue["line"].as_u64().unwrap_or(0);
202 let message = issue["message"].as_str().unwrap_or("");
203 let category = issue["category"].as_str().unwrap_or("");
204
205 let category_badge = match category {
207 "security" => format!("{}[SEC]{}", ansi::CRITICAL, ansi::RESET),
208 "best-practice" => format!("{}[BP]{}", ansi::INFO_BLUE, ansi::RESET),
209 "deprecated" => format!("{}[DEP]{}", ansi::MEDIUM, ansi::RESET),
210 "performance" => format!("{}[PERF]{}", ansi::CYAN, ansi::RESET),
211 "maintainability" => format!("{}[MAINT]{}", ansi::GRAY, ansi::RESET),
212 _ => String::new(),
213 };
214
215 let _ = writeln!(
216 handle,
217 " {}{}:{}{} {}{}{} {} {}",
218 ansi::DIM,
219 line,
220 ansi::RESET,
221 ansi::DOCKER_BLUE,
222 code,
223 ansi::RESET,
224 category_badge,
225 ansi::GRAY,
226 message,
227 );
228
229 if let Some(fix) = issue["fix"].as_str() {
231 let _ = writeln!(
232 handle,
233 " {}→ {}{}",
234 ansi::INFO_BLUE,
235 fix,
236 ansi::RESET
237 );
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"].as_u64().unwrap_or(0);
269 let high = parsed["summary"]["by_priority"]["high"].as_u64().unwrap_or(0);
270
271 if critical > 0 {
272 format!(
273 "{}{} {} {} critical, {} high priority issues{}",
274 ansi::CRITICAL,
275 icons::ERROR,
276 icons::DOCKER,
277 critical,
278 high,
279 ansi::RESET
280 )
281 } else if high > 0 {
282 format!(
283 "{}{} {} {} high priority issues{}",
284 ansi::HIGH,
285 icons::WARNING,
286 icons::DOCKER,
287 high,
288 ansi::RESET
289 )
290 } else {
291 format!(
292 "{}{} {} {} issues (medium/low){}",
293 ansi::MEDIUM,
294 icons::WARNING,
295 icons::DOCKER,
296 total,
297 ansi::RESET
298 )
299 }
300 }
301 } else {
302 format!("{} Hadolint analysis complete", icons::DOCKER)
303 }
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_format_summary_success() {
313 let json = r#"{"success": true, "summary": {"total": 0, "by_priority": {"critical": 0, "high": 0, "medium": 0, "low": 0}}}"#;
314 let summary = HadolintDisplay::format_summary(json);
315 assert!(summary.contains("OK"));
316 }
317
318 #[test]
319 fn test_format_summary_critical() {
320 let json = r#"{"success": false, "summary": {"total": 3, "by_priority": {"critical": 1, "high": 2, "medium": 0, "low": 0}}}"#;
321 let summary = HadolintDisplay::format_summary(json);
322 assert!(summary.contains("critical"));
323 }
324}