1use fallow_types::suppress::IssueKind;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
13#[serde(rename_all = "lowercase")]
14pub enum Severity {
15 #[default]
17 Error,
18 Warn,
20 Off,
22}
23
24impl Severity {
25 const fn default_warn() -> Self {
27 Self::Warn
28 }
29
30 const fn default_off() -> Self {
32 Self::Off
33 }
34}
35
36impl std::fmt::Display for Severity {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 Self::Error => write!(f, "error"),
40 Self::Warn => write!(f, "warn"),
41 Self::Off => write!(f, "off"),
42 }
43 }
44}
45
46impl std::str::FromStr for Severity {
47 type Err = String;
48
49 fn from_str(s: &str) -> Result<Self, Self::Err> {
50 match s.to_lowercase().as_str() {
51 "error" => Ok(Self::Error),
52 "warn" | "warning" => Ok(Self::Warn),
53 "off" | "none" => Ok(Self::Off),
54 other => Err(format!(
55 "unknown severity: '{other}' (expected error, warn, or off)"
56 )),
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
68#[serde(rename_all = "kebab-case")]
69pub struct RulesConfig {
70 #[serde(default, alias = "unused-file")]
72 pub unused_files: Severity,
73 #[serde(default, alias = "unused-export")]
75 pub unused_exports: Severity,
76 #[serde(default, alias = "unused-type")]
78 pub unused_types: Severity,
79 #[serde(default = "Severity::default_off", alias = "private-type-leak")]
82 pub private_type_leaks: Severity,
83 #[serde(skip)]
96 pub private_type_leaks_configured: bool,
97 #[serde(default = "Severity::default_off", alias = "deprecated-export-in-use")]
101 pub deprecated_exports_in_use: Severity,
102 #[serde(default, alias = "unused-dependency")]
105 pub unused_dependencies: Severity,
106 #[serde(default = "Severity::default_warn", alias = "unused-dev-dependency")]
109 pub unused_dev_dependencies: Severity,
110 #[serde(
113 default = "Severity::default_warn",
114 alias = "unused-optional-dependency"
115 )]
116 pub unused_optional_dependencies: Severity,
117 #[serde(default, alias = "unused-enum-member")]
119 pub unused_enum_members: Severity,
120 #[serde(default, alias = "unused-class-member")]
123 pub unused_class_members: Severity,
124 #[serde(default = "Severity::default_warn", alias = "unused-store-member")]
134 pub unused_store_members: Severity,
135 #[serde(default = "Severity::default_warn", alias = "unprovided-inject")]
143 pub unprovided_injects: Severity,
144 #[serde(default = "Severity::default_warn", alias = "unrendered-component")]
152 pub unrendered_components: Severity,
153 #[serde(default = "Severity::default_warn", alias = "unused-component-prop")]
161 pub unused_component_props: Severity,
162 #[serde(default = "Severity::default_off", alias = "absent-component-prop")]
164 pub absent_component_props: Severity,
165 #[serde(default = "Severity::default_warn", alias = "unused-component-emit")]
173 pub unused_component_emits: Severity,
174 #[serde(default = "Severity::default_warn", alias = "unused-component-input")]
183 pub unused_component_inputs: Severity,
184 #[serde(default = "Severity::default_warn", alias = "unused-component-output")]
193 pub unused_component_outputs: Severity,
194 #[serde(default = "Severity::default_warn", alias = "unused-svelte-event")]
204 pub unused_svelte_events: Severity,
205 #[serde(default = "Severity::default_warn", alias = "unused-effect-layer")]
209 pub unused_effect_layers: Severity,
210 #[serde(default = "Severity::default_warn", alias = "unused-tagged-error")]
214 pub unused_tagged_errors: Severity,
215 #[serde(default = "Severity::default_warn", alias = "unproduced-catch-tag")]
219 pub unproduced_catch_tags: Severity,
220 #[serde(default = "Severity::default_warn", alias = "unused-server-action")]
230 pub unused_server_actions: Severity,
231 #[serde(default = "Severity::default_warn", alias = "unused-load-data-key")]
242 pub unused_load_data_keys: Severity,
243 #[serde(default = "Severity::default_off", alias = "prop-drilling")]
252 pub prop_drilling: Severity,
253 #[serde(default = "Severity::default_off", alias = "thin-wrapper")]
258 pub thin_wrapper: Severity,
259 #[serde(default = "Severity::default_off", alias = "duplicate-prop-shape")]
266 pub duplicate_prop_shape: Severity,
267 #[serde(default = "Severity::default_warn", alias = "css-token-drift")]
272 pub css_token_drift: Severity,
273 #[serde(default = "Severity::default_warn", alias = "css-duplicate-block")]
277 pub css_duplicate_block: Severity,
278 #[serde(default = "Severity::default_warn", alias = "css-selector-complexity")]
282 pub css_selector_complexity: Severity,
283 #[serde(default = "Severity::default_warn", alias = "css-dead-surface")]
287 pub css_dead_surface: Severity,
288 #[serde(default = "Severity::default_warn", alias = "css-broken-reference")]
292 pub css_broken_reference: Severity,
293 #[serde(default)]
302 pub complexity_cyclomatic: Severity,
303 #[serde(default)]
307 pub complexity_cognitive: Severity,
308 #[serde(default)]
314 pub complexity_crap: Severity,
315 #[serde(default, alias = "unresolved-import")]
318 pub unresolved_imports: Severity,
319 #[serde(default, alias = "unlisted-dependency")]
322 pub unlisted_dependencies: Severity,
323 #[serde(default, alias = "duplicate-export")]
326 pub duplicate_exports: Severity,
327 #[serde(default = "Severity::default_warn", alias = "type-only-dependency")]
330 pub type_only_dependencies: Severity,
331 #[serde(default = "Severity::default_warn", alias = "test-only-dependency")]
334 pub test_only_dependencies: Severity,
335 #[serde(
339 default = "Severity::default_warn",
340 alias = "dev-dependency-in-production"
341 )]
342 pub dev_dependencies_in_production: Severity,
343 #[serde(default, alias = "circular-dependency")]
345 pub circular_dependencies: Severity,
346 #[serde(
349 default = "Severity::default_warn",
350 alias = "re-export-cycles",
351 alias = "reexport-cycle",
352 alias = "reexport-cycles"
353 )]
354 pub re_export_cycle: Severity,
355 #[serde(default = "Severity::default_warn", alias = "package-cycles")]
358 pub package_cycle: Severity,
359 #[serde(default, alias = "boundary-violations")]
362 pub boundary_violation: Severity,
363 #[serde(default = "Severity::default_off", alias = "coverage-gap")]
366 pub coverage_gaps: Severity,
367 #[serde(default = "Severity::default_off", alias = "feature-flag")]
370 pub feature_flags: Severity,
371 #[serde(default = "Severity::default_warn", alias = "stale-suppression")]
374 pub stale_suppressions: Severity,
375 #[serde(default = "Severity::default_off", alias = "suppression-reason")]
378 pub require_suppression_reason: Severity,
379 #[serde(default = "Severity::default_warn", alias = "unused-catalog-entry")]
382 pub unused_catalog_entries: Severity,
383 #[serde(default = "Severity::default_warn", alias = "empty-catalog-group")]
385 pub empty_catalog_groups: Severity,
386 #[serde(default, alias = "unresolved-catalog-reference")]
390 pub unresolved_catalog_references: Severity,
391 #[serde(
395 default = "Severity::default_warn",
396 alias = "unused-dependency-override"
397 )]
398 pub unused_dependency_overrides: Severity,
399 #[serde(default, alias = "misconfigured-dependency-override")]
403 pub misconfigured_dependency_overrides: Severity,
404 #[serde(default = "Severity::default_off")]
408 pub security_client_server_leak: Severity,
409 #[serde(default = "Severity::default_off")]
414 pub security_sink: Severity,
415 #[serde(default = "Severity::default_warn", alias = "policy-violations")]
421 pub policy_violation: Severity,
422 #[serde(default = "Severity::default_warn", alias = "invalid-client-exports")]
427 pub invalid_client_export: Severity,
428 #[serde(
433 default = "Severity::default_warn",
434 alias = "mixed-client-server-barrels"
435 )]
436 pub mixed_client_server_barrel: Severity,
437 #[serde(default = "Severity::default_warn", alias = "misplaced-directives")]
443 pub misplaced_directive: Severity,
444 #[serde(default, alias = "route-collisions")]
451 pub route_collision: Severity,
452 #[serde(default, alias = "dynamic-segment-name-conflicts")]
462 pub dynamic_segment_name_conflict: Severity,
463 #[serde(default = "Severity::default_warn")]
467 pub weakened_tests: Severity,
468 #[serde(default = "Severity::default_warn", alias = "added-suppressions")]
471 pub added_suppression: Severity,
472 #[serde(default = "Severity::default_warn", alias = "test-only-branches")]
475 pub test_only_branch: Severity,
476 #[serde(default = "Severity::default_off", alias = "diverged-clone-siblings")]
481 pub diverged_clone_sibling: Severity,
482 #[serde(default = "Severity::default_off", alias = "added-dependencies")]
485 pub added_dependency: Severity,
486}
487
488impl Default for RulesConfig {
489 fn default() -> Self {
490 Self {
491 unused_files: Severity::Error,
492 unused_exports: Severity::Error,
493 unused_types: Severity::Error,
494 private_type_leaks: Severity::Off,
495 private_type_leaks_configured: false,
496 deprecated_exports_in_use: Severity::Off,
497 unused_dependencies: Severity::Error,
498 unused_dev_dependencies: Severity::Warn,
499 unused_optional_dependencies: Severity::Warn,
500 unused_enum_members: Severity::Error,
501 unused_class_members: Severity::Error,
502 unused_store_members: Severity::Warn,
503 unprovided_injects: Severity::Warn,
504 unrendered_components: Severity::Warn,
505 unused_component_props: Severity::Warn,
506 absent_component_props: Severity::Off,
507 unused_component_emits: Severity::Warn,
508 unused_component_inputs: Severity::Warn,
509 unused_component_outputs: Severity::Warn,
510 unused_svelte_events: Severity::Warn,
511 unused_effect_layers: Severity::Warn,
512 unused_tagged_errors: Severity::Warn,
513 unproduced_catch_tags: Severity::Warn,
514 unused_server_actions: Severity::Warn,
515 unused_load_data_keys: Severity::Warn,
516 prop_drilling: Severity::Off,
517 thin_wrapper: Severity::Off,
518 duplicate_prop_shape: Severity::Off,
519 css_token_drift: Severity::Warn,
520 css_duplicate_block: Severity::Warn,
521 css_selector_complexity: Severity::Warn,
522 css_dead_surface: Severity::Warn,
523 css_broken_reference: Severity::Warn,
524 complexity_cyclomatic: Severity::Error,
525 complexity_cognitive: Severity::Error,
526 complexity_crap: Severity::Error,
527 unresolved_imports: Severity::Error,
528 unlisted_dependencies: Severity::Error,
529 duplicate_exports: Severity::Error,
530 type_only_dependencies: Severity::Warn,
531 test_only_dependencies: Severity::Warn,
532 dev_dependencies_in_production: Severity::Warn,
533 circular_dependencies: Severity::Error,
534 re_export_cycle: Severity::Warn,
535 package_cycle: Severity::Warn,
536 boundary_violation: Severity::Error,
537 coverage_gaps: Severity::Off,
538 feature_flags: Severity::Off,
539 stale_suppressions: Severity::Warn,
540 require_suppression_reason: Severity::Off,
541 unused_catalog_entries: Severity::Warn,
542 empty_catalog_groups: Severity::Warn,
543 unresolved_catalog_references: Severity::Error,
544 unused_dependency_overrides: Severity::Warn,
545 misconfigured_dependency_overrides: Severity::Error,
546 security_client_server_leak: Severity::Off,
547 security_sink: Severity::Off,
548 policy_violation: Severity::Warn,
549 invalid_client_export: Severity::Warn,
550 mixed_client_server_barrel: Severity::Warn,
551 misplaced_directive: Severity::Warn,
552 route_collision: Severity::Error,
553 dynamic_segment_name_conflict: Severity::Error,
554 weakened_tests: Severity::Warn,
555 added_suppression: Severity::Warn,
556 test_only_branch: Severity::Warn,
557 diverged_clone_sibling: Severity::Off,
558 added_dependency: Severity::Off,
559 }
560 }
561}
562
563macro_rules! apply_partial_rules {
564 ($target:expr, $partial:expr, [$($field:ident),+ $(,)?]) => {
565 $(
566 if let Some(severity) = $partial.$field {
567 $target.$field = severity;
568 }
569 )+
570 };
571}
572
573impl RulesConfig {
574 #[must_use]
586 pub const fn severity_for_kind(&self, kind: IssueKind) -> Severity {
587 match kind {
588 IssueKind::UnusedFile => self.unused_files,
589 IssueKind::UnusedExport => self.unused_exports,
590 IssueKind::UnusedType => self.unused_types,
591 IssueKind::PrivateTypeLeak => self.private_type_leaks,
592 IssueKind::DeprecatedExportInUse => self.deprecated_exports_in_use,
593 IssueKind::UnusedDependency => self.unused_dependencies,
594 IssueKind::UnusedDevDependency => self.unused_dev_dependencies,
595 IssueKind::UnusedEnumMember => self.unused_enum_members,
596 IssueKind::UnusedClassMember => self.unused_class_members,
597 IssueKind::UnusedStoreMember => self.unused_store_members,
598 IssueKind::UnprovidedInject => self.unprovided_injects,
599 IssueKind::UnresolvedImport => self.unresolved_imports,
600 IssueKind::UnlistedDependency => self.unlisted_dependencies,
601 IssueKind::DuplicateExport => self.duplicate_exports,
602 IssueKind::CircularDependency => self.circular_dependencies,
603 IssueKind::ReExportCycle => self.re_export_cycle,
604 IssueKind::PackageCycle => self.package_cycle,
605 IssueKind::TypeOnlyDependency => self.type_only_dependencies,
606 IssueKind::TestOnlyDependency => self.test_only_dependencies,
607 IssueKind::DevDependencyInProduction => self.dev_dependencies_in_production,
608 IssueKind::BoundaryViolation => self.boundary_violation,
609 IssueKind::CoverageGaps => self.coverage_gaps,
610 IssueKind::FeatureFlag => self.feature_flags,
611 IssueKind::StaleSuppression => self.stale_suppressions,
612 IssueKind::PnpmCatalogEntry => self.unused_catalog_entries,
613 IssueKind::EmptyCatalogGroup => self.empty_catalog_groups,
614 IssueKind::UnresolvedCatalogReference => self.unresolved_catalog_references,
615 IssueKind::UnusedDependencyOverride => self.unused_dependency_overrides,
616 IssueKind::MisconfiguredDependencyOverride => self.misconfigured_dependency_overrides,
617 IssueKind::SecurityClientServerLeak => self.security_client_server_leak,
618 IssueKind::SecuritySink => self.security_sink,
619 IssueKind::PolicyViolation => self.policy_violation,
620 IssueKind::InvalidClientExport => self.invalid_client_export,
621 IssueKind::MixedClientServerBarrel => self.mixed_client_server_barrel,
622 IssueKind::MisplacedDirective => self.misplaced_directive,
623 IssueKind::RouteCollision => self.route_collision,
624 IssueKind::DynamicSegmentNameConflict => self.dynamic_segment_name_conflict,
625 IssueKind::UnrenderedComponent => self.unrendered_components,
626 IssueKind::UnusedComponentProp => self.unused_component_props,
627 IssueKind::AbsentComponentProp => self.absent_component_props,
628 IssueKind::UnusedComponentEmit => self.unused_component_emits,
629 IssueKind::UnusedComponentInput => self.unused_component_inputs,
630 IssueKind::UnusedComponentOutput => self.unused_component_outputs,
631 IssueKind::UnusedSvelteEvent => self.unused_svelte_events,
632 IssueKind::UnusedEffectLayer => self.unused_effect_layers,
633 IssueKind::UnusedTaggedError => self.unused_tagged_errors,
634 IssueKind::UnproducedCatchTag => self.unproduced_catch_tags,
635 IssueKind::UnusedServerAction => self.unused_server_actions,
636 IssueKind::UnusedLoadDataKey => self.unused_load_data_keys,
637 IssueKind::PropDrilling => self.prop_drilling,
638 IssueKind::ThinWrapper => self.thin_wrapper,
639 IssueKind::DuplicatePropShape => self.duplicate_prop_shape,
640 IssueKind::CssTokenDrift => self.css_token_drift,
641 IssueKind::CssDuplicateBlock => self.css_duplicate_block,
642 IssueKind::CssSelectorComplexity => self.css_selector_complexity,
643 IssueKind::CssDeadSurface => self.css_dead_surface,
644 IssueKind::CssBrokenReference => self.css_broken_reference,
645 IssueKind::WeakenedTests => self.weakened_tests,
646 IssueKind::AddedSuppression => self.added_suppression,
647 IssueKind::TestOnlyBranch => self.test_only_branch,
648 IssueKind::DivergedCloneSibling => self.diverged_clone_sibling,
649 IssueKind::AddedDependency => self.added_dependency,
650 IssueKind::Complexity | IssueKind::CodeDuplication => Severity::Error,
651 }
652 }
653
654 #[must_use]
661 pub fn complexity_severity(&self, cyclomatic: bool, cognitive: bool, crap: bool) -> Severity {
662 [
663 (cyclomatic, self.complexity_cyclomatic),
664 (cognitive, self.complexity_cognitive),
665 (crap, self.complexity_crap),
666 ]
667 .into_iter()
668 .filter_map(|(contributed, severity)| contributed.then_some(severity))
669 .max_by_key(|severity| match severity {
670 Severity::Error => 2,
671 Severity::Warn => 1,
672 Severity::Off => 0,
673 })
674 .unwrap_or(Severity::Off)
675 }
676
677 pub const fn apply_partial(&mut self, partial: &PartialRulesConfig) {
679 apply_partial_rules!(
680 self,
681 partial,
682 [
683 unused_files,
684 unused_exports,
685 unused_types,
686 private_type_leaks,
687 deprecated_exports_in_use,
688 unused_dependencies,
689 unused_dev_dependencies,
690 unused_optional_dependencies,
691 ]
692 );
693 apply_partial_rules!(
694 self,
695 partial,
696 [
697 unused_enum_members,
698 unused_class_members,
699 unused_store_members,
700 unprovided_injects,
701 unrendered_components,
702 unused_component_props,
703 absent_component_props,
704 unused_component_emits,
705 unused_component_inputs,
706 unused_component_outputs,
707 unused_svelte_events,
708 unused_effect_layers,
709 unused_tagged_errors,
710 unproduced_catch_tags,
711 unused_server_actions,
712 unused_load_data_keys,
713 prop_drilling,
714 thin_wrapper,
715 duplicate_prop_shape,
716 css_token_drift,
717 css_duplicate_block,
718 css_selector_complexity,
719 css_dead_surface,
720 css_broken_reference,
721 complexity_cyclomatic,
722 complexity_cognitive,
723 complexity_crap,
724 ]
725 );
726 apply_partial_rules!(
727 self,
728 partial,
729 [
730 unresolved_imports,
731 unlisted_dependencies,
732 duplicate_exports,
733 type_only_dependencies,
734 test_only_dependencies,
735 dev_dependencies_in_production,
736 circular_dependencies,
737 re_export_cycle,
738 package_cycle,
739 boundary_violation,
740 ]
741 );
742 apply_partial_rules!(
743 self,
744 partial,
745 [
746 coverage_gaps,
747 feature_flags,
748 stale_suppressions,
749 require_suppression_reason,
750 unused_catalog_entries,
751 empty_catalog_groups,
752 unresolved_catalog_references,
753 unused_dependency_overrides,
754 misconfigured_dependency_overrides,
755 ]
756 );
757 apply_partial_rules!(
758 self,
759 partial,
760 [
761 security_client_server_leak,
762 security_sink,
763 policy_violation,
764 invalid_client_export,
765 mixed_client_server_barrel,
766 misplaced_directive,
767 route_collision,
768 dynamic_segment_name_conflict,
769 weakened_tests,
770 added_suppression,
771 test_only_branch,
772 diverged_clone_sibling,
773 added_dependency,
774 ]
775 );
776 }
777}
778
779#[must_use]
787pub fn default_severity_for_kind(kind: IssueKind) -> Severity {
788 RulesConfig::default().severity_for_kind(kind)
789}
790
791#[must_use]
798pub fn is_opt_in_kind(kind: IssueKind) -> bool {
799 matches!(default_severity_for_kind(kind), Severity::Off)
800}
801
802#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
804#[serde(rename_all = "kebab-case")]
805pub struct PartialRulesConfig {
806 #[serde(
808 default,
809 alias = "unused-file",
810 skip_serializing_if = "Option::is_none"
811 )]
812 pub unused_files: Option<Severity>,
813 #[serde(
815 default,
816 alias = "unused-export",
817 skip_serializing_if = "Option::is_none"
818 )]
819 pub unused_exports: Option<Severity>,
820 #[serde(
822 default,
823 alias = "unused-type",
824 skip_serializing_if = "Option::is_none"
825 )]
826 pub unused_types: Option<Severity>,
827 #[serde(
829 default,
830 alias = "private-type-leak",
831 skip_serializing_if = "Option::is_none"
832 )]
833 pub private_type_leaks: Option<Severity>,
834 #[serde(
836 default,
837 alias = "deprecated-export-in-use",
838 skip_serializing_if = "Option::is_none"
839 )]
840 pub deprecated_exports_in_use: Option<Severity>,
841 #[serde(
843 default,
844 alias = "unused-dependency",
845 skip_serializing_if = "Option::is_none"
846 )]
847 pub unused_dependencies: Option<Severity>,
848 #[serde(
850 default,
851 alias = "unused-dev-dependency",
852 skip_serializing_if = "Option::is_none"
853 )]
854 pub unused_dev_dependencies: Option<Severity>,
855 #[serde(
857 default,
858 alias = "unused-optional-dependency",
859 skip_serializing_if = "Option::is_none"
860 )]
861 pub unused_optional_dependencies: Option<Severity>,
862 #[serde(
864 default,
865 alias = "unused-enum-member",
866 skip_serializing_if = "Option::is_none"
867 )]
868 pub unused_enum_members: Option<Severity>,
869 #[serde(
871 default,
872 alias = "unused-class-member",
873 skip_serializing_if = "Option::is_none"
874 )]
875 pub unused_class_members: Option<Severity>,
876 #[serde(
878 default,
879 alias = "unused-store-member",
880 skip_serializing_if = "Option::is_none"
881 )]
882 pub unused_store_members: Option<Severity>,
883 #[serde(
885 default,
886 alias = "unprovided-inject",
887 skip_serializing_if = "Option::is_none"
888 )]
889 pub unprovided_injects: Option<Severity>,
890 #[serde(
892 default,
893 alias = "unrendered-component",
894 skip_serializing_if = "Option::is_none"
895 )]
896 pub unrendered_components: Option<Severity>,
897 #[serde(
899 default,
900 alias = "unused-component-prop",
901 skip_serializing_if = "Option::is_none"
902 )]
903 pub unused_component_props: Option<Severity>,
904 #[serde(
906 default,
907 alias = "absent-component-prop",
908 skip_serializing_if = "Option::is_none"
909 )]
910 pub absent_component_props: Option<Severity>,
911 #[serde(
913 default,
914 alias = "unused-component-emit",
915 skip_serializing_if = "Option::is_none"
916 )]
917 pub unused_component_emits: Option<Severity>,
918 #[serde(
920 default,
921 alias = "unused-component-input",
922 skip_serializing_if = "Option::is_none"
923 )]
924 pub unused_component_inputs: Option<Severity>,
925 #[serde(
927 default,
928 alias = "unused-component-output",
929 skip_serializing_if = "Option::is_none"
930 )]
931 pub unused_component_outputs: Option<Severity>,
932 #[serde(
934 default,
935 alias = "unused-svelte-event",
936 skip_serializing_if = "Option::is_none"
937 )]
938 pub unused_svelte_events: Option<Severity>,
939 #[serde(
941 default,
942 alias = "unused-effect-layer",
943 skip_serializing_if = "Option::is_none"
944 )]
945 pub unused_effect_layers: Option<Severity>,
946 #[serde(
948 default,
949 alias = "unused-tagged-error",
950 skip_serializing_if = "Option::is_none"
951 )]
952 pub unused_tagged_errors: Option<Severity>,
953 #[serde(
955 default,
956 alias = "unproduced-catch-tag",
957 skip_serializing_if = "Option::is_none"
958 )]
959 pub unproduced_catch_tags: Option<Severity>,
960 #[serde(
962 default,
963 alias = "unused-server-action",
964 skip_serializing_if = "Option::is_none"
965 )]
966 pub unused_server_actions: Option<Severity>,
967 #[serde(
969 default,
970 alias = "unused-load-data-key",
971 skip_serializing_if = "Option::is_none"
972 )]
973 pub unused_load_data_keys: Option<Severity>,
974 #[serde(
976 default,
977 alias = "prop-drilling",
978 skip_serializing_if = "Option::is_none"
979 )]
980 pub prop_drilling: Option<Severity>,
981 #[serde(
983 default,
984 alias = "thin-wrapper",
985 skip_serializing_if = "Option::is_none"
986 )]
987 pub thin_wrapper: Option<Severity>,
988 #[serde(
990 default,
991 alias = "duplicate-prop-shape",
992 skip_serializing_if = "Option::is_none"
993 )]
994 pub duplicate_prop_shape: Option<Severity>,
995 #[serde(
997 default,
998 alias = "css-token-drift",
999 skip_serializing_if = "Option::is_none"
1000 )]
1001 pub css_token_drift: Option<Severity>,
1002 #[serde(
1004 default,
1005 alias = "css-duplicate-block",
1006 skip_serializing_if = "Option::is_none"
1007 )]
1008 pub css_duplicate_block: Option<Severity>,
1009 #[serde(
1011 default,
1012 alias = "css-selector-complexity",
1013 skip_serializing_if = "Option::is_none"
1014 )]
1015 pub css_selector_complexity: Option<Severity>,
1016 #[serde(
1018 default,
1019 alias = "css-dead-surface",
1020 skip_serializing_if = "Option::is_none"
1021 )]
1022 pub css_dead_surface: Option<Severity>,
1023 #[serde(
1025 default,
1026 alias = "css-broken-reference",
1027 skip_serializing_if = "Option::is_none"
1028 )]
1029 pub css_broken_reference: Option<Severity>,
1030 #[serde(default, skip_serializing_if = "Option::is_none")]
1032 pub complexity_cyclomatic: Option<Severity>,
1033 #[serde(default, skip_serializing_if = "Option::is_none")]
1035 pub complexity_cognitive: Option<Severity>,
1036 #[serde(default, skip_serializing_if = "Option::is_none")]
1038 pub complexity_crap: Option<Severity>,
1039 #[serde(
1041 default,
1042 alias = "unresolved-import",
1043 skip_serializing_if = "Option::is_none"
1044 )]
1045 pub unresolved_imports: Option<Severity>,
1046 #[serde(
1048 default,
1049 alias = "unlisted-dependency",
1050 skip_serializing_if = "Option::is_none"
1051 )]
1052 pub unlisted_dependencies: Option<Severity>,
1053 #[serde(
1055 default,
1056 alias = "duplicate-export",
1057 skip_serializing_if = "Option::is_none"
1058 )]
1059 pub duplicate_exports: Option<Severity>,
1060 #[serde(
1062 default,
1063 alias = "type-only-dependency",
1064 skip_serializing_if = "Option::is_none"
1065 )]
1066 pub type_only_dependencies: Option<Severity>,
1067 #[serde(
1069 default,
1070 alias = "test-only-dependency",
1071 skip_serializing_if = "Option::is_none"
1072 )]
1073 pub test_only_dependencies: Option<Severity>,
1074 #[serde(
1076 default,
1077 alias = "dev-dependency-in-production",
1078 skip_serializing_if = "Option::is_none"
1079 )]
1080 pub dev_dependencies_in_production: Option<Severity>,
1081 #[serde(
1083 default,
1084 alias = "circular-dependency",
1085 skip_serializing_if = "Option::is_none"
1086 )]
1087 pub circular_dependencies: Option<Severity>,
1088 #[serde(
1090 default,
1091 alias = "re-export-cycles",
1092 alias = "reexport-cycle",
1093 alias = "reexport-cycles",
1094 skip_serializing_if = "Option::is_none"
1095 )]
1096 pub re_export_cycle: Option<Severity>,
1097 #[serde(
1099 default,
1100 alias = "package-cycles",
1101 skip_serializing_if = "Option::is_none"
1102 )]
1103 pub package_cycle: Option<Severity>,
1104 #[serde(
1106 default,
1107 alias = "boundary-violations",
1108 skip_serializing_if = "Option::is_none"
1109 )]
1110 pub boundary_violation: Option<Severity>,
1111 #[serde(
1113 default,
1114 alias = "coverage-gap",
1115 skip_serializing_if = "Option::is_none"
1116 )]
1117 pub coverage_gaps: Option<Severity>,
1118 #[serde(
1120 default,
1121 alias = "feature-flag",
1122 skip_serializing_if = "Option::is_none"
1123 )]
1124 pub feature_flags: Option<Severity>,
1125 #[serde(
1127 default,
1128 alias = "stale-suppression",
1129 skip_serializing_if = "Option::is_none"
1130 )]
1131 pub stale_suppressions: Option<Severity>,
1132 #[serde(
1134 default,
1135 alias = "suppression-reason",
1136 skip_serializing_if = "Option::is_none"
1137 )]
1138 pub require_suppression_reason: Option<Severity>,
1139 #[serde(
1141 default,
1142 alias = "unused-catalog-entry",
1143 skip_serializing_if = "Option::is_none"
1144 )]
1145 pub unused_catalog_entries: Option<Severity>,
1146 #[serde(
1148 default,
1149 alias = "empty-catalog-group",
1150 skip_serializing_if = "Option::is_none"
1151 )]
1152 pub empty_catalog_groups: Option<Severity>,
1153 #[serde(
1155 default,
1156 alias = "unresolved-catalog-reference",
1157 skip_serializing_if = "Option::is_none"
1158 )]
1159 pub unresolved_catalog_references: Option<Severity>,
1160 #[serde(
1162 default,
1163 alias = "unused-dependency-override",
1164 skip_serializing_if = "Option::is_none"
1165 )]
1166 pub unused_dependency_overrides: Option<Severity>,
1167 #[serde(
1169 default,
1170 alias = "misconfigured-dependency-override",
1171 skip_serializing_if = "Option::is_none"
1172 )]
1173 pub misconfigured_dependency_overrides: Option<Severity>,
1174 #[serde(default, skip_serializing_if = "Option::is_none")]
1176 pub security_client_server_leak: Option<Severity>,
1177 #[serde(default, skip_serializing_if = "Option::is_none")]
1179 pub security_sink: Option<Severity>,
1180 #[serde(
1182 default,
1183 alias = "policy-violations",
1184 skip_serializing_if = "Option::is_none"
1185 )]
1186 pub policy_violation: Option<Severity>,
1187 #[serde(
1189 default,
1190 alias = "invalid-client-exports",
1191 skip_serializing_if = "Option::is_none"
1192 )]
1193 pub invalid_client_export: Option<Severity>,
1194 #[serde(
1196 default,
1197 alias = "mixed-client-server-barrels",
1198 skip_serializing_if = "Option::is_none"
1199 )]
1200 pub mixed_client_server_barrel: Option<Severity>,
1201 #[serde(
1203 default,
1204 alias = "misplaced-directives",
1205 skip_serializing_if = "Option::is_none"
1206 )]
1207 pub misplaced_directive: Option<Severity>,
1208 #[serde(
1210 default,
1211 alias = "route-collisions",
1212 skip_serializing_if = "Option::is_none"
1213 )]
1214 pub route_collision: Option<Severity>,
1215 #[serde(
1217 default,
1218 alias = "dynamic-segment-name-conflicts",
1219 skip_serializing_if = "Option::is_none"
1220 )]
1221 pub dynamic_segment_name_conflict: Option<Severity>,
1222 #[serde(default, skip_serializing_if = "Option::is_none")]
1224 pub weakened_tests: Option<Severity>,
1225 #[serde(
1227 default,
1228 alias = "added-suppressions",
1229 skip_serializing_if = "Option::is_none"
1230 )]
1231 pub added_suppression: Option<Severity>,
1232 #[serde(
1234 default,
1235 alias = "test-only-branches",
1236 skip_serializing_if = "Option::is_none"
1237 )]
1238 pub test_only_branch: Option<Severity>,
1239 #[serde(
1241 default,
1242 alias = "diverged-clone-siblings",
1243 skip_serializing_if = "Option::is_none"
1244 )]
1245 pub diverged_clone_sibling: Option<Severity>,
1246 #[serde(
1248 default,
1249 alias = "added-dependencies",
1250 skip_serializing_if = "Option::is_none"
1251 )]
1252 pub added_dependency: Option<Severity>,
1253}
1254
1255pub const KNOWN_RULE_NAMES: &[&str] = &[
1270 "unused-files",
1271 "unused-exports",
1272 "unused-types",
1273 "private-type-leaks",
1274 "deprecated-exports-in-use",
1275 "unused-dependencies",
1276 "unused-dev-dependencies",
1277 "unused-optional-dependencies",
1278 "unused-enum-members",
1279 "unused-class-members",
1280 "unused-store-members",
1281 "unprovided-injects",
1282 "unrendered-components",
1283 "unused-component-props",
1284 "absent-component-props",
1285 "unused-component-emits",
1286 "unused-component-inputs",
1287 "unused-component-outputs",
1288 "unused-svelte-events",
1289 "unused-effect-layers",
1290 "unused-tagged-errors",
1291 "unproduced-catch-tags",
1292 "unused-server-actions",
1293 "unused-load-data-keys",
1294 "prop-drilling",
1295 "thin-wrapper",
1296 "duplicate-prop-shape",
1297 "css-token-drift",
1298 "css-duplicate-block",
1299 "css-selector-complexity",
1300 "css-dead-surface",
1301 "css-broken-reference",
1302 "complexity-cyclomatic",
1303 "complexity-cognitive",
1304 "complexity-crap",
1305 "unresolved-imports",
1306 "unlisted-dependencies",
1307 "duplicate-exports",
1308 "type-only-dependencies",
1309 "test-only-dependencies",
1310 "dev-dependencies-in-production",
1311 "circular-dependencies",
1312 "re-export-cycle",
1313 "package-cycle",
1314 "boundary-violation",
1315 "coverage-gaps",
1316 "feature-flags",
1317 "stale-suppressions",
1318 "require-suppression-reason",
1319 "suppression-reason",
1320 "unused-catalog-entries",
1321 "empty-catalog-groups",
1322 "unresolved-catalog-references",
1323 "unused-dependency-overrides",
1324 "misconfigured-dependency-overrides",
1325 "security-client-server-leak",
1326 "security-sink",
1327 "policy-violation",
1328 "policy-violations",
1329 "invalid-client-export",
1330 "mixed-client-server-barrel",
1331 "misplaced-directive",
1332 "route-collision",
1333 "dynamic-segment-name-conflict",
1334 "unused-file",
1335 "unused-export",
1336 "unused-type",
1337 "private-type-leak",
1338 "deprecated-export-in-use",
1339 "unused-dependency",
1340 "unused-dev-dependency",
1341 "unused-optional-dependency",
1342 "unused-enum-member",
1343 "unused-class-member",
1344 "unused-store-member",
1345 "unprovided-inject",
1346 "unrendered-component",
1347 "unused-component-prop",
1348 "absent-component-prop",
1349 "unused-component-emit",
1350 "unused-component-input",
1351 "unused-component-output",
1352 "unused-svelte-event",
1353 "unused-effect-layer",
1354 "unused-tagged-error",
1355 "unproduced-catch-tag",
1356 "unused-server-action",
1357 "unused-load-data-key",
1358 "unresolved-import",
1359 "unlisted-dependency",
1360 "duplicate-export",
1361 "type-only-dependency",
1362 "test-only-dependency",
1363 "dev-dependency-in-production",
1364 "circular-dependency",
1365 "re-export-cycles",
1366 "reexport-cycle",
1367 "reexport-cycles",
1368 "package-cycles",
1369 "boundary-violations",
1370 "coverage-gap",
1371 "feature-flag",
1372 "stale-suppression",
1373 "unused-catalog-entry",
1374 "empty-catalog-group",
1375 "unresolved-catalog-reference",
1376 "unused-dependency-override",
1377 "misconfigured-dependency-override",
1378 "invalid-client-exports",
1379 "mixed-client-server-barrels",
1380 "misplaced-directives",
1381 "route-collisions",
1382 "dynamic-segment-name-conflicts",
1383 "weakened-tests",
1384 "added-suppression",
1385 "added-suppressions",
1386 "test-only-branch",
1387 "test-only-branches",
1388 "diverged-clone-sibling",
1389 "diverged-clone-siblings",
1390 "added-dependency",
1391 "added-dependencies",
1392];
1393
1394#[must_use]
1401pub fn closest_known_rule_name(input: &str) -> Option<&'static str> {
1402 crate::levenshtein::closest_match(input, KNOWN_RULE_NAMES.iter().copied())
1403}
1404
1405#[derive(Debug, Clone, PartialEq, Eq)]
1411pub struct UnknownRuleKey {
1412 pub context: String,
1414 pub key: String,
1416 pub suggestion: Option<&'static str>,
1418}
1419
1420#[must_use]
1428pub fn find_unknown_rule_keys(value: &serde_json::Value, context: &str) -> Vec<UnknownRuleKey> {
1429 let Some(map) = value.as_object() else {
1430 return Vec::new();
1431 };
1432
1433 map.keys()
1434 .filter(|key| !KNOWN_RULE_NAMES.contains(&key.as_str()))
1435 .map(|key| UnknownRuleKey {
1436 context: context.to_owned(),
1437 key: key.clone(),
1438 suggestion: closest_known_rule_name(key),
1439 })
1440 .collect()
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445 use super::*;
1446
1447 #[test]
1448 fn severity_for_kind_maps_configured_fields_and_default() {
1449 let rules = RulesConfig {
1450 unused_exports: Severity::Warn,
1451 unused_types: Severity::Off,
1452 unresolved_imports: Severity::Warn,
1453 boundary_violation: Severity::Off,
1454 ..RulesConfig::default()
1455 };
1456
1457 assert_eq!(
1458 rules.severity_for_kind(IssueKind::UnusedExport),
1459 Severity::Warn
1460 );
1461 assert_eq!(
1462 rules.severity_for_kind(IssueKind::UnusedType),
1463 Severity::Off
1464 );
1465 assert_eq!(
1466 rules.severity_for_kind(IssueKind::UnresolvedImport),
1467 Severity::Warn
1468 );
1469 assert_eq!(
1470 rules.severity_for_kind(IssueKind::BoundaryViolation),
1471 Severity::Off
1472 );
1473 assert_eq!(
1474 rules.severity_for_kind(IssueKind::UnusedFile),
1475 Severity::Error
1476 );
1477 assert_eq!(
1478 rules.severity_for_kind(IssueKind::PrivateTypeLeak),
1479 Severity::Off
1480 );
1481 assert_eq!(
1482 rules.severity_for_kind(IssueKind::Complexity),
1483 Severity::Error
1484 );
1485 assert_eq!(
1486 rules.severity_for_kind(IssueKind::CodeDuplication),
1487 Severity::Error
1488 );
1489 }
1490
1491 #[test]
1492 fn rules_default_severities() {
1493 let rules = RulesConfig::default();
1494 assert_eq!(rules.unused_files, Severity::Error);
1495 assert_eq!(rules.unused_exports, Severity::Error);
1496 assert_eq!(rules.unused_types, Severity::Error);
1497 assert_eq!(rules.private_type_leaks, Severity::Off);
1498 assert_eq!(rules.unused_dependencies, Severity::Error);
1499 assert_eq!(rules.unused_dev_dependencies, Severity::Warn);
1500 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
1501 assert_eq!(rules.unused_enum_members, Severity::Error);
1502 assert_eq!(rules.unused_class_members, Severity::Error);
1503 assert_eq!(rules.unresolved_imports, Severity::Error);
1504 assert_eq!(rules.unlisted_dependencies, Severity::Error);
1505 assert_eq!(rules.duplicate_exports, Severity::Error);
1506 assert_eq!(rules.type_only_dependencies, Severity::Warn);
1507 assert_eq!(rules.test_only_dependencies, Severity::Warn);
1508 assert_eq!(rules.circular_dependencies, Severity::Error);
1509 assert_eq!(rules.boundary_violation, Severity::Error);
1510 assert_eq!(rules.coverage_gaps, Severity::Off);
1511 assert_eq!(rules.feature_flags, Severity::Off);
1512 assert_eq!(rules.stale_suppressions, Severity::Warn);
1513 assert_eq!(rules.unused_catalog_entries, Severity::Warn);
1514 assert_eq!(rules.empty_catalog_groups, Severity::Warn);
1515 assert_eq!(rules.unresolved_catalog_references, Severity::Error);
1516 }
1517
1518 #[test]
1519 fn rules_deserialize_kebab_case() {
1520 let json_str = r#"{
1521 "unused-files": "error",
1522 "unused-exports": "warn",
1523 "unused-types": "off"
1524 }"#;
1525 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1526 assert_eq!(rules.unused_files, Severity::Error);
1527 assert_eq!(rules.unused_exports, Severity::Warn);
1528 assert_eq!(rules.unused_types, Severity::Off);
1529 assert_eq!(rules.unresolved_imports, Severity::Error);
1530 }
1531
1532 #[test]
1533 fn empty_rules_object_matches_default_impl() {
1534 let from_empty: RulesConfig = serde_json::from_str("{}").unwrap();
1542 assert_eq!(
1543 from_empty,
1544 RulesConfig::default(),
1545 "empty `rules` object must equal RulesConfig::default(); a field whose \
1546 Default is Warn/Off needs an explicit #[serde(default = ...)]"
1547 );
1548 }
1549
1550 #[test]
1551 fn empty_rules_object_preserves_non_error_defaults() {
1552 let rules: RulesConfig = serde_json::from_str("{}").unwrap();
1555 assert_eq!(rules.coverage_gaps, Severity::Off);
1556 assert_eq!(rules.unused_store_members, Severity::Warn);
1557 assert_eq!(rules.unprovided_injects, Severity::Warn);
1558 assert_eq!(rules.unrendered_components, Severity::Warn);
1559 assert_eq!(rules.unused_component_props, Severity::Warn);
1560 assert_eq!(rules.unused_component_emits, Severity::Warn);
1561 assert_eq!(rules.unused_component_inputs, Severity::Warn);
1562 assert_eq!(rules.unused_component_outputs, Severity::Warn);
1563 assert_eq!(rules.unused_server_actions, Severity::Warn);
1564 }
1565
1566 #[test]
1567 fn explicit_rule_severity_still_overrides_default() {
1568 let rules: RulesConfig =
1571 serde_json::from_str(r#"{ "coverage-gaps": "error", "unused-component-prop": "off" }"#)
1572 .unwrap();
1573 assert_eq!(rules.coverage_gaps, Severity::Error);
1574 assert_eq!(rules.unused_component_props, Severity::Off);
1575 }
1576
1577 #[test]
1578 fn rules_re_export_cycle_default_is_warn() {
1579 let rules = RulesConfig::default();
1580 assert_eq!(rules.re_export_cycle, Severity::Warn);
1581 }
1582
1583 #[test]
1584 fn rules_package_cycle_default_is_warn() {
1585 let rules = RulesConfig::default();
1586 assert_eq!(rules.package_cycle, Severity::Warn);
1587 }
1588
1589 #[test]
1590 fn package_cycle_aliases_round_trip_to_the_same_field() {
1591 for alias in ["package-cycle", "package-cycles"] {
1592 let json = format!(r#"{{"{alias}": "error"}}"#);
1593 let partial: PartialRulesConfig = serde_json::from_str(&json)
1594 .unwrap_or_else(|e| panic!("'{alias}' should deserialize: {e}"));
1595 assert_eq!(partial.package_cycle, Some(Severity::Error), "{alias}");
1596 let rules: RulesConfig = serde_json::from_str(&json)
1597 .unwrap_or_else(|e| panic!("'{alias}' should deserialize: {e}"));
1598 assert_eq!(rules.package_cycle, Severity::Error, "{alias}");
1599 }
1600 }
1601
1602 #[test]
1603 fn rules_deserialize_re_export_cycle_aliases() {
1604 for token in [
1605 "re-export-cycle",
1606 "re-export-cycles",
1607 "reexport-cycle",
1608 "reexport-cycles",
1609 ] {
1610 let json_str = format!(r#"{{ "{token}": "error" }}"#);
1611 let rules: RulesConfig = serde_json::from_str(&json_str)
1612 .unwrap_or_else(|e| panic!("alias {token} did not deserialize: {e}"));
1613 assert_eq!(
1614 rules.re_export_cycle,
1615 Severity::Error,
1616 "alias {token} should set re_export_cycle"
1617 );
1618 }
1619 }
1620
1621 #[test]
1622 fn rules_deserialize_circular_dependency_alias() {
1623 let json_str = r#"{
1624 "circular-dependency": "off"
1625 }"#;
1626 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1627 assert_eq!(rules.circular_dependencies, Severity::Off);
1628 }
1629
1630 #[test]
1631 fn rules_deserialize_boundary_violations_alias() {
1632 let json_str = r#"{
1633 "boundary-violations": "off"
1634 }"#;
1635 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1636 assert_eq!(rules.boundary_violation, Severity::Off);
1637
1638 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
1639 assert_eq!(partial.boundary_violation, Some(Severity::Off));
1640 }
1641
1642 #[test]
1643 fn rules_deserialize_singular_aliases_for_every_plural_rule() {
1644 let json_str = r#"{
1645 "unused-file": "off",
1646 "unused-export": "off",
1647 "unused-type": "off",
1648 "private-type-leak": "warn",
1649 "unused-dependency": "off",
1650 "unused-dev-dependency": "off",
1651 "unused-optional-dependency": "off",
1652 "unused-enum-member": "off",
1653 "unused-class-member": "off",
1654 "unresolved-import": "off",
1655 "unlisted-dependency": "off",
1656 "duplicate-export": "off",
1657 "type-only-dependency": "off",
1658 "test-only-dependency": "off",
1659 "coverage-gap": "warn",
1660 "feature-flag": "warn",
1661 "stale-suppression": "off",
1662 "suppression-reason": "warn",
1663 "unused-catalog-entry": "error",
1664 "empty-catalog-group": "error",
1665 "unresolved-catalog-reference": "warn"
1666 }"#;
1667
1668 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1669 assert_eq!(rules.unused_files, Severity::Off);
1670 assert_eq!(rules.unused_exports, Severity::Off);
1671 assert_eq!(rules.unused_types, Severity::Off);
1672 assert_eq!(rules.private_type_leaks, Severity::Warn);
1673 assert_eq!(rules.unused_dependencies, Severity::Off);
1674 assert_eq!(rules.unused_dev_dependencies, Severity::Off);
1675 assert_eq!(rules.unused_optional_dependencies, Severity::Off);
1676 assert_eq!(rules.unused_enum_members, Severity::Off);
1677 assert_eq!(rules.unused_class_members, Severity::Off);
1678 assert_eq!(rules.unresolved_imports, Severity::Off);
1679 assert_eq!(rules.unlisted_dependencies, Severity::Off);
1680 assert_eq!(rules.duplicate_exports, Severity::Off);
1681 assert_eq!(rules.type_only_dependencies, Severity::Off);
1682 assert_eq!(rules.test_only_dependencies, Severity::Off);
1683 assert_eq!(rules.coverage_gaps, Severity::Warn);
1684 assert_eq!(rules.feature_flags, Severity::Warn);
1685 assert_eq!(rules.stale_suppressions, Severity::Off);
1686 assert_eq!(rules.require_suppression_reason, Severity::Warn);
1687 assert_eq!(rules.unused_catalog_entries, Severity::Error);
1688 assert_eq!(rules.empty_catalog_groups, Severity::Error);
1689 assert_eq!(rules.unresolved_catalog_references, Severity::Warn);
1690
1691 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
1692 assert_eq!(partial.unused_files, Some(Severity::Off));
1693 assert_eq!(partial.unused_exports, Some(Severity::Off));
1694 assert_eq!(partial.unused_types, Some(Severity::Off));
1695 assert_eq!(partial.private_type_leaks, Some(Severity::Warn));
1696 assert_eq!(partial.unused_dependencies, Some(Severity::Off));
1697 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Off));
1698 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
1699 assert_eq!(partial.unused_enum_members, Some(Severity::Off));
1700 assert_eq!(partial.unused_class_members, Some(Severity::Off));
1701 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
1702 assert_eq!(partial.unlisted_dependencies, Some(Severity::Off));
1703 assert_eq!(partial.duplicate_exports, Some(Severity::Off));
1704 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
1705 assert_eq!(partial.test_only_dependencies, Some(Severity::Off));
1706 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
1707 assert_eq!(partial.feature_flags, Some(Severity::Warn));
1708 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
1709 assert_eq!(partial.require_suppression_reason, Some(Severity::Warn));
1710 assert_eq!(partial.unused_catalog_entries, Some(Severity::Error));
1711 assert_eq!(partial.empty_catalog_groups, Some(Severity::Error));
1712 assert_eq!(partial.unresolved_catalog_references, Some(Severity::Warn));
1713 }
1714
1715 #[test]
1716 fn severity_from_str() {
1717 assert_eq!("error".parse::<Severity>().unwrap(), Severity::Error);
1718 assert_eq!("warn".parse::<Severity>().unwrap(), Severity::Warn);
1719 assert_eq!("warning".parse::<Severity>().unwrap(), Severity::Warn);
1720 assert_eq!("off".parse::<Severity>().unwrap(), Severity::Off);
1721 assert_eq!("none".parse::<Severity>().unwrap(), Severity::Off);
1722 assert!("invalid".parse::<Severity>().is_err());
1723 }
1724
1725 #[test]
1726 fn apply_partial_only_some_fields() {
1727 let mut rules = RulesConfig::default();
1728 let partial = PartialRulesConfig {
1729 unused_files: Some(Severity::Warn),
1730 unused_exports: Some(Severity::Off),
1731 ..Default::default()
1732 };
1733 rules.apply_partial(&partial);
1734 assert_eq!(rules.unused_files, Severity::Warn);
1735 assert_eq!(rules.unused_exports, Severity::Off);
1736 assert_eq!(rules.unused_types, Severity::Error);
1737 assert_eq!(rules.unresolved_imports, Severity::Error);
1738 assert_eq!(rules.require_suppression_reason, Severity::Off);
1739 }
1740
1741 #[test]
1742 fn require_suppression_reason_deserializes_canonical_and_alias() {
1743 let rules: RulesConfig = serde_json::from_str(
1744 r#"{
1745 "require-suppression-reason": "error"
1746 }"#,
1747 )
1748 .unwrap();
1749 assert_eq!(rules.require_suppression_reason, Severity::Error);
1750
1751 let partial: PartialRulesConfig = serde_json::from_str(
1752 r#"{
1753 "suppression-reason": "warn"
1754 }"#,
1755 )
1756 .unwrap();
1757 assert_eq!(partial.require_suppression_reason, Some(Severity::Warn));
1758 }
1759
1760 #[test]
1761 fn complexity_rules_default_to_error_and_deserialize() {
1762 let rules = RulesConfig::default();
1763 assert_eq!(rules.complexity_cyclomatic, Severity::Error);
1764 assert_eq!(rules.complexity_cognitive, Severity::Error);
1765 assert_eq!(rules.complexity_crap, Severity::Error);
1766
1767 let partial: PartialRulesConfig =
1768 serde_json::from_str(r#"{ "complexity-crap": "warn", "complexity-cognitive": "off" }"#)
1769 .unwrap();
1770 assert_eq!(partial.complexity_crap, Some(Severity::Warn));
1771 assert_eq!(partial.complexity_cognitive, Some(Severity::Off));
1772 assert_eq!(partial.complexity_cyclomatic, None);
1773 }
1774
1775 #[test]
1776 fn complexity_severity_takes_the_most_severe_contributing_rule() {
1777 let rules = RulesConfig {
1778 complexity_cyclomatic: Severity::Warn,
1779 complexity_cognitive: Severity::Off,
1780 complexity_crap: Severity::Error,
1781 ..RulesConfig::default()
1782 };
1783 assert_eq!(
1784 rules.complexity_severity(true, false, true),
1785 Severity::Error
1786 );
1787 assert_eq!(rules.complexity_severity(true, true, false), Severity::Warn);
1788 assert_eq!(rules.complexity_severity(false, true, false), Severity::Off);
1789 assert_eq!(
1790 rules.complexity_severity(true, false, false),
1791 Severity::Warn
1792 );
1793 assert_eq!(
1794 rules.complexity_severity(false, false, false),
1795 Severity::Off
1796 );
1797 }
1798
1799 #[test]
1800 fn severity_display() {
1801 assert_eq!(Severity::Error.to_string(), "error");
1802 assert_eq!(Severity::Warn.to_string(), "warn");
1803 assert_eq!(Severity::Off.to_string(), "off");
1804 }
1805
1806 #[test]
1807 fn apply_partial_all_none_changes_nothing() {
1808 let mut rules = RulesConfig::default();
1809 let original = rules.clone();
1810 let partial = PartialRulesConfig::default(); rules.apply_partial(&partial);
1812 assert_eq!(rules.unused_files, original.unused_files);
1813 assert_eq!(rules.unused_exports, original.unused_exports);
1814 assert_eq!(
1815 rules.type_only_dependencies,
1816 original.type_only_dependencies
1817 );
1818 }
1819
1820 #[test]
1821 fn apply_partial_all_fields_set() {
1822 let mut rules = RulesConfig::default();
1823 let partial = PartialRulesConfig {
1824 unused_files: Some(Severity::Off),
1825 unused_exports: Some(Severity::Off),
1826 unused_types: Some(Severity::Off),
1827 private_type_leaks: Some(Severity::Off),
1828 unused_dependencies: Some(Severity::Off),
1829 unused_dev_dependencies: Some(Severity::Off),
1830 unused_optional_dependencies: Some(Severity::Off),
1831 unused_enum_members: Some(Severity::Off),
1832 unused_class_members: Some(Severity::Off),
1833 unused_store_members: Some(Severity::Off),
1834 unprovided_injects: Some(Severity::Off),
1835 unrendered_components: Some(Severity::Off),
1836 unused_component_props: Some(Severity::Off),
1837 absent_component_props: Some(Severity::Off),
1838 unused_component_emits: Some(Severity::Off),
1839 unused_component_inputs: Some(Severity::Off),
1840 unused_component_outputs: Some(Severity::Off),
1841 unused_svelte_events: Some(Severity::Off),
1842 unused_effect_layers: Some(Severity::Off),
1843 unused_tagged_errors: Some(Severity::Off),
1844 unproduced_catch_tags: Some(Severity::Off),
1845 unused_server_actions: Some(Severity::Off),
1846 unused_load_data_keys: Some(Severity::Off),
1847 prop_drilling: Some(Severity::Off),
1848 thin_wrapper: Some(Severity::Off),
1849 duplicate_prop_shape: Some(Severity::Off),
1850 css_token_drift: Some(Severity::Off),
1851 css_duplicate_block: Some(Severity::Off),
1852 css_selector_complexity: Some(Severity::Off),
1853 css_dead_surface: Some(Severity::Off),
1854 css_broken_reference: Some(Severity::Off),
1855 complexity_cyclomatic: Some(Severity::Off),
1856 complexity_cognitive: Some(Severity::Off),
1857 complexity_crap: Some(Severity::Off),
1858 unresolved_imports: Some(Severity::Off),
1859 unlisted_dependencies: Some(Severity::Off),
1860 duplicate_exports: Some(Severity::Off),
1861 type_only_dependencies: Some(Severity::Off),
1862 test_only_dependencies: Some(Severity::Off),
1863 dev_dependencies_in_production: Some(Severity::Off),
1864 circular_dependencies: Some(Severity::Off),
1865 re_export_cycle: Some(Severity::Off),
1866 package_cycle: Some(Severity::Off),
1867 boundary_violation: Some(Severity::Off),
1868 coverage_gaps: Some(Severity::Off),
1869 feature_flags: Some(Severity::Off),
1870 stale_suppressions: Some(Severity::Off),
1871 require_suppression_reason: Some(Severity::Off),
1872 unused_catalog_entries: Some(Severity::Off),
1873 empty_catalog_groups: Some(Severity::Off),
1874 unresolved_catalog_references: Some(Severity::Off),
1875 unused_dependency_overrides: Some(Severity::Off),
1876 misconfigured_dependency_overrides: Some(Severity::Off),
1877 security_client_server_leak: Some(Severity::Off),
1878 security_sink: Some(Severity::Off),
1879 policy_violation: Some(Severity::Off),
1880 invalid_client_export: Some(Severity::Off),
1881 mixed_client_server_barrel: Some(Severity::Off),
1882 misplaced_directive: Some(Severity::Off),
1883 route_collision: Some(Severity::Off),
1884 dynamic_segment_name_conflict: Some(Severity::Off),
1885 deprecated_exports_in_use: Some(Severity::Off),
1886 weakened_tests: Some(Severity::Off),
1887 added_suppression: Some(Severity::Off),
1888 test_only_branch: Some(Severity::Off),
1889 diverged_clone_sibling: Some(Severity::Off),
1890 added_dependency: Some(Severity::Off),
1891 };
1892 rules.apply_partial(&partial);
1893 assert_eq!(rules.unused_files, Severity::Off);
1894 assert_eq!(rules.private_type_leaks, Severity::Off);
1895 assert_eq!(rules.circular_dependencies, Severity::Off);
1896 assert_eq!(rules.type_only_dependencies, Severity::Off);
1897 assert_eq!(rules.test_only_dependencies, Severity::Off);
1898 assert_eq!(rules.boundary_violation, Severity::Off);
1899 assert_eq!(rules.coverage_gaps, Severity::Off);
1900 assert_eq!(rules.feature_flags, Severity::Off);
1901 assert_eq!(rules.stale_suppressions, Severity::Off);
1902 assert_eq!(rules.require_suppression_reason, Severity::Off);
1903 assert_eq!(rules.security_sink, Severity::Off);
1904 assert_eq!(rules.policy_violation, Severity::Off);
1905 assert_eq!(rules.invalid_client_export, Severity::Off);
1906 assert_eq!(rules.mixed_client_server_barrel, Severity::Off);
1907 assert_eq!(rules.misplaced_directive, Severity::Off);
1908 assert_eq!(rules.unrendered_components, Severity::Off);
1909 assert_eq!(rules.unused_component_props, Severity::Off);
1910 assert_eq!(rules.unused_component_emits, Severity::Off);
1911 assert_eq!(rules.unused_component_inputs, Severity::Off);
1912 assert_eq!(rules.unused_component_outputs, Severity::Off);
1913 assert_eq!(rules.unused_svelte_events, Severity::Off);
1914 assert_eq!(rules.route_collision, Severity::Off);
1915 assert_eq!(rules.dynamic_segment_name_conflict, Severity::Off);
1916 }
1917
1918 #[test]
1919 fn diff_risk_rules_default_and_parse_from_kebab_keys() {
1920 let rules = RulesConfig::default();
1921 assert_eq!(rules.weakened_tests, Severity::Warn);
1922 assert_eq!(rules.added_suppression, Severity::Warn);
1923 assert_eq!(rules.test_only_branch, Severity::Warn);
1924 assert_eq!(rules.diverged_clone_sibling, Severity::Off);
1925 assert_eq!(rules.added_dependency, Severity::Off);
1926
1927 let json = r#"{"weakened-tests": "error", "added-dependency": "warn"}"#;
1928 let partial: PartialRulesConfig = serde_json::from_str(json).expect("partial rules");
1929 let mut merged = RulesConfig::default();
1930 merged.apply_partial(&partial);
1931 assert_eq!(merged.weakened_tests, Severity::Error);
1932 assert_eq!(merged.added_dependency, Severity::Warn);
1933 assert_eq!(merged.added_suppression, Severity::Warn);
1934 assert_eq!(
1935 merged.severity_for_kind(IssueKind::WeakenedTests),
1936 Severity::Error
1937 );
1938 assert_eq!(
1939 merged.severity_for_kind(IssueKind::AddedDependency),
1940 Severity::Warn
1941 );
1942 }
1943
1944 #[test]
1945 fn policy_violation_defaults_to_warn() {
1946 let rules = RulesConfig::default();
1947 assert_eq!(rules.policy_violation, Severity::Warn);
1948 }
1949
1950 #[test]
1951 fn policy_violation_accepts_plural_alias() {
1952 let json = r#"{ "policy-violations": "error" }"#;
1953 let rules: RulesConfig = serde_json::from_str(json).unwrap();
1954 assert_eq!(rules.policy_violation, Severity::Error);
1955 }
1956
1957 #[test]
1958 fn severity_from_str_case_insensitive() {
1959 assert_eq!("ERROR".parse::<Severity>().unwrap(), Severity::Error);
1960 assert_eq!("Warn".parse::<Severity>().unwrap(), Severity::Warn);
1961 assert_eq!("OFF".parse::<Severity>().unwrap(), Severity::Off);
1962 assert_eq!("Warning".parse::<Severity>().unwrap(), Severity::Warn);
1963 assert_eq!("NONE".parse::<Severity>().unwrap(), Severity::Off);
1964 }
1965
1966 #[test]
1967 fn severity_from_str_invalid_returns_error() {
1968 let result = "critical".parse::<Severity>();
1969 assert!(result.is_err());
1970 let err = result.unwrap_err();
1971 assert!(
1972 err.contains("unknown severity"),
1973 "Expected descriptive error, got: {err}"
1974 );
1975 }
1976
1977 #[test]
1982 fn known_rule_names_list_length_is_pinned() {
1983 assert_eq!(KNOWN_RULE_NAMES.len(), 122);
1984 }
1985
1986 #[test]
1992 fn known_rule_names_holds_no_unreachable_entry() {
1993 let serialized =
1994 serde_json::to_value(RulesConfig::default()).expect("RulesConfig serializes");
1995 let canonical = serialized
1996 .as_object()
1997 .expect("RulesConfig serializes to an object");
1998 let source = include_str!("rules.rs");
1999
2000 let unreachable: Vec<&&str> = KNOWN_RULE_NAMES
2001 .iter()
2002 .filter(|name| !canonical.contains_key(**name))
2003 .filter(|name| !source.contains(&format!("alias = \"{name}\"")))
2004 .collect();
2005
2006 assert!(
2007 unreachable.is_empty(),
2008 "KNOWN_RULE_NAMES entries that are neither a serialized rule nor a declared \
2009 serde alias: {unreachable:?}. closest_known_rule_name suggests from this list, \
2010 so each one can be offered to a user as a rule that does not exist."
2011 );
2012 }
2013
2014 #[test]
2015 fn known_rule_names_has_no_duplicates() {
2016 let mut sorted: Vec<&str> = KNOWN_RULE_NAMES.to_vec();
2017 sorted.sort_unstable();
2018 let original_len = sorted.len();
2019 sorted.dedup();
2020 assert_eq!(
2021 sorted.len(),
2022 original_len,
2023 "KNOWN_RULE_NAMES contains a duplicate"
2024 );
2025 }
2026
2027 #[test]
2028 fn known_rule_names_covers_every_serde_alias_in_source() {
2029 let source = include_str!("rules.rs");
2030
2031 let mut aliases_found = Vec::new();
2032 for line in source.lines() {
2033 let trimmed = line.trim();
2034 if trimmed.starts_with("//") {
2035 continue;
2036 }
2037 let Some(after) = trimmed.split("alias = \"").nth(1) else {
2038 continue;
2039 };
2040 let Some(end) = after.find('"') else {
2041 continue;
2042 };
2043 let alias = &after[..end];
2044 if alias.is_empty() || !alias.chars().all(|c| c.is_ascii_lowercase() || c == '-') {
2045 continue;
2046 }
2047 aliases_found.push(alias.to_owned());
2048 }
2049
2050 assert_eq!(
2051 aliases_found.len(),
2052 126,
2053 "expected 126 source-level alias attrs (63 per struct); got {}: {:?}",
2054 aliases_found.len(),
2055 aliases_found
2056 );
2057
2058 for alias in &aliases_found {
2059 assert!(
2060 KNOWN_RULE_NAMES.contains(&alias.as_str()),
2061 "serde alias '{alias}' is in rules.rs source but missing from KNOWN_RULE_NAMES"
2062 );
2063 }
2064 }
2065
2066 #[test]
2067 fn re_export_cycle_aliases_all_round_trip_to_the_same_field() {
2068 for alias in [
2069 "re-export-cycle",
2070 "re-export-cycles",
2071 "reexport-cycle",
2072 "reexport-cycles",
2073 ] {
2074 let json = format!(r#"{{"{alias}": "warn"}}"#);
2075 let partial: PartialRulesConfig = serde_json::from_str(&json)
2076 .unwrap_or_else(|e| panic!("'{alias}' should deserialize: {e}"));
2077 assert_eq!(
2078 partial.re_export_cycle,
2079 Some(Severity::Warn),
2080 "'{alias}' should set re_export_cycle to Warn"
2081 );
2082 let serialized = serde_json::to_value(&partial).unwrap();
2083 let map = serialized.as_object().unwrap();
2084 assert_eq!(
2085 map.len(),
2086 1,
2087 "'{alias}' should resolve to exactly one field, got: {map:?}"
2088 );
2089 }
2090 }
2091
2092 #[test]
2093 fn every_known_rule_name_round_trips_through_partial() {
2094 for &name in KNOWN_RULE_NAMES {
2095 let json = format!(r#"{{"{name}": "warn"}}"#);
2096 let partial: PartialRulesConfig = serde_json::from_str(&json)
2097 .unwrap_or_else(|e| panic!("'{name}' should deserialize: {e}"));
2098
2099 let serialized = serde_json::to_value(&partial).unwrap();
2100 let map = serialized.as_object().unwrap();
2101 assert_eq!(
2102 map.len(),
2103 1,
2104 "'{name}' should resolve to exactly one field, got: {map:?}"
2105 );
2106 }
2107 }
2108
2109 #[test]
2110 fn known_rule_names_covers_every_struct_field() {
2111 let json = serde_json::to_value(RulesConfig::default()).unwrap();
2112 let obj = json.as_object().unwrap();
2113 for key in obj.keys() {
2114 assert!(
2115 KNOWN_RULE_NAMES.contains(&key.as_str()),
2116 "field '{key}' is serialized but missing from KNOWN_RULE_NAMES"
2117 );
2118 }
2119 }
2120
2121 #[test]
2122 fn closest_known_rule_name_suggests_for_obvious_typo() {
2123 assert_eq!(
2124 closest_known_rule_name("unsued-files"),
2125 Some("unused-files")
2126 );
2127 assert_eq!(
2128 closest_known_rule_name("circular-dependnecy"),
2129 Some("circular-dependency")
2130 );
2131 assert_eq!(
2132 closest_known_rule_name("unused-dep"),
2133 None,
2134 "too short for a confident suggestion"
2135 );
2136 }
2137
2138 #[test]
2139 fn closest_known_rule_name_returns_none_for_novel_input() {
2140 assert_eq!(closest_known_rule_name("totally-fabricated"), None);
2141 assert_eq!(closest_known_rule_name("foo"), None);
2142 }
2143
2144 #[test]
2145 fn closest_known_rule_name_is_case_insensitive() {
2146 assert_eq!(
2147 closest_known_rule_name("UNSUED-FILES"),
2148 Some("unused-files")
2149 );
2150 }
2151
2152 #[test]
2153 fn closest_known_rule_name_returns_none_for_exact_match() {
2154 assert_eq!(closest_known_rule_name("unused-files"), None);
2155 }
2156
2157 #[test]
2158 fn find_unknown_rule_keys_flags_typo() {
2159 let v = serde_json::json!({
2160 "unsued-files": "warn",
2161 "unused-exports": "off",
2162 });
2163 let unknown = find_unknown_rule_keys(&v, "rules");
2164 assert_eq!(unknown.len(), 1);
2165 assert_eq!(unknown[0].key, "unsued-files");
2166 assert_eq!(unknown[0].context, "rules");
2167 assert_eq!(unknown[0].suggestion, Some("unused-files"));
2168 }
2169
2170 #[test]
2171 fn find_unknown_rule_keys_passes_aliases() {
2172 let v = serde_json::json!({
2173 "unused-file": "warn",
2174 "circular-dependency": "off",
2175 "boundary-violations": "warn",
2176 });
2177 let unknown = find_unknown_rule_keys(&v, "rules");
2178 assert!(
2179 unknown.is_empty(),
2180 "documented aliases must not flag as unknown: {unknown:?}"
2181 );
2182 }
2183
2184 #[test]
2185 fn find_unknown_rule_keys_returns_multiple_typos() {
2186 let v = serde_json::json!({
2187 "unsued-files": "warn",
2188 "circular-dependnecy": "off",
2189 });
2190 let unknown = find_unknown_rule_keys(&v, "rules");
2191 assert_eq!(unknown.len(), 2);
2192 }
2193
2194 #[test]
2195 fn find_unknown_rule_keys_carries_context() {
2196 let v = serde_json::json!({ "unsued-files": "warn" });
2197 let unknown = find_unknown_rule_keys(&v, "overrides[2].rules");
2198 assert_eq!(unknown[0].context, "overrides[2].rules");
2199 }
2200
2201 #[test]
2202 fn find_unknown_rule_keys_empty_when_not_object() {
2203 let v = serde_json::json!(null);
2204 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
2205
2206 let v = serde_json::json!([1, 2, 3]);
2207 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
2208 }
2209
2210 #[test]
2211 fn find_unknown_rule_keys_no_suggestion_for_novel_name() {
2212 let v = serde_json::json!({ "totally-fabricated-rule": "warn" });
2213 let unknown = find_unknown_rule_keys(&v, "rules");
2214 assert_eq!(unknown.len(), 1);
2215 assert_eq!(unknown[0].suggestion, None);
2216 }
2217
2218 #[test]
2219 fn partial_rules_empty_json() {
2220 let partial: PartialRulesConfig = serde_json::from_str("{}").unwrap();
2221 assert!(partial.unused_files.is_none());
2222 assert!(partial.unused_exports.is_none());
2223 assert!(partial.unused_types.is_none());
2224 assert!(partial.unused_dependencies.is_none());
2225 assert!(partial.circular_dependencies.is_none());
2226 assert!(partial.boundary_violation.is_none());
2227 assert!(partial.coverage_gaps.is_none());
2228 assert!(partial.feature_flags.is_none());
2229 assert!(partial.stale_suppressions.is_none());
2230 }
2231
2232 #[test]
2233 fn partial_rules_subset_json() {
2234 let json = r#"{
2235 "unused-files": "warn",
2236 "circular-dependencies": "off"
2237 }"#;
2238 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
2239 assert_eq!(partial.unused_files, Some(Severity::Warn));
2240 assert_eq!(partial.circular_dependencies, Some(Severity::Off));
2241 assert!(partial.unused_exports.is_none());
2242 }
2243
2244 #[test]
2245 fn partial_rules_deserialize_circular_dependency_alias() {
2246 let json = r#"{
2247 "circular-dependency": "warn"
2248 }"#;
2249 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
2250 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
2251 }
2252
2253 #[test]
2254 fn partial_rules_all_fields_json() {
2255 let json = r#"{
2256 "unused-files": "error",
2257 "unused-exports": "warn",
2258 "unused-types": "off",
2259 "unused-dependencies": "error",
2260 "unused-dev-dependencies": "warn",
2261 "unused-optional-dependencies": "off",
2262 "unused-enum-members": "error",
2263 "unused-class-members": "warn",
2264 "unresolved-imports": "off",
2265 "unlisted-dependencies": "error",
2266 "duplicate-exports": "warn",
2267 "type-only-dependencies": "off",
2268 "test-only-dependencies": "error",
2269 "circular-dependencies": "warn",
2270 "boundary-violation": "off",
2271 "coverage-gaps": "warn",
2272 "feature-flags": "error",
2273 "stale-suppressions": "off"
2274 }"#;
2275 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
2276 assert_eq!(partial.unused_files, Some(Severity::Error));
2277 assert_eq!(partial.unused_exports, Some(Severity::Warn));
2278 assert_eq!(partial.unused_types, Some(Severity::Off));
2279 assert_eq!(partial.unused_dependencies, Some(Severity::Error));
2280 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Warn));
2281 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
2282 assert_eq!(partial.unused_enum_members, Some(Severity::Error));
2283 assert_eq!(partial.unused_class_members, Some(Severity::Warn));
2284 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
2285 assert_eq!(partial.unlisted_dependencies, Some(Severity::Error));
2286 assert_eq!(partial.duplicate_exports, Some(Severity::Warn));
2287 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
2288 assert_eq!(partial.test_only_dependencies, Some(Severity::Error));
2289 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
2290 assert_eq!(partial.boundary_violation, Some(Severity::Off));
2291 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
2292 assert_eq!(partial.feature_flags, Some(Severity::Error));
2293 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
2294 }
2295
2296 #[test]
2297 fn partial_rules_none_fields_not_serialized() {
2298 let partial = PartialRulesConfig::default();
2299 let json = serde_json::to_string(&partial).unwrap();
2300 assert_eq!(
2301 json, "{}",
2302 "all-None partial should serialize to empty object"
2303 );
2304 }
2305
2306 #[test]
2307 fn partial_rules_some_fields_serialized() {
2308 let partial = PartialRulesConfig {
2309 unused_files: Some(Severity::Warn),
2310 ..Default::default()
2311 };
2312 let json = serde_json::to_string(&partial).unwrap();
2313 assert!(json.contains("unused-files"));
2314 assert!(!json.contains("unused-exports"));
2315 }
2316
2317 #[test]
2318 fn severity_json_deserialization() {
2319 let error: Severity = serde_json::from_str(r#""error""#).unwrap();
2320 assert_eq!(error, Severity::Error);
2321
2322 let warn: Severity = serde_json::from_str(r#""warn""#).unwrap();
2323 assert_eq!(warn, Severity::Warn);
2324
2325 let off: Severity = serde_json::from_str(r#""off""#).unwrap();
2326 assert_eq!(off, Severity::Off);
2327 }
2328
2329 #[test]
2330 fn severity_invalid_json_value_rejected() {
2331 let result: Result<Severity, _> = serde_json::from_str(r#""critical""#);
2332 assert!(result.is_err());
2333 }
2334
2335 #[test]
2336 fn severity_default_is_error() {
2337 assert_eq!(Severity::default(), Severity::Error);
2338 }
2339
2340 #[test]
2341 fn rules_config_json_roundtrip() {
2342 let rules = RulesConfig {
2343 unused_files: Severity::Warn,
2344 unused_exports: Severity::Off,
2345 type_only_dependencies: Severity::Error,
2346 ..RulesConfig::default()
2347 };
2348 let json = serde_json::to_string(&rules).unwrap();
2349 let restored: RulesConfig = serde_json::from_str(&json).unwrap();
2350 assert_eq!(restored.unused_files, Severity::Warn);
2351 assert_eq!(restored.unused_exports, Severity::Off);
2352 assert_eq!(restored.type_only_dependencies, Severity::Error);
2353 assert_eq!(restored.unused_dependencies, Severity::Error); }
2355
2356 #[test]
2357 fn apply_partial_preserves_type_only_default() {
2358 let mut rules = RulesConfig::default();
2359 let partial = PartialRulesConfig {
2360 unused_files: Some(Severity::Off),
2361 ..Default::default()
2362 };
2363 rules.apply_partial(&partial);
2364 assert_eq!(rules.type_only_dependencies, Severity::Warn);
2365 assert_eq!(rules.test_only_dependencies, Severity::Warn);
2366 }
2367}