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