1use std::path::Path;
2use std::time::Instant;
3
4use fallow_config::ProductionAnalysis;
5use fallow_engine::{AnalysisSession, ProjectConfig, ProjectConfigOptions};
6use fallow_output::{
7 CHECK_SCHEMA_VERSION, CheckOutputInput, DeadCodeNextStepsInput, DiffIndex, build_check_output,
8 build_dead_code_next_steps, check_meta, relative_to_diff_path,
9};
10use fallow_types::output_format::OutputFormat;
11use fallow_types::path_util::is_absolute_path_any_platform;
12use fallow_types::results::{AnalysisResults, TraceHopRole};
13use rustc_hash::FxHashSet;
14
15use crate::{
16 AnalysisOptions, BoundaryViolationsProgrammaticOutput, CircularDependenciesProgrammaticOutput,
17 DeadCodeFilters, DeadCodeOptions, DeadCodeProgrammaticOutput, ProgrammaticError,
18 analysis_context::{
19 ProgrammaticAnalysisContext, changed_files_for_run, resolve_programmatic_analysis_context,
20 },
21 next_steps::{default_workspace_ref, setup_pointer_applicable, suggestions_enabled},
22};
23
24use super::{ProgrammaticResult, root_envelope_mode};
25
26pub fn run_dead_code(options: &DeadCodeOptions) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
34 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
35 resolved.install(|| run_dead_code_inner(options, &resolved, |_| {}))
36}
37
38pub fn run_circular_dependencies(
44 options: &DeadCodeOptions,
45) -> ProgrammaticResult<CircularDependenciesProgrammaticOutput> {
46 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
47 resolved.install(|| {
48 run_dead_code_inner(options, &resolved, keep_circular_dependencies).map(Into::into)
49 })
50}
51
52pub fn run_boundary_violations(
58 options: &DeadCodeOptions,
59) -> ProgrammaticResult<BoundaryViolationsProgrammaticOutput> {
60 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
61 resolved.install(|| {
62 run_dead_code_inner(options, &resolved, keep_boundary_violations).map(Into::into)
63 })
64}
65
66fn run_dead_code_inner(
67 options: &DeadCodeOptions,
68 resolved: &ProgrammaticAnalysisContext,
69 post_filter: impl FnOnce(&mut AnalysisResults),
70) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
71 let start = Instant::now();
72 let session = load_dead_code_session(options, resolved)?;
73 run_dead_code_with_session(options, resolved, &session, None, post_filter, start)
74}
75
76pub(super) fn run_dead_code_with_session(
77 options: &DeadCodeOptions,
78 resolved: &ProgrammaticAnalysisContext,
79 session: &AnalysisSession,
80 changed_files: Option<&FxHashSet<std::path::PathBuf>>,
81 post_filter: impl FnOnce(&mut AnalysisResults),
82 start: Instant,
83) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
84 let analysis = session.analyze_dead_code().map_err(|err| {
85 ProgrammaticError::new(format!("dead-code analysis failed: {err}"), 2)
86 .with_code("FALLOW_DEAD_CODE_FAILED")
87 .with_context("dead-code")
88 })?;
89 let mut results = analysis.results;
90
91 apply_dead_code_scope(options, resolved, session, changed_files, &mut results)?;
92 apply_dead_code_filters(&options.filters, &mut results);
93 post_filter(&mut results);
94
95 let root = session.root();
96 let next_steps = build_dead_code_next_steps(DeadCodeNextStepsInput {
97 suggestions_enabled: suggestions_enabled(),
98 results: &results,
99 root,
100 offer_setup: setup_pointer_applicable(root),
101 impact_digest: None,
102 workspace_ref: default_workspace_ref(root).as_deref(),
103 audit_changed: fallow_engine::is_git_repo(root),
104 });
105 let output = build_check_output(CheckOutputInput {
106 schema_version: CHECK_SCHEMA_VERSION,
107 version: env!("CARGO_PKG_VERSION").to_string(),
108 elapsed: start.elapsed(),
109 results,
110 config_fixable: fallow_config::is_config_fixable(
111 &resolved.root,
112 resolved.config_path.as_ref(),
113 ),
114 meta: options.analysis.explain.then(check_meta),
115 workspace_diagnostics: session.workspace_diagnostics().to_vec(),
116 next_steps,
117 });
118 Ok(DeadCodeProgrammaticOutput {
119 output,
120 root: session.root().to_path_buf(),
121 envelope_mode: root_envelope_mode(),
122 telemetry_analysis_run_id: None,
123 })
124}
125
126fn keep_circular_dependencies(results: &mut AnalysisResults) {
127 let entry_point_summary = results.entry_point_summary.take();
128 let circular_dependencies = std::mem::take(&mut results.circular_dependencies);
129 *results = AnalysisResults::default();
130 results.entry_point_summary = entry_point_summary;
131 results.circular_dependencies = circular_dependencies;
132}
133
134fn keep_boundary_violations(results: &mut AnalysisResults) {
135 let entry_point_summary = results.entry_point_summary.take();
136 let boundary_violations = std::mem::take(&mut results.boundary_violations);
137 let boundary_coverage_violations = std::mem::take(&mut results.boundary_coverage_violations);
138 let boundary_call_violations = std::mem::take(&mut results.boundary_call_violations);
139 *results = AnalysisResults::default();
140 results.entry_point_summary = entry_point_summary;
141 results.boundary_violations = boundary_violations;
142 results.boundary_coverage_violations = boundary_coverage_violations;
143 results.boundary_call_violations = boundary_call_violations;
144}
145
146pub(super) fn load_dead_code_session(
147 options: &DeadCodeOptions,
148 resolved: &ProgrammaticAnalysisContext,
149) -> ProgrammaticResult<AnalysisSession> {
150 let project_config = fallow_engine::config_for_project_analysis(
151 &resolved.root,
152 resolved.config_path.as_deref(),
153 ProjectConfigOptions {
154 output: OutputFormat::Json,
155 no_cache: resolved.no_cache,
156 threads: resolved.threads,
157 production_override: resolved.production_override,
158 quiet: true,
159 analysis: ProductionAnalysis::DeadCode,
160 },
161 )
162 .map_err(|err| {
163 ProgrammaticError::new(format!("failed to load config: {err}"), 2)
164 .with_code("FALLOW_CONFIG_LOAD_FAILED")
165 .with_context("analysis.configPath")
166 })?;
167 let project_config = configure_project_for_dead_code(project_config, options);
168 Ok(AnalysisSession::from_config(project_config))
169}
170
171pub(super) fn default_dead_code_options_for_context(
172 resolved: &ProgrammaticAnalysisContext,
173) -> DeadCodeOptions {
174 DeadCodeOptions {
175 analysis: AnalysisOptions {
176 root: Some(resolved.root().to_path_buf()),
177 config_path: resolved.config_path().clone(),
178 no_cache: resolved.no_cache(),
179 threads: Some(resolved.threads()),
180 production_override: resolved.production_override(),
181 changed_since: resolved.changed_since().map(str::to_owned),
182 workspace: resolved.workspace().map(<[String]>::to_vec),
183 changed_workspaces: resolved.changed_workspaces().map(str::to_owned),
184 explain: resolved.explain_enabled(),
185 ..AnalysisOptions::default()
186 },
187 filters: DeadCodeFilters::default(),
188 files: Vec::new(),
189 include_entry_exports: false,
190 }
191}
192
193fn configure_project_for_dead_code(
194 mut project_config: ProjectConfig,
195 options: &DeadCodeOptions,
196) -> ProjectConfig {
197 if options.include_entry_exports {
198 project_config.config.include_entry_exports = true;
199 }
200 activate_explicit_dead_code_opt_ins(&options.filters, &mut project_config.config.rules);
201 project_config
202}
203
204fn activate_explicit_dead_code_opt_ins(
205 filters: &DeadCodeFilters,
206 rules: &mut fallow_config::RulesConfig,
207) {
208 if filters.private_type_leaks && rules.private_type_leaks == fallow_config::Severity::Off {
209 rules.private_type_leaks = fallow_config::Severity::Warn;
210 }
211}
212
213fn apply_dead_code_scope(
214 options: &DeadCodeOptions,
215 resolved: &ProgrammaticAnalysisContext,
216 session: &AnalysisSession,
217 changed_files: Option<&FxHashSet<std::path::PathBuf>>,
218 results: &mut AnalysisResults,
219) -> ProgrammaticResult<()> {
220 if let Some(workspace_roots) = resolved.workspace_roots.as_ref() {
221 fallow_engine::filter_to_workspaces(results, workspace_roots);
222 }
223 let resolved_changed_files = if changed_files.is_some() {
224 None
225 } else {
226 changed_files_for_run(resolved)?
227 };
228 if let Some(changed_files) = changed_files.or(resolved_changed_files.as_ref()) {
229 fallow_engine::filter_by_changed_files(results, changed_files);
230 }
231 if let Some(diff) = resolved.diff.as_ref() {
232 filter_dead_code_by_diff(results, diff, session.root());
233 }
234 apply_dead_code_file_filter(options, session.root(), results);
235 Ok(())
236}
237
238fn filter_dead_code_by_diff(results: &mut AnalysisResults, diff: &DiffIndex, root: &Path) {
239 let touches_file = |path: &Path| -> bool {
240 relative_to_diff_path(path, root).is_none_or(|rel| diff.touches_file(&rel))
241 };
242 let line_in_diff = |path: &Path, line: u32| -> bool {
243 relative_to_diff_path(path, root)
244 .is_none_or(|rel| diff.line_is_added(&rel, u64::from(line)))
245 };
246
247 filter_dead_code_source_findings(results, &touches_file, &line_in_diff);
248 filter_dead_code_security_findings(results, &touches_file, &line_in_diff);
249 filter_dead_code_dependency_findings(results, &line_in_diff);
250 filter_dead_code_graph_findings(results, &touches_file, &line_in_diff);
251 filter_dead_code_framework_findings(results, &line_in_diff);
252}
253
254fn filter_dead_code_source_findings(
255 results: &mut AnalysisResults,
256 touches_file: &dyn Fn(&Path) -> bool,
257 line_in_diff: &dyn Fn(&Path, u32) -> bool,
258) {
259 results
260 .unused_files
261 .retain(|finding| touches_file(&finding.file.path));
262 results
263 .unused_exports
264 .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
265 results
266 .unused_types
267 .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
268 results
269 .private_type_leaks
270 .retain(|finding| line_in_diff(&finding.leak.path, finding.leak.line));
271 results
272 .unused_enum_members
273 .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
274 results
275 .unused_class_members
276 .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
277 results
278 .unused_store_members
279 .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
280 results
281 .unprovided_injects
282 .retain(|finding| line_in_diff(&finding.inject.path, finding.inject.line));
283 results
284 .unrendered_components
285 .retain(|finding| line_in_diff(&finding.component.path, finding.component.line));
286 results
287 .unused_component_props
288 .retain(|finding| line_in_diff(&finding.prop.path, finding.prop.line));
289 results
290 .unused_component_emits
291 .retain(|finding| line_in_diff(&finding.emit.path, finding.emit.line));
292 results
293 .unused_component_inputs
294 .retain(|finding| line_in_diff(&finding.input.path, finding.input.line));
295 results
296 .unused_component_outputs
297 .retain(|finding| line_in_diff(&finding.output.path, finding.output.line));
298 results
299 .unused_svelte_events
300 .retain(|finding| line_in_diff(&finding.event.path, finding.event.line));
301 results
302 .unused_server_actions
303 .retain(|finding| line_in_diff(&finding.action.path, finding.action.line));
304 results
305 .unused_load_data_keys
306 .retain(|finding| line_in_diff(&finding.key.path, finding.key.line));
307 results
308 .unresolved_imports
309 .retain(|finding| line_in_diff(&finding.import.path, finding.import.line));
310}
311
312fn filter_dead_code_security_findings(
313 results: &mut AnalysisResults,
314 touches_file: &dyn Fn(&Path) -> bool,
315 line_in_diff: &dyn Fn(&Path, u32) -> bool,
316) {
317 results.security_findings.retain(|finding| {
318 line_in_diff(&finding.path, finding.line)
319 || finding.trace.iter().any(|hop| {
320 line_in_diff(&hop.path, hop.line)
321 || (matches!(hop.role, TraceHopRole::SecretSource) && touches_file(&hop.path))
322 })
323 || finding.reachability.as_ref().is_some_and(|reachability| {
324 reachability
325 .untrusted_source_trace
326 .iter()
327 .any(|hop| line_in_diff(&hop.path, hop.line))
328 })
329 });
330 results
331 .security_unresolved_callee_diagnostics
332 .retain(|finding| line_in_diff(&finding.path, finding.line));
333}
334
335fn filter_dead_code_dependency_findings(
336 results: &mut AnalysisResults,
337 line_in_diff: &dyn Fn(&Path, u32) -> bool,
338) {
339 for finding in &mut results.unlisted_dependencies {
340 finding
341 .dep
342 .imported_from
343 .retain(|source| line_in_diff(&source.path, source.line));
344 }
345 results
346 .unlisted_dependencies
347 .retain(|finding| !finding.dep.imported_from.is_empty());
348}
349
350fn filter_dead_code_graph_findings(
351 results: &mut AnalysisResults,
352 touches_file: &dyn Fn(&Path) -> bool,
353 line_in_diff: &dyn Fn(&Path, u32) -> bool,
354) {
355 results.duplicate_exports.retain(|finding| {
356 finding
357 .export
358 .locations
359 .iter()
360 .any(|location| line_in_diff(&location.path, location.line))
361 });
362 results
363 .circular_dependencies
364 .retain(|cycle| cycle.cycle.files.iter().any(|path| touches_file(path)));
365 results
366 .re_export_cycles
367 .retain(|cycle| cycle.cycle.files.iter().any(|path| touches_file(path)));
368 results
369 .boundary_violations
370 .retain(|finding| line_in_diff(&finding.violation.from_path, finding.violation.line));
371 results
372 .stale_suppressions
373 .retain(|finding| line_in_diff(&finding.path, finding.line));
374}
375
376fn filter_dead_code_framework_findings(
377 results: &mut AnalysisResults,
378 line_in_diff: &dyn Fn(&Path, u32) -> bool,
379) {
380 results
381 .invalid_client_exports
382 .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
383 results
384 .mixed_client_server_barrels
385 .retain(|finding| line_in_diff(&finding.barrel.path, finding.barrel.line));
386 results
387 .misplaced_directives
388 .retain(|finding| line_in_diff(&finding.directive_site.path, finding.directive_site.line));
389 results
390 .route_collisions
391 .retain(|finding| line_in_diff(&finding.collision.path, finding.collision.line));
392 results
393 .dynamic_segment_name_conflicts
394 .retain(|finding| line_in_diff(&finding.conflict.path, finding.conflict.line));
395}
396
397fn apply_dead_code_file_filter(
398 options: &DeadCodeOptions,
399 root: &Path,
400 results: &mut AnalysisResults,
401) {
402 if options.files.is_empty() {
403 return;
404 }
405 let file_set = options
406 .files
407 .iter()
408 .map(|path| {
409 if is_absolute_path_any_platform(path) {
410 path.clone()
411 } else {
412 root.join(path)
413 }
414 })
415 .collect::<FxHashSet<_>>();
416 fallow_engine::filter_by_changed_files(results, &file_set);
417 clear_dead_code_dependency_findings(results);
418}
419
420fn apply_dead_code_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
421 if !dead_code_filters_active(filters) {
422 return;
423 }
424 apply_dead_code_core_filters(filters, results);
425 apply_dead_code_component_filters(filters, results);
426 apply_dead_code_graph_filters(filters, results);
427 apply_dead_code_policy_filters(filters, results);
428 apply_dead_code_catalog_filters(filters, results);
429}
430
431fn dead_code_filters_active(filters: &DeadCodeFilters) -> bool {
432 filters.unused_files
433 || filters.unused_exports
434 || filters.unused_deps
435 || filters.unused_types
436 || filters.private_type_leaks
437 || filters.unused_enum_members
438 || filters.unused_class_members
439 || filters.unused_store_members
440 || filters.unprovided_injects
441 || filters.unrendered_components
442 || filters.unused_component_props
443 || filters.unused_component_emits
444 || filters.unused_component_inputs
445 || filters.unused_component_outputs
446 || filters.unused_svelte_events
447 || filters.unused_server_actions
448 || filters.unused_load_data_keys
449 || filters.unresolved_imports
450 || filters.unlisted_deps
451 || filters.duplicate_exports
452 || filters.circular_deps
453 || filters.re_export_cycles
454 || filters.boundary_violations
455 || filters.policy_violations
456 || filters.stale_suppressions
457 || filters.unused_catalog_entries
458 || filters.empty_catalog_groups
459 || filters.unresolved_catalog_references
460 || filters.unused_dependency_overrides
461 || filters.misconfigured_dependency_overrides
462}
463
464fn apply_dead_code_core_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
465 if !filters.unused_files {
466 results.unused_files.clear();
467 }
468 if !filters.unused_exports {
469 results.unused_exports.clear();
470 }
471 if !filters.unused_types {
472 results.unused_types.clear();
473 }
474 if !filters.private_type_leaks {
475 results.private_type_leaks.clear();
476 }
477 if !filters.unused_deps {
478 clear_dead_code_dependency_findings(results);
479 }
480 if !filters.unused_enum_members {
481 results.unused_enum_members.clear();
482 }
483 if !filters.unused_class_members {
484 results.unused_class_members.clear();
485 }
486 if !filters.unused_store_members {
487 results.unused_store_members.clear();
488 }
489 if !filters.unlisted_deps {
490 results.unlisted_dependencies.clear();
491 }
492}
493
494fn clear_dead_code_dependency_findings(results: &mut AnalysisResults) {
495 results.unused_dependencies.clear();
496 results.unused_dev_dependencies.clear();
497 results.unused_optional_dependencies.clear();
498 results.type_only_dependencies.clear();
499 results.test_only_dependencies.clear();
500}
501
502fn apply_dead_code_component_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
503 if !filters.unprovided_injects {
504 results.unprovided_injects.clear();
505 }
506 if !filters.unrendered_components {
507 results.unrendered_components.clear();
508 }
509 if !filters.unused_component_props {
510 results.unused_component_props.clear();
511 }
512 if !filters.unused_component_emits {
513 results.unused_component_emits.clear();
514 }
515 if !filters.unused_component_inputs {
516 results.unused_component_inputs.clear();
517 }
518 if !filters.unused_component_outputs {
519 results.unused_component_outputs.clear();
520 }
521 if !filters.unused_svelte_events {
522 results.unused_svelte_events.clear();
523 }
524 if !filters.unused_server_actions {
525 results.unused_server_actions.clear();
526 }
527 if !filters.unused_load_data_keys {
528 results.unused_load_data_keys.clear();
529 }
530 if !filters.unresolved_imports {
531 results.unresolved_imports.clear();
532 }
533}
534
535fn apply_dead_code_graph_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
536 if !filters.duplicate_exports {
537 results.duplicate_exports.clear();
538 }
539 if !filters.circular_deps {
540 results.circular_dependencies.clear();
541 }
542 if !filters.re_export_cycles {
543 results.re_export_cycles.clear();
544 }
545 if !filters.boundary_violations {
546 results.boundary_violations.clear();
547 results.boundary_coverage_violations.clear();
548 results.boundary_call_violations.clear();
549 }
550}
551
552fn apply_dead_code_policy_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
553 if !filters.policy_violations {
554 results.policy_violations.clear();
555 }
556 if !filters.stale_suppressions {
557 results.stale_suppressions.clear();
558 }
559}
560
561fn apply_dead_code_catalog_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
562 if !filters.unused_catalog_entries {
563 results.unused_catalog_entries.clear();
564 }
565 if !filters.empty_catalog_groups {
566 results.empty_catalog_groups.clear();
567 }
568 if !filters.unresolved_catalog_references {
569 results.unresolved_catalog_references.clear();
570 }
571 if !filters.unused_dependency_overrides {
572 results.unused_dependency_overrides.clear();
573 }
574 if !filters.misconfigured_dependency_overrides {
575 results.misconfigured_dependency_overrides.clear();
576 }
577}