fallow_engine/
dead_code.rs1use 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
18pub(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
30pub 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#[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}