1mod boundary;
2pub mod feature_flags;
3mod iconify;
4mod package_json_utils;
5mod predicates;
6mod re_export_cycles;
7mod security;
8mod unused_catalog;
9mod unused_deps;
10mod unused_exports;
11mod unused_files;
12mod unused_members;
13mod unused_overrides;
14
15#[cfg(test)]
16pub(crate) use unused_deps::matches_virtual_prefix;
17
18pub use security::catalogue_title as security_catalogue_title;
22pub use security::derive_security_severity;
23
24use rustc_hash::{FxHashMap, FxHashSet};
25
26use fallow_config::{PackageJson, ResolvedConfig, Severity};
27
28use crate::discover::FileId;
29use crate::extract::ModuleInfo;
30use crate::graph::ModuleGraph;
31use crate::resolve::ResolvedModule;
32use fallow_types::output_dead_code::{
33 BoundaryViolationFinding, CircularDependencyFinding, DuplicateExportFinding,
34 EmptyCatalogGroupFinding, MisconfiguredDependencyOverrideFinding, PrivateTypeLeakFinding,
35 ReExportCycleFinding, TestOnlyDependencyFinding, TypeOnlyDependencyFinding,
36 UnlistedDependencyFinding, UnresolvedCatalogReferenceFinding, UnresolvedImportFinding,
37 UnusedCatalogEntryFinding, UnusedClassMemberFinding, UnusedDependencyFinding,
38 UnusedDependencyOverrideFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding,
39 UnusedExportFinding, UnusedFileFinding, UnusedOptionalDependencyFinding, UnusedTypeFinding,
40};
41
42use crate::results::{AnalysisResults, CircularDependency, CircularDependencyEdge};
43use crate::suppress::IssueKind;
44
45use re_export_cycles::find_re_export_cycles;
46#[expect(
47 deprecated,
48 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
49)]
50use unused_catalog::{
51 find_empty_catalog_groups, find_unresolved_catalog_references, find_unused_catalog_entries,
52 gather_pnpm_catalog_state,
53};
54#[expect(
55 deprecated,
56 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
57)]
58use unused_deps::{
59 find_test_only_dependencies, find_type_only_dependencies, find_unlisted_dependencies,
60 find_unresolved_imports, find_unused_dependencies,
61};
62#[expect(
63 deprecated,
64 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
65)]
66use unused_exports::{
67 collect_export_usages, find_private_type_leaks, find_unused_exports,
68 suppress_signature_backing_types,
69};
70#[expect(
71 deprecated,
72 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
73)]
74use unused_files::find_unused_files;
75use unused_members::find_unused_members_with_public_api_entry_points;
76#[expect(
77 deprecated,
78 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
79)]
80use unused_overrides::{
81 find_misconfigured_dependency_overrides, find_unused_dependency_overrides,
82 gather_pnpm_override_state,
83};
84
85#[doc(hidden)]
88pub type LineOffsetsMap<'a> = FxHashMap<FileId, &'a [u32]>;
89
90#[doc(hidden)]
93pub fn byte_offset_to_line_col(
94 line_offsets_map: &LineOffsetsMap<'_>,
95 file_id: FileId,
96 byte_offset: u32,
97) -> (u32, u32) {
98 line_offsets_map
99 .get(&file_id)
100 .map_or((1, byte_offset), |offsets| {
101 fallow_types::extract::byte_offset_to_line_col(offsets, byte_offset)
102 })
103}
104
105fn cycle_edge_line_col(
106 graph: &ModuleGraph,
107 line_offsets_map: &LineOffsetsMap<'_>,
108 cycle: &[FileId],
109 edge_index: usize,
110) -> Option<(u32, u32)> {
111 if cycle.is_empty() {
112 return None;
113 }
114
115 let from = cycle[edge_index];
116 let to = cycle[(edge_index + 1) % cycle.len()];
117 graph
118 .find_import_span_start(from, to)
119 .map(|span_start| byte_offset_to_line_col(line_offsets_map, from, span_start))
120}
121
122fn is_circular_dependency_suppressed(
123 graph: &ModuleGraph,
124 line_offsets_map: &LineOffsetsMap<'_>,
125 suppressions: &crate::suppress::SuppressionContext<'_>,
126 cycle: &[FileId],
127) -> bool {
128 if cycle
129 .iter()
130 .any(|&id| suppressions.is_file_suppressed(id, IssueKind::CircularDependency))
131 {
132 return true;
133 }
134
135 let mut line_suppressed = false;
136 for edge_index in 0..cycle.len() {
137 let from = cycle[edge_index];
138 if let Some((line, _)) = cycle_edge_line_col(graph, line_offsets_map, cycle, edge_index)
139 && suppressions.is_suppressed(from, line, IssueKind::CircularDependency)
140 {
141 line_suppressed = true;
142 }
143 }
144 line_suppressed
145}
146
147fn read_source(path: &std::path::Path) -> String {
151 std::fs::read_to_string(path).unwrap_or_default()
152}
153
154fn is_cross_package_cycle(
159 files: &[std::path::PathBuf],
160 workspaces: &[fallow_config::WorkspaceInfo],
161) -> bool {
162 let find_workspace = |path: &std::path::Path| -> Option<&std::path::Path> {
163 workspaces
164 .iter()
165 .map(|w| w.root.as_path())
166 .filter(|root| path.starts_with(root))
167 .max_by_key(|root| root.components().count())
168 };
169
170 let mut seen_workspace: Option<&std::path::Path> = None;
171 for file in files {
172 if let Some(ws) = find_workspace(file) {
173 match &seen_workspace {
174 None => seen_workspace = Some(ws),
175 Some(prev) if *prev != ws => return true,
176 _ => {}
177 }
178 }
179 }
180 false
181}
182
183fn public_workspace_roots<'a>(
184 public_packages: &[String],
185 workspaces: &'a [fallow_config::WorkspaceInfo],
186) -> Vec<&'a std::path::Path> {
187 if public_packages.is_empty() || workspaces.is_empty() {
188 return Vec::new();
189 }
190
191 workspaces
192 .iter()
193 .filter(|ws| {
194 public_packages.iter().any(|pattern| {
195 ws.name == *pattern
196 || globset::Glob::new(pattern)
197 .ok()
198 .is_some_and(|g| g.compile_matcher().is_match(&ws.name))
199 })
200 })
201 .map(|ws| ws.root.as_path())
202 .collect()
203}
204
205fn graph_path_to_file_id(graph: &ModuleGraph) -> FxHashMap<std::path::PathBuf, FileId> {
206 let mut path_to_file_id = FxHashMap::default();
207 for module in &graph.modules {
208 path_to_file_id.insert(module.path.clone(), module.file_id);
209 if let Ok(canonical) = dunce::canonicalize(&module.path) {
210 path_to_file_id.insert(canonical, module.file_id);
211 }
212 }
213 path_to_file_id
214}
215
216fn add_package_public_api_entry_points(
217 public_api_entry_points: &mut FxHashSet<FileId>,
218 path_to_file_id: &FxHashMap<std::path::PathBuf, FileId>,
219 package_root: &std::path::Path,
220 package_json: &PackageJson,
221 canonical_project_root: &std::path::Path,
222) {
223 if package_json.private.unwrap_or(false) {
224 return;
225 }
226
227 for entry in package_json.entry_points() {
228 let Some(entry_point) = crate::discover::resolve_entry_path(
229 package_root,
230 &entry,
231 canonical_project_root,
232 crate::discover::EntryPointSource::PackageJsonExports,
233 ) else {
234 continue;
235 };
236
237 if let Some(file_id) = path_to_file_id.get(&entry_point.path).copied().or_else(|| {
238 dunce::canonicalize(&entry_point.path)
239 .ok()
240 .and_then(|canonical| path_to_file_id.get(&canonical).copied())
241 }) {
242 public_api_entry_points.insert(file_id);
243 }
244 }
245}
246
247fn is_source_index_under_package(path: &std::path::Path, package_root: &std::path::Path) -> bool {
248 let Ok(relative) = path.strip_prefix(package_root) else {
249 return false;
250 };
251
252 if !matches!(
253 relative.components().next(),
254 Some(std::path::Component::Normal(segment)) if segment == "src"
255 ) {
256 return false;
257 }
258
259 path.file_stem()
260 .and_then(|stem| stem.to_str())
261 .is_some_and(|stem| stem == "index")
262}
263
264fn add_exportless_package_source_indexes(
265 public_api_entry_points: &mut FxHashSet<FileId>,
266 graph: &ModuleGraph,
267 package_root: &std::path::Path,
268 package_json: &PackageJson,
269) {
270 if package_json.private.unwrap_or(false) || package_json.exports.is_some() {
271 return;
272 }
273
274 let mut roots = vec![package_root.to_path_buf()];
275 if let Ok(canonical) = dunce::canonicalize(package_root) {
276 roots.push(canonical);
277 }
278
279 for module in &graph.modules {
280 if roots
281 .iter()
282 .any(|root| is_source_index_under_package(&module.path, root))
283 {
284 public_api_entry_points.insert(module.file_id);
285 }
286 }
287}
288
289fn public_api_package_entry_points(
290 graph: &ModuleGraph,
291 config: &ResolvedConfig,
292 root_pkg: Option<&PackageJson>,
293 workspaces: &[fallow_config::WorkspaceInfo],
294) -> FxHashSet<FileId> {
295 let mut public_api_entry_points = FxHashSet::default();
296 let path_to_file_id = graph_path_to_file_id(graph);
297 let canonical_project_root =
298 dunce::canonicalize(&config.root).unwrap_or_else(|_| config.root.clone());
299
300 if let Some(pkg) = root_pkg {
301 add_package_public_api_entry_points(
302 &mut public_api_entry_points,
303 &path_to_file_id,
304 &config.root,
305 pkg,
306 &canonical_project_root,
307 );
308 add_exportless_package_source_indexes(
309 &mut public_api_entry_points,
310 graph,
311 &config.root,
312 pkg,
313 );
314 }
315
316 for workspace in workspaces {
317 let Ok(pkg) = PackageJson::load(&workspace.root.join("package.json")) else {
318 continue;
319 };
320 add_package_public_api_entry_points(
321 &mut public_api_entry_points,
322 &path_to_file_id,
323 &workspace.root,
324 &pkg,
325 &canonical_project_root,
326 );
327 add_exportless_package_source_indexes(
328 &mut public_api_entry_points,
329 graph,
330 &workspace.root,
331 &pkg,
332 );
333 }
334
335 public_api_entry_points
336}
337
338fn find_circular_dependencies(
339 graph: &ModuleGraph,
340 line_offsets_map: &LineOffsetsMap<'_>,
341 suppressions: &crate::suppress::SuppressionContext<'_>,
342 workspaces: &[fallow_config::WorkspaceInfo],
343) -> Vec<CircularDependency> {
344 let cycles = graph.find_cycles();
345 let mut dependencies: Vec<CircularDependency> = cycles
346 .into_iter()
347 .filter_map(|cycle| {
348 if is_circular_dependency_suppressed(graph, line_offsets_map, suppressions, &cycle) {
349 return None;
350 }
351
352 let edges: Vec<CircularDependencyEdge> = (0..cycle.len())
358 .map(|edge_index| {
359 let from = cycle[edge_index];
360 let (line, col) =
361 cycle_edge_line_col(graph, line_offsets_map, &cycle, edge_index)
362 .unwrap_or((1, 0));
363 CircularDependencyEdge {
364 path: graph.modules[from.0 as usize].path.clone(),
365 line,
366 col,
367 }
368 })
369 .collect();
370
371 let files: Vec<std::path::PathBuf> =
372 edges.iter().map(|edge| edge.path.clone()).collect();
373 let length = files.len();
374 let (line, col) = edges.first().map_or((1, 0), |edge| (edge.line, edge.col));
377 Some(CircularDependency {
378 files,
379 length,
380 line,
381 col,
382 edges,
383 is_cross_package: false,
384 })
385 })
386 .collect();
387
388 if !workspaces.is_empty() {
389 for dep in &mut dependencies {
390 dep.is_cross_package = is_cross_package_cycle(&dep.files, workspaces);
391 }
392 }
393
394 dependencies
395}
396
397fn run_circular_dep_detector(
402 graph: &ModuleGraph,
403 config: &ResolvedConfig,
404 line_offsets_by_file: &LineOffsetsMap<'_>,
405 suppressions: &crate::suppress::SuppressionContext<'_>,
406 workspaces: &[fallow_config::WorkspaceInfo],
407) -> Vec<CircularDependencyFinding> {
408 if config.rules.circular_dependencies == Severity::Off {
409 return Vec::new();
410 }
411 find_circular_dependencies(graph, line_offsets_by_file, suppressions, workspaces)
412 .into_iter()
413 .map(CircularDependencyFinding::with_actions)
414 .collect()
415}
416
417fn run_re_export_cycle_detector(
420 graph: &ModuleGraph,
421 config: &ResolvedConfig,
422 suppressions: &crate::suppress::SuppressionContext<'_>,
423) -> Vec<ReExportCycleFinding> {
424 if config.rules.re_export_cycle == Severity::Off {
425 return Vec::new();
426 }
427 find_re_export_cycles(graph, suppressions)
428}
429
430fn run_export_usages_collector(
433 graph: &ModuleGraph,
434 line_offsets_by_file: &LineOffsetsMap<'_>,
435 collect_usages: bool,
436) -> Vec<crate::results::ExportUsage> {
437 if collect_usages {
438 collect_export_usages(graph, line_offsets_by_file)
439 } else {
440 Vec::new()
441 }
442}
443
444fn collect_declared_dependency_names(
451 config: &ResolvedConfig,
452 root_pkg: Option<&PackageJson>,
453 workspaces: &[fallow_config::WorkspaceInfo],
454) -> FxHashSet<String> {
455 let mut deps: FxHashSet<String> = FxHashSet::default();
456 if let Some(pkg) = root_pkg {
457 deps.extend(pkg.all_dependency_names());
458 }
459 for ws in workspaces {
460 if ws.root == config.root {
461 continue; }
463 if let Ok(pkg) = PackageJson::load(&ws.root.join("package.json")) {
464 deps.extend(pkg.all_dependency_names());
465 }
466 }
467 deps
468}
469
470#[expect(
472 deprecated,
473 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
474)]
475#[deprecated(
476 since = "2.76.0",
477 note = "fallow_core is internal; use fallow_cli::programmatic::detect_dead_code instead. NOTE: replacement returns serde_json::Value, not typed AnalysisResults. See docs/fallow-core-migration.md and ADR-008."
478)]
479#[expect(
480 clippy::too_many_lines,
481 reason = "orchestration function calling all detectors; each call is one-line and the sequence is easier to follow in one place"
482)]
483pub fn find_dead_code_full(
484 graph: &ModuleGraph,
485 config: &ResolvedConfig,
486 resolved_modules: &[ResolvedModule],
487 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
488 workspaces: &[fallow_config::WorkspaceInfo],
489 modules: &[ModuleInfo],
490 collect_usages: bool,
491) -> AnalysisResults {
492 let _span = tracing::info_span!("find_dead_code").entered();
493
494 let suppressions = crate::suppress::SuppressionContext::new(modules);
495
496 let line_offsets_by_file: LineOffsetsMap<'_> = modules
497 .iter()
498 .filter(|m| !m.line_offsets.is_empty())
499 .map(|m| (m.file_id, m.line_offsets.as_slice()))
500 .collect();
501
502 let pkg_path = config.root.join("package.json");
503 let pkg = PackageJson::load(&pkg_path).ok();
504 let public_api_entry_points =
505 public_api_package_entry_points(graph, config, pkg.as_ref(), workspaces);
506
507 let iconify_referenced =
508 iconify::collect_iconify_referenced_deps(modules, pkg.as_ref(), workspaces);
509 let augmented_plugin_result;
510 let plugin_result = if iconify_referenced.is_empty() {
511 plugin_result
512 } else {
513 let mut owned = plugin_result.cloned().unwrap_or_default();
514 owned.referenced_dependencies.extend(iconify_referenced);
515 augmented_plugin_result = owned;
516 Some(&augmented_plugin_result)
517 };
518
519 let mut user_class_members = config.used_class_members.clone();
520 if let Some(plugin_result) = plugin_result {
521 user_class_members.extend(plugin_result.used_class_members.iter().cloned());
522 }
523
524 let virtual_prefixes: Vec<&str> = plugin_result
525 .map(|pr| {
526 pr.virtual_module_prefixes
527 .iter()
528 .map(String::as_str)
529 .collect()
530 })
531 .unwrap_or_default();
532 let generated_patterns: Vec<&str> = plugin_result
533 .map(|pr| {
534 pr.generated_import_patterns
535 .iter()
536 .map(String::as_str)
537 .collect()
538 })
539 .unwrap_or_default();
540 let generated_type_prefixes: Vec<&str> = plugin_result
541 .map(|pr| {
542 pr.generated_type_import_prefixes
543 .iter()
544 .map(String::as_str)
545 .collect()
546 })
547 .unwrap_or_default();
548
549 let (
550 (unused_files, export_results),
551 (
552 (member_results, dependency_results),
553 (
554 (unresolved_imports, duplicate_exports),
555 (boundary_violations, (circular_dependencies, (re_export_cycles, export_usages))),
556 ),
557 ),
558 ) = rayon::join(
559 || {
560 rayon::join(
561 || run_unused_file_detector(graph, config, &suppressions),
562 || {
563 run_export_detectors(
564 graph,
565 modules,
566 config,
567 plugin_result,
568 &suppressions,
569 &line_offsets_by_file,
570 )
571 },
572 )
573 },
574 || {
575 rayon::join(
576 || {
577 rayon::join(
578 || {
579 run_member_detectors(
580 graph,
581 resolved_modules,
582 modules,
583 config,
584 &suppressions,
585 &line_offsets_by_file,
586 &user_class_members,
587 &public_api_entry_points,
588 )
589 },
590 || {
591 run_dependency_detectors(
592 graph,
593 pkg.as_ref(),
594 config,
595 plugin_result,
596 workspaces,
597 resolved_modules,
598 &line_offsets_by_file,
599 )
600 },
601 )
602 },
603 || {
604 rayon::join(
605 || {
606 rayon::join(
607 || {
608 run_unresolved_import_detector(
609 resolved_modules,
610 config,
611 &suppressions,
612 &virtual_prefixes,
613 &generated_patterns,
614 &generated_type_prefixes,
615 &line_offsets_by_file,
616 )
617 },
618 || {
619 if config.rules.duplicate_exports != Severity::Off {
620 let duplicate_exports =
621 if let Some(plugin_result) = plugin_result {
622 unused_exports::find_duplicate_exports_with_plugins(
623 graph,
624 config,
625 &suppressions,
626 &line_offsets_by_file,
627 Some(plugin_result),
628 resolved_modules,
629 )
630 } else {
631 unused_exports::find_duplicate_exports(
632 graph,
633 config,
634 &suppressions,
635 &line_offsets_by_file,
636 resolved_modules,
637 )
638 };
639 duplicate_exports
640 .into_iter()
641 .map(DuplicateExportFinding::with_actions)
642 .collect::<Vec<_>>()
643 } else {
644 Vec::new()
645 }
646 },
647 )
648 },
649 || {
650 rayon::join(
651 || {
652 if config.rules.boundary_violation != Severity::Off
653 && !config.boundaries.is_empty()
654 {
655 boundary::find_boundary_violations(
656 graph,
657 config,
658 &suppressions,
659 &line_offsets_by_file,
660 )
661 .into_iter()
662 .map(BoundaryViolationFinding::with_actions)
663 .collect::<Vec<_>>()
664 } else {
665 Vec::new()
666 }
667 },
668 || {
669 rayon::join(
670 || {
671 run_circular_dep_detector(
672 graph,
673 config,
674 &line_offsets_by_file,
675 &suppressions,
676 workspaces,
677 )
678 },
679 || {
680 rayon::join(
681 || {
682 run_re_export_cycle_detector(
683 graph,
684 config,
685 &suppressions,
686 )
687 },
688 || {
689 run_export_usages_collector(
690 graph,
691 &line_offsets_by_file,
692 collect_usages,
693 )
694 },
695 )
696 },
697 )
698 },
699 )
700 },
701 )
702 },
703 )
704 },
705 );
706
707 let mut results = AnalysisResults {
708 unused_files,
709 unused_exports: export_results.unused_exports,
710 unused_types: export_results.unused_types,
711 private_type_leaks: export_results.private_type_leaks,
712 stale_suppressions: export_results.stale_suppressions,
713 unused_enum_members: member_results.unused_enum_members,
714 unused_class_members: member_results.unused_class_members,
715 unused_dependencies: dependency_results.unused_dependencies,
716 unused_dev_dependencies: dependency_results.unused_dev_dependencies,
717 unused_optional_dependencies: dependency_results.unused_optional_dependencies,
718 unlisted_dependencies: dependency_results.unlisted_dependencies,
719 type_only_dependencies: dependency_results.type_only_dependencies,
720 test_only_dependencies: dependency_results.test_only_dependencies,
721 unresolved_imports,
722 duplicate_exports,
723 boundary_violations,
724 circular_dependencies,
725 re_export_cycles,
726 export_usages,
727 ..AnalysisResults::default()
728 };
729
730 let public_roots = public_workspace_roots(&config.public_packages, workspaces);
731 if !public_roots.is_empty() {
732 results.unused_exports.retain(|e| {
733 !public_roots
734 .iter()
735 .any(|root| e.export.path.starts_with(root))
736 });
737 results.unused_types.retain(|e| {
738 !public_roots
739 .iter()
740 .any(|root| e.export.path.starts_with(root))
741 });
742 results.unused_enum_members.retain(|e| {
743 !public_roots
744 .iter()
745 .any(|root| e.member.path.starts_with(root))
746 });
747 results.unused_class_members.retain(|e| {
748 !public_roots
749 .iter()
750 .any(|root| e.member.path.starts_with(root))
751 });
752 }
753
754 let declared_deps = collect_declared_dependency_names(config, pkg.as_ref(), workspaces);
755
756 if config.rules.security_client_server_leak != Severity::Off {
757 let (security_findings, stats) =
758 security::find_security_findings(graph, modules, &suppressions, &line_offsets_by_file);
759 results.security_findings = security_findings;
760 results.security_unresolved_edge_files = stats.client_files_with_unresolved_edges;
761 }
762
763 if config.rules.security_sink != Severity::Off {
764 let categories = config.security.categories.as_ref();
765 let filter = security::CategoryFilter::new(
766 categories.and_then(|c| c.include.clone()),
767 categories.and_then(|c| c.exclude.clone()),
768 );
769 let (sink_findings, sink_stats) = security::find_tainted_sinks(
774 graph,
775 modules,
776 &suppressions,
777 &line_offsets_by_file,
778 &filter,
779 &declared_deps,
780 &config.root,
781 );
782 results.security_findings.extend(sink_findings);
783 results.security_unresolved_callee_sites = sink_stats.sinks_skipped_dynamic_callee;
784 results
785 .security_findings
786 .extend(security::find_hardcoded_secret_candidates(
787 graph,
788 modules,
789 &suppressions,
790 &line_offsets_by_file,
791 &filter,
792 &config.root,
793 ));
794 }
795
796 if !results.security_findings.is_empty() {
803 security::annotate_dead_code_cross_links(
804 graph,
805 modules,
806 &line_offsets_by_file,
807 &results.unused_files,
808 &results.unused_exports,
809 &mut results.security_findings,
810 );
811 let mut boundary_crossings: rustc_hash::FxHashMap<std::path::PathBuf, (String, String)> =
819 rustc_hash::FxHashMap::default();
820 for violation in &results.boundary_violations {
821 let zones = (
822 violation.violation.from_zone.clone(),
823 violation.violation.to_zone.clone(),
824 );
825 for path in [
826 violation.violation.from_path.clone(),
827 violation.violation.to_path.clone(),
828 ] {
829 boundary_crossings
830 .entry(path)
831 .and_modify(|existing| {
832 if zones < *existing {
833 *existing = zones.clone();
834 }
835 })
836 .or_insert_with(|| zones.clone());
837 }
838 }
839 security::rank_security_findings(
840 graph,
841 modules,
842 &line_offsets_by_file,
843 &declared_deps,
844 &boundary_crossings,
845 &mut results.security_findings,
846 );
847 }
848
849 if config.rules.stale_suppressions != Severity::Off {
850 results
851 .stale_suppressions
852 .extend(suppressions.find_stale(graph, config));
853 }
854 results.suppression_count = suppressions.used_count();
855 results.active_suppressions = suppressions.all_suppressions(graph);
856
857 let need_unused_catalogs = config.rules.unused_catalog_entries != Severity::Off;
858 let need_empty_catalog_groups = config.rules.empty_catalog_groups != Severity::Off;
859 let need_unresolved_refs = config.rules.unresolved_catalog_references != Severity::Off;
860 if (need_unused_catalogs || need_empty_catalog_groups || need_unresolved_refs)
861 && let Some(state) = gather_pnpm_catalog_state(config, workspaces)
862 {
863 if need_unused_catalogs {
864 results.unused_catalog_entries = find_unused_catalog_entries(&state)
865 .into_iter()
866 .map(UnusedCatalogEntryFinding::with_actions)
867 .collect();
868 }
869 if need_empty_catalog_groups {
870 results.empty_catalog_groups = find_empty_catalog_groups(&state)
871 .into_iter()
872 .map(EmptyCatalogGroupFinding::with_actions)
873 .collect();
874 }
875 if need_unresolved_refs {
876 results.unresolved_catalog_references = find_unresolved_catalog_references(
877 &state,
878 &config.compiled_ignore_catalog_references,
879 &config.root,
880 )
881 .into_iter()
882 .map(UnresolvedCatalogReferenceFinding::with_actions)
883 .collect();
884 }
885 }
886
887 let need_unused_overrides = config.rules.unused_dependency_overrides != Severity::Off;
888 let need_misconfigured_overrides =
889 config.rules.misconfigured_dependency_overrides != Severity::Off;
890 if (need_unused_overrides || need_misconfigured_overrides)
891 && let Some(state) = gather_pnpm_override_state(config, workspaces)
892 {
893 if need_unused_overrides {
894 results.unused_dependency_overrides = find_unused_dependency_overrides(&state, config)
895 .into_iter()
896 .map(UnusedDependencyOverrideFinding::with_actions)
897 .collect();
898 }
899 if need_misconfigured_overrides {
900 results.misconfigured_dependency_overrides =
901 find_misconfigured_dependency_overrides(&state, config)
902 .into_iter()
903 .map(MisconfiguredDependencyOverrideFinding::with_actions)
904 .collect();
905 }
906 }
907
908 results.sort();
909
910 results
911}
912
913#[expect(
914 deprecated,
915 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
916)]
917fn run_unused_file_detector(
918 graph: &ModuleGraph,
919 config: &ResolvedConfig,
920 suppressions: &crate::suppress::SuppressionContext<'_>,
921) -> Vec<UnusedFileFinding> {
922 if config.rules.unused_files == Severity::Off {
923 return Vec::new();
924 }
925 find_unused_files(graph, suppressions)
926 .into_iter()
927 .map(UnusedFileFinding::with_actions)
928 .collect()
929}
930
931#[expect(
932 deprecated,
933 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
934)]
935fn run_export_detectors(
936 graph: &ModuleGraph,
937 modules: &[ModuleInfo],
938 config: &ResolvedConfig,
939 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
940 suppressions: &crate::suppress::SuppressionContext<'_>,
941 line_offsets_by_file: &LineOffsetsMap<'_>,
942) -> AnalysisResults {
943 let mut results = AnalysisResults::default();
944 if config.rules.unused_exports == Severity::Off
945 && config.rules.unused_types == Severity::Off
946 && config.rules.private_type_leaks == Severity::Off
947 {
948 return results;
949 }
950
951 let (exports, types, stale_expected) = find_unused_exports(
952 graph,
953 modules,
954 config,
955 plugin_result,
956 suppressions,
957 line_offsets_by_file,
958 );
959 if config.rules.unused_exports != Severity::Off {
960 results.unused_exports = exports
961 .into_iter()
962 .map(UnusedExportFinding::with_actions)
963 .collect();
964 }
965 if config.rules.unused_types != Severity::Off {
966 let mut typed = types;
967 suppress_signature_backing_types(&mut typed, graph, modules);
968 results.unused_types = typed
969 .into_iter()
970 .map(UnusedTypeFinding::with_actions)
971 .collect();
972 }
973 if config.rules.private_type_leaks != Severity::Off {
974 results.private_type_leaks =
975 find_private_type_leaks(graph, modules, config, suppressions, line_offsets_by_file)
976 .into_iter()
977 .map(PrivateTypeLeakFinding::with_actions)
978 .collect();
979 }
980 if config.rules.stale_suppressions != Severity::Off {
981 results.stale_suppressions.extend(stale_expected);
982 }
983 results
984}
985
986#[expect(
987 clippy::too_many_arguments,
988 reason = "member detection needs graph context plus public API and allowlist filters"
989)]
990fn run_member_detectors(
991 graph: &ModuleGraph,
992 resolved_modules: &[ResolvedModule],
993 modules: &[ModuleInfo],
994 config: &ResolvedConfig,
995 suppressions: &crate::suppress::SuppressionContext<'_>,
996 line_offsets_by_file: &LineOffsetsMap<'_>,
997 user_class_members: &[fallow_config::UsedClassMemberRule],
998 public_api_entry_points: &FxHashSet<FileId>,
999) -> AnalysisResults {
1000 let mut results = AnalysisResults::default();
1001 if config.rules.unused_enum_members == Severity::Off
1002 && config.rules.unused_class_members == Severity::Off
1003 {
1004 return results;
1005 }
1006
1007 let (enum_members, class_members) = find_unused_members_with_public_api_entry_points(
1008 graph,
1009 resolved_modules,
1010 modules,
1011 suppressions,
1012 line_offsets_by_file,
1013 user_class_members,
1014 &config.ignore_decorators,
1015 public_api_entry_points,
1016 );
1017 if config.rules.unused_enum_members != Severity::Off {
1018 results.unused_enum_members = enum_members
1019 .into_iter()
1020 .map(UnusedEnumMemberFinding::with_actions)
1021 .collect();
1022 }
1023 if config.rules.unused_class_members != Severity::Off {
1024 results.unused_class_members = class_members
1025 .into_iter()
1026 .map(UnusedClassMemberFinding::with_actions)
1027 .collect();
1028 }
1029 results
1030}
1031
1032#[expect(
1033 deprecated,
1034 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
1035)]
1036fn run_dependency_detectors(
1037 graph: &ModuleGraph,
1038 pkg: Option<&PackageJson>,
1039 config: &ResolvedConfig,
1040 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
1041 workspaces: &[fallow_config::WorkspaceInfo],
1042 resolved_modules: &[ResolvedModule],
1043 line_offsets_by_file: &LineOffsetsMap<'_>,
1044) -> AnalysisResults {
1045 let mut results = AnalysisResults::default();
1046 let Some(pkg) = pkg else {
1047 return results;
1048 };
1049
1050 if config.rules.unused_dependencies != Severity::Off
1051 || config.rules.unused_dev_dependencies != Severity::Off
1052 || config.rules.unused_optional_dependencies != Severity::Off
1053 {
1054 let (deps, dev_deps, optional_deps) =
1055 find_unused_dependencies(graph, pkg, config, plugin_result, workspaces);
1056 if config.rules.unused_dependencies != Severity::Off {
1057 results.unused_dependencies = deps
1058 .into_iter()
1059 .map(UnusedDependencyFinding::with_actions)
1060 .collect();
1061 }
1062 if config.rules.unused_dev_dependencies != Severity::Off {
1063 results.unused_dev_dependencies = dev_deps
1064 .into_iter()
1065 .map(UnusedDevDependencyFinding::with_actions)
1066 .collect();
1067 }
1068 if config.rules.unused_optional_dependencies != Severity::Off {
1069 results.unused_optional_dependencies = optional_deps
1070 .into_iter()
1071 .map(UnusedOptionalDependencyFinding::with_actions)
1072 .collect();
1073 }
1074 }
1075
1076 if config.rules.unlisted_dependencies != Severity::Off {
1077 results.unlisted_dependencies = find_unlisted_dependencies(
1078 graph,
1079 pkg,
1080 config,
1081 workspaces,
1082 plugin_result,
1083 resolved_modules,
1084 line_offsets_by_file,
1085 )
1086 .into_iter()
1087 .map(UnlistedDependencyFinding::with_actions)
1088 .collect();
1089 }
1090
1091 if config.production {
1092 results.type_only_dependencies =
1093 find_type_only_dependencies(graph, pkg, config, workspaces)
1094 .into_iter()
1095 .map(TypeOnlyDependencyFinding::with_actions)
1096 .collect();
1097 }
1098
1099 if !config.production && config.rules.test_only_dependencies != Severity::Off {
1100 results.test_only_dependencies =
1101 find_test_only_dependencies(graph, pkg, config, workspaces)
1102 .into_iter()
1103 .map(TestOnlyDependencyFinding::with_actions)
1104 .collect();
1105 }
1106 results
1107}
1108
1109fn run_unresolved_import_detector(
1110 resolved_modules: &[ResolvedModule],
1111 config: &ResolvedConfig,
1112 suppressions: &crate::suppress::SuppressionContext<'_>,
1113 virtual_prefixes: &[&str],
1114 generated_patterns: &[&str],
1115 generated_type_prefixes: &[&str],
1116 line_offsets_by_file: &LineOffsetsMap<'_>,
1117) -> Vec<UnresolvedImportFinding> {
1118 if config.rules.unresolved_imports == Severity::Off || resolved_modules.is_empty() {
1119 return Vec::new();
1120 }
1121 find_unresolved_imports(
1122 resolved_modules,
1123 config,
1124 suppressions,
1125 virtual_prefixes,
1126 generated_patterns,
1127 generated_type_prefixes,
1128 line_offsets_by_file,
1129 )
1130 .into_iter()
1131 .map(UnresolvedImportFinding::with_actions)
1132 .collect()
1133}
1134
1135#[cfg(test)]
1136#[expect(
1137 deprecated,
1138 reason = "ADR-008 keeps direct analyzer unit tests while the public warning targets external callers"
1139)]
1140mod tests {
1141 use fallow_types::extract::{byte_offset_to_line_col, compute_line_offsets};
1142
1143 fn line_col(source: &str, byte_offset: u32) -> (u32, u32) {
1144 let offsets = compute_line_offsets(source);
1145 byte_offset_to_line_col(&offsets, byte_offset)
1146 }
1147
1148 #[test]
1149 fn compute_offsets_empty() {
1150 assert_eq!(compute_line_offsets(""), vec![0]);
1151 }
1152
1153 #[test]
1154 fn compute_offsets_single_line() {
1155 assert_eq!(compute_line_offsets("hello"), vec![0]);
1156 }
1157
1158 #[test]
1159 fn compute_offsets_multiline() {
1160 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
1161 }
1162
1163 #[test]
1164 fn compute_offsets_trailing_newline() {
1165 assert_eq!(compute_line_offsets("abc\n"), vec![0, 4]);
1166 }
1167
1168 #[test]
1169 fn compute_offsets_crlf() {
1170 assert_eq!(compute_line_offsets("ab\r\ncd"), vec![0, 4]);
1171 }
1172
1173 #[test]
1174 fn compute_offsets_consecutive_newlines() {
1175 assert_eq!(compute_line_offsets("\n\n"), vec![0, 1, 2]);
1176 }
1177
1178 #[test]
1179 fn byte_offset_empty_source() {
1180 assert_eq!(line_col("", 0), (1, 0));
1181 }
1182
1183 #[test]
1184 fn byte_offset_single_line_start() {
1185 assert_eq!(line_col("hello", 0), (1, 0));
1186 }
1187
1188 #[test]
1189 fn byte_offset_single_line_middle() {
1190 assert_eq!(line_col("hello", 4), (1, 4));
1191 }
1192
1193 #[test]
1194 fn byte_offset_multiline_start_of_line2() {
1195 assert_eq!(line_col("line1\nline2\nline3", 6), (2, 0));
1196 }
1197
1198 #[test]
1199 fn byte_offset_multiline_middle_of_line3() {
1200 assert_eq!(line_col("line1\nline2\nline3", 14), (3, 2));
1201 }
1202
1203 #[test]
1204 fn byte_offset_at_newline_boundary() {
1205 assert_eq!(line_col("line1\nline2", 5), (1, 5));
1206 }
1207
1208 #[test]
1209 fn byte_offset_multibyte_utf8() {
1210 let source = "hi\n\u{1F600}x";
1211 assert_eq!(line_col(source, 3), (2, 0));
1212 assert_eq!(line_col(source, 7), (2, 4));
1213 }
1214
1215 #[test]
1216 fn byte_offset_multibyte_accented_chars() {
1217 let source = "caf\u{00E9}\nbar";
1218 assert_eq!(line_col(source, 6), (2, 0));
1219 assert_eq!(line_col(source, 3), (1, 3));
1220 }
1221
1222 #[test]
1223 fn byte_offset_via_map_fallback() {
1224 use super::*;
1225 let map: LineOffsetsMap<'_> = FxHashMap::default();
1226 assert_eq!(
1227 super::byte_offset_to_line_col(&map, FileId(99), 42),
1228 (1, 42)
1229 );
1230 }
1231
1232 #[test]
1233 fn byte_offset_via_map_lookup() {
1234 use super::*;
1235 let offsets = compute_line_offsets("abc\ndef\nghi");
1236 let mut map: LineOffsetsMap<'_> = FxHashMap::default();
1237 map.insert(FileId(0), &offsets);
1238 assert_eq!(super::byte_offset_to_line_col(&map, FileId(0), 5), (2, 1));
1239 }
1240
1241 mod orchestration {
1242 use super::super::*;
1243 use fallow_config::{FallowConfig, OutputFormat, RulesConfig, Severity};
1244 use std::path::PathBuf;
1245
1246 fn find_dead_code(graph: &ModuleGraph, config: &ResolvedConfig) -> AnalysisResults {
1247 find_dead_code_full(graph, config, &[], None, &[], &[], false)
1248 }
1249
1250 fn make_config_with_rules(rules: RulesConfig) -> ResolvedConfig {
1251 FallowConfig {
1252 rules,
1253 ..Default::default()
1254 }
1255 .resolve(
1256 PathBuf::from("/tmp/orchestration-test"),
1257 OutputFormat::Human,
1258 1,
1259 true,
1260 true,
1261 None,
1262 )
1263 }
1264
1265 #[test]
1266 fn find_dead_code_all_rules_off_returns_empty() {
1267 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
1268 use crate::graph::ModuleGraph;
1269 use crate::resolve::ResolvedModule;
1270 use rustc_hash::FxHashSet;
1271
1272 let files = vec![DiscoveredFile {
1273 id: FileId(0),
1274 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1275 size_bytes: 100,
1276 }];
1277 let entry_points = vec![EntryPoint {
1278 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1279 source: EntryPointSource::ManualEntry,
1280 }];
1281 let resolved = vec![ResolvedModule {
1282 file_id: FileId(0),
1283 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1284 exports: vec![],
1285 re_exports: vec![],
1286 resolved_imports: vec![],
1287 resolved_dynamic_imports: vec![],
1288 resolved_dynamic_patterns: vec![],
1289 member_accesses: vec![],
1290 whole_object_uses: vec![],
1291 has_cjs_exports: false,
1292 has_angular_component_template_url: false,
1293 unused_import_bindings: FxHashSet::default(),
1294 type_referenced_import_bindings: vec![],
1295 value_referenced_import_bindings: vec![],
1296 namespace_object_aliases: vec![],
1297 }];
1298 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
1299
1300 let rules = RulesConfig {
1301 unused_files: Severity::Off,
1302 unused_exports: Severity::Off,
1303 unused_types: Severity::Off,
1304 private_type_leaks: Severity::Off,
1305 unused_dependencies: Severity::Off,
1306 unused_dev_dependencies: Severity::Off,
1307 unused_optional_dependencies: Severity::Off,
1308 unused_enum_members: Severity::Off,
1309 unused_class_members: Severity::Off,
1310 unresolved_imports: Severity::Off,
1311 unlisted_dependencies: Severity::Off,
1312 duplicate_exports: Severity::Off,
1313 type_only_dependencies: Severity::Off,
1314 circular_dependencies: Severity::Off,
1315 re_export_cycle: Severity::Off,
1316 test_only_dependencies: Severity::Off,
1317 boundary_violation: Severity::Off,
1318 coverage_gaps: Severity::Off,
1319 feature_flags: Severity::Off,
1320 stale_suppressions: Severity::Off,
1321 unused_catalog_entries: Severity::Off,
1322 empty_catalog_groups: Severity::Off,
1323 unresolved_catalog_references: Severity::Off,
1324 unused_dependency_overrides: Severity::Off,
1325 misconfigured_dependency_overrides: Severity::Off,
1326 security_client_server_leak: Severity::Off,
1327 security_sink: Severity::Off,
1328 };
1329 let config = make_config_with_rules(rules);
1330 let results = find_dead_code(&graph, &config);
1331
1332 assert!(results.unused_files.is_empty());
1333 assert!(results.unused_exports.is_empty());
1334 assert!(results.unused_types.is_empty());
1335 assert!(results.unused_dependencies.is_empty());
1336 assert!(results.unused_dev_dependencies.is_empty());
1337 assert!(results.unused_optional_dependencies.is_empty());
1338 assert!(results.unused_enum_members.is_empty());
1339 assert!(results.unused_class_members.is_empty());
1340 assert!(results.unresolved_imports.is_empty());
1341 assert!(results.unlisted_dependencies.is_empty());
1342 assert!(results.duplicate_exports.is_empty());
1343 assert!(results.circular_dependencies.is_empty());
1344 assert!(results.export_usages.is_empty());
1345 }
1346
1347 #[test]
1348 fn find_dead_code_full_collect_usages_flag() {
1349 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
1350 use crate::extract::{ExportName, VisibilityTag};
1351 use crate::graph::{ExportSymbol, ModuleGraph};
1352 use crate::resolve::ResolvedModule;
1353 use oxc_span::Span;
1354 use rustc_hash::FxHashSet;
1355
1356 let files = vec![DiscoveredFile {
1357 id: FileId(0),
1358 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1359 size_bytes: 100,
1360 }];
1361 let entry_points = vec![EntryPoint {
1362 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1363 source: EntryPointSource::ManualEntry,
1364 }];
1365 let resolved = vec![ResolvedModule {
1366 file_id: FileId(0),
1367 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1368 exports: vec![],
1369 re_exports: vec![],
1370 resolved_imports: vec![],
1371 resolved_dynamic_imports: vec![],
1372 resolved_dynamic_patterns: vec![],
1373 member_accesses: vec![],
1374 whole_object_uses: vec![],
1375 has_cjs_exports: false,
1376 has_angular_component_template_url: false,
1377 unused_import_bindings: FxHashSet::default(),
1378 type_referenced_import_bindings: vec![],
1379 value_referenced_import_bindings: vec![],
1380 namespace_object_aliases: vec![],
1381 }];
1382 let mut graph = ModuleGraph::build(&resolved, &entry_points, &files);
1383 graph.modules[0].exports = vec![ExportSymbol {
1384 name: ExportName::Named("myExport".to_string()),
1385 is_type_only: false,
1386 is_side_effect_used: false,
1387 visibility: VisibilityTag::None,
1388 span: Span::new(10, 30),
1389 references: vec![],
1390 members: vec![],
1391 }];
1392
1393 let rules = RulesConfig::default();
1394 let config = make_config_with_rules(rules);
1395
1396 let results_no_collect = find_dead_code_full(
1397 &graph,
1398 &config,
1399 &[],
1400 None,
1401 &[],
1402 &[],
1403 false, );
1405 assert!(
1406 results_no_collect.export_usages.is_empty(),
1407 "export_usages should be empty when collect_usages is false"
1408 );
1409
1410 let results_with_collect = find_dead_code_full(
1411 &graph,
1412 &config,
1413 &[],
1414 None,
1415 &[],
1416 &[],
1417 true, );
1419 assert!(
1420 !results_with_collect.export_usages.is_empty(),
1421 "export_usages should be populated when collect_usages is true"
1422 );
1423 assert_eq!(
1424 results_with_collect.export_usages[0].export_name,
1425 "myExport"
1426 );
1427 }
1428
1429 #[test]
1430 fn find_dead_code_delegates_to_find_dead_code_with_resolved() {
1431 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
1432 use crate::graph::ModuleGraph;
1433 use crate::resolve::ResolvedModule;
1434 use rustc_hash::FxHashSet;
1435
1436 let files = vec![DiscoveredFile {
1437 id: FileId(0),
1438 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1439 size_bytes: 100,
1440 }];
1441 let entry_points = vec![EntryPoint {
1442 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1443 source: EntryPointSource::ManualEntry,
1444 }];
1445 let resolved = vec![ResolvedModule {
1446 file_id: FileId(0),
1447 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
1448 exports: vec![],
1449 re_exports: vec![],
1450 resolved_imports: vec![],
1451 resolved_dynamic_imports: vec![],
1452 resolved_dynamic_patterns: vec![],
1453 member_accesses: vec![],
1454 whole_object_uses: vec![],
1455 has_cjs_exports: false,
1456 has_angular_component_template_url: false,
1457 unused_import_bindings: FxHashSet::default(),
1458 type_referenced_import_bindings: vec![],
1459 value_referenced_import_bindings: vec![],
1460 namespace_object_aliases: vec![],
1461 }];
1462 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
1463 let config = make_config_with_rules(RulesConfig::default());
1464
1465 let results = find_dead_code(&graph, &config);
1466 assert!(results.unused_exports.is_empty());
1467 }
1468
1469 #[test]
1470 fn suppressions_built_from_modules() {
1471 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
1472 use crate::extract::ModuleInfo;
1473 use crate::graph::ModuleGraph;
1474 use crate::resolve::ResolvedModule;
1475 use crate::suppress::{IssueKind, Suppression};
1476 use rustc_hash::FxHashSet;
1477
1478 let files = vec![
1479 DiscoveredFile {
1480 id: FileId(0),
1481 path: PathBuf::from("/tmp/orchestration-test/src/entry.ts"),
1482 size_bytes: 100,
1483 },
1484 DiscoveredFile {
1485 id: FileId(1),
1486 path: PathBuf::from("/tmp/orchestration-test/src/utils.ts"),
1487 size_bytes: 100,
1488 },
1489 ];
1490 let entry_points = vec![EntryPoint {
1491 path: PathBuf::from("/tmp/orchestration-test/src/entry.ts"),
1492 source: EntryPointSource::ManualEntry,
1493 }];
1494 let resolved = files
1495 .iter()
1496 .map(|f| ResolvedModule {
1497 file_id: f.id,
1498 path: f.path.clone(),
1499 exports: vec![],
1500 re_exports: vec![],
1501 resolved_imports: vec![],
1502 resolved_dynamic_imports: vec![],
1503 resolved_dynamic_patterns: vec![],
1504 member_accesses: vec![],
1505 whole_object_uses: vec![],
1506 has_cjs_exports: false,
1507 has_angular_component_template_url: false,
1508 unused_import_bindings: FxHashSet::default(),
1509 type_referenced_import_bindings: vec![],
1510 value_referenced_import_bindings: vec![],
1511 namespace_object_aliases: vec![],
1512 })
1513 .collect::<Vec<_>>();
1514 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
1515
1516 let modules = vec![ModuleInfo {
1517 file_id: FileId(1),
1518 exports: vec![],
1519 imports: vec![],
1520 re_exports: vec![],
1521 dynamic_imports: vec![],
1522 dynamic_import_patterns: vec![],
1523 require_calls: vec![],
1524 package_path_references: vec![],
1525 member_accesses: vec![],
1526 whole_object_uses: vec![],
1527 has_cjs_exports: false,
1528 has_angular_component_template_url: false,
1529 content_hash: 0,
1530 suppressions: vec![Suppression {
1531 line: 0,
1532 comment_line: 1,
1533 kind: Some(IssueKind::UnusedFile),
1534 }],
1535 unknown_suppression_kinds: vec![],
1536 unused_import_bindings: vec![],
1537 type_referenced_import_bindings: vec![],
1538 value_referenced_import_bindings: vec![],
1539 line_offsets: vec![],
1540 complexity: vec![],
1541 flag_uses: vec![],
1542 class_heritage: vec![],
1543 injection_tokens: vec![],
1544 local_type_declarations: Vec::new(),
1545 public_signature_type_references: Vec::new(),
1546 namespace_object_aliases: Vec::new(),
1547 iconify_prefixes: Vec::new(),
1548 iconify_icon_names: Vec::new(),
1549 auto_import_candidates: Vec::new(),
1550 directives: Vec::new(),
1551 security_sinks: Vec::new(),
1552 security_sinks_skipped: 0,
1553 tainted_bindings: Vec::new(),
1554 sanitized_sink_args: Vec::new(),
1555 security_control_sites: Vec::new(),
1556 }];
1557
1558 let rules = RulesConfig {
1559 unused_files: Severity::Error,
1560 ..RulesConfig::default()
1561 };
1562 let config = make_config_with_rules(rules);
1563
1564 let results = find_dead_code_full(&graph, &config, &[], None, &[], &modules, false);
1565
1566 assert!(
1567 !results.unused_files.iter().any(|f| f
1568 .file
1569 .path
1570 .to_string_lossy()
1571 .contains("utils.ts")),
1572 "suppressed file should not appear in unused_files"
1573 );
1574 }
1575 }
1576}