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 members;
10mod misplaced_directive;
11mod mixed_barrel;
12mod package_json_utils;
13mod policy;
14mod predicates;
15mod prop_drilling;
16mod re_export_cycles;
17mod react_intel;
18mod react_resolve;
19mod render_fan_in;
20mod route_collision;
21mod route_tree;
22mod security;
23mod server_only;
24mod thin_wrapper;
25mod unprovided_inject;
26mod unrendered_component;
27mod unused_catalog;
28mod unused_component_emit;
29mod unused_component_input;
30mod unused_component_output;
31mod unused_component_prop;
32mod unused_deps;
33mod unused_exports;
34mod unused_files;
35mod unused_load_data_key;
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 members::{UnusedMemberScanInput, find_unused_members_with_public_api_entry_points};
84use misplaced_directive::find_misplaced_directives;
85use mixed_barrel::find_mixed_client_server_barrels;
86use prop_drilling::find_prop_drilling_chains;
87use re_export_cycles::find_re_export_cycles;
88use react_intel::compute_react_component_intel;
89use render_fan_in::compute_render_fan_in;
90use route_collision::find_route_collisions;
91use thin_wrapper::find_thin_wrappers;
92use unprovided_inject::{UnprovidedInjectInput, find_unprovided_injects};
93use unrendered_component::{
94 LitUnrenderedInput, find_unrendered_angular_components, find_unrendered_components,
95 find_unrendered_lit_elements,
96};
97#[expect(
98 deprecated,
99 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
100)]
101use unused_catalog::{
102 find_empty_catalog_groups, find_unresolved_catalog_references, find_unused_catalog_entries,
103 gather_pnpm_catalog_state,
104};
105use unused_component_emit::find_unused_component_emits;
106use unused_component_input::find_unused_component_inputs;
107use unused_component_output::find_unused_component_outputs;
108use unused_component_prop::{find_unused_component_props, find_unused_react_props};
109#[expect(
110 deprecated,
111 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
112)]
113use unused_deps::{
114 UnlistedDependencyInput, find_dev_dependencies_in_production, find_test_only_dependencies,
115 find_type_only_dependencies, find_unlisted_dependencies, find_unresolved_imports,
116 find_unused_dependencies,
117};
118#[expect(
119 deprecated,
120 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
121)]
122use unused_exports::{
123 collect_export_usages, find_private_type_leaks, find_unused_exports,
124 suppress_signature_backing_types,
125};
126#[expect(
127 deprecated,
128 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
129)]
130use unused_files::find_unused_files;
131use unused_load_data_key::find_unused_load_data_keys;
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(crate) 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 Some(pkg) = fallow_config::load_dir_package_json(&workspace.root) 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 Some(pkg) = fallow_config::load_dir_package_json(&ws.root) {
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 = fallow_config::load_dir_package_json(&config.root);
771 let public_api_entry_points =
772 public_api_package_entry_points(graph, config, pkg.as_ref(), workspaces);
773 let declared_deps = collect_declared_dependency_names(config, pkg.as_ref(), workspaces);
774
775 DeadCodeRunContext {
776 suppressions,
777 line_offsets_by_file,
778 pkg,
779 public_api_entry_points,
780 declared_deps,
781 }
782}
783
784#[deprecated(
786 since = "2.76.0",
787 note = "fallow_core is internal; use fallow_api::run_dead_code for typed output; serialize with fallow_api::serialize_dead_code_programmatic_json for JSON output. See docs/fallow-core-migration.md."
788)]
789#[expect(
790 clippy::too_many_arguments,
791 reason = "frozen deprecated public API; signature must not change"
792)]
793pub(crate) fn find_dead_code_full(
794 graph: &ModuleGraph,
795 config: &ResolvedConfig,
796 resolved_modules: &[ResolvedModule],
797 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
798 workspaces: &[fallow_config::WorkspaceInfo],
799 modules: &[ModuleInfo],
800 collect_usages: bool,
801) -> AnalysisResults {
802 let _span = tracing::info_span!("find_dead_code").entered();
803
804 let run_context = build_dead_code_run_context(graph, config, workspaces, modules);
805
806 let mut results = run_setup_and_detect(&SetupAndDetectInput {
807 graph,
808 config,
809 resolved_modules,
810 plugin_result,
811 workspaces,
812 modules,
813 suppressions: &run_context.suppressions,
814 line_offsets_by_file: &run_context.line_offsets_by_file,
815 pkg: run_context.pkg.as_ref(),
816 public_api_entry_points: &run_context.public_api_entry_points,
817 declared_deps: &run_context.declared_deps,
818 collect_usages,
819 });
820
821 populate_post_detection_findings(&mut PostDetectionInput {
822 graph,
823 modules,
824 resolved_modules,
825 config,
826 workspaces,
827 declared_deps: &run_context.declared_deps,
828 public_api_entry_points: &run_context.public_api_entry_points,
829 suppressions: &run_context.suppressions,
830 line_offsets_by_file: &run_context.line_offsets_by_file,
831 collect_usages,
832 results: &mut results,
833 });
834
835 results.sort();
836
837 results
838}
839
840struct SetupAndDetectInput<'a, 'm> {
843 graph: &'a ModuleGraph,
844 config: &'a ResolvedConfig,
845 resolved_modules: &'a [ResolvedModule],
846 plugin_result: Option<&'a crate::plugins::AggregatedPluginResult>,
847 workspaces: &'a [fallow_config::WorkspaceInfo],
848 modules: &'a [ModuleInfo],
849 suppressions: &'a SuppressionContext<'m>,
850 line_offsets_by_file: &'a LineOffsetsMap<'m>,
851 pkg: Option<&'a PackageJson>,
852 public_api_entry_points: &'a FxHashSet<FileId>,
853 declared_deps: &'a FxHashSet<String>,
854 collect_usages: bool,
855}
856
857fn run_setup_and_detect(input: &SetupAndDetectInput<'_, '_>) -> AnalysisResults {
862 let iconify_referenced =
863 iconify::collect_iconify_referenced_deps(input.modules, input.pkg, input.workspaces);
864 let augmented_plugin_result;
865 let plugin_result = if iconify_referenced.is_empty() {
866 input.plugin_result
867 } else {
868 let mut owned = input.plugin_result.cloned().unwrap_or_default();
869 owned.referenced_dependencies.extend(iconify_referenced);
870 augmented_plugin_result = owned;
871 Some(&augmented_plugin_result)
872 };
873
874 let mut user_class_members = input.config.used_class_members.clone();
875 let mut semantic_framework_candidates = Vec::new();
876 if let Some(plugin_result) = plugin_result {
877 for rule in &plugin_result.used_class_members {
878 if input.config.type_aware.enabled
879 && plugin_result
880 .framework_class_member_contracts
881 .iter()
882 .any(|contract| framework_contract_covers_rule(contract, rule))
883 {
884 semantic_framework_candidates.push(rule.clone());
885 } else {
886 user_class_members.push(rule.clone());
887 }
888 }
889 }
890
891 let (virtual_prefixes, generated_patterns, generated_type_prefixes) =
892 derive_plugin_string_slices(plugin_result);
893
894 let mut results = run_parallel_dead_code_detectors(DeadCodeDetectorInput {
895 graph: input.graph,
896 config: input.config,
897 resolved_modules: input.resolved_modules,
898 workspaces: input.workspaces,
899 modules: input.modules,
900 suppressions: input.suppressions,
901 line_offsets_by_file: input.line_offsets_by_file,
902 plugin_result,
903 pkg: input.pkg,
904 user_class_members: &user_class_members,
905 semantic_framework_candidates: &semantic_framework_candidates,
906 public_api_entry_points: input.public_api_entry_points,
907 virtual_prefixes: &virtual_prefixes,
908 generated_patterns: &generated_patterns,
909 generated_type_prefixes: &generated_type_prefixes,
910 declared_deps: input.declared_deps,
911 collect_usages: input.collect_usages,
912 });
913 if input.config.type_aware.enabled {
914 results.semantic_framework_contracts = plugin_result.map_or_else(Vec::new, |plugins| {
915 plugins.framework_class_member_contracts.clone()
916 });
917 }
918 results
919}
920
921fn framework_contract_covers_rule(
922 contract: &fallow_types::semantic::SemanticFrameworkContract,
923 rule: &fallow_config::UsedClassMemberRule,
924) -> bool {
925 use fallow_types::semantic::SemanticFrameworkRelation;
926
927 let fallow_config::UsedClassMemberRule::Scoped(rule) = rule else {
928 return false;
929 };
930 let heritage_matches =
931 match contract.relation {
932 SemanticFrameworkRelation::Extends => {
933 rule.implements.is_none()
934 && rule.extends.as_ref().is_some_and(|name| {
935 contract.heritage_names.iter().any(|known| known == name)
936 })
937 }
938 SemanticFrameworkRelation::Implements => {
939 rule.extends.is_none()
940 && rule.implements.as_ref().is_some_and(|name| {
941 contract.heritage_names.iter().any(|known| known == name)
942 })
943 }
944 };
945 heritage_matches
946 && rule
947 .members
948 .iter()
949 .all(|member| contract.members.contains(member))
950}
951
952fn derive_plugin_string_slices(
955 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
956) -> (Vec<&str>, Vec<&str>, Vec<&str>) {
957 let virtual_prefixes = plugin_result
958 .map(|pr| {
959 pr.virtual_module_prefixes
960 .iter()
961 .map(String::as_str)
962 .collect()
963 })
964 .unwrap_or_default();
965 let generated_patterns = plugin_result
966 .map(|pr| {
967 pr.generated_import_patterns
968 .iter()
969 .map(String::as_str)
970 .collect()
971 })
972 .unwrap_or_default();
973 let generated_type_prefixes = plugin_result
974 .map(|pr| {
975 pr.generated_type_import_prefixes
976 .iter()
977 .map(String::as_str)
978 .collect()
979 })
980 .unwrap_or_default();
981 (
982 virtual_prefixes,
983 generated_patterns,
984 generated_type_prefixes,
985 )
986}
987
988struct PostDetectionInput<'a, 'm> {
991 graph: &'a ModuleGraph,
992 modules: &'a [ModuleInfo],
993 resolved_modules: &'a [ResolvedModule],
994 config: &'a ResolvedConfig,
995 workspaces: &'a [fallow_config::WorkspaceInfo],
996 declared_deps: &'a FxHashSet<String>,
997 public_api_entry_points: &'a FxHashSet<FileId>,
998 suppressions: &'a SuppressionContext<'m>,
999 line_offsets_by_file: &'a LineOffsetsMap<'m>,
1000 collect_usages: bool,
1003 results: &'a mut AnalysisResults,
1004}
1005
1006fn populate_post_detection_findings(input: &mut PostDetectionInput<'_, '_>) {
1011 filter_public_workspace_results(input.config, input.workspaces, input.results);
1012
1013 if input.config.rules.unused_server_actions != Severity::Off {
1017 reclassify_unused_server_actions(
1018 input.graph,
1019 input.modules,
1020 input.declared_deps,
1021 input.suppressions,
1022 input.results,
1023 );
1024 }
1025
1026 populate_configured_security_findings(input);
1027 populate_package_and_framework_findings(input);
1028 populate_stale_suppression_findings(input);
1029}
1030
1031fn populate_configured_security_findings(input: &mut PostDetectionInput<'_, '_>) {
1032 let request_receivers = input
1033 .config
1034 .security
1035 .request_receivers
1036 .iter()
1037 .cloned()
1038 .collect::<FxHashSet<_>>();
1039
1040 populate_security_findings(
1041 &SecurityDetectionContext {
1042 graph: input.graph,
1043 modules: input.modules,
1044 config: input.config,
1045 suppressions: input.suppressions,
1046 line_offsets_by_file: input.line_offsets_by_file,
1047 declared_deps: input.declared_deps,
1048 request_receivers: &request_receivers,
1049 },
1050 input.results,
1051 );
1052}
1053
1054fn populate_package_and_framework_findings(input: &mut PostDetectionInput<'_, '_>) {
1055 populate_pnpm_catalog_findings(input.config, input.workspaces, input.results);
1061 populate_pnpm_override_findings(input.config, input.workspaces, input.results);
1062 populate_framework_specific_findings(&mut FrameworkSpecificFindingsInput {
1063 graph: input.graph,
1064 modules: input.modules,
1065 resolved_modules: input.resolved_modules,
1066 config: input.config,
1067 workspaces: input.workspaces,
1068 declared_deps: input.declared_deps,
1069 public_api_entry_points: input.public_api_entry_points,
1070 suppressions: input.suppressions,
1071 line_offsets_by_file: input.line_offsets_by_file,
1072 collect_usages: input.collect_usages,
1073 results: input.results,
1074 });
1075}
1076
1077fn populate_stale_suppression_findings(input: &mut PostDetectionInput<'_, '_>) {
1080 if input.config.rules.stale_suppressions != Severity::Off {
1081 input
1082 .results
1083 .stale_suppressions
1084 .extend(input.suppressions.find_stale(input.graph, input.config));
1085 }
1086 if input.config.rules.require_suppression_reason != Severity::Off {
1087 input
1088 .results
1089 .stale_suppressions
1090 .extend(input.suppressions.find_missing_reasons(input.graph));
1091 }
1092 input.results.suppression_count = input.suppressions.used_count();
1093 input.results.active_suppressions = input.suppressions.all_suppressions(input.graph);
1094}
1095
1096struct FrameworkSpecificFindingsInput<'a> {
1102 graph: &'a ModuleGraph,
1103 modules: &'a [ModuleInfo],
1104 resolved_modules: &'a [ResolvedModule],
1105 config: &'a ResolvedConfig,
1106 workspaces: &'a [fallow_config::WorkspaceInfo],
1107 declared_deps: &'a FxHashSet<String>,
1108 public_api_entry_points: &'a FxHashSet<FileId>,
1109 suppressions: &'a SuppressionContext<'a>,
1110 line_offsets_by_file: &'a LineOffsetsMap<'a>,
1111 collect_usages: bool,
1114 results: &'a mut AnalysisResults,
1115}
1116
1117fn populate_framework_specific_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1118 populate_client_boundary_findings(input);
1119 populate_component_contract_findings(input);
1120 populate_react_health_findings(input);
1121 populate_nextjs_findings(input);
1122}
1123
1124fn populate_client_boundary_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1125 populate_invalid_client_export_findings(input);
1126 populate_mixed_client_server_barrel_findings(input);
1127 populate_misplaced_directive_findings(input);
1128}
1129
1130fn populate_component_contract_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1131 populate_unprovided_inject_findings(input);
1132 populate_unrendered_component_findings(input);
1133 populate_unused_component_prop_findings(input);
1134 populate_unused_component_emit_findings(
1135 input.graph,
1136 input.modules,
1137 input.config,
1138 input.declared_deps,
1139 input.line_offsets_by_file,
1140 input.results,
1141 );
1142 populate_unused_component_input_findings(
1143 input.graph,
1144 input.modules,
1145 input.config,
1146 input.declared_deps,
1147 input.line_offsets_by_file,
1148 input.results,
1149 );
1150 populate_unused_component_output_findings(
1151 input.graph,
1152 input.modules,
1153 input.config,
1154 input.declared_deps,
1155 input.line_offsets_by_file,
1156 input.results,
1157 );
1158 populate_unused_svelte_event_findings(
1159 input.graph,
1160 input.modules,
1161 input.config,
1162 input.declared_deps,
1163 input.line_offsets_by_file,
1164 input.results,
1165 );
1166 populate_unused_load_data_key_findings(input);
1167}
1168
1169fn populate_react_health_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1170 populate_prop_drilling_findings(input);
1171 populate_thin_wrapper_findings(input);
1172 populate_render_fan_in(input);
1173 populate_react_component_intel(input);
1174 populate_duplicate_prop_shape_findings(input);
1175}
1176
1177fn populate_nextjs_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1178 populate_nextjs_route_tree_findings(
1179 input.graph,
1180 input.config,
1181 input.workspaces,
1182 input.declared_deps,
1183 input.suppressions,
1184 input.results,
1185 );
1186}
1187
1188fn populate_render_fan_in(input: &mut FrameworkSpecificFindingsInput<'_>) {
1196 input.results.render_fan_in = compute_render_fan_in(
1197 input.graph,
1198 input.modules,
1199 input.resolved_modules,
1200 input.declared_deps,
1201 &input.config.root,
1202 );
1203}
1204
1205fn populate_react_component_intel(input: &mut FrameworkSpecificFindingsInput<'_>) {
1215 if !input.collect_usages {
1216 return;
1217 }
1218 input.results.react_component_intel = compute_react_component_intel(
1219 input.graph,
1220 input.modules,
1221 input.resolved_modules,
1222 input.declared_deps,
1223 &input.config.root,
1224 input.line_offsets_by_file,
1225 );
1226}
1227
1228fn populate_unused_load_data_key_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1233 if input.config.rules.unused_load_data_keys == Severity::Off {
1234 return;
1235 }
1236 let result = find_unused_load_data_keys(
1237 input.graph,
1238 input.modules,
1239 input.declared_deps,
1240 input.suppressions,
1241 input.line_offsets_by_file,
1242 &input.config.root,
1243 );
1244 if result.global_abstain {
1245 input.results.unused_load_data_keys_global_abstain = true;
1246 tracing::debug!(
1247 "unused-load-data-key: abstained project-wide (a whole-object use of \
1248 page.data / $page.data was seen; any key could be read reflectively)"
1249 );
1250 }
1251 input.results.unused_load_data_keys = result
1252 .findings
1253 .into_iter()
1254 .map(UnusedLoadDataKeyFinding::with_actions)
1255 .collect();
1256}
1257
1258fn populate_invalid_client_export_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1262 if input.config.rules.invalid_client_export == Severity::Off {
1263 return;
1264 }
1265 input.results.invalid_client_exports = find_invalid_client_exports(
1266 input.graph,
1267 input.modules,
1268 input.declared_deps,
1269 input.suppressions,
1270 input.line_offsets_by_file,
1271 )
1272 .into_iter()
1273 .map(InvalidClientExportFinding::with_actions)
1274 .collect();
1275}
1276
1277fn populate_mixed_client_server_barrel_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1281 if input.config.rules.mixed_client_server_barrel == Severity::Off {
1282 return;
1283 }
1284 input.results.mixed_client_server_barrels = find_mixed_client_server_barrels(
1285 input.graph,
1286 input.modules,
1287 input.resolved_modules,
1288 input.declared_deps,
1289 input.suppressions,
1290 input.line_offsets_by_file,
1291 )
1292 .into_iter()
1293 .map(MixedClientServerBarrelFinding::with_actions)
1294 .collect();
1295}
1296
1297fn populate_misplaced_directive_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1301 if input.config.rules.misplaced_directive == Severity::Off {
1302 return;
1303 }
1304 input.results.misplaced_directives = find_misplaced_directives(
1305 input.graph,
1306 input.modules,
1307 input.declared_deps,
1308 input.suppressions,
1309 input.line_offsets_by_file,
1310 )
1311 .into_iter()
1312 .map(MisplacedDirectiveFinding::with_actions)
1313 .collect();
1314}
1315
1316fn populate_unprovided_inject_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1320 if input.config.rules.unprovided_injects == Severity::Off {
1321 return;
1322 }
1323 input.results.unprovided_injects = find_unprovided_injects(UnprovidedInjectInput {
1324 graph: input.graph,
1325 resolved_modules: input.resolved_modules,
1326 modules: input.modules,
1327 declared_deps: input.declared_deps,
1328 public_api_entry_points: input.public_api_entry_points,
1329 suppressions: input.suppressions,
1330 line_offsets_by_file: input.line_offsets_by_file,
1331 })
1332 .into_iter()
1333 .map(UnprovidedInjectFinding::with_actions)
1334 .collect();
1335}
1336
1337fn populate_unrendered_component_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1341 if input.config.rules.unrendered_components == Severity::Off {
1342 return;
1343 }
1344 input.results.unrendered_components = find_unrendered_components(
1345 input.graph,
1346 input.resolved_modules,
1347 input.modules,
1348 input.declared_deps,
1349 input.public_api_entry_points,
1350 input.suppressions,
1351 )
1352 .into_iter()
1353 .map(UnrenderedComponentFinding::with_actions)
1354 .collect();
1355 input.results.unrendered_components.extend(
1360 find_unrendered_angular_components(
1361 input.graph,
1362 input.modules,
1363 input.declared_deps,
1364 input.public_api_entry_points,
1365 input.line_offsets_by_file,
1366 input.suppressions,
1367 )
1368 .into_iter()
1369 .map(UnrenderedComponentFinding::with_actions),
1370 );
1371 input.results.unrendered_components.extend(
1376 find_unrendered_lit_elements(&LitUnrenderedInput {
1377 graph: input.graph,
1378 modules: input.modules,
1379 declared_deps: input.declared_deps,
1380 public_api_entry_points: input.public_api_entry_points,
1381 line_offsets_by_file: input.line_offsets_by_file,
1382 suppressions: input.suppressions,
1383 root: &input.config.root,
1384 })
1385 .into_iter()
1386 .map(UnrenderedComponentFinding::with_actions),
1387 );
1388}
1389
1390fn populate_unused_component_prop_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1394 if input.config.rules.unused_component_props == Severity::Off {
1395 return;
1396 }
1397 let sfc = find_unused_component_props(
1399 input.graph,
1400 input.modules,
1401 input.declared_deps,
1402 input.line_offsets_by_file,
1403 input.config.unused_component_props_ignore.as_ref(),
1404 );
1405 input.results.unused_component_props_exempted += sfc.exempted;
1406 input.results.unused_component_props = sfc
1407 .findings
1408 .into_iter()
1409 .map(UnusedComponentPropFinding::with_actions)
1410 .collect();
1411
1412 append_react_unused_component_prop_findings(input);
1413 retain_unsuppressed_unused_component_prop_findings(input);
1414}
1415
1416fn append_react_unused_component_prop_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1417 let react = find_unused_react_props(
1421 input.graph,
1422 input.modules,
1423 input.declared_deps,
1424 input.line_offsets_by_file,
1425 input.config.unused_component_props_ignore.as_ref(),
1426 );
1427 input.results.unused_component_props_exempted += react.exempted;
1428 if react.components_scanned > 0 {
1429 tracing::info!(
1433 components_scanned = react.components_scanned,
1434 unused_props = react.findings.len(),
1435 "React detected, {} component(s) scanned for unused props",
1436 react.components_scanned
1437 );
1438 }
1439 input.results.unused_component_props.extend(
1440 react
1441 .findings
1442 .into_iter()
1443 .map(UnusedComponentPropFinding::with_actions),
1444 );
1445}
1446
1447fn retain_unsuppressed_unused_component_prop_findings(
1448 input: &mut FrameworkSpecificFindingsInput<'_>,
1449) {
1450 let path_to_id = graph_file_ids_by_path(input.graph);
1456 input.results.unused_component_props.retain(|finding| {
1457 !path_line_is_suppressed(
1458 &path_to_id,
1459 input.suppressions,
1460 finding.prop.path.as_path(),
1461 finding.prop.line,
1462 IssueKind::UnusedComponentProp,
1463 )
1464 });
1465}
1466
1467fn populate_unused_component_emit_findings(
1471 graph: &ModuleGraph,
1472 modules: &[ModuleInfo],
1473 config: &ResolvedConfig,
1474 declared_deps: &FxHashSet<String>,
1475 line_offsets_by_file: &LineOffsetsMap<'_>,
1476 results: &mut AnalysisResults,
1477) {
1478 if config.rules.unused_component_emits == Severity::Off {
1479 return;
1480 }
1481 results.unused_component_emits =
1482 find_unused_component_emits(graph, modules, declared_deps, line_offsets_by_file)
1483 .into_iter()
1484 .map(UnusedComponentEmitFinding::with_actions)
1485 .collect();
1486}
1487
1488fn populate_prop_drilling_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1495 if input.config.rules.prop_drilling == Severity::Off {
1496 return;
1497 }
1498 input.results.prop_drilling_chains = collect_prop_drilling_findings(input);
1499
1500 retain_unsuppressed_prop_drilling_findings(input);
1501}
1502
1503fn collect_prop_drilling_findings(
1504 input: &FrameworkSpecificFindingsInput<'_>,
1505) -> Vec<PropDrillingChainFinding> {
1506 let scan = find_prop_drilling_chains(
1507 input.graph,
1508 input.modules,
1509 input.resolved_modules,
1510 input.declared_deps,
1511 input.line_offsets_by_file,
1512 );
1513 if scan.components_scanned > 0 {
1514 tracing::info!(
1516 components_scanned = scan.components_scanned,
1517 prop_drilling_chains = scan.chains.len(),
1518 "React detected, {} component(s) scanned for prop drilling",
1519 scan.components_scanned
1520 );
1521 }
1522 scan.chains
1523 .into_iter()
1524 .map(PropDrillingChainFinding::with_actions)
1525 .collect()
1526}
1527
1528fn retain_unsuppressed_prop_drilling_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1529 let path_to_id = graph_file_ids_by_path(input.graph);
1535 input.results.prop_drilling_chains.retain(|finding| {
1536 let Some(source) = finding.chain.hops.first() else {
1537 return true;
1538 };
1539 !path_line_is_suppressed(
1540 &path_to_id,
1541 input.suppressions,
1542 source.file.as_path(),
1543 source.line,
1544 IssueKind::PropDrilling,
1545 )
1546 });
1547}
1548
1549fn populate_thin_wrapper_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1555 if input.config.rules.thin_wrapper == Severity::Off {
1556 return;
1557 }
1558 input.results.thin_wrappers = collect_thin_wrapper_findings(input);
1559
1560 retain_unsuppressed_thin_wrapper_findings(input);
1561}
1562
1563fn collect_thin_wrapper_findings(
1564 input: &FrameworkSpecificFindingsInput<'_>,
1565) -> Vec<ThinWrapperFinding> {
1566 let scan = find_thin_wrappers(
1567 input.graph,
1568 input.modules,
1569 input.resolved_modules,
1570 input.declared_deps,
1571 input.line_offsets_by_file,
1572 );
1573 if scan.components_scanned > 0 {
1574 tracing::info!(
1576 components_scanned = scan.components_scanned,
1577 thin_wrappers = scan.wrappers.len(),
1578 "React detected, {} component(s) scanned for thin wrappers",
1579 scan.components_scanned
1580 );
1581 }
1582 scan.wrappers
1583 .into_iter()
1584 .map(ThinWrapperFinding::with_actions)
1585 .collect()
1586}
1587
1588fn retain_unsuppressed_thin_wrapper_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1589 let path_to_id = graph_file_ids_by_path(input.graph);
1595 input.results.thin_wrappers.retain(|finding| {
1596 !path_line_is_suppressed(
1597 &path_to_id,
1598 input.suppressions,
1599 finding.wrapper.file.as_path(),
1600 finding.wrapper.line,
1601 IssueKind::ThinWrapper,
1602 )
1603 });
1604}
1605
1606fn populate_duplicate_prop_shape_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1621 if input.config.rules.duplicate_prop_shape == Severity::Off {
1622 return;
1623 }
1624 let scan = find_duplicate_prop_shapes(
1625 input.graph,
1626 input.modules,
1627 input.declared_deps,
1628 input.line_offsets_by_file,
1629 );
1630 if scan.components_scanned > 0 {
1631 tracing::info!(
1633 components_scanned = scan.components_scanned,
1634 duplicate_prop_shapes = scan.groups.len(),
1635 "React detected, {} component(s) scanned for duplicate prop shapes",
1636 scan.components_scanned
1637 );
1638 }
1639 input.results.duplicate_prop_shapes = scan
1640 .groups
1641 .into_iter()
1642 .map(DuplicatePropShapeFinding::with_actions)
1643 .collect();
1644
1645 let path_to_id = graph_file_ids_by_path(input.graph);
1650 input.results.duplicate_prop_shapes.retain(|finding| {
1651 !path_line_is_suppressed(
1652 &path_to_id,
1653 input.suppressions,
1654 finding.shape.file.as_path(),
1655 finding.shape.line,
1656 IssueKind::DuplicatePropShape,
1657 )
1658 });
1659}
1660
1661fn graph_file_ids_by_path(graph: &ModuleGraph) -> FxHashMap<&std::path::Path, FileId> {
1662 graph
1663 .modules
1664 .iter()
1665 .map(|node| (node.path.as_path(), node.file_id))
1666 .collect()
1667}
1668
1669fn path_line_is_suppressed(
1670 path_to_id: &FxHashMap<&std::path::Path, FileId>,
1671 suppressions: &SuppressionContext<'_>,
1672 path: &std::path::Path,
1673 line: u32,
1674 kind: IssueKind,
1675) -> bool {
1676 let Some(&file_id) = path_to_id.get(path) else {
1677 return false;
1678 };
1679 suppressions.is_suppressed(file_id, line, kind)
1680 || suppressions.is_file_suppressed(file_id, kind)
1681}
1682
1683fn populate_unused_component_input_findings(
1687 graph: &ModuleGraph,
1688 modules: &[ModuleInfo],
1689 config: &ResolvedConfig,
1690 declared_deps: &FxHashSet<String>,
1691 line_offsets_by_file: &LineOffsetsMap<'_>,
1692 results: &mut AnalysisResults,
1693) {
1694 if config.rules.unused_component_inputs == Severity::Off {
1695 return;
1696 }
1697 results.unused_component_inputs =
1698 find_unused_component_inputs(graph, modules, declared_deps, line_offsets_by_file)
1699 .into_iter()
1700 .map(UnusedComponentInputFinding::with_actions)
1701 .collect();
1702}
1703
1704fn populate_unused_component_output_findings(
1708 graph: &ModuleGraph,
1709 modules: &[ModuleInfo],
1710 config: &ResolvedConfig,
1711 declared_deps: &FxHashSet<String>,
1712 line_offsets_by_file: &LineOffsetsMap<'_>,
1713 results: &mut AnalysisResults,
1714) {
1715 if config.rules.unused_component_outputs == Severity::Off {
1716 return;
1717 }
1718 results.unused_component_outputs =
1719 find_unused_component_outputs(graph, modules, declared_deps, line_offsets_by_file)
1720 .into_iter()
1721 .map(UnusedComponentOutputFinding::with_actions)
1722 .collect();
1723}
1724
1725fn populate_unused_svelte_event_findings(
1729 graph: &ModuleGraph,
1730 modules: &[ModuleInfo],
1731 config: &ResolvedConfig,
1732 declared_deps: &FxHashSet<String>,
1733 line_offsets_by_file: &LineOffsetsMap<'_>,
1734 results: &mut AnalysisResults,
1735) {
1736 if config.rules.unused_svelte_events == Severity::Off {
1737 return;
1738 }
1739 results.unused_svelte_events =
1740 find_unused_svelte_events(graph, modules, declared_deps, line_offsets_by_file)
1741 .into_iter()
1742 .map(UnusedSvelteEventFinding::with_actions)
1743 .collect();
1744}
1745
1746fn populate_route_collision_findings(
1749 graph: &ModuleGraph,
1750 config: &ResolvedConfig,
1751 workspaces: &[fallow_config::WorkspaceInfo],
1752 declared_deps: &FxHashSet<String>,
1753 suppressions: &SuppressionContext<'_>,
1754 results: &mut AnalysisResults,
1755) {
1756 if config.rules.route_collision == Severity::Off {
1757 return;
1758 }
1759 results.route_collisions =
1760 find_route_collisions(graph, config, workspaces, declared_deps, suppressions)
1761 .into_iter()
1762 .map(RouteCollisionFinding::with_actions)
1763 .collect();
1764}
1765
1766fn populate_dynamic_segment_name_conflict_findings(
1770 graph: &ModuleGraph,
1771 config: &ResolvedConfig,
1772 workspaces: &[fallow_config::WorkspaceInfo],
1773 declared_deps: &FxHashSet<String>,
1774 suppressions: &SuppressionContext<'_>,
1775 results: &mut AnalysisResults,
1776) {
1777 if config.rules.dynamic_segment_name_conflict == Severity::Off {
1778 return;
1779 }
1780 results.dynamic_segment_name_conflicts =
1781 find_dynamic_segment_name_conflicts(graph, config, workspaces, declared_deps, suppressions)
1782 .into_iter()
1783 .map(DynamicSegmentNameConflictFinding::with_actions)
1784 .collect();
1785}
1786
1787fn populate_nextjs_route_tree_findings(
1792 graph: &ModuleGraph,
1793 config: &ResolvedConfig,
1794 workspaces: &[fallow_config::WorkspaceInfo],
1795 declared_deps: &FxHashSet<String>,
1796 suppressions: &SuppressionContext<'_>,
1797 results: &mut AnalysisResults,
1798) {
1799 populate_route_collision_findings(
1800 graph,
1801 config,
1802 workspaces,
1803 declared_deps,
1804 suppressions,
1805 results,
1806 );
1807 populate_dynamic_segment_name_conflict_findings(
1808 graph,
1809 config,
1810 workspaces,
1811 declared_deps,
1812 suppressions,
1813 results,
1814 );
1815}
1816
1817#[derive(Clone, Copy)]
1818struct DeadCodeDetectorInput<'a> {
1819 graph: &'a ModuleGraph,
1820 config: &'a ResolvedConfig,
1821 resolved_modules: &'a [ResolvedModule],
1822 workspaces: &'a [fallow_config::WorkspaceInfo],
1823 modules: &'a [ModuleInfo],
1824 suppressions: &'a SuppressionContext<'a>,
1825 line_offsets_by_file: &'a LineOffsetsMap<'a>,
1826 plugin_result: Option<&'a crate::plugins::AggregatedPluginResult>,
1827 pkg: Option<&'a PackageJson>,
1828 user_class_members: &'a [fallow_config::UsedClassMemberRule],
1829 semantic_framework_candidates: &'a [fallow_config::UsedClassMemberRule],
1830 public_api_entry_points: &'a FxHashSet<FileId>,
1831 virtual_prefixes: &'a [&'a str],
1832 generated_patterns: &'a [&'a str],
1833 generated_type_prefixes: &'a [&'a str],
1834 declared_deps: &'a FxHashSet<String>,
1835 collect_usages: bool,
1836}
1837
1838struct ParallelDeadCodeDetectorResults {
1839 unused_files: Vec<UnusedFileFinding>,
1840 export_results: AnalysisResults,
1841 member_results: AnalysisResults,
1842 dependency_results: AnalysisResults,
1843 unresolved_imports: Vec<UnresolvedImportFinding>,
1844 duplicate_exports: Vec<DuplicateExportFinding>,
1845 boundary_violations: Vec<BoundaryViolationFinding>,
1846 boundary_coverage_violations: Vec<BoundaryCoverageViolationFinding>,
1847 boundary_call_violations: Vec<BoundaryCallViolationFinding>,
1848 policy_violations: Vec<PolicyViolationFinding>,
1849 circular_dependencies: Vec<CircularDependencyFinding>,
1850 re_export_cycles: Vec<ReExportCycleFinding>,
1851 export_usages: Vec<crate::results::ExportUsage>,
1852}
1853
1854impl ParallelDeadCodeDetectorResults {
1855 fn into_analysis_results(self) -> AnalysisResults {
1856 AnalysisResults {
1857 unused_files: self.unused_files,
1858 unused_exports: self.export_results.unused_exports,
1859 unused_types: self.export_results.unused_types,
1860 private_type_leaks: self.export_results.private_type_leaks,
1861 stale_suppressions: self.export_results.stale_suppressions,
1862 unused_enum_members: self.member_results.unused_enum_members,
1863 unused_class_members: self.member_results.unused_class_members,
1864 unused_store_members: self.member_results.unused_store_members,
1865 unused_dependencies: self.dependency_results.unused_dependencies,
1866 unused_dev_dependencies: self.dependency_results.unused_dev_dependencies,
1867 unused_optional_dependencies: self.dependency_results.unused_optional_dependencies,
1868 unlisted_dependencies: self.dependency_results.unlisted_dependencies,
1869 type_only_dependencies: self.dependency_results.type_only_dependencies,
1870 test_only_dependencies: self.dependency_results.test_only_dependencies,
1871 dev_dependencies_in_production: self.dependency_results.dev_dependencies_in_production,
1872 unresolved_imports: self.unresolved_imports,
1873 duplicate_exports: self.duplicate_exports,
1874 boundary_violations: self.boundary_violations,
1875 boundary_coverage_violations: self.boundary_coverage_violations,
1876 boundary_call_violations: self.boundary_call_violations,
1877 policy_violations: self.policy_violations,
1878 circular_dependencies: self.circular_dependencies,
1879 re_export_cycles: self.re_export_cycles,
1880 export_usages: self.export_usages,
1881 ..AnalysisResults::default()
1882 }
1883 }
1884}
1885
1886fn run_parallel_dead_code_detectors(input: DeadCodeDetectorInput<'_>) -> AnalysisResults {
1887 collect_parallel_dead_code_detector_results(input).into_analysis_results()
1888}
1889
1890fn collect_parallel_dead_code_detector_results(
1891 input: DeadCodeDetectorInput<'_>,
1892) -> ParallelDeadCodeDetectorResults {
1893 let (
1894 (unused_files, export_results),
1895 (
1896 (member_results, dependency_results),
1897 (
1898 (unresolved_imports, duplicate_exports),
1899 (
1900 (
1901 boundary_violations,
1902 (
1903 boundary_coverage_violations,
1904 (boundary_call_violations, policy_violations),
1905 ),
1906 ),
1907 (circular_dependencies, (re_export_cycles, export_usages)),
1908 ),
1909 ),
1910 ),
1911 ) = rayon::join(
1912 || run_file_and_export_detectors(input),
1913 || {
1914 rayon::join(
1915 || run_member_and_dependency_detectors(input),
1916 || {
1917 rayon::join(
1918 || run_import_and_duplicate_detectors(input),
1919 || run_boundary_cycle_and_usage_detectors(input),
1920 )
1921 },
1922 )
1923 },
1924 );
1925
1926 ParallelDeadCodeDetectorResults {
1927 unused_files,
1928 export_results,
1929 member_results,
1930 dependency_results,
1931 unresolved_imports,
1932 duplicate_exports,
1933 boundary_violations,
1934 boundary_coverage_violations,
1935 boundary_call_violations,
1936 policy_violations,
1937 circular_dependencies,
1938 re_export_cycles,
1939 export_usages,
1940 }
1941}
1942
1943fn run_file_and_export_detectors(
1944 input: DeadCodeDetectorInput<'_>,
1945) -> (Vec<UnusedFileFinding>, AnalysisResults) {
1946 rayon::join(
1947 || run_unused_file_detector(input.graph, input.config, input.suppressions),
1948 || {
1949 run_export_detectors(
1950 input.graph,
1951 input.modules,
1952 input.config,
1953 input.plugin_result,
1954 input.suppressions,
1955 input.line_offsets_by_file,
1956 )
1957 },
1958 )
1959}
1960
1961fn run_member_and_dependency_detectors(
1962 input: DeadCodeDetectorInput<'_>,
1963) -> (AnalysisResults, AnalysisResults) {
1964 rayon::join(
1965 || {
1966 run_member_detectors(MemberDetectorInput {
1967 graph: input.graph,
1968 resolved_modules: input.resolved_modules,
1969 modules: input.modules,
1970 config: input.config,
1971 suppressions: input.suppressions,
1972 line_offsets_by_file: input.line_offsets_by_file,
1973 user_class_members: input.user_class_members,
1974 semantic_framework_candidates: input.semantic_framework_candidates,
1975 public_api_entry_points: input.public_api_entry_points,
1976 declared_deps: input.declared_deps,
1977 })
1978 },
1979 || {
1980 run_dependency_detectors(DependencyDetectorInput {
1981 graph: input.graph,
1982 pkg: input.pkg,
1983 config: input.config,
1984 plugin_result: input.plugin_result,
1985 workspaces: input.workspaces,
1986 resolved_modules: input.resolved_modules,
1987 line_offsets_by_file: input.line_offsets_by_file,
1988 })
1989 },
1990 )
1991}
1992
1993fn run_import_and_duplicate_detectors(
1994 input: DeadCodeDetectorInput<'_>,
1995) -> (Vec<UnresolvedImportFinding>, Vec<DuplicateExportFinding>) {
1996 rayon::join(
1997 || {
1998 run_unresolved_import_detector(UnresolvedImportDetectorInput {
1999 resolved_modules: input.resolved_modules,
2000 config: input.config,
2001 suppressions: input.suppressions,
2002 virtual_prefixes: input.virtual_prefixes,
2003 generated_patterns: input.generated_patterns,
2004 generated_type_prefixes: input.generated_type_prefixes,
2005 line_offsets_by_file: input.line_offsets_by_file,
2006 })
2007 },
2008 || {
2009 run_duplicate_export_detector(
2010 input.graph,
2011 input.config,
2012 input.suppressions,
2013 input.line_offsets_by_file,
2014 input.plugin_result,
2015 input.resolved_modules,
2016 )
2017 },
2018 )
2019}
2020
2021type BoundaryAuxResults = (
2022 Vec<BoundaryCoverageViolationFinding>,
2023 (
2024 Vec<BoundaryCallViolationFinding>,
2025 Vec<PolicyViolationFinding>,
2026 ),
2027);
2028
2029type BoundaryCycleUsageResults = (
2030 (Vec<BoundaryViolationFinding>, BoundaryAuxResults),
2031 (
2032 Vec<CircularDependencyFinding>,
2033 (Vec<ReExportCycleFinding>, Vec<crate::results::ExportUsage>),
2034 ),
2035);
2036
2037fn run_boundary_cycle_and_usage_detectors(
2038 input: DeadCodeDetectorInput<'_>,
2039) -> BoundaryCycleUsageResults {
2040 rayon::join(
2041 || run_boundary_detectors(input),
2042 || run_cycle_and_usage_detectors(input),
2043 )
2044}
2045
2046fn run_boundary_detectors(
2047 input: DeadCodeDetectorInput<'_>,
2048) -> (Vec<BoundaryViolationFinding>, BoundaryAuxResults) {
2049 rayon::join(
2050 || {
2051 run_boundary_violation_detector(
2052 input.graph,
2053 input.config,
2054 input.suppressions,
2055 input.line_offsets_by_file,
2056 )
2057 },
2058 || {
2059 run_boundary_aux_detectors(
2060 input.graph,
2061 input.modules,
2062 input.config,
2063 input.declared_deps,
2064 input.suppressions,
2065 input.line_offsets_by_file,
2066 )
2067 },
2068 )
2069}
2070
2071fn run_cycle_and_usage_detectors(
2072 input: DeadCodeDetectorInput<'_>,
2073) -> (
2074 Vec<CircularDependencyFinding>,
2075 (Vec<ReExportCycleFinding>, Vec<crate::results::ExportUsage>),
2076) {
2077 rayon::join(
2078 || {
2079 run_circular_dep_detector(
2080 input.graph,
2081 input.config,
2082 input.line_offsets_by_file,
2083 input.suppressions,
2084 input.workspaces,
2085 )
2086 },
2087 || {
2088 rayon::join(
2089 || run_re_export_cycle_detector(input.graph, input.config, input.suppressions),
2090 || {
2091 run_export_usages_collector(
2092 input.graph,
2093 input.line_offsets_by_file,
2094 input.collect_usages,
2095 )
2096 },
2097 )
2098 },
2099 )
2100}
2101
2102#[expect(
2103 deprecated,
2104 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2105)]
2106fn run_duplicate_export_detector(
2107 graph: &ModuleGraph,
2108 config: &ResolvedConfig,
2109 suppressions: &SuppressionContext<'_>,
2110 line_offsets_by_file: &LineOffsetsMap<'_>,
2111 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
2112 resolved_modules: &[ResolvedModule],
2113) -> Vec<DuplicateExportFinding> {
2114 if config.rules.duplicate_exports == Severity::Off {
2115 return Vec::new();
2116 }
2117 let duplicate_exports = if let Some(plugin_result) = plugin_result {
2118 unused_exports::find_duplicate_exports_with_plugins(
2119 graph,
2120 config,
2121 suppressions,
2122 line_offsets_by_file,
2123 Some(plugin_result),
2124 resolved_modules,
2125 )
2126 } else {
2127 unused_exports::find_duplicate_exports(
2128 graph,
2129 config,
2130 suppressions,
2131 line_offsets_by_file,
2132 resolved_modules,
2133 )
2134 };
2135 duplicate_exports
2136 .into_iter()
2137 .map(DuplicateExportFinding::with_actions)
2138 .collect()
2139}
2140
2141#[expect(
2142 deprecated,
2143 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2144)]
2145fn run_boundary_violation_detector(
2146 graph: &ModuleGraph,
2147 config: &ResolvedConfig,
2148 suppressions: &SuppressionContext<'_>,
2149 line_offsets_by_file: &LineOffsetsMap<'_>,
2150) -> Vec<BoundaryViolationFinding> {
2151 if config.rules.boundary_violation == Severity::Off || config.boundaries.is_empty() {
2152 return Vec::new();
2153 }
2154 boundary::find_boundary_violations(graph, config, suppressions, line_offsets_by_file)
2155 .into_iter()
2156 .map(BoundaryViolationFinding::with_actions)
2157 .collect()
2158}
2159
2160fn filter_public_workspace_results(
2161 config: &ResolvedConfig,
2162 workspaces: &[fallow_config::WorkspaceInfo],
2163 results: &mut AnalysisResults,
2164) {
2165 let public_roots = public_workspace_roots(&config.public_packages, workspaces);
2166 if public_roots.is_empty() {
2167 return;
2168 }
2169 results.unused_exports.retain(|e| {
2170 !public_roots
2171 .iter()
2172 .any(|root| e.export.path.starts_with(root))
2173 });
2174 results.unused_types.retain(|e| {
2175 !public_roots
2176 .iter()
2177 .any(|root| e.export.path.starts_with(root))
2178 });
2179 results.unused_enum_members.retain(|e| {
2180 !public_roots
2181 .iter()
2182 .any(|root| e.member.path.starts_with(root))
2183 });
2184 results.unused_class_members.retain(|e| {
2185 !public_roots
2186 .iter()
2187 .any(|root| e.member.path.starts_with(root))
2188 });
2189}
2190
2191#[expect(
2192 deprecated,
2193 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2194)]
2195fn populate_pnpm_catalog_findings(
2196 config: &ResolvedConfig,
2197 workspaces: &[fallow_config::WorkspaceInfo],
2198 results: &mut AnalysisResults,
2199) {
2200 let need_unused = config.rules.unused_catalog_entries != Severity::Off;
2201 let need_empty_groups = config.rules.empty_catalog_groups != Severity::Off;
2202 let need_unresolved_refs = config.rules.unresolved_catalog_references != Severity::Off;
2203 let Some(state) = ((need_unused || need_empty_groups || need_unresolved_refs)
2204 .then(|| gather_pnpm_catalog_state(config, workspaces)))
2205 .flatten() else {
2206 return;
2207 };
2208
2209 if need_unused {
2210 results.unused_catalog_entries = find_unused_catalog_entries(&state)
2211 .into_iter()
2212 .map(UnusedCatalogEntryFinding::with_actions)
2213 .collect();
2214 }
2215 if need_empty_groups {
2216 results.empty_catalog_groups = find_empty_catalog_groups(&state)
2217 .into_iter()
2218 .map(EmptyCatalogGroupFinding::with_actions)
2219 .collect();
2220 }
2221 if need_unresolved_refs {
2222 results.unresolved_catalog_references = find_unresolved_catalog_references(
2223 &state,
2224 &config.compiled_ignore_catalog_references,
2225 &config.root,
2226 )
2227 .into_iter()
2228 .map(UnresolvedCatalogReferenceFinding::with_actions)
2229 .collect();
2230 }
2231}
2232
2233#[expect(
2234 deprecated,
2235 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2236)]
2237fn populate_pnpm_override_findings(
2238 config: &ResolvedConfig,
2239 workspaces: &[fallow_config::WorkspaceInfo],
2240 results: &mut AnalysisResults,
2241) {
2242 let need_unused = config.rules.unused_dependency_overrides != Severity::Off;
2243 let need_misconfigured = config.rules.misconfigured_dependency_overrides != Severity::Off;
2244 let Some(state) = ((need_unused || need_misconfigured)
2245 .then(|| gather_pnpm_override_state(config, workspaces)))
2246 .flatten() else {
2247 return;
2248 };
2249
2250 if need_unused {
2251 results.unused_dependency_overrides = find_unused_dependency_overrides(&state, config)
2252 .into_iter()
2253 .map(UnusedDependencyOverrideFinding::with_actions)
2254 .collect();
2255 }
2256 if need_misconfigured {
2257 results.misconfigured_dependency_overrides =
2258 find_misconfigured_dependency_overrides(&state, config)
2259 .into_iter()
2260 .map(MisconfiguredDependencyOverrideFinding::with_actions)
2261 .collect();
2262 }
2263}
2264
2265fn populate_security_findings(
2266 ctx: &SecurityDetectionContext<'_, '_>,
2267 results: &mut AnalysisResults,
2268) {
2269 if ctx.config.rules.security_client_server_leak != Severity::Off {
2270 let (security_findings, stats) = security::find_security_findings(
2271 ctx.graph,
2272 ctx.modules,
2273 ctx.suppressions,
2274 ctx.line_offsets_by_file,
2275 );
2276 results.security_findings = security_findings;
2277 results.security_unresolved_edge_files = stats.client_files_with_unresolved_edges;
2278 }
2279
2280 if ctx.config.rules.security_sink != Severity::Off {
2281 populate_tainted_sink_findings(ctx, results);
2282 }
2283
2284 if !results.security_findings.is_empty() {
2285 annotate_security_findings(ctx, results);
2286 }
2287}
2288
2289fn populate_tainted_sink_findings(
2290 ctx: &SecurityDetectionContext<'_, '_>,
2291 results: &mut AnalysisResults,
2292) {
2293 let categories = ctx.config.security.categories.as_ref();
2294 let filter = security::CategoryFilter::new(
2295 categories.and_then(|c| c.include.clone()),
2296 categories.and_then(|c| c.exclude.clone()),
2297 );
2298 let (sink_findings, sink_stats) = security::find_tainted_sinks(
2299 ctx.graph,
2300 ctx.modules,
2301 ctx.suppressions,
2302 ctx.line_offsets_by_file,
2303 ctx.declared_deps,
2304 &security::TaintedSinkContext {
2305 category_filter: &filter,
2306 request_receivers: ctx.request_receivers,
2307 root: &ctx.config.root,
2308 },
2309 );
2310 results.security_findings.extend(sink_findings);
2311 results.security_unresolved_callee_sites = sink_stats.sinks_skipped_dynamic_callee;
2312 results.security_unresolved_callee_diagnostics = sink_stats.unresolved_callee_diagnostics;
2313 results
2314 .security_findings
2315 .extend(security::find_hardcoded_secret_candidates(
2316 ctx.graph,
2317 ctx.modules,
2318 ctx.suppressions,
2319 ctx.line_offsets_by_file,
2320 &filter,
2321 &ctx.config.root,
2322 ));
2323}
2324
2325fn annotate_security_findings(
2326 ctx: &SecurityDetectionContext<'_, '_>,
2327 results: &mut AnalysisResults,
2328) {
2329 security::annotate_dead_code_cross_links(
2330 ctx.graph,
2331 ctx.modules,
2332 ctx.line_offsets_by_file,
2333 &results.unused_files,
2334 &results.unused_exports,
2335 &mut results.security_findings,
2336 );
2337 let boundary_crossings = boundary_crossings_by_file(&results.boundary_violations);
2338 security::rank_security_findings(
2339 &security::SecurityRankingInput {
2340 graph: ctx.graph,
2341 modules: ctx.modules,
2342 line_offsets_by_file: ctx.line_offsets_by_file,
2343 declared_deps: ctx.declared_deps,
2344 request_receivers: ctx.request_receivers,
2345 boundary_crossings: &boundary_crossings,
2346 },
2347 &mut results.security_findings,
2348 );
2349}
2350
2351fn boundary_crossings_by_file(
2352 boundary_violations: &[BoundaryViolationFinding],
2353) -> FxHashMap<std::path::PathBuf, (String, String)> {
2354 let mut boundary_crossings: FxHashMap<std::path::PathBuf, (String, String)> =
2355 FxHashMap::default();
2356 for violation in boundary_violations {
2357 let zones = (
2358 violation.violation.from_zone.clone(),
2359 violation.violation.to_zone.clone(),
2360 );
2361 for path in [
2362 violation.violation.from_path.clone(),
2363 violation.violation.to_path.clone(),
2364 ] {
2365 boundary_crossings
2366 .entry(path)
2367 .and_modify(|existing| {
2368 if zones < *existing {
2369 *existing = zones.clone();
2370 }
2371 })
2372 .or_insert_with(|| zones.clone());
2373 }
2374 }
2375 boundary_crossings
2376}
2377
2378#[expect(
2379 deprecated,
2380 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2381)]
2382fn run_unused_file_detector(
2383 graph: &ModuleGraph,
2384 config: &ResolvedConfig,
2385 suppressions: &crate::suppress::SuppressionContext<'_>,
2386) -> Vec<UnusedFileFinding> {
2387 if config.rules.unused_files == Severity::Off {
2388 return Vec::new();
2389 }
2390 find_unused_files(graph, suppressions)
2391 .into_iter()
2392 .map(UnusedFileFinding::with_actions)
2393 .collect()
2394}
2395
2396#[expect(
2397 deprecated,
2398 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2399)]
2400fn run_export_detectors(
2401 graph: &ModuleGraph,
2402 modules: &[ModuleInfo],
2403 config: &ResolvedConfig,
2404 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
2405 suppressions: &crate::suppress::SuppressionContext<'_>,
2406 line_offsets_by_file: &LineOffsetsMap<'_>,
2407) -> AnalysisResults {
2408 let mut results = AnalysisResults::default();
2409 if export_rules_are_disabled(config) {
2410 return results;
2411 }
2412
2413 let (exports, types, stale_expected) = find_unused_exports(
2414 graph,
2415 modules,
2416 config,
2417 plugin_result,
2418 suppressions,
2419 line_offsets_by_file,
2420 );
2421 populate_unused_export_findings(&mut results, config, exports);
2422 populate_unused_type_findings(&mut results, config, graph, modules, types);
2423 populate_private_type_leak_findings(
2424 &mut results,
2425 graph,
2426 modules,
2427 config,
2428 suppressions,
2429 line_offsets_by_file,
2430 );
2431 populate_expected_stale_suppressions(&mut results, config, stale_expected);
2432 results
2433}
2434
2435fn export_rules_are_disabled(config: &ResolvedConfig) -> bool {
2436 config.rules.unused_exports == Severity::Off
2437 && config.rules.unused_types == Severity::Off
2438 && config.rules.private_type_leaks == Severity::Off
2439}
2440
2441fn populate_unused_export_findings(
2442 results: &mut AnalysisResults,
2443 config: &ResolvedConfig,
2444 exports: Vec<UnusedExport>,
2445) {
2446 if config.rules.unused_exports == Severity::Off {
2447 return;
2448 }
2449 results.unused_exports = exports
2450 .into_iter()
2451 .map(UnusedExportFinding::with_actions)
2452 .collect();
2453}
2454
2455fn populate_unused_type_findings(
2456 results: &mut AnalysisResults,
2457 config: &ResolvedConfig,
2458 graph: &ModuleGraph,
2459 modules: &[ModuleInfo],
2460 types: Vec<UnusedExport>,
2461) {
2462 if config.rules.unused_types == Severity::Off {
2463 return;
2464 }
2465 let mut typed = types;
2466 suppress_signature_backing_types(&mut typed, graph, modules);
2467 results.unused_types = typed
2468 .into_iter()
2469 .map(UnusedTypeFinding::with_actions)
2470 .collect();
2471}
2472
2473fn populate_private_type_leak_findings(
2474 results: &mut AnalysisResults,
2475 graph: &ModuleGraph,
2476 modules: &[ModuleInfo],
2477 config: &ResolvedConfig,
2478 suppressions: &crate::suppress::SuppressionContext<'_>,
2479 line_offsets_by_file: &LineOffsetsMap<'_>,
2480) {
2481 if config.rules.private_type_leaks == Severity::Off {
2482 return;
2483 }
2484 results.private_type_leaks =
2485 find_private_type_leaks(graph, modules, config, suppressions, line_offsets_by_file)
2486 .into_iter()
2487 .map(PrivateTypeLeakFinding::with_actions)
2488 .collect();
2489}
2490
2491fn populate_expected_stale_suppressions(
2492 results: &mut AnalysisResults,
2493 config: &ResolvedConfig,
2494 stale_expected: Vec<StaleSuppression>,
2495) {
2496 if config.rules.stale_suppressions != Severity::Off {
2497 results.stale_suppressions.extend(stale_expected);
2498 } else if config.rules.require_suppression_reason != Severity::Off {
2499 results
2500 .stale_suppressions
2501 .extend(stale_expected.into_iter().filter(|s| s.missing_reason));
2502 }
2503}
2504
2505#[derive(Clone, Copy)]
2506struct MemberDetectorInput<'a> {
2507 graph: &'a ModuleGraph,
2508 resolved_modules: &'a [ResolvedModule],
2509 modules: &'a [ModuleInfo],
2510 config: &'a ResolvedConfig,
2511 suppressions: &'a crate::suppress::SuppressionContext<'a>,
2512 line_offsets_by_file: &'a LineOffsetsMap<'a>,
2513 user_class_members: &'a [fallow_config::UsedClassMemberRule],
2514 semantic_framework_candidates: &'a [fallow_config::UsedClassMemberRule],
2515 public_api_entry_points: &'a FxHashSet<FileId>,
2516 declared_deps: &'a FxHashSet<String>,
2517}
2518
2519fn run_member_detectors(input: MemberDetectorInput<'_>) -> AnalysisResults {
2520 let mut results = AnalysisResults::default();
2521 let store_members_active = store_member_rule_is_active(input.config, input.declared_deps);
2522 if member_rules_are_disabled(input.config, store_members_active) {
2523 return results;
2524 }
2525
2526 let member_results = find_unused_members_with_public_api_entry_points(UnusedMemberScanInput {
2527 graph: input.graph,
2528 resolved_modules: input.resolved_modules,
2529 modules: input.modules,
2530 suppressions: input.suppressions,
2531 line_offsets_by_file: input.line_offsets_by_file,
2532 user_class_member_allowlist: input.user_class_members,
2533 semantic_framework_candidates: input.semantic_framework_candidates,
2534 ignore_decorators: &input.config.ignore_decorators,
2535 public_api_entry_points: input.public_api_entry_points,
2536 lit_active: input.declared_deps.contains("lit")
2537 || input.declared_deps.contains("lit-element")
2538 || input.declared_deps.contains("@lit/reactive-element"),
2539 });
2540 populate_unused_enum_member_findings(&mut results, input.config, member_results.enum_members);
2541 populate_unused_class_member_findings(&mut results, input.config, member_results.class_members);
2542 populate_unused_store_member_findings(
2543 &mut results,
2544 store_members_active,
2545 member_results.store_members,
2546 );
2547 results
2548}
2549
2550fn member_rules_are_disabled(config: &ResolvedConfig, store_members_active: bool) -> bool {
2551 config.rules.unused_enum_members == Severity::Off
2552 && config.rules.unused_class_members == Severity::Off
2553 && !store_members_active
2554}
2555
2556fn store_member_rule_is_active(config: &ResolvedConfig, declared_deps: &FxHashSet<String>) -> bool {
2557 config.rules.unused_store_members != Severity::Off
2562 && (declared_deps.contains("pinia") || declared_deps.contains("@pinia/nuxt"))
2563}
2564
2565fn populate_unused_enum_member_findings(
2566 results: &mut AnalysisResults,
2567 config: &ResolvedConfig,
2568 enum_members: Vec<UnusedMember>,
2569) {
2570 if config.rules.unused_enum_members == Severity::Off {
2571 return;
2572 }
2573 results.unused_enum_members = enum_members
2574 .into_iter()
2575 .map(UnusedEnumMemberFinding::with_actions)
2576 .collect();
2577}
2578
2579fn populate_unused_class_member_findings(
2580 results: &mut AnalysisResults,
2581 config: &ResolvedConfig,
2582 class_members: Vec<members::UnusedClassMemberCandidate>,
2583) {
2584 if config.rules.unused_class_members == Severity::Off {
2585 return;
2586 }
2587 results.unused_class_members = class_members
2588 .into_iter()
2589 .map(|candidate| {
2590 let finding = UnusedClassMemberFinding::with_actions(candidate.member);
2591 if candidate.semantic_only {
2592 finding.semantic_only_candidate()
2593 } else {
2594 finding
2595 }
2596 })
2597 .collect();
2598}
2599
2600fn populate_unused_store_member_findings(
2601 results: &mut AnalysisResults,
2602 store_members_active: bool,
2603 store_members: Vec<UnusedMember>,
2604) {
2605 if !store_members_active {
2606 return;
2607 }
2608 results.unused_store_members = store_members
2609 .into_iter()
2610 .map(UnusedStoreMemberFinding::with_actions)
2611 .collect();
2612}
2613
2614#[derive(Clone, Copy)]
2615struct DependencyDetectorInput<'a> {
2616 graph: &'a ModuleGraph,
2617 pkg: Option<&'a PackageJson>,
2618 config: &'a ResolvedConfig,
2619 plugin_result: Option<&'a crate::plugins::AggregatedPluginResult>,
2620 workspaces: &'a [fallow_config::WorkspaceInfo],
2621 resolved_modules: &'a [ResolvedModule],
2622 line_offsets_by_file: &'a LineOffsetsMap<'a>,
2623}
2624
2625fn run_dependency_detectors(input: DependencyDetectorInput<'_>) -> AnalysisResults {
2626 let mut results = AnalysisResults::default();
2627 let Some(pkg) = input.pkg else {
2628 return results;
2629 };
2630
2631 populate_unused_dependency_findings(input, pkg, &mut results);
2632 populate_unlisted_dependency_findings(input, pkg, &mut results);
2633 populate_type_only_dependency_findings(input, pkg, &mut results);
2634 populate_test_only_dependency_findings(input, pkg, &mut results);
2635 populate_dev_dependency_in_production_findings(input, pkg, &mut results);
2636 results
2637}
2638
2639fn populate_unlisted_dependency_findings(
2640 input: DependencyDetectorInput<'_>,
2641 pkg: &PackageJson,
2642 results: &mut AnalysisResults,
2643) {
2644 if input.config.rules.unlisted_dependencies != Severity::Off {
2645 results.unlisted_dependencies = find_unlisted_dependencies(UnlistedDependencyInput {
2646 graph: input.graph,
2647 pkg,
2648 config: input.config,
2649 workspaces: input.workspaces,
2650 plugin_result: input.plugin_result,
2651 resolved_modules: input.resolved_modules,
2652 line_offsets_by_file: input.line_offsets_by_file,
2653 })
2654 .into_iter()
2655 .map(UnlistedDependencyFinding::with_actions)
2656 .collect();
2657 }
2658}
2659
2660fn populate_type_only_dependency_findings(
2661 input: DependencyDetectorInput<'_>,
2662 pkg: &PackageJson,
2663 results: &mut AnalysisResults,
2664) {
2665 if input.config.production {
2666 results.type_only_dependencies =
2667 find_type_only_dependencies(input.graph, pkg, input.config, input.workspaces)
2668 .into_iter()
2669 .map(TypeOnlyDependencyFinding::with_actions)
2670 .collect();
2671 }
2672}
2673
2674fn populate_test_only_dependency_findings(
2675 input: DependencyDetectorInput<'_>,
2676 pkg: &PackageJson,
2677 results: &mut AnalysisResults,
2678) {
2679 if !input.config.production && input.config.rules.test_only_dependencies != Severity::Off {
2680 results.test_only_dependencies =
2681 find_test_only_dependencies(input.graph, pkg, input.config, input.workspaces)
2682 .into_iter()
2683 .map(TestOnlyDependencyFinding::with_actions)
2684 .collect();
2685 }
2686}
2687
2688fn populate_dev_dependency_in_production_findings(
2689 input: DependencyDetectorInput<'_>,
2690 pkg: &PackageJson,
2691 results: &mut AnalysisResults,
2692) {
2693 if input.config.rules.dev_dependencies_in_production != Severity::Off {
2699 results.dev_dependencies_in_production =
2700 find_dev_dependencies_in_production(input.graph, pkg, input.config, input.workspaces)
2701 .into_iter()
2702 .map(DevDependencyInProductionFinding::with_actions)
2703 .collect();
2704 }
2705}
2706
2707#[expect(
2711 deprecated,
2712 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2713)]
2714fn populate_unused_dependency_findings(
2715 input: DependencyDetectorInput<'_>,
2716 pkg: &PackageJson,
2717 results: &mut AnalysisResults,
2718) {
2719 if unused_dependency_rules_are_disabled(input.config) {
2720 return;
2721 }
2722
2723 let (deps, dev_deps, optional_deps) = find_unused_dependencies(
2724 input.graph,
2725 pkg,
2726 input.config,
2727 input.plugin_result,
2728 input.workspaces,
2729 );
2730 populate_unused_prod_dependency_findings(results, input.config, deps);
2731 populate_unused_dev_dependency_findings(results, input.config, dev_deps);
2732 populate_unused_optional_dependency_findings(results, input.config, optional_deps);
2733}
2734
2735fn unused_dependency_rules_are_disabled(config: &ResolvedConfig) -> bool {
2736 config.rules.unused_dependencies == Severity::Off
2737 && config.rules.unused_dev_dependencies == Severity::Off
2738 && config.rules.unused_optional_dependencies == Severity::Off
2739}
2740
2741fn populate_unused_prod_dependency_findings(
2742 results: &mut AnalysisResults,
2743 config: &ResolvedConfig,
2744 deps: Vec<UnusedDependency>,
2745) {
2746 if config.rules.unused_dependencies == Severity::Off {
2747 return;
2748 }
2749 results.unused_dependencies = deps
2750 .into_iter()
2751 .map(UnusedDependencyFinding::with_actions)
2752 .collect();
2753}
2754
2755fn populate_unused_dev_dependency_findings(
2756 results: &mut AnalysisResults,
2757 config: &ResolvedConfig,
2758 dev_deps: Vec<UnusedDependency>,
2759) {
2760 if config.rules.unused_dev_dependencies == Severity::Off {
2761 return;
2762 }
2763 results.unused_dev_dependencies = dev_deps
2764 .into_iter()
2765 .map(UnusedDevDependencyFinding::with_actions)
2766 .collect();
2767}
2768
2769fn populate_unused_optional_dependency_findings(
2770 results: &mut AnalysisResults,
2771 config: &ResolvedConfig,
2772 optional_deps: Vec<UnusedDependency>,
2773) {
2774 if config.rules.unused_optional_dependencies == Severity::Off {
2775 return;
2776 }
2777 results.unused_optional_dependencies = optional_deps
2778 .into_iter()
2779 .map(UnusedOptionalDependencyFinding::with_actions)
2780 .collect();
2781}
2782
2783#[derive(Clone, Copy)]
2784struct UnresolvedImportDetectorInput<'a> {
2785 resolved_modules: &'a [ResolvedModule],
2786 config: &'a ResolvedConfig,
2787 suppressions: &'a crate::suppress::SuppressionContext<'a>,
2788 virtual_prefixes: &'a [&'a str],
2789 generated_patterns: &'a [&'a str],
2790 generated_type_prefixes: &'a [&'a str],
2791 line_offsets_by_file: &'a LineOffsetsMap<'a>,
2792}
2793
2794fn run_unresolved_import_detector(
2795 input: UnresolvedImportDetectorInput<'_>,
2796) -> Vec<UnresolvedImportFinding> {
2797 if input.config.rules.unresolved_imports == Severity::Off || input.resolved_modules.is_empty() {
2798 return Vec::new();
2799 }
2800 find_unresolved_imports(
2801 input.resolved_modules,
2802 input.config,
2803 input.suppressions,
2804 input.virtual_prefixes,
2805 input.generated_patterns,
2806 input.generated_type_prefixes,
2807 input.line_offsets_by_file,
2808 )
2809 .into_iter()
2810 .map(UnresolvedImportFinding::with_actions)
2811 .collect()
2812}
2813
2814#[cfg(test)]
2815#[expect(
2816 deprecated,
2817 reason = "Core-internal policy keeps direct analyzer unit tests while the public warning targets external callers"
2818)]
2819mod tests {
2820 use fallow_types::extract::{byte_offset_to_line_col, compute_line_offsets};
2821
2822 #[test]
2823 fn exact_framework_contract_only_replaces_its_matching_plugin_rule() {
2824 use fallow_config::{ScopedUsedClassMemberRule, UsedClassMemberRule};
2825 use fallow_types::semantic::{SemanticFrameworkContract, SemanticFrameworkRelation};
2826
2827 let contract = SemanticFrameworkContract {
2828 framework: "lit".to_string(),
2829 package: "lit".to_string(),
2830 heritage_symbol: "LitElement".to_string(),
2831 heritage_names: vec!["LitElement".to_string()],
2832 relation: SemanticFrameworkRelation::Extends,
2833 members: vec!["render".to_string()],
2834 };
2835 let matching = UsedClassMemberRule::Scoped(ScopedUsedClassMemberRule {
2836 extends: Some("LitElement".to_string()),
2837 implements: None,
2838 members: vec!["render".to_string()],
2839 });
2840 let local_name_only = UsedClassMemberRule::Scoped(ScopedUsedClassMemberRule {
2841 extends: Some("LocalLitElement".to_string()),
2842 implements: None,
2843 members: vec!["render".to_string()],
2844 });
2845 let extra_member = UsedClassMemberRule::Scoped(ScopedUsedClassMemberRule {
2846 extends: Some("LitElement".to_string()),
2847 implements: None,
2848 members: vec!["render".to_string(), "localHook".to_string()],
2849 });
2850
2851 assert!(super::framework_contract_covers_rule(&contract, &matching));
2852 assert!(!super::framework_contract_covers_rule(
2853 &contract,
2854 &local_name_only
2855 ));
2856 assert!(!super::framework_contract_covers_rule(
2857 &contract,
2858 &extra_member
2859 ));
2860 }
2861
2862 fn line_col(source: &str, byte_offset: u32) -> (u32, u32) {
2863 let offsets = compute_line_offsets(source);
2864 byte_offset_to_line_col(&offsets, byte_offset)
2865 }
2866
2867 #[cfg(unix)]
2874 #[cfg_attr(miri, ignore)]
2875 #[test]
2876 fn scoped_canonical_matches_module_reached_through_symlink() {
2877 use fallow_types::discover::FileId;
2878
2879 let dir = tempfile::tempdir().unwrap();
2880 let real_dir = dir.path().join("real");
2881 std::fs::create_dir(&real_dir).unwrap();
2882 let real_file = real_dir.join("mod.ts");
2883 std::fs::write(&real_file, "export const x = 1;\n").unwrap();
2884 let link_dir = dir.path().join("link");
2887 std::os::unix::fs::symlink(&real_dir, &link_dir).unwrap();
2888
2889 let module_raw_path = link_dir.join("mod.ts");
2890 let canonical_entry = dunce::canonicalize(&real_file).unwrap();
2891 let package_root = dir.path();
2892
2893 let candidates = [(module_raw_path.as_path(), FileId(7))];
2895 assert_eq!(
2896 super::match_canonical_entry_under_package(
2897 candidates.iter().copied(),
2898 package_root,
2899 &canonical_entry,
2900 ),
2901 Some(FileId(7)),
2902 );
2903
2904 let outside_root = dir.path().join("other-package");
2906 assert_eq!(
2907 super::match_canonical_entry_under_package(
2908 candidates.iter().copied(),
2909 &outside_root,
2910 &canonical_entry,
2911 ),
2912 None,
2913 );
2914
2915 let unrelated = dunce::canonicalize(dir.path()).unwrap().join("nope.ts");
2917 assert_eq!(
2918 super::match_canonical_entry_under_package(
2919 candidates.iter().copied(),
2920 package_root,
2921 &unrelated,
2922 ),
2923 None,
2924 );
2925 }
2926
2927 #[test]
2928 fn compute_offsets_empty() {
2929 assert_eq!(compute_line_offsets(""), vec![0]);
2930 }
2931
2932 #[test]
2933 fn compute_offsets_single_line() {
2934 assert_eq!(compute_line_offsets("hello"), vec![0]);
2935 }
2936
2937 #[test]
2938 fn compute_offsets_multiline() {
2939 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
2940 }
2941
2942 #[test]
2943 fn compute_offsets_trailing_newline() {
2944 assert_eq!(compute_line_offsets("abc\n"), vec![0, 4]);
2945 }
2946
2947 #[test]
2948 fn compute_offsets_crlf() {
2949 assert_eq!(compute_line_offsets("ab\r\ncd"), vec![0, 4]);
2950 }
2951
2952 #[test]
2953 fn compute_offsets_consecutive_newlines() {
2954 assert_eq!(compute_line_offsets("\n\n"), vec![0, 1, 2]);
2955 }
2956
2957 #[test]
2958 fn byte_offset_empty_source() {
2959 assert_eq!(line_col("", 0), (1, 0));
2960 }
2961
2962 #[test]
2963 fn byte_offset_single_line_start() {
2964 assert_eq!(line_col("hello", 0), (1, 0));
2965 }
2966
2967 #[test]
2968 fn byte_offset_single_line_middle() {
2969 assert_eq!(line_col("hello", 4), (1, 4));
2970 }
2971
2972 #[test]
2973 fn byte_offset_multiline_start_of_line2() {
2974 assert_eq!(line_col("line1\nline2\nline3", 6), (2, 0));
2975 }
2976
2977 #[test]
2978 fn byte_offset_multiline_middle_of_line3() {
2979 assert_eq!(line_col("line1\nline2\nline3", 14), (3, 2));
2980 }
2981
2982 #[test]
2983 fn byte_offset_at_newline_boundary() {
2984 assert_eq!(line_col("line1\nline2", 5), (1, 5));
2985 }
2986
2987 #[test]
2988 fn byte_offset_multibyte_utf8() {
2989 let source = "hi\n\u{1F600}x";
2990 assert_eq!(line_col(source, 3), (2, 0));
2991 assert_eq!(line_col(source, 7), (2, 4));
2992 }
2993
2994 #[test]
2995 fn byte_offset_multibyte_accented_chars() {
2996 let source = "caf\u{00E9}\nbar";
2997 assert_eq!(line_col(source, 6), (2, 0));
2998 assert_eq!(line_col(source, 3), (1, 3));
2999 }
3000
3001 #[test]
3002 fn byte_offset_via_map_fallback() {
3003 use super::*;
3004 let map: LineOffsetsMap<'_> = FxHashMap::default();
3005 assert_eq!(
3006 super::byte_offset_to_line_col(&map, FileId(99), 42),
3007 (1, 42)
3008 );
3009 }
3010
3011 #[test]
3012 fn byte_offset_via_map_lookup() {
3013 use super::*;
3014 let offsets = compute_line_offsets("abc\ndef\nghi");
3015 let mut map: LineOffsetsMap<'_> = FxHashMap::default();
3016 map.insert(FileId(0), &offsets);
3017 assert_eq!(super::byte_offset_to_line_col(&map, FileId(0), 5), (2, 1));
3018 }
3019
3020 mod orchestration {
3021 use super::super::*;
3022 use fallow_config::{FallowConfig, OutputFormat, RulesConfig, Severity};
3023 use std::path::PathBuf;
3024
3025 fn find_dead_code(graph: &ModuleGraph, config: &ResolvedConfig) -> AnalysisResults {
3026 find_dead_code_full(graph, config, &[], None, &[], &[], false)
3027 }
3028
3029 fn make_config_with_rules(rules: RulesConfig) -> ResolvedConfig {
3030 FallowConfig {
3031 rules,
3032 ..Default::default()
3033 }
3034 .resolve(
3035 PathBuf::from("/tmp/orchestration-test"),
3036 OutputFormat::Human,
3037 1,
3038 true,
3039 true,
3040 None,
3041 )
3042 }
3043
3044 const ALL_RULES_OFF: RulesConfig = RulesConfig {
3045 unused_files: Severity::Off,
3046 unused_exports: Severity::Off,
3047 unused_types: Severity::Off,
3048 private_type_leaks: Severity::Off,
3049 unused_dependencies: Severity::Off,
3050 unused_dev_dependencies: Severity::Off,
3051 unused_optional_dependencies: Severity::Off,
3052 unused_enum_members: Severity::Off,
3053 unused_class_members: Severity::Off,
3054 unused_store_members: Severity::Off,
3055 unprovided_injects: Severity::Off,
3056 unrendered_components: Severity::Off,
3057 unused_component_props: Severity::Off,
3058 unused_component_emits: Severity::Off,
3059 unused_component_inputs: Severity::Off,
3060 unused_component_outputs: Severity::Off,
3061 unused_svelte_events: Severity::Off,
3062 unused_server_actions: Severity::Off,
3063 unused_load_data_keys: Severity::Off,
3064 prop_drilling: Severity::Off,
3065 thin_wrapper: Severity::Off,
3066 duplicate_prop_shape: Severity::Off,
3067 css_token_drift: Severity::Off,
3068 css_duplicate_block: Severity::Off,
3069 css_selector_complexity: Severity::Off,
3070 css_dead_surface: Severity::Off,
3071 css_broken_reference: Severity::Off,
3072 unresolved_imports: Severity::Off,
3073 unlisted_dependencies: Severity::Off,
3074 duplicate_exports: Severity::Off,
3075 type_only_dependencies: Severity::Off,
3076 circular_dependencies: Severity::Off,
3077 re_export_cycle: Severity::Off,
3078 test_only_dependencies: Severity::Off,
3079 dev_dependencies_in_production: Severity::Off,
3080 boundary_violation: Severity::Off,
3081 coverage_gaps: Severity::Off,
3082 feature_flags: Severity::Off,
3083 stale_suppressions: Severity::Off,
3084 require_suppression_reason: Severity::Off,
3085 unused_catalog_entries: Severity::Off,
3086 empty_catalog_groups: Severity::Off,
3087 unresolved_catalog_references: Severity::Off,
3088 unused_dependency_overrides: Severity::Off,
3089 misconfigured_dependency_overrides: Severity::Off,
3090 security_client_server_leak: Severity::Off,
3091 security_sink: Severity::Off,
3092 policy_violation: Severity::Off,
3093 invalid_client_export: Severity::Off,
3094 mixed_client_server_barrel: Severity::Off,
3095 misplaced_directive: Severity::Off,
3096 route_collision: Severity::Off,
3097 dynamic_segment_name_conflict: Severity::Off,
3098 };
3099
3100 #[test]
3101 fn find_dead_code_all_rules_off_returns_empty() {
3102 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
3103 use crate::graph::ModuleGraph;
3104 use crate::resolve::ResolvedModule;
3105 use rustc_hash::FxHashSet;
3106
3107 let files = vec![DiscoveredFile {
3108 id: FileId(0),
3109 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3110 size_bytes: 100,
3111 }];
3112 let entry_points = vec![EntryPoint {
3113 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3114 source: EntryPointSource::ManualEntry,
3115 }];
3116 let resolved = vec![ResolvedModule {
3117 file_id: FileId(0),
3118 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3119 exports: vec![],
3120 re_exports: vec![],
3121 resolved_imports: vec![],
3122 resolved_dynamic_imports: vec![],
3123 resolved_dynamic_patterns: vec![],
3124 member_accesses: vec![],
3125 semantic_facts: Box::default(),
3126 whole_object_uses: Box::default(),
3127 has_cjs_exports: false,
3128 has_angular_component_template_url: false,
3129 unused_import_bindings: FxHashSet::default(),
3130 type_referenced_import_bindings: vec![],
3131 value_referenced_import_bindings: vec![],
3132 namespace_object_aliases: vec![],
3133 exported_factory_returns: Box::default(),
3134 exported_factory_return_object_shapes: Box::default(),
3135 type_member_types: Box::default(),
3136 }];
3137 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
3138
3139 let config = make_config_with_rules(ALL_RULES_OFF);
3140 let results = find_dead_code(&graph, &config);
3141
3142 assert!(results.unused_files.is_empty());
3143 assert!(results.unused_exports.is_empty());
3144 assert!(results.unused_types.is_empty());
3145 assert!(results.unused_dependencies.is_empty());
3146 assert!(results.unused_dev_dependencies.is_empty());
3147 assert!(results.unused_optional_dependencies.is_empty());
3148 assert!(results.unused_enum_members.is_empty());
3149 assert!(results.unused_class_members.is_empty());
3150 assert!(results.unresolved_imports.is_empty());
3151 assert!(results.unlisted_dependencies.is_empty());
3152 assert!(results.duplicate_exports.is_empty());
3153 assert!(results.circular_dependencies.is_empty());
3154 assert!(results.export_usages.is_empty());
3155 }
3156
3157 #[test]
3158 fn find_dead_code_full_collect_usages_flag() {
3159 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
3160 use crate::extract::{ExportName, VisibilityTag};
3161 use crate::graph::{ExportSymbol, ModuleGraph};
3162 use crate::resolve::ResolvedModule;
3163 use oxc_span::Span;
3164 use rustc_hash::FxHashSet;
3165
3166 let files = vec![DiscoveredFile {
3167 id: FileId(0),
3168 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3169 size_bytes: 100,
3170 }];
3171 let entry_points = vec![EntryPoint {
3172 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3173 source: EntryPointSource::ManualEntry,
3174 }];
3175 let resolved = vec![ResolvedModule {
3176 file_id: FileId(0),
3177 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3178 exports: vec![],
3179 re_exports: vec![],
3180 resolved_imports: vec![],
3181 resolved_dynamic_imports: vec![],
3182 resolved_dynamic_patterns: vec![],
3183 member_accesses: vec![],
3184 semantic_facts: Box::default(),
3185 whole_object_uses: Box::default(),
3186 has_cjs_exports: false,
3187 has_angular_component_template_url: false,
3188 unused_import_bindings: FxHashSet::default(),
3189 type_referenced_import_bindings: vec![],
3190 value_referenced_import_bindings: vec![],
3191 namespace_object_aliases: vec![],
3192 exported_factory_returns: Box::default(),
3193 exported_factory_return_object_shapes: Box::default(),
3194 type_member_types: Box::default(),
3195 }];
3196 let mut graph = ModuleGraph::build(&resolved, &entry_points, &files);
3197 graph.modules[0].exports = vec![ExportSymbol {
3198 name: ExportName::Named("myExport".to_string()),
3199 is_type_only: false,
3200 is_side_effect_used: false,
3201 visibility: VisibilityTag::None,
3202 expected_unused_reason: None,
3203 span: Span::new(10, 30),
3204 references: vec![],
3205 reference_paths: Vec::new(),
3206 members: vec![],
3207 }];
3208
3209 let rules = RulesConfig::default();
3210 let config = make_config_with_rules(rules);
3211
3212 let results_no_collect = find_dead_code_full(
3213 &graph,
3214 &config,
3215 &[],
3216 None,
3217 &[],
3218 &[],
3219 false, );
3221 assert!(
3222 results_no_collect.export_usages.is_empty(),
3223 "export_usages should be empty when collect_usages is false"
3224 );
3225
3226 let results_with_collect = find_dead_code_full(
3227 &graph,
3228 &config,
3229 &[],
3230 None,
3231 &[],
3232 &[],
3233 true, );
3235 assert!(
3236 !results_with_collect.export_usages.is_empty(),
3237 "export_usages should be populated when collect_usages is true"
3238 );
3239 assert_eq!(
3240 results_with_collect.export_usages[0].export_name,
3241 "myExport"
3242 );
3243 }
3244
3245 #[test]
3246 fn find_dead_code_delegates_to_find_dead_code_with_resolved() {
3247 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
3248 use crate::graph::ModuleGraph;
3249 use crate::resolve::ResolvedModule;
3250 use rustc_hash::FxHashSet;
3251
3252 let files = vec![DiscoveredFile {
3253 id: FileId(0),
3254 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3255 size_bytes: 100,
3256 }];
3257 let entry_points = vec![EntryPoint {
3258 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3259 source: EntryPointSource::ManualEntry,
3260 }];
3261 let resolved = vec![ResolvedModule {
3262 file_id: FileId(0),
3263 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3264 exports: vec![],
3265 re_exports: vec![],
3266 resolved_imports: vec![],
3267 resolved_dynamic_imports: vec![],
3268 resolved_dynamic_patterns: vec![],
3269 member_accesses: vec![],
3270 semantic_facts: Box::default(),
3271 whole_object_uses: Box::default(),
3272 has_cjs_exports: false,
3273 has_angular_component_template_url: false,
3274 unused_import_bindings: FxHashSet::default(),
3275 type_referenced_import_bindings: vec![],
3276 value_referenced_import_bindings: vec![],
3277 namespace_object_aliases: vec![],
3278 exported_factory_returns: Box::default(),
3279 exported_factory_return_object_shapes: Box::default(),
3280 type_member_types: Box::default(),
3281 }];
3282 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
3283 let config = make_config_with_rules(RulesConfig::default());
3284
3285 let results = find_dead_code(&graph, &config);
3286 assert!(results.unused_exports.is_empty());
3287 }
3288
3289 #[test]
3290 #[expect(
3291 clippy::too_many_lines,
3292 reason = "test fixture; linear setup/assert, length is not a maintainability concern"
3293 )]
3294 fn suppressions_built_from_modules() {
3295 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
3296 use crate::extract::ModuleInfo;
3297 use crate::graph::ModuleGraph;
3298 use crate::resolve::ResolvedModule;
3299 use crate::suppress::{IssueKind, Suppression};
3300 use rustc_hash::FxHashSet;
3301
3302 let files = vec![
3303 DiscoveredFile {
3304 id: FileId(0),
3305 path: PathBuf::from("/tmp/orchestration-test/src/entry.ts"),
3306 size_bytes: 100,
3307 },
3308 DiscoveredFile {
3309 id: FileId(1),
3310 path: PathBuf::from("/tmp/orchestration-test/src/utils.ts"),
3311 size_bytes: 100,
3312 },
3313 ];
3314 let entry_points = vec![EntryPoint {
3315 path: PathBuf::from("/tmp/orchestration-test/src/entry.ts"),
3316 source: EntryPointSource::ManualEntry,
3317 }];
3318 let resolved = files
3319 .iter()
3320 .map(|f| ResolvedModule {
3321 file_id: f.id,
3322 path: f.path.clone(),
3323 exports: vec![],
3324 re_exports: vec![],
3325 resolved_imports: vec![],
3326 resolved_dynamic_imports: vec![],
3327 resolved_dynamic_patterns: vec![],
3328 member_accesses: vec![],
3329 semantic_facts: Box::default(),
3330 whole_object_uses: Box::default(),
3331 has_cjs_exports: false,
3332 has_angular_component_template_url: false,
3333 unused_import_bindings: FxHashSet::default(),
3334 type_referenced_import_bindings: vec![],
3335 value_referenced_import_bindings: vec![],
3336 namespace_object_aliases: vec![],
3337 exported_factory_returns: Box::default(),
3338 exported_factory_return_object_shapes: Box::default(),
3339 type_member_types: Box::default(),
3340 })
3341 .collect::<Vec<_>>();
3342 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
3343
3344 let modules = vec![ModuleInfo {
3345 file_id: FileId(1),
3346 exports: vec![],
3347 imports: vec![],
3348 re_exports: vec![],
3349 dynamic_imports: vec![],
3350 dynamic_import_patterns: vec![],
3351 require_calls: vec![],
3352 package_path_references: Box::default(),
3353 member_accesses: vec![],
3354 semantic_facts: Box::default(),
3355 whole_object_uses: Box::default(),
3356 has_cjs_exports: false,
3357 has_angular_component_template_url: false,
3358 content_hash: 0,
3359 suppressions: vec![Suppression::issue(0, 1, IssueKind::UnusedFile)],
3360 unknown_suppression_kinds: vec![],
3361 unused_import_bindings: vec![],
3362 type_referenced_import_bindings: vec![],
3363 value_referenced_import_bindings: vec![],
3364 line_offsets: vec![],
3365 complexity: vec![],
3366 flag_uses: vec![],
3367 class_heritage: vec![],
3368 exported_factory_returns: Box::default(),
3369 exported_factory_return_object_shapes: Box::default(),
3370 type_member_types: Box::default(),
3371 injection_tokens: vec![],
3372 local_type_declarations: Vec::new(),
3373 public_signature_type_references: Vec::new(),
3374 namespace_object_aliases: Vec::new(),
3375 iconify_prefixes: Vec::new(),
3376 iconify_icon_names: Vec::new(),
3377 auto_import_candidates: Vec::new(),
3378 directives: Vec::new(),
3379 client_only_dynamic_import_spans: Vec::new(),
3380 security_sinks: Vec::new(),
3381 security_sinks_skipped: 0,
3382 security_unresolved_callee_sites: Vec::new(),
3383 tainted_bindings: Vec::new(),
3384 sanitized_sink_args: Vec::new(),
3385 security_control_sites: Vec::new(),
3386 callee_uses: Vec::new(),
3387 misplaced_directives: Vec::new(),
3388 inline_server_action_exports: Vec::new(),
3389 di_key_sites: Vec::new(),
3390 has_dynamic_provide: false,
3391 referenced_import_bindings: Vec::new(),
3392 component_props: Vec::new(),
3393 has_props_attrs_fallthrough: false,
3394 has_define_expose: false,
3395 has_define_model: false,
3396 has_unharvestable_props: false,
3397 component_emits: Vec::new(),
3398 angular_inputs: Vec::new(),
3399 angular_outputs: Vec::new(),
3400 has_unharvestable_emits: false,
3401 has_dynamic_emit: false,
3402 has_emit_whole_object_use: false,
3403 load_return_keys: Vec::new(),
3404 has_unharvestable_load: false,
3405 has_load_data_whole_use: false,
3406 has_page_data_store_whole_use: false,
3407 has_route_loader_data_whole_use: false,
3408 component_functions: Vec::new(),
3409 react_props: Vec::new(),
3410 hook_uses: Vec::new(),
3411 render_edges: Vec::new(),
3412 svelte_dispatched_events: Vec::new(),
3413 svelte_listened_events: Vec::new(),
3414 angular_component_selectors: Vec::new(),
3415 registered_custom_elements: Vec::new(),
3416 used_custom_element_tags: Vec::new(),
3417 angular_used_selectors: Vec::new(),
3418 angular_entry_component_refs: Vec::new(),
3419 has_dynamic_component_render: false,
3420 has_dynamic_dispatch: false,
3421 }];
3422
3423 let rules = RulesConfig {
3424 unused_files: Severity::Error,
3425 ..RulesConfig::default()
3426 };
3427 let config = make_config_with_rules(rules);
3428
3429 let results = find_dead_code_full(&graph, &config, &[], None, &[], &modules, false);
3430
3431 assert!(
3432 !results.unused_files.iter().any(|f| f
3433 .file
3434 .path
3435 .to_string_lossy()
3436 .contains("utils.ts")),
3437 "suppressed file should not appear in unused_files"
3438 );
3439 }
3440 }
3441}