1use fallow_config::{ResolvedConfig, RulesConfig, Severity};
10
11use crate::effective_severity::{
12 SeveritySource, any_finding_with_severity, count_findings_with_severity,
13};
14
15pub fn has_error_severity_issues(
29 results: &crate::dead_code::AnalysisResults,
30 rules: &RulesConfig,
31 config: Option<&ResolvedConfig>,
32 promote_warns: bool,
33) -> bool {
34 let source = SeveritySource::new(rules, config, promote_warns);
35 any_finding_with_severity(results, &source, Severity::Error)
36}
37
38pub fn count_error_severity_issues(
45 results: &crate::dead_code::AnalysisResults,
46 rules: &RulesConfig,
47 config: Option<&ResolvedConfig>,
48 promote_warns: bool,
49) -> usize {
50 let source = SeveritySource::new(rules, config, promote_warns);
51 count_findings_with_severity(results, &source, Severity::Error)
52}
53
54pub fn promote_warns_to_errors(rules: &mut RulesConfig) {
56 for rule in [
57 &mut rules.unused_files,
58 &mut rules.unused_exports,
59 &mut rules.unused_types,
60 &mut rules.private_type_leaks,
61 &mut rules.deprecated_exports_in_use,
62 &mut rules.unused_dependencies,
63 &mut rules.unused_dev_dependencies,
64 &mut rules.unused_optional_dependencies,
65 &mut rules.unused_enum_members,
66 &mut rules.unused_class_members,
67 &mut rules.unused_store_members,
68 &mut rules.unprovided_injects,
69 &mut rules.unrendered_components,
70 &mut rules.unused_component_props,
71 &mut rules.absent_component_props,
72 &mut rules.unused_component_emits,
73 &mut rules.unused_component_inputs,
74 &mut rules.unused_component_outputs,
75 &mut rules.unused_svelte_events,
76 &mut rules.unused_effect_layers,
77 &mut rules.unused_tagged_errors,
78 &mut rules.unproduced_catch_tags,
79 &mut rules.unused_server_actions,
80 &mut rules.unused_load_data_keys,
81 &mut rules.unresolved_imports,
82 &mut rules.unlisted_dependencies,
83 &mut rules.duplicate_exports,
84 &mut rules.type_only_dependencies,
85 &mut rules.test_only_dependencies,
86 &mut rules.dev_dependencies_in_production,
87 &mut rules.circular_dependencies,
88 &mut rules.re_export_cycle,
89 &mut rules.package_cycle,
90 &mut rules.boundary_violation,
91 &mut rules.coverage_gaps,
92 &mut rules.stale_suppressions,
93 &mut rules.require_suppression_reason,
94 &mut rules.unused_catalog_entries,
95 &mut rules.empty_catalog_groups,
96 &mut rules.unresolved_catalog_references,
97 &mut rules.unused_dependency_overrides,
98 &mut rules.misconfigured_dependency_overrides,
99 &mut rules.policy_violation,
100 &mut rules.invalid_client_export,
101 &mut rules.mixed_client_server_barrel,
102 &mut rules.misplaced_directive,
103 &mut rules.route_collision,
104 &mut rules.dynamic_segment_name_conflict,
105 ] {
106 promote_warn_to_error(rule);
107 }
108}
109
110fn promote_warn_to_error(rule: &mut Severity) {
111 if *rule == Severity::Warn {
112 *rule = Severity::Error;
113 }
114}
115
116pub fn promote_policy_finding_warns(results: &mut crate::dead_code::AnalysisResults) {
122 use fallow_types::results::PolicyViolationSeverity;
123 for finding in &mut results.policy_violations {
124 if finding.violation.severity == PolicyViolationSeverity::Warn {
125 finding.violation.severity = PolicyViolationSeverity::Error;
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use std::path::PathBuf;
133
134 use fallow_config::{ConfigOverride, FallowConfig, PartialRulesConfig, RulesConfig, Severity};
135 use fallow_types::output_dead_code::{
136 EmptyCatalogGroupFinding, MisconfiguredDependencyOverrideFinding,
137 UnusedDependencyOverrideFinding,
138 };
139 use fallow_types::results::{
140 DependencyOverrideMisconfigReason, DependencyOverrideSource, EmptyCatalogGroup,
141 MisconfiguredDependencyOverride, UnusedDependencyOverride,
142 };
143
144 use super::{count_error_severity_issues, has_error_severity_issues};
145 use crate::dead_code::AnalysisResults;
146
147 fn manifest_findings() -> [(&'static str, AnalysisResults); 3] {
151 let mut unused = AnalysisResults::default();
152 unused
153 .unused_dependency_overrides
154 .push(UnusedDependencyOverrideFinding::with_actions(
155 UnusedDependencyOverride {
156 raw_key: "old-dep".to_string(),
157 target_package: "old-dep".to_string(),
158 parent_package: None,
159 version_constraint: None,
160 version_range: "^1.0.0".to_string(),
161 source: DependencyOverrideSource::PnpmPackageJson,
162 path: PathBuf::from("/project/package.json"),
163 line: 7,
164 hint: None,
165 },
166 ));
167 let mut misconfigured = AnalysisResults::default();
168 misconfigured.misconfigured_dependency_overrides.push(
169 MisconfiguredDependencyOverrideFinding::with_actions(MisconfiguredDependencyOverride {
170 raw_key: "bad>".to_string(),
171 target_package: None,
172 raw_value: "1.0.0".to_string(),
173 reason: DependencyOverrideMisconfigReason::UnparsableKey,
174 source: DependencyOverrideSource::PnpmPackageJson,
175 path: PathBuf::from("/project/package.json"),
176 line: 4,
177 }),
178 );
179 let mut empty_group = AnalysisResults::default();
180 empty_group
181 .empty_catalog_groups
182 .push(EmptyCatalogGroupFinding::with_actions(EmptyCatalogGroup {
183 catalog_name: "legacy".to_string(),
184 path: PathBuf::from("/project/pnpm-workspace.yaml"),
185 line: 3,
186 }));
187 [
188 ("unused-dependency-overrides", unused),
189 ("misconfigured-dependency-overrides", misconfigured),
190 ("empty-catalog-groups", empty_group),
191 ]
192 }
193
194 fn config_with_manifest_override(
195 base: Severity,
196 manifest: Severity,
197 ) -> fallow_config::ResolvedConfig {
198 FallowConfig {
199 rules: RulesConfig {
200 unused_dependency_overrides: base,
201 misconfigured_dependency_overrides: base,
202 empty_catalog_groups: base,
203 ..RulesConfig::default()
204 },
205 overrides: vec![ConfigOverride {
206 files: vec![
207 "package.json".to_string(),
208 "pnpm-workspace.yaml".to_string(),
209 ],
210 rules: PartialRulesConfig {
211 unused_dependency_overrides: Some(manifest),
212 misconfigured_dependency_overrides: Some(manifest),
213 empty_catalog_groups: Some(manifest),
214 ..PartialRulesConfig::default()
215 },
216 }],
217 ..FallowConfig::default()
218 }
219 .resolve(
220 PathBuf::from("/project"),
221 fallow_config::OutputFormat::Human,
222 1,
223 true,
224 true,
225 None,
226 )
227 }
228
229 #[test]
230 fn a_manifest_override_to_warn_clears_a_base_error() {
231 let config = config_with_manifest_override(Severity::Error, Severity::Warn);
232 for (kind, results) in manifest_findings() {
233 assert!(
234 !has_error_severity_issues(&results, &config.rules, Some(&config), false),
235 "the `warn` override for the manifest must win over the base `error` for {kind}"
236 );
237 }
238 }
239
240 #[test]
241 fn a_manifest_override_to_error_raises_a_base_warn() {
242 let config = config_with_manifest_override(Severity::Warn, Severity::Error);
243 for (kind, results) in manifest_findings() {
244 assert!(
245 has_error_severity_issues(&results, &config.rules, Some(&config), false),
246 "the `error` override for the manifest must win over the base `warn` for {kind}"
247 );
248 }
249 }
250
251 #[test]
254 fn the_count_holds_each_finding_at_its_own_severity() {
255 let config = config_with_manifest_override(Severity::Error, Severity::Error);
256 let mut all = AnalysisResults::default();
257 for (_, results) in manifest_findings() {
258 all.unused_dependency_overrides
259 .extend(results.unused_dependency_overrides);
260 all.misconfigured_dependency_overrides
261 .extend(results.misconfigured_dependency_overrides);
262 all.empty_catalog_groups
263 .extend(results.empty_catalog_groups);
264 }
265 assert_eq!(
266 count_error_severity_issues(&all, &config.rules, Some(&config), false),
267 3
268 );
269
270 let mixed = config_with_manifest_override(Severity::Error, Severity::Warn);
271 assert_eq!(
272 count_error_severity_issues(&all, &mixed.rules, Some(&mixed), false),
273 0
274 );
275 assert_eq!(
276 count_error_severity_issues(&all, &mixed.rules, Some(&mixed), true),
277 3,
278 "--fail-on-issues promotes the per-file `warn`"
279 );
280 assert_eq!(
281 count_error_severity_issues(&AnalysisResults::default(), &config.rules, None, false),
282 0
283 );
284 }
285}