use crate::model::EdgeType;
use crate::storage::capability::Storage;
use crate::storage::error::Result as StorageResult;
use crate::storage::escape_cypher_string;
use serde::{Deserialize, Serialize};
const DEFAULT_TEST_PATTERNS: &[&str] = &[
"test_*", "*_test", "*_spec", "it_*", "sec_*", "snap_*", "perf_*", "bench_*",
];
const DEFAULT_ENTRY_PATTERNS: &[&str] =
&["main", "Main", "__main__", "wmain", "WinMain", "DLLMain"];
const DEFAULT_ATTRIBUTE_ENTRIES: &[&str] = &[
"#[tool",
"#[forge",
"#[tokio::main",
"#[rocket::main",
"#[actix::main",
"#[axum::main",
];
const REASON_ZERO_INCOMING_CALLS: &str = "zero incoming CALLS edges";
#[derive(Debug, Clone)]
pub struct DeadCodeConfig {
pub entry_patterns: Vec<String>,
pub test_patterns: Vec<String>,
pub check_exported: bool,
pub check_dynamic_dispatch: bool,
pub check_reflection: bool,
pub check_ffi: bool,
pub attribute_entries: Vec<String>,
pub edge_types: Vec<EdgeType>,
}
impl Default for DeadCodeConfig {
fn default() -> Self {
Self {
entry_patterns: DEFAULT_ENTRY_PATTERNS
.iter()
.map(|s| (*s).to_string())
.collect(),
test_patterns: DEFAULT_TEST_PATTERNS
.iter()
.map(|s| (*s).to_string())
.collect(),
check_exported: true,
check_dynamic_dispatch: true,
check_reflection: false,
check_ffi: true,
attribute_entries: DEFAULT_ATTRIBUTE_ENTRIES
.iter()
.map(|s| (*s).to_string())
.collect(),
edge_types: vec![
EdgeType::Calls,
EdgeType::FfiCalls,
EdgeType::Implements,
EdgeType::HandlesRoute,
EdgeType::Usage,
EdgeType::Tests,
EdgeType::UsesType,
EdgeType::HttpCalls,
EdgeType::AsyncCalls,
],
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Confidence {
High,
Medium,
Low,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct DeadCodeEntry {
pub name: String,
pub qualified_name: String,
pub file_path: String,
pub start_line: u32,
pub language: String,
pub reason: String,
pub confidence: Confidence,
}
pub(crate) struct BatchPrefetch {
exported_ids: std::collections::HashSet<String>,
ffi_entry_ids: std::collections::HashSet<String>,
outgoing_edges: std::collections::HashMap<String, std::collections::HashSet<String>>,
reexport_target_ids: std::collections::HashSet<String>,
}
impl BatchPrefetch {
pub(crate) fn load(
storage: &dyn Storage,
project: &str,
config: &DeadCodeConfig,
functions: &[FunctionRow],
) -> StorageResult<Self> {
let mut exported_ids = std::collections::HashSet::new();
let mut ffi_entry_ids = std::collections::HashSet::new();
for func in functions {
if func.is_exported {
exported_ids.insert(func.id.clone());
}
if func.signature.contains(r#"extern "C""#) || func.signature.contains("#[no_mangle]") {
ffi_entry_ids.insert(func.id.clone());
}
}
let outgoing_edges = load_outgoing_edges(storage, project, &config.edge_types)?;
let reexport_target_ids = load_reexport_target_ids(storage, project)?;
Ok(Self {
exported_ids,
ffi_entry_ids,
outgoing_edges,
reexport_target_ids,
})
}
#[must_use]
pub(crate) fn is_exported(&self, id: &str) -> bool {
self.exported_ids.contains(id)
}
#[must_use]
pub(crate) fn is_ffi_entry(&self, id: &str) -> bool {
self.ffi_entry_ids.contains(id)
}
#[must_use]
pub(crate) fn is_reexport_target(&self, id: &str) -> bool {
self.reexport_target_ids.contains(id)
}
#[cfg(test)]
#[must_use]
pub(crate) fn exported_ids_len(&self) -> usize {
self.exported_ids.len()
}
#[cfg(test)]
#[must_use]
pub(crate) fn ffi_entry_ids_len(&self) -> usize {
self.ffi_entry_ids.len()
}
#[cfg(test)]
#[must_use]
pub(crate) fn reexport_target_ids_len(&self) -> usize {
self.reexport_target_ids.len()
}
#[must_use]
pub(crate) fn outgoing_edges(&self, id: &str) -> Option<&std::collections::HashSet<String>> {
self.outgoing_edges.get(id)
}
}
fn load_outgoing_edges(
storage: &dyn Storage,
project: &str,
edge_types: &[EdgeType],
) -> StorageResult<std::collections::HashMap<String, std::collections::HashSet<String>>> {
if edge_types.is_empty() {
return Ok(std::collections::HashMap::new());
}
let escaped = escape_cypher_string(project);
let in_list = edge_types
.iter()
.map(|t| format!("'{}'", t.as_db_type()))
.collect::<Vec<_>>()
.join(", ");
let cypher = format!(
"MATCH (e:CodeRelation) WHERE e.type IN [{in_list}] AND e.project = '{escaped}' \
RETURN e.source AS source, e.target AS target;"
);
let rows = storage.query(&cypher)?;
let mut map: std::collections::HashMap<String, std::collections::HashSet<String>> =
std::collections::HashMap::with_capacity(rows.len());
for row in rows {
if row.len() < 2 {
continue;
}
let source = row[0].as_str().unwrap_or_default().to_string();
let target = row[1].as_str().unwrap_or_default().to_string();
map.entry(source).or_default().insert(target);
}
Ok(map)
}
fn load_reexport_target_ids(
storage: &dyn Storage,
project: &str,
) -> StorageResult<std::collections::HashSet<String>> {
let escaped = escape_cypher_string(project);
let reexport_type = EdgeType::Reexports.as_db_type();
let cypher = format!(
"MATCH (e:CodeRelation) WHERE e.type = '{reexport_type}' AND e.project = '{escaped}' \
RETURN e.target AS target;"
);
let rows = storage.query(&cypher)?;
let mut set = std::collections::HashSet::with_capacity(rows.len());
for row in rows {
if row.is_empty() {
continue;
}
if let Some(s) = row[0].as_str().filter(|s| !s.is_empty()) {
set.insert(s.to_string());
}
}
Ok(set)
}
fn load_all_functions(storage: &dyn Storage, project: &str) -> StorageResult<Vec<FunctionRow>> {
let escaped = escape_cypher_string(project);
let function_cypher = format!(
"MATCH (n:Function) WHERE n.project = '{escaped}' \
RETURN n.id AS id, n.name AS name, n.qualifiedName AS qualified_name, \
n.filePath AS file_path, n.startLine AS start_line, \
n.isExported AS is_exported, n.signature AS signature;"
);
let method_cypher = format!(
"MATCH (n:Method) WHERE n.project = '{escaped}' \
RETURN n.id AS id, n.name AS name, n.qualifiedName AS qualified_name, \
n.filePath AS file_path, n.startLine AS start_line, \
n.isExported AS is_exported, n.signature AS signature;"
);
let mut out = Vec::new();
for cypher in [function_cypher, method_cypher] {
let rows = storage.query(&cypher)?;
for row in rows {
if row.len() < 7 {
continue;
}
let id = row[0].as_str().unwrap_or_default().to_string();
let name = row[1].as_str().unwrap_or_default().to_string();
let qualified_name = row[2].as_str().unwrap_or_default().to_string();
let file_path = row[3].as_str().unwrap_or_default().to_string();
let start_line = row[4]
.as_i64()
.map(|v| v as u32)
.or_else(|| row[4].as_u64().map(|v| v as u32))
.unwrap_or(0);
let is_exported = row[5].as_bool().unwrap_or(false);
let signature = row[6].as_str().unwrap_or_default().to_string();
out.push(FunctionRow {
id,
name,
qualified_name,
file_path,
start_line,
is_exported,
signature,
});
}
}
Ok(out)
}
pub(crate) struct ReachabilityAnalyzer<'a> {
prefetch: &'a BatchPrefetch,
config: &'a DeadCodeConfig,
functions: &'a [FunctionRow],
worklist: std::collections::VecDeque<String>,
live_set: std::collections::HashSet<String>,
}
impl<'a> ReachabilityAnalyzer<'a> {
#[must_use]
pub(crate) fn new(
prefetch: &'a BatchPrefetch,
config: &'a DeadCodeConfig,
functions: &'a [FunctionRow],
) -> Self {
Self {
prefetch,
config,
functions,
worklist: std::collections::VecDeque::new(),
live_set: std::collections::HashSet::new(),
}
}
pub(crate) fn collect_seeds(&mut self, entry_patterns: &[&str]) {
let config_entry_patterns: Vec<&str> = self
.config
.entry_patterns
.iter()
.map(|s| s.as_str())
.collect();
let config_test_patterns: Vec<&str> = self
.config
.test_patterns
.iter()
.map(|s| s.as_str())
.collect();
let config_attribute_entries: Vec<&str> = self
.config
.attribute_entries
.iter()
.map(|s| s.as_str())
.collect();
for func in self.functions {
if self.is_seed_function(
func,
entry_patterns,
&config_entry_patterns,
&config_test_patterns,
&config_attribute_entries,
) {
self.worklist.push_back(func.id.clone());
}
}
}
fn is_seed_function(
&self,
func: &FunctionRow,
entry_patterns: &[&str],
config_entry_patterns: &[&str],
config_test_patterns: &[&str],
attribute_entries: &[&str],
) -> bool {
if matches_any_pattern(&func.name, entry_patterns)
|| matches_any_pattern(&func.name, config_entry_patterns)
|| matches_any_pattern(&func.name, DEFAULT_ENTRY_PATTERNS)
{
return true;
}
if matches_any_pattern(&func.name, DEFAULT_TEST_PATTERNS)
|| matches_any_pattern(&func.name, config_test_patterns)
{
return true;
}
if is_test_module_function(&func.qualified_name) {
return true;
}
if is_integration_test_file(&func.file_path) {
return true;
}
if self.config.check_dynamic_dispatch && is_trait_impl_method(&func.qualified_name) {
return true;
}
if self.config.check_exported && self.prefetch.is_exported(&func.id) {
return true;
}
if self.config.check_ffi && self.prefetch.is_ffi_entry(&func.id) {
return true;
}
if self.prefetch.is_reexport_target(&func.id) {
return true;
}
if attribute_entries
.iter()
.any(|attr| func.signature.contains(attr))
{
return true;
}
false
}
pub(crate) fn propagate(&mut self) {
while let Some(id) = self.worklist.pop_front() {
if self.live_set.contains(&id) {
continue;
}
if let Some(targets) = self.prefetch.outgoing_edges(&id) {
for target in targets {
if !self.live_set.contains(target) {
self.worklist.push_back(target.clone());
}
}
}
self.live_set.insert(id);
}
}
#[must_use]
pub(crate) fn into_live_set(self) -> std::collections::HashSet<String> {
self.live_set
}
}
pub struct DeadCodeDetector<'a> {
storage: &'a dyn Storage,
config: DeadCodeConfig,
}
impl<'a> DeadCodeDetector<'a> {
#[must_use]
pub fn new(storage: &'a dyn Storage) -> Self {
Self::with_config(storage, DeadCodeConfig::default())
}
#[must_use]
pub fn with_config(storage: &'a dyn Storage, config: DeadCodeConfig) -> Self {
Self { storage, config }
}
pub fn detect(
&self,
project: &str,
entry_patterns: &[&str],
) -> StorageResult<Vec<DeadCodeEntry>> {
let functions = load_all_functions(self.storage, project)?;
let prefetch = BatchPrefetch::load(self.storage, project, &self.config, &functions)?;
let mut analyzer = ReachabilityAnalyzer::new(&prefetch, &self.config, &functions);
analyzer.collect_seeds(entry_patterns);
analyzer.propagate();
let live_set = analyzer.into_live_set();
let (calls_targets, non_calls_targets) = self.load_edge_targets_by_category(project)?;
let file_languages = self.load_file_languages(project)?;
let config_entry_patterns: Vec<&str> = self
.config
.entry_patterns
.iter()
.map(|s| s.as_str())
.collect();
let config_test_patterns: Vec<&str> = self
.config
.test_patterns
.iter()
.map(|s| s.as_str())
.collect();
let config_attribute_entries: Vec<&str> = self
.config
.attribute_entries
.iter()
.map(|s| s.as_str())
.collect();
let mut entries = Vec::new();
let reason_zero_incoming = REASON_ZERO_INCOMING_CALLS.to_string();
for func in &functions {
if live_set.contains(&func.id) {
continue;
}
if matches_any_pattern(&func.name, entry_patterns)
|| matches_any_pattern(&func.name, &config_entry_patterns)
{
continue;
}
if matches_any_pattern(&func.name, DEFAULT_TEST_PATTERNS)
|| matches_any_pattern(&func.name, &config_test_patterns)
{
continue;
}
if is_test_module_function(&func.qualified_name) {
continue;
}
if is_integration_test_file(&func.file_path) {
continue;
}
if self.config.check_exported && prefetch.is_exported(&func.id) {
continue;
}
if self.config.check_ffi && prefetch.is_ffi_entry(&func.id) {
continue;
}
if self.config.check_dynamic_dispatch && is_trait_impl_method(&func.qualified_name) {
continue;
}
if config_attribute_entries
.iter()
.any(|attr| func.signature.contains(attr))
{
continue;
}
let language = file_languages
.get(&func.file_path)
.cloned()
.unwrap_or_default();
let confidence = if calls_targets.contains(&func.id) {
Confidence::Low
} else if non_calls_targets.contains(&func.id) {
Confidence::Medium
} else {
Confidence::High
};
entries.push(DeadCodeEntry {
name: func.name.clone(),
qualified_name: func.qualified_name.clone(),
file_path: func.file_path.clone(),
start_line: func.start_line,
language,
reason: reason_zero_incoming.clone(),
confidence,
});
}
entries.sort_by(|a, b| a.qualified_name.cmp(&b.qualified_name));
Ok(entries)
}
#[cfg(test)]
fn load_referenced_ids(
&self,
project: &str,
) -> StorageResult<std::collections::HashSet<String>> {
if self.config.edge_types.is_empty() {
return Ok(std::collections::HashSet::new());
}
let escaped = escape_cypher_string(project);
let in_list = self
.config
.edge_types
.iter()
.map(|t| format!("'{}'", t.as_db_type()))
.collect::<Vec<_>>()
.join(", ");
let cypher = format!(
"MATCH (e:CodeRelation) WHERE e.type IN [{in_list}] AND e.project = '{escaped}' \
RETURN e.target AS target;"
);
let mut set = std::collections::HashSet::new();
let rows = self.storage.query(&cypher)?;
for row in rows {
if let Some(target) = row.first().and_then(|v| v.as_str()) {
set.insert(target.to_string());
}
}
Ok(set)
}
fn load_edge_targets_by_category(
&self,
project: &str,
) -> StorageResult<(
std::collections::HashSet<String>,
std::collections::HashSet<String>,
)> {
let escaped = escape_cypher_string(project);
let cypher = format!(
"MATCH (e:CodeRelation) WHERE e.project = '{escaped}' \
RETURN e.target AS target, e.type AS type;"
);
let rows = self.storage.query(&cypher)?;
let mut calls_targets = std::collections::HashSet::new();
let mut non_calls_targets = std::collections::HashSet::new();
for row in rows {
if row.len() < 2 {
continue;
}
let target = row[0].as_str().unwrap_or_default().to_string();
let edge_type = row[1].as_str().unwrap_or_default();
if edge_type == "CALLS" {
calls_targets.insert(target);
} else {
non_calls_targets.insert(target);
}
}
Ok((calls_targets, non_calls_targets))
}
#[allow(
dead_code,
reason = "exercised by unit tests; reserved for future diagnostics"
)]
fn has_incoming_edge(&self, func_id: &str, edge_type: EdgeType) -> StorageResult<bool> {
let escaped_id = escape_cypher_string(func_id);
let type_str = edge_type.as_db_type();
let cypher = format!(
"MATCH (e:CodeRelation) WHERE e.type = '{type_str}' AND e.target = '{escaped_id}' \
RETURN e.id AS id LIMIT 1;"
);
Ok(!self.storage.query(&cypher)?.is_empty())
}
fn load_file_languages(
&self,
project: &str,
) -> StorageResult<std::collections::HashMap<String, String>> {
let escaped = escape_cypher_string(project);
let cypher = format!(
"MATCH (f:File) WHERE f.project = '{escaped}' \
RETURN f.filePath AS file_path, f.language AS language;"
);
let rows = self.storage.query(&cypher)?;
let mut map = std::collections::HashMap::with_capacity(rows.len());
for row in rows {
if row.len() < 2 {
continue;
}
let file_path = row[0].as_str().unwrap_or_default().to_string();
let language = row[1].as_str().unwrap_or_default().to_string();
map.insert(file_path, language);
}
Ok(map)
}
}
pub(crate) struct FunctionRow {
id: String,
name: String,
qualified_name: String,
file_path: String,
start_line: u32,
is_exported: bool,
signature: String,
}
fn matches_any_pattern(name: &str, patterns: &[&str]) -> bool {
patterns.iter().any(|p| glob_match(p, name))
}
fn is_test_module_function(qualified_name: &str) -> bool {
let Some(idx) = qualified_name.rfind('#') else {
return false;
};
let disambiguator = &qualified_name[idx + 1..];
disambiguator == "tests" || disambiguator.starts_with("tests_")
}
fn is_trait_impl_method(qualified_name: &str) -> bool {
let Some(idx) = qualified_name.rfind('#') else {
return false;
};
let disambiguator = &qualified_name[idx + 1..];
!disambiguator.is_empty() && disambiguator != "tests" && !disambiguator.starts_with("tests_")
}
fn is_integration_test_file(file_path: &str) -> bool {
let normalized: std::borrow::Cow<'_, str> = if file_path.contains('\\') {
std::borrow::Cow::Owned(file_path.replace('\\', "/"))
} else {
std::borrow::Cow::Borrowed(file_path)
};
if normalized.starts_with("tests/") || normalized.contains("/tests/") {
return true;
}
if normalized.starts_with("test/") || normalized.contains("/test/") {
return true;
}
if normalized.contains("/src/test/") {
return true;
}
false
}
fn glob_match(pattern: &str, text: &str) -> bool {
let p: Vec<char> = pattern.chars().collect();
let t: Vec<char> = text.chars().collect();
glob_helper(&p, &t)
}
fn glob_helper(p: &[char], t: &[char]) -> bool {
match (p.first(), t.first()) {
(None, None) => true,
(None, Some(_)) => false,
(Some('*'), None) => glob_helper(&p[1..], t),
(Some('*'), Some(_)) => glob_helper(&p[1..], t) || glob_helper(p, &t[1..]),
(Some(_), None) => false,
(Some(pc), Some(tc)) => *pc == *tc && glob_helper(&p[1..], &t[1..]),
}
}
#[cfg(test)]
mod tests;