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