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