codelens_engine/import_graph/
dead_code.rs1use crate::call_graph::extract_calls;
2use crate::project::ProjectRoot;
3use anyhow::Result;
4use serde::Serialize;
5use std::collections::{HashMap, HashSet};
6use std::path::Path;
7
8use super::parsers::collect_top_level_funcs;
9use super::{DeadCodeEntry, GraphCache, collect_candidate_files};
10
11#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
12pub struct DeadCodeEntryV2 {
13 pub file: String,
14 pub symbol: Option<String>,
15 pub kind: Option<String>,
16 pub line: Option<usize>,
17 pub reason: String,
18 pub pass: u8,
19}
20
21pub(super) fn is_entry_point_file(file: &str) -> bool {
23 let name = Path::new(file)
24 .file_name()
25 .and_then(|n| n.to_str())
26 .unwrap_or(file);
27 matches!(
28 name,
29 "__init__.py"
30 | "mod.rs"
31 | "lib.rs"
32 | "main.rs"
33 | "index.ts"
34 | "index.js"
35 | "index.tsx"
36 | "index.jsx"
37 )
38}
39
40pub(super) fn is_entry_point_symbol(name: &str) -> bool {
42 name == "main"
43 || name == "__init__"
44 || name == "setUp"
45 || name == "tearDown"
46 || name.starts_with("test_")
47 || name.starts_with("Test")
48}
49
50pub(super) fn has_decorator(lines: &[&str], symbol_line: usize) -> bool {
54 if symbol_line < 2 {
55 return false;
56 }
57 let mut idx = symbol_line - 2; loop {
60 match lines.get(idx) {
61 Some(line) if line.trim_start().starts_with('@') => return true,
62 Some(line) if line.trim().is_empty() => {} _ => return false,
64 }
65 if idx == 0 {
66 return false;
67 }
68 idx -= 1;
69 }
70}
71
72pub fn find_dead_code(
73 project: &ProjectRoot,
74 max_results: usize,
75 cache: &GraphCache,
76) -> Result<Vec<DeadCodeEntry>> {
77 let graph = cache.get_or_build(project)?;
78 let mut dead: Vec<_> = graph
79 .iter()
80 .filter(|(_, node)| node.imported_by.is_empty())
81 .map(|(file, _)| DeadCodeEntry {
82 file: file.clone(),
83 symbol: None,
84 reason: "no importers".to_owned(),
85 })
86 .collect();
87 dead.sort_by(|a, b| a.file.cmp(&b.file));
88 if max_results > 0 && dead.len() > max_results {
89 dead.truncate(max_results);
90 }
91 Ok(dead)
92}
93
94pub fn find_dead_code_v2(
95 project: &ProjectRoot,
96 max_results: usize,
97 cache: &GraphCache,
98) -> Result<Vec<DeadCodeEntryV2>> {
99 let mut results: Vec<DeadCodeEntryV2> = Vec::new();
100
101 let graph = cache.get_or_build(project)?;
103 for (file, node) in graph.iter() {
104 if node.imported_by.is_empty() && !is_entry_point_file(file) {
105 results.push(DeadCodeEntryV2 {
106 file: file.clone(),
107 symbol: None,
108 kind: None,
109 line: None,
110 reason: "no importers".to_owned(),
111 pass: 1,
112 });
113 }
114 }
115
116 let candidate_files = collect_candidate_files(project.as_path())?;
118 let mut all_callees: HashSet<String> = HashSet::new();
119 for path in &candidate_files {
120 for edge in extract_calls(path) {
121 all_callees.insert(edge.callee_name);
122 }
123 }
124
125 for path in &candidate_files {
126 let relative = project.to_relative(path);
127
128 if results.iter().any(|e| e.file == relative && e.pass == 1) {
129 continue;
130 }
131 if is_entry_point_file(&relative) {
132 continue;
133 }
134
135 let source = std::fs::read_to_string(path).unwrap_or_default();
136 let lines: Vec<&str> = source.lines().collect();
137
138 let edges = extract_calls(path);
139 let mut defined_funcs: HashMap<String, usize> = HashMap::new();
140 for edge in &edges {
141 defined_funcs.entry(edge.caller_name.clone()).or_insert(0);
142 }
143 collect_top_level_funcs(path, &source, &mut defined_funcs);
144
145 for (func_name, func_line) in defined_funcs {
146 if func_name == "<module>" {
147 continue;
148 }
149 if is_entry_point_symbol(&func_name) {
150 continue;
151 }
152 if func_line > 0 && has_decorator(&lines, func_line) {
153 continue;
154 }
155 if !all_callees.contains(&func_name) {
156 results.push(DeadCodeEntryV2 {
157 file: relative.clone(),
158 symbol: Some(func_name),
159 kind: Some("function".to_owned()),
160 line: if func_line > 0 { Some(func_line) } else { None },
161 reason: "unreferenced symbol".to_owned(),
162 pass: 2,
163 });
164 }
165 }
166 }
167
168 results.sort_by(|a, b| {
169 a.pass
170 .cmp(&b.pass)
171 .then(a.file.cmp(&b.file))
172 .then(a.symbol.cmp(&b.symbol))
173 });
174 if max_results > 0 && results.len() > max_results {
175 results.truncate(max_results);
176 }
177 Ok(results)
178}