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