fallow_engine/
dead_code.rs1use std::path::{Path, PathBuf};
4
5use rustc_hash::FxHashSet;
6
7use fallow_config::ResolvedConfig;
8
9use crate::{
10 AnalysisResults, DeadCodeAnalysis, DeadCodeAnalysisArtifacts, DeadCodeAnalysisOutput,
11 DeadCodeAnalysisWithHashes, EngineResult, ModuleInfo, core_backend,
12 session::{
13 analyze_dead_code_from_config, analyze_dead_code_with_artifacts_from_config,
14 analyze_dead_code_with_complexity_from_config,
15 },
16};
17
18pub fn analyze(config: &ResolvedConfig) -> EngineResult<DeadCodeAnalysis> {
24 analyze_dead_code_with_artifacts_from_config(config, false, false).map(|output| {
25 DeadCodeAnalysis {
26 results: output.results,
27 }
28 })
29}
30
31pub fn analyze_with_usages(config: &ResolvedConfig) -> EngineResult<DeadCodeAnalysis> {
37 analyze_dead_code_from_config(config)
38}
39
40pub fn analyze_with_file_hashes(
46 config: &ResolvedConfig,
47) -> EngineResult<DeadCodeAnalysisWithHashes> {
48 analyze_dead_code_with_artifacts_from_config(config, false, false).map(|output| {
49 DeadCodeAnalysisWithHashes {
50 results: output.results,
51 file_hashes: output.file_hashes,
52 }
53 })
54}
55
56pub fn analyze_with_trace(config: &ResolvedConfig) -> EngineResult<DeadCodeAnalysisArtifacts> {
62 analyze_dead_code_with_artifacts_from_config(config, false, true)
63}
64
65pub fn analyze_retaining_modules(
71 config: &ResolvedConfig,
72 need_complexity: bool,
73 retain_graph: bool,
74) -> EngineResult<DeadCodeAnalysisArtifacts> {
75 analyze_dead_code_with_artifacts_from_config(config, need_complexity, retain_graph)
76}
77
78pub fn analyze_with_parse_result(
84 config: &ResolvedConfig,
85 modules: &[ModuleInfo],
86) -> EngineResult<DeadCodeAnalysisArtifacts> {
87 core_backend::analyze_with_parse_result(config, modules)
88}
89
90pub fn analyze_with_usages_and_complexity(
96 config: &ResolvedConfig,
97) -> EngineResult<DeadCodeAnalysisOutput> {
98 analyze_dead_code_with_complexity_from_config(config)
99}
100
101pub fn filter_to_workspaces(results: &mut AnalysisResults, ws_roots: &[PathBuf]) {
106 let any_under = |path: &Path| ws_roots.iter().any(|root| path.starts_with(root));
107 let pkg_jsons = ws_roots
108 .iter()
109 .map(|root| root.join("package.json"))
110 .collect::<Vec<_>>();
111 let in_pkg_jsons = |path: &Path| pkg_jsons.iter().any(|pkg| path == pkg);
112
113 filter_workspace_source_findings(results, &any_under);
114 filter_workspace_dependency_findings(results, &any_under, &in_pkg_jsons);
115 filter_workspace_graph_findings(results, &any_under);
116 filter_workspace_policy_findings(results, &any_under);
117}
118
119#[expect(
121 clippy::implicit_hasher,
122 reason = "fallow standardizes on FxHashSet across the workspace"
123)]
124pub fn filter_by_changed_files(results: &mut AnalysisResults, changed_files: &FxHashSet<PathBuf>) {
125 core_backend::filter_results_by_changed_files(results, changed_files);
126}
127
128fn filter_workspace_source_findings(
129 results: &mut AnalysisResults,
130 any_under: &dyn Fn(&Path) -> bool,
131) {
132 results
133 .unused_files
134 .retain(|finding| any_under(&finding.file.path));
135 results
136 .unused_exports
137 .retain(|finding| any_under(&finding.export.path));
138 results
139 .unused_types
140 .retain(|finding| any_under(&finding.export.path));
141 results
142 .private_type_leaks
143 .retain(|finding| any_under(&finding.leak.path));
144 results
145 .unused_enum_members
146 .retain(|finding| any_under(&finding.member.path));
147 results
148 .unused_class_members
149 .retain(|finding| any_under(&finding.member.path));
150 results
151 .unused_store_members
152 .retain(|finding| any_under(&finding.member.path));
153 results
154 .unprovided_injects
155 .retain(|finding| any_under(&finding.inject.path));
156 results
157 .unrendered_components
158 .retain(|finding| any_under(&finding.component.path));
159 results
160 .unused_component_props
161 .retain(|finding| any_under(&finding.prop.path));
162 results
163 .unused_component_emits
164 .retain(|finding| any_under(&finding.emit.path));
165 results
166 .unused_component_inputs
167 .retain(|finding| any_under(&finding.input.path));
168 results
169 .unused_component_outputs
170 .retain(|finding| any_under(&finding.output.path));
171 results
172 .unused_svelte_events
173 .retain(|finding| any_under(&finding.event.path));
174 results
175 .unused_server_actions
176 .retain(|finding| any_under(&finding.action.path));
177 results
178 .unused_load_data_keys
179 .retain(|finding| any_under(&finding.key.path));
180 results
181 .unresolved_imports
182 .retain(|finding| any_under(&finding.import.path));
183}
184
185fn filter_workspace_dependency_findings(
186 results: &mut AnalysisResults,
187 any_under: &dyn Fn(&Path) -> bool,
188 in_pkg_jsons: &dyn Fn(&Path) -> bool,
189) {
190 results
191 .unused_dependencies
192 .retain(|finding| in_pkg_jsons(&finding.dep.path));
193 results
194 .unused_dev_dependencies
195 .retain(|finding| in_pkg_jsons(&finding.dep.path));
196 results
197 .unused_optional_dependencies
198 .retain(|finding| in_pkg_jsons(&finding.dep.path));
199 results
200 .type_only_dependencies
201 .retain(|finding| in_pkg_jsons(&finding.dep.path));
202 results
203 .test_only_dependencies
204 .retain(|finding| in_pkg_jsons(&finding.dep.path));
205
206 results.unlisted_dependencies.retain(|finding| {
207 finding
208 .dep
209 .imported_from
210 .iter()
211 .any(|source| any_under(&source.path))
212 });
213 results.unused_dependency_overrides.clear();
214 results.misconfigured_dependency_overrides.clear();
215}
216
217fn filter_workspace_graph_findings(
218 results: &mut AnalysisResults,
219 any_under: &dyn Fn(&Path) -> bool,
220) {
221 for duplicate in &mut results.duplicate_exports {
222 duplicate
223 .export
224 .locations
225 .retain(|location| any_under(&location.path));
226 }
227 results
228 .duplicate_exports
229 .retain(|duplicate| duplicate.export.locations.len() >= 2);
230
231 results
232 .circular_dependencies
233 .retain(|cycle| cycle.cycle.files.iter().any(|path| any_under(path)));
234
235 results
236 .re_export_cycles
237 .retain(|cycle| cycle.cycle.files.iter().any(|path| any_under(path)));
238}
239
240fn filter_workspace_policy_findings(
241 results: &mut AnalysisResults,
242 any_under: &dyn Fn(&Path) -> bool,
243) {
244 results
245 .boundary_violations
246 .retain(|finding| any_under(&finding.violation.from_path));
247 results
248 .boundary_coverage_violations
249 .retain(|finding| any_under(&finding.violation.path));
250 results
251 .boundary_call_violations
252 .retain(|finding| any_under(&finding.violation.path));
253 results
254 .policy_violations
255 .retain(|finding| any_under(&finding.violation.path));
256
257 results
258 .stale_suppressions
259 .retain(|finding| any_under(&finding.path));
260
261 results
262 .security_findings
263 .retain(|finding| any_under(&finding.path));
264 results
265 .security_unresolved_callee_diagnostics
266 .retain(|finding| any_under(&finding.path));
267
268 results.unused_catalog_entries.clear();
269 results.empty_catalog_groups.clear();
270 results
271 .unresolved_catalog_references
272 .retain(|finding| any_under(&finding.reference.path));
273
274 results
275 .invalid_client_exports
276 .retain(|finding| any_under(&finding.export.path));
277
278 results
279 .mixed_client_server_barrels
280 .retain(|finding| any_under(&finding.barrel.path));
281
282 results
283 .misplaced_directives
284 .retain(|finding| any_under(&finding.directive_site.path));
285
286 results
287 .route_collisions
288 .retain(|finding| any_under(&finding.collision.path));
289
290 results
291 .dynamic_segment_name_conflicts
292 .retain(|finding| any_under(&finding.conflict.path));
293}
294
295#[cfg(test)]
296mod tests {
297 use std::path::PathBuf;
298
299 use super::*;
300 use fallow_types::output_dead_code::UnusedFileFinding;
301 use fallow_types::results::UnusedFile;
302
303 #[test]
304 fn workspace_filter_keeps_findings_under_workspace_root() {
305 let root = PathBuf::from("/repo/packages/app");
306 let mut results = AnalysisResults::default();
307 results
308 .unused_files
309 .push(UnusedFileFinding::with_actions(UnusedFile {
310 path: root.join("src/unused.ts"),
311 }));
312 results
313 .unused_files
314 .push(UnusedFileFinding::with_actions(UnusedFile {
315 path: PathBuf::from("/repo/packages/docs/src/unused.ts"),
316 }));
317
318 filter_to_workspaces(&mut results, std::slice::from_ref(&root));
319
320 assert_eq!(results.unused_files.len(), 1);
321 assert_eq!(
322 results.unused_files[0].file.path,
323 root.join("src/unused.ts")
324 );
325 }
326}