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