use std::collections::{HashMap, HashSet, VecDeque};
use std::path::{Path, PathBuf};
use ra_ap_hir::{InFile, Semantics};
use ra_ap_ide::{
Analysis, CallHierarchyConfig, FilePosition, FileRange, RaFixtureConfig, RootDatabase,
};
use ra_ap_syntax::{AstNode, TextRange, ast};
use crate::deep::{DeepContext, DeepError, FileId};
use crate::functions::walk_functions;
use crate::ingest::{EntryPointKind, SourceKind, Workspace};
#[derive(Debug)]
pub enum ReachabilityError {
Deep(DeepError),
Io(PathBuf, std::io::Error),
Parse(PathBuf, syn::Error),
UnknownItem(String),
AmbiguousItem(String, Vec<(PathBuf, usize)>),
}
impl std::fmt::Display for ReachabilityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Deep(err) => write!(f, "{err}"),
Self::Io(path, err) => crate::rules::dead_code::fmt_io_error(f, path, err),
Self::Parse(path, err) => crate::rules::dead_code::fmt_parse_error(f, path, err),
Self::UnknownItem(item_path) => {
write!(f, "no function named `{item_path}` found in the workspace")
}
Self::AmbiguousItem(item_path, candidates) => {
writeln!(
f,
"`{item_path}` matches more than one function — prefix it with `<crate-name>::` to disambiguate:"
)?;
for (index, (path, line)) in candidates.iter().enumerate() {
if index > 0 {
writeln!(f)?;
}
write!(f, " {}:{line}", path.display())?;
}
Ok(())
}
}
}
}
impl std::error::Error for ReachabilityError {}
#[derive(Debug, Clone)]
pub struct PathStep {
pub qualified_name: String,
pub file: PathBuf,
pub line: usize,
pub kind: Option<CallKind>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallKind {
Static,
Dynamic,
Macro,
Generated,
Unknown,
}
impl CallKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Static => "static",
Self::Dynamic => "dynamic",
Self::Macro => "macro",
Self::Generated => "generated",
Self::Unknown => "unknown",
}
}
}
impl std::fmt::Display for CallKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
fn classify_call_kind(
sema: &Semantics<'_, RootDatabase>,
db: &RootDatabase,
file_id: FileId,
range: TextRange,
) -> CallKind {
let source_file = sema.parse_guess_edition(file_id);
let syntax = source_file.syntax();
let Some(token) = syntax
.token_at_offset(range.start())
.find(|token| token.text_range() == range)
else {
return CallKind::Unknown;
};
let editioned_file_id = sema.attach_first_edition(file_id);
if sema.is_inside_macro_call(InFile::new(editioned_file_id.into(), &token)) {
return CallKind::Macro;
}
let Some(name_ref) = token.parent().and_then(ast::NameRef::cast) else {
return CallKind::Unknown;
};
for ancestor in name_ref.syntax().ancestors() {
if let Some(method_call) = ast::MethodCallExpr::cast(ancestor.clone()) {
let is_method_name = method_call
.name_ref()
.is_some_and(|name_ref| name_ref.syntax().text_range() == range);
if !is_method_name {
return CallKind::Unknown;
}
let is_dynamic = method_call
.receiver()
.and_then(|receiver| sema.type_of_expr(&receiver))
.is_some_and(|info| info.original.strip_references().as_dyn_trait().is_some());
return if is_dynamic {
CallKind::Dynamic
} else {
CallKind::Static
};
}
if let Some(call_expr) = ast::CallExpr::cast(ancestor.clone()) {
let is_our_callee = call_expr
.expr()
.is_some_and(|callee| callee.syntax().text_range().contains_range(range));
if !is_our_callee {
return CallKind::Unknown;
}
return call_expr
.expr()
.and_then(|callee| sema.type_of_expr(&callee))
.and_then(|info| info.original.as_callable(db))
.map_or(CallKind::Unknown, |callable| match callable.kind() {
ra_ap_hir::CallableKind::Function(_)
| ra_ap_hir::CallableKind::TupleStruct(_)
| ra_ap_hir::CallableKind::TupleEnumVariant(_) => CallKind::Static,
ra_ap_hir::CallableKind::Closure(_)
| ra_ap_hir::CallableKind::FnPtr
| ra_ap_hir::CallableKind::FnImpl(_) => CallKind::Dynamic,
});
}
}
CallKind::Unknown
}
#[derive(Debug)]
pub enum WhyLive {
Path(Vec<PathStep>),
NotReachable,
}
pub(crate) fn has_attr_ending_in(attrs: &[syn::Attribute], ident: &str) -> bool {
fn meta_ends_in(meta: &syn::Meta, ident: &str) -> bool {
if let syn::Meta::List(list) = meta
&& list.path.is_ident("unsafe")
{
return list
.parse_args::<syn::Meta>()
.is_ok_and(|inner| meta_ends_in(&inner, ident));
}
meta.path()
.segments
.last()
.is_some_and(|segment| segment.ident == ident)
}
attrs.iter().any(|attr| meta_ends_in(&attr.meta, ident))
}
fn walk_crate_functions(
krate: &crate::ingest::CrateInfo,
ctx: &DeepContext,
mut on_site: impl FnMut(FileId, &Path, crate::functions::FunctionSite<'_>),
) -> Result<(), ReachabilityError> {
for file in &krate.source_files {
let Some(file_id) = ctx.file_id(&file.path).map_err(ReachabilityError::Deep)? else {
continue;
};
let source = std::fs::read_to_string(&file.path)
.map_err(|err| ReachabilityError::Io(file.path.clone(), err))?;
let ast = syn::parse_file(&source)
.map_err(|err| ReachabilityError::Parse(file.path.clone(), err))?;
walk_functions(&ast, |site| on_site(file_id, &file.path, site));
}
Ok(())
}
pub(crate) fn entry_point_positions(
workspace: &Workspace,
ctx: &DeepContext,
include_tests: bool,
) -> Result<Vec<(String, FilePosition)>, ReachabilityError> {
let mut entries = Vec::new();
for krate in &workspace.crates {
let mut bin_or_example_name: HashMap<FileId, &str> = HashMap::new();
for entry in krate
.entry_points
.iter()
.filter(|entry| matches!(entry.kind, EntryPointKind::Bin | EntryPointKind::Example))
{
if let Some(file_id) = ctx.file_id(&entry.path).map_err(ReachabilityError::Deep)? {
bin_or_example_name.insert(file_id, entry.name.as_str());
}
}
walk_crate_functions(krate, ctx, |file_id, _path, site| {
let offset = site.ident_span.byte_range().start as u32;
let position = FilePosition {
file_id,
offset: offset.into(),
};
if let Some(entry_name) = bin_or_example_name.get(&file_id)
&& site.qualified_name == "main"
{
entries.push((format!("{entry_name}::main"), position));
return;
}
let recognized = (include_tests
&& (has_attr_ending_in(site.attrs, "test")
|| has_attr_ending_in(site.attrs, "bench")))
|| has_attr_ending_in(site.attrs, "no_mangle")
|| has_attr_ending_in(site.attrs, "export_name")
|| has_attr_ending_in(site.attrs, "wasm_bindgen");
if recognized {
entries.push((format!("{}::{}", krate.name, site.qualified_name), position));
}
})?;
}
Ok(entries)
}
pub(crate) fn entry_keys_from(entries: &[(String, FilePosition)]) -> HashSet<(FileId, u32)> {
entries
.iter()
.map(|(_, position)| position_key(*position))
.collect()
}
fn find_item_position(
workspace: &Workspace,
ctx: &DeepContext,
item_path: &str,
) -> Result<FilePosition, ReachabilityError> {
let (crates, name) = match item_path.split_once("::") {
Some((prefix, rest)) if workspace.crates.iter().any(|krate| krate.name == prefix) => {
let crates: Vec<_> = workspace
.crates
.iter()
.filter(|krate| krate.name == prefix)
.collect();
(crates, rest)
}
_ => (workspace.crates.iter().collect(), item_path),
};
let mut matches: Vec<(FilePosition, PathBuf, usize)> = Vec::new();
for krate in crates {
walk_crate_functions(krate, ctx, |file_id, path, site| {
if site.qualified_name == name {
let offset = site.ident_span.byte_range().start as u32;
matches.push((
FilePosition {
file_id,
offset: offset.into(),
},
path.to_path_buf(),
site.ident_span.start().line,
));
}
})?;
}
matches.sort_by(|(_, path_a, line_a), (_, path_b, line_b)| {
(path_a, line_a).cmp(&(path_b, line_b))
});
match matches.len() {
0 => Err(ReachabilityError::UnknownItem(item_path.to_string())),
1 => Ok(matches.into_iter().next().unwrap().0),
_ => Err(ReachabilityError::AmbiguousItem(
item_path.to_string(),
matches
.into_iter()
.map(|(_, path, line)| (path, line))
.collect(),
)),
}
}
pub(crate) fn position_key(position: FilePosition) -> (FileId, u32) {
(position.file_id, position.offset.into())
}
fn deterministic_callers(
analysis: &Analysis,
config: &CallHierarchyConfig<'_>,
position: FilePosition,
) -> Result<Vec<(FilePosition, String, Vec<FileRange>)>, ReachabilityError> {
let callers = analysis
.incoming_calls(config, position)
.map_err(|err| ReachabilityError::Deep(DeepError::Cancelled(format!("{err:?}"))))?
.ok_or_else(|| {
ReachabilityError::Deep(DeepError::UnresolvedSymbol(crate::deep::describe_position(
position,
)))
})?;
let mut callers: Vec<_> = callers
.into_iter()
.map(|call_item| {
let focus = call_item
.target
.focus_range
.unwrap_or(call_item.target.full_range);
let caller_position = FilePosition {
file_id: call_item.target.file_id,
offset: focus.start(),
};
let mut ranges = call_item.ranges;
ranges.sort_by_key(|range| (range.file_id, range.range.start()));
(
caller_position,
call_item.target.name.as_str().to_string(),
ranges,
)
})
.collect();
callers.sort_by_key(|(position, _, _)| position_key(*position));
Ok(callers)
}
pub(crate) fn is_reachable_from_entry(
analysis: &Analysis,
entry_keys: &std::collections::HashSet<(FileId, u32)>,
target: FilePosition,
include_tests: bool,
) -> Result<bool, ReachabilityError> {
if entry_keys.contains(&position_key(target)) {
return Ok(true);
}
let config = CallHierarchyConfig {
exclude_tests: !include_tests,
ra_fixture: RaFixtureConfig::default(),
};
let mut visited = HashSet::from([position_key(target)]);
let mut queue = VecDeque::from([target]);
while let Some(position) = queue.pop_front() {
for (caller_position, _name, _ranges) in deterministic_callers(analysis, &config, position)?
{
let key = position_key(caller_position);
if entry_keys.contains(&key) {
return Ok(true);
}
if visited.insert(key) {
queue.push_back(caller_position);
}
}
}
Ok(false)
}
pub fn why_live(
workspace: &Workspace,
item_path: &str,
include_tests: bool,
) -> Result<WhyLive, ReachabilityError> {
let ctx = DeepContext::load(&workspace.root).map_err(ReachabilityError::Deep)?;
let analysis = ctx.analysis();
let db = ctx.raw_database();
let sema = Semantics::new(db);
let mut file_source_kind: HashMap<FileId, SourceKind> = HashMap::new();
for file in workspace
.crates
.iter()
.flat_map(|krate| &krate.source_files)
{
if let Some(file_id) = ctx.file_id(&file.path).map_err(ReachabilityError::Deep)? {
file_source_kind.insert(file_id, file.kind);
}
}
let entries = entry_point_positions(workspace, &ctx, include_tests)?;
let entry_keys = entry_keys_from(&entries);
let target = find_item_position(workspace, &ctx, item_path)?;
let (target_file, target_line) = describe_position(workspace, &ctx, target);
if entry_keys.contains(&position_key(target)) {
return Ok(WhyLive::Path(vec![PathStep {
qualified_name: item_path.to_string(),
file: target_file,
line: target_line,
kind: None,
}]));
}
let config = CallHierarchyConfig {
exclude_tests: !include_tests,
ra_fixture: RaFixtureConfig::default(),
};
let mut visited = HashSet::from([position_key(target)]);
let initial_path = vec![PathStep {
qualified_name: item_path.to_string(),
file: target_file,
line: target_line,
kind: None,
}];
let mut queue: VecDeque<(FilePosition, Vec<PathStep>)> =
VecDeque::from([(target, initial_path)]);
while let Some((position, path_so_far)) = queue.pop_front() {
let callers = deterministic_callers(&analysis, &config, position)?;
for (caller_position, caller_name, ranges) in callers {
let key = position_key(caller_position);
if !visited.insert(key) {
continue;
}
let kind =
if file_source_kind.get(&caller_position.file_id) == Some(&SourceKind::Generated) {
CallKind::Generated
} else if let Some(call_site) = ranges.first() {
classify_call_kind(&sema, db, call_site.file_id, call_site.range)
} else {
CallKind::Unknown
};
let (caller_file, caller_line) = describe_position(workspace, &ctx, caller_position);
let mut new_path = path_so_far.clone();
new_path.push(PathStep {
qualified_name: caller_name,
file: caller_file,
line: caller_line,
kind: Some(kind),
});
if entry_keys.contains(&key) {
return Ok(WhyLive::Path(new_path));
}
queue.push_back((caller_position, new_path));
}
}
Ok(WhyLive::NotReachable)
}
fn describe_position(
workspace: &Workspace,
ctx: &DeepContext,
position: FilePosition,
) -> (PathBuf, usize) {
for krate in &workspace.crates {
for file in &krate.source_files {
match ctx.file_id(&file.path) {
Ok(Some(file_id)) if file_id == position.file_id => {}
_ => continue,
}
let line = std::fs::read_to_string(&file.path)
.ok()
.map(|source| {
let offset = usize::from(position.offset).min(source.len());
source[..offset].matches('\n').count() + 1
})
.unwrap_or(0);
return (file.path.clone(), line);
}
}
(PathBuf::from("<unknown>"), 0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::TempDir;
fn load_bin_workspace(dir: &TempDir, lib_source: &str, bin_source: &str) -> Workspace {
load_bin_workspace_with_edition(dir, "2021", lib_source, bin_source)
}
fn load_bin_workspace_with_edition(
dir: &TempDir,
edition: &str,
lib_source: &str,
bin_source: &str,
) -> Workspace {
std::fs::write(
dir.join("Cargo.toml"),
format!(
r#"
[package]
name = "why-live-fixture"
version = "0.1.0"
edition = "{edition}"
"#
),
)
.unwrap();
std::fs::create_dir_all(dir.join("src/bin")).unwrap();
std::fs::write(dir.join("src/lib.rs"), lib_source).unwrap();
std::fs::write(dir.join("src/bin/tool.rs"), bin_source).unwrap();
crate::ingest::load(Some(&dir.join("Cargo.toml"))).unwrap()
}
#[test]
fn finds_a_path_from_main_through_an_intermediate_call() {
let dir = TempDir::new("why-live-reachable");
let workspace = load_bin_workspace(
&dir,
r#"pub fn deep_helper() -> i32 {
1
}
pub fn middle() -> i32 {
deep_helper()
}
"#,
r#"fn main() {
why_live_fixture::middle();
}
"#,
);
let result = why_live(&workspace, "deep_helper", true).unwrap();
let WhyLive::Path(path) = result else {
panic!("expected a path, got NotReachable");
};
let names: Vec<_> = path
.iter()
.map(|step| step.qualified_name.as_str())
.collect();
assert_eq!(names.first(), Some(&"deep_helper"));
assert!(names.contains(&"middle"));
assert_eq!(names.last(), Some(&"main"));
assert_eq!(
path[0].kind, None,
"the target's own step has no incoming edge"
);
assert!(
path[1..]
.iter()
.all(|step| step.kind == Some(CallKind::Static)),
"every call in this fixture is a direct static call: {:?}",
path.iter().map(|s| s.kind).collect::<Vec<_>>()
);
}
#[test]
fn reports_not_reachable_for_truly_dead_code() {
let dir = TempDir::new("why-live-dead");
let workspace = load_bin_workspace(
&dir,
r#"pub fn never_called() -> i32 {
1
}
"#,
r#"fn main() {}
"#,
);
let result = why_live(&workspace, "never_called", true).unwrap();
assert!(matches!(result, WhyLive::NotReachable));
}
#[test]
fn unknown_item_path_is_an_error() {
let dir = TempDir::new("why-live-unknown");
let workspace = load_bin_workspace(&dir, "", "fn main() {}\n");
let err = why_live(&workspace, "does_not_exist", true).unwrap_err();
assert!(matches!(err, ReachabilityError::UnknownItem(_)));
}
#[test]
fn a_test_function_is_a_recognized_entry_point_when_include_tests_is_set() {
let dir = TempDir::new("why-live-test-entry");
let workspace = load_bin_workspace(
&dir,
r#"pub fn helper() -> i32 {
1
}
#[test]
fn a_test() {
helper();
}
"#,
"fn main() {}\n",
);
let result = why_live(&workspace, "helper", true).unwrap();
let WhyLive::Path(path) = result else {
panic!("expected a path via the test function, got NotReachable");
};
assert_eq!(
path.last().map(|step| step.qualified_name.as_str()),
Some("a_test")
);
let result = why_live(&workspace, "helper", false).unwrap();
assert!(
matches!(result, WhyLive::NotReachable),
"a #[test] function must not count as an entry point in production-only mode"
);
}
#[test]
fn a_no_mangle_function_is_an_entry_point_regardless_of_include_tests() {
let dir = TempDir::new("why-live-no-mangle-entry");
let workspace = load_bin_workspace(
&dir,
r#"pub fn helper() -> i32 {
1
}
#[no_mangle]
pub extern "C" fn exported() -> i32 {
helper()
}
"#,
"fn main() {}\n",
);
let result = why_live(&workspace, "helper", false).unwrap();
let WhyLive::Path(path) = result else {
panic!("expected a path via the #[no_mangle] export, got NotReachable");
};
assert_eq!(
path.last().map(|step| step.qualified_name.as_str()),
Some("exported")
);
}
#[test]
fn an_unsafe_wrapped_export_attribute_is_an_entry_point_regardless_of_include_tests() {
let dir = TempDir::new("why-live-unsafe-attr-entry");
let workspace = load_bin_workspace_with_edition(
&dir,
"2024",
r#"pub fn helper_a() -> i32 {
1
}
pub fn helper_b() -> i32 {
2
}
#[unsafe(no_mangle)]
pub extern "C" fn exported_a() -> i32 {
helper_a()
}
#[unsafe(export_name = "exported_b_symbol")]
pub extern "C" fn exported_b() -> i32 {
helper_b()
}
"#,
"fn main() {}\n",
);
let result = why_live(&workspace, "helper_a", false).unwrap();
let WhyLive::Path(path) = result else {
panic!("expected a path via the #[unsafe(no_mangle)] export, got NotReachable");
};
assert_eq!(
path.last().map(|step| step.qualified_name.as_str()),
Some("exported_a")
);
let result = why_live(&workspace, "helper_b", false).unwrap();
let WhyLive::Path(path) = result else {
panic!("expected a path via the #[unsafe(export_name = ...)] export, got NotReachable");
};
assert_eq!(
path.last().map(|step| step.qualified_name.as_str()),
Some("exported_b")
);
}
#[test]
fn a_call_through_a_dyn_trait_object_is_classified_as_dynamic() {
let dir = TempDir::new("why-live-dyn-dispatch");
let workspace = load_bin_workspace(
&dir,
r#"pub trait Greet {
fn hi(&self) -> i32 {
1
}
}
pub struct Impl;
// Doesn't override `hi` — the call below dispatches to the trait's own
// default method, so `Greet::hi`'s definition is exactly what a `dyn Greet`
// method call resolves to (an overriding impl method would instead be a
// distinct, statically-unreachable-from-here definition).
impl Greet for Impl {}
pub fn dispatch(g: &dyn Greet) -> i32 {
g.hi()
}
"#,
r#"fn main() {
why_live_fixture::dispatch(&why_live_fixture::Impl);
}
"#,
);
let result = why_live(&workspace, "Greet::hi", true).unwrap();
let WhyLive::Path(path) = result else {
panic!("expected a path, got NotReachable");
};
let dispatch_step = path
.iter()
.find(|step| step.qualified_name == "dispatch")
.expect("dispatch must appear in the path");
assert_eq!(dispatch_step.kind, Some(CallKind::Dynamic));
let main_step = path
.iter()
.find(|step| step.qualified_name == "main")
.expect("main must appear in the path");
assert_eq!(
main_step.kind,
Some(CallKind::Static),
"`dispatch(...)` itself is called directly, not through a trait object"
);
}
#[test]
fn a_call_inside_a_macro_invocation_is_classified_as_macro() {
let dir = TempDir::new("why-live-macro-call");
let workspace = load_bin_workspace(
&dir,
r#"macro_rules! call_it {
($func:ident) => {
$func()
};
}
pub fn helper() -> i32 {
1
}
pub fn caller() -> i32 {
call_it!(helper)
}
"#,
r#"fn main() {
why_live_fixture::caller();
}
"#,
);
let result = why_live(&workspace, "helper", true).unwrap();
let WhyLive::Path(path) = result else {
panic!("expected a path, got NotReachable");
};
let caller_step = path
.iter()
.find(|step| step.qualified_name == "caller")
.expect("caller must appear in the path");
assert_eq!(caller_step.kind, Some(CallKind::Macro));
}
#[test]
fn a_call_in_a_generated_file_is_classified_as_generated() {
let dir = TempDir::new("why-live-generated-call");
std::fs::write(
dir.join("Cargo.toml"),
r#"
[package]
name = "why-live-fixture"
version = "0.1.0"
edition = "2021"
"#,
)
.unwrap();
std::fs::create_dir_all(dir.join("src/bin")).unwrap();
std::fs::write(
dir.join("src/lib.rs"),
"pub mod generated;\n\npub fn helper() -> i32 {\n 1\n}\n",
)
.unwrap();
std::fs::write(
dir.join("src/bin/tool.rs"),
"fn main() {\n why_live_fixture::generated::from_generated();\n}\n",
)
.unwrap();
std::fs::write(
dir.join("src/generated.rs"),
"// @generated\npub fn from_generated() -> i32 {\n crate::helper()\n}\n",
)
.unwrap();
let workspace = crate::ingest::load(Some(&dir.join("Cargo.toml"))).unwrap();
let result = why_live(&workspace, "helper", true).unwrap();
let WhyLive::Path(path) = result else {
panic!("expected a path via the generated file, got NotReachable");
};
let generated_step = path
.iter()
.find(|step| step.qualified_name == "from_generated")
.expect("from_generated must appear in the path");
assert_eq!(generated_step.kind, Some(CallKind::Generated));
}
#[test]
fn an_item_path_matching_two_files_is_reported_as_ambiguous() {
let dir = TempDir::new("why-live-ambiguous");
let workspace = load_bin_workspace(
&dir,
"pub fn helper() -> i32 {\n 1\n}\n",
"pub fn helper() -> i32 {\n 2\n}\n\nfn main() {}\n",
);
let err = why_live(&workspace, "helper", true).unwrap_err();
let ReachabilityError::AmbiguousItem(item, candidates) = err else {
panic!("expected AmbiguousItem, got {err:?}");
};
assert_eq!(item, "helper");
assert_eq!(candidates.len(), 2);
}
fn write_crate_with_helper(dir: &TempDir, name: &str, return_value: i32) {
std::fs::create_dir_all(dir.join(name).join("src")).unwrap();
std::fs::write(
dir.join(name).join("Cargo.toml"),
format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"),
)
.unwrap();
std::fs::write(
dir.join(name).join("src/lib.rs"),
format!("pub fn helper() -> i32 {{\n {return_value}\n}}\n"),
)
.unwrap();
}
#[test]
fn a_crate_name_prefix_disambiguates_an_item_path_shared_across_crates() {
let dir = TempDir::new("why-live-crate-prefix");
write_crate_with_helper(&dir, "crate_a", 1);
write_crate_with_helper(&dir, "crate_b", 2);
std::fs::write(
dir.join("Cargo.toml"),
r#"[workspace]
members = ["crate_a", "crate_b"]
resolver = "2"
"#,
)
.unwrap();
let workspace = crate::ingest::load(Some(&dir.join("Cargo.toml"))).unwrap();
let err = why_live(&workspace, "helper", true).unwrap_err();
assert!(matches!(err, ReachabilityError::AmbiguousItem(_, _)));
let result = why_live(&workspace, "crate_a::helper", true).unwrap();
assert!(
matches!(result, WhyLive::NotReachable),
"crate-prefixed lookup must resolve uniquely instead of erroring"
);
}
#[test]
fn a_fn_reachable_only_via_a_ctor_style_registration_attribute_is_reported_not_reachable() {
let dir = TempDir::new("why-live-ctor-registration");
let workspace = load_bin_workspace(
&dir,
r#"pub fn helper() -> i32 {
1
}
#[ctor::ctor]
fn runs_at_startup() {
helper();
}
"#,
"fn main() {}\n",
);
let result = why_live(&workspace, "helper", true).unwrap();
assert!(
matches!(result, WhyLive::NotReachable),
"a ctor-style registration attribute is a documented blind spot, not a recognized \
entry point — `helper` is genuinely live at runtime but this analysis cannot see \
it: {result:?}"
);
}
}