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_server_actions,
77 &mut rules.unused_load_data_keys,
78 &mut rules.unresolved_imports,
79 &mut rules.unlisted_dependencies,
80 &mut rules.duplicate_exports,
81 &mut rules.type_only_dependencies,
82 &mut rules.test_only_dependencies,
83 &mut rules.dev_dependencies_in_production,
84 &mut rules.circular_dependencies,
85 &mut rules.re_export_cycle,
86 &mut rules.package_cycle,
87 &mut rules.boundary_violation,
88 &mut rules.coverage_gaps,
89 &mut rules.stale_suppressions,
90 &mut rules.require_suppression_reason,
91 &mut rules.unused_catalog_entries,
92 &mut rules.empty_catalog_groups,
93 &mut rules.unresolved_catalog_references,
94 &mut rules.unused_dependency_overrides,
95 &mut rules.misconfigured_dependency_overrides,
96 &mut rules.policy_violation,
97 &mut rules.invalid_client_export,
98 &mut rules.mixed_client_server_barrel,
99 &mut rules.misplaced_directive,
100 &mut rules.route_collision,
101 &mut rules.dynamic_segment_name_conflict,
102 ] {
103 promote_warn_to_error(rule);
104 }
105}
106
107fn promote_warn_to_error(rule: &mut Severity) {
108 if *rule == Severity::Warn {
109 *rule = Severity::Error;
110 }
111}
112
113pub fn promote_policy_finding_warns(results: &mut crate::dead_code::AnalysisResults) {
119 use fallow_types::results::PolicyViolationSeverity;
120 for finding in &mut results.policy_violations {
121 if finding.violation.severity == PolicyViolationSeverity::Warn {
122 finding.violation.severity = PolicyViolationSeverity::Error;
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use std::path::PathBuf;
130
131 use fallow_config::{ConfigOverride, FallowConfig, PartialRulesConfig, RulesConfig, Severity};
132 use fallow_types::output_dead_code::{
133 EmptyCatalogGroupFinding, MisconfiguredDependencyOverrideFinding,
134 UnusedDependencyOverrideFinding,
135 };
136 use fallow_types::results::{
137 DependencyOverrideMisconfigReason, DependencyOverrideSource, EmptyCatalogGroup,
138 MisconfiguredDependencyOverride, UnusedDependencyOverride,
139 };
140
141 use super::{count_error_severity_issues, has_error_severity_issues};
142 use crate::dead_code::AnalysisResults;
143
144 fn manifest_findings() -> [(&'static str, AnalysisResults); 3] {
148 let mut unused = AnalysisResults::default();
149 unused
150 .unused_dependency_overrides
151 .push(UnusedDependencyOverrideFinding::with_actions(
152 UnusedDependencyOverride {
153 raw_key: "old-dep".to_string(),
154 target_package: "old-dep".to_string(),
155 parent_package: None,
156 version_constraint: None,
157 version_range: "^1.0.0".to_string(),
158 source: DependencyOverrideSource::PnpmPackageJson,
159 path: PathBuf::from("/project/package.json"),
160 line: 7,
161 hint: None,
162 },
163 ));
164 let mut misconfigured = AnalysisResults::default();
165 misconfigured.misconfigured_dependency_overrides.push(
166 MisconfiguredDependencyOverrideFinding::with_actions(MisconfiguredDependencyOverride {
167 raw_key: "bad>".to_string(),
168 target_package: None,
169 raw_value: "1.0.0".to_string(),
170 reason: DependencyOverrideMisconfigReason::UnparsableKey,
171 source: DependencyOverrideSource::PnpmPackageJson,
172 path: PathBuf::from("/project/package.json"),
173 line: 4,
174 }),
175 );
176 let mut empty_group = AnalysisResults::default();
177 empty_group
178 .empty_catalog_groups
179 .push(EmptyCatalogGroupFinding::with_actions(EmptyCatalogGroup {
180 catalog_name: "legacy".to_string(),
181 path: PathBuf::from("/project/pnpm-workspace.yaml"),
182 line: 3,
183 }));
184 [
185 ("unused-dependency-overrides", unused),
186 ("misconfigured-dependency-overrides", misconfigured),
187 ("empty-catalog-groups", empty_group),
188 ]
189 }
190
191 fn config_with_manifest_override(
192 base: Severity,
193 manifest: Severity,
194 ) -> fallow_config::ResolvedConfig {
195 FallowConfig {
196 rules: RulesConfig {
197 unused_dependency_overrides: base,
198 misconfigured_dependency_overrides: base,
199 empty_catalog_groups: base,
200 ..RulesConfig::default()
201 },
202 overrides: vec![ConfigOverride {
203 files: vec![
204 "package.json".to_string(),
205 "pnpm-workspace.yaml".to_string(),
206 ],
207 rules: PartialRulesConfig {
208 unused_dependency_overrides: Some(manifest),
209 misconfigured_dependency_overrides: Some(manifest),
210 empty_catalog_groups: Some(manifest),
211 ..PartialRulesConfig::default()
212 },
213 }],
214 ..FallowConfig::default()
215 }
216 .resolve(
217 PathBuf::from("/project"),
218 fallow_config::OutputFormat::Human,
219 1,
220 true,
221 true,
222 None,
223 )
224 }
225
226 #[test]
227 fn a_manifest_override_to_warn_clears_a_base_error() {
228 let config = config_with_manifest_override(Severity::Error, Severity::Warn);
229 for (kind, results) in manifest_findings() {
230 assert!(
231 !has_error_severity_issues(&results, &config.rules, Some(&config), false),
232 "the `warn` override for the manifest must win over the base `error` for {kind}"
233 );
234 }
235 }
236
237 #[test]
238 fn a_manifest_override_to_error_raises_a_base_warn() {
239 let config = config_with_manifest_override(Severity::Warn, Severity::Error);
240 for (kind, results) in manifest_findings() {
241 assert!(
242 has_error_severity_issues(&results, &config.rules, Some(&config), false),
243 "the `error` override for the manifest must win over the base `warn` for {kind}"
244 );
245 }
246 }
247
248 #[test]
251 fn the_count_holds_each_finding_at_its_own_severity() {
252 let config = config_with_manifest_override(Severity::Error, Severity::Error);
253 let mut all = AnalysisResults::default();
254 for (_, results) in manifest_findings() {
255 all.unused_dependency_overrides
256 .extend(results.unused_dependency_overrides);
257 all.misconfigured_dependency_overrides
258 .extend(results.misconfigured_dependency_overrides);
259 all.empty_catalog_groups
260 .extend(results.empty_catalog_groups);
261 }
262 assert_eq!(
263 count_error_severity_issues(&all, &config.rules, Some(&config), false),
264 3
265 );
266
267 let mixed = config_with_manifest_override(Severity::Error, Severity::Warn);
268 assert_eq!(
269 count_error_severity_issues(&all, &mixed.rules, Some(&mixed), false),
270 0
271 );
272 assert_eq!(
273 count_error_severity_issues(&all, &mixed.rules, Some(&mixed), true),
274 3,
275 "--fail-on-issues promotes the per-file `warn`"
276 );
277 assert_eq!(
278 count_error_severity_issues(&AnalysisResults::default(), &config.rules, None, false),
279 0
280 );
281 }
282}