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    });
329    let config_fixable =
330        fallow_config::is_config_fixable(&resolved.root, resolved.config_path.as_ref());
331    let mut meta = options.analysis.explain.then(check_meta);
332    if let Some(type_aware) = type_aware_meta {
333        meta.get_or_insert_with(Default::default).type_aware = Some(type_aware);
334    }
335    let output = build_check_output(CheckOutputInput {
336        schema_version: CHECK_SCHEMA_VERSION,
337        version: env!("CARGO_PKG_VERSION").to_string(),
338        elapsed: start.elapsed(),
339        results,
340        config_fixable,
341        meta,
342        workspace_diagnostics: session.current_workspace_diagnostics(),
343        next_steps,
344    });
345    DeadCodeProgrammaticOutput {
346        output,
347        root: session.root().to_path_buf(),
348        config_fixable,
349        envelope_mode: root_envelope_mode(),
350        telemetry_analysis_run_id: None,
351    }
352}
353
354fn keep_circular_dependencies(results: &mut AnalysisResults) {
355    let entry_point_summary = results.entry_point_summary.take();
356    let circular_dependencies = std::mem::take(&mut results.circular_dependencies);
357    *results = AnalysisResults::default();
358    results.entry_point_summary = entry_point_summary;
359    results.circular_dependencies = circular_dependencies;
360}
361
362fn keep_boundary_violations(results: &mut AnalysisResults) {
363    let entry_point_summary = results.entry_point_summary.take();
364    let boundary_violations = std::mem::take(&mut results.boundary_violations);
365    let boundary_coverage_violations = std::mem::take(&mut results.boundary_coverage_violations);
366    let boundary_call_violations = std::mem::take(&mut results.boundary_call_violations);
367    *results = AnalysisResults::default();
368    results.entry_point_summary = entry_point_summary;
369    results.boundary_violations = boundary_violations;
370    results.boundary_coverage_violations = boundary_coverage_violations;
371    results.boundary_call_violations = boundary_call_violations;
372}
373
374pub(super) fn load_dead_code_session(
375    options: &DeadCodeOptions,
376    resolved: &ProgrammaticAnalysisContext,
377) -> ProgrammaticResult<AnalysisSession> {
378    let project_config = fallow_engine::project_config::config_for_project_analysis(
379        &resolved.root,
380        resolved.config_path.as_deref(),
381        ProjectConfigOptions {
382            output: OutputFormat::Json,
383            no_cache: resolved.no_cache,
384            threads: resolved.threads,
385            production_override: resolved.production_override,
386            quiet: true,
387            analysis: ProductionAnalysis::DeadCode,
388            allow_remote_extends: resolved.allow_remote_extends,
389        },
390    )
391    .map_err(|err| {
392        ProgrammaticError::new(format!("failed to load config: {err}"), 2)
393            .with_code("FALLOW_CONFIG_LOAD_FAILED")
394            .with_context("analysis.configPath")
395    })?;
396    let project_config = configure_project_for_dead_code(project_config, options);
397    Ok(attach_cancellation(
398        AnalysisSession::from_config(project_config),
399        resolved,
400    ))
401}
402
403/// Hand the caller's cancellation token to the engine session.
404///
405/// Without this the session runs to completion no matter what the API layer
406/// checks between its own stages.
407pub(super) fn attach_cancellation(
408    session: AnalysisSession,
409    resolved: &ProgrammaticAnalysisContext,
410) -> AnalysisSession {
411    match resolved.cancellation() {
412        Some(cancellation) => session.with_cancellation(std::sync::Arc::clone(cancellation)),
413        None => session,
414    }
415}
416
417pub(super) fn default_dead_code_options_for_context(
418    resolved: &ProgrammaticAnalysisContext,
419) -> DeadCodeOptions {
420    DeadCodeOptions {
421        analysis: AnalysisOptions {
422            root: Some(resolved.root().to_path_buf()),
423            config_path: resolved.config_path().clone(),
424            no_cache: resolved.no_cache(),
425            threads: Some(resolved.threads()),
426            production_override: resolved.production_override(),
427            changed_since: resolved.changed_since().map(str::to_owned),
428            workspace: resolved.workspace().map(<[String]>::to_vec),
429            changed_workspaces: resolved.changed_workspaces().map(str::to_owned),
430            explain: resolved.explain_enabled(),
431            ..AnalysisOptions::default()
432        },
433        filters: DeadCodeFilters::default(),
434        files: Vec::new(),
435        include_entry_exports: false,
436    }
437}
438
439fn configure_project_for_dead_code(
440    mut project_config: ProjectConfig,
441    options: &DeadCodeOptions,
442) -> ProjectConfig {
443    if options.include_entry_exports {
444        project_config.config.include_entry_exports = true;
445    }
446    activate_explicit_dead_code_opt_ins(&options.filters, &mut project_config.config.rules);
447    project_config
448}
449
450fn activate_explicit_dead_code_opt_ins(
451    filters: &DeadCodeFilters,
452    rules: &mut fallow_config::RulesConfig,
453) {
454    if filters.private_type_leaks && rules.private_type_leaks == fallow_config::Severity::Off {
455        rules.private_type_leaks = fallow_config::Severity::Warn;
456    }
457}
458
459fn apply_dead_code_scope(
460    options: &DeadCodeOptions,
461    resolved: &ProgrammaticAnalysisContext,
462    session: &AnalysisSession,
463    changed_files: Option<&FxHashSet<std::path::PathBuf>>,
464    results: &mut AnalysisResults,
465) -> ProgrammaticResult<()> {
466    let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
467    if let Some(workspace_roots) = workspace_roots.as_ref() {
468        fallow_engine::dead_code::filter_to_workspaces(results, workspace_roots);
469    }
470    let resolved_changed_files = if changed_files.is_some() {
471        None
472    } else {
473        changed_files_for_run(resolved)?
474    };
475    if let Some(changed_files) = changed_files.or(resolved_changed_files.as_ref()) {
476        fallow_engine::dead_code::filter_by_changed_files(results, changed_files);
477    }
478    if let Some(diff) = resolved.diff.as_ref() {
479        filter_dead_code_by_diff(results, diff, session.root());
480    }
481    apply_dead_code_file_filter(options, session.root(), results);
482    Ok(())
483}
484
485fn filter_dead_code_by_diff(results: &mut AnalysisResults, diff: &DiffIndex, root: &Path) {
486    let touches_file = |path: &Path| -> bool {
487        diff.key_for(path, root)
488            .is_none_or(|rel| diff.touches_file(&rel))
489    };
490    let line_in_diff = |path: &Path, line: u32| -> bool {
491        diff.key_for(path, root)
492            .is_none_or(|rel| diff.line_is_added(&rel, u64::from(line)))
493    };
494
495    filter_dead_code_source_findings(results, &touches_file, &line_in_diff);
496    filter_dead_code_security_findings(results, &touches_file, &line_in_diff);
497    filter_dead_code_dependency_findings(results, &line_in_diff);
498    filter_dead_code_graph_findings(results, &touches_file, &line_in_diff);
499    filter_dead_code_framework_findings(results, &line_in_diff);
500}
501
502fn filter_dead_code_source_findings(
503    results: &mut AnalysisResults,
504    touches_file: &dyn Fn(&Path) -> bool,
505    line_in_diff: &dyn Fn(&Path, u32) -> bool,
506) {
507    results
508        .unused_files
509        .retain(|finding| touches_file(&finding.file.path));
510    results
511        .unused_exports
512        .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
513    results
514        .unused_types
515        .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
516    results
517        .private_type_leaks
518        .retain(|finding| line_in_diff(&finding.leak.path, finding.leak.line));
519    results
520        .unused_enum_members
521        .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
522    results
523        .unused_class_members
524        .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
525    results
526        .unused_store_members
527        .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
528    results
529        .unprovided_injects
530        .retain(|finding| line_in_diff(&finding.inject.path, finding.inject.line));
531    results
532        .unrendered_components
533        .retain(|finding| line_in_diff(&finding.component.path, finding.component.line));
534    results
535        .unused_component_props
536        .retain(|finding| line_in_diff(&finding.prop.path, finding.prop.line));
537    results
538        .unused_component_emits
539        .retain(|finding| line_in_diff(&finding.emit.path, finding.emit.line));
540    results
541        .unused_component_inputs
542        .retain(|finding| line_in_diff(&finding.input.path, finding.input.line));
543    results
544        .unused_component_outputs
545        .retain(|finding| line_in_diff(&finding.output.path, finding.output.line));
546    results
547        .unused_svelte_events
548        .retain(|finding| line_in_diff(&finding.event.path, finding.event.line));
549    results
550        .unused_server_actions
551        .retain(|finding| line_in_diff(&finding.action.path, finding.action.line));
552    results
553        .unused_load_data_keys
554        .retain(|finding| line_in_diff(&finding.key.path, finding.key.line));
555    results
556        .unresolved_imports
557        .retain(|finding| line_in_diff(&finding.import.path, finding.import.line));
558}
559
560fn filter_dead_code_security_findings(
561    results: &mut AnalysisResults,
562    touches_file: &dyn Fn(&Path) -> bool,
563    line_in_diff: &dyn Fn(&Path, u32) -> bool,
564) {
565    results.security_findings.retain(|finding| {
566        line_in_diff(&finding.path, finding.line)
567            || finding.trace.iter().any(|hop| {
568                line_in_diff(&hop.path, hop.line)
569                    || (matches!(hop.role, TraceHopRole::SecretSource) && touches_file(&hop.path))
570            })
571            || finding.reachability.as_ref().is_some_and(|reachability| {
572                reachability
573                    .untrusted_source_trace
574                    .iter()
575                    .any(|hop| line_in_diff(&hop.path, hop.line))
576            })
577    });
578    results
579        .security_unresolved_callee_diagnostics
580        .retain(|finding| line_in_diff(&finding.path, finding.line));
581}
582
583fn filter_dead_code_dependency_findings(
584    results: &mut AnalysisResults,
585    line_in_diff: &dyn Fn(&Path, u32) -> bool,
586) {
587    for finding in &mut results.unlisted_dependencies {
588        finding
589            .dep
590            .imported_from
591            .retain(|source| line_in_diff(&source.path, source.line));
592    }
593    results
594        .unlisted_dependencies
595        .retain(|finding| !finding.dep.imported_from.is_empty());
596}
597
598fn filter_dead_code_graph_findings(
599    results: &mut AnalysisResults,
600    touches_file: &dyn Fn(&Path) -> bool,
601    line_in_diff: &dyn Fn(&Path, u32) -> bool,
602) {
603    results.duplicate_exports.retain(|finding| {
604        finding
605            .export
606            .locations
607            .iter()
608            .any(|location| line_in_diff(&location.path, location.line))
609    });
610    results
611        .circular_dependencies
612        .retain(|cycle| cycle.cycle.files.iter().any(|path| touches_file(path)));
613    results
614        .re_export_cycles
615        .retain(|cycle| cycle.cycle.files.iter().any(|path| touches_file(path)));
616    results
617        .boundary_violations
618        .retain(|finding| line_in_diff(&finding.violation.from_path, finding.violation.line));
619    results
620        .stale_suppressions
621        .retain(|finding| line_in_diff(&finding.path, finding.line));
622}
623
624fn filter_dead_code_framework_findings(
625    results: &mut AnalysisResults,
626    line_in_diff: &dyn Fn(&Path, u32) -> bool,
627) {
628    results
629        .invalid_client_exports
630        .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
631    results
632        .mixed_client_server_barrels
633        .retain(|finding| line_in_diff(&finding.barrel.path, finding.barrel.line));
634    results
635        .misplaced_directives
636        .retain(|finding| line_in_diff(&finding.directive_site.path, finding.directive_site.line));
637    results
638        .route_collisions
639        .retain(|finding| line_in_diff(&finding.collision.path, finding.collision.line));
640    results
641        .dynamic_segment_name_conflicts
642        .retain(|finding| line_in_diff(&finding.conflict.path, finding.conflict.line));
643}
644
645fn apply_dead_code_file_filter(
646    options: &DeadCodeOptions,
647    root: &Path,
648    results: &mut AnalysisResults,
649) {
650    if options.files.is_empty() {
651        return;
652    }
653    let file_set = options
654        .files
655        .iter()
656        .map(|path| {
657            if is_absolute_path_any_platform(path) {
658                path.clone()
659            } else {
660                root.join(path)
661            }
662        })
663        .collect::<FxHashSet<_>>();
664    fallow_engine::dead_code::filter_by_changed_files(results, &file_set);
665    clear_dead_code_dependency_findings(results);
666}
667
668fn apply_dead_code_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
669    if !dead_code_filters_active(filters) {
670        return;
671    }
672    apply_dead_code_core_filters(filters, results);
673    apply_dead_code_component_filters(filters, results);
674    apply_dead_code_graph_filters(filters, results);
675    apply_dead_code_policy_filters(filters, results);
676    apply_dead_code_catalog_filters(filters, results);
677}
678
679fn dead_code_filters_active(filters: &DeadCodeFilters) -> bool {
680    filters.unused_files
681        || filters.unused_exports
682        || filters.unused_deps
683        || filters.unused_types
684        || filters.private_type_leaks
685        || filters.unused_enum_members
686        || filters.unused_class_members
687        || filters.unused_store_members
688        || filters.unprovided_injects
689        || filters.unrendered_components
690        || filters.unused_component_props
691        || filters.unused_component_emits
692        || filters.unused_component_inputs
693        || filters.unused_component_outputs
694        || filters.unused_svelte_events
695        || filters.unused_server_actions
696        || filters.unused_load_data_keys
697        || filters.unresolved_imports
698        || filters.unlisted_deps
699        || filters.duplicate_exports
700        || filters.circular_deps
701        || filters.re_export_cycles
702        || filters.boundary_violations
703        || filters.policy_violations
704        || filters.stale_suppressions
705        || filters.unused_catalog_entries
706        || filters.empty_catalog_groups
707        || filters.unresolved_catalog_references
708        || filters.unused_dependency_overrides
709        || filters.misconfigured_dependency_overrides
710}
711
712fn apply_dead_code_core_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
713    if !filters.unused_files {
714        results.unused_files.clear();
715    }
716    if !filters.unused_exports {
717        results.unused_exports.clear();
718    }
719    if !filters.unused_types {
720        results.unused_types.clear();
721    }
722    if !filters.private_type_leaks {
723        results.private_type_leaks.clear();
724    }
725    if !filters.unused_deps {
726        clear_dead_code_dependency_findings(results);
727    }
728    if !filters.unused_enum_members {
729        results.unused_enum_members.clear();
730    }
731    if !filters.unused_class_members {
732        results.unused_class_members.clear();
733    }
734    if !filters.unused_store_members {
735        results.unused_store_members.clear();
736    }
737    if !filters.unlisted_deps {
738        results.unlisted_dependencies.clear();
739    }
740}
741
742fn clear_dead_code_dependency_findings(results: &mut AnalysisResults) {
743    results.unused_dependencies.clear();
744    results.unused_dev_dependencies.clear();
745    results.unused_optional_dependencies.clear();
746    results.type_only_dependencies.clear();
747    results.test_only_dependencies.clear();
748    results.dev_dependencies_in_production.clear();
749}
750
751fn apply_dead_code_component_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
752    if !filters.unprovided_injects {
753        results.unprovided_injects.clear();
754    }
755    if !filters.unrendered_components {
756        results.unrendered_components.clear();
757    }
758    if !filters.unused_component_props {
759        results.unused_component_props.clear();
760    }
761    if !filters.unused_component_emits {
762        results.unused_component_emits.clear();
763    }
764    if !filters.unused_component_inputs {
765        results.unused_component_inputs.clear();
766    }
767    if !filters.unused_component_outputs {
768        results.unused_component_outputs.clear();
769    }
770    if !filters.unused_svelte_events {
771        results.unused_svelte_events.clear();
772    }
773    if !filters.unused_server_actions {
774        results.unused_server_actions.clear();
775    }
776    if !filters.unused_load_data_keys {
777        results.unused_load_data_keys.clear();
778    }
779    if !filters.unresolved_imports {
780        results.unresolved_imports.clear();
781    }
782}
783
784fn apply_dead_code_graph_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
785    if !filters.duplicate_exports {
786        results.duplicate_exports.clear();
787    }
788    if !filters.circular_deps {
789        results.circular_dependencies.clear();
790    }
791    if !filters.re_export_cycles {
792        results.re_export_cycles.clear();
793    }
794    if !filters.boundary_violations {
795        results.boundary_violations.clear();
796        results.boundary_coverage_violations.clear();
797        results.boundary_call_violations.clear();
798    }
799}
800
801fn apply_dead_code_policy_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
802    if !filters.policy_violations {
803        results.policy_violations.clear();
804    }
805    if !filters.stale_suppressions {
806        results.stale_suppressions.clear();
807    }
808}
809
810fn apply_dead_code_catalog_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
811    if !filters.unused_catalog_entries {
812        results.unused_catalog_entries.clear();
813    }
814    if !filters.empty_catalog_groups {
815        results.empty_catalog_groups.clear();
816    }
817    if !filters.unresolved_catalog_references {
818        results.unresolved_catalog_references.clear();
819    }
820    if !filters.unused_dependency_overrides {
821        results.unused_dependency_overrides.clear();
822    }
823    if !filters.misconfigured_dependency_overrides {
824        results.misconfigured_dependency_overrides.clear();
825    }
826}