1mod predicates;
2mod unused_deps;
3mod unused_exports;
4mod unused_files;
5mod unused_members;
6
7use rustc_hash::FxHashMap;
8
9use fallow_config::{PackageJson, ResolvedConfig, Severity};
10
11use crate::discover::FileId;
12use crate::extract::ModuleInfo;
13use crate::graph::ModuleGraph;
14use crate::resolve::ResolvedModule;
15use crate::results::*;
16use crate::suppress::{self, IssueKind, Suppression};
17
18use unused_deps::{
19 find_type_only_dependencies, find_unlisted_dependencies, find_unresolved_imports,
20 find_unused_dependencies,
21};
22use unused_exports::{collect_export_usages, find_duplicate_exports, find_unused_exports};
23use unused_files::find_unused_files;
24use unused_members::find_unused_members;
25
26pub(crate) type LineOffsetsMap<'a> = FxHashMap<FileId, &'a [u32]>;
29
30pub(crate) fn byte_offset_to_line_col(
33 line_offsets_map: &LineOffsetsMap<'_>,
34 file_id: FileId,
35 byte_offset: u32,
36) -> (u32, u32) {
37 line_offsets_map
38 .get(&file_id)
39 .map_or((1, byte_offset), |offsets| {
40 fallow_types::extract::byte_offset_to_line_col(offsets, byte_offset)
41 })
42}
43
44fn read_source(path: &std::path::Path) -> String {
48 std::fs::read_to_string(path).unwrap_or_default()
49}
50
51pub fn find_dead_code(graph: &ModuleGraph, config: &ResolvedConfig) -> AnalysisResults {
53 find_dead_code_with_resolved(graph, config, &[], None)
54}
55
56pub fn find_dead_code_with_resolved(
58 graph: &ModuleGraph,
59 config: &ResolvedConfig,
60 resolved_modules: &[ResolvedModule],
61 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
62) -> AnalysisResults {
63 find_dead_code_full(
64 graph,
65 config,
66 resolved_modules,
67 plugin_result,
68 &[],
69 &[],
70 false,
71 )
72}
73
74pub fn find_dead_code_full(
76 graph: &ModuleGraph,
77 config: &ResolvedConfig,
78 resolved_modules: &[ResolvedModule],
79 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
80 workspaces: &[fallow_config::WorkspaceInfo],
81 modules: &[ModuleInfo],
82 collect_usages: bool,
83) -> AnalysisResults {
84 let _span = tracing::info_span!("find_dead_code").entered();
85
86 let suppressions_by_file: FxHashMap<FileId, &[Suppression]> = modules
88 .iter()
89 .filter(|m| !m.suppressions.is_empty())
90 .map(|m| (m.file_id, m.suppressions.as_slice()))
91 .collect();
92
93 let line_offsets_by_file: LineOffsetsMap<'_> = modules
96 .iter()
97 .filter(|m| !m.line_offsets.is_empty())
98 .map(|m| (m.file_id, m.line_offsets.as_slice()))
99 .collect();
100
101 let mut results = AnalysisResults::default();
102
103 if config.rules.unused_files != Severity::Off {
104 results.unused_files = find_unused_files(graph, &suppressions_by_file);
105 }
106
107 if config.rules.unused_exports != Severity::Off || config.rules.unused_types != Severity::Off {
108 let (exports, types) = find_unused_exports(
109 graph,
110 config,
111 plugin_result,
112 &suppressions_by_file,
113 &line_offsets_by_file,
114 );
115 if config.rules.unused_exports != Severity::Off {
116 results.unused_exports = exports;
117 }
118 if config.rules.unused_types != Severity::Off {
119 results.unused_types = types;
120 }
121 }
122
123 if config.rules.unused_enum_members != Severity::Off
124 || config.rules.unused_class_members != Severity::Off
125 {
126 let (enum_members, class_members) = find_unused_members(
127 graph,
128 config,
129 resolved_modules,
130 &suppressions_by_file,
131 &line_offsets_by_file,
132 );
133 if config.rules.unused_enum_members != Severity::Off {
134 results.unused_enum_members = enum_members;
135 }
136 if config.rules.unused_class_members != Severity::Off {
137 results.unused_class_members = class_members;
138 }
139 }
140
141 let pkg_path = config.root.join("package.json");
143 let pkg = PackageJson::load(&pkg_path).ok();
144 if let Some(ref pkg) = pkg {
145 if config.rules.unused_dependencies != Severity::Off
146 || config.rules.unused_dev_dependencies != Severity::Off
147 || config.rules.unused_optional_dependencies != Severity::Off
148 {
149 let (deps, dev_deps, optional_deps) =
150 find_unused_dependencies(graph, pkg, config, plugin_result, workspaces);
151 if config.rules.unused_dependencies != Severity::Off {
152 results.unused_dependencies = deps;
153 }
154 if config.rules.unused_dev_dependencies != Severity::Off {
155 results.unused_dev_dependencies = dev_deps;
156 }
157 if config.rules.unused_optional_dependencies != Severity::Off {
158 results.unused_optional_dependencies = optional_deps;
159 }
160 }
161
162 if config.rules.unlisted_dependencies != Severity::Off {
163 results.unlisted_dependencies =
164 find_unlisted_dependencies(graph, pkg, config, workspaces, plugin_result);
165 }
166 }
167
168 if config.rules.unresolved_imports != Severity::Off && !resolved_modules.is_empty() {
169 let virtual_prefixes: Vec<&str> = plugin_result
170 .map(|pr| {
171 pr.virtual_module_prefixes
172 .iter()
173 .map(|s| s.as_str())
174 .collect()
175 })
176 .unwrap_or_default();
177 results.unresolved_imports = find_unresolved_imports(
178 resolved_modules,
179 config,
180 &suppressions_by_file,
181 &virtual_prefixes,
182 &line_offsets_by_file,
183 );
184 }
185
186 if config.rules.duplicate_exports != Severity::Off {
187 results.duplicate_exports =
188 find_duplicate_exports(graph, config, &suppressions_by_file, &line_offsets_by_file);
189 }
190
191 if config.production
193 && let Some(ref pkg) = pkg
194 {
195 results.type_only_dependencies =
196 find_type_only_dependencies(graph, pkg, config, workspaces);
197 }
198
199 if config.rules.circular_dependencies != Severity::Off {
201 let cycles = graph.find_cycles();
202 results.circular_dependencies = cycles
203 .into_iter()
204 .filter(|cycle| {
205 !cycle.iter().any(|&id| {
207 suppressions_by_file.get(&id).is_some_and(|supps| {
208 suppress::is_file_suppressed(supps, IssueKind::CircularDependency)
209 })
210 })
211 })
212 .map(|cycle| {
213 let files: Vec<std::path::PathBuf> = cycle
214 .iter()
215 .map(|&id| graph.modules[id.0 as usize].path.clone())
216 .collect();
217 let length = files.len();
218 CircularDependency { files, length }
219 })
220 .collect();
221 }
222
223 if collect_usages {
226 results.export_usages = collect_export_usages(graph, &line_offsets_by_file);
227 }
228
229 results
230}
231
232#[cfg(test)]
233mod tests {
234 use fallow_types::extract::{byte_offset_to_line_col, compute_line_offsets};
235
236 fn line_col(source: &str, byte_offset: u32) -> (u32, u32) {
238 let offsets = compute_line_offsets(source);
239 byte_offset_to_line_col(&offsets, byte_offset)
240 }
241
242 #[test]
245 fn compute_offsets_empty() {
246 assert_eq!(compute_line_offsets(""), vec![0]);
247 }
248
249 #[test]
250 fn compute_offsets_single_line() {
251 assert_eq!(compute_line_offsets("hello"), vec![0]);
252 }
253
254 #[test]
255 fn compute_offsets_multiline() {
256 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
257 }
258
259 #[test]
260 fn compute_offsets_trailing_newline() {
261 assert_eq!(compute_line_offsets("abc\n"), vec![0, 4]);
262 }
263
264 #[test]
265 fn compute_offsets_crlf() {
266 assert_eq!(compute_line_offsets("ab\r\ncd"), vec![0, 4]);
267 }
268
269 #[test]
270 fn compute_offsets_consecutive_newlines() {
271 assert_eq!(compute_line_offsets("\n\n"), vec![0, 1, 2]);
272 }
273
274 #[test]
277 fn byte_offset_empty_source() {
278 assert_eq!(line_col("", 0), (1, 0));
279 }
280
281 #[test]
282 fn byte_offset_single_line_start() {
283 assert_eq!(line_col("hello", 0), (1, 0));
284 }
285
286 #[test]
287 fn byte_offset_single_line_middle() {
288 assert_eq!(line_col("hello", 4), (1, 4));
289 }
290
291 #[test]
292 fn byte_offset_multiline_start_of_line2() {
293 assert_eq!(line_col("line1\nline2\nline3", 6), (2, 0));
294 }
295
296 #[test]
297 fn byte_offset_multiline_middle_of_line3() {
298 assert_eq!(line_col("line1\nline2\nline3", 14), (3, 2));
299 }
300
301 #[test]
302 fn byte_offset_at_newline_boundary() {
303 assert_eq!(line_col("line1\nline2", 5), (1, 5));
304 }
305
306 #[test]
307 fn byte_offset_multibyte_utf8() {
308 let source = "hi\n\u{1F600}x";
309 assert_eq!(line_col(source, 3), (2, 0));
310 assert_eq!(line_col(source, 7), (2, 4));
311 }
312
313 #[test]
314 fn byte_offset_multibyte_accented_chars() {
315 let source = "caf\u{00E9}\nbar";
316 assert_eq!(line_col(source, 6), (2, 0));
317 assert_eq!(line_col(source, 3), (1, 3));
318 }
319
320 #[test]
321 fn byte_offset_via_map_fallback() {
322 use super::*;
323 let map: LineOffsetsMap<'_> = FxHashMap::default();
324 assert_eq!(
325 super::byte_offset_to_line_col(&map, FileId(99), 42),
326 (1, 42)
327 );
328 }
329
330 #[test]
331 fn byte_offset_via_map_lookup() {
332 use super::*;
333 let offsets = compute_line_offsets("abc\ndef\nghi");
334 let mut map: LineOffsetsMap<'_> = FxHashMap::default();
335 map.insert(FileId(0), &offsets);
336 assert_eq!(super::byte_offset_to_line_col(&map, FileId(0), 5), (2, 1));
337 }
338}