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