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