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 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 = find_unlisted_dependencies(
164 graph,
165 pkg,
166 config,
167 workspaces,
168 plugin_result,
169 resolved_modules,
170 &line_offsets_by_file,
171 );
172 }
173 }
174
175 if config.rules.unresolved_imports != Severity::Off && !resolved_modules.is_empty() {
176 let virtual_prefixes: Vec<&str> = plugin_result
177 .map(|pr| {
178 pr.virtual_module_prefixes
179 .iter()
180 .map(|s| s.as_str())
181 .collect()
182 })
183 .unwrap_or_default();
184 results.unresolved_imports = find_unresolved_imports(
185 resolved_modules,
186 config,
187 &suppressions_by_file,
188 &virtual_prefixes,
189 &line_offsets_by_file,
190 );
191 }
192
193 if config.rules.duplicate_exports != Severity::Off {
194 results.duplicate_exports =
195 find_duplicate_exports(graph, &suppressions_by_file, &line_offsets_by_file);
196 }
197
198 if config.production
200 && let Some(ref pkg) = pkg
201 {
202 results.type_only_dependencies =
203 find_type_only_dependencies(graph, pkg, config, workspaces);
204 }
205
206 if config.rules.circular_dependencies != Severity::Off {
208 let cycles = graph.find_cycles();
209 results.circular_dependencies = cycles
210 .into_iter()
211 .filter(|cycle| {
212 !cycle.iter().any(|&id| {
214 suppressions_by_file.get(&id).is_some_and(|supps| {
215 suppress::is_file_suppressed(supps, IssueKind::CircularDependency)
216 })
217 })
218 })
219 .map(|cycle| {
220 let files: Vec<std::path::PathBuf> = cycle
221 .iter()
222 .map(|&id| graph.modules[id.0 as usize].path.clone())
223 .collect();
224 let length = files.len();
225 let (line, col) = if cycle.len() >= 2 {
227 graph
228 .find_import_span_start(cycle[0], cycle[1])
229 .map_or((1, 0), |span_start| {
230 byte_offset_to_line_col(&line_offsets_by_file, cycle[0], span_start)
231 })
232 } else {
233 (1, 0)
234 };
235 CircularDependency {
236 files,
237 length,
238 line,
239 col,
240 }
241 })
242 .collect();
243 }
244
245 if collect_usages {
248 results.export_usages = collect_export_usages(graph, &line_offsets_by_file);
249 }
250
251 results
252}
253
254#[cfg(test)]
255mod tests {
256 use fallow_types::extract::{byte_offset_to_line_col, compute_line_offsets};
257
258 fn line_col(source: &str, byte_offset: u32) -> (u32, u32) {
260 let offsets = compute_line_offsets(source);
261 byte_offset_to_line_col(&offsets, byte_offset)
262 }
263
264 #[test]
267 fn compute_offsets_empty() {
268 assert_eq!(compute_line_offsets(""), vec![0]);
269 }
270
271 #[test]
272 fn compute_offsets_single_line() {
273 assert_eq!(compute_line_offsets("hello"), vec![0]);
274 }
275
276 #[test]
277 fn compute_offsets_multiline() {
278 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
279 }
280
281 #[test]
282 fn compute_offsets_trailing_newline() {
283 assert_eq!(compute_line_offsets("abc\n"), vec![0, 4]);
284 }
285
286 #[test]
287 fn compute_offsets_crlf() {
288 assert_eq!(compute_line_offsets("ab\r\ncd"), vec![0, 4]);
289 }
290
291 #[test]
292 fn compute_offsets_consecutive_newlines() {
293 assert_eq!(compute_line_offsets("\n\n"), vec![0, 1, 2]);
294 }
295
296 #[test]
299 fn byte_offset_empty_source() {
300 assert_eq!(line_col("", 0), (1, 0));
301 }
302
303 #[test]
304 fn byte_offset_single_line_start() {
305 assert_eq!(line_col("hello", 0), (1, 0));
306 }
307
308 #[test]
309 fn byte_offset_single_line_middle() {
310 assert_eq!(line_col("hello", 4), (1, 4));
311 }
312
313 #[test]
314 fn byte_offset_multiline_start_of_line2() {
315 assert_eq!(line_col("line1\nline2\nline3", 6), (2, 0));
316 }
317
318 #[test]
319 fn byte_offset_multiline_middle_of_line3() {
320 assert_eq!(line_col("line1\nline2\nline3", 14), (3, 2));
321 }
322
323 #[test]
324 fn byte_offset_at_newline_boundary() {
325 assert_eq!(line_col("line1\nline2", 5), (1, 5));
326 }
327
328 #[test]
329 fn byte_offset_multibyte_utf8() {
330 let source = "hi\n\u{1F600}x";
331 assert_eq!(line_col(source, 3), (2, 0));
332 assert_eq!(line_col(source, 7), (2, 4));
333 }
334
335 #[test]
336 fn byte_offset_multibyte_accented_chars() {
337 let source = "caf\u{00E9}\nbar";
338 assert_eq!(line_col(source, 6), (2, 0));
339 assert_eq!(line_col(source, 3), (1, 3));
340 }
341
342 #[test]
343 fn byte_offset_via_map_fallback() {
344 use super::*;
345 let map: LineOffsetsMap<'_> = FxHashMap::default();
346 assert_eq!(
347 super::byte_offset_to_line_col(&map, FileId(99), 42),
348 (1, 42)
349 );
350 }
351
352 #[test]
353 fn byte_offset_via_map_lookup() {
354 use super::*;
355 let offsets = compute_line_offsets("abc\ndef\nghi");
356 let mut map: LineOffsetsMap<'_> = FxHashMap::default();
357 map.insert(FileId(0), &offsets);
358 assert_eq!(super::byte_offset_to_line_col(&map, FileId(0), 5), (2, 1));
359 }
360}