Skip to main content

cha_core/plugins/
dead_code.rs

1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3/// Detect non-exported functions/classes that may be dead code.
4/// Note: single-file heuristic — flags unexported items as potential dead code.
5pub struct DeadCodeAnalyzer;
6
7impl Plugin for DeadCodeAnalyzer {
8    fn name(&self) -> &str {
9        "dead_code"
10    }
11
12    fn smells(&self) -> Vec<String> {
13        vec!["dead_code".into()]
14    }
15
16    fn description(&self) -> &str {
17        "Unexported and unreferenced code"
18    }
19
20    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
21        let mut findings = Vec::new();
22        check_dead_functions(ctx, &mut findings);
23        check_dead_classes(ctx, &mut findings);
24        findings
25    }
26}
27
28/// Flag unexported, unreferenced functions as potential dead code.
29fn check_dead_functions(ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
30    for f in &ctx.model.functions {
31        if f.is_exported || is_entry_point(&f.name) {
32            continue;
33        }
34        if !is_referenced(&ctx.file.content, &f.name, f.start_line, f.end_line) {
35            findings.push(make_dead_code_finding(
36                ctx,
37                f.start_line,
38                f.name_col,
39                f.name_end_col,
40                &f.name,
41                "Function",
42            ));
43        }
44    }
45}
46
47/// Flag unexported, unreferenced classes as potential dead code.
48fn check_dead_classes(ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
49    for c in &ctx.model.classes {
50        if c.is_exported || is_referenced(&ctx.file.content, &c.name, c.start_line, c.end_line) {
51            continue;
52        }
53        findings.push(make_dead_code_finding(
54            ctx,
55            c.start_line,
56            c.name_col,
57            c.name_end_col,
58            &c.name,
59            "Class",
60        ));
61    }
62}
63
64/// Build a dead code finding for a given symbol.
65fn make_dead_code_finding(
66    ctx: &AnalysisContext,
67    start_line: usize,
68    name_col: usize,
69    name_end_col: usize,
70    name: &str,
71    kind: &str,
72) -> Finding {
73    Finding {
74        smell_name: "dead_code".into(),
75        category: SmellCategory::Dispensables,
76        severity: Severity::Hint,
77        location: Location {
78            path: ctx.file.path.clone(),
79            start_line,
80            start_col: name_col,
81            end_line: start_line,
82            end_col: name_end_col,
83            name: Some(name.to_string()),
84        },
85        message: format!("{} `{}` is not exported and may be unused", kind, name),
86        suggested_refactorings: vec!["Remove dead code".into()],
87        ..Default::default()
88    }
89}
90
91/// Check if a name is referenced outside its own definition lines.
92fn is_referenced(content: &str, name: &str, def_start: usize, def_end: usize) -> bool {
93    for (i, line) in content.lines().enumerate() {
94        let line_num = i + 1;
95        if line_num >= def_start && line_num <= def_end {
96            continue;
97        }
98        if line.contains(name) {
99            return true;
100        }
101    }
102    false
103}
104
105/// Names that are entry points or framework callbacks, not dead code.
106fn is_entry_point(name: &str) -> bool {
107    matches!(name, "main" | "new" | "default" | "drop" | "fmt")
108}