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
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}