1mod boundary;
2pub mod feature_flags;
3mod package_json_utils;
4mod predicates;
5mod re_export_cycles;
6mod unused_catalog;
7mod unused_deps;
8mod unused_exports;
9mod unused_files;
10mod unused_members;
11mod unused_overrides;
12
13use rustc_hash::FxHashMap;
14
15use fallow_config::{PackageJson, ResolvedConfig, Severity};
16
17use crate::discover::FileId;
18use crate::extract::ModuleInfo;
19use crate::graph::ModuleGraph;
20use crate::resolve::ResolvedModule;
21use fallow_types::output_dead_code::{
22 BoundaryViolationFinding, CircularDependencyFinding, DuplicateExportFinding,
23 EmptyCatalogGroupFinding, MisconfiguredDependencyOverrideFinding, PrivateTypeLeakFinding,
24 ReExportCycleFinding, TestOnlyDependencyFinding, TypeOnlyDependencyFinding,
25 UnlistedDependencyFinding, UnresolvedCatalogReferenceFinding, UnresolvedImportFinding,
26 UnusedCatalogEntryFinding, UnusedClassMemberFinding, UnusedDependencyFinding,
27 UnusedDependencyOverrideFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding,
28 UnusedExportFinding, UnusedFileFinding, UnusedOptionalDependencyFinding, UnusedTypeFinding,
29};
30
31use crate::results::{AnalysisResults, CircularDependency};
32use crate::suppress::IssueKind;
33
34use re_export_cycles::find_re_export_cycles;
35#[expect(
36 deprecated,
37 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
38)]
39use unused_catalog::{
40 find_empty_catalog_groups, find_unresolved_catalog_references, find_unused_catalog_entries,
41 gather_pnpm_catalog_state,
42};
43#[expect(
44 deprecated,
45 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
46)]
47use unused_deps::{
48 find_test_only_dependencies, find_type_only_dependencies, find_unlisted_dependencies,
49 find_unresolved_imports, find_unused_dependencies,
50};
51#[expect(
52 deprecated,
53 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
54)]
55use unused_exports::{
56 collect_export_usages, find_duplicate_exports, find_private_type_leaks, find_unused_exports,
57 suppress_signature_backing_types,
58};
59#[expect(
60 deprecated,
61 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
62)]
63use unused_files::find_unused_files;
64#[expect(
65 deprecated,
66 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
67)]
68use unused_members::find_unused_members;
69#[expect(
70 deprecated,
71 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
72)]
73use unused_overrides::{
74 find_misconfigured_dependency_overrides, find_unused_dependency_overrides,
75 gather_pnpm_override_state,
76};
77
78#[doc(hidden)]
81pub type LineOffsetsMap<'a> = FxHashMap<FileId, &'a [u32]>;
82
83#[doc(hidden)]
86pub fn byte_offset_to_line_col(
87 line_offsets_map: &LineOffsetsMap<'_>,
88 file_id: FileId,
89 byte_offset: u32,
90) -> (u32, u32) {
91 line_offsets_map
92 .get(&file_id)
93 .map_or((1, byte_offset), |offsets| {
94 fallow_types::extract::byte_offset_to_line_col(offsets, byte_offset)
95 })
96}
97
98fn cycle_edge_line_col(
99 graph: &ModuleGraph,
100 line_offsets_map: &LineOffsetsMap<'_>,
101 cycle: &[FileId],
102 edge_index: usize,
103) -> Option<(u32, u32)> {
104 if cycle.is_empty() {
105 return None;
106 }
107
108 let from = cycle[edge_index];
109 let to = cycle[(edge_index + 1) % cycle.len()];
110 graph
111 .find_import_span_start(from, to)
112 .map(|span_start| byte_offset_to_line_col(line_offsets_map, from, span_start))
113}
114
115fn is_circular_dependency_suppressed(
116 graph: &ModuleGraph,
117 line_offsets_map: &LineOffsetsMap<'_>,
118 suppressions: &crate::suppress::SuppressionContext<'_>,
119 cycle: &[FileId],
120) -> bool {
121 if cycle
122 .iter()
123 .any(|&id| suppressions.is_file_suppressed(id, IssueKind::CircularDependency))
124 {
125 return true;
126 }
127
128 let mut line_suppressed = false;
129 for edge_index in 0..cycle.len() {
130 let from = cycle[edge_index];
131 if let Some((line, _)) = cycle_edge_line_col(graph, line_offsets_map, cycle, edge_index)
132 && suppressions.is_suppressed(from, line, IssueKind::CircularDependency)
133 {
134 line_suppressed = true;
135 }
136 }
137 line_suppressed
138}
139
140fn read_source(path: &std::path::Path) -> String {
144 std::fs::read_to_string(path).unwrap_or_default()
145}
146
147fn is_cross_package_cycle(
152 files: &[std::path::PathBuf],
153 workspaces: &[fallow_config::WorkspaceInfo],
154) -> bool {
155 let find_workspace = |path: &std::path::Path| -> Option<&std::path::Path> {
156 workspaces
157 .iter()
158 .map(|w| w.root.as_path())
159 .filter(|root| path.starts_with(root))
160 .max_by_key(|root| root.components().count())
161 };
162
163 let mut seen_workspace: Option<&std::path::Path> = None;
164 for file in files {
165 if let Some(ws) = find_workspace(file) {
166 match &seen_workspace {
167 None => seen_workspace = Some(ws),
168 Some(prev) if *prev != ws => return true,
169 _ => {}
170 }
171 }
172 }
173 false
174}
175
176fn public_workspace_roots<'a>(
177 public_packages: &[String],
178 workspaces: &'a [fallow_config::WorkspaceInfo],
179) -> Vec<&'a std::path::Path> {
180 if public_packages.is_empty() || workspaces.is_empty() {
181 return Vec::new();
182 }
183
184 workspaces
185 .iter()
186 .filter(|ws| {
187 public_packages.iter().any(|pattern| {
188 ws.name == *pattern
189 || globset::Glob::new(pattern)
190 .ok()
191 .is_some_and(|g| g.compile_matcher().is_match(&ws.name))
192 })
193 })
194 .map(|ws| ws.root.as_path())
195 .collect()
196}
197
198fn find_circular_dependencies(
199 graph: &ModuleGraph,
200 line_offsets_map: &LineOffsetsMap<'_>,
201 suppressions: &crate::suppress::SuppressionContext<'_>,
202 workspaces: &[fallow_config::WorkspaceInfo],
203) -> Vec<CircularDependency> {
204 let cycles = graph.find_cycles();
205 let mut dependencies: Vec<CircularDependency> = cycles
206 .into_iter()
207 .filter_map(|cycle| {
208 if is_circular_dependency_suppressed(graph, line_offsets_map, suppressions, &cycle) {
209 return None;
210 }
211
212 let files: Vec<std::path::PathBuf> = cycle
213 .iter()
214 .map(|&id| graph.modules[id.0 as usize].path.clone())
215 .collect();
216 let length = files.len();
217 let (line, col) =
219 cycle_edge_line_col(graph, line_offsets_map, &cycle, 0).unwrap_or((1, 0));
220 Some(CircularDependency {
221 files,
222 length,
223 line,
224 col,
225 is_cross_package: false,
226 })
227 })
228 .collect();
229
230 if !workspaces.is_empty() {
232 for dep in &mut dependencies {
233 dep.is_cross_package = is_cross_package_cycle(&dep.files, workspaces);
234 }
235 }
236
237 dependencies
238}
239
240fn run_circular_dep_detector(
245 graph: &ModuleGraph,
246 config: &ResolvedConfig,
247 line_offsets_by_file: &LineOffsetsMap<'_>,
248 suppressions: &crate::suppress::SuppressionContext<'_>,
249 workspaces: &[fallow_config::WorkspaceInfo],
250) -> Vec<CircularDependencyFinding> {
251 if config.rules.circular_dependencies == Severity::Off {
252 return Vec::new();
253 }
254 find_circular_dependencies(graph, line_offsets_by_file, suppressions, workspaces)
255 .into_iter()
256 .map(CircularDependencyFinding::with_actions)
257 .collect()
258}
259
260fn run_re_export_cycle_detector(
263 graph: &ModuleGraph,
264 config: &ResolvedConfig,
265 suppressions: &crate::suppress::SuppressionContext<'_>,
266) -> Vec<ReExportCycleFinding> {
267 if config.rules.re_export_cycle == Severity::Off {
268 return Vec::new();
269 }
270 find_re_export_cycles(graph, suppressions)
271}
272
273fn run_export_usages_collector(
276 graph: &ModuleGraph,
277 line_offsets_by_file: &LineOffsetsMap<'_>,
278 collect_usages: bool,
279) -> Vec<crate::results::ExportUsage> {
280 if collect_usages {
281 collect_export_usages(graph, line_offsets_by_file)
282 } else {
283 Vec::new()
284 }
285}
286
287#[expect(
289 deprecated,
290 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
291)]
292#[deprecated(
293 since = "2.76.0",
294 note = "fallow_core is internal; use fallow_cli::programmatic::detect_dead_code instead. NOTE: replacement returns serde_json::Value, not typed AnalysisResults. See docs/fallow-core-migration.md and ADR-008."
295)]
296#[expect(
297 clippy::too_many_lines,
298 reason = "orchestration function calling all detectors; each call is one-line and the sequence is easier to follow in one place"
299)]
300pub fn find_dead_code_full(
301 graph: &ModuleGraph,
302 config: &ResolvedConfig,
303 resolved_modules: &[ResolvedModule],
304 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
305 workspaces: &[fallow_config::WorkspaceInfo],
306 modules: &[ModuleInfo],
307 collect_usages: bool,
308) -> AnalysisResults {
309 let _span = tracing::info_span!("find_dead_code").entered();
310
311 let suppressions = crate::suppress::SuppressionContext::new(modules);
313
314 let line_offsets_by_file: LineOffsetsMap<'_> = modules
317 .iter()
318 .filter(|m| !m.line_offsets.is_empty())
319 .map(|m| (m.file_id, m.line_offsets.as_slice()))
320 .collect();
321
322 let pkg_path = config.root.join("package.json");
324 let pkg = PackageJson::load(&pkg_path).ok();
325
326 let mut user_class_members = config.used_class_members.clone();
330 if let Some(plugin_result) = plugin_result {
331 user_class_members.extend(plugin_result.used_class_members.iter().cloned());
332 }
333
334 let virtual_prefixes: Vec<&str> = plugin_result
335 .map(|pr| {
336 pr.virtual_module_prefixes
337 .iter()
338 .map(String::as_str)
339 .collect()
340 })
341 .unwrap_or_default();
342 let generated_patterns: Vec<&str> = plugin_result
343 .map(|pr| {
344 pr.generated_import_patterns
345 .iter()
346 .map(String::as_str)
347 .collect()
348 })
349 .unwrap_or_default();
350
351 let (
352 (unused_files, export_results),
353 (
354 (member_results, dependency_results),
355 (
356 (unresolved_imports, duplicate_exports),
357 (boundary_violations, (circular_dependencies, (re_export_cycles, export_usages))),
358 ),
359 ),
360 ) = rayon::join(
361 || {
362 rayon::join(
363 || {
364 if config.rules.unused_files != Severity::Off {
365 find_unused_files(graph, &suppressions)
366 .into_iter()
367 .map(UnusedFileFinding::with_actions)
368 .collect::<Vec<_>>()
369 } else {
370 Vec::new()
371 }
372 },
373 || {
374 let mut results = AnalysisResults::default();
375 if config.rules.unused_exports != Severity::Off
376 || config.rules.unused_types != Severity::Off
377 || config.rules.private_type_leaks != Severity::Off
378 {
379 let (exports, types, stale_expected) = find_unused_exports(
380 graph,
381 modules,
382 config,
383 plugin_result,
384 &suppressions,
385 &line_offsets_by_file,
386 );
387 if config.rules.unused_exports != Severity::Off {
388 results.unused_exports = exports
389 .into_iter()
390 .map(UnusedExportFinding::with_actions)
391 .collect();
392 }
393 if config.rules.unused_types != Severity::Off {
394 let mut typed = types;
395 suppress_signature_backing_types(&mut typed, graph, modules);
396 results.unused_types = typed
397 .into_iter()
398 .map(UnusedTypeFinding::with_actions)
399 .collect();
400 }
401 if config.rules.private_type_leaks != Severity::Off {
402 results.private_type_leaks = find_private_type_leaks(
403 graph,
404 modules,
405 config,
406 &suppressions,
407 &line_offsets_by_file,
408 )
409 .into_iter()
410 .map(PrivateTypeLeakFinding::with_actions)
411 .collect();
412 }
413 if config.rules.stale_suppressions != Severity::Off {
415 results.stale_suppressions.extend(stale_expected);
416 }
417 }
418 results
419 },
420 )
421 },
422 || {
423 rayon::join(
424 || {
425 rayon::join(
426 || {
427 let mut results = AnalysisResults::default();
428 if config.rules.unused_enum_members != Severity::Off
429 || config.rules.unused_class_members != Severity::Off
430 {
431 let (enum_members, class_members) = find_unused_members(
432 graph,
433 resolved_modules,
434 modules,
435 &suppressions,
436 &line_offsets_by_file,
437 &user_class_members,
438 &config.ignore_decorators,
439 );
440 if config.rules.unused_enum_members != Severity::Off {
441 results.unused_enum_members = enum_members
442 .into_iter()
443 .map(UnusedEnumMemberFinding::with_actions)
444 .collect();
445 }
446 if config.rules.unused_class_members != Severity::Off {
447 results.unused_class_members = class_members
448 .into_iter()
449 .map(UnusedClassMemberFinding::with_actions)
450 .collect();
451 }
452 }
453 results
454 },
455 || {
456 let mut results = AnalysisResults::default();
457 if let Some(ref pkg) = pkg {
458 if config.rules.unused_dependencies != Severity::Off
459 || config.rules.unused_dev_dependencies != Severity::Off
460 || config.rules.unused_optional_dependencies != Severity::Off
461 {
462 let (deps, dev_deps, optional_deps) = find_unused_dependencies(
463 graph,
464 pkg,
465 config,
466 plugin_result,
467 workspaces,
468 );
469 if config.rules.unused_dependencies != Severity::Off {
470 results.unused_dependencies = deps
471 .into_iter()
472 .map(UnusedDependencyFinding::with_actions)
473 .collect();
474 }
475 if config.rules.unused_dev_dependencies != Severity::Off {
476 results.unused_dev_dependencies = dev_deps
477 .into_iter()
478 .map(UnusedDevDependencyFinding::with_actions)
479 .collect();
480 }
481 if config.rules.unused_optional_dependencies != Severity::Off {
482 results.unused_optional_dependencies = optional_deps
483 .into_iter()
484 .map(UnusedOptionalDependencyFinding::with_actions)
485 .collect();
486 }
487 }
488
489 if config.rules.unlisted_dependencies != Severity::Off {
490 results.unlisted_dependencies = find_unlisted_dependencies(
491 graph,
492 pkg,
493 config,
494 workspaces,
495 plugin_result,
496 resolved_modules,
497 &line_offsets_by_file,
498 )
499 .into_iter()
500 .map(UnlistedDependencyFinding::with_actions)
501 .collect();
502 }
503
504 if config.production {
507 results.type_only_dependencies =
508 find_type_only_dependencies(graph, pkg, config, workspaces)
509 .into_iter()
510 .map(TypeOnlyDependencyFinding::with_actions)
511 .collect();
512 }
513
514 if !config.production
517 && config.rules.test_only_dependencies != Severity::Off
518 {
519 results.test_only_dependencies =
520 find_test_only_dependencies(graph, pkg, config, workspaces)
521 .into_iter()
522 .map(TestOnlyDependencyFinding::with_actions)
523 .collect();
524 }
525 }
526 results
527 },
528 )
529 },
530 || {
531 rayon::join(
532 || {
533 rayon::join(
534 || {
535 if config.rules.unresolved_imports != Severity::Off
536 && !resolved_modules.is_empty()
537 {
538 find_unresolved_imports(
539 resolved_modules,
540 config,
541 &suppressions,
542 &virtual_prefixes,
543 &generated_patterns,
544 &line_offsets_by_file,
545 )
546 .into_iter()
547 .map(UnresolvedImportFinding::with_actions)
548 .collect::<Vec<_>>()
549 } else {
550 Vec::new()
551 }
552 },
553 || {
554 if config.rules.duplicate_exports != Severity::Off {
555 find_duplicate_exports(
556 graph,
557 config,
558 &suppressions,
559 &line_offsets_by_file,
560 resolved_modules,
561 )
562 .into_iter()
563 .map(DuplicateExportFinding::with_actions)
564 .collect::<Vec<_>>()
565 } else {
566 Vec::new()
567 }
568 },
569 )
570 },
571 || {
572 rayon::join(
573 || {
574 if config.rules.boundary_violation != Severity::Off
575 && !config.boundaries.is_empty()
576 {
577 boundary::find_boundary_violations(
578 graph,
579 config,
580 &suppressions,
581 &line_offsets_by_file,
582 )
583 .into_iter()
584 .map(BoundaryViolationFinding::with_actions)
585 .collect::<Vec<_>>()
586 } else {
587 Vec::new()
588 }
589 },
590 || {
591 rayon::join(
592 || {
593 run_circular_dep_detector(
594 graph,
595 config,
596 &line_offsets_by_file,
597 &suppressions,
598 workspaces,
599 )
600 },
601 || {
602 rayon::join(
603 || {
604 run_re_export_cycle_detector(
605 graph,
606 config,
607 &suppressions,
608 )
609 },
610 || {
611 run_export_usages_collector(
612 graph,
613 &line_offsets_by_file,
614 collect_usages,
615 )
616 },
617 )
618 },
619 )
620 },
621 )
622 },
623 )
624 },
625 )
626 },
627 );
628
629 let mut results = AnalysisResults {
630 unused_files,
631 unused_exports: export_results.unused_exports,
632 unused_types: export_results.unused_types,
633 private_type_leaks: export_results.private_type_leaks,
634 stale_suppressions: export_results.stale_suppressions,
635 unused_enum_members: member_results.unused_enum_members,
636 unused_class_members: member_results.unused_class_members,
637 unused_dependencies: dependency_results.unused_dependencies,
638 unused_dev_dependencies: dependency_results.unused_dev_dependencies,
639 unused_optional_dependencies: dependency_results.unused_optional_dependencies,
640 unlisted_dependencies: dependency_results.unlisted_dependencies,
641 type_only_dependencies: dependency_results.type_only_dependencies,
642 test_only_dependencies: dependency_results.test_only_dependencies,
643 unresolved_imports,
644 duplicate_exports,
645 boundary_violations,
646 circular_dependencies,
647 re_export_cycles,
648 export_usages,
649 ..AnalysisResults::default()
650 };
651
652 let public_roots = public_workspace_roots(&config.public_packages, workspaces);
655 if !public_roots.is_empty() {
656 results.unused_exports.retain(|e| {
657 !public_roots
658 .iter()
659 .any(|root| e.export.path.starts_with(root))
660 });
661 results.unused_types.retain(|e| {
662 !public_roots
663 .iter()
664 .any(|root| e.export.path.starts_with(root))
665 });
666 results.unused_enum_members.retain(|e| {
667 !public_roots
668 .iter()
669 .any(|root| e.member.path.starts_with(root))
670 });
671 results.unused_class_members.retain(|e| {
672 !public_roots
673 .iter()
674 .any(|root| e.member.path.starts_with(root))
675 });
676 }
677
678 if config.rules.stale_suppressions != Severity::Off {
680 results
681 .stale_suppressions
682 .extend(suppressions.find_stale(graph, config));
683 }
684 results.suppression_count = suppressions.used_count();
685
686 let need_unused_catalogs = config.rules.unused_catalog_entries != Severity::Off;
690 let need_empty_catalog_groups = config.rules.empty_catalog_groups != Severity::Off;
691 let need_unresolved_refs = config.rules.unresolved_catalog_references != Severity::Off;
692 if (need_unused_catalogs || need_empty_catalog_groups || need_unresolved_refs)
693 && let Some(state) = gather_pnpm_catalog_state(config, workspaces)
694 {
695 if need_unused_catalogs {
696 results.unused_catalog_entries = find_unused_catalog_entries(&state)
697 .into_iter()
698 .map(UnusedCatalogEntryFinding::with_actions)
699 .collect();
700 }
701 if need_empty_catalog_groups {
702 results.empty_catalog_groups = find_empty_catalog_groups(&state)
703 .into_iter()
704 .map(EmptyCatalogGroupFinding::with_actions)
705 .collect();
706 }
707 if need_unresolved_refs {
708 results.unresolved_catalog_references = find_unresolved_catalog_references(
709 &state,
710 &config.compiled_ignore_catalog_references,
711 &config.root,
712 )
713 .into_iter()
714 .map(UnresolvedCatalogReferenceFinding::with_actions)
715 .collect();
716 }
717 }
718
719 let need_unused_overrides = config.rules.unused_dependency_overrides != Severity::Off;
725 let need_misconfigured_overrides =
726 config.rules.misconfigured_dependency_overrides != Severity::Off;
727 if (need_unused_overrides || need_misconfigured_overrides)
728 && let Some(state) = gather_pnpm_override_state(config, workspaces)
729 {
730 if need_unused_overrides {
731 results.unused_dependency_overrides = find_unused_dependency_overrides(&state, config)
732 .into_iter()
733 .map(UnusedDependencyOverrideFinding::with_actions)
734 .collect();
735 }
736 if need_misconfigured_overrides {
737 results.misconfigured_dependency_overrides =
738 find_misconfigured_dependency_overrides(&state, config)
739 .into_iter()
740 .map(MisconfiguredDependencyOverrideFinding::with_actions)
741 .collect();
742 }
743 }
744
745 results.sort();
749
750 results
751}
752
753#[cfg(test)]
754#[expect(
755 deprecated,
756 reason = "ADR-008 keeps direct analyzer unit tests while the public warning targets external callers"
757)]
758mod tests {
759 use fallow_types::extract::{byte_offset_to_line_col, compute_line_offsets};
760
761 fn line_col(source: &str, byte_offset: u32) -> (u32, u32) {
763 let offsets = compute_line_offsets(source);
764 byte_offset_to_line_col(&offsets, byte_offset)
765 }
766
767 #[test]
770 fn compute_offsets_empty() {
771 assert_eq!(compute_line_offsets(""), vec![0]);
772 }
773
774 #[test]
775 fn compute_offsets_single_line() {
776 assert_eq!(compute_line_offsets("hello"), vec![0]);
777 }
778
779 #[test]
780 fn compute_offsets_multiline() {
781 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
782 }
783
784 #[test]
785 fn compute_offsets_trailing_newline() {
786 assert_eq!(compute_line_offsets("abc\n"), vec![0, 4]);
787 }
788
789 #[test]
790 fn compute_offsets_crlf() {
791 assert_eq!(compute_line_offsets("ab\r\ncd"), vec![0, 4]);
792 }
793
794 #[test]
795 fn compute_offsets_consecutive_newlines() {
796 assert_eq!(compute_line_offsets("\n\n"), vec![0, 1, 2]);
797 }
798
799 #[test]
802 fn byte_offset_empty_source() {
803 assert_eq!(line_col("", 0), (1, 0));
804 }
805
806 #[test]
807 fn byte_offset_single_line_start() {
808 assert_eq!(line_col("hello", 0), (1, 0));
809 }
810
811 #[test]
812 fn byte_offset_single_line_middle() {
813 assert_eq!(line_col("hello", 4), (1, 4));
814 }
815
816 #[test]
817 fn byte_offset_multiline_start_of_line2() {
818 assert_eq!(line_col("line1\nline2\nline3", 6), (2, 0));
819 }
820
821 #[test]
822 fn byte_offset_multiline_middle_of_line3() {
823 assert_eq!(line_col("line1\nline2\nline3", 14), (3, 2));
824 }
825
826 #[test]
827 fn byte_offset_at_newline_boundary() {
828 assert_eq!(line_col("line1\nline2", 5), (1, 5));
829 }
830
831 #[test]
832 fn byte_offset_multibyte_utf8() {
833 let source = "hi\n\u{1F600}x";
834 assert_eq!(line_col(source, 3), (2, 0));
835 assert_eq!(line_col(source, 7), (2, 4));
836 }
837
838 #[test]
839 fn byte_offset_multibyte_accented_chars() {
840 let source = "caf\u{00E9}\nbar";
841 assert_eq!(line_col(source, 6), (2, 0));
842 assert_eq!(line_col(source, 3), (1, 3));
843 }
844
845 #[test]
846 fn byte_offset_via_map_fallback() {
847 use super::*;
848 let map: LineOffsetsMap<'_> = FxHashMap::default();
849 assert_eq!(
850 super::byte_offset_to_line_col(&map, FileId(99), 42),
851 (1, 42)
852 );
853 }
854
855 #[test]
856 fn byte_offset_via_map_lookup() {
857 use super::*;
858 let offsets = compute_line_offsets("abc\ndef\nghi");
859 let mut map: LineOffsetsMap<'_> = FxHashMap::default();
860 map.insert(FileId(0), &offsets);
861 assert_eq!(super::byte_offset_to_line_col(&map, FileId(0), 5), (2, 1));
862 }
863
864 mod orchestration {
867 use super::super::*;
868 use fallow_config::{FallowConfig, OutputFormat, RulesConfig, Severity};
869 use std::path::PathBuf;
870
871 fn find_dead_code(graph: &ModuleGraph, config: &ResolvedConfig) -> AnalysisResults {
872 find_dead_code_full(graph, config, &[], None, &[], &[], false)
873 }
874
875 fn make_config_with_rules(rules: RulesConfig) -> ResolvedConfig {
876 FallowConfig {
877 rules,
878 ..Default::default()
879 }
880 .resolve(
881 PathBuf::from("/tmp/orchestration-test"),
882 OutputFormat::Human,
883 1,
884 true,
885 true,
886 None,
887 )
888 }
889
890 #[test]
891 fn find_dead_code_all_rules_off_returns_empty() {
892 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
893 use crate::graph::ModuleGraph;
894 use crate::resolve::ResolvedModule;
895 use rustc_hash::FxHashSet;
896
897 let files = vec![DiscoveredFile {
898 id: FileId(0),
899 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
900 size_bytes: 100,
901 }];
902 let entry_points = vec![EntryPoint {
903 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
904 source: EntryPointSource::ManualEntry,
905 }];
906 let resolved = vec![ResolvedModule {
907 file_id: FileId(0),
908 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
909 exports: vec![],
910 re_exports: vec![],
911 resolved_imports: vec![],
912 resolved_dynamic_imports: vec![],
913 resolved_dynamic_patterns: vec![],
914 member_accesses: vec![],
915 whole_object_uses: vec![],
916 has_cjs_exports: false,
917 has_angular_component_template_url: false,
918 unused_import_bindings: FxHashSet::default(),
919 type_referenced_import_bindings: vec![],
920 value_referenced_import_bindings: vec![],
921 namespace_object_aliases: vec![],
922 }];
923 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
924
925 let rules = RulesConfig {
926 unused_files: Severity::Off,
927 unused_exports: Severity::Off,
928 unused_types: Severity::Off,
929 private_type_leaks: Severity::Off,
930 unused_dependencies: Severity::Off,
931 unused_dev_dependencies: Severity::Off,
932 unused_optional_dependencies: Severity::Off,
933 unused_enum_members: Severity::Off,
934 unused_class_members: Severity::Off,
935 unresolved_imports: Severity::Off,
936 unlisted_dependencies: Severity::Off,
937 duplicate_exports: Severity::Off,
938 type_only_dependencies: Severity::Off,
939 circular_dependencies: Severity::Off,
940 re_export_cycle: Severity::Off,
941 test_only_dependencies: Severity::Off,
942 boundary_violation: Severity::Off,
943 coverage_gaps: Severity::Off,
944 feature_flags: Severity::Off,
945 stale_suppressions: Severity::Off,
946 unused_catalog_entries: Severity::Off,
947 empty_catalog_groups: Severity::Off,
948 unresolved_catalog_references: Severity::Off,
949 unused_dependency_overrides: Severity::Off,
950 misconfigured_dependency_overrides: Severity::Off,
951 };
952 let config = make_config_with_rules(rules);
953 let results = find_dead_code(&graph, &config);
954
955 assert!(results.unused_files.is_empty());
956 assert!(results.unused_exports.is_empty());
957 assert!(results.unused_types.is_empty());
958 assert!(results.unused_dependencies.is_empty());
959 assert!(results.unused_dev_dependencies.is_empty());
960 assert!(results.unused_optional_dependencies.is_empty());
961 assert!(results.unused_enum_members.is_empty());
962 assert!(results.unused_class_members.is_empty());
963 assert!(results.unresolved_imports.is_empty());
964 assert!(results.unlisted_dependencies.is_empty());
965 assert!(results.duplicate_exports.is_empty());
966 assert!(results.circular_dependencies.is_empty());
967 assert!(results.export_usages.is_empty());
968 }
969
970 #[test]
971 fn find_dead_code_full_collect_usages_flag() {
972 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
973 use crate::extract::{ExportName, VisibilityTag};
974 use crate::graph::{ExportSymbol, ModuleGraph};
975 use crate::resolve::ResolvedModule;
976 use oxc_span::Span;
977 use rustc_hash::FxHashSet;
978
979 let files = vec![DiscoveredFile {
980 id: FileId(0),
981 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
982 size_bytes: 100,
983 }];
984 let entry_points = vec![EntryPoint {
985 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
986 source: EntryPointSource::ManualEntry,
987 }];
988 let resolved = vec![ResolvedModule {
989 file_id: FileId(0),
990 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
991 exports: vec![],
992 re_exports: vec![],
993 resolved_imports: vec![],
994 resolved_dynamic_imports: vec![],
995 resolved_dynamic_patterns: vec![],
996 member_accesses: vec![],
997 whole_object_uses: vec![],
998 has_cjs_exports: false,
999 has_angular_component_template_url: false,
1000 unused_import_bindings: FxHashSet::default(),
1001 type_referenced_import_bindings: vec![],
1002 value_referenced_import_bindings: vec![],
1003 namespace_object_aliases: vec![],
1004 }];
1005 let mut graph = ModuleGraph::build(&resolved, &entry_points, &files);
1006 graph.modules[0].exports = vec![ExportSymbol {
1007 name: ExportName::Named("myExport".to_string()),
1008 is_type_only: false,
1009 is_side_effect_used: false,
1010 visibility: VisibilityTag::None,
1011 span: Span::new(10, 30),
1012 references: vec![],
1013 members: vec![],
1014 }];
1015
1016 let rules = RulesConfig::default();
1017 let config = make_config_with_rules(rules);
1018
1019 let results_no_collect = find_dead_code_full(
1021 &graph,
1022 &config,
1023 &[],
1024 None,
1025 &[],
1026 &[],
1027 false, );
1029 assert!(
1030 results_no_collect.export_usages.is_empty(),
1031 "export_usages should be empty when collect_usages is false"
1032 );
1033
1034 let results_with_collect = find_dead_code_full(
1036 &graph,
1037 &config,
1038 &[],
1039 None,
1040 &[],
1041 &[],
1042 true, );
1044 assert!(
1045 !results_with_collect.export_usages.is_empty(),
1046 "export_usages should be populated when collect_usages is true"
1047 );
1048 assert_eq!(
1049 results_with_collect.export_usages[0].export_name,
1050 "myExport"
1051 );
1052 }
1053
1054 #[test]
1055 fn find_dead_code_delegates_to_find_dead_code_with_resolved() {
1056 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
1057 use crate::graph::ModuleGraph;
1058 use crate::resolve::ResolvedModule;
1059 use rustc_hash::FxHashSet;
1060
1061 let files = vec![DiscoveredFile {
1062 id: FileId(0),
1063 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1064 size_bytes: 100,
1065 }];
1066 let entry_points = vec![EntryPoint {
1067 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1068 source: EntryPointSource::ManualEntry,
1069 }];
1070 let resolved = vec![ResolvedModule {
1071 file_id: FileId(0),
1072 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1073 exports: vec![],
1074 re_exports: vec![],
1075 resolved_imports: vec![],
1076 resolved_dynamic_imports: vec![],
1077 resolved_dynamic_patterns: vec![],
1078 member_accesses: vec![],
1079 whole_object_uses: vec![],
1080 has_cjs_exports: false,
1081 has_angular_component_template_url: false,
1082 unused_import_bindings: FxHashSet::default(),
1083 type_referenced_import_bindings: vec![],
1084 value_referenced_import_bindings: vec![],
1085 namespace_object_aliases: vec![],
1086 }];
1087 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
1088 let config = make_config_with_rules(RulesConfig::default());
1089
1090 let results = find_dead_code(&graph, &config);
1092 assert!(results.unused_exports.is_empty());
1094 }
1095
1096 #[test]
1097 fn suppressions_built_from_modules() {
1098 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
1099 use crate::extract::ModuleInfo;
1100 use crate::graph::ModuleGraph;
1101 use crate::resolve::ResolvedModule;
1102 use crate::suppress::{IssueKind, Suppression};
1103 use rustc_hash::FxHashSet;
1104
1105 let files = vec![
1106 DiscoveredFile {
1107 id: FileId(0),
1108 path: PathBuf::from("/tmp/orchestration-test/src/entry.ts"),
1109 size_bytes: 100,
1110 },
1111 DiscoveredFile {
1112 id: FileId(1),
1113 path: PathBuf::from("/tmp/orchestration-test/src/utils.ts"),
1114 size_bytes: 100,
1115 },
1116 ];
1117 let entry_points = vec![EntryPoint {
1118 path: PathBuf::from("/tmp/orchestration-test/src/entry.ts"),
1119 source: EntryPointSource::ManualEntry,
1120 }];
1121 let resolved = files
1122 .iter()
1123 .map(|f| ResolvedModule {
1124 file_id: f.id,
1125 path: f.path.clone(),
1126 exports: vec![],
1127 re_exports: vec![],
1128 resolved_imports: vec![],
1129 resolved_dynamic_imports: vec![],
1130 resolved_dynamic_patterns: vec![],
1131 member_accesses: vec![],
1132 whole_object_uses: vec![],
1133 has_cjs_exports: false,
1134 has_angular_component_template_url: false,
1135 unused_import_bindings: FxHashSet::default(),
1136 type_referenced_import_bindings: vec![],
1137 value_referenced_import_bindings: vec![],
1138 namespace_object_aliases: vec![],
1139 })
1140 .collect::<Vec<_>>();
1141 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
1142
1143 let modules = vec![ModuleInfo {
1145 file_id: FileId(1),
1146 exports: vec![],
1147 imports: vec![],
1148 re_exports: vec![],
1149 dynamic_imports: vec![],
1150 dynamic_import_patterns: vec![],
1151 require_calls: vec![],
1152 member_accesses: vec![],
1153 whole_object_uses: vec![],
1154 has_cjs_exports: false,
1155 has_angular_component_template_url: false,
1156 content_hash: 0,
1157 suppressions: vec![Suppression {
1158 line: 0,
1159 comment_line: 1,
1160 kind: Some(IssueKind::UnusedFile),
1161 }],
1162 unknown_suppression_kinds: vec![],
1163 unused_import_bindings: vec![],
1164 type_referenced_import_bindings: vec![],
1165 value_referenced_import_bindings: vec![],
1166 line_offsets: vec![],
1167 complexity: vec![],
1168 flag_uses: vec![],
1169 class_heritage: vec![],
1170 local_type_declarations: Vec::new(),
1171 public_signature_type_references: Vec::new(),
1172 namespace_object_aliases: Vec::new(),
1173 }];
1174
1175 let rules = RulesConfig {
1176 unused_files: Severity::Error,
1177 ..RulesConfig::default()
1178 };
1179 let config = make_config_with_rules(rules);
1180
1181 let results = find_dead_code_full(&graph, &config, &[], None, &[], &modules, false);
1182
1183 assert!(
1188 !results.unused_files.iter().any(|f| f
1189 .file
1190 .path
1191 .to_string_lossy()
1192 .contains("utils.ts")),
1193 "suppressed file should not appear in unused_files"
1194 );
1195 }
1196 }
1197}