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/// Run circular-dependency analysis and return typed API output before JSON.
51///
52/// # Errors
53///
54/// Returns the same structured errors as [`run_dead_code`].
55pub fn run_circular_dependencies(
56    options: &DeadCodeOptions,
57) -> ProgrammaticResult<CircularDependenciesProgrammaticOutput> {
58    let resolved = resolve_programmatic_analysis_context_deferred_workspace(&options.analysis)?;
59    resolved.install(|| {
60        run_dead_code_inner(options, &resolved, keep_circular_dependencies).map(Into::into)
61    })
62}
63
64/// Run boundary-family analysis and return typed API output before JSON.
65///
66/// # Errors
67///
68/// Returns the same structured errors as [`run_dead_code`].
69pub fn run_boundary_violations(
70    options: &DeadCodeOptions,
71) -> ProgrammaticResult<BoundaryViolationsProgrammaticOutput> {
72    let resolved = resolve_programmatic_analysis_context_deferred_workspace(&options.analysis)?;
73    resolved.install(|| {
74        run_dead_code_inner(options, &resolved, keep_boundary_violations).map(Into::into)
75    })
76}
77
78fn run_dead_code_inner(
79    options: &DeadCodeOptions,
80    resolved: &ProgrammaticAnalysisContext,
81    post_filter: impl FnOnce(&mut AnalysisResults),
82) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
83    let start = Instant::now();
84    let session = load_dead_code_session(options, resolved)?;
85    run_dead_code_with_session(options, resolved, &session, None, post_filter, start)
86}
87
88pub(super) fn run_dead_code_with_session(
89    options: &DeadCodeOptions,
90    resolved: &ProgrammaticAnalysisContext,
91    session: &AnalysisSession,
92    changed_files: Option<&FxHashSet<std::path::PathBuf>>,
93    post_filter: impl FnOnce(&mut AnalysisResults),
94    start: Instant,
95) -> ProgrammaticResult<DeadCodeProgrammaticOutput> {
96    let analysis = session.analyze_dead_code().map_err(|err| {
97        ProgrammaticError::new(format!("dead-code analysis failed: {err}"), 2)
98            .with_code("FALLOW_DEAD_CODE_FAILED")
99            .with_context("dead-code")
100    })?;
101    let mut results = analysis.results;
102
103    apply_dead_code_scope(options, resolved, session, changed_files, &mut results)?;
104    apply_dead_code_filters(&options.filters, &mut results);
105    post_filter(&mut results);
106
107    Ok(build_dead_code_programmatic_output(
108        options, resolved, session, results, start,
109    ))
110}
111
112pub(super) fn run_dead_code_with_session_artifacts(
113    options: &DeadCodeOptions,
114    resolved: &ProgrammaticAnalysisContext,
115    session: &AnalysisSession,
116    changed_files: Option<&FxHashSet<std::path::PathBuf>>,
117    post_filter: impl FnOnce(&mut AnalysisResults),
118    start: Instant,
119) -> ProgrammaticResult<DeadCodeProgrammaticRunWithArtifacts> {
120    let mut artifacts = session
121        .analyze_dead_code_with_artifacts(true, true)
122        .map_err(|err| {
123            ProgrammaticError::new(format!("dead-code analysis failed: {err}"), 2)
124                .with_code("FALLOW_DEAD_CODE_FAILED")
125                .with_context("dead-code")
126        })?;
127
128    apply_dead_code_scope(
129        options,
130        resolved,
131        session,
132        changed_files,
133        &mut artifacts.results,
134    )?;
135    apply_dead_code_filters(&options.filters, &mut artifacts.results);
136    post_filter(&mut artifacts.results);
137
138    Ok(build_dead_code_run_with_artifacts(
139        options, resolved, session, artifacts, start,
140    ))
141}
142
143pub(super) fn run_dead_code_from_artifacts(
144    options: &DeadCodeOptions,
145    resolved: &ProgrammaticAnalysisContext,
146    session: &AnalysisSession,
147    changed_files: Option<&FxHashSet<std::path::PathBuf>>,
148    mut artifacts: DeadCodeAnalysisArtifacts,
149    start: Instant,
150) -> ProgrammaticResult<DeadCodeProgrammaticRunWithArtifacts> {
151    apply_dead_code_scope(
152        options,
153        resolved,
154        session,
155        changed_files,
156        &mut artifacts.results,
157    )?;
158    apply_dead_code_filters(&options.filters, &mut artifacts.results);
159
160    Ok(build_dead_code_run_with_artifacts(
161        options, resolved, session, artifacts, start,
162    ))
163}
164
165fn build_dead_code_run_with_artifacts(
166    options: &DeadCodeOptions,
167    resolved: &ProgrammaticAnalysisContext,
168    session: &AnalysisSession,
169    artifacts: DeadCodeAnalysisArtifacts,
170    start: Instant,
171) -> DeadCodeProgrammaticRunWithArtifacts {
172    let output = build_dead_code_programmatic_output(
173        options,
174        resolved,
175        session,
176        artifacts.results.clone(),
177        start,
178    );
179    DeadCodeProgrammaticRunWithArtifacts { output, artifacts }
180}
181
182fn build_dead_code_programmatic_output(
183    options: &DeadCodeOptions,
184    resolved: &ProgrammaticAnalysisContext,
185    session: &AnalysisSession,
186    results: AnalysisResults,
187    start: Instant,
188) -> DeadCodeProgrammaticOutput {
189    let root = session.root();
190    let next_steps = build_dead_code_next_steps(DeadCodeNextStepsInput {
191        suggestions_enabled: suggestions_enabled(),
192        results: &results,
193        root,
194        offer_setup: setup_pointer_applicable(root),
195        impact_digest: None,
196        workspace_ref: default_workspace_ref_for_workspaces(root, session.workspaces()).as_deref(),
197        audit_changed: fallow_engine::churn::is_git_repo(root),
198        has_external_plugins: !fallow_config::discover_external_plugins(root, &[]).is_empty(),
199    });
200    let config_fixable =
201        fallow_config::is_config_fixable(&resolved.root, resolved.config_path.as_ref());
202    let output = build_check_output(CheckOutputInput {
203        schema_version: CHECK_SCHEMA_VERSION,
204        version: env!("CARGO_PKG_VERSION").to_string(),
205        elapsed: start.elapsed(),
206        results,
207        config_fixable,
208        meta: options.analysis.explain.then(check_meta),
209        workspace_diagnostics: session.current_workspace_diagnostics(),
210        next_steps,
211    });
212    DeadCodeProgrammaticOutput {
213        output,
214        root: session.root().to_path_buf(),
215        config_fixable,
216        envelope_mode: root_envelope_mode(),
217        telemetry_analysis_run_id: None,
218    }
219}
220
221fn keep_circular_dependencies(results: &mut AnalysisResults) {
222    let entry_point_summary = results.entry_point_summary.take();
223    let circular_dependencies = std::mem::take(&mut results.circular_dependencies);
224    *results = AnalysisResults::default();
225    results.entry_point_summary = entry_point_summary;
226    results.circular_dependencies = circular_dependencies;
227}
228
229fn keep_boundary_violations(results: &mut AnalysisResults) {
230    let entry_point_summary = results.entry_point_summary.take();
231    let boundary_violations = std::mem::take(&mut results.boundary_violations);
232    let boundary_coverage_violations = std::mem::take(&mut results.boundary_coverage_violations);
233    let boundary_call_violations = std::mem::take(&mut results.boundary_call_violations);
234    *results = AnalysisResults::default();
235    results.entry_point_summary = entry_point_summary;
236    results.boundary_violations = boundary_violations;
237    results.boundary_coverage_violations = boundary_coverage_violations;
238    results.boundary_call_violations = boundary_call_violations;
239}
240
241pub(super) fn load_dead_code_session(
242    options: &DeadCodeOptions,
243    resolved: &ProgrammaticAnalysisContext,
244) -> ProgrammaticResult<AnalysisSession> {
245    let project_config = fallow_engine::project_config::config_for_project_analysis(
246        &resolved.root,
247        resolved.config_path.as_deref(),
248        ProjectConfigOptions {
249            output: OutputFormat::Json,
250            no_cache: resolved.no_cache,
251            threads: resolved.threads,
252            production_override: resolved.production_override,
253            quiet: true,
254            analysis: ProductionAnalysis::DeadCode,
255            allow_remote_extends: resolved.allow_remote_extends,
256        },
257    )
258    .map_err(|err| {
259        ProgrammaticError::new(format!("failed to load config: {err}"), 2)
260            .with_code("FALLOW_CONFIG_LOAD_FAILED")
261            .with_context("analysis.configPath")
262    })?;
263    let project_config = configure_project_for_dead_code(project_config, options);
264    Ok(AnalysisSession::from_config(project_config))
265}
266
267pub(super) fn default_dead_code_options_for_context(
268    resolved: &ProgrammaticAnalysisContext,
269) -> DeadCodeOptions {
270    DeadCodeOptions {
271        analysis: AnalysisOptions {
272            root: Some(resolved.root().to_path_buf()),
273            config_path: resolved.config_path().clone(),
274            no_cache: resolved.no_cache(),
275            threads: Some(resolved.threads()),
276            production_override: resolved.production_override(),
277            changed_since: resolved.changed_since().map(str::to_owned),
278            workspace: resolved.workspace().map(<[String]>::to_vec),
279            changed_workspaces: resolved.changed_workspaces().map(str::to_owned),
280            explain: resolved.explain_enabled(),
281            ..AnalysisOptions::default()
282        },
283        filters: DeadCodeFilters::default(),
284        files: Vec::new(),
285        include_entry_exports: false,
286    }
287}
288
289fn configure_project_for_dead_code(
290    mut project_config: ProjectConfig,
291    options: &DeadCodeOptions,
292) -> ProjectConfig {
293    if options.include_entry_exports {
294        project_config.config.include_entry_exports = true;
295    }
296    activate_explicit_dead_code_opt_ins(&options.filters, &mut project_config.config.rules);
297    project_config
298}
299
300fn activate_explicit_dead_code_opt_ins(
301    filters: &DeadCodeFilters,
302    rules: &mut fallow_config::RulesConfig,
303) {
304    if filters.private_type_leaks && rules.private_type_leaks == fallow_config::Severity::Off {
305        rules.private_type_leaks = fallow_config::Severity::Warn;
306    }
307}
308
309fn apply_dead_code_scope(
310    options: &DeadCodeOptions,
311    resolved: &ProgrammaticAnalysisContext,
312    session: &AnalysisSession,
313    changed_files: Option<&FxHashSet<std::path::PathBuf>>,
314    results: &mut AnalysisResults,
315) -> ProgrammaticResult<()> {
316    let workspace_roots = workspace_roots_for_session(resolved, session.workspaces())?;
317    if let Some(workspace_roots) = workspace_roots.as_ref() {
318        fallow_engine::dead_code::filter_to_workspaces(results, workspace_roots);
319    }
320    let resolved_changed_files = if changed_files.is_some() {
321        None
322    } else {
323        changed_files_for_run(resolved)?
324    };
325    if let Some(changed_files) = changed_files.or(resolved_changed_files.as_ref()) {
326        fallow_engine::dead_code::filter_by_changed_files(results, changed_files);
327    }
328    if let Some(diff) = resolved.diff.as_ref() {
329        filter_dead_code_by_diff(results, diff, session.root());
330    }
331    apply_dead_code_file_filter(options, session.root(), results);
332    Ok(())
333}
334
335fn filter_dead_code_by_diff(results: &mut AnalysisResults, diff: &DiffIndex, root: &Path) {
336    let touches_file = |path: &Path| -> bool {
337        diff.key_for(path, root)
338            .is_none_or(|rel| diff.touches_file(&rel))
339    };
340    let line_in_diff = |path: &Path, line: u32| -> bool {
341        diff.key_for(path, root)
342            .is_none_or(|rel| diff.line_is_added(&rel, u64::from(line)))
343    };
344
345    filter_dead_code_source_findings(results, &touches_file, &line_in_diff);
346    filter_dead_code_security_findings(results, &touches_file, &line_in_diff);
347    filter_dead_code_dependency_findings(results, &line_in_diff);
348    filter_dead_code_graph_findings(results, &touches_file, &line_in_diff);
349    filter_dead_code_framework_findings(results, &line_in_diff);
350}
351
352fn filter_dead_code_source_findings(
353    results: &mut AnalysisResults,
354    touches_file: &dyn Fn(&Path) -> bool,
355    line_in_diff: &dyn Fn(&Path, u32) -> bool,
356) {
357    results
358        .unused_files
359        .retain(|finding| touches_file(&finding.file.path));
360    results
361        .unused_exports
362        .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
363    results
364        .unused_types
365        .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
366    results
367        .private_type_leaks
368        .retain(|finding| line_in_diff(&finding.leak.path, finding.leak.line));
369    results
370        .unused_enum_members
371        .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
372    results
373        .unused_class_members
374        .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
375    results
376        .unused_store_members
377        .retain(|finding| line_in_diff(&finding.member.path, finding.member.line));
378    results
379        .unprovided_injects
380        .retain(|finding| line_in_diff(&finding.inject.path, finding.inject.line));
381    results
382        .unrendered_components
383        .retain(|finding| line_in_diff(&finding.component.path, finding.component.line));
384    results
385        .unused_component_props
386        .retain(|finding| line_in_diff(&finding.prop.path, finding.prop.line));
387    results
388        .unused_component_emits
389        .retain(|finding| line_in_diff(&finding.emit.path, finding.emit.line));
390    results
391        .unused_component_inputs
392        .retain(|finding| line_in_diff(&finding.input.path, finding.input.line));
393    results
394        .unused_component_outputs
395        .retain(|finding| line_in_diff(&finding.output.path, finding.output.line));
396    results
397        .unused_svelte_events
398        .retain(|finding| line_in_diff(&finding.event.path, finding.event.line));
399    results
400        .unused_server_actions
401        .retain(|finding| line_in_diff(&finding.action.path, finding.action.line));
402    results
403        .unused_load_data_keys
404        .retain(|finding| line_in_diff(&finding.key.path, finding.key.line));
405    results
406        .unresolved_imports
407        .retain(|finding| line_in_diff(&finding.import.path, finding.import.line));
408}
409
410fn filter_dead_code_security_findings(
411    results: &mut AnalysisResults,
412    touches_file: &dyn Fn(&Path) -> bool,
413    line_in_diff: &dyn Fn(&Path, u32) -> bool,
414) {
415    results.security_findings.retain(|finding| {
416        line_in_diff(&finding.path, finding.line)
417            || finding.trace.iter().any(|hop| {
418                line_in_diff(&hop.path, hop.line)
419                    || (matches!(hop.role, TraceHopRole::SecretSource) && touches_file(&hop.path))
420            })
421            || finding.reachability.as_ref().is_some_and(|reachability| {
422                reachability
423                    .untrusted_source_trace
424                    .iter()
425                    .any(|hop| line_in_diff(&hop.path, hop.line))
426            })
427    });
428    results
429        .security_unresolved_callee_diagnostics
430        .retain(|finding| line_in_diff(&finding.path, finding.line));
431}
432
433fn filter_dead_code_dependency_findings(
434    results: &mut AnalysisResults,
435    line_in_diff: &dyn Fn(&Path, u32) -> bool,
436) {
437    for finding in &mut results.unlisted_dependencies {
438        finding
439            .dep
440            .imported_from
441            .retain(|source| line_in_diff(&source.path, source.line));
442    }
443    results
444        .unlisted_dependencies
445        .retain(|finding| !finding.dep.imported_from.is_empty());
446}
447
448fn filter_dead_code_graph_findings(
449    results: &mut AnalysisResults,
450    touches_file: &dyn Fn(&Path) -> bool,
451    line_in_diff: &dyn Fn(&Path, u32) -> bool,
452) {
453    results.duplicate_exports.retain(|finding| {
454        finding
455            .export
456            .locations
457            .iter()
458            .any(|location| line_in_diff(&location.path, location.line))
459    });
460    results
461        .circular_dependencies
462        .retain(|cycle| cycle.cycle.files.iter().any(|path| touches_file(path)));
463    results
464        .re_export_cycles
465        .retain(|cycle| cycle.cycle.files.iter().any(|path| touches_file(path)));
466    results
467        .boundary_violations
468        .retain(|finding| line_in_diff(&finding.violation.from_path, finding.violation.line));
469    results
470        .stale_suppressions
471        .retain(|finding| line_in_diff(&finding.path, finding.line));
472}
473
474fn filter_dead_code_framework_findings(
475    results: &mut AnalysisResults,
476    line_in_diff: &dyn Fn(&Path, u32) -> bool,
477) {
478    results
479        .invalid_client_exports
480        .retain(|finding| line_in_diff(&finding.export.path, finding.export.line));
481    results
482        .mixed_client_server_barrels
483        .retain(|finding| line_in_diff(&finding.barrel.path, finding.barrel.line));
484    results
485        .misplaced_directives
486        .retain(|finding| line_in_diff(&finding.directive_site.path, finding.directive_site.line));
487    results
488        .route_collisions
489        .retain(|finding| line_in_diff(&finding.collision.path, finding.collision.line));
490    results
491        .dynamic_segment_name_conflicts
492        .retain(|finding| line_in_diff(&finding.conflict.path, finding.conflict.line));
493}
494
495fn apply_dead_code_file_filter(
496    options: &DeadCodeOptions,
497    root: &Path,
498    results: &mut AnalysisResults,
499) {
500    if options.files.is_empty() {
501        return;
502    }
503    let file_set = options
504        .files
505        .iter()
506        .map(|path| {
507            if is_absolute_path_any_platform(path) {
508                path.clone()
509            } else {
510                root.join(path)
511            }
512        })
513        .collect::<FxHashSet<_>>();
514    fallow_engine::dead_code::filter_by_changed_files(results, &file_set);
515    clear_dead_code_dependency_findings(results);
516}
517
518fn apply_dead_code_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
519    if !dead_code_filters_active(filters) {
520        return;
521    }
522    apply_dead_code_core_filters(filters, results);
523    apply_dead_code_component_filters(filters, results);
524    apply_dead_code_graph_filters(filters, results);
525    apply_dead_code_policy_filters(filters, results);
526    apply_dead_code_catalog_filters(filters, results);
527}
528
529fn dead_code_filters_active(filters: &DeadCodeFilters) -> bool {
530    filters.unused_files
531        || filters.unused_exports
532        || filters.unused_deps
533        || filters.unused_types
534        || filters.private_type_leaks
535        || filters.unused_enum_members
536        || filters.unused_class_members
537        || filters.unused_store_members
538        || filters.unprovided_injects
539        || filters.unrendered_components
540        || filters.unused_component_props
541        || filters.unused_component_emits
542        || filters.unused_component_inputs
543        || filters.unused_component_outputs
544        || filters.unused_svelte_events
545        || filters.unused_server_actions
546        || filters.unused_load_data_keys
547        || filters.unresolved_imports
548        || filters.unlisted_deps
549        || filters.duplicate_exports
550        || filters.circular_deps
551        || filters.re_export_cycles
552        || filters.boundary_violations
553        || filters.policy_violations
554        || filters.stale_suppressions
555        || filters.unused_catalog_entries
556        || filters.empty_catalog_groups
557        || filters.unresolved_catalog_references
558        || filters.unused_dependency_overrides
559        || filters.misconfigured_dependency_overrides
560}
561
562fn apply_dead_code_core_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
563    if !filters.unused_files {
564        results.unused_files.clear();
565    }
566    if !filters.unused_exports {
567        results.unused_exports.clear();
568    }
569    if !filters.unused_types {
570        results.unused_types.clear();
571    }
572    if !filters.private_type_leaks {
573        results.private_type_leaks.clear();
574    }
575    if !filters.unused_deps {
576        clear_dead_code_dependency_findings(results);
577    }
578    if !filters.unused_enum_members {
579        results.unused_enum_members.clear();
580    }
581    if !filters.unused_class_members {
582        results.unused_class_members.clear();
583    }
584    if !filters.unused_store_members {
585        results.unused_store_members.clear();
586    }
587    if !filters.unlisted_deps {
588        results.unlisted_dependencies.clear();
589    }
590}
591
592fn clear_dead_code_dependency_findings(results: &mut AnalysisResults) {
593    results.unused_dependencies.clear();
594    results.unused_dev_dependencies.clear();
595    results.unused_optional_dependencies.clear();
596    results.type_only_dependencies.clear();
597    results.test_only_dependencies.clear();
598    results.dev_dependencies_in_production.clear();
599}
600
601fn apply_dead_code_component_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
602    if !filters.unprovided_injects {
603        results.unprovided_injects.clear();
604    }
605    if !filters.unrendered_components {
606        results.unrendered_components.clear();
607    }
608    if !filters.unused_component_props {
609        results.unused_component_props.clear();
610    }
611    if !filters.unused_component_emits {
612        results.unused_component_emits.clear();
613    }
614    if !filters.unused_component_inputs {
615        results.unused_component_inputs.clear();
616    }
617    if !filters.unused_component_outputs {
618        results.unused_component_outputs.clear();
619    }
620    if !filters.unused_svelte_events {
621        results.unused_svelte_events.clear();
622    }
623    if !filters.unused_server_actions {
624        results.unused_server_actions.clear();
625    }
626    if !filters.unused_load_data_keys {
627        results.unused_load_data_keys.clear();
628    }
629    if !filters.unresolved_imports {
630        results.unresolved_imports.clear();
631    }
632}
633
634fn apply_dead_code_graph_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
635    if !filters.duplicate_exports {
636        results.duplicate_exports.clear();
637    }
638    if !filters.circular_deps {
639        results.circular_dependencies.clear();
640    }
641    if !filters.re_export_cycles {
642        results.re_export_cycles.clear();
643    }
644    if !filters.boundary_violations {
645        results.boundary_violations.clear();
646        results.boundary_coverage_violations.clear();
647        results.boundary_call_violations.clear();
648    }
649}
650
651fn apply_dead_code_policy_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
652    if !filters.policy_violations {
653        results.policy_violations.clear();
654    }
655    if !filters.stale_suppressions {
656        results.stale_suppressions.clear();
657    }
658}
659
660fn apply_dead_code_catalog_filters(filters: &DeadCodeFilters, results: &mut AnalysisResults) {
661    if !filters.unused_catalog_entries {
662        results.unused_catalog_entries.clear();
663    }
664    if !filters.empty_catalog_groups {
665        results.empty_catalog_groups.clear();
666    }
667    if !filters.unresolved_catalog_references {
668        results.unresolved_catalog_references.clear();
669    }
670    if !filters.unused_dependency_overrides {
671        results.unused_dependency_overrides.clear();
672    }
673    if !filters.misconfigured_dependency_overrides {
674        results.misconfigured_dependency_overrides.clear();
675    }
676}