use std::fmt::Write as _;
use anyhow::Result;
use crate::{
Body, Category, CategorySet, FuncId, Graph, Solution, Terminal,
args::{Args, Format},
select::Selection,
util::Map,
verify::{Missed, Verdict, Verdicts},
witness,
};
pub(crate) struct Finding<'a> {
pub krate: &'a str,
pub name: &'a str,
pub ids: Vec<FuncId>,
pub categories: CategorySet,
}
impl Finding<'_> {
fn id(&self) -> FuncId {
self.ids.first().copied().unwrap_or(FuncId(0))
}
}
pub fn analysis(
graph: &Graph,
solution: &Solution,
args: &Args,
verdicts: Option<&Verdicts>,
out: &mut String,
) -> Result<()> {
let findings = collect(graph, solution, args);
match args.format {
Format::Human => {
human(graph, solution, args, &findings, verdicts, out);
}
Format::Json => {
json(graph, solution, &findings, args, verdicts, out)?;
}
Format::Github => github(graph, &findings, args, out),
#[cfg(feature = "svg")]
Format::Svg => {}
}
Ok(())
}
fn verdict_of(
graph: &Graph,
verdicts: &Verdicts,
finding: &Finding<'_>,
category: Category,
) -> Verdict {
let mut all_absent = true;
for &id in &finding.ids {
match verdicts.of(&graph.body(id).key, category) {
Verdict::Confirmed => return Verdict::Confirmed,
Verdict::Absent => {}
Verdict::Unverified => all_absent = false,
}
}
if all_absent {
Verdict::Absent
} else {
Verdict::Unverified
}
}
fn considered<'a>(
graph: &'a Graph,
solution: &'a Solution,
args: &'a Args,
) -> impl Iterator<Item = (FuncId, &'a Body, CategorySet)> {
let selection = args.selection();
selection.functions(graph).map(move |(id, body)| {
(id, body, selection.shown(solution.enabled(id)))
})
}
pub(crate) fn reported_name<'a>(body: &'a Body, args: &Args) -> &'a str {
args.selection().name(body)
}
#[must_use]
pub fn any_finding(graph: &Graph, solution: &Solution, args: &Args) -> bool {
!collect(graph, solution, args).is_empty()
}
pub(crate) fn collect<'a>(
graph: &'a Graph,
solution: &'a Solution,
args: &'a Args,
) -> Vec<Finding<'a>> {
let mut findings: Vec<Finding<'a>> = Vec::new();
let mut index: Map<(&str, &str), usize> = Map::default();
for (id, body, categories) in considered(graph, solution, args) {
let name = reported_name(body, args);
let key = (body.krate.as_str(), name);
let at = if let Some(&at) = index.get(&key) {
at
} else {
index.insert(key, findings.len());
findings.push(Finding {
krate: &body.krate,
name,
ids: Vec::new(),
categories: CategorySet::EMPTY,
});
findings.len().saturating_sub(1)
};
let Some(finding) = findings.get_mut(at) else {
continue;
};
let bears_name = body.display == name;
let first_does_not = finding
.ids
.first()
.is_some_and(|&first| graph.body(first).display != name);
if bears_name && first_does_not {
finding.ids.insert(0, id);
} else {
finding.ids.push(id);
}
finding.categories = finding.categories.union(categories);
}
findings.retain(|finding| !finding.categories.is_empty());
findings.sort_by(|a, b| a.name.cmp(b.name));
findings
}
fn missed_prose(graph: &Graph, missed: &[Missed], out: &mut String) {
if missed.is_empty() {
return;
}
out.push_str(
"The compiled artifact reaches panics the analysis did not report:\n\n",
);
for entry in missed {
let body = graph.body(entry.id);
let _ = writeln!(out, "{}", body.display);
if let Some(loc) = &body.loc {
let _ = writeln!(out, " defined at {loc}");
}
for set in &entry.reaches {
let _ = writeln!(out, " {}", set.names().join(", or "));
}
out.push('\n');
}
}
fn human(
graph: &Graph,
solution: &Solution,
args: &Args,
findings: &[Finding<'_>],
verdicts: Option<&Verdicts>,
out: &mut String,
) {
header(graph, args, findings.len(), out);
if findings.is_empty() {
out.push_str("\nNo function can panic under this policy.\n");
return;
}
out.push('\n');
for finding in findings {
let body = graph.body(finding.id());
let _ = writeln!(out, "{}", reported_name(body, args));
if let Some(loc) = &body.loc {
let _ = writeln!(out, " defined at {loc}");
}
for category in finding.categories.iter() {
match direct_site(graph, solution, finding, category) {
Some(site) => {
let _ = write!(out, " {category:<18} {}", site.0);
if let Some(loc) = site.1 {
let _ = write!(out, " at {loc}");
}
if always(graph, solution, finding, category) {
out.push_str(" (always)");
}
}
None => {
let _ = write!(
out,
" {category:<18} reached through a call"
);
}
}
if let Some(verdicts) = verdicts {
let word = match verdict_of(graph, verdicts, finding, category)
{
Verdict::Confirmed => "confirmed in",
Verdict::Absent => "absent from",
Verdict::Unverified => "unverified in",
};
let _ = write!(out, " ({word} the compiled artifact)");
}
out.push('\n');
}
out.push('\n');
}
if let Some(verdicts) = verdicts {
missed_prose(graph, &verdicts.missed(graph, solution), out);
}
let _ =
writeln!(out, "Run `panicgraph why <function>` to see a call path.");
}
fn always(
graph: &Graph,
solution: &Solution,
finding: &Finding<'_>,
category: Category,
) -> bool {
finding.ids.iter().any(|&id| {
let body = graph.body(id);
let activity = solution.activity(graph, id);
body.sites.iter().enumerate().any(|(i, site)| {
site.certain && site.category == category && activity.site(i)
})
})
}
fn direct_site<'a>(
graph: &'a Graph,
solution: &Solution,
finding: &Finding<'_>,
category: Category,
) -> Option<(&'a str, Option<String>)> {
for &id in &finding.ids {
let body = graph.body(id);
let activity = solution.activity(graph, id);
let hit =
body.sites.iter().enumerate().find(|(i, site)| {
site.category == category && activity.site(*i)
});
if let Some((_, site)) = hit {
return Some((
site.reason.as_str(),
site.loc.as_ref().map(ToString::to_string),
));
}
}
None
}
fn header(graph: &Graph, args: &Args, found: usize, out: &mut String) {
out.push_str("Analysis\n");
if let Some(config) = graph.config() {
let _ = writeln!(out, " rustc {}", config.rustc);
let _ = writeln!(
out,
" profile {} (debug assertions {}, overflow \
checks {})",
config.profile,
on_off(config.debug_assertions),
on_off(config.overflow_checks),
);
let _ =
writeln!(out, " standard library {}", config.std_mode.name());
if let Some(level) = config.mir_opt_level {
let _ = writeln!(out, " mir opt level {level}");
}
}
if !args.features.is_default() {
let _ = writeln!(
out,
" features {}",
args.features.describe()
);
}
let suppressed = if args.suppress.is_empty() {
"nothing".to_owned()
} else {
args.suppress.to_string()
};
let _ = writeln!(out, " suppressed {suppressed}");
let _ = writeln!(
out,
" functions {} analysed, {found} can panic",
graph.len()
);
}
fn json(
graph: &Graph,
solution: &Solution,
findings: &[Finding<'_>],
args: &Args,
verdicts: Option<&Verdicts>,
out: &mut String,
) -> Result<()> {
let items: Vec<serde_json::Value> = findings
.iter()
.map(|f| {
let body = graph.body(f.id());
let mut item = serde_json::json!({
"function": reported_name(body, args),
"crate": body.krate,
"location": body.loc.as_ref().map(ToString::to_string),
"categories": f
.categories.names(),
});
let certain: Vec<&str> = f
.categories
.iter()
.filter(|category| always(graph, solution, f, *category))
.map(Category::name)
.collect();
if !certain.is_empty() {
item["always"] = certain.into();
}
if let Some(verdicts) = verdicts {
let verified: serde_json::Map<String, serde_json::Value> = f
.categories
.iter()
.map(|category| {
(
category.name().to_owned(),
verdict_of(graph, verdicts, f, category)
.name()
.into(),
)
})
.collect();
item["verified"] = verified.into();
}
item
})
.collect();
let mut doc = serde_json::json!({
"config": graph.config(),
"analysed": graph.len(),
"findings": items,
});
if let Some(verdicts) = verdicts {
let missed: Vec<serde_json::Value> = verdicts
.missed(graph, solution)
.iter()
.map(|entry| {
let body = graph.body(entry.id);
serde_json::json!({
"function": reported_name(body, args),
"crate": body.krate,
"location": body.loc.as_ref().map(ToString::to_string),
"reaches": entry
.reaches
.iter()
.map(|set| set.names())
.collect::<Vec<_>>(),
})
})
.collect();
doc["missed"] = missed.into();
}
out.push_str(&serde_json::to_string_pretty(&doc)?);
out.push('\n');
Ok(())
}
pub(crate) fn workflow_location(loc: Option<&str>) -> String {
fn split(loc: &str) -> Option<String> {
let mut parts = loc.rsplitn(3, ':');
let col = parts.next()?;
let line = parts.next()?;
let file = parts.next()?;
Some(format!("file={file},line={line},col={col},"))
}
loc.and_then(split).unwrap_or_default()
}
fn github(
graph: &Graph,
findings: &[Finding<'_>],
args: &Args,
out: &mut String,
) {
for finding in findings {
let body = graph.body(finding.id());
let loc = body.loc.as_ref().map(ToString::to_string);
let where_at = workflow_location(loc.as_deref());
let _ = writeln!(
out,
"::warning {where_at}title=Function can panic::{} can panic \
with {}",
reported_name(body, args),
finding.categories.names().join(", ")
);
}
}
struct Explanation<'a> {
body: &'a Body,
matched: usize,
ambiguous: bool,
bodies: usize,
categories: CategorySet,
paths: Vec<(Category, witness::Witness)>,
}
fn explain<'a>(
graph: &'a Graph,
solution: &Solution,
selection: Selection,
name: &str,
) -> Option<Explanation<'a>> {
let matches = graph.find_by_display(name);
let &first = matches.first()?;
let body = graph.body(first);
let ids = selection.namesakes(graph, first);
let categories = ids.iter().fold(CategorySet::EMPTY, |set, id| {
set.union(solution.enabled(*id))
});
let paths = categories
.iter()
.filter_map(|category| {
witness::find_any(graph, solution, &ids, category)
.map(|path| (category, path))
})
.collect();
Some(Explanation {
body,
matched: matches.len(),
ambiguous: matches
.iter()
.any(|&other| graph.body(other).display != body.display),
bodies: ids.len(),
categories,
paths,
})
}
pub fn why(
graph: &Graph,
solution: &Solution,
args: &Args,
name: &str,
out: &mut String,
) -> Result<()> {
let selection = args.selection();
let found = explain(graph, solution, selection, name);
if args.format == Format::Json {
return why_json(graph, selection, name, found, out);
}
why_prose(graph, selection, name, found, out);
Ok(())
}
fn why_json(
graph: &Graph,
selection: Selection,
name: &str,
found: Option<Explanation<'_>>,
out: &mut String,
) -> Result<()> {
let doc = match found {
None => serde_json::json!({ "query": name, "matched": 0 }),
Some(found) => {
let paths: Vec<serde_json::Value> = found
.paths
.iter()
.map(|(category, path)| {
serde_json::json!({
"category": category.name(),
"from": graph.body(path.root).display,
"hops": path
.hops
.iter()
.map(|hop| serde_json::json!({
"function": graph.body(hop.callee).display,
"kind": hop.kind.name(),
"location": hop
.loc
.as_ref()
.map(ToString::to_string),
}))
.collect::<Vec<_>>(),
"ending": ending(graph, path),
})
})
.collect();
serde_json::json!({
"query": name,
"matched": found.matched,
"function": selection.name(found.body),
"crate": found.body.krate,
"location": found.body.loc.as_ref().map(ToString::to_string),
"bodies": found.bodies,
"categories": found.categories.names(),
"paths": paths,
})
}
};
out.push_str(&serde_json::to_string_pretty(&doc)?);
out.push('\n');
Ok(())
}
fn ending(graph: &Graph, path: &witness::Witness) -> serde_json::Value {
let body = graph.body(path.func);
match path.terminal {
Terminal::Site(i) => body.sites.get(i).map_or_else(
|| serde_json::json!({ "kind": "site" }),
|site| {
serde_json::json!({
"kind": "site",
"reason": site.reason,
"location": site.loc.as_ref().map(ToString::to_string),
})
},
),
Terminal::Opaque => serde_json::json!({
"kind": if body.foreign { "foreign" } else { "opaque" },
}),
Terminal::Unresolved(i) => body.calls.get(i).map_or_else(
|| serde_json::json!({ "kind": "unresolved" }),
|call| {
serde_json::json!({
"kind": "unresolved",
"callee": call.callee_display,
"edge": call.kind.name(),
"location": call.loc.as_ref().map(ToString::to_string),
})
},
),
}
}
fn why_prose(
graph: &Graph,
selection: Selection,
name: &str,
found: Option<Explanation<'_>>,
out: &mut String,
) {
let Some(found) = found else {
let _ = writeln!(out, "No function matching `{name}` was analysed.");
return;
};
let function = selection.name(found.body);
if found.ambiguous {
let _ = writeln!(
out,
"`{name}` matched {} functions; explaining `{function}`.\n",
found.matched
);
}
if found.bodies > 1 {
let _ = writeln!(
out,
"`{function}` has {} bodies; each panic below is shown from one \
that reaches it.\n",
found.bodies
);
}
if found.categories.is_empty() {
let _ = writeln!(out, "{function} cannot panic under this policy.");
return;
}
for (category, path) in &found.paths {
let _ = writeln!(out, "{function} can panic with `{category}`:\n");
let _ = writeln!(out, " {}", graph.body(path.root).display);
for hop in &path.hops {
if let Some(loc) = &hop.loc {
let _ = writeln!(out, " at {loc} [{}]", hop.kind.name());
}
let _ = writeln!(out, " -> {}", graph.body(hop.callee).display);
}
describe_terminal(graph, path, out);
out.push('\n');
}
}
fn describe_terminal(graph: &Graph, path: &witness::Witness, out: &mut String) {
let body = graph.body(path.func);
match path.terminal {
Terminal::Site(i) => {
let Some(site) = body.sites.get(i) else {
return;
};
let _ = write!(out, " {}", site.reason);
if let Some(loc) = &site.loc {
let _ = write!(out, " at {loc}");
}
out.push('\n');
}
Terminal::Opaque if body.foreign => {
let _ = writeln!(
out,
" foreign code, which has no Rust body to read"
);
}
Terminal::Opaque => {
let _ = writeln!(
out,
" no MIR available for this function, so its panics \
are unknown"
);
let _ = writeln!(
out,
" re-run with `--std full` to see inside the standard \
library"
);
}
Terminal::Unresolved(i) => {
let Some(call) = body.calls.get(i) else {
return;
};
let _ = write!(
out,
" calls {} through a {} edge, target unknown",
call.callee_display,
call.kind.name()
);
if let Some(loc) = &call.loc {
let _ = write!(out, " at {loc}");
}
out.push('\n');
}
}
}
pub fn kinds_json(out: &mut String) -> Result<()> {
let doc = serde_json::json!({
"categories": crate::category::ALL
.iter()
.map(|category| serde_json::json!({
"name": category.name(),
"describe": category.describe(),
"assumed": CategorySet::assumed().contains(*category),
}))
.collect::<Vec<_>>(),
"aliases": serde_json::json!({
"oom": CategorySet::oom().names(),
"default": CategorySet::default_suppressed().names(),
"assumed": CategorySet::assumed().names(),
"all": crate::category::ALL
.iter()
.map(|category| category.name())
.collect::<Vec<_>>(),
}),
});
out.push_str(&serde_json::to_string_pretty(&doc)?);
out.push('\n');
Ok(())
}
pub fn kinds(out: &mut String) {
out.push_str("Panic categories\n\n");
for category in crate::category::ALL {
let _ =
writeln!(out, " {:<18} {}", category.name(), category.describe());
}
out.push_str(
"\nGroup aliases: `oom` covers capacity-overflow and alloc-failure, \
`default` adds ub-check, `assumed` covers what the analysis could \
not read, `all` covers everything.\n",
);
}
const fn on_off(value: bool) -> &'static str {
if value { "on" } else { "off" }
}
pub fn suppressed_hint(
graph: &Graph,
solution: &Solution,
args: &Args,
) -> Result<Option<String>> {
if args.suppress.is_empty() {
return Ok(None);
}
let hidden = solution.cleared_by_suppression(graph, args.selection())?;
Ok((hidden > 0).then(|| {
format!(
"{hidden} local functions panic only through suppressed \
categories ({}).",
args.suppress
)
}))
}