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 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
28fn 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
47fn 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
64fn 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
91fn 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
105fn is_entry_point(name: &str) -> bool {
107 matches!(name, "main" | "new" | "default" | "drop" | "fmt")
108}