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