1use std::collections::HashMap;
5
6use colored::*;
7use serde::Serialize;
8
9use crate::code_audit::{kind_label, CodeAuditReport};
10use crate::intel::CrateIntel;
11use crate::suggestions::{AutofixSafety, Confidence, Impact, MigrationRisk, SuggestionKind};
12use crate::suggestions::{EvidenceSource, Suggestion};
13
14pub fn render_report(
19 project_name: &str,
20 version: &str,
21 suggestions: &[Suggestion],
22 intel: &HashMap<String, CrateIntel>,
23) {
24 if suggestions.is_empty() {
25 println!(
26 "{}",
27 "✅ Your dependencies are already blessed! Nothing to modernize.".green()
28 );
29 return;
30 }
31
32 println!(
33 "{}",
34 format!("🚀 Modernization report for {} v{}", project_name, version).bold()
35 );
36 println!();
37
38 for suggestion in suggestions {
39 let icon = match suggestion.kind {
40 SuggestionKind::ModernAlternative => "•",
41 SuggestionKind::FeatureOptimization => "•",
42 SuggestionKind::StdReplacement => "•",
43 SuggestionKind::ComboWin => "•",
44 SuggestionKind::Unmaintained => "⚠️",
45 };
46
47 let impact_tag = match suggestion.impact {
48 Impact::High => "[HIGH]".red().bold(),
49 Impact::Medium => "[MED]".yellow().bold(),
50 Impact::Low => "[LOW]".dimmed(),
51 };
52 let confidence_tag = match suggestion.confidence {
53 Confidence::High => "[HIGH confidence]".green().bold(),
54 Confidence::Medium => "[MED confidence]".yellow(),
55 Confidence::Low => "[LOW confidence]".red(),
56 };
57 let risk_tag = match suggestion.migration_risk {
58 MigrationRisk::High => "[HIGH risk]".red().bold(),
59 MigrationRisk::Medium => "[MED risk]".yellow(),
60 MigrationRisk::Low => "[LOW risk]".green(),
61 };
62 let autofix_tag = match suggestion.autofix_safety {
63 AutofixSafety::CargoTomlOnly => "[autofix: Cargo.toml-only]".green(),
64 AutofixSafety::ManualOnly => "[autofix: manual]".dimmed(),
65 };
66 let verb = match suggestion.confidence {
67 Confidence::High => "→",
68 Confidence::Medium | Confidence::Low => "→ consider",
69 };
70
71 println!(
73 " {} {} {} {} {}",
74 icon,
75 impact_tag,
76 suggestion.current.yellow(),
77 verb,
78 suggestion.recommended.green(),
79 );
80 println!(
81 " {} {} {} {}",
82 confidence_tag,
83 risk_tag,
84 autofix_tag,
85 format!("evidence: {}", evidence_label(&suggestion.evidence_source)).dimmed()
86 );
87 println!(" {}", suggestion.reason.dimmed());
88
89 let crate_names: Vec<&str> = suggestion.current.split('+').collect();
92 for crate_name in crate_names {
93 if let Some(info) = intel.get(crate_name) {
94 let mut enrichment = format!(" latest: v{}", info.latest_version);
95 if let Some(recent) = info.recent_downloads {
96 enrichment
97 .push_str(&format!(", {} recent downloads", format_downloads(recent)));
98 }
99 println!(" {}", enrichment.dimmed());
100 }
101 }
102 }
103
104 let high_count = suggestions
105 .iter()
106 .filter(|s| matches!(s.impact, Impact::High))
107 .count();
108
109 println!();
110 println!(
111 "{}",
112 format!(
113 "{} high-impact upgrade{} available. Run `cargo bless --fix --dry-run` to preview Cargo.toml-only fixes.",
114 high_count,
115 if high_count == 1 { "" } else { "s" }
116 )
117 .bold()
118 );
119}
120
121fn evidence_label(source: &EvidenceSource) -> &'static str {
122 match source {
123 EvidenceSource::BlessedRs => "blessed.rs",
124 EvidenceSource::RustSec => "RustSec",
125 EvidenceSource::StdDocs => "std docs",
126 EvidenceSource::CrateDocs => "crate docs",
127 EvidenceSource::CratesIo => "crates.io",
128 EvidenceSource::Heuristic => "heuristic",
129 }
130}
131
132pub fn render_code_audit_report(report: &CodeAuditReport, verbose: bool) {
133 println!();
134 println!("{}", "🧨 Bullshit detector code audit".bold());
135 println!(
136 "{}",
137 format!(
138 "Scanned {} Rust file{}.",
139 report.files_scanned,
140 if report.files_scanned == 1 { "" } else { "s" }
141 )
142 .dimmed()
143 );
144
145 if report.is_clean() {
146 println!("{}", "✅ No bullshit detected in Rust source.".green());
147 return;
148 }
149
150 println!(
151 "{}",
152 format!(
153 "🚨 Bullshit detected: {} finding{} · heat {:.1}",
154 report.alerts.len(),
155 if report.alerts.len() == 1 { "" } else { "s" },
156 report.alerts.iter().map(|a| a.severity).sum::<f32>() * 10.0
157 )
158 .red()
159 .bold()
160 );
161
162 let mut counts = HashMap::<&'static str, usize>::new();
163 for alert in &report.alerts {
164 *counts.entry(kind_label(alert.kind)).or_default() += 1;
165 }
166 let mut counts: Vec<_> = counts.into_iter().collect();
167 counts.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(b.0)));
168 let summary = counts
169 .iter()
170 .map(|(kind, count)| format!("{kind}: {count}"))
171 .collect::<Vec<_>>()
172 .join(", ");
173 println!("{}", summary.dimmed());
174 println!();
175
176 let shown = if verbose {
177 report.alerts.len()
178 } else {
179 report.alerts.len().min(5)
180 };
181
182 for alert in report.alerts.iter().take(shown) {
183 println!(
184 " {} {} {}:{}:{}",
185 "•".red(),
186 kind_label(alert.kind).yellow().bold(),
187 alert.file.display().to_string().dimmed(),
188 alert.line,
189 alert.column
190 );
191 println!(" {}", alert.why_bs);
192 println!(" {}", format!("Fix: {}", alert.suggestion).green());
193 if !alert.context_snippet.is_empty() {
194 println!(" {}", alert.context_snippet.dimmed());
195 }
196 }
197
198 if !verbose && report.alerts.len() > shown {
199 println!();
200 println!(
201 "{}",
202 format!(
203 "Showing top {shown}. Run with --verbose for all {} findings, or --json for machine output.",
204 report.alerts.len()
205 )
206 .dimmed()
207 );
208 }
209}
210
211fn format_downloads(count: u64) -> String {
213 if count >= 1_000_000 {
214 format!("{:.1}M", count as f64 / 1_000_000.0)
215 } else if count >= 1_000 {
216 format!("{:.1}K", count as f64 / 1_000.0)
217 } else {
218 count.to_string()
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_format_downloads() {
228 assert_eq!(format_downloads(0), "0");
229 assert_eq!(format_downloads(500), "500");
230 assert_eq!(format_downloads(1_500), "1.5K");
231 assert_eq!(format_downloads(1_200_000), "1.2M");
232 assert_eq!(format_downloads(100_000_000), "100.0M");
233 }
234}
235
236#[derive(Serialize)]
237pub struct JsonReport<'a> {
238 pub dependency_suggestions: &'a [Suggestion],
239 pub code_audit: Option<&'a CodeAuditReport>,
240}
241
242pub fn render_json_report(suggestions: &[Suggestion], code_audit: Option<&CodeAuditReport>) {
244 let report = JsonReport {
245 dependency_suggestions: suggestions,
246 code_audit,
247 };
248 match serde_json::to_string_pretty(&report) {
249 Ok(json) => println!("{}", json),
250 Err(e) => eprintln!("cargo-bless: failed to serialize JSON output: {}", e),
251 }
252}
253
254pub fn render_json(suggestions: &[Suggestion]) {
257 match serde_json::to_string_pretty(suggestions) {
258 Ok(json) => println!("{}", json),
259 Err(e) => eprintln!("cargo-bless: failed to serialize JSON output: {}", e),
260 }
261}