use crate::*;
pub(crate) fn tally_effects(fns: &[ReportEntry]) -> (BTreeMap<&'static str, usize>, Vec<String>) {
let mut tally: BTreeMap<&'static str, usize> = EFFECTS.iter().map(|e| (*e, 0)).collect();
let mut unresolved: Vec<String> = Vec::new();
for e in fns {
for x in &e.inferred {
if let Some(n) = tally.get_mut(x.as_str()) {
*n += 1;
}
}
if e.unresolved || e.inferred.iter().any(|x| x == "Unknown") {
unresolved.push(e.func.clone());
}
}
(tally, unresolved)
}
pub(crate) fn gained_effects(cur: &BTreeSet<String>, base: &BTreeSet<String>) -> Vec<String> {
cur.difference(base).cloned().collect()
}
pub(crate) fn cmd_audit(args: &[String]) -> i32 {
let (pre, ver, suspect_path) = match args {
[a, b, c, ..] => (a.as_str(), b.as_str(), c.as_str()),
_ => {
eprintln!("usage: candor-query audit <prefix> <engine_ver> <suspect_file> [--coverage]");
return 2;
}
};
let coverage = args.iter().any(|a| a == "--coverage" || a == "-c");
let base = prefix_base(pre);
let mut fns: Vec<ReportEntry> = Vec::new();
let mut percrate: Vec<(String, usize)> = Vec::new();
let mut analyzed: BTreeSet<String> = BTreeSet::new();
let rfs = report_files(pre);
if rfs.is_empty() {
eprintln!("candor: no report files at prefix `{pre}` — check the path.");
return 2;
}
for rf in rfs {
let label = format!("{}.{}", rf.krate, rf.kind);
analyzed.insert(rf.krate.clone());
let es = match std::fs::read_to_string(&rf.path) {
Ok(t) => match report_entries_counted(&t) {
Some((es, dropped)) => {
if dropped > 0 {
eprintln!("candor: report {} — {dropped} function entr{} could not be parsed and {} OMITTED from this audit (corrupt or mid-write); re-run the scan",
rf.path.display(),
if dropped == 1 { "y" } else { "ies" },
if dropped == 1 { "is" } else { "are" });
}
es
}
None => {
eprintln!("candor: report {} failed to parse — its functions are OMITTED from this audit (corrupt or mid-write); re-run the scan", rf.path.display());
Vec::new()
}
},
Err(_) => {
eprintln!("candor: report {} could not be read — its functions are OMITTED from this audit", rf.path.display());
Vec::new()
}
};
percrate.push((label, es.len()));
fns.extend(es);
}
if fns.is_empty() {
println!("candor: no effectful functions found (everything candor can see is pure).");
return 0;
}
let (tally, unresolved) = tally_effects(&fns);
println!("candor @{ver}");
let pc = percrate.iter().map(|(k, n)| format!("{n} {k}")).collect::<Vec<_>>().join(" · ");
println!("{} effectful functions · {}", fns.len(), pc);
println!();
let mut ranked: Vec<(usize, &str)> = EFFECTS.iter().filter(|e| tally[**e] > 0).map(|e| (tally[*e], *e)).collect();
ranked.sort_by(|a, b| b.cmp(a));
let eff_line = ranked.iter().map(|(n, e)| format!("{n} {e}")).collect::<Vec<_>>().join(" · ");
println!(" effects {eff_line}");
println!();
if !unresolved.is_empty() {
let u: Vec<String> = unresolved.iter().cloned().collect::<BTreeSet<_>>().into_iter().collect();
let extra = if u.len() > 5 { format!(" (+{} more)", u.len() - 5) } else { String::new() };
println!(" ⚠ {} make calls candor can't resolve — their effect set may be incomplete (read these):", u.len());
println!(" {}{extra}", u.iter().take(5).cloned().collect::<Vec<_>>().join(", "));
println!();
}
let (calib_c, calib_p, calib_path) = load_calibrated(pre, &base);
let seen = encountered_set(pre);
let suspect = std::fs::read_to_string(suspect_path).ok().map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
let suspect = suspect.and_then(|s| match Regex::new(&s) {
Ok(re) => Some(re),
Err(e) => {
eprintln!("candor: ignoring unparsable suspect pattern ({e}); coverage-gap check skipped");
None
}
});
let calibrated = |c: &str| -> bool {
calib_c.contains(c)
|| calib_c.iter().any(|k| c == k || c.starts_with(&format!("{k}_")))
|| calib_p.iter().any(|p| c.starts_with(p))
|| calib_path.contains(c) };
let uncovered: Vec<String> =
seen.iter().filter(|c| !calibrated(c) && !analyzed.contains(*c)).cloned().collect();
let suspect_gaps: Vec<String> = match &suspect {
Some(re) => uncovered.iter().filter(|c| re.is_match(c)).cloned().collect(),
None => Vec::new(),
};
if !suspect_gaps.is_empty() {
println!(" ⚠ coverage: {} uncalibrated — effects through them may be under-counted", suspect_gaps.join(", "));
println!();
}
if coverage {
if uncovered.is_empty() {
println!(" coverage: every external crate candor called into is calibrated — no blind spots.");
} else {
println!(" coverage — {} external crate(s) candor has NO effect rules for; calls into them", uncovered.len());
println!(" are assumed PURE, so any I/O they perform is UNDER-REPORTED. Verify the effectful ones:");
for c in &uncovered {
println!(" {c}");
}
}
println!();
} else if !uncovered.is_empty() {
let other = uncovered.len() - suspect_gaps.len();
if other > 0 {
println!(" {other} more external crate(s) have no effect rules (assumed pure) — `cargo candor audit --coverage` to list", );
println!();
}
}
let width = |e: &ReportEntry| e.inferred.iter().filter(|x| *x != "Unknown").count();
let mut top: Vec<&ReportEntry> = fns.iter().filter(|e| width(e) > 0).collect();
top.sort_by(|a, b| width(b).cmp(&width(a)).then_with(|| a.func.cmp(&b.func)));
top.truncate(8);
if !top.is_empty() {
let w = top.iter().map(|e| e.func.chars().count()).max().unwrap_or(0);
println!(" broadest effect surface");
for e in &top {
let effs: Vec<String> = sorted(&e.inferred).into_iter().filter(|x| x != "Unknown").collect();
println!(" {:<w$} {{ {} }}", e.func, effs.join(" "), w = w);
}
println!();
}
println!(" full per-function view: cargo candor audit --all");
if !coverage {
println!(" classifier blind spots: cargo candor audit --coverage");
}
println!(" guard against new effects: cargo candor snapshot .candor/baseline");
0
}