1mod boundary;
2mod boundary_calls;
3mod boundary_coverage;
4mod duplicate_prop_shape;
5mod dynamic_segment_name_conflict;
6pub mod feature_flags;
7mod iconify;
8mod invalid_client_exports;
9mod misplaced_directive;
10mod mixed_barrel;
11mod package_json_utils;
12mod policy;
13mod predicates;
14mod prop_drilling;
15mod re_export_cycles;
16mod react_intel;
17mod react_resolve;
18mod render_fan_in;
19mod route_collision;
20mod route_tree;
21mod security;
22mod server_only;
23mod thin_wrapper;
24mod unprovided_inject;
25mod unrendered_component;
26mod unused_catalog;
27mod unused_component_emit;
28mod unused_component_input;
29mod unused_component_output;
30mod unused_component_prop;
31mod unused_deps;
32mod unused_exports;
33mod unused_files;
34mod unused_load_data_key;
35mod unused_members;
36mod unused_overrides;
37mod unused_server_action;
38mod unused_svelte_event;
39
40#[cfg(test)]
41pub(crate) mod test_support;
42
43#[cfg(test)]
44pub(crate) use unused_deps::matches_virtual_prefix;
45
46pub use security::catalogue_title as security_catalogue_title;
50pub use security::derive_security_severity;
51
52use rustc_hash::{FxHashMap, FxHashSet};
53
54use fallow_config::{PackageJson, ResolvedConfig, Severity};
55
56use crate::discover::FileId;
57use crate::extract::ModuleInfo;
58use crate::graph::ModuleGraph;
59use crate::resolve::ResolvedModule;
60use fallow_types::output_dead_code::{
61 BoundaryCallViolationFinding, BoundaryCoverageViolationFinding, BoundaryViolationFinding,
62 CircularDependencyFinding, DuplicateExportFinding, DuplicatePropShapeFinding,
63 DynamicSegmentNameConflictFinding, EmptyCatalogGroupFinding, InvalidClientExportFinding,
64 MisconfiguredDependencyOverrideFinding, MisplacedDirectiveFinding,
65 MixedClientServerBarrelFinding, PolicyViolationFinding, PrivateTypeLeakFinding,
66 PropDrillingChainFinding, ReExportCycleFinding, RouteCollisionFinding,
67 TestOnlyDependencyFinding, ThinWrapperFinding, TypeOnlyDependencyFinding,
68 UnlistedDependencyFinding, UnprovidedInjectFinding, UnrenderedComponentFinding,
69 UnresolvedCatalogReferenceFinding, UnresolvedImportFinding, UnusedCatalogEntryFinding,
70 UnusedClassMemberFinding, UnusedComponentEmitFinding, UnusedComponentInputFinding,
71 UnusedComponentOutputFinding, UnusedComponentPropFinding, UnusedDependencyFinding,
72 UnusedDependencyOverrideFinding, UnusedDevDependencyFinding, UnusedEnumMemberFinding,
73 UnusedExportFinding, UnusedFileFinding, UnusedLoadDataKeyFinding,
74 UnusedOptionalDependencyFinding, UnusedStoreMemberFinding, UnusedSvelteEventFinding,
75 UnusedTypeFinding,
76};
77
78use crate::results::{
79 AnalysisResults, CircularDependency, CircularDependencyEdge, StaleSuppression,
80 UnusedDependency, UnusedExport, UnusedMember,
81};
82use crate::suppress::{IssueKind, SuppressionContext};
83
84use duplicate_prop_shape::find_duplicate_prop_shapes;
85use dynamic_segment_name_conflict::find_dynamic_segment_name_conflicts;
86use invalid_client_exports::find_invalid_client_exports;
87use misplaced_directive::find_misplaced_directives;
88use mixed_barrel::find_mixed_client_server_barrels;
89use prop_drilling::find_prop_drilling_chains;
90use re_export_cycles::find_re_export_cycles;
91use react_intel::compute_react_component_intel;
92use render_fan_in::compute_render_fan_in;
93use route_collision::find_route_collisions;
94use thin_wrapper::find_thin_wrappers;
95use unprovided_inject::{UnprovidedInjectInput, find_unprovided_injects};
96use unrendered_component::{
97 LitUnrenderedInput, find_unrendered_angular_components, find_unrendered_components,
98 find_unrendered_lit_elements,
99};
100#[expect(
101 deprecated,
102 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
103)]
104use unused_catalog::{
105 find_empty_catalog_groups, find_unresolved_catalog_references, find_unused_catalog_entries,
106 gather_pnpm_catalog_state,
107};
108use unused_component_emit::find_unused_component_emits;
109use unused_component_input::find_unused_component_inputs;
110use unused_component_output::find_unused_component_outputs;
111use unused_component_prop::{find_unused_component_props, find_unused_react_props};
112#[expect(
113 deprecated,
114 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
115)]
116use unused_deps::{
117 UnlistedDependencyInput, find_test_only_dependencies, find_type_only_dependencies,
118 find_unlisted_dependencies, find_unresolved_imports, find_unused_dependencies,
119};
120#[expect(
121 deprecated,
122 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
123)]
124use unused_exports::{
125 collect_export_usages, find_private_type_leaks, find_unused_exports,
126 suppress_signature_backing_types,
127};
128#[expect(
129 deprecated,
130 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
131)]
132use unused_files::find_unused_files;
133use unused_load_data_key::find_unused_load_data_keys;
134use unused_members::{UnusedMemberScanInput, find_unused_members_with_public_api_entry_points};
135#[expect(
136 deprecated,
137 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
138)]
139use unused_overrides::{
140 find_misconfigured_dependency_overrides, find_unused_dependency_overrides,
141 gather_pnpm_override_state,
142};
143use unused_server_action::reclassify_unused_server_actions;
144use unused_svelte_event::find_unused_svelte_events;
145
146#[doc(hidden)]
149pub type LineOffsetsMap<'a> = FxHashMap<FileId, &'a [u32]>;
150
151struct SecurityDetectionContext<'a, 'm> {
152 graph: &'a ModuleGraph,
153 modules: &'a [ModuleInfo],
154 config: &'a ResolvedConfig,
155 suppressions: &'a crate::suppress::SuppressionContext<'m>,
156 line_offsets_by_file: &'a LineOffsetsMap<'m>,
157 declared_deps: &'a FxHashSet<String>,
158 request_receivers: &'a FxHashSet<String>,
159}
160
161#[doc(hidden)]
164pub fn byte_offset_to_line_col(
165 line_offsets_map: &LineOffsetsMap<'_>,
166 file_id: FileId,
167 byte_offset: u32,
168) -> (u32, u32) {
169 line_offsets_map
170 .get(&file_id)
171 .map_or((1, byte_offset), |offsets| {
172 fallow_types::extract::byte_offset_to_line_col(offsets, byte_offset)
173 })
174}
175
176fn cycle_edge_line_col(
177 graph: &ModuleGraph,
178 line_offsets_map: &LineOffsetsMap<'_>,
179 cycle: &[FileId],
180 edge_index: usize,
181) -> Option<(u32, u32)> {
182 if cycle.is_empty() {
183 return None;
184 }
185
186 let from = cycle[edge_index];
187 let to = cycle[(edge_index + 1) % cycle.len()];
188 graph
189 .find_import_span_start(from, to)
190 .map(|span_start| byte_offset_to_line_col(line_offsets_map, from, span_start))
191}
192
193fn is_circular_dependency_suppressed(
194 graph: &ModuleGraph,
195 line_offsets_map: &LineOffsetsMap<'_>,
196 suppressions: &crate::suppress::SuppressionContext<'_>,
197 cycle: &[FileId],
198) -> bool {
199 if cycle
200 .iter()
201 .any(|&id| suppressions.is_file_suppressed(id, IssueKind::CircularDependency))
202 {
203 return true;
204 }
205
206 let mut line_suppressed = false;
207 for edge_index in 0..cycle.len() {
208 let from = cycle[edge_index];
209 if let Some((line, _)) = cycle_edge_line_col(graph, line_offsets_map, cycle, edge_index)
210 && suppressions.is_suppressed(from, line, IssueKind::CircularDependency)
211 {
212 line_suppressed = true;
213 }
214 }
215 line_suppressed
216}
217
218fn read_source(path: &std::path::Path) -> String {
222 std::fs::read_to_string(path).unwrap_or_default()
223}
224
225fn is_cross_package_cycle(
230 files: &[std::path::PathBuf],
231 workspaces: &[fallow_config::WorkspaceInfo],
232) -> bool {
233 let find_workspace = |path: &std::path::Path| -> Option<&std::path::Path> {
234 workspaces
235 .iter()
236 .map(|w| w.root.as_path())
237 .filter(|root| path.starts_with(root))
238 .max_by_key(|root| root.components().count())
239 };
240
241 let mut seen_workspace: Option<&std::path::Path> = None;
242 for file in files {
243 if let Some(ws) = find_workspace(file) {
244 match &seen_workspace {
245 None => seen_workspace = Some(ws),
246 Some(prev) if *prev != ws => return true,
247 _ => {}
248 }
249 }
250 }
251 false
252}
253
254fn public_workspace_roots<'a>(
255 public_packages: &[String],
256 workspaces: &'a [fallow_config::WorkspaceInfo],
257) -> Vec<&'a std::path::Path> {
258 if public_packages.is_empty() || workspaces.is_empty() {
259 return Vec::new();
260 }
261
262 workspaces
263 .iter()
264 .filter(|ws| {
265 public_packages.iter().any(|pattern| {
266 ws.name == *pattern
267 || globset::Glob::new(pattern)
268 .ok()
269 .is_some_and(|g| g.compile_matcher().is_match(&ws.name))
270 })
271 })
272 .map(|ws| ws.root.as_path())
273 .collect()
274}
275
276fn graph_path_to_file_id(graph: &ModuleGraph) -> FxHashMap<std::path::PathBuf, FileId> {
288 graph
289 .modules
290 .iter()
291 .map(|module| (module.path.clone(), module.file_id))
292 .collect()
293}
294
295fn resolve_entry_via_scoped_canonical(
305 graph: &ModuleGraph,
306 package_root: &std::path::Path,
307 canonical_entry: &std::path::Path,
308) -> Option<FileId> {
309 match_canonical_entry_under_package(
310 graph.modules.iter().map(|m| (m.path.as_path(), m.file_id)),
311 package_root,
312 canonical_entry,
313 )
314}
315
316fn match_canonical_entry_under_package<'a>(
321 candidates: impl Iterator<Item = (&'a std::path::Path, FileId)>,
322 package_root: &std::path::Path,
323 canonical_entry: &std::path::Path,
324) -> Option<FileId> {
325 candidates
326 .filter(|(path, _)| path.starts_with(package_root))
327 .find_map(|(path, file_id)| {
328 (dunce::canonicalize(path).ok().as_deref() == Some(canonical_entry)).then_some(file_id)
329 })
330}
331
332fn add_package_public_api_entry_points(
333 public_api_entry_points: &mut FxHashSet<FileId>,
334 graph: &ModuleGraph,
335 path_to_file_id: &FxHashMap<std::path::PathBuf, FileId>,
336 package_root: &std::path::Path,
337 package_json: &PackageJson,
338 canonical_project_root: &std::path::Path,
339) {
340 if package_json.private.unwrap_or(false) {
341 return;
342 }
343
344 for entry in package_json.entry_points() {
345 let Some(entry_point) = crate::discover::resolve_entry_path(
346 package_root,
347 &entry,
348 canonical_project_root,
349 crate::discover::EntryPointSource::PackageJsonExports,
350 ) else {
351 continue;
352 };
353
354 if let Some(file_id) = path_to_file_id.get(&entry_point.path).copied().or_else(|| {
355 dunce::canonicalize(&entry_point.path)
356 .ok()
357 .and_then(|canonical| {
358 path_to_file_id.get(&canonical).copied().or_else(|| {
359 resolve_entry_via_scoped_canonical(graph, package_root, &canonical)
360 })
361 })
362 }) {
363 public_api_entry_points.insert(file_id);
364 }
365 }
366}
367
368fn is_source_index_under_package(path: &std::path::Path, package_root: &std::path::Path) -> bool {
369 let Ok(relative) = path.strip_prefix(package_root) else {
370 return false;
371 };
372
373 if !matches!(
374 relative.components().next(),
375 Some(std::path::Component::Normal(segment)) if segment == "src"
376 ) {
377 return false;
378 }
379
380 path.file_stem()
381 .and_then(|stem| stem.to_str())
382 .is_some_and(|stem| stem == "index")
383}
384
385fn add_exportless_package_source_indexes(
386 public_api_entry_points: &mut FxHashSet<FileId>,
387 graph: &ModuleGraph,
388 package_root: &std::path::Path,
389 package_json: &PackageJson,
390) {
391 if package_json.private.unwrap_or(false) || package_json.exports.is_some() {
392 return;
393 }
394
395 let mut roots = vec![package_root.to_path_buf()];
396 if let Ok(canonical) = dunce::canonicalize(package_root) {
397 roots.push(canonical);
398 }
399
400 for module in &graph.modules {
401 if roots
402 .iter()
403 .any(|root| is_source_index_under_package(&module.path, root))
404 {
405 public_api_entry_points.insert(module.file_id);
406 }
407 }
408}
409
410pub fn public_api_package_entry_points(
417 graph: &ModuleGraph,
418 config: &ResolvedConfig,
419 root_pkg: Option<&PackageJson>,
420 workspaces: &[fallow_config::WorkspaceInfo],
421) -> FxHashSet<FileId> {
422 let mut public_api_entry_points = FxHashSet::default();
423 let path_to_file_id = graph_path_to_file_id(graph);
424 let canonical_project_root =
425 dunce::canonicalize(&config.root).unwrap_or_else(|_| config.root.clone());
426
427 add_root_public_api_entry_points(
428 &mut public_api_entry_points,
429 graph,
430 &path_to_file_id,
431 config,
432 root_pkg,
433 &canonical_project_root,
434 );
435 add_workspace_public_api_entry_points(
436 &mut public_api_entry_points,
437 graph,
438 &path_to_file_id,
439 workspaces,
440 &canonical_project_root,
441 );
442
443 public_api_entry_points
444}
445
446fn add_root_public_api_entry_points(
447 public_api_entry_points: &mut FxHashSet<FileId>,
448 graph: &ModuleGraph,
449 path_to_file_id: &FxHashMap<std::path::PathBuf, FileId>,
450 config: &ResolvedConfig,
451 root_pkg: Option<&PackageJson>,
452 canonical_project_root: &std::path::Path,
453) {
454 if let Some(pkg) = root_pkg {
455 add_package_public_api_entry_points(
456 public_api_entry_points,
457 graph,
458 path_to_file_id,
459 &config.root,
460 pkg,
461 canonical_project_root,
462 );
463 add_exportless_package_source_indexes(public_api_entry_points, graph, &config.root, pkg);
464 }
465}
466
467fn add_workspace_public_api_entry_points(
468 public_api_entry_points: &mut FxHashSet<FileId>,
469 graph: &ModuleGraph,
470 path_to_file_id: &FxHashMap<std::path::PathBuf, FileId>,
471 workspaces: &[fallow_config::WorkspaceInfo],
472 canonical_project_root: &std::path::Path,
473) {
474 for workspace in workspaces {
475 let Ok(pkg) = PackageJson::load(&workspace.root.join("package.json")) else {
476 continue;
477 };
478 add_package_public_api_entry_points(
479 public_api_entry_points,
480 graph,
481 path_to_file_id,
482 &workspace.root,
483 &pkg,
484 canonical_project_root,
485 );
486 add_exportless_package_source_indexes(
487 public_api_entry_points,
488 graph,
489 &workspace.root,
490 &pkg,
491 );
492 }
493}
494
495fn find_circular_dependencies(
496 graph: &ModuleGraph,
497 line_offsets_map: &LineOffsetsMap<'_>,
498 suppressions: &crate::suppress::SuppressionContext<'_>,
499 workspaces: &[fallow_config::WorkspaceInfo],
500) -> Vec<CircularDependency> {
501 let cycles = graph.find_cycles();
502 let mut dependencies: Vec<CircularDependency> = cycles
503 .into_iter()
504 .filter_map(|cycle| {
505 if is_circular_dependency_suppressed(graph, line_offsets_map, suppressions, &cycle) {
506 return None;
507 }
508 Some(circular_dependency_from_cycle(
509 graph,
510 line_offsets_map,
511 &cycle,
512 ))
513 })
514 .collect();
515
516 if !workspaces.is_empty() {
517 for dep in &mut dependencies {
518 dep.is_cross_package = is_cross_package_cycle(&dep.files, workspaces);
519 }
520 }
521
522 dependencies
523}
524
525fn circular_dependency_from_cycle(
526 graph: &ModuleGraph,
527 line_offsets_map: &LineOffsetsMap<'_>,
528 cycle: &[FileId],
529) -> CircularDependency {
530 let edges: Vec<CircularDependencyEdge> = (0..cycle.len())
536 .map(|edge_index| {
537 let from = cycle[edge_index];
538 let (line, col) =
539 cycle_edge_line_col(graph, line_offsets_map, cycle, edge_index).unwrap_or((1, 0));
540 CircularDependencyEdge {
541 path: graph.modules[from.0 as usize].path.clone(),
542 line,
543 col,
544 }
545 })
546 .collect();
547
548 let files: Vec<std::path::PathBuf> = edges.iter().map(|edge| edge.path.clone()).collect();
549 let length = files.len();
550 let (line, col) = edges.first().map_or((1, 0), |edge| (edge.line, edge.col));
553 CircularDependency {
554 files,
555 length,
556 line,
557 col,
558 edges,
559 is_cross_package: false,
560 }
561}
562
563fn run_circular_dep_detector(
568 graph: &ModuleGraph,
569 config: &ResolvedConfig,
570 line_offsets_by_file: &LineOffsetsMap<'_>,
571 suppressions: &crate::suppress::SuppressionContext<'_>,
572 workspaces: &[fallow_config::WorkspaceInfo],
573) -> Vec<CircularDependencyFinding> {
574 if config.rules.circular_dependencies == Severity::Off {
575 return Vec::new();
576 }
577 find_circular_dependencies(graph, line_offsets_by_file, suppressions, workspaces)
578 .into_iter()
579 .map(CircularDependencyFinding::with_actions)
580 .collect()
581}
582
583fn run_boundary_coverage_detector(
588 graph: &ModuleGraph,
589 config: &ResolvedConfig,
590 suppressions: &crate::suppress::SuppressionContext<'_>,
591) -> Vec<BoundaryCoverageViolationFinding> {
592 if config.rules.boundary_violation == Severity::Off {
593 return Vec::new();
594 }
595 boundary_coverage::find_boundary_coverage_violations(graph, config, suppressions)
596 .into_iter()
597 .map(BoundaryCoverageViolationFinding::with_actions)
598 .collect()
599}
600
601fn run_boundary_call_detector(
605 graph: &ModuleGraph,
606 modules: &[ModuleInfo],
607 config: &ResolvedConfig,
608 suppressions: &crate::suppress::SuppressionContext<'_>,
609 line_offsets_by_file: &LineOffsetsMap<'_>,
610) -> Vec<BoundaryCallViolationFinding> {
611 if config.rules.boundary_violation == Severity::Off {
612 return Vec::new();
613 }
614 boundary_calls::find_boundary_call_violations(
615 graph,
616 modules,
617 config,
618 suppressions,
619 line_offsets_by_file,
620 )
621 .into_iter()
622 .map(BoundaryCallViolationFinding::with_actions)
623 .collect()
624}
625
626fn run_policy_detector(
631 graph: &ModuleGraph,
632 modules: &[ModuleInfo],
633 config: &ResolvedConfig,
634 declared_deps: &FxHashSet<String>,
635 suppressions: &crate::suppress::SuppressionContext<'_>,
636 line_offsets_by_file: &LineOffsetsMap<'_>,
637) -> Vec<PolicyViolationFinding> {
638 if config.rules.policy_violation == Severity::Off || config.rule_packs.is_empty() {
639 return Vec::new();
640 }
641 policy::find_policy_violations(
642 graph,
643 modules,
644 config,
645 declared_deps,
646 suppressions,
647 line_offsets_by_file,
648 )
649 .into_iter()
650 .map(PolicyViolationFinding::with_actions)
651 .collect()
652}
653
654fn run_boundary_aux_detectors(
658 graph: &ModuleGraph,
659 modules: &[ModuleInfo],
660 config: &ResolvedConfig,
661 declared_deps: &FxHashSet<String>,
662 suppressions: &crate::suppress::SuppressionContext<'_>,
663 line_offsets_by_file: &LineOffsetsMap<'_>,
664) -> (
665 Vec<BoundaryCoverageViolationFinding>,
666 (
667 Vec<BoundaryCallViolationFinding>,
668 Vec<PolicyViolationFinding>,
669 ),
670) {
671 rayon::join(
672 || run_boundary_coverage_detector(graph, config, suppressions),
673 || {
674 rayon::join(
675 || {
676 run_boundary_call_detector(
677 graph,
678 modules,
679 config,
680 suppressions,
681 line_offsets_by_file,
682 )
683 },
684 || {
685 run_policy_detector(
686 graph,
687 modules,
688 config,
689 declared_deps,
690 suppressions,
691 line_offsets_by_file,
692 )
693 },
694 )
695 },
696 )
697}
698
699fn run_re_export_cycle_detector(
702 graph: &ModuleGraph,
703 config: &ResolvedConfig,
704 suppressions: &crate::suppress::SuppressionContext<'_>,
705) -> Vec<ReExportCycleFinding> {
706 if config.rules.re_export_cycle == Severity::Off {
707 return Vec::new();
708 }
709 find_re_export_cycles(graph, suppressions)
710}
711
712fn run_export_usages_collector(
715 graph: &ModuleGraph,
716 line_offsets_by_file: &LineOffsetsMap<'_>,
717 collect_usages: bool,
718) -> Vec<crate::results::ExportUsage> {
719 if collect_usages {
720 collect_export_usages(graph, line_offsets_by_file)
721 } else {
722 Vec::new()
723 }
724}
725
726fn collect_declared_dependency_names(
733 config: &ResolvedConfig,
734 root_pkg: Option<&PackageJson>,
735 workspaces: &[fallow_config::WorkspaceInfo],
736) -> FxHashSet<String> {
737 let mut deps: FxHashSet<String> = FxHashSet::default();
738 if let Some(pkg) = root_pkg {
739 deps.extend(pkg.all_dependency_names());
740 }
741 for ws in workspaces {
742 if ws.root == config.root {
743 continue; }
745 if let Ok(pkg) = PackageJson::load(&ws.root.join("package.json")) {
746 deps.extend(pkg.all_dependency_names());
747 }
748 }
749 deps
750}
751
752struct DeadCodeRunContext<'a> {
753 suppressions: SuppressionContext<'a>,
754 line_offsets_by_file: LineOffsetsMap<'a>,
755 pkg: Option<PackageJson>,
756 public_api_entry_points: FxHashSet<FileId>,
757 declared_deps: FxHashSet<String>,
758}
759
760fn build_dead_code_run_context<'a>(
761 graph: &'a ModuleGraph,
762 config: &ResolvedConfig,
763 workspaces: &[fallow_config::WorkspaceInfo],
764 modules: &'a [ModuleInfo],
765) -> DeadCodeRunContext<'a> {
766 let suppressions = SuppressionContext::new(modules);
767 let line_offsets_by_file: LineOffsetsMap<'a> = modules
768 .iter()
769 .filter(|m| !m.line_offsets.is_empty())
770 .map(|m| (m.file_id, m.line_offsets.as_slice()))
771 .collect();
772
773 let pkg_path = config.root.join("package.json");
774 let pkg = PackageJson::load(&pkg_path).ok();
775 let public_api_entry_points =
776 public_api_package_entry_points(graph, config, pkg.as_ref(), workspaces);
777 let declared_deps = collect_declared_dependency_names(config, pkg.as_ref(), workspaces);
778
779 DeadCodeRunContext {
780 suppressions,
781 line_offsets_by_file,
782 pkg,
783 public_api_entry_points,
784 declared_deps,
785 }
786}
787
788#[deprecated(
790 since = "2.76.0",
791 note = "fallow_core is internal; use fallow_api::run_dead_code for typed output; serialize with fallow_api::serialize_dead_code_programmatic_json for JSON output. See docs/fallow-core-migration.md and ADR-008."
792)]
793#[expect(
794 clippy::too_many_arguments,
795 reason = "frozen deprecated public API (ADR-008); signature must not change"
796)]
797pub fn find_dead_code_full(
798 graph: &ModuleGraph,
799 config: &ResolvedConfig,
800 resolved_modules: &[ResolvedModule],
801 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
802 workspaces: &[fallow_config::WorkspaceInfo],
803 modules: &[ModuleInfo],
804 collect_usages: bool,
805) -> AnalysisResults {
806 let _span = tracing::info_span!("find_dead_code").entered();
807
808 let run_context = build_dead_code_run_context(graph, config, workspaces, modules);
809
810 let mut results = run_setup_and_detect(&SetupAndDetectInput {
811 graph,
812 config,
813 resolved_modules,
814 plugin_result,
815 workspaces,
816 modules,
817 suppressions: &run_context.suppressions,
818 line_offsets_by_file: &run_context.line_offsets_by_file,
819 pkg: run_context.pkg.as_ref(),
820 public_api_entry_points: &run_context.public_api_entry_points,
821 declared_deps: &run_context.declared_deps,
822 collect_usages,
823 });
824
825 populate_post_detection_findings(&mut PostDetectionInput {
826 graph,
827 modules,
828 resolved_modules,
829 config,
830 workspaces,
831 declared_deps: &run_context.declared_deps,
832 public_api_entry_points: &run_context.public_api_entry_points,
833 suppressions: &run_context.suppressions,
834 line_offsets_by_file: &run_context.line_offsets_by_file,
835 collect_usages,
836 results: &mut results,
837 });
838
839 results.sort();
840
841 results
842}
843
844struct SetupAndDetectInput<'a, 'm> {
847 graph: &'a ModuleGraph,
848 config: &'a ResolvedConfig,
849 resolved_modules: &'a [ResolvedModule],
850 plugin_result: Option<&'a crate::plugins::AggregatedPluginResult>,
851 workspaces: &'a [fallow_config::WorkspaceInfo],
852 modules: &'a [ModuleInfo],
853 suppressions: &'a SuppressionContext<'m>,
854 line_offsets_by_file: &'a LineOffsetsMap<'m>,
855 pkg: Option<&'a PackageJson>,
856 public_api_entry_points: &'a FxHashSet<FileId>,
857 declared_deps: &'a FxHashSet<String>,
858 collect_usages: bool,
859}
860
861fn run_setup_and_detect(input: &SetupAndDetectInput<'_, '_>) -> AnalysisResults {
866 let iconify_referenced =
867 iconify::collect_iconify_referenced_deps(input.modules, input.pkg, input.workspaces);
868 let augmented_plugin_result;
869 let plugin_result = if iconify_referenced.is_empty() {
870 input.plugin_result
871 } else {
872 let mut owned = input.plugin_result.cloned().unwrap_or_default();
873 owned.referenced_dependencies.extend(iconify_referenced);
874 augmented_plugin_result = owned;
875 Some(&augmented_plugin_result)
876 };
877
878 let mut user_class_members = input.config.used_class_members.clone();
879 if let Some(plugin_result) = plugin_result {
880 user_class_members.extend(plugin_result.used_class_members.iter().cloned());
881 }
882
883 let (virtual_prefixes, generated_patterns, generated_type_prefixes) =
884 derive_plugin_string_slices(plugin_result);
885
886 run_parallel_dead_code_detectors(DeadCodeDetectorInput {
887 graph: input.graph,
888 config: input.config,
889 resolved_modules: input.resolved_modules,
890 workspaces: input.workspaces,
891 modules: input.modules,
892 suppressions: input.suppressions,
893 line_offsets_by_file: input.line_offsets_by_file,
894 plugin_result,
895 pkg: input.pkg,
896 user_class_members: &user_class_members,
897 public_api_entry_points: input.public_api_entry_points,
898 virtual_prefixes: &virtual_prefixes,
899 generated_patterns: &generated_patterns,
900 generated_type_prefixes: &generated_type_prefixes,
901 declared_deps: input.declared_deps,
902 collect_usages: input.collect_usages,
903 })
904}
905
906fn derive_plugin_string_slices(
909 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
910) -> (Vec<&str>, Vec<&str>, Vec<&str>) {
911 let virtual_prefixes = plugin_result
912 .map(|pr| {
913 pr.virtual_module_prefixes
914 .iter()
915 .map(String::as_str)
916 .collect()
917 })
918 .unwrap_or_default();
919 let generated_patterns = plugin_result
920 .map(|pr| {
921 pr.generated_import_patterns
922 .iter()
923 .map(String::as_str)
924 .collect()
925 })
926 .unwrap_or_default();
927 let generated_type_prefixes = plugin_result
928 .map(|pr| {
929 pr.generated_type_import_prefixes
930 .iter()
931 .map(String::as_str)
932 .collect()
933 })
934 .unwrap_or_default();
935 (
936 virtual_prefixes,
937 generated_patterns,
938 generated_type_prefixes,
939 )
940}
941
942struct PostDetectionInput<'a, 'm> {
945 graph: &'a ModuleGraph,
946 modules: &'a [ModuleInfo],
947 resolved_modules: &'a [ResolvedModule],
948 config: &'a ResolvedConfig,
949 workspaces: &'a [fallow_config::WorkspaceInfo],
950 declared_deps: &'a FxHashSet<String>,
951 public_api_entry_points: &'a FxHashSet<FileId>,
952 suppressions: &'a SuppressionContext<'m>,
953 line_offsets_by_file: &'a LineOffsetsMap<'m>,
954 collect_usages: bool,
957 results: &'a mut AnalysisResults,
958}
959
960fn populate_post_detection_findings(input: &mut PostDetectionInput<'_, '_>) {
965 filter_public_workspace_results(input.config, input.workspaces, input.results);
966
967 if input.config.rules.unused_server_actions != Severity::Off {
971 reclassify_unused_server_actions(
972 input.graph,
973 input.modules,
974 input.declared_deps,
975 input.suppressions,
976 input.results,
977 );
978 }
979
980 populate_configured_security_findings(input);
981 populate_package_and_framework_findings(input);
982 populate_stale_suppression_findings(input);
983}
984
985fn populate_configured_security_findings(input: &mut PostDetectionInput<'_, '_>) {
986 let request_receivers = input
987 .config
988 .security
989 .request_receivers
990 .iter()
991 .cloned()
992 .collect::<FxHashSet<_>>();
993
994 populate_security_findings(
995 &SecurityDetectionContext {
996 graph: input.graph,
997 modules: input.modules,
998 config: input.config,
999 suppressions: input.suppressions,
1000 line_offsets_by_file: input.line_offsets_by_file,
1001 declared_deps: input.declared_deps,
1002 request_receivers: &request_receivers,
1003 },
1004 input.results,
1005 );
1006}
1007
1008fn populate_package_and_framework_findings(input: &mut PostDetectionInput<'_, '_>) {
1009 populate_pnpm_catalog_findings(input.config, input.workspaces, input.results);
1015 populate_pnpm_override_findings(input.config, input.workspaces, input.results);
1016 populate_framework_specific_findings(&mut FrameworkSpecificFindingsInput {
1017 graph: input.graph,
1018 modules: input.modules,
1019 resolved_modules: input.resolved_modules,
1020 config: input.config,
1021 workspaces: input.workspaces,
1022 declared_deps: input.declared_deps,
1023 public_api_entry_points: input.public_api_entry_points,
1024 suppressions: input.suppressions,
1025 line_offsets_by_file: input.line_offsets_by_file,
1026 collect_usages: input.collect_usages,
1027 results: input.results,
1028 });
1029}
1030
1031fn populate_stale_suppression_findings(input: &mut PostDetectionInput<'_, '_>) {
1034 if input.config.rules.stale_suppressions != Severity::Off {
1035 input
1036 .results
1037 .stale_suppressions
1038 .extend(input.suppressions.find_stale(input.graph, input.config));
1039 }
1040 if input.config.rules.require_suppression_reason != Severity::Off {
1041 input
1042 .results
1043 .stale_suppressions
1044 .extend(input.suppressions.find_missing_reasons(input.graph));
1045 }
1046 input.results.suppression_count = input.suppressions.used_count();
1047 input.results.active_suppressions = input.suppressions.all_suppressions(input.graph);
1048}
1049
1050struct FrameworkSpecificFindingsInput<'a> {
1056 graph: &'a ModuleGraph,
1057 modules: &'a [ModuleInfo],
1058 resolved_modules: &'a [ResolvedModule],
1059 config: &'a ResolvedConfig,
1060 workspaces: &'a [fallow_config::WorkspaceInfo],
1061 declared_deps: &'a FxHashSet<String>,
1062 public_api_entry_points: &'a FxHashSet<FileId>,
1063 suppressions: &'a SuppressionContext<'a>,
1064 line_offsets_by_file: &'a LineOffsetsMap<'a>,
1065 collect_usages: bool,
1068 results: &'a mut AnalysisResults,
1069}
1070
1071fn populate_framework_specific_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1072 populate_client_boundary_findings(input);
1073 populate_component_contract_findings(input);
1074 populate_react_health_findings(input);
1075 populate_nextjs_findings(input);
1076}
1077
1078fn populate_client_boundary_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1079 populate_invalid_client_export_findings(input);
1080 populate_mixed_client_server_barrel_findings(input);
1081 populate_misplaced_directive_findings(input);
1082}
1083
1084fn populate_component_contract_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1085 populate_unprovided_inject_findings(input);
1086 populate_unrendered_component_findings(input);
1087 populate_unused_component_prop_findings(input);
1088 populate_unused_component_emit_findings(
1089 input.graph,
1090 input.modules,
1091 input.config,
1092 input.declared_deps,
1093 input.line_offsets_by_file,
1094 input.results,
1095 );
1096 populate_unused_component_input_findings(
1097 input.graph,
1098 input.modules,
1099 input.config,
1100 input.declared_deps,
1101 input.line_offsets_by_file,
1102 input.results,
1103 );
1104 populate_unused_component_output_findings(
1105 input.graph,
1106 input.modules,
1107 input.config,
1108 input.declared_deps,
1109 input.line_offsets_by_file,
1110 input.results,
1111 );
1112 populate_unused_svelte_event_findings(
1113 input.graph,
1114 input.modules,
1115 input.config,
1116 input.declared_deps,
1117 input.line_offsets_by_file,
1118 input.results,
1119 );
1120 populate_unused_load_data_key_findings(input);
1121}
1122
1123fn populate_react_health_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1124 populate_prop_drilling_findings(input);
1125 populate_thin_wrapper_findings(input);
1126 populate_render_fan_in(input);
1127 populate_react_component_intel(input);
1128 populate_duplicate_prop_shape_findings(input);
1129}
1130
1131fn populate_nextjs_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1132 populate_nextjs_route_tree_findings(
1133 input.graph,
1134 input.config,
1135 input.workspaces,
1136 input.declared_deps,
1137 input.suppressions,
1138 input.results,
1139 );
1140}
1141
1142fn populate_render_fan_in(input: &mut FrameworkSpecificFindingsInput<'_>) {
1150 input.results.render_fan_in = compute_render_fan_in(
1151 input.graph,
1152 input.modules,
1153 input.resolved_modules,
1154 input.declared_deps,
1155 &input.config.root,
1156 );
1157}
1158
1159fn populate_react_component_intel(input: &mut FrameworkSpecificFindingsInput<'_>) {
1169 if !input.collect_usages {
1170 return;
1171 }
1172 input.results.react_component_intel = compute_react_component_intel(
1173 input.graph,
1174 input.modules,
1175 input.resolved_modules,
1176 input.declared_deps,
1177 &input.config.root,
1178 input.line_offsets_by_file,
1179 );
1180}
1181
1182fn populate_unused_load_data_key_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1187 if input.config.rules.unused_load_data_keys == Severity::Off {
1188 return;
1189 }
1190 let result = find_unused_load_data_keys(
1191 input.graph,
1192 input.modules,
1193 input.declared_deps,
1194 input.suppressions,
1195 input.line_offsets_by_file,
1196 &input.config.root,
1197 );
1198 if result.global_abstain {
1199 input.results.unused_load_data_keys_global_abstain = true;
1200 tracing::debug!(
1201 "unused-load-data-key: abstained project-wide (a whole-object use of \
1202 page.data / $page.data was seen; any key could be read reflectively)"
1203 );
1204 }
1205 input.results.unused_load_data_keys = result
1206 .findings
1207 .into_iter()
1208 .map(UnusedLoadDataKeyFinding::with_actions)
1209 .collect();
1210}
1211
1212fn populate_invalid_client_export_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1216 if input.config.rules.invalid_client_export == Severity::Off {
1217 return;
1218 }
1219 input.results.invalid_client_exports = find_invalid_client_exports(
1220 input.graph,
1221 input.modules,
1222 input.declared_deps,
1223 input.suppressions,
1224 input.line_offsets_by_file,
1225 )
1226 .into_iter()
1227 .map(InvalidClientExportFinding::with_actions)
1228 .collect();
1229}
1230
1231fn populate_mixed_client_server_barrel_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1235 if input.config.rules.mixed_client_server_barrel == Severity::Off {
1236 return;
1237 }
1238 input.results.mixed_client_server_barrels = find_mixed_client_server_barrels(
1239 input.graph,
1240 input.modules,
1241 input.resolved_modules,
1242 input.declared_deps,
1243 input.suppressions,
1244 input.line_offsets_by_file,
1245 )
1246 .into_iter()
1247 .map(MixedClientServerBarrelFinding::with_actions)
1248 .collect();
1249}
1250
1251fn populate_misplaced_directive_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1255 if input.config.rules.misplaced_directive == Severity::Off {
1256 return;
1257 }
1258 input.results.misplaced_directives = find_misplaced_directives(
1259 input.graph,
1260 input.modules,
1261 input.declared_deps,
1262 input.suppressions,
1263 input.line_offsets_by_file,
1264 )
1265 .into_iter()
1266 .map(MisplacedDirectiveFinding::with_actions)
1267 .collect();
1268}
1269
1270fn populate_unprovided_inject_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1274 if input.config.rules.unprovided_injects == Severity::Off {
1275 return;
1276 }
1277 input.results.unprovided_injects = find_unprovided_injects(UnprovidedInjectInput {
1278 graph: input.graph,
1279 resolved_modules: input.resolved_modules,
1280 modules: input.modules,
1281 declared_deps: input.declared_deps,
1282 public_api_entry_points: input.public_api_entry_points,
1283 suppressions: input.suppressions,
1284 line_offsets_by_file: input.line_offsets_by_file,
1285 })
1286 .into_iter()
1287 .map(UnprovidedInjectFinding::with_actions)
1288 .collect();
1289}
1290
1291fn populate_unrendered_component_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1295 if input.config.rules.unrendered_components == Severity::Off {
1296 return;
1297 }
1298 input.results.unrendered_components = find_unrendered_components(
1299 input.graph,
1300 input.resolved_modules,
1301 input.modules,
1302 input.declared_deps,
1303 input.public_api_entry_points,
1304 input.suppressions,
1305 )
1306 .into_iter()
1307 .map(UnrenderedComponentFinding::with_actions)
1308 .collect();
1309 input.results.unrendered_components.extend(
1314 find_unrendered_angular_components(
1315 input.graph,
1316 input.modules,
1317 input.declared_deps,
1318 input.public_api_entry_points,
1319 input.line_offsets_by_file,
1320 input.suppressions,
1321 )
1322 .into_iter()
1323 .map(UnrenderedComponentFinding::with_actions),
1324 );
1325 input.results.unrendered_components.extend(
1330 find_unrendered_lit_elements(&LitUnrenderedInput {
1331 graph: input.graph,
1332 modules: input.modules,
1333 declared_deps: input.declared_deps,
1334 public_api_entry_points: input.public_api_entry_points,
1335 line_offsets_by_file: input.line_offsets_by_file,
1336 suppressions: input.suppressions,
1337 root: &input.config.root,
1338 })
1339 .into_iter()
1340 .map(UnrenderedComponentFinding::with_actions),
1341 );
1342}
1343
1344fn populate_unused_component_prop_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1348 if input.config.rules.unused_component_props == Severity::Off {
1349 return;
1350 }
1351 let sfc = find_unused_component_props(
1353 input.graph,
1354 input.modules,
1355 input.declared_deps,
1356 input.line_offsets_by_file,
1357 input.config.unused_component_props_ignore.as_ref(),
1358 );
1359 input.results.unused_component_props_exempted += sfc.exempted;
1360 input.results.unused_component_props = sfc
1361 .findings
1362 .into_iter()
1363 .map(UnusedComponentPropFinding::with_actions)
1364 .collect();
1365
1366 append_react_unused_component_prop_findings(input);
1367 retain_unsuppressed_unused_component_prop_findings(input);
1368}
1369
1370fn append_react_unused_component_prop_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1371 let react = find_unused_react_props(
1375 input.graph,
1376 input.modules,
1377 input.declared_deps,
1378 input.line_offsets_by_file,
1379 input.config.unused_component_props_ignore.as_ref(),
1380 );
1381 input.results.unused_component_props_exempted += react.exempted;
1382 if react.components_scanned > 0 {
1383 tracing::info!(
1387 components_scanned = react.components_scanned,
1388 unused_props = react.findings.len(),
1389 "React detected, {} component(s) scanned for unused props",
1390 react.components_scanned
1391 );
1392 }
1393 input.results.unused_component_props.extend(
1394 react
1395 .findings
1396 .into_iter()
1397 .map(UnusedComponentPropFinding::with_actions),
1398 );
1399}
1400
1401fn retain_unsuppressed_unused_component_prop_findings(
1402 input: &mut FrameworkSpecificFindingsInput<'_>,
1403) {
1404 let path_to_id = graph_file_ids_by_path(input.graph);
1410 input.results.unused_component_props.retain(|finding| {
1411 !path_line_is_suppressed(
1412 &path_to_id,
1413 input.suppressions,
1414 finding.prop.path.as_path(),
1415 finding.prop.line,
1416 IssueKind::UnusedComponentProp,
1417 )
1418 });
1419}
1420
1421fn populate_unused_component_emit_findings(
1425 graph: &ModuleGraph,
1426 modules: &[ModuleInfo],
1427 config: &ResolvedConfig,
1428 declared_deps: &FxHashSet<String>,
1429 line_offsets_by_file: &LineOffsetsMap<'_>,
1430 results: &mut AnalysisResults,
1431) {
1432 if config.rules.unused_component_emits == Severity::Off {
1433 return;
1434 }
1435 results.unused_component_emits =
1436 find_unused_component_emits(graph, modules, declared_deps, line_offsets_by_file)
1437 .into_iter()
1438 .map(UnusedComponentEmitFinding::with_actions)
1439 .collect();
1440}
1441
1442fn populate_prop_drilling_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1449 if input.config.rules.prop_drilling == Severity::Off {
1450 return;
1451 }
1452 input.results.prop_drilling_chains = collect_prop_drilling_findings(input);
1453
1454 retain_unsuppressed_prop_drilling_findings(input);
1455}
1456
1457fn collect_prop_drilling_findings(
1458 input: &FrameworkSpecificFindingsInput<'_>,
1459) -> Vec<PropDrillingChainFinding> {
1460 let scan = find_prop_drilling_chains(
1461 input.graph,
1462 input.modules,
1463 input.resolved_modules,
1464 input.declared_deps,
1465 input.line_offsets_by_file,
1466 );
1467 if scan.components_scanned > 0 {
1468 tracing::info!(
1470 components_scanned = scan.components_scanned,
1471 prop_drilling_chains = scan.chains.len(),
1472 "React detected, {} component(s) scanned for prop drilling",
1473 scan.components_scanned
1474 );
1475 }
1476 scan.chains
1477 .into_iter()
1478 .map(PropDrillingChainFinding::with_actions)
1479 .collect()
1480}
1481
1482fn retain_unsuppressed_prop_drilling_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1483 let path_to_id = graph_file_ids_by_path(input.graph);
1489 input.results.prop_drilling_chains.retain(|finding| {
1490 let Some(source) = finding.chain.hops.first() else {
1491 return true;
1492 };
1493 !path_line_is_suppressed(
1494 &path_to_id,
1495 input.suppressions,
1496 source.file.as_path(),
1497 source.line,
1498 IssueKind::PropDrilling,
1499 )
1500 });
1501}
1502
1503fn populate_thin_wrapper_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1509 if input.config.rules.thin_wrapper == Severity::Off {
1510 return;
1511 }
1512 input.results.thin_wrappers = collect_thin_wrapper_findings(input);
1513
1514 retain_unsuppressed_thin_wrapper_findings(input);
1515}
1516
1517fn collect_thin_wrapper_findings(
1518 input: &FrameworkSpecificFindingsInput<'_>,
1519) -> Vec<ThinWrapperFinding> {
1520 let scan = find_thin_wrappers(
1521 input.graph,
1522 input.modules,
1523 input.resolved_modules,
1524 input.declared_deps,
1525 input.line_offsets_by_file,
1526 );
1527 if scan.components_scanned > 0 {
1528 tracing::info!(
1530 components_scanned = scan.components_scanned,
1531 thin_wrappers = scan.wrappers.len(),
1532 "React detected, {} component(s) scanned for thin wrappers",
1533 scan.components_scanned
1534 );
1535 }
1536 scan.wrappers
1537 .into_iter()
1538 .map(ThinWrapperFinding::with_actions)
1539 .collect()
1540}
1541
1542fn retain_unsuppressed_thin_wrapper_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1543 let path_to_id = graph_file_ids_by_path(input.graph);
1549 input.results.thin_wrappers.retain(|finding| {
1550 !path_line_is_suppressed(
1551 &path_to_id,
1552 input.suppressions,
1553 finding.wrapper.file.as_path(),
1554 finding.wrapper.line,
1555 IssueKind::ThinWrapper,
1556 )
1557 });
1558}
1559
1560fn populate_duplicate_prop_shape_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1575 if input.config.rules.duplicate_prop_shape == Severity::Off {
1576 return;
1577 }
1578 let scan = find_duplicate_prop_shapes(
1579 input.graph,
1580 input.modules,
1581 input.declared_deps,
1582 input.line_offsets_by_file,
1583 );
1584 if scan.components_scanned > 0 {
1585 tracing::info!(
1587 components_scanned = scan.components_scanned,
1588 duplicate_prop_shapes = scan.groups.len(),
1589 "React detected, {} component(s) scanned for duplicate prop shapes",
1590 scan.components_scanned
1591 );
1592 }
1593 input.results.duplicate_prop_shapes = scan
1594 .groups
1595 .into_iter()
1596 .map(DuplicatePropShapeFinding::with_actions)
1597 .collect();
1598
1599 let path_to_id = graph_file_ids_by_path(input.graph);
1604 input.results.duplicate_prop_shapes.retain(|finding| {
1605 !path_line_is_suppressed(
1606 &path_to_id,
1607 input.suppressions,
1608 finding.shape.file.as_path(),
1609 finding.shape.line,
1610 IssueKind::DuplicatePropShape,
1611 )
1612 });
1613}
1614
1615fn graph_file_ids_by_path(graph: &ModuleGraph) -> FxHashMap<&std::path::Path, FileId> {
1616 graph
1617 .modules
1618 .iter()
1619 .map(|node| (node.path.as_path(), node.file_id))
1620 .collect()
1621}
1622
1623fn path_line_is_suppressed(
1624 path_to_id: &FxHashMap<&std::path::Path, FileId>,
1625 suppressions: &SuppressionContext<'_>,
1626 path: &std::path::Path,
1627 line: u32,
1628 kind: IssueKind,
1629) -> bool {
1630 let Some(&file_id) = path_to_id.get(path) else {
1631 return false;
1632 };
1633 suppressions.is_suppressed(file_id, line, kind)
1634 || suppressions.is_file_suppressed(file_id, kind)
1635}
1636
1637fn populate_unused_component_input_findings(
1641 graph: &ModuleGraph,
1642 modules: &[ModuleInfo],
1643 config: &ResolvedConfig,
1644 declared_deps: &FxHashSet<String>,
1645 line_offsets_by_file: &LineOffsetsMap<'_>,
1646 results: &mut AnalysisResults,
1647) {
1648 if config.rules.unused_component_inputs == Severity::Off {
1649 return;
1650 }
1651 results.unused_component_inputs =
1652 find_unused_component_inputs(graph, modules, declared_deps, line_offsets_by_file)
1653 .into_iter()
1654 .map(UnusedComponentInputFinding::with_actions)
1655 .collect();
1656}
1657
1658fn populate_unused_component_output_findings(
1662 graph: &ModuleGraph,
1663 modules: &[ModuleInfo],
1664 config: &ResolvedConfig,
1665 declared_deps: &FxHashSet<String>,
1666 line_offsets_by_file: &LineOffsetsMap<'_>,
1667 results: &mut AnalysisResults,
1668) {
1669 if config.rules.unused_component_outputs == Severity::Off {
1670 return;
1671 }
1672 results.unused_component_outputs =
1673 find_unused_component_outputs(graph, modules, declared_deps, line_offsets_by_file)
1674 .into_iter()
1675 .map(UnusedComponentOutputFinding::with_actions)
1676 .collect();
1677}
1678
1679fn populate_unused_svelte_event_findings(
1683 graph: &ModuleGraph,
1684 modules: &[ModuleInfo],
1685 config: &ResolvedConfig,
1686 declared_deps: &FxHashSet<String>,
1687 line_offsets_by_file: &LineOffsetsMap<'_>,
1688 results: &mut AnalysisResults,
1689) {
1690 if config.rules.unused_svelte_events == Severity::Off {
1691 return;
1692 }
1693 results.unused_svelte_events =
1694 find_unused_svelte_events(graph, modules, declared_deps, line_offsets_by_file)
1695 .into_iter()
1696 .map(UnusedSvelteEventFinding::with_actions)
1697 .collect();
1698}
1699
1700fn populate_route_collision_findings(
1703 graph: &ModuleGraph,
1704 config: &ResolvedConfig,
1705 workspaces: &[fallow_config::WorkspaceInfo],
1706 declared_deps: &FxHashSet<String>,
1707 suppressions: &SuppressionContext<'_>,
1708 results: &mut AnalysisResults,
1709) {
1710 if config.rules.route_collision == Severity::Off {
1711 return;
1712 }
1713 results.route_collisions =
1714 find_route_collisions(graph, config, workspaces, declared_deps, suppressions)
1715 .into_iter()
1716 .map(RouteCollisionFinding::with_actions)
1717 .collect();
1718}
1719
1720fn populate_dynamic_segment_name_conflict_findings(
1724 graph: &ModuleGraph,
1725 config: &ResolvedConfig,
1726 workspaces: &[fallow_config::WorkspaceInfo],
1727 declared_deps: &FxHashSet<String>,
1728 suppressions: &SuppressionContext<'_>,
1729 results: &mut AnalysisResults,
1730) {
1731 if config.rules.dynamic_segment_name_conflict == Severity::Off {
1732 return;
1733 }
1734 results.dynamic_segment_name_conflicts =
1735 find_dynamic_segment_name_conflicts(graph, config, workspaces, declared_deps, suppressions)
1736 .into_iter()
1737 .map(DynamicSegmentNameConflictFinding::with_actions)
1738 .collect();
1739}
1740
1741fn populate_nextjs_route_tree_findings(
1746 graph: &ModuleGraph,
1747 config: &ResolvedConfig,
1748 workspaces: &[fallow_config::WorkspaceInfo],
1749 declared_deps: &FxHashSet<String>,
1750 suppressions: &SuppressionContext<'_>,
1751 results: &mut AnalysisResults,
1752) {
1753 populate_route_collision_findings(
1754 graph,
1755 config,
1756 workspaces,
1757 declared_deps,
1758 suppressions,
1759 results,
1760 );
1761 populate_dynamic_segment_name_conflict_findings(
1762 graph,
1763 config,
1764 workspaces,
1765 declared_deps,
1766 suppressions,
1767 results,
1768 );
1769}
1770
1771#[derive(Clone, Copy)]
1772struct DeadCodeDetectorInput<'a> {
1773 graph: &'a ModuleGraph,
1774 config: &'a ResolvedConfig,
1775 resolved_modules: &'a [ResolvedModule],
1776 workspaces: &'a [fallow_config::WorkspaceInfo],
1777 modules: &'a [ModuleInfo],
1778 suppressions: &'a SuppressionContext<'a>,
1779 line_offsets_by_file: &'a LineOffsetsMap<'a>,
1780 plugin_result: Option<&'a crate::plugins::AggregatedPluginResult>,
1781 pkg: Option<&'a PackageJson>,
1782 user_class_members: &'a [fallow_config::UsedClassMemberRule],
1783 public_api_entry_points: &'a FxHashSet<FileId>,
1784 virtual_prefixes: &'a [&'a str],
1785 generated_patterns: &'a [&'a str],
1786 generated_type_prefixes: &'a [&'a str],
1787 declared_deps: &'a FxHashSet<String>,
1788 collect_usages: bool,
1789}
1790
1791struct ParallelDeadCodeDetectorResults {
1792 unused_files: Vec<UnusedFileFinding>,
1793 export_results: AnalysisResults,
1794 member_results: AnalysisResults,
1795 dependency_results: AnalysisResults,
1796 unresolved_imports: Vec<UnresolvedImportFinding>,
1797 duplicate_exports: Vec<DuplicateExportFinding>,
1798 boundary_violations: Vec<BoundaryViolationFinding>,
1799 boundary_coverage_violations: Vec<BoundaryCoverageViolationFinding>,
1800 boundary_call_violations: Vec<BoundaryCallViolationFinding>,
1801 policy_violations: Vec<PolicyViolationFinding>,
1802 circular_dependencies: Vec<CircularDependencyFinding>,
1803 re_export_cycles: Vec<ReExportCycleFinding>,
1804 export_usages: Vec<crate::results::ExportUsage>,
1805}
1806
1807impl ParallelDeadCodeDetectorResults {
1808 fn into_analysis_results(self) -> AnalysisResults {
1809 AnalysisResults {
1810 unused_files: self.unused_files,
1811 unused_exports: self.export_results.unused_exports,
1812 unused_types: self.export_results.unused_types,
1813 private_type_leaks: self.export_results.private_type_leaks,
1814 stale_suppressions: self.export_results.stale_suppressions,
1815 unused_enum_members: self.member_results.unused_enum_members,
1816 unused_class_members: self.member_results.unused_class_members,
1817 unused_store_members: self.member_results.unused_store_members,
1818 unused_dependencies: self.dependency_results.unused_dependencies,
1819 unused_dev_dependencies: self.dependency_results.unused_dev_dependencies,
1820 unused_optional_dependencies: self.dependency_results.unused_optional_dependencies,
1821 unlisted_dependencies: self.dependency_results.unlisted_dependencies,
1822 type_only_dependencies: self.dependency_results.type_only_dependencies,
1823 test_only_dependencies: self.dependency_results.test_only_dependencies,
1824 unresolved_imports: self.unresolved_imports,
1825 duplicate_exports: self.duplicate_exports,
1826 boundary_violations: self.boundary_violations,
1827 boundary_coverage_violations: self.boundary_coverage_violations,
1828 boundary_call_violations: self.boundary_call_violations,
1829 policy_violations: self.policy_violations,
1830 circular_dependencies: self.circular_dependencies,
1831 re_export_cycles: self.re_export_cycles,
1832 export_usages: self.export_usages,
1833 ..AnalysisResults::default()
1834 }
1835 }
1836}
1837
1838fn run_parallel_dead_code_detectors(input: DeadCodeDetectorInput<'_>) -> AnalysisResults {
1839 collect_parallel_dead_code_detector_results(input).into_analysis_results()
1840}
1841
1842fn collect_parallel_dead_code_detector_results(
1843 input: DeadCodeDetectorInput<'_>,
1844) -> ParallelDeadCodeDetectorResults {
1845 let (
1846 (unused_files, export_results),
1847 (
1848 (member_results, dependency_results),
1849 (
1850 (unresolved_imports, duplicate_exports),
1851 (
1852 (
1853 boundary_violations,
1854 (
1855 boundary_coverage_violations,
1856 (boundary_call_violations, policy_violations),
1857 ),
1858 ),
1859 (circular_dependencies, (re_export_cycles, export_usages)),
1860 ),
1861 ),
1862 ),
1863 ) = rayon::join(
1864 || run_file_and_export_detectors(input),
1865 || {
1866 rayon::join(
1867 || run_member_and_dependency_detectors(input),
1868 || {
1869 rayon::join(
1870 || run_import_and_duplicate_detectors(input),
1871 || run_boundary_cycle_and_usage_detectors(input),
1872 )
1873 },
1874 )
1875 },
1876 );
1877
1878 ParallelDeadCodeDetectorResults {
1879 unused_files,
1880 export_results,
1881 member_results,
1882 dependency_results,
1883 unresolved_imports,
1884 duplicate_exports,
1885 boundary_violations,
1886 boundary_coverage_violations,
1887 boundary_call_violations,
1888 policy_violations,
1889 circular_dependencies,
1890 re_export_cycles,
1891 export_usages,
1892 }
1893}
1894
1895fn run_file_and_export_detectors(
1896 input: DeadCodeDetectorInput<'_>,
1897) -> (Vec<UnusedFileFinding>, AnalysisResults) {
1898 rayon::join(
1899 || run_unused_file_detector(input.graph, input.config, input.suppressions),
1900 || {
1901 run_export_detectors(
1902 input.graph,
1903 input.modules,
1904 input.config,
1905 input.plugin_result,
1906 input.suppressions,
1907 input.line_offsets_by_file,
1908 )
1909 },
1910 )
1911}
1912
1913fn run_member_and_dependency_detectors(
1914 input: DeadCodeDetectorInput<'_>,
1915) -> (AnalysisResults, AnalysisResults) {
1916 rayon::join(
1917 || {
1918 run_member_detectors(MemberDetectorInput {
1919 graph: input.graph,
1920 resolved_modules: input.resolved_modules,
1921 modules: input.modules,
1922 config: input.config,
1923 suppressions: input.suppressions,
1924 line_offsets_by_file: input.line_offsets_by_file,
1925 user_class_members: input.user_class_members,
1926 public_api_entry_points: input.public_api_entry_points,
1927 declared_deps: input.declared_deps,
1928 })
1929 },
1930 || {
1931 run_dependency_detectors(DependencyDetectorInput {
1932 graph: input.graph,
1933 pkg: input.pkg,
1934 config: input.config,
1935 plugin_result: input.plugin_result,
1936 workspaces: input.workspaces,
1937 resolved_modules: input.resolved_modules,
1938 line_offsets_by_file: input.line_offsets_by_file,
1939 })
1940 },
1941 )
1942}
1943
1944fn run_import_and_duplicate_detectors(
1945 input: DeadCodeDetectorInput<'_>,
1946) -> (Vec<UnresolvedImportFinding>, Vec<DuplicateExportFinding>) {
1947 rayon::join(
1948 || {
1949 run_unresolved_import_detector(UnresolvedImportDetectorInput {
1950 resolved_modules: input.resolved_modules,
1951 config: input.config,
1952 suppressions: input.suppressions,
1953 virtual_prefixes: input.virtual_prefixes,
1954 generated_patterns: input.generated_patterns,
1955 generated_type_prefixes: input.generated_type_prefixes,
1956 line_offsets_by_file: input.line_offsets_by_file,
1957 })
1958 },
1959 || {
1960 run_duplicate_export_detector(
1961 input.graph,
1962 input.config,
1963 input.suppressions,
1964 input.line_offsets_by_file,
1965 input.plugin_result,
1966 input.resolved_modules,
1967 )
1968 },
1969 )
1970}
1971
1972type BoundaryAuxResults = (
1973 Vec<BoundaryCoverageViolationFinding>,
1974 (
1975 Vec<BoundaryCallViolationFinding>,
1976 Vec<PolicyViolationFinding>,
1977 ),
1978);
1979
1980type BoundaryCycleUsageResults = (
1981 (Vec<BoundaryViolationFinding>, BoundaryAuxResults),
1982 (
1983 Vec<CircularDependencyFinding>,
1984 (Vec<ReExportCycleFinding>, Vec<crate::results::ExportUsage>),
1985 ),
1986);
1987
1988fn run_boundary_cycle_and_usage_detectors(
1989 input: DeadCodeDetectorInput<'_>,
1990) -> BoundaryCycleUsageResults {
1991 rayon::join(
1992 || run_boundary_detectors(input),
1993 || run_cycle_and_usage_detectors(input),
1994 )
1995}
1996
1997fn run_boundary_detectors(
1998 input: DeadCodeDetectorInput<'_>,
1999) -> (Vec<BoundaryViolationFinding>, BoundaryAuxResults) {
2000 rayon::join(
2001 || {
2002 run_boundary_violation_detector(
2003 input.graph,
2004 input.config,
2005 input.suppressions,
2006 input.line_offsets_by_file,
2007 )
2008 },
2009 || {
2010 run_boundary_aux_detectors(
2011 input.graph,
2012 input.modules,
2013 input.config,
2014 input.declared_deps,
2015 input.suppressions,
2016 input.line_offsets_by_file,
2017 )
2018 },
2019 )
2020}
2021
2022fn run_cycle_and_usage_detectors(
2023 input: DeadCodeDetectorInput<'_>,
2024) -> (
2025 Vec<CircularDependencyFinding>,
2026 (Vec<ReExportCycleFinding>, Vec<crate::results::ExportUsage>),
2027) {
2028 rayon::join(
2029 || {
2030 run_circular_dep_detector(
2031 input.graph,
2032 input.config,
2033 input.line_offsets_by_file,
2034 input.suppressions,
2035 input.workspaces,
2036 )
2037 },
2038 || {
2039 rayon::join(
2040 || run_re_export_cycle_detector(input.graph, input.config, input.suppressions),
2041 || {
2042 run_export_usages_collector(
2043 input.graph,
2044 input.line_offsets_by_file,
2045 input.collect_usages,
2046 )
2047 },
2048 )
2049 },
2050 )
2051}
2052
2053#[expect(
2054 deprecated,
2055 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
2056)]
2057fn run_duplicate_export_detector(
2058 graph: &ModuleGraph,
2059 config: &ResolvedConfig,
2060 suppressions: &SuppressionContext<'_>,
2061 line_offsets_by_file: &LineOffsetsMap<'_>,
2062 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
2063 resolved_modules: &[ResolvedModule],
2064) -> Vec<DuplicateExportFinding> {
2065 if config.rules.duplicate_exports == Severity::Off {
2066 return Vec::new();
2067 }
2068 let duplicate_exports = if let Some(plugin_result) = plugin_result {
2069 unused_exports::find_duplicate_exports_with_plugins(
2070 graph,
2071 config,
2072 suppressions,
2073 line_offsets_by_file,
2074 Some(plugin_result),
2075 resolved_modules,
2076 )
2077 } else {
2078 unused_exports::find_duplicate_exports(
2079 graph,
2080 config,
2081 suppressions,
2082 line_offsets_by_file,
2083 resolved_modules,
2084 )
2085 };
2086 duplicate_exports
2087 .into_iter()
2088 .map(DuplicateExportFinding::with_actions)
2089 .collect()
2090}
2091
2092#[expect(
2093 deprecated,
2094 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
2095)]
2096fn run_boundary_violation_detector(
2097 graph: &ModuleGraph,
2098 config: &ResolvedConfig,
2099 suppressions: &SuppressionContext<'_>,
2100 line_offsets_by_file: &LineOffsetsMap<'_>,
2101) -> Vec<BoundaryViolationFinding> {
2102 if config.rules.boundary_violation == Severity::Off || config.boundaries.is_empty() {
2103 return Vec::new();
2104 }
2105 boundary::find_boundary_violations(graph, config, suppressions, line_offsets_by_file)
2106 .into_iter()
2107 .map(BoundaryViolationFinding::with_actions)
2108 .collect()
2109}
2110
2111fn filter_public_workspace_results(
2112 config: &ResolvedConfig,
2113 workspaces: &[fallow_config::WorkspaceInfo],
2114 results: &mut AnalysisResults,
2115) {
2116 let public_roots = public_workspace_roots(&config.public_packages, workspaces);
2117 if public_roots.is_empty() {
2118 return;
2119 }
2120 results.unused_exports.retain(|e| {
2121 !public_roots
2122 .iter()
2123 .any(|root| e.export.path.starts_with(root))
2124 });
2125 results.unused_types.retain(|e| {
2126 !public_roots
2127 .iter()
2128 .any(|root| e.export.path.starts_with(root))
2129 });
2130 results.unused_enum_members.retain(|e| {
2131 !public_roots
2132 .iter()
2133 .any(|root| e.member.path.starts_with(root))
2134 });
2135 results.unused_class_members.retain(|e| {
2136 !public_roots
2137 .iter()
2138 .any(|root| e.member.path.starts_with(root))
2139 });
2140}
2141
2142#[expect(
2143 deprecated,
2144 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
2145)]
2146fn populate_pnpm_catalog_findings(
2147 config: &ResolvedConfig,
2148 workspaces: &[fallow_config::WorkspaceInfo],
2149 results: &mut AnalysisResults,
2150) {
2151 let need_unused = config.rules.unused_catalog_entries != Severity::Off;
2152 let need_empty_groups = config.rules.empty_catalog_groups != Severity::Off;
2153 let need_unresolved_refs = config.rules.unresolved_catalog_references != Severity::Off;
2154 let Some(state) = ((need_unused || need_empty_groups || need_unresolved_refs)
2155 .then(|| gather_pnpm_catalog_state(config, workspaces)))
2156 .flatten() else {
2157 return;
2158 };
2159
2160 if need_unused {
2161 results.unused_catalog_entries = find_unused_catalog_entries(&state)
2162 .into_iter()
2163 .map(UnusedCatalogEntryFinding::with_actions)
2164 .collect();
2165 }
2166 if need_empty_groups {
2167 results.empty_catalog_groups = find_empty_catalog_groups(&state)
2168 .into_iter()
2169 .map(EmptyCatalogGroupFinding::with_actions)
2170 .collect();
2171 }
2172 if need_unresolved_refs {
2173 results.unresolved_catalog_references = find_unresolved_catalog_references(
2174 &state,
2175 &config.compiled_ignore_catalog_references,
2176 &config.root,
2177 )
2178 .into_iter()
2179 .map(UnresolvedCatalogReferenceFinding::with_actions)
2180 .collect();
2181 }
2182}
2183
2184#[expect(
2185 deprecated,
2186 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
2187)]
2188fn populate_pnpm_override_findings(
2189 config: &ResolvedConfig,
2190 workspaces: &[fallow_config::WorkspaceInfo],
2191 results: &mut AnalysisResults,
2192) {
2193 let need_unused = config.rules.unused_dependency_overrides != Severity::Off;
2194 let need_misconfigured = config.rules.misconfigured_dependency_overrides != Severity::Off;
2195 let Some(state) = ((need_unused || need_misconfigured)
2196 .then(|| gather_pnpm_override_state(config, workspaces)))
2197 .flatten() else {
2198 return;
2199 };
2200
2201 if need_unused {
2202 results.unused_dependency_overrides = find_unused_dependency_overrides(&state, config)
2203 .into_iter()
2204 .map(UnusedDependencyOverrideFinding::with_actions)
2205 .collect();
2206 }
2207 if need_misconfigured {
2208 results.misconfigured_dependency_overrides =
2209 find_misconfigured_dependency_overrides(&state, config)
2210 .into_iter()
2211 .map(MisconfiguredDependencyOverrideFinding::with_actions)
2212 .collect();
2213 }
2214}
2215
2216fn populate_security_findings(
2217 ctx: &SecurityDetectionContext<'_, '_>,
2218 results: &mut AnalysisResults,
2219) {
2220 if ctx.config.rules.security_client_server_leak != Severity::Off {
2221 let (security_findings, stats) = security::find_security_findings(
2222 ctx.graph,
2223 ctx.modules,
2224 ctx.suppressions,
2225 ctx.line_offsets_by_file,
2226 );
2227 results.security_findings = security_findings;
2228 results.security_unresolved_edge_files = stats.client_files_with_unresolved_edges;
2229 }
2230
2231 if ctx.config.rules.security_sink != Severity::Off {
2232 populate_tainted_sink_findings(ctx, results);
2233 }
2234
2235 if !results.security_findings.is_empty() {
2236 annotate_security_findings(ctx, results);
2237 }
2238}
2239
2240fn populate_tainted_sink_findings(
2241 ctx: &SecurityDetectionContext<'_, '_>,
2242 results: &mut AnalysisResults,
2243) {
2244 let categories = ctx.config.security.categories.as_ref();
2245 let filter = security::CategoryFilter::new(
2246 categories.and_then(|c| c.include.clone()),
2247 categories.and_then(|c| c.exclude.clone()),
2248 );
2249 let (sink_findings, sink_stats) = security::find_tainted_sinks(
2250 ctx.graph,
2251 ctx.modules,
2252 ctx.suppressions,
2253 ctx.line_offsets_by_file,
2254 ctx.declared_deps,
2255 &security::TaintedSinkContext {
2256 category_filter: &filter,
2257 request_receivers: ctx.request_receivers,
2258 root: &ctx.config.root,
2259 },
2260 );
2261 results.security_findings.extend(sink_findings);
2262 results.security_unresolved_callee_sites = sink_stats.sinks_skipped_dynamic_callee;
2263 results.security_unresolved_callee_diagnostics = sink_stats.unresolved_callee_diagnostics;
2264 results
2265 .security_findings
2266 .extend(security::find_hardcoded_secret_candidates(
2267 ctx.graph,
2268 ctx.modules,
2269 ctx.suppressions,
2270 ctx.line_offsets_by_file,
2271 &filter,
2272 &ctx.config.root,
2273 ));
2274}
2275
2276fn annotate_security_findings(
2277 ctx: &SecurityDetectionContext<'_, '_>,
2278 results: &mut AnalysisResults,
2279) {
2280 security::annotate_dead_code_cross_links(
2281 ctx.graph,
2282 ctx.modules,
2283 ctx.line_offsets_by_file,
2284 &results.unused_files,
2285 &results.unused_exports,
2286 &mut results.security_findings,
2287 );
2288 let boundary_crossings = boundary_crossings_by_file(&results.boundary_violations);
2289 security::rank_security_findings(
2290 &security::SecurityRankingInput {
2291 graph: ctx.graph,
2292 modules: ctx.modules,
2293 line_offsets_by_file: ctx.line_offsets_by_file,
2294 declared_deps: ctx.declared_deps,
2295 request_receivers: ctx.request_receivers,
2296 boundary_crossings: &boundary_crossings,
2297 },
2298 &mut results.security_findings,
2299 );
2300}
2301
2302fn boundary_crossings_by_file(
2303 boundary_violations: &[BoundaryViolationFinding],
2304) -> FxHashMap<std::path::PathBuf, (String, String)> {
2305 let mut boundary_crossings: FxHashMap<std::path::PathBuf, (String, String)> =
2306 FxHashMap::default();
2307 for violation in boundary_violations {
2308 let zones = (
2309 violation.violation.from_zone.clone(),
2310 violation.violation.to_zone.clone(),
2311 );
2312 for path in [
2313 violation.violation.from_path.clone(),
2314 violation.violation.to_path.clone(),
2315 ] {
2316 boundary_crossings
2317 .entry(path)
2318 .and_modify(|existing| {
2319 if zones < *existing {
2320 *existing = zones.clone();
2321 }
2322 })
2323 .or_insert_with(|| zones.clone());
2324 }
2325 }
2326 boundary_crossings
2327}
2328
2329#[expect(
2330 deprecated,
2331 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
2332)]
2333fn run_unused_file_detector(
2334 graph: &ModuleGraph,
2335 config: &ResolvedConfig,
2336 suppressions: &crate::suppress::SuppressionContext<'_>,
2337) -> Vec<UnusedFileFinding> {
2338 if config.rules.unused_files == Severity::Off {
2339 return Vec::new();
2340 }
2341 find_unused_files(graph, suppressions)
2342 .into_iter()
2343 .map(UnusedFileFinding::with_actions)
2344 .collect()
2345}
2346
2347#[expect(
2348 deprecated,
2349 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
2350)]
2351fn run_export_detectors(
2352 graph: &ModuleGraph,
2353 modules: &[ModuleInfo],
2354 config: &ResolvedConfig,
2355 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
2356 suppressions: &crate::suppress::SuppressionContext<'_>,
2357 line_offsets_by_file: &LineOffsetsMap<'_>,
2358) -> AnalysisResults {
2359 let mut results = AnalysisResults::default();
2360 if export_rules_are_disabled(config) {
2361 return results;
2362 }
2363
2364 let (exports, types, stale_expected) = find_unused_exports(
2365 graph,
2366 modules,
2367 config,
2368 plugin_result,
2369 suppressions,
2370 line_offsets_by_file,
2371 );
2372 populate_unused_export_findings(&mut results, config, exports);
2373 populate_unused_type_findings(&mut results, config, graph, modules, types);
2374 populate_private_type_leak_findings(
2375 &mut results,
2376 graph,
2377 modules,
2378 config,
2379 suppressions,
2380 line_offsets_by_file,
2381 );
2382 populate_expected_stale_suppressions(&mut results, config, stale_expected);
2383 results
2384}
2385
2386fn export_rules_are_disabled(config: &ResolvedConfig) -> bool {
2387 config.rules.unused_exports == Severity::Off
2388 && config.rules.unused_types == Severity::Off
2389 && config.rules.private_type_leaks == Severity::Off
2390}
2391
2392fn populate_unused_export_findings(
2393 results: &mut AnalysisResults,
2394 config: &ResolvedConfig,
2395 exports: Vec<UnusedExport>,
2396) {
2397 if config.rules.unused_exports == Severity::Off {
2398 return;
2399 }
2400 results.unused_exports = exports
2401 .into_iter()
2402 .map(UnusedExportFinding::with_actions)
2403 .collect();
2404}
2405
2406fn populate_unused_type_findings(
2407 results: &mut AnalysisResults,
2408 config: &ResolvedConfig,
2409 graph: &ModuleGraph,
2410 modules: &[ModuleInfo],
2411 types: Vec<UnusedExport>,
2412) {
2413 if config.rules.unused_types == Severity::Off {
2414 return;
2415 }
2416 let mut typed = types;
2417 suppress_signature_backing_types(&mut typed, graph, modules);
2418 results.unused_types = typed
2419 .into_iter()
2420 .map(UnusedTypeFinding::with_actions)
2421 .collect();
2422}
2423
2424fn populate_private_type_leak_findings(
2425 results: &mut AnalysisResults,
2426 graph: &ModuleGraph,
2427 modules: &[ModuleInfo],
2428 config: &ResolvedConfig,
2429 suppressions: &crate::suppress::SuppressionContext<'_>,
2430 line_offsets_by_file: &LineOffsetsMap<'_>,
2431) {
2432 if config.rules.private_type_leaks == Severity::Off {
2433 return;
2434 }
2435 results.private_type_leaks =
2436 find_private_type_leaks(graph, modules, config, suppressions, line_offsets_by_file)
2437 .into_iter()
2438 .map(PrivateTypeLeakFinding::with_actions)
2439 .collect();
2440}
2441
2442fn populate_expected_stale_suppressions(
2443 results: &mut AnalysisResults,
2444 config: &ResolvedConfig,
2445 stale_expected: Vec<StaleSuppression>,
2446) {
2447 if config.rules.stale_suppressions != Severity::Off {
2448 results.stale_suppressions.extend(stale_expected);
2449 } else if config.rules.require_suppression_reason != Severity::Off {
2450 results
2451 .stale_suppressions
2452 .extend(stale_expected.into_iter().filter(|s| s.missing_reason));
2453 }
2454}
2455
2456#[derive(Clone, Copy)]
2457struct MemberDetectorInput<'a> {
2458 graph: &'a ModuleGraph,
2459 resolved_modules: &'a [ResolvedModule],
2460 modules: &'a [ModuleInfo],
2461 config: &'a ResolvedConfig,
2462 suppressions: &'a crate::suppress::SuppressionContext<'a>,
2463 line_offsets_by_file: &'a LineOffsetsMap<'a>,
2464 user_class_members: &'a [fallow_config::UsedClassMemberRule],
2465 public_api_entry_points: &'a FxHashSet<FileId>,
2466 declared_deps: &'a FxHashSet<String>,
2467}
2468
2469fn run_member_detectors(input: MemberDetectorInput<'_>) -> AnalysisResults {
2470 let mut results = AnalysisResults::default();
2471 let store_members_active = store_member_rule_is_active(input.config, input.declared_deps);
2472 if member_rules_are_disabled(input.config, store_members_active) {
2473 return results;
2474 }
2475
2476 let member_results = find_unused_members_with_public_api_entry_points(UnusedMemberScanInput {
2477 graph: input.graph,
2478 resolved_modules: input.resolved_modules,
2479 modules: input.modules,
2480 suppressions: input.suppressions,
2481 line_offsets_by_file: input.line_offsets_by_file,
2482 user_class_member_allowlist: input.user_class_members,
2483 ignore_decorators: &input.config.ignore_decorators,
2484 public_api_entry_points: input.public_api_entry_points,
2485 lit_active: input.declared_deps.contains("lit")
2486 || input.declared_deps.contains("lit-element")
2487 || input.declared_deps.contains("@lit/reactive-element"),
2488 });
2489 populate_unused_enum_member_findings(&mut results, input.config, member_results.enum_members);
2490 populate_unused_class_member_findings(&mut results, input.config, member_results.class_members);
2491 populate_unused_store_member_findings(
2492 &mut results,
2493 store_members_active,
2494 member_results.store_members,
2495 );
2496 results
2497}
2498
2499fn member_rules_are_disabled(config: &ResolvedConfig, store_members_active: bool) -> bool {
2500 config.rules.unused_enum_members == Severity::Off
2501 && config.rules.unused_class_members == Severity::Off
2502 && !store_members_active
2503}
2504
2505fn store_member_rule_is_active(config: &ResolvedConfig, declared_deps: &FxHashSet<String>) -> bool {
2506 config.rules.unused_store_members != Severity::Off
2511 && (declared_deps.contains("pinia") || declared_deps.contains("@pinia/nuxt"))
2512}
2513
2514fn populate_unused_enum_member_findings(
2515 results: &mut AnalysisResults,
2516 config: &ResolvedConfig,
2517 enum_members: Vec<UnusedMember>,
2518) {
2519 if config.rules.unused_enum_members == Severity::Off {
2520 return;
2521 }
2522 results.unused_enum_members = enum_members
2523 .into_iter()
2524 .map(UnusedEnumMemberFinding::with_actions)
2525 .collect();
2526}
2527
2528fn populate_unused_class_member_findings(
2529 results: &mut AnalysisResults,
2530 config: &ResolvedConfig,
2531 class_members: Vec<UnusedMember>,
2532) {
2533 if config.rules.unused_class_members == Severity::Off {
2534 return;
2535 }
2536 results.unused_class_members = class_members
2537 .into_iter()
2538 .map(UnusedClassMemberFinding::with_actions)
2539 .collect();
2540}
2541
2542fn populate_unused_store_member_findings(
2543 results: &mut AnalysisResults,
2544 store_members_active: bool,
2545 store_members: Vec<UnusedMember>,
2546) {
2547 if !store_members_active {
2548 return;
2549 }
2550 results.unused_store_members = store_members
2551 .into_iter()
2552 .map(UnusedStoreMemberFinding::with_actions)
2553 .collect();
2554}
2555
2556#[derive(Clone, Copy)]
2557struct DependencyDetectorInput<'a> {
2558 graph: &'a ModuleGraph,
2559 pkg: Option<&'a PackageJson>,
2560 config: &'a ResolvedConfig,
2561 plugin_result: Option<&'a crate::plugins::AggregatedPluginResult>,
2562 workspaces: &'a [fallow_config::WorkspaceInfo],
2563 resolved_modules: &'a [ResolvedModule],
2564 line_offsets_by_file: &'a LineOffsetsMap<'a>,
2565}
2566
2567fn run_dependency_detectors(input: DependencyDetectorInput<'_>) -> AnalysisResults {
2568 let mut results = AnalysisResults::default();
2569 let Some(pkg) = input.pkg else {
2570 return results;
2571 };
2572
2573 populate_unused_dependency_findings(input, pkg, &mut results);
2574 populate_unlisted_dependency_findings(input, pkg, &mut results);
2575 populate_type_only_dependency_findings(input, pkg, &mut results);
2576 populate_test_only_dependency_findings(input, pkg, &mut results);
2577 results
2578}
2579
2580fn populate_unlisted_dependency_findings(
2581 input: DependencyDetectorInput<'_>,
2582 pkg: &PackageJson,
2583 results: &mut AnalysisResults,
2584) {
2585 if input.config.rules.unlisted_dependencies != Severity::Off {
2586 results.unlisted_dependencies = find_unlisted_dependencies(UnlistedDependencyInput {
2587 graph: input.graph,
2588 pkg,
2589 config: input.config,
2590 workspaces: input.workspaces,
2591 plugin_result: input.plugin_result,
2592 resolved_modules: input.resolved_modules,
2593 line_offsets_by_file: input.line_offsets_by_file,
2594 })
2595 .into_iter()
2596 .map(UnlistedDependencyFinding::with_actions)
2597 .collect();
2598 }
2599}
2600
2601fn populate_type_only_dependency_findings(
2602 input: DependencyDetectorInput<'_>,
2603 pkg: &PackageJson,
2604 results: &mut AnalysisResults,
2605) {
2606 if input.config.production {
2607 results.type_only_dependencies =
2608 find_type_only_dependencies(input.graph, pkg, input.config, input.workspaces)
2609 .into_iter()
2610 .map(TypeOnlyDependencyFinding::with_actions)
2611 .collect();
2612 }
2613}
2614
2615fn populate_test_only_dependency_findings(
2616 input: DependencyDetectorInput<'_>,
2617 pkg: &PackageJson,
2618 results: &mut AnalysisResults,
2619) {
2620 if !input.config.production && input.config.rules.test_only_dependencies != Severity::Off {
2621 results.test_only_dependencies =
2622 find_test_only_dependencies(input.graph, pkg, input.config, input.workspaces)
2623 .into_iter()
2624 .map(TestOnlyDependencyFinding::with_actions)
2625 .collect();
2626 }
2627}
2628
2629#[expect(
2633 deprecated,
2634 reason = "ADR-008 deprecates detector helpers for external callers; core orchestration still calls them internally"
2635)]
2636fn populate_unused_dependency_findings(
2637 input: DependencyDetectorInput<'_>,
2638 pkg: &PackageJson,
2639 results: &mut AnalysisResults,
2640) {
2641 if unused_dependency_rules_are_disabled(input.config) {
2642 return;
2643 }
2644
2645 let (deps, dev_deps, optional_deps) = find_unused_dependencies(
2646 input.graph,
2647 pkg,
2648 input.config,
2649 input.plugin_result,
2650 input.workspaces,
2651 );
2652 populate_unused_prod_dependency_findings(results, input.config, deps);
2653 populate_unused_dev_dependency_findings(results, input.config, dev_deps);
2654 populate_unused_optional_dependency_findings(results, input.config, optional_deps);
2655}
2656
2657fn unused_dependency_rules_are_disabled(config: &ResolvedConfig) -> bool {
2658 config.rules.unused_dependencies == Severity::Off
2659 && config.rules.unused_dev_dependencies == Severity::Off
2660 && config.rules.unused_optional_dependencies == Severity::Off
2661}
2662
2663fn populate_unused_prod_dependency_findings(
2664 results: &mut AnalysisResults,
2665 config: &ResolvedConfig,
2666 deps: Vec<UnusedDependency>,
2667) {
2668 if config.rules.unused_dependencies == Severity::Off {
2669 return;
2670 }
2671 results.unused_dependencies = deps
2672 .into_iter()
2673 .map(UnusedDependencyFinding::with_actions)
2674 .collect();
2675}
2676
2677fn populate_unused_dev_dependency_findings(
2678 results: &mut AnalysisResults,
2679 config: &ResolvedConfig,
2680 dev_deps: Vec<UnusedDependency>,
2681) {
2682 if config.rules.unused_dev_dependencies == Severity::Off {
2683 return;
2684 }
2685 results.unused_dev_dependencies = dev_deps
2686 .into_iter()
2687 .map(UnusedDevDependencyFinding::with_actions)
2688 .collect();
2689}
2690
2691fn populate_unused_optional_dependency_findings(
2692 results: &mut AnalysisResults,
2693 config: &ResolvedConfig,
2694 optional_deps: Vec<UnusedDependency>,
2695) {
2696 if config.rules.unused_optional_dependencies == Severity::Off {
2697 return;
2698 }
2699 results.unused_optional_dependencies = optional_deps
2700 .into_iter()
2701 .map(UnusedOptionalDependencyFinding::with_actions)
2702 .collect();
2703}
2704
2705#[derive(Clone, Copy)]
2706struct UnresolvedImportDetectorInput<'a> {
2707 resolved_modules: &'a [ResolvedModule],
2708 config: &'a ResolvedConfig,
2709 suppressions: &'a crate::suppress::SuppressionContext<'a>,
2710 virtual_prefixes: &'a [&'a str],
2711 generated_patterns: &'a [&'a str],
2712 generated_type_prefixes: &'a [&'a str],
2713 line_offsets_by_file: &'a LineOffsetsMap<'a>,
2714}
2715
2716fn run_unresolved_import_detector(
2717 input: UnresolvedImportDetectorInput<'_>,
2718) -> Vec<UnresolvedImportFinding> {
2719 if input.config.rules.unresolved_imports == Severity::Off || input.resolved_modules.is_empty() {
2720 return Vec::new();
2721 }
2722 find_unresolved_imports(
2723 input.resolved_modules,
2724 input.config,
2725 input.suppressions,
2726 input.virtual_prefixes,
2727 input.generated_patterns,
2728 input.generated_type_prefixes,
2729 input.line_offsets_by_file,
2730 )
2731 .into_iter()
2732 .map(UnresolvedImportFinding::with_actions)
2733 .collect()
2734}
2735
2736#[cfg(test)]
2737#[expect(
2738 deprecated,
2739 reason = "ADR-008 keeps direct analyzer unit tests while the public warning targets external callers"
2740)]
2741mod tests {
2742 use fallow_types::extract::{byte_offset_to_line_col, compute_line_offsets};
2743
2744 fn line_col(source: &str, byte_offset: u32) -> (u32, u32) {
2745 let offsets = compute_line_offsets(source);
2746 byte_offset_to_line_col(&offsets, byte_offset)
2747 }
2748
2749 #[cfg(unix)]
2756 #[cfg_attr(miri, ignore)]
2757 #[test]
2758 fn scoped_canonical_matches_module_reached_through_symlink() {
2759 use fallow_types::discover::FileId;
2760
2761 let dir = tempfile::tempdir().unwrap();
2762 let real_dir = dir.path().join("real");
2763 std::fs::create_dir(&real_dir).unwrap();
2764 let real_file = real_dir.join("mod.ts");
2765 std::fs::write(&real_file, "export const x = 1;\n").unwrap();
2766 let link_dir = dir.path().join("link");
2769 std::os::unix::fs::symlink(&real_dir, &link_dir).unwrap();
2770
2771 let module_raw_path = link_dir.join("mod.ts");
2772 let canonical_entry = dunce::canonicalize(&real_file).unwrap();
2773 let package_root = dir.path();
2774
2775 let candidates = [(module_raw_path.as_path(), FileId(7))];
2777 assert_eq!(
2778 super::match_canonical_entry_under_package(
2779 candidates.iter().copied(),
2780 package_root,
2781 &canonical_entry,
2782 ),
2783 Some(FileId(7)),
2784 );
2785
2786 let outside_root = dir.path().join("other-package");
2788 assert_eq!(
2789 super::match_canonical_entry_under_package(
2790 candidates.iter().copied(),
2791 &outside_root,
2792 &canonical_entry,
2793 ),
2794 None,
2795 );
2796
2797 let unrelated = dunce::canonicalize(dir.path()).unwrap().join("nope.ts");
2799 assert_eq!(
2800 super::match_canonical_entry_under_package(
2801 candidates.iter().copied(),
2802 package_root,
2803 &unrelated,
2804 ),
2805 None,
2806 );
2807 }
2808
2809 #[test]
2810 fn compute_offsets_empty() {
2811 assert_eq!(compute_line_offsets(""), vec![0]);
2812 }
2813
2814 #[test]
2815 fn compute_offsets_single_line() {
2816 assert_eq!(compute_line_offsets("hello"), vec![0]);
2817 }
2818
2819 #[test]
2820 fn compute_offsets_multiline() {
2821 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
2822 }
2823
2824 #[test]
2825 fn compute_offsets_trailing_newline() {
2826 assert_eq!(compute_line_offsets("abc\n"), vec![0, 4]);
2827 }
2828
2829 #[test]
2830 fn compute_offsets_crlf() {
2831 assert_eq!(compute_line_offsets("ab\r\ncd"), vec![0, 4]);
2832 }
2833
2834 #[test]
2835 fn compute_offsets_consecutive_newlines() {
2836 assert_eq!(compute_line_offsets("\n\n"), vec![0, 1, 2]);
2837 }
2838
2839 #[test]
2840 fn byte_offset_empty_source() {
2841 assert_eq!(line_col("", 0), (1, 0));
2842 }
2843
2844 #[test]
2845 fn byte_offset_single_line_start() {
2846 assert_eq!(line_col("hello", 0), (1, 0));
2847 }
2848
2849 #[test]
2850 fn byte_offset_single_line_middle() {
2851 assert_eq!(line_col("hello", 4), (1, 4));
2852 }
2853
2854 #[test]
2855 fn byte_offset_multiline_start_of_line2() {
2856 assert_eq!(line_col("line1\nline2\nline3", 6), (2, 0));
2857 }
2858
2859 #[test]
2860 fn byte_offset_multiline_middle_of_line3() {
2861 assert_eq!(line_col("line1\nline2\nline3", 14), (3, 2));
2862 }
2863
2864 #[test]
2865 fn byte_offset_at_newline_boundary() {
2866 assert_eq!(line_col("line1\nline2", 5), (1, 5));
2867 }
2868
2869 #[test]
2870 fn byte_offset_multibyte_utf8() {
2871 let source = "hi\n\u{1F600}x";
2872 assert_eq!(line_col(source, 3), (2, 0));
2873 assert_eq!(line_col(source, 7), (2, 4));
2874 }
2875
2876 #[test]
2877 fn byte_offset_multibyte_accented_chars() {
2878 let source = "caf\u{00E9}\nbar";
2879 assert_eq!(line_col(source, 6), (2, 0));
2880 assert_eq!(line_col(source, 3), (1, 3));
2881 }
2882
2883 #[test]
2884 fn byte_offset_via_map_fallback() {
2885 use super::*;
2886 let map: LineOffsetsMap<'_> = FxHashMap::default();
2887 assert_eq!(
2888 super::byte_offset_to_line_col(&map, FileId(99), 42),
2889 (1, 42)
2890 );
2891 }
2892
2893 #[test]
2894 fn byte_offset_via_map_lookup() {
2895 use super::*;
2896 let offsets = compute_line_offsets("abc\ndef\nghi");
2897 let mut map: LineOffsetsMap<'_> = FxHashMap::default();
2898 map.insert(FileId(0), &offsets);
2899 assert_eq!(super::byte_offset_to_line_col(&map, FileId(0), 5), (2, 1));
2900 }
2901
2902 mod orchestration {
2903 use super::super::*;
2904 use fallow_config::{FallowConfig, OutputFormat, RulesConfig, Severity};
2905 use std::path::PathBuf;
2906
2907 fn find_dead_code(graph: &ModuleGraph, config: &ResolvedConfig) -> AnalysisResults {
2908 find_dead_code_full(graph, config, &[], None, &[], &[], false)
2909 }
2910
2911 fn make_config_with_rules(rules: RulesConfig) -> ResolvedConfig {
2912 FallowConfig {
2913 rules,
2914 ..Default::default()
2915 }
2916 .resolve(
2917 PathBuf::from("/tmp/orchestration-test"),
2918 OutputFormat::Human,
2919 1,
2920 true,
2921 true,
2922 None,
2923 )
2924 }
2925
2926 #[test]
2927 fn find_dead_code_all_rules_off_returns_empty() {
2928 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
2929 use crate::graph::ModuleGraph;
2930 use crate::resolve::ResolvedModule;
2931 use rustc_hash::FxHashSet;
2932
2933 let files = vec![DiscoveredFile {
2934 id: FileId(0),
2935 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
2936 size_bytes: 100,
2937 }];
2938 let entry_points = vec![EntryPoint {
2939 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
2940 source: EntryPointSource::ManualEntry,
2941 }];
2942 let resolved = vec![ResolvedModule {
2943 file_id: FileId(0),
2944 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
2945 exports: vec![],
2946 re_exports: vec![],
2947 resolved_imports: vec![],
2948 resolved_dynamic_imports: vec![],
2949 resolved_dynamic_patterns: vec![],
2950 member_accesses: vec![],
2951 semantic_facts: Box::default(),
2952 whole_object_uses: Box::default(),
2953 has_cjs_exports: false,
2954 has_angular_component_template_url: false,
2955 unused_import_bindings: FxHashSet::default(),
2956 type_referenced_import_bindings: vec![],
2957 value_referenced_import_bindings: vec![],
2958 namespace_object_aliases: vec![],
2959 exported_factory_returns: Box::default(),
2960 }];
2961 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
2962
2963 let rules = RulesConfig {
2964 unused_files: Severity::Off,
2965 unused_exports: Severity::Off,
2966 unused_types: Severity::Off,
2967 private_type_leaks: Severity::Off,
2968 unused_dependencies: Severity::Off,
2969 unused_dev_dependencies: Severity::Off,
2970 unused_optional_dependencies: Severity::Off,
2971 unused_enum_members: Severity::Off,
2972 unused_class_members: Severity::Off,
2973 unused_store_members: Severity::Off,
2974 unprovided_injects: Severity::Off,
2975 unrendered_components: Severity::Off,
2976 unused_component_props: Severity::Off,
2977 unused_component_emits: Severity::Off,
2978 unused_component_inputs: Severity::Off,
2979 unused_component_outputs: Severity::Off,
2980 unused_svelte_events: Severity::Off,
2981 unused_server_actions: Severity::Off,
2982 unused_load_data_keys: Severity::Off,
2983 prop_drilling: Severity::Off,
2984 thin_wrapper: Severity::Off,
2985 duplicate_prop_shape: Severity::Off,
2986 unresolved_imports: Severity::Off,
2987 unlisted_dependencies: Severity::Off,
2988 duplicate_exports: Severity::Off,
2989 type_only_dependencies: Severity::Off,
2990 circular_dependencies: Severity::Off,
2991 re_export_cycle: Severity::Off,
2992 test_only_dependencies: Severity::Off,
2993 boundary_violation: Severity::Off,
2994 coverage_gaps: Severity::Off,
2995 feature_flags: Severity::Off,
2996 stale_suppressions: Severity::Off,
2997 require_suppression_reason: Severity::Off,
2998 unused_catalog_entries: Severity::Off,
2999 empty_catalog_groups: Severity::Off,
3000 unresolved_catalog_references: Severity::Off,
3001 unused_dependency_overrides: Severity::Off,
3002 misconfigured_dependency_overrides: Severity::Off,
3003 security_client_server_leak: Severity::Off,
3004 security_sink: Severity::Off,
3005 policy_violation: Severity::Off,
3006 invalid_client_export: Severity::Off,
3007 mixed_client_server_barrel: Severity::Off,
3008 misplaced_directive: Severity::Off,
3009 route_collision: Severity::Off,
3010 dynamic_segment_name_conflict: Severity::Off,
3011 };
3012 let config = make_config_with_rules(rules);
3013 let results = find_dead_code(&graph, &config);
3014
3015 assert!(results.unused_files.is_empty());
3016 assert!(results.unused_exports.is_empty());
3017 assert!(results.unused_types.is_empty());
3018 assert!(results.unused_dependencies.is_empty());
3019 assert!(results.unused_dev_dependencies.is_empty());
3020 assert!(results.unused_optional_dependencies.is_empty());
3021 assert!(results.unused_enum_members.is_empty());
3022 assert!(results.unused_class_members.is_empty());
3023 assert!(results.unresolved_imports.is_empty());
3024 assert!(results.unlisted_dependencies.is_empty());
3025 assert!(results.duplicate_exports.is_empty());
3026 assert!(results.circular_dependencies.is_empty());
3027 assert!(results.export_usages.is_empty());
3028 }
3029
3030 #[test]
3031 fn find_dead_code_full_collect_usages_flag() {
3032 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
3033 use crate::extract::{ExportName, VisibilityTag};
3034 use crate::graph::{ExportSymbol, ModuleGraph};
3035 use crate::resolve::ResolvedModule;
3036 use oxc_span::Span;
3037 use rustc_hash::FxHashSet;
3038
3039 let files = vec![DiscoveredFile {
3040 id: FileId(0),
3041 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3042 size_bytes: 100,
3043 }];
3044 let entry_points = vec![EntryPoint {
3045 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3046 source: EntryPointSource::ManualEntry,
3047 }];
3048 let resolved = vec![ResolvedModule {
3049 file_id: FileId(0),
3050 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3051 exports: vec![],
3052 re_exports: vec![],
3053 resolved_imports: vec![],
3054 resolved_dynamic_imports: vec![],
3055 resolved_dynamic_patterns: vec![],
3056 member_accesses: vec![],
3057 semantic_facts: Box::default(),
3058 whole_object_uses: Box::default(),
3059 has_cjs_exports: false,
3060 has_angular_component_template_url: false,
3061 unused_import_bindings: FxHashSet::default(),
3062 type_referenced_import_bindings: vec![],
3063 value_referenced_import_bindings: vec![],
3064 namespace_object_aliases: vec![],
3065 exported_factory_returns: Box::default(),
3066 }];
3067 let mut graph = ModuleGraph::build(&resolved, &entry_points, &files);
3068 graph.modules[0].exports = vec![ExportSymbol {
3069 name: ExportName::Named("myExport".to_string()),
3070 is_type_only: false,
3071 is_side_effect_used: false,
3072 visibility: VisibilityTag::None,
3073 expected_unused_reason: None,
3074 span: Span::new(10, 30),
3075 references: vec![],
3076 members: vec![],
3077 }];
3078
3079 let rules = RulesConfig::default();
3080 let config = make_config_with_rules(rules);
3081
3082 let results_no_collect = find_dead_code_full(
3083 &graph,
3084 &config,
3085 &[],
3086 None,
3087 &[],
3088 &[],
3089 false, );
3091 assert!(
3092 results_no_collect.export_usages.is_empty(),
3093 "export_usages should be empty when collect_usages is false"
3094 );
3095
3096 let results_with_collect = find_dead_code_full(
3097 &graph,
3098 &config,
3099 &[],
3100 None,
3101 &[],
3102 &[],
3103 true, );
3105 assert!(
3106 !results_with_collect.export_usages.is_empty(),
3107 "export_usages should be populated when collect_usages is true"
3108 );
3109 assert_eq!(
3110 results_with_collect.export_usages[0].export_name,
3111 "myExport"
3112 );
3113 }
3114
3115 #[test]
3116 fn find_dead_code_delegates_to_find_dead_code_with_resolved() {
3117 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
3118 use crate::graph::ModuleGraph;
3119 use crate::resolve::ResolvedModule;
3120 use rustc_hash::FxHashSet;
3121
3122 let files = vec![DiscoveredFile {
3123 id: FileId(0),
3124 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3125 size_bytes: 100,
3126 }];
3127 let entry_points = vec![EntryPoint {
3128 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3129 source: EntryPointSource::ManualEntry,
3130 }];
3131 let resolved = vec![ResolvedModule {
3132 file_id: FileId(0),
3133 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3134 exports: vec![],
3135 re_exports: vec![],
3136 resolved_imports: vec![],
3137 resolved_dynamic_imports: vec![],
3138 resolved_dynamic_patterns: vec![],
3139 member_accesses: vec![],
3140 semantic_facts: Box::default(),
3141 whole_object_uses: Box::default(),
3142 has_cjs_exports: false,
3143 has_angular_component_template_url: false,
3144 unused_import_bindings: FxHashSet::default(),
3145 type_referenced_import_bindings: vec![],
3146 value_referenced_import_bindings: vec![],
3147 namespace_object_aliases: vec![],
3148 exported_factory_returns: Box::default(),
3149 }];
3150 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
3151 let config = make_config_with_rules(RulesConfig::default());
3152
3153 let results = find_dead_code(&graph, &config);
3154 assert!(results.unused_exports.is_empty());
3155 }
3156
3157 #[test]
3158 #[expect(
3159 clippy::too_many_lines,
3160 reason = "test fixture; linear setup/assert, length is not a maintainability concern"
3161 )]
3162 fn suppressions_built_from_modules() {
3163 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
3164 use crate::extract::ModuleInfo;
3165 use crate::graph::ModuleGraph;
3166 use crate::resolve::ResolvedModule;
3167 use crate::suppress::{IssueKind, Suppression};
3168 use rustc_hash::FxHashSet;
3169
3170 let files = vec![
3171 DiscoveredFile {
3172 id: FileId(0),
3173 path: PathBuf::from("/tmp/orchestration-test/src/entry.ts"),
3174 size_bytes: 100,
3175 },
3176 DiscoveredFile {
3177 id: FileId(1),
3178 path: PathBuf::from("/tmp/orchestration-test/src/utils.ts"),
3179 size_bytes: 100,
3180 },
3181 ];
3182 let entry_points = vec![EntryPoint {
3183 path: PathBuf::from("/tmp/orchestration-test/src/entry.ts"),
3184 source: EntryPointSource::ManualEntry,
3185 }];
3186 let resolved = files
3187 .iter()
3188 .map(|f| ResolvedModule {
3189 file_id: f.id,
3190 path: f.path.clone(),
3191 exports: vec![],
3192 re_exports: vec![],
3193 resolved_imports: vec![],
3194 resolved_dynamic_imports: vec![],
3195 resolved_dynamic_patterns: vec![],
3196 member_accesses: vec![],
3197 semantic_facts: Box::default(),
3198 whole_object_uses: Box::default(),
3199 has_cjs_exports: false,
3200 has_angular_component_template_url: false,
3201 unused_import_bindings: FxHashSet::default(),
3202 type_referenced_import_bindings: vec![],
3203 value_referenced_import_bindings: vec![],
3204 namespace_object_aliases: vec![],
3205 exported_factory_returns: Box::default(),
3206 })
3207 .collect::<Vec<_>>();
3208 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
3209
3210 let modules = vec![ModuleInfo {
3211 file_id: FileId(1),
3212 exports: vec![],
3213 imports: vec![],
3214 re_exports: vec![],
3215 dynamic_imports: vec![],
3216 dynamic_import_patterns: vec![],
3217 require_calls: vec![],
3218 package_path_references: Box::default(),
3219 member_accesses: vec![],
3220 semantic_facts: Box::default(),
3221 whole_object_uses: Box::default(),
3222 has_cjs_exports: false,
3223 has_angular_component_template_url: false,
3224 content_hash: 0,
3225 suppressions: vec![Suppression::issue(0, 1, IssueKind::UnusedFile)],
3226 unknown_suppression_kinds: vec![],
3227 unused_import_bindings: vec![],
3228 type_referenced_import_bindings: vec![],
3229 value_referenced_import_bindings: vec![],
3230 line_offsets: vec![],
3231 complexity: vec![],
3232 flag_uses: vec![],
3233 class_heritage: vec![],
3234 exported_factory_returns: Box::default(),
3235 injection_tokens: vec![],
3236 local_type_declarations: Vec::new(),
3237 public_signature_type_references: Vec::new(),
3238 namespace_object_aliases: Vec::new(),
3239 iconify_prefixes: Vec::new(),
3240 iconify_icon_names: Vec::new(),
3241 auto_import_candidates: Vec::new(),
3242 directives: Vec::new(),
3243 client_only_dynamic_import_spans: Vec::new(),
3244 security_sinks: Vec::new(),
3245 security_sinks_skipped: 0,
3246 security_unresolved_callee_sites: Vec::new(),
3247 tainted_bindings: Vec::new(),
3248 sanitized_sink_args: Vec::new(),
3249 security_control_sites: Vec::new(),
3250 callee_uses: Vec::new(),
3251 misplaced_directives: Vec::new(),
3252 inline_server_action_exports: Vec::new(),
3253 di_key_sites: Vec::new(),
3254 has_dynamic_provide: false,
3255 referenced_import_bindings: Vec::new(),
3256 component_props: Vec::new(),
3257 has_props_attrs_fallthrough: false,
3258 has_define_expose: false,
3259 has_define_model: false,
3260 has_unharvestable_props: false,
3261 component_emits: Vec::new(),
3262 angular_inputs: Vec::new(),
3263 angular_outputs: Vec::new(),
3264 has_unharvestable_emits: false,
3265 has_dynamic_emit: false,
3266 has_emit_whole_object_use: false,
3267 load_return_keys: Vec::new(),
3268 has_unharvestable_load: false,
3269 has_load_data_whole_use: false,
3270 has_page_data_store_whole_use: false,
3271 component_functions: Vec::new(),
3272 react_props: Vec::new(),
3273 hook_uses: Vec::new(),
3274 render_edges: Vec::new(),
3275 svelte_dispatched_events: Vec::new(),
3276 svelte_listened_events: Vec::new(),
3277 angular_component_selectors: Vec::new(),
3278 registered_custom_elements: Vec::new(),
3279 used_custom_element_tags: Vec::new(),
3280 angular_used_selectors: Vec::new(),
3281 angular_entry_component_refs: Vec::new(),
3282 has_dynamic_component_render: false,
3283 has_dynamic_dispatch: false,
3284 }];
3285
3286 let rules = RulesConfig {
3287 unused_files: Severity::Error,
3288 ..RulesConfig::default()
3289 };
3290 let config = make_config_with_rules(rules);
3291
3292 let results = find_dead_code_full(&graph, &config, &[], None, &[], &modules, false);
3293
3294 assert!(
3295 !results.unused_files.iter().any(|f| f
3296 .file
3297 .path
3298 .to_string_lossy()
3299 .contains("utils.ts")),
3300 "suppressed file should not appear in unused_files"
3301 );
3302 }
3303 }
3304}