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