use crate::error::{ForgeError, Result};
use crate::types::Symbol;
use std::path::Path;
use std::sync::Arc;
pub struct DeadCodeAnalyzer<'a> {
db_path: &'a Path,
}
impl<'a> DeadCodeAnalyzer<'a> {
pub fn new(db_path: &'a Path) -> Self {
Self { db_path }
}
pub fn find_dead_code(&self) -> Result<Vec<DeadSymbol>> {
use sqlitegraph::{open_graph, snapshot::SnapshotId, GraphConfig};
let config = GraphConfig::sqlite();
let backend = open_graph(self.db_path, &config)
.map_err(|e| ForgeError::DatabaseError(format!("Failed to open graph: {}", e)))?;
let snapshot = SnapshotId::current();
let mut dead_symbols = Vec::new();
let entity_ids = backend
.entity_ids()
.map_err(|e| ForgeError::DatabaseError(format!("Failed to list entities: {}", e)))?;
for id in entity_ids {
if let Ok(node) = backend.get_node(snapshot, id) {
if !is_function_kind(&node.kind) {
continue;
}
if is_test_or_entry_point(&node.name) {
continue;
}
let incoming = backend
.fetch_incoming(id)
.map_err(|e| ForgeError::DatabaseError(format!("Query failed: {}", e)))?;
if incoming.is_empty() {
let is_public = node.data.to_string().contains("\"public\":true")
|| node.data.to_string().contains("\"visibility\":\"public\"");
if !is_public {
dead_symbols.push(DeadSymbol {
id,
kind: node.kind,
name: node.name,
file_path: node.file_path.unwrap_or_default(),
is_public,
reason: "No references found".to_string(),
});
}
}
}
}
Ok(dead_symbols)
}
}
fn is_function_kind(kind: &str) -> bool {
matches!(kind, "fn" | "function" | "method" | "const" | "static")
}
fn is_test_or_entry_point(name: &str) -> bool {
name.starts_with("test_")
|| name.ends_with("_test")
|| matches!(name, "main" | "lib" | "init" | "setup" | "teardown")
}
#[derive(Debug, Clone)]
pub struct DeadSymbol {
pub id: i64,
pub kind: String,
pub name: String,
pub file_path: String,
pub is_public: bool,
pub reason: String,
}
impl From<DeadSymbol> for Symbol {
fn from(dead: DeadSymbol) -> Self {
use crate::types::{Language, Location, SymbolId};
Symbol {
id: SymbolId(dead.id),
name: Arc::from(dead.name.clone()),
fully_qualified_name: Arc::from(dead.name),
kind: parse_symbol_kind(&dead.kind),
language: Language::Rust,
location: Location {
file_path: std::path::PathBuf::from(&dead.file_path),
byte_start: 0,
byte_end: 0,
line_number: 0,
},
parent_id: None,
metadata: serde_json::json!({
"dead_code": true,
"reason": dead.reason,
"is_public": dead.is_public,
}),
}
}
}
fn parse_symbol_kind(kind: &str) -> crate::types::SymbolKind {
match kind {
"fn" | "function" | "method" => crate::types::SymbolKind::Function,
"struct" => crate::types::SymbolKind::Struct,
"enum" => crate::types::SymbolKind::Enum,
"trait" => crate::types::SymbolKind::Trait,
"impl" => crate::types::SymbolKind::Impl,
"const" => crate::types::SymbolKind::Constant,
"static" => crate::types::SymbolKind::Static,
"module" | "mod" => crate::types::SymbolKind::Module,
"type" => crate::types::SymbolKind::TypeAlias,
_ => crate::types::SymbolKind::LocalVariable,
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_analyzer_creation() {
let temp = tempdir().unwrap();
let db_path = temp.path().join("test.db");
let analyzer = DeadCodeAnalyzer::new(&db_path);
assert!(!analyzer.db_path.exists()); }
}