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