1use std::path::Path;
2use std::time::Instant;
3
4use fallow_config::ProductionAnalysis;
5use fallow_engine::{
6 change_scope::ChangeScope,
7 dead_code::DeadCodeAnalysisArtifacts,
8 project_config::{ProjectConfig, ProjectConfigOptions},
9 session::AnalysisSession,
10};
11use fallow_output::{
12 CheckEnvelope, CheckOutputInput, DeadCodeNextStepsInput, architecture_meta, build_check_output,
13 build_dead_code_next_steps, check_meta,
14};
15use fallow_types::output_format::OutputFormat;
16use fallow_types::path_util::is_absolute_path_any_platform;
17use fallow_types::results::AnalysisResults;
18use rustc_hash::FxHashSet;
19
20use crate::{
21 AnalysisOptions, ArchitectureOptions, ArchitectureProgrammaticOutput,
22 BoundaryViolationsProgrammaticOutput, CircularDependenciesProgrammaticOutput, DeadCodeFilters,
23 DeadCodeOptions, DeadCodeProgrammaticOutput, ProgrammaticError,
24 analysis_context::{
25 ProgrammaticAnalysisContext, changed_files_for_run, resolve_programmatic_analysis_context,
26 workspace_roots_for_session,
27 },
28 next_steps::{
29 audit_changed_applicable, default_workspace_ref_for_workspaces, setup_pointer_applicable,
30 suggestions_enabled,
31 },
32};
33
34use super::ProgrammaticResult;
35
36pub(super) struct DeadCodeProgrammaticRunWithArtifacts {
37 pub output: DeadCodeProgrammaticOutput,
38 pub artifacts: DeadCodeAnalysisArtifacts,
39}
40
41pub fn run_dead_code(options: &DeadCodeOptions) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
51 run_dead_code_with_baseline(options, None)
52}
53
54pub fn run_dead_code_with_baseline(
67 options: &DeadCodeOptions,
68 baseline: Option<&Path>,
69) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
70 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
71 reject_config_type_aware(options, &resolved)?;
72 run_dead_code_in_context(options, baseline, &resolved)
73}
74
75fn reject_config_type_aware(
79 options: &DeadCodeOptions,
80 resolved: &ProgrammaticAnalysisContext,
81) -> ProgrammaticResult<()> {
82 super::type_aware_config::reject_config_type_aware(
83 &options.analysis.type_aware,
84 resolved,
85 super::type_aware_config::TypeAwareCommand::Analysis,
86 )
87}
88
89pub(super) fn run_dead_code_in_context(
92 options: &DeadCodeOptions,
93 baseline: Option<&Path>,
94 resolved: &ProgrammaticAnalysisContext,
95) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
96 resolved.install(|| {
97 let start = Instant::now();
98 resolved.ensure_not_cancelled("config load and file discovery")?;
99 let finding_ids = parse_finding_ids(options)?;
100 let session = load_dead_code_session(options, resolved)?;
101 resolve_package_map_before_analysis(resolved, &session)?;
102 let (mut results, finished) =
103 analyze_dead_code_results(options, resolved, &session, finding_ids)?;
104 let FinishedDeadCode {
105 type_aware_meta,
106 mut finding_id_trace,
107 change_reason,
108 package_baselines,
109 } = finished;
110 if let Some(trace) = finding_id_trace.as_mut() {
111 trace.start_stage(&mut results);
112 }
113 let install_state = match baseline {
114 Some(baseline) => apply_baseline(
115 &mut results,
116 baseline,
117 session.root(),
118 type_aware_meta.as_ref(),
119 )?,
120 None => None,
121 };
122 let finding_id_query = finding_id_trace.map(|mut trace| {
123 trace.end_stage(&mut results);
124 trace.finish(
125 &mut results,
126 session.config(),
127 finding_id_run_reasons(
128 options,
129 resolved,
130 &session,
131 RunScope {
132 change_reason,
133 baseline: baseline.is_some(),
134 },
135 ),
136 )
137 });
138 let mut output = build_dead_code_programmatic_output(
139 options,
140 resolved,
141 &session,
142 DeadCodeReport {
143 results,
144 type_aware_meta,
145 package_baselines,
146 },
147 start,
148 CheckEnvelope::DeadCode,
149 );
150 output.output.finding_id_query = finding_id_query;
151 output.output.workspace_diagnostics.extend(install_state);
152 Ok(output)
153 })
154}
155
156fn parse_finding_ids(
159 options: &DeadCodeOptions,
160) -> ProgrammaticResult<Option<fallow_engine::dead_code::FindingIdFilter>> {
161 fallow_engine::dead_code::FindingIdFilter::parse(&options.finding_ids).map_err(|message| {
162 ProgrammaticError::new(message, 2)
163 .with_code("FALLOW_INVALID_FINDING_ID")
164 .with_context("findingIds")
165 })
166}
167
168fn reject_finding_ids(options: &DeadCodeOptions) -> ProgrammaticResult<()> {
171 if options.finding_ids.is_empty() {
172 return Ok(());
173 }
174 Err(
175 ProgrammaticError::new("findingIds is supported only by the dead-code analysis", 2)
176 .with_code("FALLOW_UNSUPPORTED_OPTION")
177 .with_context("findingIds"),
178 )
179}
180
181#[derive(Clone, Copy)]
183struct RunScope {
184 change_reason: Option<fallow_output::ScopeReason>,
187 baseline: bool,
189}
190
191fn finding_id_run_reasons(
195 options: &DeadCodeOptions,
196 resolved: &ProgrammaticAnalysisContext,
197 session: &AnalysisSession,
198 run: RunScope,
199) -> Vec<fallow_output::FindingIdQueryReason> {
200 use fallow_output::FindingIdQueryReason as Reason;
201
202 [
203 (resolved.diff.is_some(), Reason::Diff),
204 (
205 run.change_reason == Some(fallow_output::ScopeReason::ChangedSince)
206 || options.analysis.ambient_changed_since.is_some(),
207 Reason::ChangedSince,
208 ),
209 (
210 run.change_reason == Some(fallow_output::ScopeReason::PackageBaselines),
211 Reason::PackageBaselines,
212 ),
213 (resolved.workspace().is_some(), Reason::Workspace),
214 (
215 resolved.changed_workspaces().is_some(),
216 Reason::ChangedWorkspaces,
217 ),
218 (!options.files.is_empty(), Reason::File),
219 (options.filters.any_active(), Reason::IssueTypeFilter),
220 (session.config().production, Reason::Production),
221 (
222 session.config().include_entry_exports,
223 Reason::IncludeEntryExports,
224 ),
225 (run.baseline, Reason::Baseline),
226 ]
227 .into_iter()
228 .filter_map(|(active, reason)| active.then_some(reason))
229 .collect()
230}
231
232pub(super) fn map_engine_error(
238 err: &fallow_engine::EngineError,
239 failure_message: &str,
240 failure_code: &'static str,
241 context: &'static str,
242) -> ProgrammaticError {
243 if err.is_cancelled() {
244 return crate::analysis_context::cancelled_error_message(err.message())
245 .with_context(context);
246 }
247 ProgrammaticError::new(format!("{failure_message}: {err}"), 2)
248 .with_code(failure_code)
249 .with_context(context)
250}
251
252pub fn run_circular_dependencies(
258 options: &DeadCodeOptions,
259) -> ProgrammaticResult<CircularDependenciesProgrammaticOutput> {
260 reject_finding_ids(options)?;
261 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
262 reject_config_type_aware(options, &resolved)?;
263 resolved.install(|| {
264 run_dead_code_inner(options, &resolved, keep_circular_dependencies).map(Into::into)
265 })
266}
267
268pub fn run_architecture(
281 options: &ArchitectureOptions,
282) -> ProgrammaticResult<ArchitectureProgrammaticOutput> {
283 let dead_code = options.dead_code_options();
284 let resolved = resolve_programmatic_analysis_context(&dead_code.analysis)?;
285 resolved.install(|| {
286 let start = Instant::now();
287 resolved.ensure_not_cancelled("config load and file discovery")?;
288 let session = load_dead_code_session(&dead_code, &resolved)?;
289 resolve_package_map_before_analysis(&resolved, &session)?;
290 run_dead_code_with_changed_files(
291 &dead_code,
292 &resolved,
293 &session,
294 SessionRun {
295 changed_files: None,
296 start,
297 envelope: CheckEnvelope::Architecture,
298 },
299 |_| {},
300 )
301 .map(Into::into)
302 })
303}
304
305pub fn run_boundary_violations(
311 options: &DeadCodeOptions,
312) -> ProgrammaticResult<BoundaryViolationsProgrammaticOutput> {
313 reject_finding_ids(options)?;
314 let resolved = resolve_programmatic_analysis_context(&options.analysis)?;
315 reject_config_type_aware(options, &resolved)?;
316 resolved.install(|| {
317 run_dead_code_inner(options, &resolved, keep_boundary_violations).map(Into::into)
318 })
319}
320
321fn run_dead_code_inner(
322 options: &DeadCodeOptions,
323 resolved: &ProgrammaticAnalysisContext,
324 post_filter: impl FnOnce(&mut AnalysisResults),
325) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
326 let start = Instant::now();
327 resolved.ensure_not_cancelled("config load and file discovery")?;
328 let session = load_dead_code_session(options, resolved)?;
329 resolve_package_map_before_analysis(resolved, &session)?;
330 run_dead_code_with_session(options, resolved, &session, None, post_filter, start)
331}
332
333pub(super) fn run_dead_code_with_session(
334 options: &DeadCodeOptions,
335 resolved: &ProgrammaticAnalysisContext,
336 session: &AnalysisSession,
337 changed_files: Option<&FxHashSet<std::path::PathBuf>>,
338 post_filter: impl FnOnce(&mut AnalysisResults),
339 start: Instant,
340) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
341 run_dead_code_with_changed_files(
342 options,
343 resolved,
344 session,
345 SessionRun {
346 changed_files,
347 start,
348 envelope: CheckEnvelope::DeadCode,
349 },
350 post_filter,
351 )
352}
353
354#[derive(Clone, Copy)]
358struct SessionRun<'a> {
359 changed_files: Option<&'a FxHashSet<std::path::PathBuf>>,
360 start: Instant,
361 envelope: CheckEnvelope,
362}
363
364fn run_dead_code_with_changed_files(
365 options: &DeadCodeOptions,
366 resolved: &ProgrammaticAnalysisContext,
367 session: &AnalysisSession,
368 run: SessionRun<'_>,
369 post_filter: impl FnOnce(&mut AnalysisResults),
370) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
371 let SessionRun {
372 changed_files,
373 start,
374 envelope,
375 } = run;
376 resolved.ensure_not_cancelled("dead-code analysis")?;
377 let mut results = analyze_session_dead_code(session)?;
378 let unfiltered_unused_files = results.unused_files.clone();
379 let finished = finish_dead_code_results(
380 DeadCodeReportInputs {
381 options,
382 resolved,
383 session,
384 changed_files,
385 unfiltered_unused_files,
386 finding_ids: None,
387 },
388 &mut results,
389 post_filter,
390 )?;
391 Ok(build_dead_code_programmatic_output(
392 options,
393 resolved,
394 session,
395 finished.into_report(results),
396 start,
397 envelope,
398 ))
399}
400
401fn analyze_dead_code_results(
404 options: &DeadCodeOptions,
405 resolved: &ProgrammaticAnalysisContext,
406 session: &AnalysisSession,
407 finding_ids: Option<fallow_engine::dead_code::FindingIdFilter>,
408) -> ProgrammaticResult<(AnalysisResults, FinishedDeadCode)> {
409 resolved.ensure_not_cancelled("dead-code analysis")?;
410 let mut results = analyze_session_dead_code(session)?;
411 let unfiltered_unused_files = results.unused_files.clone();
412
413 let finished = finish_dead_code_results(
414 DeadCodeReportInputs {
415 options,
416 resolved,
417 session,
418 changed_files: None,
419 unfiltered_unused_files,
420 finding_ids,
421 },
422 &mut results,
423 |_| {},
424 )?;
425 Ok((results, finished))
426}
427
428fn analyze_session_dead_code(session: &AnalysisSession) -> ProgrammaticResult<AnalysisResults> {
429 session
430 .analyze_dead_code()
431 .map(|analysis| analysis.results)
432 .map_err(|err| {
433 map_engine_error(
434 &err,
435 "dead-code analysis failed",
436 "FALLOW_DEAD_CODE_FAILED",
437 "dead-code",
438 )
439 })
440}
441
442fn apply_baseline(
447 results: &mut AnalysisResults,
448 baseline: &Path,
449 root: &Path,
450 type_aware_meta: Option<&fallow_types::envelope::TypeAwareMeta>,
451) -> ProgrammaticResult<Option<fallow_types::workspace::WorkspaceDiagnostic>> {
452 use fallow_engine::baseline::{DeadCodeBaselineError, apply_dead_code_baseline};
453
454 let path = if is_absolute_path_any_platform(baseline) {
455 baseline.to_path_buf()
456 } else {
457 root.join(baseline)
458 };
459 let content = std::fs::read_to_string(&path).map_err(|err| {
460 ProgrammaticError::new(
461 format!("failed to read baseline {}: {err}", path.display()),
462 2,
463 )
464 .with_code("FALLOW_BASELINE_READ_FAILED")
465 .with_context("baseline")
466 })?;
467 let identity = type_aware_meta
468 .and_then(|meta| meta.identity.clone())
469 .unwrap_or_default();
470 let run_state = fallow_config::node_modules_state(root);
471 apply_dead_code_baseline(results, &content, root, &identity, false)
472 .map(|_| {
473 fallow_engine::baseline::install_state_diagnostic(root, &path, &content, run_state)
474 })
475 .map_err(|err| match err {
476 DeadCodeBaselineError::Parse(message) => ProgrammaticError::new(
477 format!("failed to parse baseline {}: {message}", path.display()),
478 2,
479 )
480 .with_code("FALLOW_BASELINE_INVALID")
481 .with_context("baseline"),
482 DeadCodeBaselineError::IncompatibleIdentity(fields) => ProgrammaticError::new(
483 format!(
484 "baseline analysis identity is incompatible in: {}. Save it again with the same analysis options",
485 fields.join(", ")
486 ),
487 2,
488 )
489 .with_code("FALLOW_BASELINE_IDENTITY_INCOMPATIBLE")
490 .with_context("baseline"),
491 })
492}
493
494pub(super) fn run_dead_code_with_session_artifacts(
495 options: &DeadCodeOptions,
496 resolved: &ProgrammaticAnalysisContext,
497 session: &AnalysisSession,
498 changed_files: Option<&FxHashSet<std::path::PathBuf>>,
499 post_filter: impl FnOnce(&mut AnalysisResults),
500 start: Instant,
501) -> ProgrammaticResult<DeadCodeProgrammaticRunWithArtifacts> {
502 resolved.ensure_not_cancelled("dead-code analysis")?;
503 let mut artifacts = session
504 .analyze_dead_code_with_artifacts(true, true)
505 .map_err(|err| {
506 map_engine_error(
507 &err,
508 "dead-code analysis failed",
509 "FALLOW_DEAD_CODE_FAILED",
510 "dead-code",
511 )
512 })?;
513 let unfiltered_unused_files = artifacts.results.unused_files.clone();
514
515 let finished = finish_dead_code_results(
516 DeadCodeReportInputs {
517 options,
518 resolved,
519 session,
520 changed_files,
521 unfiltered_unused_files,
522 finding_ids: None,
523 },
524 &mut artifacts.results,
525 post_filter,
526 )?;
527
528 Ok(build_dead_code_run_with_artifacts(
529 options, resolved, session, artifacts, finished, start,
530 ))
531}
532
533pub(super) fn run_dead_code_from_artifacts(
534 options: &DeadCodeOptions,
535 resolved: &ProgrammaticAnalysisContext,
536 session: &AnalysisSession,
537 changed_files: Option<&FxHashSet<std::path::PathBuf>>,
538 mut artifacts: DeadCodeAnalysisArtifacts,
539 start: Instant,
540) -> ProgrammaticResult<DeadCodeProgrammaticRunWithArtifacts> {
541 let unfiltered_unused_files = artifacts.results.unused_files.clone();
542 let finished = finish_dead_code_results(
543 DeadCodeReportInputs {
544 options,
545 resolved,
546 session,
547 changed_files,
548 unfiltered_unused_files,
549 finding_ids: None,
550 },
551 &mut artifacts.results,
552 |_| {},
553 )?;
554
555 Ok(build_dead_code_run_with_artifacts(
556 options, resolved, session, artifacts, finished, start,
557 ))
558}
559
560struct DeadCodeReportInputs<'a> {
563 options: &'a DeadCodeOptions,
564 resolved: &'a ProgrammaticAnalysisContext,
565 session: &'a AnalysisSession,
566 changed_files: Option<&'a FxHashSet<std::path::PathBuf>>,
567 unfiltered_unused_files: Vec<fallow_types::output_dead_code::UnusedFileFinding>,
570 finding_ids: Option<fallow_engine::dead_code::FindingIdFilter>,
573}
574
575struct FinishedDeadCode {
577 type_aware_meta: Option<fallow_types::envelope::TypeAwareMeta>,
578 finding_id_trace: Option<fallow_engine::dead_code::FindingIdTrace>,
579 change_reason: Option<fallow_output::ScopeReason>,
581 package_baselines: Vec<fallow_output::PackageBaselineStatus>,
583}
584
585impl FinishedDeadCode {
586 fn into_report(self, results: AnalysisResults) -> DeadCodeReport {
587 DeadCodeReport {
588 results,
589 type_aware_meta: self.type_aware_meta,
590 package_baselines: self.package_baselines,
591 }
592 }
593}
594
595struct DeadCodeReport {
597 results: AnalysisResults,
598 type_aware_meta: Option<fallow_types::envelope::TypeAwareMeta>,
599 package_baselines: Vec<fallow_output::PackageBaselineStatus>,
600}
601
602fn finish_dead_code_results(
611 inputs: DeadCodeReportInputs<'_>,
612 results: &mut AnalysisResults,
613 post_filter: impl FnOnce(&mut AnalysisResults),
614) -> ProgrammaticResult<FinishedDeadCode> {
615 let DeadCodeReportInputs {
616 options,
617 resolved,
618 session,
619 changed_files,
620 unfiltered_unused_files,
621 finding_ids,
622 } = inputs;
623 let mut finding_id_trace =
624 finding_ids.map(|filter| fallow_engine::dead_code::FindingIdTrace::start(filter, results));
625 let resolved_changed_files = if changed_files.is_some() {
626 None
627 } else {
628 changed_files_for_run(resolved)?
629 };
630 let global_files = changed_files.or(resolved_changed_files.as_ref());
631 if global_files.is_some() {
632 resolved
633 .measure_changed_since_scope(session.files().iter().map(|file| file.path.as_path()));
634 }
635 let change_scope =
636 resolved.change_scope(global_files, session.config(), session.workspaces())?;
637 apply_dead_code_scope(options, resolved, session, &change_scope, results)?;
638 apply_dead_code_filters(&options.filters, results);
639 fallow_engine::dead_code::apply_rule_severities(results, session.config());
640 post_filter(results);
641 if let Some(trace) = finding_id_trace.as_mut() {
642 trace.end_stage(results);
643 }
644 let type_aware_meta = refine_with_unfiltered_unused_files(
645 &options.analysis.type_aware,
646 &options.filters,
647 session,
648 results,
649 unfiltered_unused_files,
650 )?;
651 if type_aware_meta.is_some() {
652 apply_dead_code_scope(options, resolved, session, &change_scope, results)?;
655 }
656 Ok(FinishedDeadCode {
657 type_aware_meta,
658 finding_id_trace,
659 change_reason: change_scope.scope_reason(),
660 package_baselines: change_scope.package_baselines(),
661 })
662}
663
664fn refine_with_unfiltered_unused_files(
665 options: &crate::TypeAwareOptions,
666 filters: &DeadCodeFilters,
667 session: &AnalysisSession,
668 results: &mut AnalysisResults,
669 unfiltered_unused_files: Vec<fallow_types::output_dead_code::UnusedFileFinding>,
670) -> ProgrammaticResult<Option<fallow_types::envelope::TypeAwareMeta>> {
671 let reported_unused_files =
672 std::mem::replace(&mut results.unused_files, unfiltered_unused_files);
673 let outcome =
674 crate::type_aware::refine_programmatic_dead_code(options, filters, session, results);
675 results.unused_files = reported_unused_files;
676 if options.requested() {
677 fallow_engine::dead_code::apply_rule_severities(results, session.config());
682 }
683 outcome
684}
685
686fn build_dead_code_run_with_artifacts(
687 options: &DeadCodeOptions,
688 resolved: &ProgrammaticAnalysisContext,
689 session: &AnalysisSession,
690 artifacts: DeadCodeAnalysisArtifacts,
691 finished: FinishedDeadCode,
692 start: Instant,
693) -> DeadCodeProgrammaticRunWithArtifacts {
694 let output = build_dead_code_programmatic_output(
695 options,
696 resolved,
697 session,
698 finished.into_report(artifacts.results.clone()),
699 start,
700 CheckEnvelope::DeadCode,
701 );
702 DeadCodeProgrammaticRunWithArtifacts { output, artifacts }
703}
704
705fn build_dead_code_programmatic_output(
706 options: &DeadCodeOptions,
707 resolved: &ProgrammaticAnalysisContext,
708 session: &AnalysisSession,
709 report: DeadCodeReport,
710 start: Instant,
711 envelope: CheckEnvelope,
712) -> DeadCodeProgrammaticOutput {
713 let DeadCodeReport {
714 results,
715 type_aware_meta,
716 package_baselines,
717 } = report;
718 let root = session.root();
719 let next_steps = build_dead_code_next_steps(DeadCodeNextStepsInput {
720 suggestions_enabled: suggestions_enabled(),
721 results: &results,
722 root,
723 offer_setup: setup_pointer_applicable(root),
724 impact_digest: None,
725 workspace_ref: default_workspace_ref_for_workspaces(root, session.workspaces()).as_deref(),
726 audit_changed: audit_changed_applicable(root),
727 has_external_plugins: !fallow_config::discover_external_plugins(root, &[]).is_empty(),
728 baseline_recheck: None,
731 command: envelope.command(),
732 });
733 let config_fixable =
734 fallow_config::is_config_fixable(&resolved.root, resolved.config_path.as_ref());
735 let mut meta = options.analysis.explain.then(|| match envelope {
736 CheckEnvelope::DeadCode => check_meta(),
737 CheckEnvelope::Architecture => architecture_meta(),
738 });
739 if let Some(type_aware) = type_aware_meta {
740 meta.get_or_insert_with(Default::default).type_aware = Some(type_aware);
741 }
742 let reports_dependencies =
745 options.filters.reports_dependency_findings() && options.files.is_empty();
746 let workspace_diagnostics = fallow_types::workspace::merge_workspace_diagnostics(
747 session.current_workspace_diagnostics(),
748 fallow_engine::dead_code::config_pattern_diagnostics(
749 session.config(),
750 reports_dependencies,
751 ),
752 );
753 let mut output = build_check_output(CheckOutputInput {
754 schema_version: envelope.schema_version(),
755 version: env!("CARGO_PKG_VERSION").to_string(),
756 elapsed: start.elapsed(),
757 results,
758 config_fixable,
759 meta,
760 workspace_diagnostics,
761 next_steps,
762 });
763 output.request_outcomes = resolved.request_outcomes();
764 output.package_baselines = package_baselines;
765 DeadCodeProgrammaticOutput {
766 output,
767 root: session.root().to_path_buf(),
768 config_fixable,
769 telemetry_analysis_run_id: None,
770 }
771}
772
773fn keep_circular_dependencies(results: &mut AnalysisResults) {
774 let entry_point_summary = results.entry_point_summary.take();
775 let circular_dependencies = std::mem::take(&mut results.circular_dependencies);
776 *results = AnalysisResults::default();
777 results.entry_point_summary = entry_point_summary;
778 results.circular_dependencies = circular_dependencies;
779}
780
781fn keep_boundary_violations(results: &mut AnalysisResults) {
782 let entry_point_summary = results.entry_point_summary.take();
783 let boundary_violations = std::mem::take(&mut results.boundary_violations);
784 let boundary_coverage_violations = std::mem::take(&mut results.boundary_coverage_violations);
785 let boundary_call_violations = std::mem::take(&mut results.boundary_call_violations);
786 *results = AnalysisResults::default();
787 results.entry_point_summary = entry_point_summary;
788 results.boundary_violations = boundary_violations;
789 results.boundary_coverage_violations = boundary_coverage_violations;
790 results.boundary_call_violations = boundary_call_violations;
791}
792
793pub(super) fn load_dead_code_session(
794 options: &DeadCodeOptions,
795 resolved: &ProgrammaticAnalysisContext,
796) -> ProgrammaticResult<AnalysisSession> {
797 let project_config = fallow_engine::project_config::config_for_project_analysis(
798 &resolved.root,
799 resolved.config_path.as_deref(),
800 ProjectConfigOptions {
801 output: OutputFormat::Json,
802 no_cache: resolved.no_cache,
803 threads: resolved.threads,
804 production_override: resolved.production_override,
805 quiet: true,
806 analysis: ProductionAnalysis::DeadCode,
807 allow_remote_extends: resolved.allow_remote_extends,
808 },
809 )
810 .map_err(|err| {
811 ProgrammaticError::new(format!("failed to load config: {err}"), 2)
812 .with_code("FALLOW_CONFIG_LOAD_FAILED")
813 .with_context("analysis.configPath")
814 })?;
815 let project_config = configure_project_for_dead_code(project_config, options);
816 Ok(attach_cancellation(
817 AnalysisSession::from_config(project_config),
818 resolved,
819 ))
820}
821
822pub(super) fn resolve_package_map_before_analysis(
829 resolved: &ProgrammaticAnalysisContext,
830 session: &AnalysisSession,
831) -> ProgrammaticResult<()> {
832 resolved
833 .change_scope(None, session.config(), session.workspaces())
834 .map(drop)
835}
836
837pub(super) fn attach_cancellation(
842 session: AnalysisSession,
843 resolved: &ProgrammaticAnalysisContext,
844) -> AnalysisSession {
845 match resolved.cancellation() {
846 Some(cancellation) => session.with_cancellation(std::sync::Arc::clone(cancellation)),
847 None => session,
848 }
849}
850
851pub(super) fn default_dead_code_options_for_context(
852 resolved: &ProgrammaticAnalysisContext,
853) -> DeadCodeOptions {
854 DeadCodeOptions {
855 finding_ids: Vec::new(),
856 analysis: AnalysisOptions {
857 root: Some(resolved.root().to_path_buf()),
858 config_path: resolved.config_path().clone(),
859 no_cache: resolved.no_cache(),
860 threads: Some(resolved.threads()),
861 production_override: resolved.production_override(),
862 changed_since: resolved.changed_since().map(str::to_owned),
863 workspace: resolved.workspace().map(<[String]>::to_vec),
864 changed_workspaces: resolved.changed_workspaces().map(str::to_owned),
865 explain: resolved.explain_enabled(),
866 ..AnalysisOptions::default()
867 },
868 filters: DeadCodeFilters::default(),
869 files: Vec::new(),
870 include_entry_exports: false,
871 show_cascade: false,
872 }
873}
874
875fn configure_project_for_dead_code(
876 mut project_config: ProjectConfig,
877 options: &DeadCodeOptions,
878) -> ProjectConfig {
879 if options.include_entry_exports {
880 project_config.config.include_entry_exports = true;
881 }
882 if options.show_cascade {
883 project_config.config.show_cascade = true;
884 }
885 activate_explicit_dead_code_opt_ins(&options.filters, &mut project_config.config.rules);
886 apply_explicit_type_aware_choice(options, &mut project_config.config);
887 project_config
888}
889
890fn apply_explicit_type_aware_choice(
897 options: &DeadCodeOptions,
898 config: &mut fallow_config::ResolvedConfig,
899) {
900 let Some(enabled) = options.analysis.type_aware.enabled else {
901 return;
902 };
903 config.type_aware.enabled = enabled;
904 if enabled
905 && !options.filters.any_active()
906 && config.rules.private_type_leaks == fallow_config::Severity::Off
907 && !config.rules.private_type_leaks_configured
908 {
909 config.rules.private_type_leaks = fallow_config::Severity::Warn;
910 }
911}
912
913fn activate_explicit_dead_code_opt_ins(
914 filters: &DeadCodeFilters,
915 rules: &mut fallow_config::RulesConfig,
916) {
917 if filters.absent_component_props
918 && rules.absent_component_props == fallow_config::Severity::Off
919 {
920 rules.absent_component_props = fallow_config::Severity::Warn;
921 }
922 if filters.private_type_leaks && rules.private_type_leaks == fallow_config::Severity::Off {
923 rules.private_type_leaks = fallow_config::Severity::Warn;
924 }
925 if filters.deprecated_exports_in_use
926 && rules.deprecated_exports_in_use == fallow_config::Severity::Off
927 {
928 rules.deprecated_exports_in_use = fallow_config::Severity::Warn;
929 }
930}
931
932fn apply_dead_code_scope(
933 options: &DeadCodeOptions,
934 resolved: &ProgrammaticAnalysisContext,
935 session: &AnalysisSession,
936 change_scope: &ChangeScope,
937 results: &mut AnalysisResults,
938) -> ProgrammaticResult<()> {
939 let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
940 let files = file_scope(options, session.root());
941 fallow_engine::dead_code::apply_scope(
942 results,
943 &fallow_engine::dead_code::DeadCodeScope {
944 workspace_roots: workspace_roots.as_deref(),
945 changes: Some(change_scope),
946 diff: resolved.diff.as_ref().map(|diff| (diff, session.root())),
947 files: files.as_ref(),
948 },
949 session.config(),
950 );
951 Ok(())
952}
953
954fn file_scope(options: &DeadCodeOptions, root: &Path) -> Option<FxHashSet<std::path::PathBuf>> {
956 if options.files.is_empty() {
957 return None;
958 }
959 Some(
960 options
961 .files
962 .iter()
963 .map(|path| {
964 if is_absolute_path_any_platform(path) {
965 path.clone()
966 } else {
967 root.join(path)
968 }
969 })
970 .collect(),
971 )
972}
973
974fn apply_dead_code_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
975 if !dead_code_filters_active(filters) {
976 return;
977 }
978 apply_dead_code_core_filters(filters, results);
979 apply_dead_code_component_filters(filters, results);
980 apply_dead_code_graph_filters(filters, results);
981 apply_dead_code_policy_filters(filters, results);
982 apply_dead_code_catalog_filters(filters, results);
983}
984
985fn dead_code_filters_active(filters: &DeadCodeFilters) -> bool {
986 filters.unused_files
987 || filters.unused_exports
988 || filters.unused_deps
989 || filters.unused_types
990 || filters.private_type_leaks
991 || filters.deprecated_exports_in_use
992 || filters.unused_enum_members
993 || filters.unused_class_members
994 || filters.unused_store_members
995 || filters.unprovided_injects
996 || filters.unrendered_components
997 || filters.unused_component_props
998 || filters.absent_component_props
999 || filters.unused_component_emits
1000 || filters.unused_component_inputs
1001 || filters.unused_component_outputs
1002 || filters.unused_svelte_events
1003 || filters.unused_effect_layers
1004 || filters.unused_tagged_errors
1005 || filters.unproduced_catch_tags
1006 || filters.unused_server_actions
1007 || filters.unused_load_data_keys
1008 || filters.unresolved_imports
1009 || filters.unlisted_deps
1010 || filters.duplicate_exports
1011 || filters.circular_deps
1012 || filters.re_export_cycles
1013 || filters.package_cycles
1014 || filters.boundary_violations
1015 || filters.policy_violations
1016 || filters.stale_suppressions
1017 || filters.unused_catalog_entries
1018 || filters.empty_catalog_groups
1019 || filters.unresolved_catalog_references
1020 || filters.unused_dependency_overrides
1021 || filters.misconfigured_dependency_overrides
1022}
1023
1024fn apply_dead_code_core_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
1025 if !filters.unused_files {
1026 results.unused_files.clear();
1027 results.cascade.suppressed_unused_files.clear();
1028 }
1029 if !filters.unused_exports {
1030 results.unused_exports.clear();
1031 }
1032 if !filters.unused_types {
1033 results.unused_types.clear();
1034 }
1035 if !filters.private_type_leaks {
1036 results.private_type_leaks.clear();
1037 }
1038 if !filters.deprecated_exports_in_use {
1039 results.deprecated_exports_in_use.clear();
1040 }
1041 if !filters.unused_deps {
1042 clear_dead_code_dependency_findings(results);
1043 }
1044 if !filters.unused_enum_members {
1045 results.unused_enum_members.clear();
1046 }
1047 if !filters.unused_class_members {
1048 results.unused_class_members.clear();
1049 }
1050 if !filters.unused_store_members {
1051 results.unused_store_members.clear();
1052 }
1053 if !filters.unlisted_deps {
1054 results.unlisted_dependencies.clear();
1055 }
1056}
1057
1058fn clear_dead_code_dependency_findings(results: &mut AnalysisResults) {
1059 results.unused_dependencies.clear();
1060 results.unused_dev_dependencies.clear();
1061 results.unused_optional_dependencies.clear();
1062 results.type_only_dependencies.clear();
1063 results.test_only_dependencies.clear();
1064 results.dev_dependencies_in_production.clear();
1065}
1066
1067fn apply_dead_code_component_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
1068 if !filters.unprovided_injects {
1069 results.unprovided_injects.clear();
1070 }
1071 if !filters.unrendered_components {
1072 results.unrendered_components.clear();
1073 }
1074 if !filters.absent_component_props {
1075 results.absent_component_props.clear();
1076 }
1077 if !filters.unused_component_props {
1078 results.unused_component_props.clear();
1079 }
1080 if !filters.unused_component_emits {
1081 results.unused_component_emits.clear();
1082 }
1083 if !filters.unused_component_inputs {
1084 results.unused_component_inputs.clear();
1085 }
1086 if !filters.unused_component_outputs {
1087 results.unused_component_outputs.clear();
1088 }
1089 if !filters.unused_svelte_events {
1090 results.unused_svelte_events.clear();
1091 }
1092 if !filters.unused_effect_layers {
1093 results.unused_effect_layers.clear();
1094 }
1095 if !filters.unused_tagged_errors {
1096 results.unused_tagged_errors.clear();
1097 }
1098 if !filters.unproduced_catch_tags {
1099 results.unproduced_catch_tags.clear();
1100 }
1101 if !filters.unused_server_actions {
1102 results.unused_server_actions.clear();
1103 }
1104 if !filters.unused_load_data_keys {
1105 results.unused_load_data_keys.clear();
1106 }
1107 if !filters.unresolved_imports {
1108 results.unresolved_imports.clear();
1109 }
1110}
1111
1112fn apply_dead_code_graph_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
1113 if !filters.duplicate_exports {
1114 results.duplicate_exports.clear();
1115 }
1116 if !filters.circular_deps {
1117 results.circular_dependencies.clear();
1118 }
1119 if !filters.re_export_cycles {
1120 results.re_export_cycles.clear();
1121 }
1122 if !filters.package_cycles {
1123 results.package_cycles.clear();
1124 }
1125 if !filters.boundary_violations {
1126 results.boundary_violations.clear();
1127 results.boundary_coverage_violations.clear();
1128 results.boundary_call_violations.clear();
1129 }
1130}
1131
1132fn apply_dead_code_policy_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
1133 if !filters.policy_violations {
1134 results.policy_violations.clear();
1135 }
1136 if !filters.stale_suppressions {
1137 results.stale_suppressions.clear();
1138 }
1139}
1140
1141fn apply_dead_code_catalog_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
1142 if !filters.unused_catalog_entries {
1143 results.unused_catalog_entries.clear();
1144 }
1145 if !filters.empty_catalog_groups {
1146 results.empty_catalog_groups.clear();
1147 }
1148 if !filters.unresolved_catalog_references {
1149 results.unresolved_catalog_references.clear();
1150 }
1151 if !filters.unused_dependency_overrides {
1152 results.unused_dependency_overrides.clear();
1153 }
1154 if !filters.misconfigured_dependency_overrides {
1155 results.misconfigured_dependency_overrides.clear();
1156 }
1157}