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