cha_core/plugins/
dead_code.rs1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub struct DeadCodeAnalyzer;
10
11impl Plugin for DeadCodeAnalyzer {
12 fn name(&self) -> &str {
13 "dead_code"
14 }
15
16 fn smells(&self) -> Vec<String> {
17 vec!["dead_code".into()]
18 }
19
20 fn description(&self) -> &str {
21 "Unexported and unreferenced code"
22 }
23
24 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
25 if matches!(ctx.model.language.as_str(), "c" | "cpp")
31 && has_token_concat_macros(&ctx.file.content)
32 {
33 return vec![];
34 }
35
36 let mut findings = Vec::new();
37 check_dead_functions(ctx, &mut findings);
38 check_dead_classes(ctx, &mut findings);
39 findings
40 }
41}
42
43fn has_token_concat_macros(content: &str) -> bool {
48 let mut in_define = false;
49 for line in content.lines() {
50 let trimmed = line.trim_start();
51 if trimmed.starts_with("#define") {
52 in_define = true;
53 }
54 if in_define && trimmed.contains("##") {
55 return true;
56 }
57 if in_define && !line.trim_end().ends_with('\\') {
58 in_define = false;
59 }
60 }
61 false
62}
63
64fn check_dead_functions(ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
66 for f in &ctx.model.functions {
67 if f.is_exported || is_entry_point(&f.name) {
68 continue;
69 }
70 if is_in_file_referenced(&ctx.file.content, &f.name, f.start_line, f.end_line) {
71 continue;
72 }
73 if let Some(p) = ctx.project
74 && p.is_called_externally(&f.name, &ctx.file.path)
75 {
76 continue;
77 }
78 findings.push(make_dead_code_finding(
79 ctx,
80 f.start_line,
81 f.name_col,
82 f.name_end_col,
83 &f.name,
84 "Function",
85 ));
86 }
87}
88
89fn check_dead_classes(ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
91 for c in &ctx.model.classes {
92 if c.is_exported {
93 continue;
94 }
95 if is_in_file_referenced(&ctx.file.content, &c.name, c.start_line, c.end_line) {
96 continue;
97 }
98 if let Some(p) = ctx.project
99 && p.is_called_externally(&c.name, &ctx.file.path)
100 {
101 continue;
102 }
103 findings.push(make_dead_code_finding(
104 ctx,
105 c.start_line,
106 c.name_col,
107 c.name_end_col,
108 &c.name,
109 "Class",
110 ));
111 }
112}
113
114fn make_dead_code_finding(
116 ctx: &AnalysisContext,
117 start_line: usize,
118 name_col: usize,
119 name_end_col: usize,
120 name: &str,
121 kind: &str,
122) -> Finding {
123 Finding {
124 smell_name: "dead_code".into(),
125 category: SmellCategory::Dispensables,
126 severity: Severity::Hint,
127 location: Location {
128 path: ctx.file.path.clone(),
129 start_line,
130 start_col: name_col,
131 end_line: start_line,
132 end_col: name_end_col,
133 name: Some(name.to_string()),
134 },
135 message: format!("{} `{}` is not exported and may be unused", kind, name),
136 suggested_refactorings: vec!["Remove dead code".into()],
137 ..Default::default()
138 }
139}
140
141fn is_in_file_referenced(content: &str, name: &str, def_start: usize, def_end: usize) -> bool {
143 for (i, line) in content.lines().enumerate() {
144 let line_num = i + 1;
145 if line_num >= def_start && line_num <= def_end {
146 continue;
147 }
148 if line.contains(name) {
149 return true;
150 }
151 }
152 false
153}
154
155fn is_entry_point(name: &str) -> bool {
157 matches!(name, "main" | "new" | "default" | "drop" | "fmt")
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn macro_detection_finds_simple_concat() {
166 let src = "#define FN(name) handle_##name##_attr\n";
167 assert!(has_token_concat_macros(src));
168 }
169
170 #[test]
171 fn macro_detection_finds_multiline_concat() {
172 let src = "#define X(a, b) \\\n foo_##a##_##b\n";
173 assert!(has_token_concat_macros(src));
174 }
175
176 #[test]
177 fn macro_detection_ignores_concat_outside_define() {
178 let src = "// this comment has ## in it\nlet s = \"a##b\";\n";
179 assert!(!has_token_concat_macros(src));
180 }
181
182 #[test]
183 fn macro_detection_ignores_define_without_concat() {
184 let src = "#define MAX(a, b) ((a) > (b) ? (a) : (b))\n";
185 assert!(!has_token_concat_macros(src));
186 }
187
188 #[test]
189 fn macro_detection_handles_no_macros() {
190 let src = "int main() { return 0; }\n";
191 assert!(!has_token_concat_macros(src));
192 }
193}