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