use crate::artifact::{self, SymClass};
use crate::attribute::{Attributor, RlibMap};
use crate::builder::BuildOutput;
use crate::cli::TreemapArgs;
use crate::model::{artifact_nodes, crate_bin_totals, Node};
use crate::parity::lib_target_name;
use crate::util::{split_path, strip_generics};
use anyhow::{Context, Result};
use cargo_metadata::{DependencyKind, Metadata, Package, TargetKind};
use serde::Serialize;
use std::cmp::Reverse;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
#[derive(Serialize)]
pub struct DepValue {
pub name: String,
pub surface_fns: u32,
pub total_public: u32,
pub used_fns: u32,
pub ratio: Option<f32>,
pub size_text: u64,
pub size_data: u64,
pub referenced: bool,
pub is_proc_macro: bool,
pub rustdoc_ok: bool,
pub flag_unused: bool,
pub flag_low_value: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
fn collect_sizes(root: &Node) -> HashMap<String, (u64, u64)> {
crate_bin_totals(root)
.into_iter()
.map(|(k, (t, d, _, _))| (k, (t, d)))
.collect()
}
fn collect_tree_fns(root: &Node) -> HashMap<String, HashSet<String>> {
let mut out: HashMap<String, HashSet<String>> = HashMap::new();
for art in artifact_nodes(root) {
for c in &art.groups {
collect_leaf_fns(c, out.entry(c.label.clone()).or_default());
}
}
out
}
fn collect_leaf_fns(node: &Node, out: &mut HashSet<String>) {
if node.groups.is_empty() {
if node.text > 0 {
out.insert(node.label.clone());
}
} else {
for c in &node.groups {
collect_leaf_fns(c, out);
}
}
}
pub fn crate_fns(build: &BuildOutput, split_std: bool) -> Result<HashMap<String, HashSet<String>>> {
let rlib_map = RlibMap::build(&build.rlibs);
let attr = Attributor::new(&rlib_map, &build.crate_names, split_std);
let mut out: HashMap<String, HashSet<String>> = HashMap::new();
for path in &build.artifacts {
let parsed = artifact::parse(path, false)?;
for sym in &parsed.symbols {
if sym.class != SymClass::Text {
continue;
}
let a = attr.attribute(&sym.name, sym.address, None);
let segs = split_path(&a.demangled);
let leaf = match segs.last() {
Some(s) => strip_generics(s),
None => continue,
};
out.entry(a.crate_name).or_default().insert(leaf);
}
}
Ok(out)
}
fn rustdoc_surface(json: &std::path::Path) -> Result<(HashSet<String>, u32)> {
use public_api::tokens::Token;
let api = public_api::Builder::from_rustdoc_json(json)
.build()
.with_context(|| format!("parsing {}", json.display()))?;
let mut fns = HashSet::new();
let mut total = 0u32;
for item in api.items() {
total += 1;
for tok in item.tokens() {
if let Token::Function(n) = tok {
fns.insert(n.clone());
break;
}
}
}
Ok((fns, total))
}
fn gen_rustdoc(args: &TreemapArgs, meta: &Metadata, spec: &str, lib_name: &str) -> Result<PathBuf> {
let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
let mut cmd = std::process::Command::new(&cargo);
cmd.env("RUSTC_BOOTSTRAP", "1") .arg("rustdoc")
.arg("-q")
.arg("-p")
.arg(spec)
.arg("--lib"); if let Some(t) = &args.target {
cmd.arg("--target").arg(t);
}
if let Some(mp) = &args.manifest.manifest_path {
cmd.arg("--manifest-path").arg(mp);
}
cmd.arg("--")
.arg("-Z")
.arg("unstable-options")
.arg("--output-format")
.arg("json");
cmd.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
let status = cmd.status().context("spawning cargo rustdoc")?;
if !status.success() {
anyhow::bail!("cargo rustdoc failed for {spec}");
}
let td = &meta.target_directory;
let file = format!("{lib_name}.json");
let candidates = [
td.join("doc").join(&file),
match &args.target {
Some(t) => td.join(t).join("doc").join(&file),
None => td.join("doc").join(&file),
},
];
candidates
.iter()
.find(|p| p.exists())
.map(|p| p.clone().into_std_path_buf())
.with_context(|| format!("rustdoc JSON for {spec} not found under {td}/doc"))
}
fn scan_referenced(meta: &Metadata, idents: &HashSet<String>) -> HashSet<String> {
let mut found = HashSet::new();
let root = meta.workspace_root.as_std_path();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(rd) = std::fs::read_dir(&dir) else {
continue;
};
for ent in rd.flatten() {
let p = ent.path();
if p.is_dir() {
let name = p.file_name().and_then(|s| s.to_str()).unwrap_or("");
if name != "target" && name != ".git" {
stack.push(p);
}
} else if p.extension().and_then(|s| s.to_str()) == Some("rs") {
if let Ok(src) = std::fs::read_to_string(&p) {
for id in idents {
if !found.contains(id) && mentions_ident(&src, id) {
found.insert(id.clone());
}
}
}
}
}
}
found
}
fn mentions_ident(src: &str, ident: &str) -> bool {
let mut from = 0;
while let Some(rel) = src[from..].find(ident) {
let i = from + rel;
let before = src[..i].chars().next_back();
let after = src[i + ident.len()..].chars().next();
let ok_before = before.is_none_or(|c| !(c.is_alphanumeric() || c == '_'));
let ok_after = after.is_none_or(|c| !(c.is_alphanumeric() || c == '_'));
if ok_before && ok_after {
return true;
}
from = i + ident.len();
}
false
}
pub fn analyze(
meta: &Metadata,
selected: &[&Package],
root: &Node,
used_override: Option<&HashMap<String, HashSet<String>>>,
args: &TreemapArgs,
) -> Result<Vec<DepValue>> {
let sizes = collect_sizes(root);
let tree_fns;
let fns = match used_override {
Some(m) => m,
None => {
tree_fns = collect_tree_fns(root);
&tree_fns
}
};
let by_name: HashMap<String, &Package> =
meta.packages.iter().map(|p| (p.name.to_string(), p)).collect();
let mut specs: Vec<String> = Vec::new();
let mut seen = HashSet::new();
for pkg in selected {
for dep in &pkg.dependencies {
if dep.kind == DependencyKind::Normal && seen.insert(dep.name.clone()) {
specs.push(dep.name.clone());
}
}
}
specs.sort();
let idents: HashSet<String> = specs
.iter()
.filter_map(|s| by_name.get(s).and_then(|p| lib_target_name(p)))
.collect();
let referenced = scan_referenced(meta, &idents);
let mut out = Vec::new();
for spec in &specs {
let Some(pkg) = by_name.get(spec) else {
continue;
};
let Some(lib) = lib_target_name(pkg) else {
continue;
};
let is_proc_macro = pkg
.targets
.iter()
.any(|t| t.kind.contains(&TargetKind::ProcMacro));
let (size_text, size_data) = sizes.get(&lib).copied().unwrap_or((0, 0));
let used_set = fns.get(&lib);
let is_ref = referenced.contains(&lib);
if is_proc_macro {
out.push(DepValue {
name: lib,
surface_fns: 0,
total_public: 0,
used_fns: 0,
ratio: None,
size_text,
size_data,
referenced: is_ref,
is_proc_macro: true,
rustdoc_ok: false,
flag_unused: false,
flag_low_value: false,
note: Some("proc-macro: emits no runtime symbols (unmeasurable)".to_string()),
});
continue;
}
eprintln!("cargo-treemap: api-usage: documenting {spec}…");
let (surface, total, rustdoc_ok, mut note) =
match gen_rustdoc(args, meta, spec, &lib).and_then(|j| rustdoc_surface(&j)) {
Ok((fns, total)) => (fns, total, true, None),
Err(e) => (HashSet::new(), 0, false, Some(format!("rustdoc unavailable: {e:#}"))),
};
let used = used_set.map_or(0, |set| surface.intersection(set).count() as u32);
let ratio = if surface.is_empty() {
if rustdoc_ok {
note = Some(
"no free functions in public API (trait/derive/facade crate)".to_string(),
);
}
None
} else {
Some(used as f32 / surface.len() as f32)
};
out.push(DepValue {
name: lib,
surface_fns: surface.len() as u32,
total_public: total,
used_fns: used,
ratio,
size_text,
size_data,
referenced: is_ref,
is_proc_macro: false,
rustdoc_ok,
flag_unused: !is_ref && size_text + size_data == 0,
flag_low_value: false, note,
});
}
let mut sizes: Vec<u64> = out.iter().map(|d| d.size_text).filter(|s| *s > 0).collect();
sizes.sort_unstable();
let p75 = sizes
.get(sizes.len().saturating_sub(1).min(sizes.len() * 3 / 4))
.copied()
.unwrap_or(0);
let threshold = 0.15;
for d in &mut out {
if !d.is_proc_macro && d.referenced && d.size_text >= p75 && p75 > 0 {
if let Some(r) = d.ratio {
if r <= threshold {
d.flag_low_value = true;
}
}
}
}
out.sort_by_key(|d| Reverse(d.size_text + d.size_data));
Ok(out)
}