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
135    results.unlisted_dependencies.retain(|finding| {
136        finding
137            .dep
138            .imported_from
139            .iter()
140            .any(|source| any_under(&source.path))
141    });
142    results.unused_dependency_overrides.clear();
143    results.misconfigured_dependency_overrides.clear();
144}
145
146fn filter_workspace_graph_findings(
147    results: &mut AnalysisResults,
148    any_under: &dyn Fn(&Path) -> bool,
149) {
150    for duplicate in &mut results.duplicate_exports {
151        duplicate
152            .export
153            .locations
154            .retain(|location| any_under(&location.path));
155    }
156    results
157        .duplicate_exports
158        .retain(|duplicate| duplicate.export.locations.len() >= 2);
159
160    results
161        .circular_dependencies
162        .retain(|cycle| cycle.cycle.files.iter().any(|path| any_under(path)));
163
164    results
165        .re_export_cycles
166        .retain(|cycle| cycle.cycle.files.iter().any(|path| any_under(path)));
167}
168
169fn filter_workspace_policy_findings(
170    results: &mut AnalysisResults,
171    any_under: &dyn Fn(&Path) -> bool,
172) {
173    results
174        .boundary_violations
175        .retain(|finding| any_under(&finding.violation.from_path));
176    results
177        .boundary_coverage_violations
178        .retain(|finding| any_under(&finding.violation.path));
179    results
180        .boundary_call_violations
181        .retain(|finding| any_under(&finding.violation.path));
182    results
183        .policy_violations
184        .retain(|finding| any_under(&finding.violation.path));
185
186    results
187        .stale_suppressions
188        .retain(|finding| any_under(&finding.path));
189
190    results
191        .security_findings
192        .retain(|finding| any_under(&finding.path));
193    results
194        .security_unresolved_callee_diagnostics
195        .retain(|finding| any_under(&finding.path));
196
197    results.unused_catalog_entries.clear();
198    results.empty_catalog_groups.clear();
199    results
200        .unresolved_catalog_references
201        .retain(|finding| any_under(&finding.reference.path));
202
203    results
204        .invalid_client_exports
205        .retain(|finding| any_under(&finding.export.path));
206
207    results
208        .mixed_client_server_barrels
209        .retain(|finding| any_under(&finding.barrel.path));
210
211    results
212        .misplaced_directives
213        .retain(|finding| any_under(&finding.directive_site.path));
214
215    results
216        .route_collisions
217        .retain(|finding| any_under(&finding.collision.path));
218
219    results
220        .dynamic_segment_name_conflicts
221        .retain(|finding| any_under(&finding.conflict.path));
222}
223
224#[cfg(test)]
225mod tests {
226    use std::path::PathBuf;
227
228    use super::*;
229    use fallow_types::output_dead_code::UnusedFileFinding;
230    use fallow_types::results::UnusedFile;
231
232    #[test]
233    fn workspace_filter_keeps_findings_under_workspace_root() {
234        let root = PathBuf::from("/repo/packages/app");
235        let mut results = AnalysisResults::default();
236        results
237            .unused_files
238            .push(UnusedFileFinding::with_actions(UnusedFile {
239                path: root.join("src/unused.ts"),
240            }));
241        results
242            .unused_files
243            .push(UnusedFileFinding::with_actions(UnusedFile {
244                path: PathBuf::from("/repo/packages/docs/src/unused.ts"),
245            }));
246
247        filter_to_workspaces(&mut results, std::slice::from_ref(&root));
248
249        assert_eq!(results.unused_files.len(), 1);
250        assert_eq!(
251            results.unused_files[0].file.path,
252            root.join("src/unused.ts")
253        );
254    }
255}