use crate::flamegraph::DepTreeData;
use crate::metrics::{Confidence, RemovalStrategy, UpstreamTarget};
use crate::scanner::display_path;
use serde::{Deserialize, Serialize};
use std::io::Write;
#[derive(Debug, Serialize, Deserialize)]
pub struct AnalysisReport {
pub tool_version: String,
pub timestamp: String,
pub workspace_root: String,
pub threshold: f64,
pub total_dependencies: usize,
#[serde(default)]
pub platform_dependencies: Option<usize>,
#[serde(default)]
pub phantom_dependencies: usize,
pub heavy_nodes_found: usize,
pub targets: Vec<UpstreamTarget>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dep_tree: Option<DepTreeData>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unused_edges: Vec<(String, String)>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unused_direct_deps: Vec<UnusedDirectDep>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub direct_dep_summary: Vec<DirectDepSummary>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DirectDepSummary {
pub workspace_member: String,
pub dep_name: String,
pub dep_version: String,
pub unique_transitive_deps: usize,
pub total_transitive_deps: usize,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnusedDirectDep {
pub from_crate: String,
pub dep_name: String,
pub dep_version: String,
pub real_deps_saved: usize,
pub is_test_example: bool,
}
pub fn render_json(report: &AnalysisReport, writer: &mut dyn Write) -> anyhow::Result<()> {
serde_json::to_writer_pretty(&mut *writer, report)?;
writeln!(writer)?;
Ok(())
}
pub fn render_text(
report: &AnalysisReport,
writer: &mut dyn Write,
verbose: bool,
) -> anyhow::Result<()> {
writeln!(writer)?;
writeln!(writer, "depflame — Dependency Analysis Report")?;
if let Some(platform_deps) = report.platform_dependencies {
writeln!(
writer,
"{} dependencies ({} compiled on this platform)",
report.total_dependencies, platform_deps
)?;
} else {
writeln!(writer, "{} dependencies", report.total_dependencies)?;
}
writeln!(writer)?;
if !report.direct_dep_summary.is_empty() {
writeln!(
writer,
"Direct dependencies by unique transitive dep count:"
)?;
writeln!(writer)?;
let name_w = report
.direct_dep_summary
.iter()
.map(|e| e.dep_name.len())
.max()
.unwrap_or(10)
.max(10);
let ver_w = report
.direct_dep_summary
.iter()
.map(|e| e.dep_version.len())
.max()
.unwrap_or(7)
.max(7);
let idx_w = format!("{}", report.direct_dep_summary.len()).len().max(1);
writeln!(
writer,
" {:>idx_w$} {:<name_w$} {:<ver_w$} {:>6} {:>5}",
"#",
"Dependency",
"Version",
"Unique",
"Total",
idx_w = idx_w,
name_w = name_w,
ver_w = ver_w,
)?;
writeln!(
writer,
" {:─>idx_w$} {:─<name_w$} {:─<ver_w$} {:─>6} {:─>5}",
"",
"",
"",
"",
"",
idx_w = idx_w,
name_w = name_w,
ver_w = ver_w,
)?;
for (i, entry) in report.direct_dep_summary.iter().enumerate() {
writeln!(
writer,
" {:>idx_w$} {:<name_w$} {:<ver_w$} {:>6} {:>5}",
i + 1,
entry.dep_name,
entry.dep_version,
entry.unique_transitive_deps,
entry.total_transitive_deps,
idx_w = idx_w,
name_w = name_w,
ver_w = ver_w,
)?;
}
writeln!(writer)?;
}
if report.targets.is_empty() {
writeln!(
writer,
"No actionable targets found. Your dependency tree looks clean!"
)?;
return Ok(());
}
let is_actionable = |t: &UpstreamTarget| -> bool {
matches!(t.confidence, Confidence::High | Confidence::Medium) && t.w_unique > 0
};
let ws_remove: Vec<&UpstreamTarget> = report
.targets
.iter()
.filter(|t| {
is_actionable(t)
&& matches!(t.suggestion, RemovalStrategy::Remove)
&& t.intermediate_is_workspace_member
})
.collect();
let ws_feature_gate: Vec<&UpstreamTarget> = report
.targets
.iter()
.filter(|t| {
is_actionable(t)
&& matches!(t.suggestion, RemovalStrategy::FeatureGate)
&& t.intermediate_is_workspace_member
})
.collect();
let ws_already_gated: Vec<&UpstreamTarget> = report
.targets
.iter()
.filter(|t| {
is_actionable(t)
&& matches!(t.suggestion, RemovalStrategy::AlreadyGated { .. })
&& t.intermediate_is_workspace_member
})
.collect();
let ws_std_replacements: Vec<&UpstreamTarget> = report
.targets
.iter()
.filter(|t| {
is_actionable(t)
&& matches!(t.suggestion, RemovalStrategy::ReplaceWithStd { .. })
&& t.intermediate_is_workspace_member
})
.collect();
let has_ws_findings = !ws_remove.is_empty()
|| !ws_feature_gate.is_empty()
|| !ws_already_gated.is_empty()
|| !ws_std_replacements.is_empty();
if has_ws_findings {
writeln!(writer, "Your crate — recommended changes:")?;
writeln!(writer)?;
for target in &ws_remove {
let test_badge = if is_test_or_example_crate(&target.intermediate.name) {
" [test/example]"
} else {
""
};
writeln!(
writer,
" (-{} deps) Remove `{}` from `{}` (0 code references found){}",
target.w_unique, target.heavy_dependency.name, target.intermediate.name, test_badge,
)?;
}
for target in &ws_feature_gate {
let test_badge = if is_test_or_example_crate(&target.intermediate.name) {
" [test/example]"
} else {
""
};
writeln!(
writer,
" (-{} deps) Make `{}` optional in `{}` ({} refs){}",
target.w_unique,
target.heavy_dependency.name,
target.intermediate.name,
target.c_ref,
test_badge,
)?;
}
for target in &ws_already_gated {
writeln!(
writer,
" (-{} deps) `{}` is optional in `{}` — check if you need it",
target.w_unique, target.heavy_dependency.name, target.intermediate.name,
)?;
}
for target in &ws_std_replacements {
if let RemovalStrategy::ReplaceWithStd { suggestion } = &target.suggestion {
writeln!(
writer,
" (-{} deps) Replace `{}` with {} in `{}`",
target.w_unique,
target.heavy_dependency.name,
suggestion,
target.intermediate.name,
)?;
}
}
writeln!(writer)?;
}
let upstream_feature_gate: Vec<&UpstreamTarget> = report
.targets
.iter()
.filter(|t| {
is_actionable(t)
&& matches!(t.suggestion, RemovalStrategy::FeatureGate)
&& !t.intermediate_is_workspace_member
})
.collect();
let upstream_already_gated: Vec<&UpstreamTarget> = report
.targets
.iter()
.filter(|t| {
is_actionable(t)
&& matches!(t.suggestion, RemovalStrategy::AlreadyGated { .. })
&& !t.intermediate_is_workspace_member
})
.collect();
let upstream_remove: Vec<&UpstreamTarget> = report
.targets
.iter()
.filter(|t| {
is_actionable(t)
&& matches!(t.suggestion, RemovalStrategy::Remove)
&& !t.intermediate_is_workspace_member
})
.collect();
let std_replacements: Vec<&UpstreamTarget> = report
.targets
.iter()
.filter(|t| {
is_actionable(t)
&& matches!(t.suggestion, RemovalStrategy::ReplaceWithStd { .. })
&& !t.intermediate_is_workspace_member
})
.collect();
let inline_candidates: Vec<&UpstreamTarget> = report
.targets
.iter()
.filter(|t| {
is_actionable(t) && matches!(t.suggestion, RemovalStrategy::InlineUpstream { .. })
})
.collect();
if !inline_candidates.is_empty() {
writeln!(writer, "Consider inlining (small dep or light usage):")?;
writeln!(writer)?;
for target in &inline_candidates {
let chain = format_short_chain(&target.dep_chain, &target.intermediate.name);
let usage_str = if target.heavy_dep_loc > 0 {
format!(" ({} LOC)", target.heavy_dep_loc)
} else {
String::new()
};
writeln!(
writer,
" (-{} deps) Inline `{}`{} into `{}` {}",
target.w_unique,
target.heavy_dependency.name,
usage_str,
target.intermediate.name,
chain,
)?;
}
writeln!(writer)?;
}
if !upstream_feature_gate.is_empty() {
writeln!(writer, "Propose feature-gating in upstream crates:")?;
writeln!(writer)?;
for target in &upstream_feature_gate {
let chain = format_short_chain(&target.dep_chain, &target.intermediate.name);
writeln!(
writer,
" (-{} deps) Make `{}` optional in `{}` {}",
target.w_unique, target.heavy_dependency.name, target.intermediate.name, chain,
)?;
}
writeln!(writer)?;
}
if !upstream_already_gated.is_empty() {
writeln!(
writer,
"Already optional — check if you need these features enabled:"
)?;
writeln!(writer)?;
for target in &upstream_already_gated {
let chain = format_short_chain(&target.dep_chain, &target.intermediate.name);
writeln!(
writer,
" (-{} deps) `{}` is optional in `{}` {}",
target.w_unique, target.heavy_dependency.name, target.intermediate.name, chain,
)?;
}
writeln!(writer)?;
}
if !upstream_remove.is_empty() {
writeln!(writer, "Possibly unused (propose removal in upstream):")?;
writeln!(writer)?;
for target in &upstream_remove {
writeln!(
writer,
" (-{} deps) `{}` appears unused in `{}`",
target.w_unique, target.heavy_dependency.name, target.intermediate.name,
)?;
}
writeln!(writer)?;
}
if !std_replacements.is_empty() {
writeln!(writer, "Replace with std equivalents:")?;
writeln!(writer)?;
for target in &std_replacements {
if let RemovalStrategy::ReplaceWithStd { suggestion } = &target.suggestion {
writeln!(
writer,
" (-{} deps) Replace `{}` with {} in `{}`",
target.w_unique,
target.heavy_dependency.name,
suggestion,
target.intermediate.name,
)?;
}
}
writeln!(writer)?;
}
let noise_count = report
.targets
.iter()
.filter(|t| t.confidence == Confidence::Low && t.w_unique == 0)
.count();
if noise_count > 0 {
writeln!(
writer,
"({noise_count} low-impact targets with 0 unique deps hidden. Use -v to see all.)"
)?;
writeln!(writer)?;
}
if verbose {
writeln!(writer)?;
writeln!(writer, "=== Detailed Analysis ===")?;
writeln!(writer)?;
render_detailed(report, writer)?;
}
Ok(())
}
fn is_test_or_example_crate(name: &str) -> bool {
let lower = name.to_lowercase();
let patterns = [
"test",
"example",
"bench",
"doc-example",
"stress",
"tester",
"poc",
"guide",
"wasm-example",
];
patterns.iter().any(|p| lower.contains(p))
}
fn format_short_chain(dep_chain: &[String], intermediate_name: &str) -> String {
if dep_chain.len() <= 2 {
return String::new();
}
let first = &dep_chain[0];
if dep_chain.len() == 3 {
format!("(via {first} -> {intermediate_name})")
} else {
format!("(via {first} -> ... -> {intermediate_name})",)
}
}
fn render_detailed(report: &AnalysisReport, writer: &mut dyn Write) -> anyhow::Result<()> {
for (i, target) in report.targets.iter().enumerate() {
let rank = i + 1;
let hurrs_display = if target.hurrs.is_none() {
"INF".to_string()
} else {
format!("{:.1}", target.hurrs.unwrap_or(0.0))
};
writeln!(writer, "--- #{rank} ---")?;
writeln!(
writer,
" Edge: {} v{} -> {} v{}",
target.intermediate.name,
target.intermediate.version,
target.heavy_dependency.name,
target.heavy_dependency.version,
)?;
writeln!(
writer,
" Metrics: W_trans={}, W_uniq={}, C_ref={}, hURRS={}",
target.w_transitive, target.w_unique, target.c_ref, hurrs_display,
)?;
writeln!(
writer,
" Status: {} | Action: {}",
target.confidence, target.suggestion,
)?;
let mut badges = Vec::new();
if target.phantom {
badges.push("PHANTOM");
}
if target.intermediate_is_workspace_member {
if is_test_or_example_crate(&target.intermediate.name) {
badges.push("YOUR-CRATE (test/example)");
} else {
badges.push("YOUR-CRATE");
}
}
if target.edge_meta.build_only {
badges.push("BUILD-ONLY");
}
if target.edge_meta.already_optional {
badges.push("ALREADY-OPTIONAL");
}
if target.edge_meta.platform_conditional {
badges.push("PLATFORM-CONDITIONAL");
}
if !badges.is_empty() {
writeln!(writer, " Flags: [{}]", badges.join(", "))?;
}
if !target.dep_chain.is_empty() {
writeln!(writer, " Chain: {}", target.dep_chain.join(" -> "))?;
}
if !target.scan_result.file_matches.is_empty() {
writeln!(writer, " Refs:")?;
let mut current_file = String::new();
for m in &target.scan_result.file_matches {
let display = display_path(&m.path);
if display != current_file {
let label = if m.in_generated_file {
format!(" {} (generated)", display)
} else {
format!(" {display}")
};
writeln!(writer, "{label}")?;
current_file = display;
}
writeln!(writer, " L{}: {}", m.line_number, m.line_content)?;
}
}
writeln!(writer)?;
}
writeln!(writer, "=== Summary Table ===")?;
let int_w = report
.targets
.iter()
.map(|t| t.intermediate.name.len())
.max()
.unwrap_or(12)
.max(12);
let dep_w = report
.targets
.iter()
.map(|t| t.heavy_dependency.name.len())
.max()
.unwrap_or(9)
.max(9);
let act_w = report
.targets
.iter()
.map(|t| format!("{}", t.suggestion).len())
.max()
.unwrap_or(6)
.max(6);
let idx_w = format!("{}", report.targets.len()).len().max(1);
writeln!(
writer,
" {:>idx_w$} {:<int_w$} {:<dep_w$} {:>6} {:>5} {:>10} {:<act_w$}",
"#",
"Intermediate",
"Heavy Dep",
"W_uniq",
"C_ref",
"Confidence",
"Action",
idx_w = idx_w,
int_w = int_w,
dep_w = dep_w,
act_w = act_w,
)?;
writeln!(
writer,
" {:─>idx_w$} {:─<int_w$} {:─<dep_w$} {:─>6} {:─>5} {:─>10} {:─<act_w$}",
"",
"",
"",
"",
"",
"",
"",
idx_w = idx_w,
int_w = int_w,
dep_w = dep_w,
act_w = act_w,
)?;
for (i, target) in report.targets.iter().enumerate() {
writeln!(
writer,
" {:>idx_w$} {:<int_w$} {:<dep_w$} {:>6} {:>5} {:>10} {:<act_w$}",
i + 1,
target.intermediate.name,
target.heavy_dependency.name,
target.w_unique,
target.c_ref,
target.confidence,
target.suggestion,
idx_w = idx_w,
int_w = int_w,
dep_w = dep_w,
act_w = act_w,
)?;
}
writeln!(writer)?;
Ok(())
}