Skip to main content

fallow_engine/
dead_code.rs

1//! Dead-code result helpers exposed through the engine boundary.
2
3use std::path::{Path, PathBuf};
4
5use rustc_hash::FxHashSet;
6
7use crate::AnalysisResults;
8
9/// Scope dead-code results to the union of the given workspace roots.
10///
11/// The full cross-workspace graph is still built before this helper runs, so
12/// cross-package imports are resolved. Only reported findings are narrowed.
13pub fn filter_to_workspaces(results: &mut AnalysisResults, ws_roots: &[PathBuf]) {
14    let any_under = |path: &Path| ws_roots.iter().any(|root| path.starts_with(root));
15    let pkg_jsons = ws_roots
16        .iter()
17        .map(|root| root.join("package.json"))
18        .collect::<Vec<_>>();
19    let in_pkg_jsons = |path: &Path| pkg_jsons.iter().any(|pkg| path == pkg);
20
21    filter_workspace_source_findings(results, &any_under);
22    filter_workspace_dependency_findings(results, &any_under, &in_pkg_jsons);
23    filter_workspace_graph_findings(results, &any_under);
24    filter_workspace_policy_findings(results, &any_under);
25}
26
27/// Scope dead-code results to findings affected by changed files.
28#[expect(
29    clippy::implicit_hasher,
30    reason = "fallow standardizes on FxHashSet across the workspace"
31)]
32pub fn filter_by_changed_files(results: &mut AnalysisResults, changed_files: &FxHashSet<PathBuf>) {
33    fallow_core::changed_files::filter_results_by_changed_files(results, changed_files);
34}
35
36fn filter_workspace_source_findings(
37    results: &mut AnalysisResults,
38    any_under: &dyn Fn(&Path) -> bool,
39) {
40    results
41        .unused_files
42        .retain(|finding| any_under(&finding.file.path));
43    results
44        .unused_exports
45        .retain(|finding| any_under(&finding.export.path));
46    results
47        .unused_types
48        .retain(|finding| any_under(&finding.export.path));
49    results
50        .private_type_leaks
51        .retain(|finding| any_under(&finding.leak.path));
52    results
53        .unused_enum_members
54        .retain(|finding| any_under(&finding.member.path));
55    results
56        .unused_class_members
57        .retain(|finding| any_under(&finding.member.path));
58    results
59        .unused_store_members
60        .retain(|finding| any_under(&finding.member.path));
61    results
62        .unprovided_injects
63        .retain(|finding| any_under(&finding.inject.path));
64    results
65        .unrendered_components
66        .retain(|finding| any_under(&finding.component.path));
67    results
68        .unused_component_props
69        .retain(|finding| any_under(&finding.prop.path));
70    results
71        .unused_component_emits
72        .retain(|finding| any_under(&finding.emit.path));
73    results
74        .unused_component_inputs
75        .retain(|finding| any_under(&finding.input.path));
76    results
77        .unused_component_outputs
78        .retain(|finding| any_under(&finding.output.path));
79    results
80        .unused_svelte_events
81        .retain(|finding| any_under(&finding.event.path));
82    results
83        .unused_server_actions
84        .retain(|finding| any_under(&finding.action.path));
85    results
86        .unused_load_data_keys
87        .retain(|finding| any_under(&finding.key.path));
88    results
89        .unresolved_imports
90        .retain(|finding| any_under(&finding.import.path));
91}
92
93fn filter_workspace_dependency_findings(
94    results: &mut AnalysisResults,
95    any_under: &dyn Fn(&Path) -> bool,
96    in_pkg_jsons: &dyn Fn(&Path) -> bool,
97) {
98    results
99        .unused_dependencies
100        .retain(|finding| in_pkg_jsons(&finding.dep.path));
101    results
102        .unused_dev_dependencies
103        .retain(|finding| in_pkg_jsons(&finding.dep.path));
104    results
105        .unused_optional_dependencies
106        .retain(|finding| in_pkg_jsons(&finding.dep.path));
107    results
108        .type_only_dependencies
109        .retain(|finding| in_pkg_jsons(&finding.dep.path));
110    results
111        .test_only_dependencies
112        .retain(|finding| in_pkg_jsons(&finding.dep.path));
113
114    results.unlisted_dependencies.retain(|finding| {
115        finding
116            .dep
117            .imported_from
118            .iter()
119            .any(|source| any_under(&source.path))
120    });
121    results.unused_dependency_overrides.clear();
122    results.misconfigured_dependency_overrides.clear();
123}
124
125fn filter_workspace_graph_findings(
126    results: &mut AnalysisResults,
127    any_under: &dyn Fn(&Path) -> bool,
128) {
129    for duplicate in &mut results.duplicate_exports {
130        duplicate
131            .export
132            .locations
133            .retain(|location| any_under(&location.path));
134    }
135    results
136        .duplicate_exports
137        .retain(|duplicate| duplicate.export.locations.len() >= 2);
138
139    results
140        .circular_dependencies
141        .retain(|cycle| cycle.cycle.files.iter().any(|path| any_under(path)));
142
143    results
144        .re_export_cycles
145        .retain(|cycle| cycle.cycle.files.iter().any(|path| any_under(path)));
146}
147
148fn filter_workspace_policy_findings(
149    results: &mut AnalysisResults,
150    any_under: &dyn Fn(&Path) -> bool,
151) {
152    results
153        .boundary_violations
154        .retain(|finding| any_under(&finding.violation.from_path));
155    results
156        .boundary_coverage_violations
157        .retain(|finding| any_under(&finding.violation.path));
158    results
159        .boundary_call_violations
160        .retain(|finding| any_under(&finding.violation.path));
161    results
162        .policy_violations
163        .retain(|finding| any_under(&finding.violation.path));
164
165    results
166        .stale_suppressions
167        .retain(|finding| any_under(&finding.path));
168
169    results
170        .security_findings
171        .retain(|finding| any_under(&finding.path));
172    results
173        .security_unresolved_callee_diagnostics
174        .retain(|finding| any_under(&finding.path));
175
176    results.unused_catalog_entries.clear();
177    results.empty_catalog_groups.clear();
178    results
179        .unresolved_catalog_references
180        .retain(|finding| any_under(&finding.reference.path));
181
182    results
183        .invalid_client_exports
184        .retain(|finding| any_under(&finding.export.path));
185
186    results
187        .mixed_client_server_barrels
188        .retain(|finding| any_under(&finding.barrel.path));
189
190    results
191        .misplaced_directives
192        .retain(|finding| any_under(&finding.directive_site.path));
193
194    results
195        .route_collisions
196        .retain(|finding| any_under(&finding.collision.path));
197
198    results
199        .dynamic_segment_name_conflicts
200        .retain(|finding| any_under(&finding.conflict.path));
201}
202
203#[cfg(test)]
204mod tests {
205    use std::path::PathBuf;
206
207    use super::*;
208    use fallow_types::output_dead_code::UnusedFileFinding;
209    use fallow_types::results::UnusedFile;
210
211    #[test]
212    fn workspace_filter_keeps_findings_under_workspace_root() {
213        let root = PathBuf::from("/repo/packages/app");
214        let mut results = AnalysisResults::default();
215        results
216            .unused_files
217            .push(UnusedFileFinding::with_actions(UnusedFile {
218                path: root.join("src/unused.ts"),
219            }));
220        results
221            .unused_files
222            .push(UnusedFileFinding::with_actions(UnusedFile {
223                path: PathBuf::from("/repo/packages/docs/src/unused.ts"),
224            }));
225
226        filter_to_workspaces(&mut results, std::slice::from_ref(&root));
227
228        assert_eq!(results.unused_files.len(), 1);
229        assert_eq!(
230            results.unused_files[0].file.path,
231            root.join("src/unused.ts")
232        );
233    }
234}