forgekit_core/analysis/
dead_code.rs1use crate::error::{ForgeError, Result};
6use crate::types::Symbol;
7use std::path::Path;
8use std::sync::Arc;
9
10pub struct DeadCodeAnalyzer<'a> {
12 db_path: &'a Path,
13}
14
15impl<'a> DeadCodeAnalyzer<'a> {
16 pub fn new(db_path: &'a Path) -> Self {
18 Self { db_path }
19 }
20
21 pub fn find_dead_code(&self) -> Result<Vec<DeadSymbol>> {
23 use sqlitegraph::{open_graph, snapshot::SnapshotId, GraphConfig};
24
25 let config = GraphConfig::sqlite();
26 let backend = open_graph(self.db_path, &config)
27 .map_err(|e| ForgeError::DatabaseError(format!("Failed to open graph: {}", e)))?;
28
29 let snapshot = SnapshotId::current();
30 let mut dead_symbols = Vec::new();
31
32 let entity_ids = backend
33 .entity_ids()
34 .map_err(|e| ForgeError::DatabaseError(format!("Failed to list entities: {}", e)))?;
35
36 for id in entity_ids {
37 if let Ok(node) = backend.get_node(snapshot, id) {
38 if !is_function_kind(&node.kind) {
39 continue;
40 }
41
42 if is_test_or_entry_point(&node.name) {
43 continue;
44 }
45
46 let incoming = backend
47 .fetch_incoming(id)
48 .map_err(|e| ForgeError::DatabaseError(format!("Query failed: {}", e)))?;
49
50 if incoming.is_empty() {
51 let is_public = node.data.to_string().contains("\"public\":true")
52 || node.data.to_string().contains("\"visibility\":\"public\"");
53
54 if !is_public {
55 dead_symbols.push(DeadSymbol {
56 id,
57 kind: node.kind,
58 name: node.name,
59 file_path: node.file_path.unwrap_or_default(),
60 is_public,
61 reason: "No references found".to_string(),
62 });
63 }
64 }
65 }
66 }
67
68 Ok(dead_symbols)
69 }
70}
71
72fn is_function_kind(kind: &str) -> bool {
73 matches!(kind, "fn" | "function" | "method" | "const" | "static")
74}
75
76fn is_test_or_entry_point(name: &str) -> bool {
77 name.starts_with("test_")
78 || name.ends_with("_test")
79 || matches!(name, "main" | "lib" | "init" | "setup" | "teardown")
80}
81
82#[derive(Debug, Clone)]
84pub struct DeadSymbol {
85 pub id: i64,
86 pub kind: String,
87 pub name: String,
88 pub file_path: String,
89 pub is_public: bool,
90 pub reason: String,
91}
92
93impl From<DeadSymbol> for Symbol {
94 fn from(dead: DeadSymbol) -> Self {
95 use crate::types::{Language, Location, SymbolId};
96
97 Symbol {
98 id: SymbolId(dead.id),
99 name: Arc::from(dead.name.clone()),
100 fully_qualified_name: Arc::from(dead.name),
101 kind: parse_symbol_kind(&dead.kind),
102 language: Language::Rust,
103 location: Location {
104 file_path: std::path::PathBuf::from(&dead.file_path),
105 byte_start: 0,
106 byte_end: 0,
107 line_number: 0,
108 },
109 parent_id: None,
110 metadata: serde_json::json!({
111 "dead_code": true,
112 "reason": dead.reason,
113 "is_public": dead.is_public,
114 }),
115 }
116 }
117}
118
119fn parse_symbol_kind(kind: &str) -> crate::types::SymbolKind {
120 match kind {
121 "fn" | "function" | "method" => crate::types::SymbolKind::Function,
122 "struct" => crate::types::SymbolKind::Struct,
123 "enum" => crate::types::SymbolKind::Enum,
124 "trait" => crate::types::SymbolKind::Trait,
125 "impl" => crate::types::SymbolKind::Impl,
126 "const" => crate::types::SymbolKind::Constant,
127 "static" => crate::types::SymbolKind::Static,
128 "module" | "mod" => crate::types::SymbolKind::Module,
129 "type" => crate::types::SymbolKind::TypeAlias,
130 _ => crate::types::SymbolKind::LocalVariable,
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use tempfile::tempdir;
138
139 #[test]
140 fn test_analyzer_creation() {
141 let temp = tempdir().unwrap();
142 let db_path = temp.path().join("test.db");
143
144 let analyzer = DeadCodeAnalyzer::new(&db_path);
145 assert!(!analyzer.db_path.exists()); }
148}