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