cha_core/plugins/
dead_code.rs1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub 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
24fn 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
42fn 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
58fn 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
82fn 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
96fn is_entry_point(name: &str) -> bool {
98 matches!(name, "main" | "new" | "default" | "drop" | "fmt")
99}