Skip to main content

fallow_api/runtime/
dead_code.rs

1use std::path::Path;
2use std::time::Instant;
3
4use fallow_config::ProductionAnalysis;
5use fallow_engine::{
6    dead_code::DeadCodeAnalysisArtifacts,
7    project_config::{ProjectConfig, ProjectConfigOptions},
8    session::AnalysisSession,
9};
10use fallow_output::{
11    CHECK_SCHEMA_VERSION, CheckOutputInput, DeadCodeNextStepsInput, DiffIndex, build_check_output,
12    build_dead_code_next_steps, check_meta,
13};
14use fallow_types::output_format::OutputFormat;
15use fallow_types::path_util::is_absolute_path_any_platform;
16use fallow_types::results::{AnalysisResults, TraceHopRole};
17use rustc_hash::FxHashSet;
18
19use crate::{
20    AnalysisOptions, BoundaryViolationsProgrammaticOutput, CircularDependenciesProgrammaticOutput,
21    DeadCodeFilters, DeadCodeOptions, DeadCodeProgrammaticOutput, ProgrammaticError,
22    analysis_context::{
23        ProgrammaticAnalysisContext, changed_files_for_run,
24        resolve_programmatic_analysis_context_deferred_workspace, workspace_roots_for_session,
25    },
26    next_steps::{
27        default_workspace_ref_for_workspaces, setup_pointer_applicable, suggestions_enabled,
28    },
29};
30
31use super::{ProgrammaticResult, root_envelope_mode};
32
33pub(super) struct DeadCodeProgrammaticRunWithArtifacts {
34    pub output: DeadCodeProgrammaticOutput,
35    pub artifacts: DeadCodeAnalysisArtifacts,
36}
37
38/// Run dead-code analysis and return typed API output before serialization.
39///
40/// # Errors
41///
42/// Returns a structured programmatic error for unsupported options, invalid
43/// options, config load failures, analysis failures, or git changed-file
44/// failures.
45pub fn run_dead_code(options: &DeadCodeOptions) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
46    let resolved = resolve_programmatic_analysis_context_deferred_workspace(&options.analysis)?;
47    resolved.install(|| run_dead_code_inner(options, &resolved, |_| {}))
48}
49
50/// Turn an engine failure into a programmatic error, keeping a cancelled run
51/// distinguishable from a failed one.
52///
53/// The engine's cancellation message already names the pipeline boundary the
54/// run stopped at, so it is carried through rather than replaced.
55pub(super) fn map_engine_error(
56    err: &fallow_engine::EngineError,
57    failure_message: &str,
58    failure_code: &'static str,
59    context: &'static str,
60) -> ProgrammaticError {
61    if err.is_cancelled() {
62        return crate::analysis_context::cancelled_error_message(err.message())
63            .with_context(context);
64    }
65    ProgrammaticError::new(format!("{failure_message}: {err}"), 2)
66        .with_code(failure_code)
67        .with_context(context)
68}
69
70/// Run circular-dependency analysis and return typed API output before JSON.
71///
72/// # Errors
73///
74/// Returns the same structured errors as [`run_dead_code`].
75pub fn run_circular_dependencies(
76    options: &DeadCodeOptions,
77) -> ProgrammaticResult<CircularDependenciesProgrammaticOutput> {
78    let resolved = resolve_programmatic_analysis_context_deferred_workspace(&options.analysis)?;
79    resolved.install(|| {
80        run_dead_code_inner(options, &resolved, keep_circular_dependencies).map(Into::into)
81    })
82}
83
84/// Run boundary-family analysis and return typed API output before JSON.
85///
86/// # Errors
87///
88/// Returns the same structured errors as [`run_dead_code`].
89pub fn run_boundary_violations(
90    options: &DeadCodeOptions,
91) -> ProgrammaticResult<BoundaryViolationsProgrammaticOutput> {
92    let resolved = resolve_programmatic_analysis_context_deferred_workspace(&options.analysis)?;
93    resolved.install(|| {
94        run_dead_code_inner(options, &resolved, keep_boundary_violations).map(Into::into)
95    })
96}
97
98fn run_dead_code_inner(
99    options: &DeadCodeOptions,
100    resolved: &ProgrammaticAnalysisContext,
101    post_filter: impl FnOnce(&mut AnalysisResults),
102) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
103    let start = Instant::now();
104    resolved.ensure_not_cancelled("config load and file discovery")?;
105    let session = load_dead_code_session(options, resolved)?;
106    run_dead_code_with_session(options, resolved, &session, None, post_filter, start)
107}
108
109pub(super) fn run_dead_code_with_session(
110    options: &DeadCodeOptions,
111    resolved: &ProgrammaticAnalysisContext,
112    session: &AnalysisSession,
113    changed_files: Option<&FxHashSet<std::path::PathBuf>>,
114    post_filter: impl FnOnce(&mut AnalysisResults),
115    start: Instant,
116) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
117    resolved.ensure_not_cancelled("dead-code analysis")?;
118    let analysis = session.analyze_dead_code().map_err(|err| {
119        map_engine_error(
120            &err,
121            "dead-code analysis failed",
122            "FALLOW_DEAD_CODE_FAILED",
123            "dead-code",
124        )
125    })?;
126    let mut results = analysis.results;
127    let unfiltered_unused_files = results.unused_files.clone();
128
129    let type_aware_meta = finish_dead_code_results(
130        DeadCodeReportInputs {
131            options,
132            resolved,
133            session,
134            changed_files,
135            unfiltered_unused_files,
136        },
137        &mut results,
138        post_filter,
139    )?;
140
141    Ok(build_dead_code_programmatic_output(
142        options,
143        resolved,
144        session,
145        results,
146        type_aware_meta,
147        start,
148    ))
149}
150
151pub(super) fn run_dead_code_with_session_artifacts(
152    options: &DeadCodeOptions,
153    resolved: &ProgrammaticAnalysisContext,
154    session: &AnalysisSession,
155    changed_files: Option<&FxHashSet<std::path::PathBuf>>,
156    post_filter: impl FnOnce(&mut AnalysisResults),
157    start: Instant,
158) -> ProgrammaticResult<DeadCodeProgrammaticRunWithArtifacts> {
159    resolved.ensure_not_cancelled("dead-code analysis")?;
160    let mut artifacts = session
161        .analyze_dead_code_with_artifacts(true, true)
162        .map_err(|err| {
163            map_engine_error(
164                &err,
165                "dead-code analysis failed",
166                "FALLOW_DEAD_CODE_FAILED",
167                "dead-code",
168            )
169        })?;
170    let unfiltered_unused_files = artifacts.results.unused_files.clone();
171
172    let type_aware_meta = finish_dead_code_results(
173        DeadCodeReportInputs {
174            options,
175            resolved,
176            session,
177            changed_files,
178            unfiltered_unused_files,
179        },
180        &mut artifacts.results,
181        post_filter,
182    )?;
183
184    Ok(build_dead_code_run_with_artifacts(
185        options,
186        resolved,
187        session,
188        artifacts,
189        type_aware_meta,
190        start,
191    ))
192}
193
194pub(super) fn run_dead_code_from_artifacts(
195    options: &DeadCodeOptions,
196    resolved: &ProgrammaticAnalysisContext,
197    session: &AnalysisSession,
198    changed_files: Option<&FxHashSet<std::path::PathBuf>>,
199    mut artifacts: DeadCodeAnalysisArtifacts,
200    start: Instant,
201) -> ProgrammaticResult<DeadCodeProgrammaticRunWithArtifacts> {
202    let unfiltered_unused_files = artifacts.results.unused_files.clone();
203    let type_aware_meta = finish_dead_code_results(
204        DeadCodeReportInputs {
205            options,
206            resolved,
207            session,
208            changed_files,
209            unfiltered_unused_files,
210        },
211        &mut artifacts.results,
212        |_| {},
213    )?;
214
215    Ok(build_dead_code_run_with_artifacts(
216        options,
217        resolved,
218        session,
219        artifacts,
220        type_aware_meta,
221        start,
222    ))
223}
224
225/// Inputs the reporting tail needs from whichever entry point produced the
226/// analysis.
227struct DeadCodeReportInputs<'a> {
228    options: &'a DeadCodeOptions,
229    resolved: &'a ProgrammaticAnalysisContext,
230    session: &'a AnalysisSession,
231    changed_files: Option<&'a FxHashSet<std::path::PathBuf>>,
232    /// Unused files as the engine reported them, before scope narrowing, which
233    /// type-aware refinement needs to reason about the whole project.
234    unfiltered_unused_files: Vec<fallow_types::output_dead_code::UnusedFileFinding>,
235}
236
237/// Shared reporting tail for every programmatic dead-code entry point: scope,
238/// issue-type filters, effective rule severities, the caller's family filter,
239/// and type-aware refinement.
240///
241/// The severity pass belongs here rather than at each entry point. Spelling
242/// the sequence out per entry point is what let the programmatic runtime
243/// report findings for rules a project had turned off while the CLI and the
244/// editor did not.
245fn finish_dead_code_results(
246    inputs: DeadCodeReportInputs<'_>,
247    results: &mut AnalysisResults,
248    post_filter: impl FnOnce(&mut AnalysisResults),
249) -> ProgrammaticResult<Option<fallow_types::envelope::TypeAwareMeta>> {
250    let DeadCodeReportInputs {
251        options,
252        resolved,
253        session,
254        changed_files,
255        unfiltered_unused_files,
256    } = inputs;
257    apply_dead_code_scope(options, resolved, session, changed_files, results)?;
258    apply_dead_code_filters(&options.filters, results);
259    fallow_engine::dead_code::apply_rule_severities(results, session.config());
260    post_filter(results);
261    refine_with_unfiltered_unused_files(
262        &options.analysis.type_aware,
263        &options.filters,
264        session,
265        results,
266        unfiltered_unused_files,
267    )
268}
269
270fn refine_with_unfiltered_unused_files(
271    options: &crate::TypeAwareOptions,
272    filters: &DeadCodeFilters,
273    session: &AnalysisSession,
274    results: &mut AnalysisResults,
275    unfiltered_unused_files: Vec<fallow_types::output_dead_code::UnusedFileFinding>,
276) -> ProgrammaticResult<Option<fallow_types::envelope::TypeAwareMeta>> {
277    let reported_unused_files =
278        std::mem::replace(&mut results.unused_files, unfiltered_unused_files);
279    let outcome =
280        crate::type_aware::refine_programmatic_dead_code(options, filters, session, results);
281    results.unused_files = reported_unused_files;
282    if options.enabled {
283        // Reconciliation can add findings, so rule severities are resolved
284        // again over the refined set. The pass only removes findings, so
285        // repeating it is idempotent. Mirrors EditorAnalysisSession.
286        fallow_engine::dead_code::apply_rule_severities(results, session.config());
287    }
288    outcome
289}
290
291fn build_dead_code_run_with_artifacts(
292    options: &DeadCodeOptions,
293    resolved: &ProgrammaticAnalysisContext,
294    session: &AnalysisSession,
295    artifacts: DeadCodeAnalysisArtifacts,
296    type_aware_meta: Option<fallow_types::envelope::TypeAwareMeta>,
297    start: Instant,
298) -> DeadCodeProgrammaticRunWithArtifacts {
299    let output = build_dead_code_programmatic_output(
300        options,
301        resolved,
302        session,
303        artifacts.results.clone(),
304        type_aware_meta,
305        start,
306    );
307    DeadCodeProgrammaticRunWithArtifacts { output, artifacts }
308}
309
310fn build_dead_code_programmatic_output(
311    options: &DeadCodeOptions,
312    resolved: &ProgrammaticAnalysisContext,
313    session: &AnalysisSession,
314    results: AnalysisResults,
315    type_aware_meta: Option<fallow_types::envelope::TypeAwareMeta>,
316    start: Instant,
317) -> DeadCodeProgrammaticOutput {
318    let root = session.root();
319    let next_steps = build_dead_code_next_steps(DeadCodeNextStepsInput {
320        suggestions_enabled: suggestions_enabled(),
321        results: &results,
322        root,
323        offer_setup: setup_pointer_applicable(root),
324        impact_digest: None,
325        workspace_ref: default_workspace_ref_for_workspaces(root, session.workspaces()).as_deref(),
326        audit_changed: fallow_engine::churn::is_git_repo(root),
327        has_external_plugins: !fallow_config::discover_external_plugins(root, &[]).is_empty(),
328        // The programmatic runtime loads no baseline, so there is never one to
329        // re-check.
330        baseline_recheck: None,
331    });
332    let config_fixable =
333        fallow_config::is_config_fixable(&resolved.root, resolved.config_path.as_ref());
334    let mut meta = options.analysis.explain.then(check_meta);
335    if let Some(type_aware) = type_aware_meta {
336        meta.get_or_insert_with(Default::default).type_aware = Some(type_aware);
337    }
338    let output = build_check_output(CheckOutputInput {
339        schema_version: CHECK_SCHEMA_VERSION,
340        version: env!("CARGO_PKG_VERSION").to_string(),
341        elapsed: start.elapsed(),
342        results,
343        config_fixable,
344        meta,
345        workspace_diagnostics: session.current_workspace_diagnostics(),
346        next_steps,
347    });
348    DeadCodeProgrammaticOutput {
349        output,
350        root: session.root().to_path_buf(),
351        config_fixable,
352        envelope_mode: root_envelope_mode(),
353        telemetry_analysis_run_id: None,
354    }
355}
356
357fn keep_circular_dependencies(results: &mut AnalysisResults) {
358    let entry_point_summary = results.entry_point_summary.take();
359    let circular_dependencies = std::mem::take(&mut results.circular_dependencies);
360    *results = AnalysisResults::default();
361    results.entry_point_summary = entry_point_summary;
362    results.circular_dependencies = circular_dependencies;
363}
364
365fn keep_boundary_violations(results: &mut AnalysisResults) {
366    let entry_point_summary = results.entry_point_summary.take();
367    let boundary_violations = std::mem::take(&mut results.boundary_violations);
368    let boundary_coverage_violations = std::mem::take(&mut results.boundary_coverage_violations);
369    let boundary_call_violations = std::mem::take(&mut results.boundary_call_violations);
370    *results = AnalysisResults::default();
371    results.entry_point_summary = entry_point_summary;
372    results.boundary_violations = boundary_violations;
373    results.boundary_coverage_violations = boundary_coverage_violations;
374    results.boundary_call_violations = boundary_call_violations;
375}
376
377pub(super) fn load_dead_code_session(
378    options: &DeadCodeOptions,
379    resolved: &ProgrammaticAnalysisContext,
380) -> ProgrammaticResult<AnalysisSession> {
381    let project_config = fallow_engine::project_config::config_for_project_analysis(
382        &resolved.root,
383        resolved.config_path.as_deref(),
384        ProjectConfigOptions {
385            output: OutputFormat::Json,
386            no_cache: resolved.no_cache,
387            threads: resolved.threads,
388            production_override: resolved.production_override,
389            quiet: true,
390            analysis: ProductionAnalysis::DeadCode,
391            allow_remote_extends: resolved.allow_remote_extends,
392        },
393    )
394    .map_err(|err| {
395        ProgrammaticError::new(format!("failed to load config: {err}"), 2)
396            .with_code("FALLOW_CONFIG_LOAD_FAILED")
397            .with_context("analysis.configPath")
398    })?;
399    let project_config = configure_project_for_dead_code(project_config, options);
400    Ok(attach_cancellation(
401        AnalysisSession::from_config(project_config),
402        resolved,
403    ))
404}
405
406/// Hand the caller's cancellation token to the engine session.
407///
408/// Without this the session runs to completion no matter what the API layer
409/// checks between its own stages.
410pub(super) fn attach_cancellation(
411    session: AnalysisSession,
412    resolved: &ProgrammaticAnalysisContext,
413) -> AnalysisSession {
414    match resolved.cancellation() {
415        Some(cancellation) => session.with_cancellation(std::sync::Arc::clone(cancellation)),
416        None => session,
417    }
418}
419
420pub(super) fn default_dead_code_options_for_context(
421    resolved: &ProgrammaticAnalysisContext,
422) -> DeadCodeOptions {
423    DeadCodeOptions {
424        analysis: AnalysisOptions {
425            root: Some(resolved.root().to_path_buf()),
426            config_path: resolved.config_path().clone(),
427            no_cache: resolved.no_cache(),
428            threads: Some(resolved.threads()),
429            production_override: resolved.production_override(),
430            changed_since: resolved.changed_since().map(str::to_owned),
431            workspace: resolved.workspace().map(<[String]>::to_vec),
432            changed_workspaces: resolved.changed_workspaces().map(str::to_owned),
433            explain: resolved.explain_enabled(),
434            ..AnalysisOptions::default()
435        },
436        filters: DeadCodeFilters::default(),
437        files: Vec::new(),
438        include_entry_exports: false,
439    }
440}
441
442fn configure_project_for_dead_code(
443    mut project_config: ProjectConfig,
444    options: &DeadCodeOptions,
445) -> ProjectConfig {
446    if options.include_entry_exports {
447        project_config.config.include_entry_exports = true;
448    }
449    activate_explicit_dead_code_opt_ins(&options.filters, &mut project_config.config.rules);
450    project_config
451}
452
453fn activate_explicit_dead_code_opt_ins(
454    filters: &DeadCodeFilters,
455    rules: &mut fallow_config::RulesConfig,
456) {
457    if filters.private_type_leaks && rules.private_type_leaks == fallow_config::Severity::Off {
458        rules.private_type_leaks = fallow_config::Severity::Warn;
459    }
460}
461
462fn apply_dead_code_scope(
463    options: &DeadCodeOptions,
464    resolved: &ProgrammaticAnalysisContext,
465    session: &AnalysisSession,
466    changed_files: Option<&FxHashSet<std::path::PathBuf>>,
467    results: &mut AnalysisResults,
468) -> ProgrammaticResult<()> {
469    let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
470    if let Some(workspace_roots) = workspace_roots.as_ref() {
471        fallow_engine::dead_code::filter_to_workspaces(results, workspace_roots);
472    }
473    let resolved_changed_files = if changed_files.is_some() {
474        None
475    } else {
476        changed_files_for_run(resolved)?
477    };
478    if let Some(changed_files) = changed_files.or(resolved_changed_files.as_ref()) {
479        fallow_engine::dead_code::filter_by_changed_files(results, changed_files);
480    }
481    if let Some(diff) = resolved.diff.as_ref() {
482        filter_dead_code_by_diff(results, diff, session.root());
483    }
484    apply_dead_code_file_filter(options, session.root(), results);
485    Ok(())
486}
487
488fn filter_dead_code_by_diff(results: &mut AnalysisResults, diff: &DiffIndex, root: &Path) {
489    let touches_file = |path: &Path| -> bool {
490        diff.key_for(path, root)
491            .is_none_or(|rel| diff.touches_file(&rel))
492    };
493    let line_in_diff = |path: &Path, line: u32| -> bool {
494        diff.key_for(path, root)
495            .is_none_or(|rel| diff.line_is_added(&rel, u64::from(line)))
496    };
497
498    filter_dead_code_source_findings(results, &touches_file, &line_in_diff);
499    filter_dead_code_security_findings(results, &touches_file, &line_in_diff);
500    filter_dead_code_dependency_findings(results, &line_in_diff);
501    filter_dead_code_graph_findings(results, &touches_file, &line_in_diff);
502    filter_dead_code_framework_findings(results, &line_in_diff);
503}
504
505fn filter_dead_code_source_findings(
506    results: &mut AnalysisResults,
507    touches_file: &dyn Fn(&Path) -> bool,
508    line_in_diff: &dyn Fn(&Path, u32) -> bool,
509) {
510    results
511        .unused_files
512        .retain(|finding| touches_file(&finding.file.path));
513    results
514        .unused_exports
515        .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
516    results
517        .unused_types
518        .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
519    results
520        .private_type_leaks
521        .retain(|finding| line_in_diff(&finding.leak.path, finding.leak.line));
522    results
523        .unused_enum_members
524        .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
525    results
526        .unused_class_members
527        .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
528    results
529        .unused_store_members
530        .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
531    results
532        .unprovided_injects
533        .retain(|finding| line_in_diff(&finding.inject.path, finding.inject.line));
534    results
535        .unrendered_components
536        .retain(|finding| line_in_diff(&finding.component.path, finding.component.line));
537    results
538        .unused_component_props
539        .retain(|finding| line_in_diff(&finding.prop.path, finding.prop.line));
540    results
541        .unused_component_emits
542        .retain(|finding| line_in_diff(&finding.emit.path, finding.emit.line));
543    results
544        .unused_component_inputs
545        .retain(|finding| line_in_diff(&finding.input.path, finding.input.line));
546    results
547        .unused_component_outputs
548        .retain(|finding| line_in_diff(&finding.output.path, finding.output.line));
549    results
550        .unused_svelte_events
551        .retain(|finding| line_in_diff(&finding.event.path, finding.event.line));
552    results
553        .unused_server_actions
554        .retain(|finding| line_in_diff(&finding.action.path, finding.action.line));
555    results
556        .unused_load_data_keys
557        .retain(|finding| line_in_diff(&finding.key.path, finding.key.line));
558    results
559        .unresolved_imports
560        .retain(|finding| line_in_diff(&finding.import.path, finding.import.line));
561}
562
563fn filter_dead_code_security_findings(
564    results: &mut AnalysisResults,
565    touches_file: &dyn Fn(&Path) -> bool,
566    line_in_diff: &dyn Fn(&Path, u32) -> bool,
567) {
568    results.security_findings.retain(|finding| {
569        line_in_diff(&finding.path, finding.line)
570            || finding.trace.iter().any(|hop| {
571                line_in_diff(&hop.path, hop.line)
572                    || (matches!(hop.role, TraceHopRole::SecretSource) && touches_file(&hop.path))
573            })
574            || finding.reachability.as_ref().is_some_and(|reachability| {
575                reachability
576                    .untrusted_source_trace
577                    .iter()
578                    .any(|hop| line_in_diff(&hop.path, hop.line))
579            })
580    });
581    results
582        .security_unresolved_callee_diagnostics
583        .retain(|finding| line_in_diff(&finding.path, finding.line));
584}
585
586fn filter_dead_code_dependency_findings(
587    results: &mut AnalysisResults,
588    line_in_diff: &dyn Fn(&Path, u32) -> bool,
589) {
590    for finding in &mut results.unlisted_dependencies {
591        finding
592            .dep
593            .imported_from
594            .retain(|source| line_in_diff(&source.path, source.line));
595    }
596    results
597        .unlisted_dependencies
598        .retain(|finding| !finding.dep.imported_from.is_empty());
599}
600
601fn filter_dead_code_graph_findings(
602    results: &mut AnalysisResults,
603    touches_file: &dyn Fn(&Path) -> bool,
604    line_in_diff: &dyn Fn(&Path, u32) -> bool,
605) {
606    results.duplicate_exports.retain(|finding| {
607        finding
608            .export
609            .locations
610            .iter()
611            .any(|location| line_in_diff(&location.path, location.line))
612    });
613    results
614        .circular_dependencies
615        .retain(|cycle| cycle.cycle.files.iter().any(|path| touches_file(path)));
616    results
617        .re_export_cycles
618        .retain(|cycle| cycle.cycle.files.iter().any(|path| touches_file(path)));
619    results
620        .boundary_violations
621        .retain(|finding| line_in_diff(&finding.violation.from_path, finding.violation.line));
622    results
623        .stale_suppressions
624        .retain(|finding| line_in_diff(&finding.path, finding.line));
625}
626
627fn filter_dead_code_framework_findings(
628    results: &mut AnalysisResults,
629    line_in_diff: &dyn Fn(&Path, u32) -> bool,
630) {
631    results
632        .invalid_client_exports
633        .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
634    results
635        .mixed_client_server_barrels
636        .retain(|finding| line_in_diff(&finding.barrel.path, finding.barrel.line));
637    results
638        .misplaced_directives
639        .retain(|finding| line_in_diff(&finding.directive_site.path, finding.directive_site.line));
640    results
641        .route_collisions
642        .retain(|finding| line_in_diff(&finding.collision.path, finding.collision.line));
643    results
644        .dynamic_segment_name_conflicts
645        .retain(|finding| line_in_diff(&finding.conflict.path, finding.conflict.line));
646}
647
648fn apply_dead_code_file_filter(
649    options: &DeadCodeOptions,
650    root: &Path,
651    results: &mut AnalysisResults,
652) {
653    if options.files.is_empty() {
654        return;
655    }
656    let file_set = options
657        .files
658        .iter()
659        .map(|path| {
660            if is_absolute_path_any_platform(path) {
661                path.clone()
662            } else {
663                root.join(path)
664            }
665        })
666        .collect::<FxHashSet<_>>();
667    fallow_engine::dead_code::filter_by_changed_files(results, &file_set);
668    clear_dead_code_dependency_findings(results);
669}
670
671fn apply_dead_code_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
672    if !dead_code_filters_active(filters) {
673        return;
674    }
675    apply_dead_code_core_filters(filters, results);
676    apply_dead_code_component_filters(filters, results);
677    apply_dead_code_graph_filters(filters, results);
678    apply_dead_code_policy_filters(filters, results);
679    apply_dead_code_catalog_filters(filters, results);
680}
681
682fn dead_code_filters_active(filters: &DeadCodeFilters) -> bool {
683    filters.unused_files
684        || filters.unused_exports
685        || filters.unused_deps
686        || filters.unused_types
687        || filters.private_type_leaks
688        || filters.unused_enum_members
689        || filters.unused_class_members
690        || filters.unused_store_members
691        || filters.unprovided_injects
692        || filters.unrendered_components
693        || filters.unused_component_props
694        || filters.unused_component_emits
695        || filters.unused_component_inputs
696        || filters.unused_component_outputs
697        || filters.unused_svelte_events
698        || filters.unused_server_actions
699        || filters.unused_load_data_keys
700        || filters.unresolved_imports
701        || filters.unlisted_deps
702        || filters.duplicate_exports
703        || filters.circular_deps
704        || filters.re_export_cycles
705        || filters.boundary_violations
706        || filters.policy_violations
707        || filters.stale_suppressions
708        || filters.unused_catalog_entries
709        || filters.empty_catalog_groups
710        || filters.unresolved_catalog_references
711        || filters.unused_dependency_overrides
712        || filters.misconfigured_dependency_overrides
713}
714
715fn apply_dead_code_core_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
716    if !filters.unused_files {
717        results.unused_files.clear();
718    }
719    if !filters.unused_exports {
720        results.unused_exports.clear();
721    }
722    if !filters.unused_types {
723        results.unused_types.clear();
724    }
725    if !filters.private_type_leaks {
726        results.private_type_leaks.clear();
727    }
728    if !filters.unused_deps {
729        clear_dead_code_dependency_findings(results);
730    }
731    if !filters.unused_enum_members {
732        results.unused_enum_members.clear();
733    }
734    if !filters.unused_class_members {
735        results.unused_class_members.clear();
736    }
737    if !filters.unused_store_members {
738        results.unused_store_members.clear();
739    }
740    if !filters.unlisted_deps {
741        results.unlisted_dependencies.clear();
742    }
743}
744
745fn clear_dead_code_dependency_findings(results: &mut AnalysisResults) {
746    results.unused_dependencies.clear();
747    results.unused_dev_dependencies.clear();
748    results.unused_optional_dependencies.clear();
749    results.type_only_dependencies.clear();
750    results.test_only_dependencies.clear();
751    results.dev_dependencies_in_production.clear();
752}
753
754fn apply_dead_code_component_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
755    if !filters.unprovided_injects {
756        results.unprovided_injects.clear();
757    }
758    if !filters.unrendered_components {
759        results.unrendered_components.clear();
760    }
761    if !filters.unused_component_props {
762        results.unused_component_props.clear();
763    }
764    if !filters.unused_component_emits {
765        results.unused_component_emits.clear();
766    }
767    if !filters.unused_component_inputs {
768        results.unused_component_inputs.clear();
769    }
770    if !filters.unused_component_outputs {
771        results.unused_component_outputs.clear();
772    }
773    if !filters.unused_svelte_events {
774        results.unused_svelte_events.clear();
775    }
776    if !filters.unused_server_actions {
777        results.unused_server_actions.clear();
778    }
779    if !filters.unused_load_data_keys {
780        results.unused_load_data_keys.clear();
781    }
782    if !filters.unresolved_imports {
783        results.unresolved_imports.clear();
784    }
785}
786
787fn apply_dead_code_graph_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
788    if !filters.duplicate_exports {
789        results.duplicate_exports.clear();
790    }
791    if !filters.circular_deps {
792        results.circular_dependencies.clear();
793    }
794    if !filters.re_export_cycles {
795        results.re_export_cycles.clear();
796    }
797    if !filters.boundary_violations {
798        results.boundary_violations.clear();
799        results.boundary_coverage_violations.clear();
800        results.boundary_call_violations.clear();
801    }
802}
803
804fn apply_dead_code_policy_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
805    if !filters.policy_violations {
806        results.policy_violations.clear();
807    }
808    if !filters.stale_suppressions {
809        results.stale_suppressions.clear();
810    }
811}
812
813fn apply_dead_code_catalog_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
814    if !filters.unused_catalog_entries {
815        results.unused_catalog_entries.clear();
816    }
817    if !filters.empty_catalog_groups {
818        results.empty_catalog_groups.clear();
819    }
820    if !filters.unresolved_catalog_references {
821        results.unresolved_catalog_references.clear();
822    }
823    if !filters.unused_dependency_overrides {
824        results.unused_dependency_overrides.clear();
825    }
826    if !filters.misconfigured_dependency_overrides {
827        results.misconfigured_dependency_overrides.clear();
828    }
829}