use crate::config::Config;
use crate::heuristics::detect_panic_cause;
use crate::heuristics::is_panic_triggering_function;
use crate::panic_cause::PanicCause;
use crate::project_context::ProjectContext;
use crate::sym::CallGraph;
use dashmap::DashSet;
use rayon::prelude::*;
use rustc_demangle::demangle;
use std::collections::{HashMap, HashSet};
use std::sync::{LazyLock, Mutex};
static PRE_FILTER_LOCATIONS: LazyLock<Mutex<HashSet<(String, u32)>>> =
LazyLock::new(|| Mutex::new(HashSet::new()));
pub fn take_pre_filter_locations() -> HashSet<(String, u32)> {
std::mem::take(&mut *PRE_FILTER_LOCATIONS.lock().unwrap())
}
fn collect_locations(points: &[CrateCodePoint], locations: &mut HashSet<(String, u32)>) {
for p in points {
locations.insert((p.file.clone(), p.line));
collect_locations(&p.children, locations);
}
}
use std::sync::Arc;
#[derive(Debug)]
pub struct CallTreeNode {
pub name: String,
pub file: Option<String>,
pub line: Option<u32>,
pub column: Option<u32>,
pub callers: Vec<CallTreeNode>,
}
impl CallTreeNode {
pub fn new_root(name: String) -> Self {
CallTreeNode {
name,
file: None,
line: None,
column: None,
callers: Vec::new(),
}
}
}
pub fn build_call_tree_parallel_filtered(
call_graph: &CallGraph<'_>,
target_addr: u64,
visited: &Arc<DashSet<u64>>,
project_context: &ProjectContext,
) -> Vec<CallTreeNode> {
let callers = call_graph.get_callers(target_addr);
callers
.par_iter()
.filter_map(|caller_info| {
let caller_addr = caller_info.caller_start_address;
let should_recurse = visited.insert(caller_addr);
let file = caller_info.caller_file.clone().or(caller_info.file.clone());
let child_callers = if should_recurse {
build_call_tree_sequential_filtered(
call_graph,
caller_addr,
visited,
project_context,
)
} else {
build_shallow_callers_filtered(call_graph, caller_addr, project_context)
};
if child_callers.is_empty()
&& !file
.as_ref()
.is_some_and(|f| project_context.is_crate_source(f))
{
return None;
}
Some(CallTreeNode {
name: caller_info.caller_name.clone().into_owned(),
file,
line: caller_info.line,
column: caller_info.column,
callers: child_callers,
})
})
.collect()
}
pub fn build_call_tree_sequential_filtered(
call_graph: &CallGraph<'_>,
target_addr: u64,
visited: &Arc<DashSet<u64>>,
project_context: &ProjectContext,
) -> Vec<CallTreeNode> {
let callers = call_graph.get_callers(target_addr);
callers
.iter()
.filter_map(|caller_info| {
let caller_addr = caller_info.caller_start_address;
let should_recurse = visited.insert(caller_addr);
let file = caller_info.caller_file.clone().or(caller_info.file.clone());
let child_callers = if should_recurse {
build_call_tree_sequential_filtered(
call_graph,
caller_addr,
visited,
project_context,
)
} else {
build_shallow_callers_filtered(call_graph, caller_addr, project_context)
};
if child_callers.is_empty()
&& !file
.as_ref()
.is_some_and(|f| project_context.is_crate_source(f))
{
return None;
}
Some(CallTreeNode {
name: caller_info.caller_name.clone().into_owned(),
file,
line: caller_info.line,
column: caller_info.column,
callers: child_callers,
})
})
.collect()
}
pub fn build_shallow_callers_filtered(
call_graph: &CallGraph<'_>,
target_addr: u64,
project_context: &ProjectContext,
) -> Vec<CallTreeNode> {
call_graph
.get_callers(target_addr)
.iter()
.filter_map(|caller_info| {
let file = caller_info.caller_file.clone().or(caller_info.file.clone());
if !file
.as_ref()
.is_some_and(|f| project_context.is_crate_source(f))
{
return None;
}
Some(CallTreeNode {
name: caller_info.caller_name.clone().into_owned(),
file,
line: caller_info.line,
column: caller_info.column,
callers: vec![], })
})
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrateCodePoint {
pub name: String,
pub file: String,
pub line: u32,
pub column: Option<u32>,
pub causes: HashSet<PanicCause>,
pub children: Vec<CrateCodePoint>,
pub is_direct_panic: bool,
pub called_function: Option<String>,
}
pub type CodePointKey = (String, u32);
pub type CodePointInfo = (
String,
Option<u32>,
HashSet<PanicCause>,
HashSet<CodePointKey>,
bool, Option<String>, );
pub type CodePointMap = HashMap<CodePointKey, CodePointInfo>;
fn extract_qualified_function_name(full_name: &str) -> String {
let demangled = demangle(full_name).to_string();
let mut cleaned = String::new();
let mut depth = 0;
for c in demangled.chars() {
match c {
'<' => depth += 1,
'>' => depth -= 1,
_ if depth == 0 => cleaned.push(c),
_ => {}
}
}
let mut segments: Vec<&str> = cleaned
.split("::")
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
if let Some(last) = segments.last() {
if last.starts_with('h')
&& last.len() > 1
&& last[1..].chars().all(|c| c.is_ascii_hexdigit())
{
segments.pop();
}
}
if segments.is_empty() {
cleaned.trim().to_string()
} else {
segments.join("::")
}
}
pub fn collect_crate_code_points_hierarchical(
node: &CallTreeNode,
project_context: &ProjectContext,
) -> Vec<CrateCodePoint> {
let mut points: CodePointMap = CodePointMap::new();
let workspace_root = Some(std::path::Path::new(project_context.project_root()));
collect_crate_relationships(
node,
&mut points,
None,
None,
None,
project_context,
workspace_root,
);
let mut roots: Vec<CodePointKey> = points.keys().cloned().collect();
roots.sort();
fn build_subtree(
key: &CodePointKey,
points: &CodePointMap,
path: &mut HashSet<CodePointKey>,
cache: &mut HashMap<CodePointKey, CrateCodePoint>,
) -> Option<CrateCodePoint> {
if path.contains(key) {
return None;
}
if let Some(cached) = cache.get(key) {
return Some(CrateCodePoint {
name: cached.name.clone(),
file: cached.file.clone(),
line: cached.line,
column: cached.column,
causes: cached.causes.clone(),
children: vec![], is_direct_panic: cached.is_direct_panic,
called_function: cached.called_function.clone(),
});
}
path.insert(key.clone());
let (name, column, causes, child_keys_set, is_direct_panic, called_function) =
points.get(key)?;
let mut child_keys: Vec<_> = child_keys_set.iter().cloned().collect();
child_keys.sort();
let mut children: Vec<CrateCodePoint> = child_keys
.iter()
.filter_map(|child_key| build_subtree(child_key, points, path, cache))
.collect();
children.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
path.remove(key);
let point = CrateCodePoint {
name: name.clone(),
file: key.0.clone(),
line: key.1,
column: *column,
causes: causes.clone(),
children,
is_direct_panic: *is_direct_panic,
called_function: called_function.clone(),
};
cache.insert(key.clone(), point.clone());
Some(point)
}
roots
.iter()
.filter_map(|root| {
let mut cache: HashMap<CodePointKey, CrateCodePoint> = HashMap::new();
build_subtree(root, &points, &mut HashSet::new(), &mut cache)
})
.collect()
}
pub fn collect_crate_relationships(
node: &CallTreeNode,
points: &mut CodePointMap,
child_crate_key: Option<CodePointKey>,
current_cause: Option<PanicCause>,
immediate_callee: Option<&str>,
project_context: &ProjectContext,
workspace_root: Option<&std::path::Path>,
) {
let detected_cause = detect_panic_cause(&node.name).or(current_cause);
let file_matches = node
.file
.as_ref()
.is_some_and(|file| project_context.is_crate_source(file));
let node_key = if let (Some(file), Some(line)) = (&node.file, &node.line)
&& file_matches
&& *line > 0
{
Some((file.clone(), *line))
} else {
None
};
if let Some(key) = &node_key {
let is_direct = immediate_callee
.map(is_panic_triggering_function)
.unwrap_or(false);
let called_fn = if !is_direct {
immediate_callee.map(extract_qualified_function_name)
} else {
None
};
let entry = points.entry(key.clone()).or_insert_with(|| {
(
node.name.clone(),
node.column,
HashSet::new(),
HashSet::new(),
is_direct,
called_fn.clone(),
)
});
if is_direct {
entry.4 = true;
entry.5 = None; } else if entry.5.is_none() && called_fn.is_some() {
entry.5 = called_fn;
}
if let Some(cause) = &detected_cause {
entry.2.insert(cause.clone());
}
if let Some(child_key) = &child_crate_key {
entry.3.insert(child_key.clone());
}
}
let propagated_cause = if let (Some(key), Some(cause)) = (&node_key, &detected_cause) {
let cause_id = cause.id();
if crate::inline_allows::check_inline_allow(&key.0, key.1, cause_id, workspace_root) {
None
} else {
detected_cause.clone()
}
} else {
detected_cause.clone()
};
let next_child = node_key.or(child_crate_key);
for caller in &node.callers {
collect_crate_relationships(
caller,
points,
next_child.clone(),
propagated_cause.clone(),
Some(&node.name),
project_context,
workspace_root,
);
}
}
pub fn collect_crate_code_points(
node: &CallTreeNode,
config: &Config,
project_context: &ProjectContext,
) -> (Vec<CrateCodePoint>, AnalysisSummary) {
let mut roots = collect_crate_code_points_hierarchical(node, project_context);
assign_unknown_causes(&mut roots);
collect_locations(&roots, &mut *PRE_FILTER_LOCATIONS.lock().unwrap());
filter_allowed_causes(&mut roots, config, project_context);
dedupe_crate_points(&mut roots);
roots.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
let summary = count_crate_points_and_files(&roots);
(roots, summary)
}
fn assign_unknown_causes(points: &mut [CrateCodePoint]) {
for point in points.iter_mut() {
assign_unknown_causes(&mut point.children);
if point.causes.is_empty() {
if point.is_direct_panic {
point.causes.insert(PanicCause::ExplicitPanic);
} else {
point.causes.insert(PanicCause::Unknown);
}
}
}
}
pub fn filter_allowed_causes(
points: &mut Vec<CrateCodePoint>,
config: &Config,
project_context: &ProjectContext,
) {
use crate::inline_allows::check_inline_allow;
let workspace_root = Some(std::path::Path::new(project_context.project_root()));
points.retain_mut(|point| {
point.causes.retain(|cause| {
let cause_id = cause.id();
if check_inline_allow(&point.file, point.line, cause_id, workspace_root) {
return false;
}
if !config.is_denied_at(cause, Some(&point.file), Some(&point.name)) {
return false;
}
if let Some(ref called_fn) = point.called_function {
if !config.is_denied_at(cause, Some(&point.file), Some(called_fn)) {
return false;
}
}
true
});
let should_keep = !point.causes.is_empty();
if should_keep {
filter_allowed_causes(&mut point.children, config, project_context);
}
should_keep
});
}
#[derive(Debug, Clone)]
pub struct AnalysisResult {
pub project_name: String,
pub project_root: String,
pub code_points: Vec<CrateCodePoint>,
}
impl AnalysisResult {
pub fn new(
project_name: impl Into<String>,
project_root: impl Into<String>,
code_points: Vec<CrateCodePoint>,
) -> Self {
Self {
project_name: project_name.into(),
project_root: project_root.into(),
code_points,
}
}
pub fn summary(&self) -> AnalysisSummary {
count_crate_points_and_files(&self.code_points)
}
pub fn panic_points(&self) -> usize {
self.summary().panic_points()
}
}
#[derive(Debug, Default, Clone)]
pub struct AnalysisSummary {
points: HashSet<(String, u32)>,
files: HashSet<String>,
}
impl AnalysisSummary {
pub fn from_points(points: HashSet<(String, u32)>, files: HashSet<String>) -> Self {
Self { points, files }
}
pub fn add(&mut self, other: &AnalysisSummary) {
self.points.extend(other.points.iter().cloned());
self.files.extend(other.files.iter().cloned());
}
pub fn panic_points(&self) -> usize {
self.points.len()
}
pub fn files_affected(&self) -> usize {
self.files.len()
}
}
fn count_crate_points_and_files(points: &[CrateCodePoint]) -> AnalysisSummary {
let mut seen_points = HashSet::new();
let mut seen_files = HashSet::new();
collect_unique_point_keys_and_files(points, &mut seen_points, &mut seen_files);
AnalysisSummary {
points: seen_points,
files: seen_files,
}
}
fn collect_unique_point_keys_and_files(
points: &[CrateCodePoint],
seen_points: &mut HashSet<(String, u32)>,
seen_files: &mut HashSet<String>,
) {
for p in points {
seen_points.insert((p.file.clone(), p.line));
seen_files.insert(p.file.clone());
collect_unique_point_keys_and_files(&p.children, seen_points, seen_files);
}
}
fn dedupe_crate_points(points: &mut Vec<CrateCodePoint>) {
let mut seen: HashMap<(String, u32), usize> = HashMap::new();
let mut result: Vec<CrateCodePoint> = Vec::new();
for point in points.drain(..) {
let key = (point.file.clone(), point.line);
if let Some(&idx) = seen.get(&key) {
result[idx].children.extend(point.children);
} else {
seen.insert(key, result.len());
result.push(point);
}
}
for point in &mut result {
dedupe_crate_points(&mut point.children);
}
*points = result;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_qualified_function_name() {
assert_eq!(
extract_qualified_function_name("my_crate::TimeStamp::now"),
"my_crate::TimeStamp::now"
);
assert_eq!(
extract_qualified_function_name("std::collections::HashMap::new"),
"std::collections::HashMap::new"
);
assert_eq!(
extract_qualified_function_name("my_crate::module::init"),
"my_crate::module::init"
);
assert_eq!(
extract_qualified_function_name("simple_function"),
"simple_function"
);
assert_eq!(
extract_qualified_function_name(
"_ZN3std11collections4hash3set16HashSet$LT$T$GT$3new17ha7a7fdf7dbcd659dE"
),
"std::collections::hash::set::HashSet::new"
);
assert_eq!(
extract_qualified_function_name("Option::unwrap"),
"Option::unwrap"
);
assert_eq!(
extract_qualified_function_name("h2::client::send"),
"h2::client::send"
);
}
#[test]
fn test_filter_allows_called_function_rule() {
use crate::config::Config;
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let toml_path = dir.path().join("jonesy.toml");
let mut f = std::fs::File::create(&toml_path).unwrap();
writeln!(
f,
"[[rules]]\nfunction = \"my_crate::time::TimeStamp::now\"\nallow = [\"unwrap\"]"
)
.unwrap();
let mut config = Config::with_defaults();
config.load_from_jones_toml(&toml_path);
let mut points = vec![CrateCodePoint {
name: "app::run".to_string(),
file: "src/main.rs".to_string(),
line: 42,
column: Some(5),
causes: vec![PanicCause::Unwrap].into_iter().collect(),
children: vec![],
is_direct_panic: false,
called_function: Some("my_crate::time::TimeStamp::now".to_string()),
}];
let ctx = ProjectContext::default();
filter_allowed_causes(&mut points, &config, &ctx);
assert!(
points.is_empty(),
"Point should be filtered out by called-function allow rule, but found: {points:?}"
);
}
#[test]
fn test_filter_global_allow_capacity_single_cause() {
use crate::config::Config;
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let toml_path = dir.path().join("jonesy.toml");
let mut f = std::fs::File::create(&toml_path).unwrap();
writeln!(f, "allow = [\"capacity\"]").unwrap();
let mut config = Config::with_defaults();
config.load_from_jones_toml(&toml_path);
let mut points = vec![CrateCodePoint {
name: "meshchat::device::DeviceIdentifier::hash".to_string(),
file: "src/device.rs".to_string(),
line: 74,
column: Some(22),
causes: vec![PanicCause::CapacityOverflow].into_iter().collect(),
children: vec![],
is_direct_panic: false,
called_function: Some("core::hash::Hash::hash".to_string()),
}];
let ctx = ProjectContext::default();
filter_allowed_causes(&mut points, &config, &ctx);
assert!(
points.is_empty(),
"Point should be filtered out by global allow for 'capacity', but found: {points:?}"
);
}
#[test]
fn test_filter_global_allow_capacity_with_other_cause_remaining() {
use crate::config::Config;
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let toml_path = dir.path().join("jonesy.toml");
let mut f = std::fs::File::create(&toml_path).unwrap();
writeln!(f, "allow = [\"capacity\"]").unwrap();
let mut config = Config::with_defaults();
config.load_from_jones_toml(&toml_path);
let mut points = vec![CrateCodePoint {
name: "meshchat::device::DeviceIdentifier::hash".to_string(),
file: "src/device.rs".to_string(),
line: 74,
column: Some(22),
causes: vec![PanicCause::CapacityOverflow, PanicCause::Unknown]
.into_iter()
.collect(),
children: vec![],
is_direct_panic: false,
called_function: Some("core::hash::Hash::hash".to_string()),
}];
let ctx = ProjectContext::default();
filter_allowed_causes(&mut points, &config, &ctx);
assert_eq!(
points.len(),
1,
"Point should remain because Unknown cause is not allowed"
);
assert!(
!points[0].causes.contains(&PanicCause::CapacityOverflow),
"CapacityOverflow should have been removed"
);
assert!(
points[0].causes.contains(&PanicCause::Unknown),
"Unknown should remain"
);
}
#[test]
fn test_filter_keeps_non_matching_called_function() {
use crate::config::Config;
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let toml_path = dir.path().join("jonesy.toml");
let mut f = std::fs::File::create(&toml_path).unwrap();
writeln!(
f,
"[[rules]]\nfunction = \"my_crate::time::TimeStamp::now\"\nallow = [\"unwrap\"]"
)
.unwrap();
let mut config = Config::with_defaults();
config.load_from_jones_toml(&toml_path);
let mut points = vec![CrateCodePoint {
name: "app::run".to_string(),
file: "src/main.rs".to_string(),
line: 42,
column: Some(5),
causes: vec![PanicCause::Unwrap].into_iter().collect(),
children: vec![],
is_direct_panic: false,
called_function: Some("other_crate::Config::parse".to_string()),
}];
let ctx = ProjectContext::default();
filter_allowed_causes(&mut points, &config, &ctx);
assert_eq!(
points.len(),
1,
"Point should be kept - rule doesn't match different called function"
);
}
#[test]
fn test_assign_unknown_causes_non_leaf() {
let mut points = vec![CrateCodePoint {
name: "app::run".to_string(),
file: "src/main.rs".to_string(),
line: 10,
column: Some(1),
causes: HashSet::new(),
children: vec![CrateCodePoint {
name: "app::helper".to_string(),
file: "src/main.rs".to_string(),
line: 20,
column: Some(1),
causes: HashSet::new(),
children: vec![],
is_direct_panic: false,
called_function: None,
}],
is_direct_panic: false,
called_function: None,
}];
assign_unknown_causes(&mut points);
assert!(
points[0].causes.contains(&PanicCause::Unknown),
"Non-leaf point with empty causes should get Unknown assigned"
);
assert!(
points[0].children[0].causes.contains(&PanicCause::Unknown),
"Leaf child with empty causes should get Unknown assigned"
);
}
#[test]
fn test_filter_unknown_cause_with_allow() {
use crate::config::Config;
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let toml_path = dir.path().join("jonesy.toml");
let mut f = std::fs::File::create(&toml_path).unwrap();
writeln!(f, "allow = [\"unknown\"]").unwrap();
let mut config = Config::with_defaults();
config.load_from_jones_toml(&toml_path);
let mut points = vec![CrateCodePoint {
name: "app::run".to_string(),
file: "src/main.rs".to_string(),
line: 42,
column: Some(5),
causes: vec![PanicCause::Unknown].into_iter().collect(),
children: vec![],
is_direct_panic: false,
called_function: None,
}];
let ctx = ProjectContext::default();
filter_allowed_causes(&mut points, &config, &ctx);
assert!(
points.is_empty(),
"Point with Unknown cause should be filtered out by allow = [\"unknown\"]"
);
}
}