Skip to main content

fallow_engine/
error_severity.rs

1//! The error-severity rule: whether a dead-code result holds a finding whose
2//! effective severity is `error`.
3//!
4//! This rule decides the exit code of `dead-code` and `check`, the
5//! `error-severity-findings` gate entry, the combined verdict and the audit
6//! summary. Every caller reads it from here, so a finding cannot fail one
7//! command and pass another.
8
9use fallow_config::{ResolvedConfig, RulesConfig, Severity};
10
11use crate::effective_severity::{SeveritySource, any_finding_with_severity};
12
13/// Check whether any issue type with `Severity::Error` has remaining issues.
14///
15/// The severity of each finding comes from the rule table in
16/// `crate::effective_severity`, the same table that writes the
17/// `effective_severity` of each finding for the CI formats and that the audit
18/// ledger reads. When overrides are configured, file-scoped findings resolve
19/// the rules for their own path. Circular dependencies resolve against every
20/// file in the cycle. Project-level findings read `rules`.
21///
22/// `promote_warns` mirrors `--fail-on-issues`: `rules` is expected to arrive
23/// already promoted, and the per-file override path promotes each resolved
24/// severity after override resolution so an explicit per-path `warn` fails
25/// the run just like the base rules would.
26pub fn has_error_severity_issues(
27    results: &crate::dead_code::AnalysisResults,
28    rules: &RulesConfig,
29    config: Option<&ResolvedConfig>,
30    promote_warns: bool,
31) -> bool {
32    let source = SeveritySource::new(rules, config, promote_warns);
33    any_finding_with_severity(results, &source, Severity::Error)
34}
35
36/// Promote all `Warn` severities to `Error` for a single run.
37pub fn promote_warns_to_errors(rules: &mut RulesConfig) {
38    for rule in [
39        &mut rules.unused_files,
40        &mut rules.unused_exports,
41        &mut rules.unused_types,
42        &mut rules.private_type_leaks,
43        &mut rules.deprecated_exports_in_use,
44        &mut rules.unused_dependencies,
45        &mut rules.unused_dev_dependencies,
46        &mut rules.unused_optional_dependencies,
47        &mut rules.unused_enum_members,
48        &mut rules.unused_class_members,
49        &mut rules.unused_store_members,
50        &mut rules.unprovided_injects,
51        &mut rules.unrendered_components,
52        &mut rules.unused_component_props,
53        &mut rules.unused_component_emits,
54        &mut rules.unused_component_inputs,
55        &mut rules.unused_component_outputs,
56        &mut rules.unused_svelte_events,
57        &mut rules.unused_server_actions,
58        &mut rules.unused_load_data_keys,
59        &mut rules.unresolved_imports,
60        &mut rules.unlisted_dependencies,
61        &mut rules.duplicate_exports,
62        &mut rules.type_only_dependencies,
63        &mut rules.test_only_dependencies,
64        &mut rules.dev_dependencies_in_production,
65        &mut rules.circular_dependencies,
66        &mut rules.re_export_cycle,
67        &mut rules.package_cycle,
68        &mut rules.boundary_violation,
69        &mut rules.coverage_gaps,
70        &mut rules.stale_suppressions,
71        &mut rules.require_suppression_reason,
72        &mut rules.unused_catalog_entries,
73        &mut rules.empty_catalog_groups,
74        &mut rules.unresolved_catalog_references,
75        &mut rules.unused_dependency_overrides,
76        &mut rules.misconfigured_dependency_overrides,
77        &mut rules.policy_violation,
78        &mut rules.invalid_client_export,
79        &mut rules.mixed_client_server_barrel,
80        &mut rules.misplaced_directive,
81        &mut rules.route_collision,
82        &mut rules.dynamic_segment_name_conflict,
83    ] {
84        promote_warn_to_error(rule);
85    }
86}
87
88fn promote_warn_to_error(rule: &mut Severity) {
89    if *rule == Severity::Warn {
90        *rule = Severity::Error;
91    }
92}
93
94/// Promote per-finding `warn` policy-violation severities to `error` for a
95/// strict (fail-on-issues) run. Policy findings carry their effective
96/// severity baked by the evaluator, so the rule-level promotion in
97/// [`promote_warns_to_errors`] alone would not flip findings whose rule
98/// explicitly opted down to `warn`; under strict mode every warning fails.
99pub fn promote_policy_finding_warns(results: &mut crate::dead_code::AnalysisResults) {
100    use fallow_types::results::PolicyViolationSeverity;
101    for finding in &mut results.policy_violations {
102        if finding.violation.severity == PolicyViolationSeverity::Warn {
103            finding.violation.severity = PolicyViolationSeverity::Error;
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use std::path::PathBuf;
111
112    use fallow_config::{ConfigOverride, FallowConfig, PartialRulesConfig, RulesConfig, Severity};
113    use fallow_types::output_dead_code::{
114        EmptyCatalogGroupFinding, MisconfiguredDependencyOverrideFinding,
115        UnusedDependencyOverrideFinding,
116    };
117    use fallow_types::results::{
118        DependencyOverrideMisconfigReason, DependencyOverrideSource, EmptyCatalogGroup,
119        MisconfiguredDependencyOverride, UnusedDependencyOverride,
120    };
121
122    use super::has_error_severity_issues;
123    use crate::dead_code::AnalysisResults;
124
125    /// One finding of each manifest-level kind that per-file `overrides` can
126    /// change: an unused and a misconfigured dependency override in
127    /// `package.json`, and an empty catalog group in `pnpm-workspace.yaml`.
128    fn manifest_findings() -> [(&'static str, AnalysisResults); 3] {
129        let mut unused = AnalysisResults::default();
130        unused
131            .unused_dependency_overrides
132            .push(UnusedDependencyOverrideFinding::with_actions(
133                UnusedDependencyOverride {
134                    raw_key: "old-dep".to_string(),
135                    target_package: "old-dep".to_string(),
136                    parent_package: None,
137                    version_constraint: None,
138                    version_range: "^1.0.0".to_string(),
139                    source: DependencyOverrideSource::PnpmPackageJson,
140                    path: PathBuf::from("/project/package.json"),
141                    line: 7,
142                    hint: None,
143                },
144            ));
145        let mut misconfigured = AnalysisResults::default();
146        misconfigured.misconfigured_dependency_overrides.push(
147            MisconfiguredDependencyOverrideFinding::with_actions(MisconfiguredDependencyOverride {
148                raw_key: "bad>".to_string(),
149                target_package: None,
150                raw_value: "1.0.0".to_string(),
151                reason: DependencyOverrideMisconfigReason::UnparsableKey,
152                source: DependencyOverrideSource::PnpmPackageJson,
153                path: PathBuf::from("/project/package.json"),
154                line: 4,
155            }),
156        );
157        let mut empty_group = AnalysisResults::default();
158        empty_group
159            .empty_catalog_groups
160            .push(EmptyCatalogGroupFinding::with_actions(EmptyCatalogGroup {
161                catalog_name: "legacy".to_string(),
162                path: PathBuf::from("/project/pnpm-workspace.yaml"),
163                line: 3,
164            }));
165        [
166            ("unused-dependency-overrides", unused),
167            ("misconfigured-dependency-overrides", misconfigured),
168            ("empty-catalog-groups", empty_group),
169        ]
170    }
171
172    fn config_with_manifest_override(
173        base: Severity,
174        manifest: Severity,
175    ) -> fallow_config::ResolvedConfig {
176        FallowConfig {
177            rules: RulesConfig {
178                unused_dependency_overrides: base,
179                misconfigured_dependency_overrides: base,
180                empty_catalog_groups: base,
181                ..RulesConfig::default()
182            },
183            overrides: vec![ConfigOverride {
184                files: vec![
185                    "package.json".to_string(),
186                    "pnpm-workspace.yaml".to_string(),
187                ],
188                rules: PartialRulesConfig {
189                    unused_dependency_overrides: Some(manifest),
190                    misconfigured_dependency_overrides: Some(manifest),
191                    empty_catalog_groups: Some(manifest),
192                    ..PartialRulesConfig::default()
193                },
194            }],
195            ..FallowConfig::default()
196        }
197        .resolve(
198            PathBuf::from("/project"),
199            fallow_config::OutputFormat::Human,
200            1,
201            true,
202            true,
203            None,
204        )
205    }
206
207    #[test]
208    fn a_manifest_override_to_warn_clears_a_base_error() {
209        let config = config_with_manifest_override(Severity::Error, Severity::Warn);
210        for (kind, results) in manifest_findings() {
211            assert!(
212                !has_error_severity_issues(&results, &config.rules, Some(&config), false),
213                "the `warn` override for the manifest must win over the base `error` for {kind}"
214            );
215        }
216    }
217
218    #[test]
219    fn a_manifest_override_to_error_raises_a_base_warn() {
220        let config = config_with_manifest_override(Severity::Warn, Severity::Error);
221        for (kind, results) in manifest_findings() {
222            assert!(
223                has_error_severity_issues(&results, &config.rules, Some(&config), false),
224                "the `error` override for the manifest must win over the base `warn` for {kind}"
225            );
226        }
227    }
228}