1use std::path::Path;
31
32use fallow_config::{ResolvedConfig, RulesConfig, Severity};
33use fallow_types::output_dead_code::{
34 BoundaryCallViolationFinding, BoundaryCoverageViolationFinding, BoundaryViolationFinding,
35 CircularDependencyFinding, DeprecatedExportInUseFinding, DevDependencyInProductionFinding,
36 DuplicateExportFinding, DynamicSegmentNameConflictFinding, EffectiveSeverity,
37 EmptyCatalogGroupFinding, GatedFinding, InvalidClientExportFinding,
38 MisconfiguredDependencyOverrideFinding, MisplacedDirectiveFinding,
39 MixedClientServerBarrelFinding, PolicyViolationFinding, PrivateTypeLeakFinding,
40 ReExportCycleFinding, RouteCollisionFinding, TestOnlyDependencyFinding,
41 TypeOnlyDependencyFinding, UnlistedDependencyFinding, UnprovidedInjectFinding,
42 UnrenderedComponentFinding, UnresolvedCatalogReferenceFinding, UnresolvedImportFinding,
43 UnusedCatalogEntryFinding, UnusedClassMemberFinding, UnusedComponentEmitFinding,
44 UnusedComponentInputFinding, UnusedComponentOutputFinding, UnusedComponentPropFinding,
45 UnusedDependencyFinding, UnusedDependencyOverrideFinding, UnusedDevDependencyFinding,
46 UnusedEnumMemberFinding, UnusedExportFinding, UnusedFileFinding, UnusedLoadDataKeyFinding,
47 UnusedOptionalDependencyFinding, UnusedServerActionFinding, UnusedStoreMemberFinding,
48 UnusedSvelteEventFinding, UnusedTypeFinding,
49};
50use fallow_types::results::{AnalysisResults, PolicyViolationSeverity, StaleSuppression};
51
52use crate::error_severity::promote_warns_to_errors;
53
54fn gate(severity: Severity) -> Option<EffectiveSeverity> {
55 match severity {
56 Severity::Error => Some(EffectiveSeverity::Error),
57 Severity::Warn => Some(EffectiveSeverity::Warn),
58 Severity::Off => None,
59 }
60}
61
62#[derive(Clone, Copy)]
64pub struct SeveritySource<'a> {
65 base: &'a RulesConfig,
66 overrides: Option<&'a ResolvedConfig>,
67 promote_warns: bool,
68}
69
70impl<'a> SeveritySource<'a> {
71 #[must_use]
73 pub fn from_config(config: &'a ResolvedConfig) -> Self {
74 Self::new(&config.rules, Some(config), false)
75 }
76
77 #[must_use]
82 pub fn new(
83 base: &'a RulesConfig,
84 config: Option<&'a ResolvedConfig>,
85 promote_warns: bool,
86 ) -> Self {
87 Self {
88 base,
89 overrides: config.filter(|config| !config.overrides.is_empty()),
90 promote_warns,
91 }
92 }
93
94 fn for_path(&self, path: &Path, rule: fn(&RulesConfig) -> Severity) -> Severity {
95 let Some(config) = self.overrides else {
96 return rule(self.base);
97 };
98 let mut rules = config.resolve_rules_for_path(path);
99 if self.promote_warns {
100 promote_warns_to_errors(&mut rules);
101 }
102 rule(&rules)
103 }
104
105 fn project(&self, rule: fn(&RulesConfig) -> Severity) -> Severity {
106 rule(self.base)
107 }
108
109 fn uniform(&self, rule: fn(&RulesConfig) -> Severity) -> Option<Severity> {
112 self.overrides.is_none().then(|| rule(self.base))
113 }
114}
115
116pub trait RuleSeverity {
118 fn rule_severity(&self, source: &SeveritySource<'_>) -> Severity;
120
121 fn uniform_severity(_source: &SeveritySource<'_>) -> Option<Severity>
127 where
128 Self: Sized,
129 {
130 None
131 }
132}
133
134macro_rules! file_scoped {
135 ($($finding:ty => $path:ident . $field:ident, $rule:ident;)+) => {
136 $(
137 impl RuleSeverity for $finding {
138 fn rule_severity(&self, source: &SeveritySource<'_>) -> Severity {
139 source.for_path(&self.$path.$field, |rules| rules.$rule)
140 }
141
142 fn uniform_severity(source: &SeveritySource<'_>) -> Option<Severity> {
143 source.uniform(|rules| rules.$rule)
144 }
145 }
146 )+
147 };
148}
149
150macro_rules! project_level {
151 ($($finding:ty => $rule:ident;)+) => {
152 $(
153 impl RuleSeverity for $finding {
154 fn rule_severity(&self, source: &SeveritySource<'_>) -> Severity {
155 source.project(|rules| rules.$rule)
156 }
157
158 fn uniform_severity(source: &SeveritySource<'_>) -> Option<Severity> {
159 Some(source.project(|rules| rules.$rule))
160 }
161 }
162 )+
163 };
164}
165
166file_scoped! {
167 UnusedFileFinding => file.path, unused_files;
168 UnusedExportFinding => export.path, unused_exports;
169 UnusedTypeFinding => export.path, unused_types;
170 PrivateTypeLeakFinding => leak.path, private_type_leaks;
171 DeprecatedExportInUseFinding => export.path, deprecated_exports_in_use;
172 UnusedEnumMemberFinding => member.path, unused_enum_members;
173 UnusedClassMemberFinding => member.path, unused_class_members;
174 UnusedStoreMemberFinding => member.path, unused_store_members;
175 UnprovidedInjectFinding => inject.path, unprovided_injects;
176 UnresolvedImportFinding => import.path, unresolved_imports;
177 UnrenderedComponentFinding => component.path, unrendered_components;
178 UnusedComponentPropFinding => prop.path, unused_component_props;
179 UnusedComponentEmitFinding => emit.path, unused_component_emits;
180 UnusedComponentInputFinding => input.path, unused_component_inputs;
181 UnusedComponentOutputFinding => output.path, unused_component_outputs;
182 UnusedSvelteEventFinding => event.path, unused_svelte_events;
183 UnusedServerActionFinding => action.path, unused_server_actions;
184 UnusedLoadDataKeyFinding => key.path, unused_load_data_keys;
185 InvalidClientExportFinding => export.path, invalid_client_export;
186 MixedClientServerBarrelFinding => barrel.path, mixed_client_server_barrel;
187 MisplacedDirectiveFinding => directive_site.path, misplaced_directive;
188 RouteCollisionFinding => collision.path, route_collision;
189 DynamicSegmentNameConflictFinding => conflict.path, dynamic_segment_name_conflict;
190 BoundaryViolationFinding => violation.from_path, boundary_violation;
191 BoundaryCoverageViolationFinding => violation.path, boundary_violation;
192 BoundaryCallViolationFinding => violation.path, boundary_violation;
193 UnresolvedCatalogReferenceFinding => reference.path, unresolved_catalog_references;
194 EmptyCatalogGroupFinding => group.path, empty_catalog_groups;
195 UnusedDependencyOverrideFinding => entry.path, unused_dependency_overrides;
196 MisconfiguredDependencyOverrideFinding => entry.path, misconfigured_dependency_overrides;
197}
198
199project_level! {
200 UnusedDependencyFinding => unused_dependencies;
201 UnusedDevDependencyFinding => unused_dev_dependencies;
202 UnusedOptionalDependencyFinding => unused_optional_dependencies;
203 UnlistedDependencyFinding => unlisted_dependencies;
204 DuplicateExportFinding => duplicate_exports;
205 TypeOnlyDependencyFinding => type_only_dependencies;
206 TestOnlyDependencyFinding => test_only_dependencies;
207 DevDependencyInProductionFinding => dev_dependencies_in_production;
208 ReExportCycleFinding => re_export_cycle;
209 UnusedCatalogEntryFinding => unused_catalog_entries;
210}
211
212impl RuleSeverity for CircularDependencyFinding {
213 fn rule_severity(&self, source: &SeveritySource<'_>) -> Severity {
214 self.cycle
215 .files
216 .iter()
217 .map(|path| source.for_path(path, |rules| rules.circular_dependencies))
218 .max_by_key(|severity| severity_rank(*severity))
219 .unwrap_or_else(|| source.project(|rules| rules.circular_dependencies))
220 }
221
222 fn uniform_severity(source: &SeveritySource<'_>) -> Option<Severity> {
223 source.uniform(|rules| rules.circular_dependencies)
224 }
225}
226
227impl RuleSeverity for StaleSuppression {
228 fn rule_severity(&self, source: &SeveritySource<'_>) -> Severity {
229 if self.missing_reason {
230 source.for_path(&self.path, |rules| rules.require_suppression_reason)
231 } else {
232 source.for_path(&self.path, |rules| rules.stale_suppressions)
233 }
234 }
235
236 fn uniform_severity(source: &SeveritySource<'_>) -> Option<Severity> {
237 let stale = source.uniform(|rules| rules.stale_suppressions)?;
238 let missing_reason = source.uniform(|rules| rules.require_suppression_reason)?;
239 (stale == missing_reason).then_some(stale)
240 }
241}
242
243impl RuleSeverity for PolicyViolationFinding {
244 fn rule_severity(&self, _source: &SeveritySource<'_>) -> Severity {
245 match self.violation.severity {
246 PolicyViolationSeverity::Error => Severity::Error,
247 PolicyViolationSeverity::Warn => Severity::Warn,
248 }
249 }
250}
251
252const fn severity_rank(severity: Severity) -> u8 {
253 match severity {
254 Severity::Off => 0,
255 Severity::Warn => 1,
256 Severity::Error => 2,
257 }
258}
259
260trait GatedRuleFinding: GatedFinding + RuleSeverity {}
262
263impl<T: GatedFinding + RuleSeverity> GatedRuleFinding for T {}
264
265pub fn apply_effective_severities(results: &mut AnalysisResults, config: &ResolvedConfig) {
271 let source = SeveritySource::from_config(config);
272 for_each_gated_finding(results, &mut |finding| {
273 let severity = finding.rule_severity(&source);
274 finding.set_effective_severity(gate(severity));
275 });
276 apply_non_gating_severities(results, &config.rules);
277}
278
279fn apply_non_gating_severities(results: &mut AnalysisResults, rules: &RulesConfig) {
286 set_all(&mut results.prop_drilling_chains, rules.prop_drilling);
287 set_all(&mut results.thin_wrappers, rules.thin_wrapper);
288 set_all(
289 &mut results.duplicate_prop_shapes,
290 rules.duplicate_prop_shape,
291 );
292}
293
294fn set_all<T: GatedFinding>(findings: &mut [T], rule: Severity) {
295 for finding in findings {
296 finding.set_effective_severity(gate(rule));
297 }
298}
299
300#[must_use]
309pub fn findings_without_severity(mut results: AnalysisResults) -> usize {
310 let mut missing = 0;
311 for_each_gated_finding(&mut results, &mut |finding| {
312 if finding.effective_severity().is_none() {
313 missing += 1;
314 }
315 });
316 missing
317}
318
319pub fn promote_effective_warns(results: &mut AnalysisResults) {
324 for_each_gated_finding(results, &mut |finding| {
325 if finding.effective_severity() == Some(EffectiveSeverity::Warn) {
326 finding.set_effective_severity(Some(EffectiveSeverity::Error));
327 }
328 });
329}
330
331#[must_use]
335pub fn any_finding_with_severity(
336 results: &AnalysisResults,
337 source: &SeveritySource<'_>,
338 severity: Severity,
339) -> bool {
340 results
341 .policy_violations
342 .iter()
343 .any(|finding| finding.rule_severity(source) == severity)
344 || any_gated_finding(results, source, severity)
345}
346
347fn visit<T: GatedRuleFinding>(findings: &mut [T], f: &mut dyn FnMut(&mut dyn GatedRuleFinding)) {
348 for finding in findings {
349 f(finding);
350 }
351}
352
353fn any<T: RuleSeverity>(findings: &[T], source: &SeveritySource<'_>, severity: Severity) -> bool {
358 if findings.is_empty() {
359 return false;
360 }
361 match T::uniform_severity(source) {
362 Some(uniform) => uniform == severity,
363 None => findings
364 .iter()
365 .any(|finding| finding.rule_severity(source) == severity),
366 }
367}
368
369#[expect(
374 clippy::too_many_lines,
375 reason = "one exhaustive list of finding collections; splitting it would lose the compile-time guard"
376)]
377fn for_each_gated_finding(
378 results: &mut AnalysisResults,
379 f: &mut dyn FnMut(&mut dyn GatedRuleFinding),
380) {
381 let AnalysisResults {
382 unused_files,
383 unused_exports,
384 unused_types,
385 private_type_leaks,
386 deprecated_exports_in_use,
387 unused_dependencies,
388 unused_dev_dependencies,
389 unused_optional_dependencies,
390 unused_enum_members,
391 unused_class_members,
392 unused_store_members,
393 unresolved_imports,
394 unlisted_dependencies,
395 duplicate_exports,
396 type_only_dependencies,
397 test_only_dependencies,
398 dev_dependencies_in_production,
399 circular_dependencies,
400 re_export_cycles,
401 boundary_violations,
402 boundary_coverage_violations,
403 boundary_call_violations,
404 stale_suppressions,
405 unused_catalog_entries,
406 empty_catalog_groups,
407 unresolved_catalog_references,
408 unused_dependency_overrides,
409 misconfigured_dependency_overrides,
410 invalid_client_exports,
411 mixed_client_server_barrels,
412 misplaced_directives,
413 unprovided_injects,
414 unrendered_components,
415 route_collisions,
416 dynamic_segment_name_conflicts,
417 unused_component_props,
418 unused_component_emits,
419 unused_component_inputs,
420 unused_component_outputs,
421 unused_svelte_events,
422 unused_server_actions,
423 unused_load_data_keys,
424 policy_violations: _,
426 prop_drilling_chains: _,
428 thin_wrappers: _,
429 duplicate_prop_shapes: _,
430 unused_load_data_keys_global_abstain: _,
432 suppression_count: _,
433 unused_component_props_exempted: _,
434 active_suppressions: _,
435 feature_flags: _,
436 export_usages: _,
437 entry_point_summary: _,
438 render_fan_in: _,
439 react_component_intel: _,
440 semantic_framework_contracts: _,
441 security_findings: _,
443 security_unresolved_edge_files: _,
444 security_unresolved_callee_sites: _,
445 security_unresolved_callee_diagnostics: _,
446 } = results;
447 visit(unused_files, f);
448 visit(unused_exports, f);
449 visit(unused_types, f);
450 visit(private_type_leaks, f);
451 visit(deprecated_exports_in_use, f);
452 visit(unused_dependencies, f);
453 visit(unused_dev_dependencies, f);
454 visit(unused_optional_dependencies, f);
455 visit(unused_enum_members, f);
456 visit(unused_class_members, f);
457 visit(unused_store_members, f);
458 visit(unresolved_imports, f);
459 visit(unlisted_dependencies, f);
460 visit(duplicate_exports, f);
461 visit(type_only_dependencies, f);
462 visit(test_only_dependencies, f);
463 visit(dev_dependencies_in_production, f);
464 visit(circular_dependencies, f);
465 visit(re_export_cycles, f);
466 visit(boundary_violations, f);
467 visit(boundary_coverage_violations, f);
468 visit(boundary_call_violations, f);
469 visit(stale_suppressions, f);
470 visit(unused_catalog_entries, f);
471 visit(empty_catalog_groups, f);
472 visit(unresolved_catalog_references, f);
473 visit(unused_dependency_overrides, f);
474 visit(misconfigured_dependency_overrides, f);
475 visit(invalid_client_exports, f);
476 visit(mixed_client_server_barrels, f);
477 visit(misplaced_directives, f);
478 visit(unprovided_injects, f);
479 visit(unrendered_components, f);
480 visit(route_collisions, f);
481 visit(dynamic_segment_name_conflicts, f);
482 visit(unused_component_props, f);
483 visit(unused_component_emits, f);
484 visit(unused_component_inputs, f);
485 visit(unused_component_outputs, f);
486 visit(unused_svelte_events, f);
487 visit(unused_server_actions, f);
488 visit(unused_load_data_keys, f);
489}
490
491#[expect(
497 clippy::too_many_lines,
498 reason = "one exhaustive list of finding collections; splitting it would lose the compile-time guard"
499)]
500fn any_gated_finding(
501 results: &AnalysisResults,
502 source: &SeveritySource<'_>,
503 severity: Severity,
504) -> bool {
505 let AnalysisResults {
506 unused_files,
507 unused_exports,
508 unused_types,
509 private_type_leaks,
510 deprecated_exports_in_use,
511 unused_dependencies,
512 unused_dev_dependencies,
513 unused_optional_dependencies,
514 unused_enum_members,
515 unused_class_members,
516 unused_store_members,
517 unresolved_imports,
518 unlisted_dependencies,
519 duplicate_exports,
520 type_only_dependencies,
521 test_only_dependencies,
522 dev_dependencies_in_production,
523 circular_dependencies,
524 re_export_cycles,
525 boundary_violations,
526 boundary_coverage_violations,
527 boundary_call_violations,
528 stale_suppressions,
529 unused_catalog_entries,
530 empty_catalog_groups,
531 unresolved_catalog_references,
532 unused_dependency_overrides,
533 misconfigured_dependency_overrides,
534 invalid_client_exports,
535 mixed_client_server_barrels,
536 misplaced_directives,
537 unprovided_injects,
538 unrendered_components,
539 route_collisions,
540 dynamic_segment_name_conflicts,
541 unused_component_props,
542 unused_component_emits,
543 unused_component_inputs,
544 unused_component_outputs,
545 unused_svelte_events,
546 unused_server_actions,
547 unused_load_data_keys,
548 policy_violations: _,
549 prop_drilling_chains: _,
550 thin_wrappers: _,
551 duplicate_prop_shapes: _,
552 unused_load_data_keys_global_abstain: _,
553 suppression_count: _,
554 unused_component_props_exempted: _,
555 active_suppressions: _,
556 feature_flags: _,
557 export_usages: _,
558 entry_point_summary: _,
559 render_fan_in: _,
560 react_component_intel: _,
561 semantic_framework_contracts: _,
562 security_findings: _,
563 security_unresolved_edge_files: _,
564 security_unresolved_callee_sites: _,
565 security_unresolved_callee_diagnostics: _,
566 } = results;
567 any(unused_files, source, severity)
568 || any(unused_exports, source, severity)
569 || any(unused_types, source, severity)
570 || any(private_type_leaks, source, severity)
571 || any(deprecated_exports_in_use, source, severity)
572 || any(unused_dependencies, source, severity)
573 || any(unused_dev_dependencies, source, severity)
574 || any(unused_optional_dependencies, source, severity)
575 || any(unused_enum_members, source, severity)
576 || any(unused_class_members, source, severity)
577 || any(unused_store_members, source, severity)
578 || any(unresolved_imports, source, severity)
579 || any(unlisted_dependencies, source, severity)
580 || any(duplicate_exports, source, severity)
581 || any(type_only_dependencies, source, severity)
582 || any(test_only_dependencies, source, severity)
583 || any(dev_dependencies_in_production, source, severity)
584 || any(circular_dependencies, source, severity)
585 || any(re_export_cycles, source, severity)
586 || any(boundary_violations, source, severity)
587 || any(boundary_coverage_violations, source, severity)
588 || any(boundary_call_violations, source, severity)
589 || any(stale_suppressions, source, severity)
590 || any(unused_catalog_entries, source, severity)
591 || any(empty_catalog_groups, source, severity)
592 || any(unresolved_catalog_references, source, severity)
593 || any(unused_dependency_overrides, source, severity)
594 || any(misconfigured_dependency_overrides, source, severity)
595 || any(invalid_client_exports, source, severity)
596 || any(mixed_client_server_barrels, source, severity)
597 || any(misplaced_directives, source, severity)
598 || any(unprovided_injects, source, severity)
599 || any(unrendered_components, source, severity)
600 || any(route_collisions, source, severity)
601 || any(dynamic_segment_name_conflicts, source, severity)
602 || any(unused_component_props, source, severity)
603 || any(unused_component_emits, source, severity)
604 || any(unused_component_inputs, source, severity)
605 || any(unused_component_outputs, source, severity)
606 || any(unused_svelte_events, source, severity)
607 || any(unused_server_actions, source, severity)
608 || any(unused_load_data_keys, source, severity)
609}
610
611#[cfg(test)]
612mod tests {
613 use std::path::PathBuf;
614
615 use fallow_types::output_dead_code::{
616 CircularDependencyFinding, MisconfiguredDependencyOverrideFinding, UnusedDependencyFinding,
617 UnusedExportFinding,
618 };
619 use fallow_types::results::StaleSuppression;
620 use serde_json::json;
621
622 use super::*;
623
624 const ROOT: &str = "/project";
625
626 fn config(json: &str) -> ResolvedConfig {
627 serde_json::from_str::<fallow_config::FallowConfig>(json)
628 .expect("config parses")
629 .resolve(
630 PathBuf::from(ROOT),
631 fallow_config::OutputFormat::Human,
632 1,
633 true,
634 true,
635 None,
636 )
637 }
638
639 fn legacy_warn_config() -> ResolvedConfig {
640 config(
641 r#"{
642 "rules": {
643 "unused-exports": "error",
644 "circular-dependencies": "error",
645 "unused-dependencies": "error",
646 "stale-suppressions": "warn",
647 "require-suppression-reason": "error"
648 },
649 "overrides": [{
650 "files": ["src/legacy/**", "package.json"],
651 "rules": {
652 "unused-exports": "warn",
653 "circular-dependencies": "warn",
654 "unused-dependencies": "warn",
655 "require-suppression-reason": "warn"
656 }
657 }]
658 }"#,
659 )
660 }
661
662 fn export(path: &str) -> UnusedExportFinding {
663 serde_json::from_value(json!({
664 "path": format!("{ROOT}/{path}"),
665 "export_name": "unused",
666 "is_type_only": false,
667 "line": 1,
668 "col": 0,
669 "span_start": 0,
670 "is_re_export": false,
671 "actions": [],
672 }))
673 .expect("export finding")
674 }
675
676 fn cycle(files: &[&str]) -> CircularDependencyFinding {
677 serde_json::from_value(json!({
678 "files": files.iter().map(|file| format!("{ROOT}/{file}")).collect::<Vec<_>>(),
679 "length": files.len(),
680 "line": 1,
681 "col": 0,
682 "actions": [],
683 }))
684 .expect("cycle finding")
685 }
686
687 fn stale(path: &str, missing_reason: bool) -> StaleSuppression {
688 serde_json::from_value(json!({
689 "path": format!("{ROOT}/{path}"),
690 "line": 1,
691 "col": 0,
692 "origin": { "type": "comment", "is_file_level": false },
693 "missing_reason": missing_reason,
694 "actions": [],
695 }))
696 .expect("stale suppression")
697 }
698
699 #[test]
700 fn file_scoped_findings_follow_the_override_for_their_path() {
701 let mut results = AnalysisResults::default();
702 results.unused_exports.push(export("src/app.ts"));
703 results.unused_exports.push(export("src/legacy/old.ts"));
704
705 apply_effective_severities(&mut results, &legacy_warn_config());
706
707 let severities: Vec<_> = results
708 .unused_exports
709 .iter()
710 .map(|finding| finding.effective_severity)
711 .collect();
712 assert_eq!(
713 severities,
714 vec![
715 Some(EffectiveSeverity::Error),
716 Some(EffectiveSeverity::Warn)
717 ]
718 );
719 }
720
721 #[test]
722 fn a_cycle_is_error_when_any_file_in_it_resolves_to_error() {
723 let mut results = AnalysisResults::default();
724 results
725 .circular_dependencies
726 .push(cycle(&["src/legacy/a.ts", "src/b.ts"]));
727 results
728 .circular_dependencies
729 .push(cycle(&["src/legacy/a.ts", "src/legacy/b.ts"]));
730
731 apply_effective_severities(&mut results, &legacy_warn_config());
732
733 assert_eq!(
734 results.circular_dependencies[0].effective_severity,
735 Some(EffectiveSeverity::Error)
736 );
737 assert_eq!(
738 results.circular_dependencies[1].effective_severity,
739 Some(EffectiveSeverity::Warn)
740 );
741 }
742
743 #[test]
744 fn project_level_findings_use_the_base_rules() {
745 let mut results = AnalysisResults::default();
746 results.unused_dependencies.push(
747 serde_json::from_value::<UnusedDependencyFinding>(json!({
748 "package_name": "left-pad",
749 "location": "dependencies",
750 "path": format!("{ROOT}/package.json"),
751 "line": 3,
752 "actions": [],
753 }))
754 .expect("dependency finding"),
755 );
756
757 apply_effective_severities(&mut results, &legacy_warn_config());
758
759 assert_eq!(
760 results.unused_dependencies[0].effective_severity,
761 Some(EffectiveSeverity::Error)
762 );
763 }
764
765 #[test]
766 fn a_dependency_override_follows_the_override_for_its_file() {
767 let config = config(
768 r#"{
769 "rules": { "misconfigured-dependency-overrides": "error" },
770 "overrides": [{
771 "files": ["pnpm-workspace.yaml"],
772 "rules": { "misconfigured-dependency-overrides": "warn" }
773 }]
774 }"#,
775 );
776 let mut results = AnalysisResults::default();
777 for file in ["pnpm-workspace.yaml", "package.json"] {
778 results.misconfigured_dependency_overrides.push(
779 serde_json::from_value::<MisconfiguredDependencyOverrideFinding>(json!({
780 "raw_key": "",
781 "raw_value": "^1.0.0",
782 "reason": "empty-value",
783 "source": file,
784 "path": format!("{ROOT}/{file}"),
785 "line": 2,
786 "actions": [],
787 }))
788 .expect("override finding"),
789 );
790 }
791
792 apply_effective_severities(&mut results, &config);
793
794 let severities: Vec<_> = results
795 .misconfigured_dependency_overrides
796 .iter()
797 .map(|finding| finding.effective_severity)
798 .collect();
799 assert_eq!(
800 severities,
801 vec![
802 Some(EffectiveSeverity::Warn),
803 Some(EffectiveSeverity::Error)
804 ]
805 );
806 }
807
808 #[test]
809 fn a_stale_suppression_reads_the_rule_for_its_kind() {
810 let mut results = AnalysisResults::default();
811 results.stale_suppressions.push(stale("src/app.ts", false));
812 results.stale_suppressions.push(stale("src/app.ts", true));
813 results
814 .stale_suppressions
815 .push(stale("src/legacy/old.ts", true));
816
817 apply_effective_severities(&mut results, &legacy_warn_config());
818
819 let severities: Vec<_> = results
820 .stale_suppressions
821 .iter()
822 .map(|finding| finding.effective_severity)
823 .collect();
824 assert_eq!(
825 severities,
826 vec![
827 Some(EffectiveSeverity::Warn),
828 Some(EffectiveSeverity::Error),
829 Some(EffectiveSeverity::Warn),
830 ]
831 );
832 }
833
834 #[test]
835 fn fail_on_issues_promotion_raises_warn_and_keeps_error() {
836 let mut results = AnalysisResults::default();
837 results.unused_exports.push(export("src/app.ts"));
838 results.unused_exports.push(export("src/legacy/old.ts"));
839 apply_effective_severities(&mut results, &legacy_warn_config());
840
841 promote_effective_warns(&mut results);
842
843 assert!(
844 results
845 .unused_exports
846 .iter()
847 .all(|finding| finding.effective_severity == Some(EffectiveSeverity::Error))
848 );
849 }
850
851 #[test]
855 fn a_cycle_whose_files_all_resolve_to_off_gets_no_severity() {
856 let config = config(
857 r#"{
858 "rules": { "circular-dependencies": "error" },
859 "overrides": [{
860 "files": ["src/legacy/**"],
861 "rules": { "circular-dependencies": "off" }
862 }]
863 }"#,
864 );
865 let mut results = AnalysisResults::default();
866 results
867 .circular_dependencies
868 .push(cycle(&["src/legacy/a.ts", "src/legacy/b.ts"]));
869
870 apply_effective_severities(&mut results, &config);
871
872 assert_eq!(results.circular_dependencies[0].effective_severity, None);
873 assert!(!crate::error_severity::has_error_severity_issues(
874 &results,
875 &config.rules,
876 Some(&config),
877 false
878 ));
879 assert_eq!(
881 results.circular_dependencies[0].rule_severity(&SeveritySource::from_config(&config)),
882 Severity::Off
883 );
884 }
885
886 #[test]
887 fn the_collection_check_agrees_with_the_per_finding_check_without_overrides() {
888 let configs = [
889 r#"{ "rules": { "unused-exports": "error", "circular-dependencies": "warn",
890 "unused-dependencies": "off", "stale-suppressions": "warn",
891 "require-suppression-reason": "warn" } }"#,
892 r#"{ "rules": { "unused-exports": "warn", "circular-dependencies": "error",
893 "unused-dependencies": "error", "stale-suppressions": "warn",
894 "require-suppression-reason": "error" } }"#,
895 r#"{ "rules": { "unused-exports": "off", "circular-dependencies": "off",
896 "unused-dependencies": "warn", "stale-suppressions": "error",
897 "require-suppression-reason": "off" } }"#,
898 ];
899 let mut results = AnalysisResults::default();
900 results.unused_exports.push(export("src/app.ts"));
901 results
902 .circular_dependencies
903 .push(cycle(&["src/a.ts", "src/b.ts"]));
904 results.circular_dependencies.push(cycle(&[]));
905 results.unused_dependencies.push(
906 serde_json::from_value::<UnusedDependencyFinding>(json!({
907 "package_name": "left-pad",
908 "location": "dependencies",
909 "path": format!("{ROOT}/package.json"),
910 "line": 3,
911 "actions": [],
912 }))
913 .expect("dependency finding"),
914 );
915 results.stale_suppressions.push(stale("src/app.ts", false));
916 results.stale_suppressions.push(stale("src/app.ts", true));
917
918 for json in configs {
919 let config = config(json);
920 for promote in [false, true] {
921 let mut rules = config.rules.clone();
922 if promote {
923 promote_warns_to_errors(&mut rules);
924 }
925 let source = SeveritySource::new(&rules, Some(&config), promote);
926 assert!(source.overrides.is_none());
927 for severity in [Severity::Error, Severity::Warn, Severity::Off] {
928 let mut per_finding = false;
929 for_each_gated_finding(&mut results, &mut |finding| {
930 per_finding |= finding.rule_severity(&source) == severity;
931 });
932 assert_eq!(
933 any_gated_finding(&results, &source, severity),
934 per_finding,
935 "{json} promote={promote} {severity:?}"
936 );
937 }
938 }
939 }
940 }
941
942 type AddFinding = fn(&mut AnalysisResults);
943
944 #[test]
945 fn the_exit_code_rule_fails_exactly_when_a_finding_is_stamped_error() {
946 let config = legacy_warn_config();
947 let cases: [(&str, AddFinding); 4] = [
948 ("legacy export", |r| {
949 r.unused_exports.push(export("src/legacy/old.ts"));
950 }),
951 ("app export", |r| {
952 r.unused_exports.push(export("src/app.ts"));
953 }),
954 ("legacy cycle", |r| {
955 r.circular_dependencies
956 .push(cycle(&["src/legacy/a.ts", "src/legacy/b.ts"]));
957 }),
958 ("stale suppression", |r| {
959 r.stale_suppressions.push(stale("src/app.ts", false));
960 }),
961 ];
962 for (name, add) in cases {
963 let mut results = AnalysisResults::default();
964 add(&mut results);
965 apply_effective_severities(&mut results, &config);
966 let mut stamped_error = false;
967 for_each_gated_finding(&mut results, &mut |finding| {
968 stamped_error |= finding.effective_severity() == Some(EffectiveSeverity::Error);
969 });
970 assert_eq!(
971 crate::error_severity::has_error_severity_issues(
972 &results,
973 &config.rules,
974 Some(&config),
975 false
976 ),
977 stamped_error,
978 "{name}"
979 );
980 }
981 }
982}