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