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