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