1use 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
13pub struct DeadCodeDetector {
15 definitions: HashMap<String, DefinitionInfo>,
17 references: HashSet<String>,
19 imports: HashMap<String, ImportInfo>,
21 variables: HashMap<String, HashMap<String, VariableInfo>>,
23 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 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 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 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 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 if let Some(ref parent) = symbol.parent {
113 self.references.insert(parent.clone());
114 }
115 }
116 }
117
118 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 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 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 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 fn is_entry_point(&self, symbol: &Symbol, language: Language) -> bool {
166 let name = &symbol.name;
167
168 if name == "main" || name == "init" || name == "__init__" {
170 return true;
171 }
172
173 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 match language {
184 Language::Python => {
185 name.starts_with("__") && name.ends_with("__")
187 },
188 Language::JavaScript | Language::TypeScript => {
189 name.chars().next().is_some_and(|c| c.is_uppercase())
191 && matches!(symbol.kind, SymbolKind::Class | SymbolKind::Function)
192 },
193 Language::Rust => {
194 matches!(symbol.visibility, Visibility::Public)
196 },
197 Language::Go => {
198 name.chars().next().is_some_and(|c| c.is_uppercase())
200 },
201 Language::Java | Language::Kotlin => {
202 name == "main"
204 || matches!(symbol.visibility, Visibility::Public)
205 && matches!(symbol.kind, SymbolKind::Method)
206 },
207 Language::Ruby => {
208 name == "initialize" || name.starts_with("before_") || name.starts_with("after_")
210 },
211 Language::Php => {
212 name.starts_with("__")
214 },
215 Language::Swift => {
216 name == "viewDidLoad" || name == "applicationDidFinishLaunching"
218 },
219 Language::Elixir => {
220 name == "start" || name.starts_with("handle_") || name == "child_spec"
222 },
223 _ => false,
224 }
225 }
226
227 pub fn detect(&self) -> DeadCodeInfo {
229 DeadCodeInfo {
230 unused_exports: self.find_unused_exports(),
231 unreachable_code: Vec::new(), unused_private: self.find_unused_private(),
233 unused_imports: self.find_unused_imports(),
234 unused_variables: self.find_unused_variables(),
235 }
236 }
237
238 fn find_unused_exports(&self) -> Vec<UnusedExport> {
240 let mut unused = Vec::new();
241
242 for (name, def) in &self.definitions {
243 if !matches!(def.visibility, Visibility::Public) {
245 continue;
246 }
247
248 if def.is_entry_point {
250 continue;
251 }
252
253 if self.references.contains(name) {
255 continue;
256 }
257
258 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 fn find_unused_private(&self) -> Vec<UnusedSymbol> {
276 let mut unused = Vec::new();
277
278 for (name, def) in &self.definitions {
279 if matches!(def.visibility, Visibility::Public) {
281 continue;
282 }
283
284 if def.is_entry_point {
286 continue;
287 }
288
289 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 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 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 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 fn calculate_confidence(&self, def: &DefinitionInfo) -> f32 {
346 let mut confidence: f32 = 0.5; if matches!(def.visibility, Visibility::Private | Visibility::Internal) {
350 confidence += 0.3;
351 }
352
353 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 confidence.min(0.95)
363 }
364}
365
366impl Default for DeadCodeDetector {
367 fn default() -> Self {
368 Self::new()
369 }
370}
371
372pub 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 if is_terminator(trimmed) {
390 after_terminator = true;
391 terminator_line = line_num;
392 continue;
393 }
394
395 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 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 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
438fn 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 if line.starts_with("return ") && line.ends_with(';') {
464 return true;
465 }
466
467 false
468}
469
470pub 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 let symbols = [
503 make_symbol("used_func", SymbolKind::Function, Visibility::Public),
504 make_symbol("unused_func", SymbolKind::Function, Visibility::Public),
505 ];
506
507 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 assert!(result
522 .unused_exports
523 .iter()
524 .any(|e| e.name == "unused_func"));
525
526 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 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 assert!(!result.unused_variables.iter().any(|v| v.name == "_unused"));
613
614 assert!(result.unused_variables.iter().any(|v| v.name == "not_used"));
616 }
617}