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