Skip to main content

forgekit_core/analysis/
dead_code.rs

1//! Dead code detection analysis
2//!
3//! Finds symbols that are defined but never referenced/called.
4
5use crate::error::{ForgeError, Result};
6use crate::types::Symbol;
7use std::path::Path;
8use std::sync::Arc;
9
10/// Dead code analyzer
11pub struct DeadCodeAnalyzer<'a> {
12    db_path: &'a Path,
13}
14
15impl<'a> DeadCodeAnalyzer<'a> {
16    /// Create a new dead code analyzer
17    pub fn new(db_path: &'a Path) -> Self {
18        Self { db_path }
19    }
20
21    /// Find all dead code (symbols with no references)
22    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/// Dead symbol information
83#[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        // Just verify it creates without error
146        assert!(!analyzer.db_path.exists()); // DB doesn't exist yet
147    }
148}