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(|workspace| fallow_config::workspace_is_public(&workspace.name, public_packages))
262 .map(|ws| ws.root.as_path())
263 .collect()
264}
265
266fn graph_path_to_file_id(graph: &ModuleGraph) -> FxHashMap<std::path::PathBuf, FileId> {
278 graph
279 .modules
280 .iter()
281 .map(|module| (module.path.clone(), module.file_id))
282 .collect()
283}
284
285fn resolve_entry_via_scoped_canonical(
295 graph: &ModuleGraph,
296 package_root: &std::path::Path,
297 canonical_entry: &std::path::Path,
298) -> Option<FileId> {
299 match_canonical_entry_under_package(
300 graph.modules.iter().map(|m| (m.path.as_path(), m.file_id)),
301 package_root,
302 canonical_entry,
303 )
304}
305
306fn match_canonical_entry_under_package<'a>(
311 candidates: impl Iterator<Item = (&'a std::path::Path, FileId)>,
312 package_root: &std::path::Path,
313 canonical_entry: &std::path::Path,
314) -> Option<FileId> {
315 candidates
316 .filter(|(path, _)| path.starts_with(package_root))
317 .find_map(|(path, file_id)| {
318 (dunce::canonicalize(path).ok().as_deref() == Some(canonical_entry)).then_some(file_id)
319 })
320}
321
322fn add_package_public_api_entry_points(
323 public_api_entry_points: &mut FxHashSet<FileId>,
324 graph: &ModuleGraph,
325 path_to_file_id: &FxHashMap<std::path::PathBuf, FileId>,
326 package_root: &std::path::Path,
327 package_json: &PackageJson,
328 canonical_project_root: &std::path::Path,
329) {
330 if package_json.private.unwrap_or(false) {
331 return;
332 }
333
334 for entry in package_json.entry_points() {
335 let Some(entry_point) = crate::discover::resolve_entry_path(
336 package_root,
337 &entry,
338 canonical_project_root,
339 crate::discover::EntryPointSource::PackageJsonExports,
340 ) else {
341 continue;
342 };
343
344 if let Some(file_id) = path_to_file_id.get(&entry_point.path).copied().or_else(|| {
345 dunce::canonicalize(&entry_point.path)
346 .ok()
347 .and_then(|canonical| {
348 path_to_file_id.get(&canonical).copied().or_else(|| {
349 resolve_entry_via_scoped_canonical(graph, package_root, &canonical)
350 })
351 })
352 }) {
353 public_api_entry_points.insert(file_id);
354 }
355 }
356}
357
358fn is_source_index_under_package(path: &std::path::Path, package_root: &std::path::Path) -> bool {
359 let Ok(relative) = path.strip_prefix(package_root) else {
360 return false;
361 };
362
363 if !matches!(
364 relative.components().next(),
365 Some(std::path::Component::Normal(segment)) if segment == "src"
366 ) {
367 return false;
368 }
369
370 path.file_stem()
371 .and_then(|stem| stem.to_str())
372 .is_some_and(|stem| stem == "index")
373}
374
375fn add_exportless_package_source_indexes(
376 public_api_entry_points: &mut FxHashSet<FileId>,
377 graph: &ModuleGraph,
378 package_root: &std::path::Path,
379 package_json: &PackageJson,
380) {
381 if package_json.private.unwrap_or(false) || package_json.exports.is_some() {
382 return;
383 }
384
385 let mut roots = vec![package_root.to_path_buf()];
386 if let Ok(canonical) = dunce::canonicalize(package_root) {
387 roots.push(canonical);
388 }
389
390 for module in &graph.modules {
391 if roots
392 .iter()
393 .any(|root| is_source_index_under_package(&module.path, root))
394 {
395 public_api_entry_points.insert(module.file_id);
396 }
397 }
398}
399
400fn public_api_package_entry_points(
404 graph: &ModuleGraph,
405 config: &ResolvedConfig,
406 root_pkg: Option<&PackageJson>,
407 workspaces: &[fallow_config::WorkspaceInfo],
408) -> FxHashSet<FileId> {
409 let mut public_api_entry_points = FxHashSet::default();
410 let path_to_file_id = graph_path_to_file_id(graph);
411 let canonical_project_root =
412 dunce::canonicalize(&config.root).unwrap_or_else(|_| config.root.clone());
413
414 add_root_public_api_entry_points(
415 &mut public_api_entry_points,
416 graph,
417 &path_to_file_id,
418 config,
419 root_pkg,
420 &canonical_project_root,
421 );
422 add_workspace_public_api_entry_points(
423 &mut public_api_entry_points,
424 graph,
425 &path_to_file_id,
426 workspaces,
427 &config.public_packages,
428 &canonical_project_root,
429 );
430
431 public_api_entry_points
432}
433
434fn add_root_public_api_entry_points(
435 public_api_entry_points: &mut FxHashSet<FileId>,
436 graph: &ModuleGraph,
437 path_to_file_id: &FxHashMap<std::path::PathBuf, FileId>,
438 config: &ResolvedConfig,
439 root_pkg: Option<&PackageJson>,
440 canonical_project_root: &std::path::Path,
441) {
442 if let Some(pkg) = root_pkg {
443 add_package_public_api_entry_points(
444 public_api_entry_points,
445 graph,
446 path_to_file_id,
447 &config.root,
448 pkg,
449 canonical_project_root,
450 );
451 add_exportless_package_source_indexes(public_api_entry_points, graph, &config.root, pkg);
452 }
453}
454
455fn add_workspace_public_api_entry_points(
456 public_api_entry_points: &mut FxHashSet<FileId>,
457 graph: &ModuleGraph,
458 path_to_file_id: &FxHashMap<std::path::PathBuf, FileId>,
459 workspaces: &[fallow_config::WorkspaceInfo],
460 public_packages: &[String],
461 canonical_project_root: &std::path::Path,
462) {
463 for workspace in workspaces
464 .iter()
465 .filter(|workspace| fallow_config::workspace_is_public(&workspace.name, public_packages))
466 {
467 let Some(pkg) = fallow_config::load_dir_package_json(&workspace.root) else {
468 continue;
469 };
470 add_package_public_api_entry_points(
471 public_api_entry_points,
472 graph,
473 path_to_file_id,
474 &workspace.root,
475 &pkg,
476 canonical_project_root,
477 );
478 add_exportless_package_source_indexes(
479 public_api_entry_points,
480 graph,
481 &workspace.root,
482 &pkg,
483 );
484 }
485}
486
487fn find_circular_dependencies(
488 graph: &ModuleGraph,
489 line_offsets_map: &LineOffsetsMap<'_>,
490 suppressions: &crate::suppress::SuppressionContext<'_>,
491 workspaces: &[fallow_config::WorkspaceInfo],
492) -> Vec<CircularDependency> {
493 let cycles = graph.find_cycles();
494 let mut dependencies: Vec<CircularDependency> = cycles
495 .into_iter()
496 .filter_map(|cycle| {
497 if is_circular_dependency_suppressed(graph, line_offsets_map, suppressions, &cycle) {
498 return None;
499 }
500 Some(circular_dependency_from_cycle(
501 graph,
502 line_offsets_map,
503 &cycle,
504 ))
505 })
506 .collect();
507
508 if !workspaces.is_empty() {
509 for dep in &mut dependencies {
510 dep.is_cross_package = is_cross_package_cycle(&dep.files, workspaces);
511 }
512 }
513
514 dependencies
515}
516
517fn circular_dependency_from_cycle(
518 graph: &ModuleGraph,
519 line_offsets_map: &LineOffsetsMap<'_>,
520 cycle: &[FileId],
521) -> CircularDependency {
522 let edges: Vec<CircularDependencyEdge> = (0..cycle.len())
528 .map(|edge_index| {
529 let from = cycle[edge_index];
530 let (line, col) =
531 cycle_edge_line_col(graph, line_offsets_map, cycle, edge_index).unwrap_or((1, 0));
532 CircularDependencyEdge {
533 path: graph.modules[from.0 as usize].path.clone(),
534 line,
535 col,
536 }
537 })
538 .collect();
539
540 let files: Vec<std::path::PathBuf> = edges.iter().map(|edge| edge.path.clone()).collect();
541 let length = files.len();
542 let (line, col) = edges.first().map_or((1, 0), |edge| (edge.line, edge.col));
545 CircularDependency {
546 files,
547 length,
548 line,
549 col,
550 edges,
551 is_cross_package: false,
552 }
553}
554
555fn run_circular_dep_detector(
560 graph: &ModuleGraph,
561 config: &ResolvedConfig,
562 line_offsets_by_file: &LineOffsetsMap<'_>,
563 suppressions: &crate::suppress::SuppressionContext<'_>,
564 workspaces: &[fallow_config::WorkspaceInfo],
565) -> Vec<CircularDependencyFinding> {
566 if config.rules.circular_dependencies == Severity::Off {
567 return Vec::new();
568 }
569 find_circular_dependencies(graph, line_offsets_by_file, suppressions, workspaces)
570 .into_iter()
571 .map(CircularDependencyFinding::with_actions)
572 .collect()
573}
574
575fn run_boundary_coverage_detector(
580 graph: &ModuleGraph,
581 config: &ResolvedConfig,
582 suppressions: &crate::suppress::SuppressionContext<'_>,
583) -> Vec<BoundaryCoverageViolationFinding> {
584 if config.rules.boundary_violation == Severity::Off {
585 return Vec::new();
586 }
587 boundary_coverage::find_boundary_coverage_violations(graph, config, suppressions)
588 .into_iter()
589 .map(BoundaryCoverageViolationFinding::with_actions)
590 .collect()
591}
592
593fn run_boundary_call_detector(
597 graph: &ModuleGraph,
598 modules: &[ModuleInfo],
599 config: &ResolvedConfig,
600 suppressions: &crate::suppress::SuppressionContext<'_>,
601 line_offsets_by_file: &LineOffsetsMap<'_>,
602) -> Vec<BoundaryCallViolationFinding> {
603 if config.rules.boundary_violation == Severity::Off {
604 return Vec::new();
605 }
606 boundary_calls::find_boundary_call_violations(
607 graph,
608 modules,
609 config,
610 suppressions,
611 line_offsets_by_file,
612 )
613 .into_iter()
614 .map(BoundaryCallViolationFinding::with_actions)
615 .collect()
616}
617
618fn run_policy_detector(
623 graph: &ModuleGraph,
624 modules: &[ModuleInfo],
625 config: &ResolvedConfig,
626 declared_deps: &FxHashSet<String>,
627 suppressions: &crate::suppress::SuppressionContext<'_>,
628 line_offsets_by_file: &LineOffsetsMap<'_>,
629) -> Vec<PolicyViolationFinding> {
630 if config.rules.policy_violation == Severity::Off || config.rule_packs.is_empty() {
631 return Vec::new();
632 }
633 policy::find_policy_violations(
634 graph,
635 modules,
636 config,
637 declared_deps,
638 suppressions,
639 line_offsets_by_file,
640 )
641 .into_iter()
642 .map(PolicyViolationFinding::with_actions)
643 .collect()
644}
645
646fn run_boundary_aux_detectors(
650 graph: &ModuleGraph,
651 modules: &[ModuleInfo],
652 config: &ResolvedConfig,
653 declared_deps: &FxHashSet<String>,
654 suppressions: &crate::suppress::SuppressionContext<'_>,
655 line_offsets_by_file: &LineOffsetsMap<'_>,
656) -> (
657 Vec<BoundaryCoverageViolationFinding>,
658 (
659 Vec<BoundaryCallViolationFinding>,
660 Vec<PolicyViolationFinding>,
661 ),
662) {
663 rayon::join(
664 || run_boundary_coverage_detector(graph, config, suppressions),
665 || {
666 rayon::join(
667 || {
668 run_boundary_call_detector(
669 graph,
670 modules,
671 config,
672 suppressions,
673 line_offsets_by_file,
674 )
675 },
676 || {
677 run_policy_detector(
678 graph,
679 modules,
680 config,
681 declared_deps,
682 suppressions,
683 line_offsets_by_file,
684 )
685 },
686 )
687 },
688 )
689}
690
691fn run_re_export_cycle_detector(
694 graph: &ModuleGraph,
695 config: &ResolvedConfig,
696 suppressions: &crate::suppress::SuppressionContext<'_>,
697) -> Vec<ReExportCycleFinding> {
698 if config.rules.re_export_cycle == Severity::Off {
699 return Vec::new();
700 }
701 find_re_export_cycles(graph, suppressions)
702}
703
704fn run_export_usages_collector(
707 graph: &ModuleGraph,
708 line_offsets_by_file: &LineOffsetsMap<'_>,
709 collect_usages: bool,
710) -> Vec<crate::results::ExportUsage> {
711 if collect_usages {
712 collect_export_usages(graph, line_offsets_by_file)
713 } else {
714 Vec::new()
715 }
716}
717
718fn collect_declared_dependency_names(
725 config: &ResolvedConfig,
726 root_pkg: Option<&PackageJson>,
727 workspaces: &[fallow_config::WorkspaceInfo],
728) -> FxHashSet<String> {
729 let mut deps: FxHashSet<String> = FxHashSet::default();
730 if let Some(pkg) = root_pkg {
731 deps.extend(pkg.all_dependency_names());
732 }
733 for ws in workspaces {
734 if ws.root == config.root {
735 continue; }
737 if let Some(pkg) = fallow_config::load_dir_package_json(&ws.root) {
738 deps.extend(pkg.all_dependency_names());
739 }
740 }
741 deps
742}
743
744struct DeadCodeRunContext<'a> {
745 suppressions: SuppressionContext<'a>,
746 line_offsets_by_file: LineOffsetsMap<'a>,
747 pkg: Option<PackageJson>,
748 public_api_entry_points: FxHashSet<FileId>,
749 declared_deps: FxHashSet<String>,
750}
751
752fn build_dead_code_run_context<'a>(
753 graph: &'a ModuleGraph,
754 config: &ResolvedConfig,
755 workspaces: &[fallow_config::WorkspaceInfo],
756 modules: &'a [ModuleInfo],
757) -> DeadCodeRunContext<'a> {
758 let suppressions = SuppressionContext::new(modules);
759 let line_offsets_by_file: LineOffsetsMap<'a> = modules
760 .iter()
761 .filter(|m| !m.line_offsets.is_empty())
762 .map(|m| (m.file_id, m.line_offsets.as_slice()))
763 .collect();
764
765 let pkg = fallow_config::load_dir_package_json(&config.root);
766 let public_api_entry_points =
767 public_api_package_entry_points(graph, config, pkg.as_ref(), workspaces);
768 let declared_deps = collect_declared_dependency_names(config, pkg.as_ref(), workspaces);
769
770 DeadCodeRunContext {
771 suppressions,
772 line_offsets_by_file,
773 pkg,
774 public_api_entry_points,
775 declared_deps,
776 }
777}
778
779#[deprecated(
781 since = "2.76.0",
782 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."
783)]
784#[expect(
785 clippy::too_many_arguments,
786 reason = "frozen deprecated public API; signature must not change"
787)]
788pub(crate) fn find_dead_code_full(
789 graph: &ModuleGraph,
790 config: &ResolvedConfig,
791 resolved_modules: &[ResolvedModule],
792 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
793 workspaces: &[fallow_config::WorkspaceInfo],
794 modules: &[ModuleInfo],
795 collect_usages: bool,
796) -> AnalysisResults {
797 let _span = tracing::info_span!("find_dead_code").entered();
798
799 fallow_config::clear_analysis_stage_diagnostics(&config.root);
804
805 let run_context = build_dead_code_run_context(graph, config, workspaces, modules);
806
807 let mut results = run_setup_and_detect(&SetupAndDetectInput {
808 graph,
809 config,
810 resolved_modules,
811 plugin_result,
812 workspaces,
813 modules,
814 suppressions: &run_context.suppressions,
815 line_offsets_by_file: &run_context.line_offsets_by_file,
816 pkg: run_context.pkg.as_ref(),
817 public_api_entry_points: &run_context.public_api_entry_points,
818 declared_deps: &run_context.declared_deps,
819 collect_usages,
820 });
821
822 populate_post_detection_findings(&mut PostDetectionInput {
823 graph,
824 modules,
825 resolved_modules,
826 config,
827 workspaces,
828 declared_deps: &run_context.declared_deps,
829 public_api_entry_points: &run_context.public_api_entry_points,
830 suppressions: &run_context.suppressions,
831 line_offsets_by_file: &run_context.line_offsets_by_file,
832 collect_usages,
833 results: &mut results,
834 });
835
836 results.sort();
837
838 results
839}
840
841struct SetupAndDetectInput<'a, 'm> {
844 graph: &'a ModuleGraph,
845 config: &'a ResolvedConfig,
846 resolved_modules: &'a [ResolvedModule],
847 plugin_result: Option<&'a crate::plugins::AggregatedPluginResult>,
848 workspaces: &'a [fallow_config::WorkspaceInfo],
849 modules: &'a [ModuleInfo],
850 suppressions: &'a SuppressionContext<'m>,
851 line_offsets_by_file: &'a LineOffsetsMap<'m>,
852 pkg: Option<&'a PackageJson>,
853 public_api_entry_points: &'a FxHashSet<FileId>,
854 declared_deps: &'a FxHashSet<String>,
855 collect_usages: bool,
856}
857
858fn run_setup_and_detect(input: &SetupAndDetectInput<'_, '_>) -> AnalysisResults {
863 let iconify_referenced =
864 iconify::collect_iconify_referenced_deps(input.modules, input.pkg, input.workspaces);
865 let augmented_plugin_result;
866 let plugin_result = if iconify_referenced.is_empty() {
867 input.plugin_result
868 } else {
869 let mut owned = input.plugin_result.cloned().unwrap_or_default();
870 owned.referenced_dependencies.extend(iconify_referenced);
871 augmented_plugin_result = owned;
872 Some(&augmented_plugin_result)
873 };
874
875 let mut user_class_members = input.config.used_class_members.clone();
876 let mut semantic_framework_candidates = Vec::new();
877 if let Some(plugin_result) = plugin_result {
878 for rule in &plugin_result.used_class_members {
879 if input.config.type_aware.enabled
880 && plugin_result
881 .framework_class_member_contracts
882 .iter()
883 .any(|contract| framework_contract_covers_rule(contract, rule))
884 {
885 semantic_framework_candidates.push(rule.clone());
886 } else {
887 user_class_members.push(rule.clone());
888 }
889 }
890 }
891
892 let (virtual_prefixes, generated_patterns, generated_type_prefixes) =
893 derive_plugin_string_slices(plugin_result);
894
895 let mut results = run_parallel_dead_code_detectors(DeadCodeDetectorInput {
896 graph: input.graph,
897 config: input.config,
898 resolved_modules: input.resolved_modules,
899 workspaces: input.workspaces,
900 modules: input.modules,
901 suppressions: input.suppressions,
902 line_offsets_by_file: input.line_offsets_by_file,
903 plugin_result,
904 pkg: input.pkg,
905 user_class_members: &user_class_members,
906 semantic_framework_candidates: &semantic_framework_candidates,
907 public_api_entry_points: input.public_api_entry_points,
908 virtual_prefixes: &virtual_prefixes,
909 generated_patterns: &generated_patterns,
910 generated_type_prefixes: &generated_type_prefixes,
911 declared_deps: input.declared_deps,
912 collect_usages: input.collect_usages,
913 });
914 if input.config.type_aware.enabled {
915 results.semantic_framework_contracts = plugin_result.map_or_else(Vec::new, |plugins| {
916 plugins.framework_class_member_contracts.clone()
917 });
918 }
919 results
920}
921
922fn framework_contract_covers_rule(
923 contract: &fallow_types::semantic::SemanticFrameworkContract,
924 rule: &fallow_config::UsedClassMemberRule,
925) -> bool {
926 use fallow_types::semantic::SemanticFrameworkRelation;
927
928 let fallow_config::UsedClassMemberRule::Scoped(rule) = rule else {
929 return false;
930 };
931 let heritage_matches =
932 match contract.relation {
933 SemanticFrameworkRelation::Extends => {
934 rule.implements.is_none()
935 && rule.extends.as_ref().is_some_and(|name| {
936 contract.heritage_names.iter().any(|known| known == name)
937 })
938 }
939 SemanticFrameworkRelation::Implements => {
940 rule.extends.is_none()
941 && rule.implements.as_ref().is_some_and(|name| {
942 contract.heritage_names.iter().any(|known| known == name)
943 })
944 }
945 };
946 heritage_matches
947 && rule
948 .members
949 .iter()
950 .all(|member| contract.members.contains(member))
951}
952
953fn derive_plugin_string_slices(
956 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
957) -> (Vec<&str>, Vec<&str>, Vec<&str>) {
958 let virtual_prefixes = plugin_result
959 .map(|pr| {
960 pr.virtual_module_prefixes
961 .iter()
962 .map(String::as_str)
963 .collect()
964 })
965 .unwrap_or_default();
966 let generated_patterns = plugin_result
967 .map(|pr| {
968 pr.generated_import_patterns
969 .iter()
970 .map(String::as_str)
971 .collect()
972 })
973 .unwrap_or_default();
974 let generated_type_prefixes = plugin_result
975 .map(|pr| {
976 pr.generated_type_import_prefixes
977 .iter()
978 .map(String::as_str)
979 .collect()
980 })
981 .unwrap_or_default();
982 (
983 virtual_prefixes,
984 generated_patterns,
985 generated_type_prefixes,
986 )
987}
988
989struct PostDetectionInput<'a, 'm> {
992 graph: &'a ModuleGraph,
993 modules: &'a [ModuleInfo],
994 resolved_modules: &'a [ResolvedModule],
995 config: &'a ResolvedConfig,
996 workspaces: &'a [fallow_config::WorkspaceInfo],
997 declared_deps: &'a FxHashSet<String>,
998 public_api_entry_points: &'a FxHashSet<FileId>,
999 suppressions: &'a SuppressionContext<'m>,
1000 line_offsets_by_file: &'a LineOffsetsMap<'m>,
1001 collect_usages: bool,
1004 results: &'a mut AnalysisResults,
1005}
1006
1007fn populate_post_detection_findings(input: &mut PostDetectionInput<'_, '_>) {
1012 filter_public_workspace_results(input.config, input.workspaces, input.results);
1013
1014 if input.config.rules.unused_server_actions != Severity::Off {
1018 reclassify_unused_server_actions(
1019 input.graph,
1020 input.modules,
1021 input.declared_deps,
1022 input.suppressions,
1023 input.results,
1024 );
1025 }
1026
1027 populate_configured_security_findings(input);
1028 populate_package_and_framework_findings(input);
1029 populate_stale_suppression_findings(input);
1030}
1031
1032fn populate_configured_security_findings(input: &mut PostDetectionInput<'_, '_>) {
1033 let request_receivers = input
1034 .config
1035 .security
1036 .request_receivers
1037 .iter()
1038 .cloned()
1039 .collect::<FxHashSet<_>>();
1040
1041 populate_security_findings(
1042 &SecurityDetectionContext {
1043 graph: input.graph,
1044 modules: input.modules,
1045 config: input.config,
1046 suppressions: input.suppressions,
1047 line_offsets_by_file: input.line_offsets_by_file,
1048 declared_deps: input.declared_deps,
1049 request_receivers: &request_receivers,
1050 },
1051 input.results,
1052 );
1053}
1054
1055fn populate_package_and_framework_findings(input: &mut PostDetectionInput<'_, '_>) {
1056 populate_pnpm_catalog_findings(input.config, input.workspaces, input.results);
1062 populate_pnpm_override_findings(input.config, input.workspaces, input.results);
1063 populate_framework_specific_findings(&mut FrameworkSpecificFindingsInput {
1064 graph: input.graph,
1065 modules: input.modules,
1066 resolved_modules: input.resolved_modules,
1067 config: input.config,
1068 workspaces: input.workspaces,
1069 declared_deps: input.declared_deps,
1070 public_api_entry_points: input.public_api_entry_points,
1071 suppressions: input.suppressions,
1072 line_offsets_by_file: input.line_offsets_by_file,
1073 collect_usages: input.collect_usages,
1074 results: input.results,
1075 });
1076}
1077
1078fn populate_stale_suppression_findings(input: &mut PostDetectionInput<'_, '_>) {
1081 if input.config.rules.stale_suppressions != Severity::Off {
1082 input
1083 .results
1084 .stale_suppressions
1085 .extend(input.suppressions.find_stale(input.graph, input.config));
1086 }
1087 if input.config.rules.require_suppression_reason != Severity::Off {
1088 input
1089 .results
1090 .stale_suppressions
1091 .extend(input.suppressions.find_missing_reasons(input.graph));
1092 }
1093 input.results.suppression_count = input.suppressions.used_count();
1094 input.results.active_suppressions = input.suppressions.all_suppressions(input.graph);
1095}
1096
1097struct FrameworkSpecificFindingsInput<'a> {
1103 graph: &'a ModuleGraph,
1104 modules: &'a [ModuleInfo],
1105 resolved_modules: &'a [ResolvedModule],
1106 config: &'a ResolvedConfig,
1107 workspaces: &'a [fallow_config::WorkspaceInfo],
1108 declared_deps: &'a FxHashSet<String>,
1109 public_api_entry_points: &'a FxHashSet<FileId>,
1110 suppressions: &'a SuppressionContext<'a>,
1111 line_offsets_by_file: &'a LineOffsetsMap<'a>,
1112 collect_usages: bool,
1115 results: &'a mut AnalysisResults,
1116}
1117
1118fn populate_framework_specific_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1119 populate_client_boundary_findings(input);
1120 populate_component_contract_findings(input);
1121 populate_react_health_findings(input);
1122 populate_nextjs_findings(input);
1123}
1124
1125fn populate_client_boundary_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1126 populate_invalid_client_export_findings(input);
1127 populate_mixed_client_server_barrel_findings(input);
1128 populate_misplaced_directive_findings(input);
1129}
1130
1131fn populate_component_contract_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1132 populate_unprovided_inject_findings(input);
1133 populate_unrendered_component_findings(input);
1134 populate_unused_component_prop_findings(input);
1135 populate_unused_component_emit_findings(
1136 input.graph,
1137 input.modules,
1138 input.config,
1139 input.declared_deps,
1140 input.line_offsets_by_file,
1141 input.results,
1142 );
1143 populate_unused_component_input_findings(
1144 input.graph,
1145 input.modules,
1146 input.config,
1147 input.declared_deps,
1148 input.line_offsets_by_file,
1149 input.results,
1150 );
1151 populate_unused_component_output_findings(
1152 input.graph,
1153 input.modules,
1154 input.config,
1155 input.declared_deps,
1156 input.line_offsets_by_file,
1157 input.results,
1158 );
1159 populate_unused_svelte_event_findings(
1160 input.graph,
1161 input.modules,
1162 input.config,
1163 input.declared_deps,
1164 input.line_offsets_by_file,
1165 input.results,
1166 );
1167 populate_unused_load_data_key_findings(input);
1168}
1169
1170fn populate_react_health_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1171 populate_prop_drilling_findings(input);
1172 populate_thin_wrapper_findings(input);
1173 populate_render_fan_in(input);
1174 populate_react_component_intel(input);
1175 populate_duplicate_prop_shape_findings(input);
1176}
1177
1178fn populate_nextjs_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1179 populate_nextjs_route_tree_findings(
1180 input.graph,
1181 input.config,
1182 input.workspaces,
1183 input.declared_deps,
1184 input.suppressions,
1185 input.results,
1186 );
1187}
1188
1189fn populate_render_fan_in(input: &mut FrameworkSpecificFindingsInput<'_>) {
1197 input.results.render_fan_in = compute_render_fan_in(
1198 input.graph,
1199 input.modules,
1200 input.resolved_modules,
1201 input.declared_deps,
1202 &input.config.root,
1203 );
1204}
1205
1206fn populate_react_component_intel(input: &mut FrameworkSpecificFindingsInput<'_>) {
1216 if !input.collect_usages {
1217 return;
1218 }
1219 input.results.react_component_intel = compute_react_component_intel(
1220 input.graph,
1221 input.modules,
1222 input.resolved_modules,
1223 input.declared_deps,
1224 &input.config.root,
1225 input.line_offsets_by_file,
1226 );
1227}
1228
1229fn populate_unused_load_data_key_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1234 if input.config.rules.unused_load_data_keys == Severity::Off {
1235 return;
1236 }
1237 let result = find_unused_load_data_keys(
1238 input.graph,
1239 input.modules,
1240 input.declared_deps,
1241 input.suppressions,
1242 input.line_offsets_by_file,
1243 &input.config.root,
1244 );
1245 if result.global_abstain {
1246 input.results.unused_load_data_keys_global_abstain = true;
1247 tracing::debug!(
1248 "unused-load-data-key: abstained project-wide (a whole-object use of \
1249 page.data / $page.data was seen; any key could be read reflectively)"
1250 );
1251 }
1252 input.results.unused_load_data_keys = result
1253 .findings
1254 .into_iter()
1255 .map(UnusedLoadDataKeyFinding::with_actions)
1256 .collect();
1257}
1258
1259fn populate_invalid_client_export_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1263 if input.config.rules.invalid_client_export == Severity::Off {
1264 return;
1265 }
1266 input.results.invalid_client_exports = find_invalid_client_exports(
1267 input.graph,
1268 input.modules,
1269 input.declared_deps,
1270 input.suppressions,
1271 input.line_offsets_by_file,
1272 )
1273 .into_iter()
1274 .map(InvalidClientExportFinding::with_actions)
1275 .collect();
1276}
1277
1278fn populate_mixed_client_server_barrel_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1282 if input.config.rules.mixed_client_server_barrel == Severity::Off {
1283 return;
1284 }
1285 input.results.mixed_client_server_barrels = find_mixed_client_server_barrels(
1286 input.graph,
1287 input.modules,
1288 input.resolved_modules,
1289 input.declared_deps,
1290 input.suppressions,
1291 input.line_offsets_by_file,
1292 )
1293 .into_iter()
1294 .map(MixedClientServerBarrelFinding::with_actions)
1295 .collect();
1296}
1297
1298fn populate_misplaced_directive_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1302 if input.config.rules.misplaced_directive == Severity::Off {
1303 return;
1304 }
1305 input.results.misplaced_directives = find_misplaced_directives(
1306 input.graph,
1307 input.modules,
1308 input.declared_deps,
1309 input.suppressions,
1310 input.line_offsets_by_file,
1311 )
1312 .into_iter()
1313 .map(MisplacedDirectiveFinding::with_actions)
1314 .collect();
1315}
1316
1317fn populate_unprovided_inject_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1321 if input.config.rules.unprovided_injects == Severity::Off {
1322 return;
1323 }
1324 input.results.unprovided_injects = find_unprovided_injects(UnprovidedInjectInput {
1325 graph: input.graph,
1326 resolved_modules: input.resolved_modules,
1327 modules: input.modules,
1328 declared_deps: input.declared_deps,
1329 public_api_entry_points: input.public_api_entry_points,
1330 suppressions: input.suppressions,
1331 line_offsets_by_file: input.line_offsets_by_file,
1332 })
1333 .into_iter()
1334 .map(UnprovidedInjectFinding::with_actions)
1335 .collect();
1336}
1337
1338fn populate_unrendered_component_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1342 if input.config.rules.unrendered_components == Severity::Off {
1343 return;
1344 }
1345 input.results.unrendered_components = find_unrendered_components(
1346 input.graph,
1347 input.resolved_modules,
1348 input.modules,
1349 input.declared_deps,
1350 input.public_api_entry_points,
1351 input.suppressions,
1352 )
1353 .into_iter()
1354 .map(UnrenderedComponentFinding::with_actions)
1355 .collect();
1356 input.results.unrendered_components.extend(
1361 find_unrendered_angular_components(
1362 input.graph,
1363 input.modules,
1364 input.declared_deps,
1365 input.public_api_entry_points,
1366 input.line_offsets_by_file,
1367 input.suppressions,
1368 )
1369 .into_iter()
1370 .map(UnrenderedComponentFinding::with_actions),
1371 );
1372 input.results.unrendered_components.extend(
1377 find_unrendered_lit_elements(&LitUnrenderedInput {
1378 graph: input.graph,
1379 modules: input.modules,
1380 declared_deps: input.declared_deps,
1381 public_api_entry_points: input.public_api_entry_points,
1382 line_offsets_by_file: input.line_offsets_by_file,
1383 suppressions: input.suppressions,
1384 root: &input.config.root,
1385 })
1386 .into_iter()
1387 .map(UnrenderedComponentFinding::with_actions),
1388 );
1389}
1390
1391fn populate_unused_component_prop_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1395 if input.config.rules.unused_component_props == Severity::Off {
1396 return;
1397 }
1398 let sfc = find_unused_component_props(
1400 input.graph,
1401 input.modules,
1402 input.declared_deps,
1403 input.line_offsets_by_file,
1404 input.config.unused_component_props_ignore.as_ref(),
1405 );
1406 input.results.unused_component_props_exempted += sfc.exempted;
1407 input.results.unused_component_props = sfc
1408 .findings
1409 .into_iter()
1410 .map(UnusedComponentPropFinding::with_actions)
1411 .collect();
1412
1413 append_react_unused_component_prop_findings(input);
1414 retain_unsuppressed_unused_component_prop_findings(input);
1415}
1416
1417fn append_react_unused_component_prop_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1418 let react = find_unused_react_props(
1422 input.graph,
1423 input.modules,
1424 input.declared_deps,
1425 input.line_offsets_by_file,
1426 input.config.unused_component_props_ignore.as_ref(),
1427 );
1428 input.results.unused_component_props_exempted += react.exempted;
1429 if react.components_scanned > 0 {
1430 tracing::info!(
1434 components_scanned = react.components_scanned,
1435 unused_props = react.findings.len(),
1436 "React detected, {} component(s) scanned for unused props",
1437 react.components_scanned
1438 );
1439 }
1440 input.results.unused_component_props.extend(
1441 react
1442 .findings
1443 .into_iter()
1444 .map(UnusedComponentPropFinding::with_actions),
1445 );
1446}
1447
1448fn retain_unsuppressed_unused_component_prop_findings(
1449 input: &mut FrameworkSpecificFindingsInput<'_>,
1450) {
1451 let path_to_id = graph_file_ids_by_path(input.graph);
1457 input.results.unused_component_props.retain(|finding| {
1458 !path_line_is_suppressed(
1459 &path_to_id,
1460 input.suppressions,
1461 finding.prop.path.as_path(),
1462 finding.prop.line,
1463 IssueKind::UnusedComponentProp,
1464 )
1465 });
1466}
1467
1468fn populate_unused_component_emit_findings(
1472 graph: &ModuleGraph,
1473 modules: &[ModuleInfo],
1474 config: &ResolvedConfig,
1475 declared_deps: &FxHashSet<String>,
1476 line_offsets_by_file: &LineOffsetsMap<'_>,
1477 results: &mut AnalysisResults,
1478) {
1479 if config.rules.unused_component_emits == Severity::Off {
1480 return;
1481 }
1482 results.unused_component_emits =
1483 find_unused_component_emits(graph, modules, declared_deps, line_offsets_by_file)
1484 .into_iter()
1485 .map(UnusedComponentEmitFinding::with_actions)
1486 .collect();
1487}
1488
1489fn populate_prop_drilling_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1496 if input.config.rules.prop_drilling == Severity::Off {
1497 return;
1498 }
1499 input.results.prop_drilling_chains = collect_prop_drilling_findings(input);
1500
1501 retain_unsuppressed_prop_drilling_findings(input);
1502}
1503
1504fn collect_prop_drilling_findings(
1505 input: &FrameworkSpecificFindingsInput<'_>,
1506) -> Vec<PropDrillingChainFinding> {
1507 let scan = find_prop_drilling_chains(
1508 input.graph,
1509 input.modules,
1510 input.resolved_modules,
1511 input.declared_deps,
1512 input.line_offsets_by_file,
1513 );
1514 if scan.components_scanned > 0 {
1515 tracing::info!(
1517 components_scanned = scan.components_scanned,
1518 prop_drilling_chains = scan.chains.len(),
1519 "React detected, {} component(s) scanned for prop drilling",
1520 scan.components_scanned
1521 );
1522 }
1523 scan.chains
1524 .into_iter()
1525 .map(PropDrillingChainFinding::with_actions)
1526 .collect()
1527}
1528
1529fn retain_unsuppressed_prop_drilling_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1530 let path_to_id = graph_file_ids_by_path(input.graph);
1536 input.results.prop_drilling_chains.retain(|finding| {
1537 let Some(source) = finding.chain.hops.first() else {
1538 return true;
1539 };
1540 !path_line_is_suppressed(
1541 &path_to_id,
1542 input.suppressions,
1543 source.file.as_path(),
1544 source.line,
1545 IssueKind::PropDrilling,
1546 )
1547 });
1548}
1549
1550fn populate_thin_wrapper_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1556 if input.config.rules.thin_wrapper == Severity::Off {
1557 return;
1558 }
1559 input.results.thin_wrappers = collect_thin_wrapper_findings(input);
1560
1561 retain_unsuppressed_thin_wrapper_findings(input);
1562}
1563
1564fn collect_thin_wrapper_findings(
1565 input: &FrameworkSpecificFindingsInput<'_>,
1566) -> Vec<ThinWrapperFinding> {
1567 let scan = find_thin_wrappers(
1568 input.graph,
1569 input.modules,
1570 input.resolved_modules,
1571 input.declared_deps,
1572 input.line_offsets_by_file,
1573 );
1574 if scan.components_scanned > 0 {
1575 tracing::info!(
1577 components_scanned = scan.components_scanned,
1578 thin_wrappers = scan.wrappers.len(),
1579 "React detected, {} component(s) scanned for thin wrappers",
1580 scan.components_scanned
1581 );
1582 }
1583 scan.wrappers
1584 .into_iter()
1585 .map(ThinWrapperFinding::with_actions)
1586 .collect()
1587}
1588
1589fn retain_unsuppressed_thin_wrapper_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1590 let path_to_id = graph_file_ids_by_path(input.graph);
1596 input.results.thin_wrappers.retain(|finding| {
1597 !path_line_is_suppressed(
1598 &path_to_id,
1599 input.suppressions,
1600 finding.wrapper.file.as_path(),
1601 finding.wrapper.line,
1602 IssueKind::ThinWrapper,
1603 )
1604 });
1605}
1606
1607fn populate_duplicate_prop_shape_findings(input: &mut FrameworkSpecificFindingsInput<'_>) {
1622 if input.config.rules.duplicate_prop_shape == Severity::Off {
1623 return;
1624 }
1625 let scan = find_duplicate_prop_shapes(
1626 input.graph,
1627 input.modules,
1628 input.declared_deps,
1629 input.line_offsets_by_file,
1630 );
1631 if scan.components_scanned > 0 {
1632 tracing::info!(
1634 components_scanned = scan.components_scanned,
1635 duplicate_prop_shapes = scan.groups.len(),
1636 "React detected, {} component(s) scanned for duplicate prop shapes",
1637 scan.components_scanned
1638 );
1639 }
1640 input.results.duplicate_prop_shapes = scan
1641 .groups
1642 .into_iter()
1643 .map(DuplicatePropShapeFinding::with_actions)
1644 .collect();
1645
1646 let path_to_id = graph_file_ids_by_path(input.graph);
1651 input.results.duplicate_prop_shapes.retain(|finding| {
1652 !path_line_is_suppressed(
1653 &path_to_id,
1654 input.suppressions,
1655 finding.shape.file.as_path(),
1656 finding.shape.line,
1657 IssueKind::DuplicatePropShape,
1658 )
1659 });
1660}
1661
1662fn graph_file_ids_by_path(graph: &ModuleGraph) -> FxHashMap<&std::path::Path, FileId> {
1663 graph
1664 .modules
1665 .iter()
1666 .map(|node| (node.path.as_path(), node.file_id))
1667 .collect()
1668}
1669
1670fn path_line_is_suppressed(
1671 path_to_id: &FxHashMap<&std::path::Path, FileId>,
1672 suppressions: &SuppressionContext<'_>,
1673 path: &std::path::Path,
1674 line: u32,
1675 kind: IssueKind,
1676) -> bool {
1677 let Some(&file_id) = path_to_id.get(path) else {
1678 return false;
1679 };
1680 suppressions.is_suppressed(file_id, line, kind)
1681 || suppressions.is_file_suppressed(file_id, kind)
1682}
1683
1684fn populate_unused_component_input_findings(
1688 graph: &ModuleGraph,
1689 modules: &[ModuleInfo],
1690 config: &ResolvedConfig,
1691 declared_deps: &FxHashSet<String>,
1692 line_offsets_by_file: &LineOffsetsMap<'_>,
1693 results: &mut AnalysisResults,
1694) {
1695 if config.rules.unused_component_inputs == Severity::Off {
1696 return;
1697 }
1698 results.unused_component_inputs =
1699 find_unused_component_inputs(graph, modules, declared_deps, line_offsets_by_file)
1700 .into_iter()
1701 .map(UnusedComponentInputFinding::with_actions)
1702 .collect();
1703}
1704
1705fn populate_unused_component_output_findings(
1709 graph: &ModuleGraph,
1710 modules: &[ModuleInfo],
1711 config: &ResolvedConfig,
1712 declared_deps: &FxHashSet<String>,
1713 line_offsets_by_file: &LineOffsetsMap<'_>,
1714 results: &mut AnalysisResults,
1715) {
1716 if config.rules.unused_component_outputs == Severity::Off {
1717 return;
1718 }
1719 results.unused_component_outputs =
1720 find_unused_component_outputs(graph, modules, declared_deps, line_offsets_by_file)
1721 .into_iter()
1722 .map(UnusedComponentOutputFinding::with_actions)
1723 .collect();
1724}
1725
1726fn populate_unused_svelte_event_findings(
1730 graph: &ModuleGraph,
1731 modules: &[ModuleInfo],
1732 config: &ResolvedConfig,
1733 declared_deps: &FxHashSet<String>,
1734 line_offsets_by_file: &LineOffsetsMap<'_>,
1735 results: &mut AnalysisResults,
1736) {
1737 if config.rules.unused_svelte_events == Severity::Off {
1738 return;
1739 }
1740 results.unused_svelte_events =
1741 find_unused_svelte_events(graph, modules, declared_deps, line_offsets_by_file)
1742 .into_iter()
1743 .map(UnusedSvelteEventFinding::with_actions)
1744 .collect();
1745}
1746
1747fn populate_route_collision_findings(
1750 graph: &ModuleGraph,
1751 config: &ResolvedConfig,
1752 workspaces: &[fallow_config::WorkspaceInfo],
1753 declared_deps: &FxHashSet<String>,
1754 suppressions: &SuppressionContext<'_>,
1755 results: &mut AnalysisResults,
1756) {
1757 if config.rules.route_collision == Severity::Off {
1758 return;
1759 }
1760 results.route_collisions =
1761 find_route_collisions(graph, config, workspaces, declared_deps, suppressions)
1762 .into_iter()
1763 .map(RouteCollisionFinding::with_actions)
1764 .collect();
1765}
1766
1767fn populate_dynamic_segment_name_conflict_findings(
1771 graph: &ModuleGraph,
1772 config: &ResolvedConfig,
1773 workspaces: &[fallow_config::WorkspaceInfo],
1774 declared_deps: &FxHashSet<String>,
1775 suppressions: &SuppressionContext<'_>,
1776 results: &mut AnalysisResults,
1777) {
1778 if config.rules.dynamic_segment_name_conflict == Severity::Off {
1779 return;
1780 }
1781 results.dynamic_segment_name_conflicts =
1782 find_dynamic_segment_name_conflicts(graph, config, workspaces, declared_deps, suppressions)
1783 .into_iter()
1784 .map(DynamicSegmentNameConflictFinding::with_actions)
1785 .collect();
1786}
1787
1788fn populate_nextjs_route_tree_findings(
1793 graph: &ModuleGraph,
1794 config: &ResolvedConfig,
1795 workspaces: &[fallow_config::WorkspaceInfo],
1796 declared_deps: &FxHashSet<String>,
1797 suppressions: &SuppressionContext<'_>,
1798 results: &mut AnalysisResults,
1799) {
1800 populate_route_collision_findings(
1801 graph,
1802 config,
1803 workspaces,
1804 declared_deps,
1805 suppressions,
1806 results,
1807 );
1808 populate_dynamic_segment_name_conflict_findings(
1809 graph,
1810 config,
1811 workspaces,
1812 declared_deps,
1813 suppressions,
1814 results,
1815 );
1816}
1817
1818#[derive(Clone, Copy)]
1819struct DeadCodeDetectorInput<'a> {
1820 graph: &'a ModuleGraph,
1821 config: &'a ResolvedConfig,
1822 resolved_modules: &'a [ResolvedModule],
1823 workspaces: &'a [fallow_config::WorkspaceInfo],
1824 modules: &'a [ModuleInfo],
1825 suppressions: &'a SuppressionContext<'a>,
1826 line_offsets_by_file: &'a LineOffsetsMap<'a>,
1827 plugin_result: Option<&'a crate::plugins::AggregatedPluginResult>,
1828 pkg: Option<&'a PackageJson>,
1829 user_class_members: &'a [fallow_config::UsedClassMemberRule],
1830 semantic_framework_candidates: &'a [fallow_config::UsedClassMemberRule],
1831 public_api_entry_points: &'a FxHashSet<FileId>,
1832 virtual_prefixes: &'a [&'a str],
1833 generated_patterns: &'a [&'a str],
1834 generated_type_prefixes: &'a [&'a str],
1835 declared_deps: &'a FxHashSet<String>,
1836 collect_usages: bool,
1837}
1838
1839struct ParallelDeadCodeDetectorResults {
1840 unused_files: Vec<UnusedFileFinding>,
1841 export_results: AnalysisResults,
1842 member_results: AnalysisResults,
1843 dependency_results: AnalysisResults,
1844 unresolved_imports: Vec<UnresolvedImportFinding>,
1845 duplicate_exports: Vec<DuplicateExportFinding>,
1846 boundary_violations: Vec<BoundaryViolationFinding>,
1847 boundary_coverage_violations: Vec<BoundaryCoverageViolationFinding>,
1848 boundary_call_violations: Vec<BoundaryCallViolationFinding>,
1849 policy_violations: Vec<PolicyViolationFinding>,
1850 circular_dependencies: Vec<CircularDependencyFinding>,
1851 re_export_cycles: Vec<ReExportCycleFinding>,
1852 export_usages: Vec<crate::results::ExportUsage>,
1853}
1854
1855impl ParallelDeadCodeDetectorResults {
1856 fn into_analysis_results(self) -> AnalysisResults {
1857 AnalysisResults {
1858 unused_files: self.unused_files,
1859 unused_exports: self.export_results.unused_exports,
1860 unused_types: self.export_results.unused_types,
1861 private_type_leaks: self.export_results.private_type_leaks,
1862 stale_suppressions: self.export_results.stale_suppressions,
1863 unused_enum_members: self.member_results.unused_enum_members,
1864 unused_class_members: self.member_results.unused_class_members,
1865 unused_store_members: self.member_results.unused_store_members,
1866 unused_dependencies: self.dependency_results.unused_dependencies,
1867 unused_dev_dependencies: self.dependency_results.unused_dev_dependencies,
1868 unused_optional_dependencies: self.dependency_results.unused_optional_dependencies,
1869 unlisted_dependencies: self.dependency_results.unlisted_dependencies,
1870 type_only_dependencies: self.dependency_results.type_only_dependencies,
1871 test_only_dependencies: self.dependency_results.test_only_dependencies,
1872 dev_dependencies_in_production: self.dependency_results.dev_dependencies_in_production,
1873 unresolved_imports: self.unresolved_imports,
1874 duplicate_exports: self.duplicate_exports,
1875 boundary_violations: self.boundary_violations,
1876 boundary_coverage_violations: self.boundary_coverage_violations,
1877 boundary_call_violations: self.boundary_call_violations,
1878 policy_violations: self.policy_violations,
1879 circular_dependencies: self.circular_dependencies,
1880 re_export_cycles: self.re_export_cycles,
1881 export_usages: self.export_usages,
1882 ..AnalysisResults::default()
1883 }
1884 }
1885}
1886
1887fn run_parallel_dead_code_detectors(input: DeadCodeDetectorInput<'_>) -> AnalysisResults {
1888 collect_parallel_dead_code_detector_results(input).into_analysis_results()
1889}
1890
1891fn collect_parallel_dead_code_detector_results(
1892 input: DeadCodeDetectorInput<'_>,
1893) -> ParallelDeadCodeDetectorResults {
1894 let (
1895 (unused_files, export_results),
1896 (
1897 (member_results, dependency_results),
1898 (
1899 (unresolved_imports, duplicate_exports),
1900 (
1901 (
1902 boundary_violations,
1903 (
1904 boundary_coverage_violations,
1905 (boundary_call_violations, policy_violations),
1906 ),
1907 ),
1908 (circular_dependencies, (re_export_cycles, export_usages)),
1909 ),
1910 ),
1911 ),
1912 ) = rayon::join(
1913 || run_file_and_export_detectors(input),
1914 || {
1915 rayon::join(
1916 || run_member_and_dependency_detectors(input),
1917 || {
1918 rayon::join(
1919 || run_import_and_duplicate_detectors(input),
1920 || run_boundary_cycle_and_usage_detectors(input),
1921 )
1922 },
1923 )
1924 },
1925 );
1926
1927 ParallelDeadCodeDetectorResults {
1928 unused_files,
1929 export_results,
1930 member_results,
1931 dependency_results,
1932 unresolved_imports,
1933 duplicate_exports,
1934 boundary_violations,
1935 boundary_coverage_violations,
1936 boundary_call_violations,
1937 policy_violations,
1938 circular_dependencies,
1939 re_export_cycles,
1940 export_usages,
1941 }
1942}
1943
1944fn run_file_and_export_detectors(
1945 input: DeadCodeDetectorInput<'_>,
1946) -> (Vec<UnusedFileFinding>, AnalysisResults) {
1947 rayon::join(
1948 || run_unused_file_detector(input.graph, input.config, input.suppressions),
1949 || {
1950 run_export_detectors(
1951 input.graph,
1952 input.modules,
1953 input.config,
1954 input.plugin_result,
1955 input.suppressions,
1956 input.line_offsets_by_file,
1957 )
1958 },
1959 )
1960}
1961
1962fn run_member_and_dependency_detectors(
1963 input: DeadCodeDetectorInput<'_>,
1964) -> (AnalysisResults, AnalysisResults) {
1965 rayon::join(
1966 || {
1967 run_member_detectors(MemberDetectorInput {
1968 graph: input.graph,
1969 resolved_modules: input.resolved_modules,
1970 modules: input.modules,
1971 config: input.config,
1972 suppressions: input.suppressions,
1973 line_offsets_by_file: input.line_offsets_by_file,
1974 user_class_members: input.user_class_members,
1975 semantic_framework_candidates: input.semantic_framework_candidates,
1976 public_api_entry_points: input.public_api_entry_points,
1977 declared_deps: input.declared_deps,
1978 })
1979 },
1980 || {
1981 run_dependency_detectors(DependencyDetectorInput {
1982 graph: input.graph,
1983 pkg: input.pkg,
1984 config: input.config,
1985 plugin_result: input.plugin_result,
1986 workspaces: input.workspaces,
1987 resolved_modules: input.resolved_modules,
1988 line_offsets_by_file: input.line_offsets_by_file,
1989 })
1990 },
1991 )
1992}
1993
1994fn run_import_and_duplicate_detectors(
1995 input: DeadCodeDetectorInput<'_>,
1996) -> (Vec<UnresolvedImportFinding>, Vec<DuplicateExportFinding>) {
1997 rayon::join(
1998 || {
1999 run_unresolved_import_detector(UnresolvedImportDetectorInput {
2000 resolved_modules: input.resolved_modules,
2001 config: input.config,
2002 suppressions: input.suppressions,
2003 virtual_prefixes: input.virtual_prefixes,
2004 generated_patterns: input.generated_patterns,
2005 generated_type_prefixes: input.generated_type_prefixes,
2006 line_offsets_by_file: input.line_offsets_by_file,
2007 })
2008 },
2009 || {
2010 run_duplicate_export_detector(
2011 input.graph,
2012 input.config,
2013 input.suppressions,
2014 input.line_offsets_by_file,
2015 input.plugin_result,
2016 input.resolved_modules,
2017 )
2018 },
2019 )
2020}
2021
2022type BoundaryAuxResults = (
2023 Vec<BoundaryCoverageViolationFinding>,
2024 (
2025 Vec<BoundaryCallViolationFinding>,
2026 Vec<PolicyViolationFinding>,
2027 ),
2028);
2029
2030type BoundaryCycleUsageResults = (
2031 (Vec<BoundaryViolationFinding>, BoundaryAuxResults),
2032 (
2033 Vec<CircularDependencyFinding>,
2034 (Vec<ReExportCycleFinding>, Vec<crate::results::ExportUsage>),
2035 ),
2036);
2037
2038fn run_boundary_cycle_and_usage_detectors(
2039 input: DeadCodeDetectorInput<'_>,
2040) -> BoundaryCycleUsageResults {
2041 rayon::join(
2042 || run_boundary_detectors(input),
2043 || run_cycle_and_usage_detectors(input),
2044 )
2045}
2046
2047fn run_boundary_detectors(
2048 input: DeadCodeDetectorInput<'_>,
2049) -> (Vec<BoundaryViolationFinding>, BoundaryAuxResults) {
2050 rayon::join(
2051 || {
2052 run_boundary_violation_detector(
2053 input.graph,
2054 input.config,
2055 input.suppressions,
2056 input.line_offsets_by_file,
2057 )
2058 },
2059 || {
2060 run_boundary_aux_detectors(
2061 input.graph,
2062 input.modules,
2063 input.config,
2064 input.declared_deps,
2065 input.suppressions,
2066 input.line_offsets_by_file,
2067 )
2068 },
2069 )
2070}
2071
2072fn run_cycle_and_usage_detectors(
2073 input: DeadCodeDetectorInput<'_>,
2074) -> (
2075 Vec<CircularDependencyFinding>,
2076 (Vec<ReExportCycleFinding>, Vec<crate::results::ExportUsage>),
2077) {
2078 rayon::join(
2079 || {
2080 run_circular_dep_detector(
2081 input.graph,
2082 input.config,
2083 input.line_offsets_by_file,
2084 input.suppressions,
2085 input.workspaces,
2086 )
2087 },
2088 || {
2089 rayon::join(
2090 || run_re_export_cycle_detector(input.graph, input.config, input.suppressions),
2091 || {
2092 run_export_usages_collector(
2093 input.graph,
2094 input.line_offsets_by_file,
2095 input.collect_usages,
2096 )
2097 },
2098 )
2099 },
2100 )
2101}
2102
2103#[expect(
2104 deprecated,
2105 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2106)]
2107fn run_duplicate_export_detector(
2108 graph: &ModuleGraph,
2109 config: &ResolvedConfig,
2110 suppressions: &SuppressionContext<'_>,
2111 line_offsets_by_file: &LineOffsetsMap<'_>,
2112 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
2113 resolved_modules: &[ResolvedModule],
2114) -> Vec<DuplicateExportFinding> {
2115 if config.rules.duplicate_exports == Severity::Off {
2116 return Vec::new();
2117 }
2118 let duplicate_exports = if let Some(plugin_result) = plugin_result {
2119 unused_exports::find_duplicate_exports_with_plugins(
2120 graph,
2121 config,
2122 suppressions,
2123 line_offsets_by_file,
2124 Some(plugin_result),
2125 resolved_modules,
2126 )
2127 } else {
2128 unused_exports::find_duplicate_exports(
2129 graph,
2130 config,
2131 suppressions,
2132 line_offsets_by_file,
2133 resolved_modules,
2134 )
2135 };
2136 duplicate_exports
2137 .into_iter()
2138 .map(DuplicateExportFinding::with_actions)
2139 .collect()
2140}
2141
2142#[expect(
2143 deprecated,
2144 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2145)]
2146fn run_boundary_violation_detector(
2147 graph: &ModuleGraph,
2148 config: &ResolvedConfig,
2149 suppressions: &SuppressionContext<'_>,
2150 line_offsets_by_file: &LineOffsetsMap<'_>,
2151) -> Vec<BoundaryViolationFinding> {
2152 if config.rules.boundary_violation == Severity::Off || config.boundaries.is_empty() {
2153 return Vec::new();
2154 }
2155 boundary::find_boundary_violations(graph, config, suppressions, line_offsets_by_file)
2156 .into_iter()
2157 .map(BoundaryViolationFinding::with_actions)
2158 .collect()
2159}
2160
2161fn filter_public_workspace_results(
2162 config: &ResolvedConfig,
2163 workspaces: &[fallow_config::WorkspaceInfo],
2164 results: &mut AnalysisResults,
2165) {
2166 let public_roots = public_workspace_roots(&config.public_packages, workspaces);
2167 if public_roots.is_empty() {
2168 return;
2169 }
2170 results.unused_exports.retain(|e| {
2171 !public_roots
2172 .iter()
2173 .any(|root| e.export.path.starts_with(root))
2174 });
2175 results.unused_types.retain(|e| {
2176 !public_roots
2177 .iter()
2178 .any(|root| e.export.path.starts_with(root))
2179 });
2180 results.unused_enum_members.retain(|e| {
2181 !public_roots
2182 .iter()
2183 .any(|root| e.member.path.starts_with(root))
2184 });
2185 results.unused_class_members.retain(|e| {
2186 !public_roots
2187 .iter()
2188 .any(|root| e.member.path.starts_with(root))
2189 });
2190}
2191
2192#[expect(
2193 deprecated,
2194 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2195)]
2196fn populate_pnpm_catalog_findings(
2197 config: &ResolvedConfig,
2198 workspaces: &[fallow_config::WorkspaceInfo],
2199 results: &mut AnalysisResults,
2200) {
2201 let need_unused = config.rules.unused_catalog_entries != Severity::Off;
2202 let need_empty_groups = config.rules.empty_catalog_groups != Severity::Off;
2203 let need_unresolved_refs = config.rules.unresolved_catalog_references != Severity::Off;
2204 let Some(state) = ((need_unused || need_empty_groups || need_unresolved_refs)
2205 .then(|| gather_pnpm_catalog_state(config, workspaces)))
2206 .flatten() else {
2207 return;
2208 };
2209
2210 if need_unused {
2211 results.unused_catalog_entries = find_unused_catalog_entries(&state)
2212 .into_iter()
2213 .map(UnusedCatalogEntryFinding::with_actions)
2214 .collect();
2215 }
2216 if need_empty_groups {
2217 results.empty_catalog_groups = find_empty_catalog_groups(&state)
2218 .into_iter()
2219 .map(EmptyCatalogGroupFinding::with_actions)
2220 .collect();
2221 }
2222 if need_unresolved_refs {
2223 results.unresolved_catalog_references = find_unresolved_catalog_references(
2224 &state,
2225 &config.compiled_ignore_catalog_references,
2226 &config.root,
2227 )
2228 .into_iter()
2229 .map(UnresolvedCatalogReferenceFinding::with_actions)
2230 .collect();
2231 }
2232}
2233
2234#[expect(
2235 deprecated,
2236 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2237)]
2238fn populate_pnpm_override_findings(
2239 config: &ResolvedConfig,
2240 workspaces: &[fallow_config::WorkspaceInfo],
2241 results: &mut AnalysisResults,
2242) {
2243 let need_unused = config.rules.unused_dependency_overrides != Severity::Off;
2244 let need_misconfigured = config.rules.misconfigured_dependency_overrides != Severity::Off;
2245 let Some(state) = ((need_unused || need_misconfigured)
2246 .then(|| gather_pnpm_override_state(config, workspaces)))
2247 .flatten() else {
2248 return;
2249 };
2250
2251 if need_unused {
2252 results.unused_dependency_overrides = find_unused_dependency_overrides(&state, config)
2253 .into_iter()
2254 .map(UnusedDependencyOverrideFinding::with_actions)
2255 .collect();
2256 }
2257 if need_misconfigured {
2258 results.misconfigured_dependency_overrides =
2259 find_misconfigured_dependency_overrides(&state, config)
2260 .into_iter()
2261 .map(MisconfiguredDependencyOverrideFinding::with_actions)
2262 .collect();
2263 }
2264}
2265
2266fn populate_security_findings(
2267 ctx: &SecurityDetectionContext<'_, '_>,
2268 results: &mut AnalysisResults,
2269) {
2270 if ctx.config.rules.security_client_server_leak != Severity::Off {
2271 let (security_findings, stats) = security::find_security_findings(
2272 ctx.graph,
2273 ctx.modules,
2274 ctx.suppressions,
2275 ctx.line_offsets_by_file,
2276 );
2277 results.security_findings = security_findings;
2278 results.security_unresolved_edge_files = stats.client_files_with_unresolved_edges;
2279 }
2280
2281 if ctx.config.rules.security_sink != Severity::Off {
2282 populate_tainted_sink_findings(ctx, results);
2283 }
2284
2285 if !results.security_findings.is_empty() {
2286 annotate_security_findings(ctx, results);
2287 }
2288}
2289
2290fn populate_tainted_sink_findings(
2291 ctx: &SecurityDetectionContext<'_, '_>,
2292 results: &mut AnalysisResults,
2293) {
2294 let categories = ctx.config.security.categories.as_ref();
2295 let filter = security::CategoryFilter::new(
2296 categories.and_then(|c| c.include.clone()),
2297 categories.and_then(|c| c.exclude.clone()),
2298 );
2299 let (sink_findings, sink_stats) = security::find_tainted_sinks(
2300 ctx.graph,
2301 ctx.modules,
2302 ctx.suppressions,
2303 ctx.line_offsets_by_file,
2304 ctx.declared_deps,
2305 &security::TaintedSinkContext {
2306 category_filter: &filter,
2307 request_receivers: ctx.request_receivers,
2308 root: &ctx.config.root,
2309 },
2310 );
2311 results.security_findings.extend(sink_findings);
2312 results.security_unresolved_callee_sites = sink_stats.sinks_skipped_dynamic_callee;
2313 results.security_unresolved_callee_diagnostics = sink_stats.unresolved_callee_diagnostics;
2314 results
2315 .security_findings
2316 .extend(security::find_hardcoded_secret_candidates(
2317 ctx.graph,
2318 ctx.modules,
2319 ctx.suppressions,
2320 ctx.line_offsets_by_file,
2321 &filter,
2322 &ctx.config.root,
2323 ));
2324}
2325
2326fn annotate_security_findings(
2327 ctx: &SecurityDetectionContext<'_, '_>,
2328 results: &mut AnalysisResults,
2329) {
2330 security::annotate_dead_code_cross_links(
2331 ctx.graph,
2332 ctx.modules,
2333 ctx.line_offsets_by_file,
2334 &results.unused_files,
2335 &results.unused_exports,
2336 &mut results.security_findings,
2337 );
2338 let boundary_crossings = boundary_crossings_by_file(&results.boundary_violations);
2339 security::rank_security_findings(
2340 &security::SecurityRankingInput {
2341 graph: ctx.graph,
2342 modules: ctx.modules,
2343 line_offsets_by_file: ctx.line_offsets_by_file,
2344 declared_deps: ctx.declared_deps,
2345 request_receivers: ctx.request_receivers,
2346 boundary_crossings: &boundary_crossings,
2347 },
2348 &mut results.security_findings,
2349 );
2350}
2351
2352fn boundary_crossings_by_file(
2353 boundary_violations: &[BoundaryViolationFinding],
2354) -> FxHashMap<std::path::PathBuf, (String, String)> {
2355 let mut boundary_crossings: FxHashMap<std::path::PathBuf, (String, String)> =
2356 FxHashMap::default();
2357 for violation in boundary_violations {
2358 let zones = (
2359 violation.violation.from_zone.clone(),
2360 violation.violation.to_zone.clone(),
2361 );
2362 for path in [
2363 violation.violation.from_path.clone(),
2364 violation.violation.to_path.clone(),
2365 ] {
2366 boundary_crossings
2367 .entry(path)
2368 .and_modify(|existing| {
2369 if zones < *existing {
2370 *existing = zones.clone();
2371 }
2372 })
2373 .or_insert_with(|| zones.clone());
2374 }
2375 }
2376 boundary_crossings
2377}
2378
2379#[expect(
2380 deprecated,
2381 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2382)]
2383fn run_unused_file_detector(
2384 graph: &ModuleGraph,
2385 config: &ResolvedConfig,
2386 suppressions: &crate::suppress::SuppressionContext<'_>,
2387) -> Vec<UnusedFileFinding> {
2388 if config.rules.unused_files == Severity::Off {
2389 return Vec::new();
2390 }
2391 find_unused_files(graph, suppressions)
2392 .into_iter()
2393 .map(UnusedFileFinding::with_actions)
2394 .collect()
2395}
2396
2397#[expect(
2398 deprecated,
2399 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2400)]
2401fn run_export_detectors(
2402 graph: &ModuleGraph,
2403 modules: &[ModuleInfo],
2404 config: &ResolvedConfig,
2405 plugin_result: Option<&crate::plugins::AggregatedPluginResult>,
2406 suppressions: &crate::suppress::SuppressionContext<'_>,
2407 line_offsets_by_file: &LineOffsetsMap<'_>,
2408) -> AnalysisResults {
2409 let mut results = AnalysisResults::default();
2410 if export_rules_are_disabled(config) {
2411 return results;
2412 }
2413
2414 let (exports, types, stale_expected) = find_unused_exports(
2415 graph,
2416 modules,
2417 config,
2418 plugin_result,
2419 suppressions,
2420 line_offsets_by_file,
2421 );
2422 populate_unused_export_findings(&mut results, config, exports);
2423 populate_unused_type_findings(&mut results, config, graph, modules, types);
2424 populate_private_type_leak_findings(
2425 &mut results,
2426 graph,
2427 modules,
2428 config,
2429 suppressions,
2430 line_offsets_by_file,
2431 );
2432 populate_expected_stale_suppressions(&mut results, config, stale_expected);
2433 results
2434}
2435
2436fn export_rules_are_disabled(config: &ResolvedConfig) -> bool {
2437 config.rules.unused_exports == Severity::Off
2438 && config.rules.unused_types == Severity::Off
2439 && config.rules.private_type_leaks == Severity::Off
2440}
2441
2442fn populate_unused_export_findings(
2443 results: &mut AnalysisResults,
2444 config: &ResolvedConfig,
2445 exports: Vec<UnusedExport>,
2446) {
2447 if config.rules.unused_exports == Severity::Off {
2448 return;
2449 }
2450 results.unused_exports = exports
2451 .into_iter()
2452 .map(UnusedExportFinding::with_actions)
2453 .collect();
2454}
2455
2456fn populate_unused_type_findings(
2457 results: &mut AnalysisResults,
2458 config: &ResolvedConfig,
2459 graph: &ModuleGraph,
2460 modules: &[ModuleInfo],
2461 types: Vec<UnusedExport>,
2462) {
2463 if config.rules.unused_types == Severity::Off {
2464 return;
2465 }
2466 let mut typed = types;
2467 suppress_signature_backing_types(&mut typed, graph, modules);
2468 results.unused_types = typed
2469 .into_iter()
2470 .map(UnusedTypeFinding::with_actions)
2471 .collect();
2472}
2473
2474fn populate_private_type_leak_findings(
2475 results: &mut AnalysisResults,
2476 graph: &ModuleGraph,
2477 modules: &[ModuleInfo],
2478 config: &ResolvedConfig,
2479 suppressions: &crate::suppress::SuppressionContext<'_>,
2480 line_offsets_by_file: &LineOffsetsMap<'_>,
2481) {
2482 if config.rules.private_type_leaks == Severity::Off {
2483 return;
2484 }
2485 results.private_type_leaks =
2486 find_private_type_leaks(graph, modules, config, suppressions, line_offsets_by_file)
2487 .into_iter()
2488 .map(PrivateTypeLeakFinding::with_actions)
2489 .collect();
2490}
2491
2492fn populate_expected_stale_suppressions(
2493 results: &mut AnalysisResults,
2494 config: &ResolvedConfig,
2495 stale_expected: Vec<StaleSuppression>,
2496) {
2497 if config.rules.stale_suppressions != Severity::Off {
2498 results.stale_suppressions.extend(stale_expected);
2499 } else if config.rules.require_suppression_reason != Severity::Off {
2500 results
2501 .stale_suppressions
2502 .extend(stale_expected.into_iter().filter(|s| s.missing_reason));
2503 }
2504}
2505
2506#[derive(Clone, Copy)]
2507struct MemberDetectorInput<'a> {
2508 graph: &'a ModuleGraph,
2509 resolved_modules: &'a [ResolvedModule],
2510 modules: &'a [ModuleInfo],
2511 config: &'a ResolvedConfig,
2512 suppressions: &'a crate::suppress::SuppressionContext<'a>,
2513 line_offsets_by_file: &'a LineOffsetsMap<'a>,
2514 user_class_members: &'a [fallow_config::UsedClassMemberRule],
2515 semantic_framework_candidates: &'a [fallow_config::UsedClassMemberRule],
2516 public_api_entry_points: &'a FxHashSet<FileId>,
2517 declared_deps: &'a FxHashSet<String>,
2518}
2519
2520fn run_member_detectors(input: MemberDetectorInput<'_>) -> AnalysisResults {
2521 let mut results = AnalysisResults::default();
2522 let store_members_active = store_member_rule_is_active(input.config, input.declared_deps);
2523 if member_rules_are_disabled(input.config, store_members_active) {
2524 return results;
2525 }
2526
2527 let member_results = find_unused_members_with_public_api_entry_points(UnusedMemberScanInput {
2528 graph: input.graph,
2529 resolved_modules: input.resolved_modules,
2530 modules: input.modules,
2531 suppressions: input.suppressions,
2532 line_offsets_by_file: input.line_offsets_by_file,
2533 user_class_member_allowlist: input.user_class_members,
2534 semantic_framework_candidates: input.semantic_framework_candidates,
2535 ignore_decorators: &input.config.ignore_decorators,
2536 public_api_entry_points: input.public_api_entry_points,
2537 lit_active: input.declared_deps.contains("lit")
2538 || input.declared_deps.contains("lit-element")
2539 || input.declared_deps.contains("@lit/reactive-element"),
2540 });
2541 populate_unused_enum_member_findings(&mut results, input.config, member_results.enum_members);
2542 populate_unused_class_member_findings(&mut results, input.config, member_results.class_members);
2543 populate_unused_store_member_findings(
2544 &mut results,
2545 store_members_active,
2546 member_results.store_members,
2547 );
2548 results
2549}
2550
2551fn member_rules_are_disabled(config: &ResolvedConfig, store_members_active: bool) -> bool {
2552 config.rules.unused_enum_members == Severity::Off
2553 && config.rules.unused_class_members == Severity::Off
2554 && !store_members_active
2555}
2556
2557fn store_member_rule_is_active(config: &ResolvedConfig, declared_deps: &FxHashSet<String>) -> bool {
2558 config.rules.unused_store_members != Severity::Off
2563 && (declared_deps.contains("pinia") || declared_deps.contains("@pinia/nuxt"))
2564}
2565
2566fn populate_unused_enum_member_findings(
2567 results: &mut AnalysisResults,
2568 config: &ResolvedConfig,
2569 enum_members: Vec<UnusedMember>,
2570) {
2571 if config.rules.unused_enum_members == Severity::Off {
2572 return;
2573 }
2574 results.unused_enum_members = enum_members
2575 .into_iter()
2576 .map(UnusedEnumMemberFinding::with_actions)
2577 .collect();
2578}
2579
2580fn populate_unused_class_member_findings(
2581 results: &mut AnalysisResults,
2582 config: &ResolvedConfig,
2583 class_members: Vec<members::UnusedClassMemberCandidate>,
2584) {
2585 if config.rules.unused_class_members == Severity::Off {
2586 return;
2587 }
2588 results.unused_class_members = class_members
2589 .into_iter()
2590 .map(|candidate| {
2591 let finding = UnusedClassMemberFinding::with_actions(candidate.member);
2592 if candidate.semantic_only {
2593 finding.semantic_only_candidate()
2594 } else {
2595 finding
2596 }
2597 })
2598 .collect();
2599}
2600
2601fn populate_unused_store_member_findings(
2602 results: &mut AnalysisResults,
2603 store_members_active: bool,
2604 store_members: Vec<UnusedMember>,
2605) {
2606 if !store_members_active {
2607 return;
2608 }
2609 results.unused_store_members = store_members
2610 .into_iter()
2611 .map(UnusedStoreMemberFinding::with_actions)
2612 .collect();
2613}
2614
2615#[derive(Clone, Copy)]
2616struct DependencyDetectorInput<'a> {
2617 graph: &'a ModuleGraph,
2618 pkg: Option<&'a PackageJson>,
2619 config: &'a ResolvedConfig,
2620 plugin_result: Option<&'a crate::plugins::AggregatedPluginResult>,
2621 workspaces: &'a [fallow_config::WorkspaceInfo],
2622 resolved_modules: &'a [ResolvedModule],
2623 line_offsets_by_file: &'a LineOffsetsMap<'a>,
2624}
2625
2626fn run_dependency_detectors(input: DependencyDetectorInput<'_>) -> AnalysisResults {
2627 let mut results = AnalysisResults::default();
2628 let Some(pkg) = input.pkg else {
2629 return results;
2630 };
2631
2632 populate_unused_dependency_findings(input, pkg, &mut results);
2633 populate_unlisted_dependency_findings(input, pkg, &mut results);
2634 populate_type_only_dependency_findings(input, pkg, &mut results);
2635 populate_test_only_dependency_findings(input, pkg, &mut results);
2636 populate_dev_dependency_in_production_findings(input, pkg, &mut results);
2637 results
2638}
2639
2640fn populate_unlisted_dependency_findings(
2641 input: DependencyDetectorInput<'_>,
2642 pkg: &PackageJson,
2643 results: &mut AnalysisResults,
2644) {
2645 if input.config.rules.unlisted_dependencies != Severity::Off {
2646 results.unlisted_dependencies = find_unlisted_dependencies(UnlistedDependencyInput {
2647 graph: input.graph,
2648 pkg,
2649 config: input.config,
2650 workspaces: input.workspaces,
2651 plugin_result: input.plugin_result,
2652 resolved_modules: input.resolved_modules,
2653 line_offsets_by_file: input.line_offsets_by_file,
2654 })
2655 .into_iter()
2656 .map(UnlistedDependencyFinding::with_actions)
2657 .collect();
2658 }
2659}
2660
2661fn populate_type_only_dependency_findings(
2662 input: DependencyDetectorInput<'_>,
2663 pkg: &PackageJson,
2664 results: &mut AnalysisResults,
2665) {
2666 if input.config.production {
2667 results.type_only_dependencies =
2668 find_type_only_dependencies(input.graph, pkg, input.config, input.workspaces)
2669 .into_iter()
2670 .map(TypeOnlyDependencyFinding::with_actions)
2671 .collect();
2672 }
2673}
2674
2675fn populate_test_only_dependency_findings(
2676 input: DependencyDetectorInput<'_>,
2677 pkg: &PackageJson,
2678 results: &mut AnalysisResults,
2679) {
2680 if !input.config.production && input.config.rules.test_only_dependencies != Severity::Off {
2681 results.test_only_dependencies =
2682 find_test_only_dependencies(input.graph, pkg, input.config, input.workspaces)
2683 .into_iter()
2684 .map(TestOnlyDependencyFinding::with_actions)
2685 .collect();
2686 }
2687}
2688
2689fn populate_dev_dependency_in_production_findings(
2690 input: DependencyDetectorInput<'_>,
2691 pkg: &PackageJson,
2692 results: &mut AnalysisResults,
2693) {
2694 if input.config.rules.dev_dependencies_in_production != Severity::Off {
2700 results.dev_dependencies_in_production =
2701 find_dev_dependencies_in_production(input.graph, pkg, input.config, input.workspaces)
2702 .into_iter()
2703 .map(DevDependencyInProductionFinding::with_actions)
2704 .collect();
2705 }
2706}
2707
2708#[expect(
2712 deprecated,
2713 reason = "Core-internal policy deprecates detector helpers for external callers; core orchestration still calls them internally"
2714)]
2715fn populate_unused_dependency_findings(
2716 input: DependencyDetectorInput<'_>,
2717 pkg: &PackageJson,
2718 results: &mut AnalysisResults,
2719) {
2720 if unused_dependency_rules_are_disabled(input.config) {
2721 return;
2722 }
2723
2724 let (deps, dev_deps, optional_deps) = find_unused_dependencies(
2725 input.graph,
2726 pkg,
2727 input.config,
2728 input.plugin_result,
2729 input.workspaces,
2730 );
2731 populate_unused_prod_dependency_findings(results, input.config, deps);
2732 populate_unused_dev_dependency_findings(results, input.config, dev_deps);
2733 populate_unused_optional_dependency_findings(results, input.config, optional_deps);
2734}
2735
2736fn unused_dependency_rules_are_disabled(config: &ResolvedConfig) -> bool {
2737 config.rules.unused_dependencies == Severity::Off
2738 && config.rules.unused_dev_dependencies == Severity::Off
2739 && config.rules.unused_optional_dependencies == Severity::Off
2740}
2741
2742fn populate_unused_prod_dependency_findings(
2743 results: &mut AnalysisResults,
2744 config: &ResolvedConfig,
2745 deps: Vec<UnusedDependency>,
2746) {
2747 if config.rules.unused_dependencies == Severity::Off {
2748 return;
2749 }
2750 results.unused_dependencies = deps
2751 .into_iter()
2752 .map(UnusedDependencyFinding::with_actions)
2753 .collect();
2754}
2755
2756fn populate_unused_dev_dependency_findings(
2757 results: &mut AnalysisResults,
2758 config: &ResolvedConfig,
2759 dev_deps: Vec<UnusedDependency>,
2760) {
2761 if config.rules.unused_dev_dependencies == Severity::Off {
2762 return;
2763 }
2764 results.unused_dev_dependencies = dev_deps
2765 .into_iter()
2766 .map(UnusedDevDependencyFinding::with_actions)
2767 .collect();
2768}
2769
2770fn populate_unused_optional_dependency_findings(
2771 results: &mut AnalysisResults,
2772 config: &ResolvedConfig,
2773 optional_deps: Vec<UnusedDependency>,
2774) {
2775 if config.rules.unused_optional_dependencies == Severity::Off {
2776 return;
2777 }
2778 results.unused_optional_dependencies = optional_deps
2779 .into_iter()
2780 .map(UnusedOptionalDependencyFinding::with_actions)
2781 .collect();
2782}
2783
2784#[derive(Clone, Copy)]
2785struct UnresolvedImportDetectorInput<'a> {
2786 resolved_modules: &'a [ResolvedModule],
2787 config: &'a ResolvedConfig,
2788 suppressions: &'a crate::suppress::SuppressionContext<'a>,
2789 virtual_prefixes: &'a [&'a str],
2790 generated_patterns: &'a [&'a str],
2791 generated_type_prefixes: &'a [&'a str],
2792 line_offsets_by_file: &'a LineOffsetsMap<'a>,
2793}
2794
2795fn run_unresolved_import_detector(
2796 input: UnresolvedImportDetectorInput<'_>,
2797) -> Vec<UnresolvedImportFinding> {
2798 if input.config.rules.unresolved_imports == Severity::Off || input.resolved_modules.is_empty() {
2799 return Vec::new();
2800 }
2801 find_unresolved_imports(
2802 input.resolved_modules,
2803 input.config,
2804 input.suppressions,
2805 input.virtual_prefixes,
2806 input.generated_patterns,
2807 input.generated_type_prefixes,
2808 input.line_offsets_by_file,
2809 )
2810 .into_iter()
2811 .map(UnresolvedImportFinding::with_actions)
2812 .collect()
2813}
2814
2815#[cfg(test)]
2816#[expect(
2817 deprecated,
2818 reason = "Core-internal policy keeps direct analyzer unit tests while the public warning targets external callers"
2819)]
2820mod tests {
2821 use fallow_types::extract::{byte_offset_to_line_col, compute_line_offsets};
2822
2823 #[test]
2824 fn exact_framework_contract_only_replaces_its_matching_plugin_rule() {
2825 use fallow_config::{ScopedUsedClassMemberRule, UsedClassMemberRule};
2826 use fallow_types::semantic::{SemanticFrameworkContract, SemanticFrameworkRelation};
2827
2828 let contract = SemanticFrameworkContract {
2829 framework: "lit".to_string(),
2830 package: "lit".to_string(),
2831 heritage_symbol: "LitElement".to_string(),
2832 heritage_names: vec!["LitElement".to_string()],
2833 relation: SemanticFrameworkRelation::Extends,
2834 members: vec!["render".to_string()],
2835 };
2836 let matching = UsedClassMemberRule::Scoped(ScopedUsedClassMemberRule {
2837 extends: Some("LitElement".to_string()),
2838 implements: None,
2839 members: vec!["render".to_string()],
2840 });
2841 let local_name_only = UsedClassMemberRule::Scoped(ScopedUsedClassMemberRule {
2842 extends: Some("LocalLitElement".to_string()),
2843 implements: None,
2844 members: vec!["render".to_string()],
2845 });
2846 let extra_member = UsedClassMemberRule::Scoped(ScopedUsedClassMemberRule {
2847 extends: Some("LitElement".to_string()),
2848 implements: None,
2849 members: vec!["render".to_string(), "localHook".to_string()],
2850 });
2851
2852 assert!(super::framework_contract_covers_rule(&contract, &matching));
2853 assert!(!super::framework_contract_covers_rule(
2854 &contract,
2855 &local_name_only
2856 ));
2857 assert!(!super::framework_contract_covers_rule(
2858 &contract,
2859 &extra_member
2860 ));
2861 }
2862
2863 fn line_col(source: &str, byte_offset: u32) -> (u32, u32) {
2864 let offsets = compute_line_offsets(source);
2865 byte_offset_to_line_col(&offsets, byte_offset)
2866 }
2867
2868 #[cfg(unix)]
2875 #[cfg_attr(miri, ignore)]
2876 #[test]
2877 fn scoped_canonical_matches_module_reached_through_symlink() {
2878 use fallow_types::discover::FileId;
2879
2880 let dir = tempfile::tempdir().unwrap();
2881 let real_dir = dir.path().join("real");
2882 std::fs::create_dir(&real_dir).unwrap();
2883 let real_file = real_dir.join("mod.ts");
2884 std::fs::write(&real_file, "export const x = 1;\n").unwrap();
2885 let link_dir = dir.path().join("link");
2888 std::os::unix::fs::symlink(&real_dir, &link_dir).unwrap();
2889
2890 let module_raw_path = link_dir.join("mod.ts");
2891 let canonical_entry = dunce::canonicalize(&real_file).unwrap();
2892 let package_root = dir.path();
2893
2894 let candidates = [(module_raw_path.as_path(), FileId(7))];
2896 assert_eq!(
2897 super::match_canonical_entry_under_package(
2898 candidates.iter().copied(),
2899 package_root,
2900 &canonical_entry,
2901 ),
2902 Some(FileId(7)),
2903 );
2904
2905 let outside_root = dir.path().join("other-package");
2907 assert_eq!(
2908 super::match_canonical_entry_under_package(
2909 candidates.iter().copied(),
2910 &outside_root,
2911 &canonical_entry,
2912 ),
2913 None,
2914 );
2915
2916 let unrelated = dunce::canonicalize(dir.path()).unwrap().join("nope.ts");
2918 assert_eq!(
2919 super::match_canonical_entry_under_package(
2920 candidates.iter().copied(),
2921 package_root,
2922 &unrelated,
2923 ),
2924 None,
2925 );
2926 }
2927
2928 #[test]
2929 fn compute_offsets_empty() {
2930 assert_eq!(compute_line_offsets(""), vec![0]);
2931 }
2932
2933 #[test]
2934 fn compute_offsets_single_line() {
2935 assert_eq!(compute_line_offsets("hello"), vec![0]);
2936 }
2937
2938 #[test]
2939 fn compute_offsets_multiline() {
2940 assert_eq!(compute_line_offsets("abc\ndef\nghi"), vec![0, 4, 8]);
2941 }
2942
2943 #[test]
2944 fn compute_offsets_trailing_newline() {
2945 assert_eq!(compute_line_offsets("abc\n"), vec![0, 4]);
2946 }
2947
2948 #[test]
2949 fn compute_offsets_crlf() {
2950 assert_eq!(compute_line_offsets("ab\r\ncd"), vec![0, 4]);
2951 }
2952
2953 #[test]
2954 fn compute_offsets_consecutive_newlines() {
2955 assert_eq!(compute_line_offsets("\n\n"), vec![0, 1, 2]);
2956 }
2957
2958 #[test]
2959 fn byte_offset_empty_source() {
2960 assert_eq!(line_col("", 0), (1, 0));
2961 }
2962
2963 #[test]
2964 fn byte_offset_single_line_start() {
2965 assert_eq!(line_col("hello", 0), (1, 0));
2966 }
2967
2968 #[test]
2969 fn byte_offset_single_line_middle() {
2970 assert_eq!(line_col("hello", 4), (1, 4));
2971 }
2972
2973 #[test]
2974 fn byte_offset_multiline_start_of_line2() {
2975 assert_eq!(line_col("line1\nline2\nline3", 6), (2, 0));
2976 }
2977
2978 #[test]
2979 fn byte_offset_multiline_middle_of_line3() {
2980 assert_eq!(line_col("line1\nline2\nline3", 14), (3, 2));
2981 }
2982
2983 #[test]
2984 fn byte_offset_at_newline_boundary() {
2985 assert_eq!(line_col("line1\nline2", 5), (1, 5));
2986 }
2987
2988 #[test]
2989 fn byte_offset_multibyte_utf8() {
2990 let source = "hi\n\u{1F600}x";
2991 assert_eq!(line_col(source, 3), (2, 0));
2992 assert_eq!(line_col(source, 7), (2, 4));
2993 }
2994
2995 #[test]
2996 fn byte_offset_multibyte_accented_chars() {
2997 let source = "caf\u{00E9}\nbar";
2998 assert_eq!(line_col(source, 6), (2, 0));
2999 assert_eq!(line_col(source, 3), (1, 3));
3000 }
3001
3002 #[test]
3003 fn byte_offset_via_map_fallback() {
3004 use super::*;
3005 let map: LineOffsetsMap<'_> = FxHashMap::default();
3006 assert_eq!(
3007 super::byte_offset_to_line_col(&map, FileId(99), 42),
3008 (1, 42)
3009 );
3010 }
3011
3012 #[test]
3013 fn byte_offset_via_map_lookup() {
3014 use super::*;
3015 let offsets = compute_line_offsets("abc\ndef\nghi");
3016 let mut map: LineOffsetsMap<'_> = FxHashMap::default();
3017 map.insert(FileId(0), &offsets);
3018 assert_eq!(super::byte_offset_to_line_col(&map, FileId(0), 5), (2, 1));
3019 }
3020
3021 mod orchestration {
3022 use super::super::*;
3023 use fallow_config::{FallowConfig, OutputFormat, RulesConfig, Severity};
3024 use std::path::PathBuf;
3025
3026 fn find_dead_code(graph: &ModuleGraph, config: &ResolvedConfig) -> AnalysisResults {
3027 find_dead_code_full(graph, config, &[], None, &[], &[], false)
3028 }
3029
3030 fn make_config_with_rules(rules: RulesConfig) -> ResolvedConfig {
3031 FallowConfig {
3032 rules,
3033 ..Default::default()
3034 }
3035 .resolve(
3036 PathBuf::from("/tmp/orchestration-test"),
3037 OutputFormat::Human,
3038 1,
3039 true,
3040 true,
3041 None,
3042 )
3043 }
3044
3045 const ALL_RULES_OFF: RulesConfig = RulesConfig {
3046 unused_files: Severity::Off,
3047 unused_exports: Severity::Off,
3048 unused_types: Severity::Off,
3049 private_type_leaks: Severity::Off,
3050 private_type_leaks_configured: false,
3051 unused_dependencies: Severity::Off,
3052 unused_dev_dependencies: Severity::Off,
3053 unused_optional_dependencies: Severity::Off,
3054 unused_enum_members: Severity::Off,
3055 unused_class_members: Severity::Off,
3056 unused_store_members: Severity::Off,
3057 unprovided_injects: Severity::Off,
3058 unrendered_components: Severity::Off,
3059 unused_component_props: Severity::Off,
3060 unused_component_emits: Severity::Off,
3061 unused_component_inputs: Severity::Off,
3062 unused_component_outputs: Severity::Off,
3063 unused_svelte_events: Severity::Off,
3064 unused_server_actions: Severity::Off,
3065 unused_load_data_keys: Severity::Off,
3066 prop_drilling: Severity::Off,
3067 thin_wrapper: Severity::Off,
3068 duplicate_prop_shape: Severity::Off,
3069 css_token_drift: Severity::Off,
3070 css_duplicate_block: Severity::Off,
3071 css_selector_complexity: Severity::Off,
3072 css_dead_surface: Severity::Off,
3073 css_broken_reference: Severity::Off,
3074 unresolved_imports: Severity::Off,
3075 unlisted_dependencies: Severity::Off,
3076 duplicate_exports: Severity::Off,
3077 type_only_dependencies: Severity::Off,
3078 circular_dependencies: Severity::Off,
3079 re_export_cycle: Severity::Off,
3080 test_only_dependencies: Severity::Off,
3081 dev_dependencies_in_production: Severity::Off,
3082 boundary_violation: Severity::Off,
3083 coverage_gaps: Severity::Off,
3084 feature_flags: Severity::Off,
3085 stale_suppressions: Severity::Off,
3086 require_suppression_reason: Severity::Off,
3087 unused_catalog_entries: Severity::Off,
3088 empty_catalog_groups: Severity::Off,
3089 unresolved_catalog_references: Severity::Off,
3090 unused_dependency_overrides: Severity::Off,
3091 misconfigured_dependency_overrides: Severity::Off,
3092 security_client_server_leak: Severity::Off,
3093 security_sink: Severity::Off,
3094 policy_violation: Severity::Off,
3095 invalid_client_export: Severity::Off,
3096 mixed_client_server_barrel: Severity::Off,
3097 misplaced_directive: Severity::Off,
3098 route_collision: Severity::Off,
3099 dynamic_segment_name_conflict: Severity::Off,
3100 };
3101
3102 #[test]
3103 fn find_dead_code_all_rules_off_returns_empty() {
3104 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
3105 use crate::graph::ModuleGraph;
3106 use crate::resolve::ResolvedModule;
3107 use rustc_hash::FxHashSet;
3108
3109 let files = vec![DiscoveredFile {
3110 id: FileId(0),
3111 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3112 size_bytes: 100,
3113 }];
3114 let entry_points = vec![EntryPoint {
3115 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3116 source: EntryPointSource::ManualEntry,
3117 }];
3118 let resolved = vec![ResolvedModule {
3119 file_id: FileId(0),
3120 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3121 exports: vec![].into(),
3122 re_exports: vec![],
3123 resolved_imports: vec![],
3124 resolved_dynamic_imports: vec![],
3125 resolved_dynamic_patterns: vec![],
3126 member_accesses: vec![].into(),
3127 semantic_facts: std::sync::Arc::default(),
3128 whole_object_uses: std::sync::Arc::default(),
3129 has_cjs_exports: false,
3130 has_angular_component_template_url: false,
3131 unused_import_bindings: FxHashSet::default(),
3132 type_referenced_import_bindings: vec![],
3133 value_referenced_import_bindings: vec![],
3134 namespace_object_aliases: vec![],
3135 exported_factory_returns: std::sync::Arc::default(),
3136 exported_factory_return_object_shapes: std::sync::Arc::default(),
3137 type_member_types: std::sync::Arc::default(),
3138 }];
3139 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
3140
3141 let config = make_config_with_rules(ALL_RULES_OFF);
3142 let results = find_dead_code(&graph, &config);
3143
3144 assert!(results.unused_files.is_empty());
3145 assert!(results.unused_exports.is_empty());
3146 assert!(results.unused_types.is_empty());
3147 assert!(results.unused_dependencies.is_empty());
3148 assert!(results.unused_dev_dependencies.is_empty());
3149 assert!(results.unused_optional_dependencies.is_empty());
3150 assert!(results.unused_enum_members.is_empty());
3151 assert!(results.unused_class_members.is_empty());
3152 assert!(results.unresolved_imports.is_empty());
3153 assert!(results.unlisted_dependencies.is_empty());
3154 assert!(results.duplicate_exports.is_empty());
3155 assert!(results.circular_dependencies.is_empty());
3156 assert!(results.export_usages.is_empty());
3157 }
3158
3159 #[test]
3160 fn find_dead_code_full_collect_usages_flag() {
3161 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
3162 use crate::extract::{ExportName, VisibilityTag};
3163 use crate::graph::{ExportSymbol, ModuleGraph};
3164 use crate::resolve::ResolvedModule;
3165 use oxc_span::Span;
3166 use rustc_hash::FxHashSet;
3167
3168 let files = vec![DiscoveredFile {
3169 id: FileId(0),
3170 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3171 size_bytes: 100,
3172 }];
3173 let entry_points = vec![EntryPoint {
3174 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3175 source: EntryPointSource::ManualEntry,
3176 }];
3177 let resolved = vec![ResolvedModule {
3178 file_id: FileId(0),
3179 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3180 exports: vec![].into(),
3181 re_exports: vec![],
3182 resolved_imports: vec![],
3183 resolved_dynamic_imports: vec![],
3184 resolved_dynamic_patterns: vec![],
3185 member_accesses: vec![].into(),
3186 semantic_facts: std::sync::Arc::default(),
3187 whole_object_uses: std::sync::Arc::default(),
3188 has_cjs_exports: false,
3189 has_angular_component_template_url: false,
3190 unused_import_bindings: FxHashSet::default(),
3191 type_referenced_import_bindings: vec![],
3192 value_referenced_import_bindings: vec![],
3193 namespace_object_aliases: vec![],
3194 exported_factory_returns: std::sync::Arc::default(),
3195 exported_factory_return_object_shapes: std::sync::Arc::default(),
3196 type_member_types: std::sync::Arc::default(),
3197 }];
3198 let mut graph = ModuleGraph::build(&resolved, &entry_points, &files);
3199 graph.modules[0].exports = vec![ExportSymbol {
3200 name: ExportName::Named("myExport".to_string()),
3201 is_type_only: false,
3202 is_side_effect_used: false,
3203 visibility: VisibilityTag::None,
3204 expected_unused_reason: None,
3205 span: Span::new(10, 30),
3206 references: vec![],
3207 reference_paths: Vec::new(),
3208 members: vec![],
3209 }];
3210
3211 let rules = RulesConfig::default();
3212 let config = make_config_with_rules(rules);
3213
3214 let results_no_collect = find_dead_code_full(
3215 &graph,
3216 &config,
3217 &[],
3218 None,
3219 &[],
3220 &[],
3221 false, );
3223 assert!(
3224 results_no_collect.export_usages.is_empty(),
3225 "export_usages should be empty when collect_usages is false"
3226 );
3227
3228 let results_with_collect = find_dead_code_full(
3229 &graph,
3230 &config,
3231 &[],
3232 None,
3233 &[],
3234 &[],
3235 true, );
3237 assert!(
3238 !results_with_collect.export_usages.is_empty(),
3239 "export_usages should be populated when collect_usages is true"
3240 );
3241 assert_eq!(
3242 results_with_collect.export_usages[0].export_name,
3243 "myExport"
3244 );
3245 }
3246
3247 #[test]
3248 fn find_dead_code_delegates_to_find_dead_code_with_resolved() {
3249 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
3250 use crate::graph::ModuleGraph;
3251 use crate::resolve::ResolvedModule;
3252 use rustc_hash::FxHashSet;
3253
3254 let files = vec![DiscoveredFile {
3255 id: FileId(0),
3256 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3257 size_bytes: 100,
3258 }];
3259 let entry_points = vec![EntryPoint {
3260 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3261 source: EntryPointSource::ManualEntry,
3262 }];
3263 let resolved = vec![ResolvedModule {
3264 file_id: FileId(0),
3265 path: PathBuf::from("/tmp/orchestration-test/src/index.ts"),
3266 exports: vec![].into(),
3267 re_exports: vec![],
3268 resolved_imports: vec![],
3269 resolved_dynamic_imports: vec![],
3270 resolved_dynamic_patterns: vec![],
3271 member_accesses: vec![].into(),
3272 semantic_facts: std::sync::Arc::default(),
3273 whole_object_uses: std::sync::Arc::default(),
3274 has_cjs_exports: false,
3275 has_angular_component_template_url: false,
3276 unused_import_bindings: FxHashSet::default(),
3277 type_referenced_import_bindings: vec![],
3278 value_referenced_import_bindings: vec![],
3279 namespace_object_aliases: vec![],
3280 exported_factory_returns: std::sync::Arc::default(),
3281 exported_factory_return_object_shapes: std::sync::Arc::default(),
3282 type_member_types: std::sync::Arc::default(),
3283 }];
3284 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
3285 let config = make_config_with_rules(RulesConfig::default());
3286
3287 let results = find_dead_code(&graph, &config);
3288 assert!(results.unused_exports.is_empty());
3289 }
3290
3291 #[test]
3292 fn suppressions_built_from_modules() {
3293 use crate::discover::{DiscoveredFile, EntryPoint, EntryPointSource, FileId};
3294 use crate::extract::ModuleInfo;
3295 use crate::graph::ModuleGraph;
3296 use crate::resolve::ResolvedModule;
3297 use crate::suppress::{IssueKind, Suppression};
3298 use rustc_hash::FxHashSet;
3299
3300 let files = vec![
3301 DiscoveredFile {
3302 id: FileId(0),
3303 path: PathBuf::from("/tmp/orchestration-test/src/entry.ts"),
3304 size_bytes: 100,
3305 },
3306 DiscoveredFile {
3307 id: FileId(1),
3308 path: PathBuf::from("/tmp/orchestration-test/src/utils.ts"),
3309 size_bytes: 100,
3310 },
3311 ];
3312 let entry_points = vec![EntryPoint {
3313 path: PathBuf::from("/tmp/orchestration-test/src/entry.ts"),
3314 source: EntryPointSource::ManualEntry,
3315 }];
3316 let resolved = files
3317 .iter()
3318 .map(|f| ResolvedModule {
3319 file_id: f.id,
3320 path: f.path.clone(),
3321 exports: vec![].into(),
3322 re_exports: vec![],
3323 resolved_imports: vec![],
3324 resolved_dynamic_imports: vec![],
3325 resolved_dynamic_patterns: vec![],
3326 member_accesses: vec![].into(),
3327 semantic_facts: std::sync::Arc::default(),
3328 whole_object_uses: std::sync::Arc::default(),
3329 has_cjs_exports: false,
3330 has_angular_component_template_url: false,
3331 unused_import_bindings: FxHashSet::default(),
3332 type_referenced_import_bindings: vec![],
3333 value_referenced_import_bindings: vec![],
3334 namespace_object_aliases: vec![],
3335 exported_factory_returns: std::sync::Arc::default(),
3336 exported_factory_return_object_shapes: std::sync::Arc::default(),
3337 type_member_types: std::sync::Arc::default(),
3338 })
3339 .collect::<Vec<_>>();
3340 let graph = ModuleGraph::build(&resolved, &entry_points, &files);
3341
3342 let modules = vec![ModuleInfo {
3343 suppressions: vec![Suppression::issue(0, 1, IssueKind::UnusedFile)],
3344 ..ModuleInfo::empty(FileId(1))
3345 }];
3346
3347 let rules = RulesConfig {
3348 unused_files: Severity::Error,
3349 ..RulesConfig::default()
3350 };
3351 let config = make_config_with_rules(rules);
3352
3353 let results = find_dead_code_full(&graph, &config, &[], None, &[], &modules, false);
3354
3355 assert!(
3356 !results.unused_files.iter().any(|f| f
3357 .file
3358 .path
3359 .to_string_lossy()
3360 .contains("utils.ts")),
3361 "suppressed file should not appear in unused_files"
3362 );
3363 }
3364 }
3365}