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