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.name_col,
35 f.name_end_col,
36 &f.name,
37 "Function",
38 ));
39 }
40 }
41}
42
43fn 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
60fn 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
87fn 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
101fn is_entry_point(name: &str) -> bool {
103 matches!(name, "main" | "new" | "default" | "drop" | "fmt")
104}