infiniloom_engine/analysis/
dead_code.rs

1//! Dead code detection for all supported languages
2//!
3//! Detects unused exports, unreachable code, unused private symbols,
4//! unused imports, and unused variables.
5
6use crate::analysis::types::{
7    DeadCodeInfo, UnreachableCode, UnusedExport, UnusedImport, UnusedSymbol, UnusedVariable,
8};
9use crate::parser::Language;
10use crate::types::{Symbol, SymbolKind, Visibility};
11use std::collections::{HashMap, HashSet};
12
13/// Detects dead code across a codebase
14pub struct DeadCodeDetector {
15    /// Map of symbol name to its definition info
16    definitions: HashMap<String, DefinitionInfo>,
17    /// Set of referenced symbols
18    references: HashSet<String>,
19    /// Map of import name to import info
20    imports: HashMap<String, ImportInfo>,
21    /// Map of variable name to variable info (per scope)
22    variables: HashMap<String, HashMap<String, VariableInfo>>,
23    /// Files being analyzed
24    files: Vec<FileInfo>,
25}
26
27#[derive(Debug, Clone)]
28struct DefinitionInfo {
29    name: String,
30    kind: SymbolKind,
31    visibility: Visibility,
32    file_path: String,
33    line: u32,
34    is_entry_point: bool,
35}
36
37#[derive(Debug, Clone)]
38struct ImportInfo {
39    name: String,
40    import_path: String,
41    file_path: String,
42    line: u32,
43    is_used: bool,
44}
45
46#[derive(Debug, Clone)]
47struct VariableInfo {
48    name: String,
49    file_path: String,
50    line: u32,
51    scope: String,
52    is_used: bool,
53}
54
55#[derive(Debug, Clone)]
56struct FileInfo {
57    path: String,
58    language: Language,
59    symbols: Vec<Symbol>,
60}
61
62impl DeadCodeDetector {
63    /// Create a new detector
64    pub fn new() -> Self {
65        Self {
66            definitions: HashMap::new(),
67            references: HashSet::new(),
68            imports: HashMap::new(),
69            variables: HashMap::new(),
70            files: Vec::new(),
71        }
72    }
73
74    /// Add a file's symbols for analysis
75    pub fn add_file(&mut self, file_path: &str, symbols: &[Symbol], language: Language) {
76        self.files.push(FileInfo {
77            path: file_path.to_owned(),
78            language,
79            symbols: symbols.to_vec(),
80        });
81
82        for symbol in symbols {
83            // Track definition
84            let is_entry_point = self.is_entry_point(symbol, language);
85
86            self.definitions.insert(
87                symbol.name.clone(),
88                DefinitionInfo {
89                    name: symbol.name.clone(),
90                    kind: symbol.kind,
91                    visibility: symbol.visibility,
92                    file_path: file_path.to_owned(),
93                    line: symbol.start_line,
94                    is_entry_point,
95                },
96            );
97
98            // Track references (calls, extends, implements)
99            for call in &symbol.calls {
100                self.references.insert(call.clone());
101            }
102
103            if let Some(ref extends) = symbol.extends {
104                self.references.insert(extends.clone());
105            }
106
107            for implements in &symbol.implements {
108                self.references.insert(implements.clone());
109            }
110
111            // Track parent reference
112            if let Some(ref parent) = symbol.parent {
113                self.references.insert(parent.clone());
114            }
115        }
116    }
117
118    /// Add import information
119    pub fn add_import(&mut self, name: &str, import_path: &str, file_path: &str, line: u32) {
120        self.imports.insert(
121            format!("{}:{}", file_path, name),
122            ImportInfo {
123                name: name.to_owned(),
124                import_path: import_path.to_owned(),
125                file_path: file_path.to_owned(),
126                line,
127                is_used: false,
128            },
129        );
130    }
131
132    /// Add variable information
133    pub fn add_variable(&mut self, name: &str, file_path: &str, line: u32, scope: &str) {
134        let scope_vars = self.variables.entry(scope.to_owned()).or_default();
135        scope_vars.insert(
136            name.to_owned(),
137            VariableInfo {
138                name: name.to_owned(),
139                file_path: file_path.to_owned(),
140                line,
141                scope: scope.to_owned(),
142                is_used: false,
143            },
144        );
145    }
146
147    /// Mark an import as used
148    pub fn mark_import_used(&mut self, name: &str, file_path: &str) {
149        let key = format!("{}:{}", file_path, name);
150        if let Some(import) = self.imports.get_mut(&key) {
151            import.is_used = true;
152        }
153    }
154
155    /// Mark a variable as used
156    pub fn mark_variable_used(&mut self, name: &str, scope: &str) {
157        if let Some(scope_vars) = self.variables.get_mut(scope) {
158            if let Some(var) = scope_vars.get_mut(name) {
159                var.is_used = true;
160            }
161        }
162    }
163
164    /// Check if a symbol is an entry point (should not be flagged as unused)
165    fn is_entry_point(&self, symbol: &Symbol, language: Language) -> bool {
166        let name = &symbol.name;
167
168        // Common entry point patterns
169        if name == "main" || name == "init" || name == "__init__" {
170            return true;
171        }
172
173        // Test functions
174        if name.starts_with("test_")
175            || name.starts_with("Test")
176            || name.ends_with("_test")
177            || name.ends_with("Test")
178        {
179            return true;
180        }
181
182        // Language-specific entry points
183        match language {
184            Language::Python => {
185                // __all__ exports, __main__, decorators like @app.route
186                name.starts_with("__") && name.ends_with("__")
187            },
188            Language::JavaScript | Language::TypeScript => {
189                // Module exports, React components, etc.
190                name.chars().next().is_some_and(|c| c.is_uppercase())
191                    && matches!(symbol.kind, SymbolKind::Class | SymbolKind::Function)
192            },
193            Language::Rust => {
194                // pub items, #[test], #[no_mangle]
195                matches!(symbol.visibility, Visibility::Public)
196            },
197            Language::Go => {
198                // Exported (capitalized) names
199                name.chars().next().is_some_and(|c| c.is_uppercase())
200            },
201            Language::Java | Language::Kotlin => {
202                // public static void main, @Test, @Bean, etc.
203                name == "main"
204                    || matches!(symbol.visibility, Visibility::Public)
205                        && matches!(symbol.kind, SymbolKind::Method)
206            },
207            Language::Ruby => {
208                // initialize, Rails callbacks
209                name == "initialize" || name.starts_with("before_") || name.starts_with("after_")
210            },
211            Language::Php => {
212                // __construct, __destruct, magic methods
213                name.starts_with("__")
214            },
215            Language::Swift => {
216                // @main, viewDidLoad, etc.
217                name == "viewDidLoad" || name == "applicationDidFinishLaunching"
218            },
219            Language::Elixir => {
220                // start, init, handle_* callbacks
221                name == "start" || name.starts_with("handle_") || name == "child_spec"
222            },
223            _ => false,
224        }
225    }
226
227    /// Detect all dead code
228    pub fn detect(&self) -> DeadCodeInfo {
229        DeadCodeInfo {
230            unused_exports: self.find_unused_exports(),
231            unreachable_code: Vec::new(), // Requires AST analysis
232            unused_private: self.find_unused_private(),
233            unused_imports: self.find_unused_imports(),
234            unused_variables: self.find_unused_variables(),
235        }
236    }
237
238    /// Find unused public exports
239    fn find_unused_exports(&self) -> Vec<UnusedExport> {
240        let mut unused = Vec::new();
241
242        for (name, def) in &self.definitions {
243            // Skip if not public
244            if !matches!(def.visibility, Visibility::Public) {
245                continue;
246            }
247
248            // Skip entry points
249            if def.is_entry_point {
250                continue;
251            }
252
253            // Skip if referenced
254            if self.references.contains(name) {
255                continue;
256            }
257
258            // Calculate confidence based on analysis scope
259            let confidence = self.calculate_confidence(def);
260
261            unused.push(UnusedExport {
262                name: name.clone(),
263                kind: format!("{:?}", def.kind),
264                file_path: def.file_path.clone(),
265                line: def.line,
266                confidence,
267                reason: "No references found in analyzed codebase".to_owned(),
268            });
269        }
270
271        unused
272    }
273
274    /// Find unused private symbols
275    fn find_unused_private(&self) -> Vec<UnusedSymbol> {
276        let mut unused = Vec::new();
277
278        for (name, def) in &self.definitions {
279            // Only check private symbols
280            if matches!(def.visibility, Visibility::Public) {
281                continue;
282            }
283
284            // Skip entry points
285            if def.is_entry_point {
286                continue;
287            }
288
289            // Skip if referenced
290            if self.references.contains(name) {
291                continue;
292            }
293
294            unused.push(UnusedSymbol {
295                name: name.clone(),
296                kind: format!("{:?}", def.kind),
297                file_path: def.file_path.clone(),
298                line: def.line,
299            });
300        }
301
302        unused
303    }
304
305    /// Find unused imports
306    fn find_unused_imports(&self) -> Vec<UnusedImport> {
307        self.imports
308            .values()
309            .filter(|import| !import.is_used)
310            .map(|import| UnusedImport {
311                name: import.name.clone(),
312                import_path: import.import_path.clone(),
313                file_path: import.file_path.clone(),
314                line: import.line,
315            })
316            .collect()
317    }
318
319    /// Find unused variables
320    fn find_unused_variables(&self) -> Vec<UnusedVariable> {
321        let mut unused = Vec::new();
322
323        for (scope, vars) in &self.variables {
324            for var in vars.values() {
325                if !var.is_used {
326                    // Skip underscore-prefixed variables (intentionally unused)
327                    if var.name.starts_with('_') {
328                        continue;
329                    }
330
331                    unused.push(UnusedVariable {
332                        name: var.name.clone(),
333                        file_path: var.file_path.clone(),
334                        line: var.line,
335                        scope: Some(scope.clone()),
336                    });
337                }
338            }
339        }
340
341        unused
342    }
343
344    /// Calculate confidence for unused export detection
345    fn calculate_confidence(&self, def: &DefinitionInfo) -> f32 {
346        let mut confidence: f32 = 0.5; // Base confidence
347
348        // Higher confidence for private/internal visibility
349        if matches!(def.visibility, Visibility::Private | Visibility::Internal) {
350            confidence += 0.3;
351        }
352
353        // Higher confidence for certain symbol kinds
354        match def.kind {
355            SymbolKind::Function | SymbolKind::Method => confidence += 0.1,
356            SymbolKind::Class | SymbolKind::Struct => confidence += 0.05,
357            SymbolKind::Variable | SymbolKind::Constant => confidence += 0.15,
358            _ => {},
359        }
360
361        // Cap at 0.95 since we can't be 100% sure without full program analysis
362        confidence.min(0.95)
363    }
364}
365
366impl Default for DeadCodeDetector {
367    fn default() -> Self {
368        Self::new()
369    }
370}
371
372/// Detect unreachable code in a function body
373pub fn detect_unreachable_code(
374    source: &str,
375    file_path: &str,
376    _language: Language,
377) -> Vec<UnreachableCode> {
378    let mut unreachable = Vec::new();
379
380    let lines: Vec<&str> = source.lines().collect();
381    let mut after_terminator = false;
382    let mut terminator_line = 0u32;
383
384    for (i, line) in lines.iter().enumerate() {
385        let trimmed = line.trim();
386        let line_num = (i + 1) as u32;
387
388        // Check for terminators
389        if is_terminator(trimmed) {
390            after_terminator = true;
391            terminator_line = line_num;
392            continue;
393        }
394
395        // If we're after a terminator and this is code (not blank/comment/closing brace)
396        if after_terminator {
397            if trimmed.is_empty()
398                || trimmed.starts_with("//")
399                || trimmed.starts_with('#')
400                || trimmed.starts_with("/*")
401                || trimmed.starts_with('*')
402                || trimmed == "}"
403                || trimmed == ")"
404                || trimmed == "]"
405            {
406                continue;
407            }
408
409            // Check for control flow that makes it reachable
410            if trimmed.starts_with("case ")
411                || trimmed.starts_with("default:")
412                || trimmed.starts_with("else")
413                || trimmed.starts_with("catch")
414                || trimmed.starts_with("except")
415                || trimmed.starts_with("rescue")
416                || trimmed.starts_with("finally")
417            {
418                after_terminator = false;
419                continue;
420            }
421
422            // This is unreachable code
423            unreachable.push(UnreachableCode {
424                file_path: file_path.to_owned(),
425                start_line: line_num,
426                end_line: line_num,
427                snippet: trimmed.to_owned(),
428                reason: format!("Code after terminator on line {}", terminator_line),
429            });
430
431            after_terminator = false;
432        }
433    }
434
435    unreachable
436}
437
438/// Check if a line is a terminator (return, throw, break, continue, etc.)
439fn is_terminator(line: &str) -> bool {
440    let terminators = [
441        "return",
442        "return;",
443        "throw",
444        "raise",
445        "break",
446        "break;",
447        "continue",
448        "continue;",
449        "exit",
450        "exit(",
451        "die(",
452        "panic!",
453        "unreachable!",
454    ];
455
456    for term in &terminators {
457        if line.starts_with(term) || line == *term {
458            return true;
459        }
460    }
461
462    // Language-specific patterns
463    if line.starts_with("return ") && line.ends_with(';') {
464        return true;
465    }
466
467    false
468}
469
470/// Convenience function to detect dead code in a set of files
471pub fn detect_dead_code(files: &[(String, Vec<Symbol>, Language)]) -> DeadCodeInfo {
472    let mut detector = DeadCodeDetector::new();
473
474    for (path, symbols, language) in files {
475        detector.add_file(path, symbols, *language);
476    }
477
478    detector.detect()
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use crate::types::Visibility;
485
486    fn make_symbol(name: &str, kind: SymbolKind, visibility: Visibility) -> Symbol {
487        Symbol {
488            name: name.to_owned(),
489            kind,
490            visibility,
491            start_line: 1,
492            end_line: 10,
493            ..Default::default()
494        }
495    }
496
497    #[test]
498    fn test_unused_export_detection() {
499        let mut detector = DeadCodeDetector::new();
500
501        // Use C language where public visibility doesn't auto-mark as entry point
502        let symbols = [
503            make_symbol("used_func", SymbolKind::Function, Visibility::Public),
504            make_symbol("unused_func", SymbolKind::Function, Visibility::Public),
505        ];
506
507        // Add a reference to used_func
508        let caller = Symbol {
509            name: "caller".to_owned(),
510            kind: SymbolKind::Function,
511            visibility: Visibility::Private,
512            calls: vec!["used_func".to_owned()],
513            ..Default::default()
514        };
515
516        detector.add_file("test.c", &[symbols[0].clone(), symbols[1].clone(), caller], Language::C);
517
518        let result = detector.detect();
519
520        // unused_func should be detected
521        assert!(result
522            .unused_exports
523            .iter()
524            .any(|e| e.name == "unused_func"));
525
526        // used_func should not be detected
527        assert!(!result.unused_exports.iter().any(|e| e.name == "used_func"));
528    }
529
530    #[test]
531    fn test_entry_point_not_flagged() {
532        let mut detector = DeadCodeDetector::new();
533
534        let symbols = vec![
535            make_symbol("main", SymbolKind::Function, Visibility::Public),
536            make_symbol("test_something", SymbolKind::Function, Visibility::Public),
537            make_symbol("__init__", SymbolKind::Method, Visibility::Public),
538        ];
539
540        detector.add_file("test.py", &symbols, Language::Python);
541
542        let result = detector.detect();
543
544        // Entry points should not be flagged
545        assert!(!result.unused_exports.iter().any(|e| e.name == "main"));
546        assert!(!result
547            .unused_exports
548            .iter()
549            .any(|e| e.name == "test_something"));
550        assert!(!result.unused_exports.iter().any(|e| e.name == "__init__"));
551    }
552
553    #[test]
554    fn test_unreachable_code_detection() {
555        let source = r#"
556fn example() {
557    let x = 1;
558    return x;
559    let y = 2; // unreachable
560    println!("{}", y);
561}
562"#;
563
564        let unreachable = detect_unreachable_code(source, "test.rs", Language::Rust);
565
566        assert!(!unreachable.is_empty());
567        assert!(unreachable.iter().any(|u| u.snippet.contains("let y")));
568    }
569
570    #[test]
571    fn test_is_terminator() {
572        assert!(is_terminator("return;"));
573        assert!(is_terminator("return x"));
574        assert!(is_terminator("throw new Error()"));
575        assert!(is_terminator("break;"));
576        assert!(is_terminator("continue;"));
577        assert!(is_terminator("panic!(\"error\")"));
578
579        assert!(!is_terminator("let x = 1;"));
580        assert!(!is_terminator("if (x) {"));
581        assert!(!is_terminator("// return"));
582    }
583
584    #[test]
585    fn test_unused_imports() {
586        let mut detector = DeadCodeDetector::new();
587
588        detector.add_import("used_import", "some/path", "test.ts", 1);
589        detector.add_import("unused_import", "other/path", "test.ts", 2);
590
591        detector.mark_import_used("used_import", "test.ts");
592
593        let result = detector.detect();
594
595        assert_eq!(result.unused_imports.len(), 1);
596        assert_eq!(result.unused_imports[0].name, "unused_import");
597    }
598
599    #[test]
600    fn test_underscore_variables_ignored() {
601        let mut detector = DeadCodeDetector::new();
602
603        detector.add_variable("_unused", "test.rs", 1, "main");
604        detector.add_variable("used", "test.rs", 2, "main");
605        detector.add_variable("not_used", "test.rs", 3, "main");
606
607        detector.mark_variable_used("used", "main");
608
609        let result = detector.detect();
610
611        // _unused should be ignored (intentionally unused)
612        assert!(!result.unused_variables.iter().any(|v| v.name == "_unused"));
613
614        // not_used should be flagged
615        assert!(result.unused_variables.iter().any(|v| v.name == "not_used"));
616    }
617}