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