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