1use fallow_types::suppress::IssueKind;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
10#[serde(rename_all = "lowercase")]
11pub enum Severity {
12 #[default]
14 Error,
15 Warn,
17 Off,
19}
20
21impl Severity {
22 const fn default_warn() -> Self {
24 Self::Warn
25 }
26
27 const fn default_off() -> Self {
29 Self::Off
30 }
31}
32
33impl std::fmt::Display for Severity {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 Self::Error => write!(f, "error"),
37 Self::Warn => write!(f, "warn"),
38 Self::Off => write!(f, "off"),
39 }
40 }
41}
42
43impl std::str::FromStr for Severity {
44 type Err = String;
45
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 match s.to_lowercase().as_str() {
48 "error" => Ok(Self::Error),
49 "warn" | "warning" => Ok(Self::Warn),
50 "off" | "none" => Ok(Self::Off),
51 other => Err(format!(
52 "unknown severity: '{other}' (expected error, warn, or off)"
53 )),
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
65#[serde(rename_all = "kebab-case")]
66pub struct RulesConfig {
67 #[serde(default, alias = "unused-file")]
69 pub unused_files: Severity,
70 #[serde(default, alias = "unused-export")]
72 pub unused_exports: Severity,
73 #[serde(default, alias = "unused-type")]
75 pub unused_types: Severity,
76 #[serde(default = "Severity::default_off", alias = "private-type-leak")]
79 pub private_type_leaks: Severity,
80 #[serde(skip)]
93 pub private_type_leaks_configured: bool,
94 #[serde(default, alias = "unused-dependency")]
97 pub unused_dependencies: Severity,
98 #[serde(default = "Severity::default_warn", alias = "unused-dev-dependency")]
101 pub unused_dev_dependencies: Severity,
102 #[serde(
105 default = "Severity::default_warn",
106 alias = "unused-optional-dependency"
107 )]
108 pub unused_optional_dependencies: Severity,
109 #[serde(default, alias = "unused-enum-member")]
111 pub unused_enum_members: Severity,
112 #[serde(default, alias = "unused-class-member")]
115 pub unused_class_members: Severity,
116 #[serde(default = "Severity::default_warn", alias = "unused-store-member")]
124 pub unused_store_members: Severity,
125 #[serde(default = "Severity::default_warn", alias = "unprovided-inject")]
131 pub unprovided_injects: Severity,
132 #[serde(default = "Severity::default_warn", alias = "unrendered-component")]
138 pub unrendered_components: Severity,
139 #[serde(default = "Severity::default_warn", alias = "unused-component-prop")]
145 pub unused_component_props: Severity,
146 #[serde(default = "Severity::default_warn", alias = "unused-component-emit")]
152 pub unused_component_emits: Severity,
153 #[serde(default = "Severity::default_warn", alias = "unused-component-input")]
160 pub unused_component_inputs: Severity,
161 #[serde(default = "Severity::default_warn", alias = "unused-component-output")]
168 pub unused_component_outputs: Severity,
169 #[serde(default = "Severity::default_warn", alias = "unused-svelte-event")]
177 pub unused_svelte_events: Severity,
178 #[serde(default = "Severity::default_warn", alias = "unused-server-action")]
186 pub unused_server_actions: Severity,
187 #[serde(default = "Severity::default_warn", alias = "unused-load-data-key")]
195 pub unused_load_data_keys: Severity,
196 #[serde(default = "Severity::default_off", alias = "prop-drilling")]
202 pub prop_drilling: Severity,
203 #[serde(default = "Severity::default_off", alias = "thin-wrapper")]
208 pub thin_wrapper: Severity,
209 #[serde(default = "Severity::default_off", alias = "duplicate-prop-shape")]
216 pub duplicate_prop_shape: Severity,
217 #[serde(default = "Severity::default_warn", alias = "css-token-drift")]
222 pub css_token_drift: Severity,
223 #[serde(default = "Severity::default_warn", alias = "css-duplicate-block")]
227 pub css_duplicate_block: Severity,
228 #[serde(default = "Severity::default_warn", alias = "css-selector-complexity")]
232 pub css_selector_complexity: Severity,
233 #[serde(default = "Severity::default_warn", alias = "css-dead-surface")]
237 pub css_dead_surface: Severity,
238 #[serde(default = "Severity::default_warn", alias = "css-broken-reference")]
242 pub css_broken_reference: Severity,
243 #[serde(default, alias = "unresolved-import")]
246 pub unresolved_imports: Severity,
247 #[serde(default, alias = "unlisted-dependency")]
250 pub unlisted_dependencies: Severity,
251 #[serde(default, alias = "duplicate-export")]
254 pub duplicate_exports: Severity,
255 #[serde(default = "Severity::default_warn", alias = "type-only-dependency")]
258 pub type_only_dependencies: Severity,
259 #[serde(default = "Severity::default_warn", alias = "test-only-dependency")]
262 pub test_only_dependencies: Severity,
263 #[serde(
267 default = "Severity::default_warn",
268 alias = "dev-dependency-in-production"
269 )]
270 pub dev_dependencies_in_production: Severity,
271 #[serde(default, alias = "circular-dependency")]
273 pub circular_dependencies: Severity,
274 #[serde(
277 default = "Severity::default_warn",
278 alias = "re-export-cycles",
279 alias = "reexport-cycle",
280 alias = "reexport-cycles"
281 )]
282 pub re_export_cycle: Severity,
283 #[serde(default, alias = "boundary-violations")]
286 pub boundary_violation: Severity,
287 #[serde(default = "Severity::default_off", alias = "coverage-gap")]
290 pub coverage_gaps: Severity,
291 #[serde(default = "Severity::default_off", alias = "feature-flag")]
294 pub feature_flags: Severity,
295 #[serde(default = "Severity::default_warn", alias = "stale-suppression")]
298 pub stale_suppressions: Severity,
299 #[serde(default = "Severity::default_off", alias = "suppression-reason")]
302 pub require_suppression_reason: Severity,
303 #[serde(default = "Severity::default_warn", alias = "unused-catalog-entry")]
306 pub unused_catalog_entries: Severity,
307 #[serde(default = "Severity::default_warn", alias = "empty-catalog-group")]
309 pub empty_catalog_groups: Severity,
310 #[serde(default, alias = "unresolved-catalog-reference")]
314 pub unresolved_catalog_references: Severity,
315 #[serde(
319 default = "Severity::default_warn",
320 alias = "unused-dependency-override"
321 )]
322 pub unused_dependency_overrides: Severity,
323 #[serde(default, alias = "misconfigured-dependency-override")]
327 pub misconfigured_dependency_overrides: Severity,
328 #[serde(default = "Severity::default_off")]
332 pub security_client_server_leak: Severity,
333 #[serde(default = "Severity::default_off")]
338 pub security_sink: Severity,
339 #[serde(default = "Severity::default_warn", alias = "policy-violations")]
345 pub policy_violation: Severity,
346 #[serde(default = "Severity::default_warn", alias = "invalid-client-exports")]
351 pub invalid_client_export: Severity,
352 #[serde(
357 default = "Severity::default_warn",
358 alias = "mixed-client-server-barrels"
359 )]
360 pub mixed_client_server_barrel: Severity,
361 #[serde(default = "Severity::default_warn", alias = "misplaced-directives")]
367 pub misplaced_directive: Severity,
368 #[serde(default, alias = "route-collisions")]
375 pub route_collision: Severity,
376 #[serde(default, alias = "dynamic-segment-name-conflicts")]
386 pub dynamic_segment_name_conflict: Severity,
387}
388
389impl Default for RulesConfig {
390 fn default() -> Self {
391 Self {
392 unused_files: Severity::Error,
393 unused_exports: Severity::Error,
394 unused_types: Severity::Error,
395 private_type_leaks: Severity::Off,
396 private_type_leaks_configured: false,
397 unused_dependencies: Severity::Error,
398 unused_dev_dependencies: Severity::Warn,
399 unused_optional_dependencies: Severity::Warn,
400 unused_enum_members: Severity::Error,
401 unused_class_members: Severity::Error,
402 unused_store_members: Severity::Warn,
403 unprovided_injects: Severity::Warn,
404 unrendered_components: Severity::Warn,
405 unused_component_props: Severity::Warn,
406 unused_component_emits: Severity::Warn,
407 unused_component_inputs: Severity::Warn,
408 unused_component_outputs: Severity::Warn,
409 unused_svelte_events: Severity::Warn,
410 unused_server_actions: Severity::Warn,
411 unused_load_data_keys: Severity::Warn,
412 prop_drilling: Severity::Off,
413 thin_wrapper: Severity::Off,
414 duplicate_prop_shape: Severity::Off,
415 css_token_drift: Severity::Warn,
416 css_duplicate_block: Severity::Warn,
417 css_selector_complexity: Severity::Warn,
418 css_dead_surface: Severity::Warn,
419 css_broken_reference: Severity::Warn,
420 unresolved_imports: Severity::Error,
421 unlisted_dependencies: Severity::Error,
422 duplicate_exports: Severity::Error,
423 type_only_dependencies: Severity::Warn,
424 test_only_dependencies: Severity::Warn,
425 dev_dependencies_in_production: Severity::Warn,
426 circular_dependencies: Severity::Error,
427 re_export_cycle: Severity::Warn,
428 boundary_violation: Severity::Error,
429 coverage_gaps: Severity::Off,
430 feature_flags: Severity::Off,
431 stale_suppressions: Severity::Warn,
432 require_suppression_reason: Severity::Off,
433 unused_catalog_entries: Severity::Warn,
434 empty_catalog_groups: Severity::Warn,
435 unresolved_catalog_references: Severity::Error,
436 unused_dependency_overrides: Severity::Warn,
437 misconfigured_dependency_overrides: Severity::Error,
438 security_client_server_leak: Severity::Off,
439 security_sink: Severity::Off,
440 policy_violation: Severity::Warn,
441 invalid_client_export: Severity::Warn,
442 mixed_client_server_barrel: Severity::Warn,
443 misplaced_directive: Severity::Warn,
444 route_collision: Severity::Error,
445 dynamic_segment_name_conflict: Severity::Error,
446 }
447 }
448}
449
450macro_rules! apply_partial_rules {
451 ($target:expr, $partial:expr, [$($field:ident),+ $(,)?]) => {
452 $(
453 if let Some(severity) = $partial.$field {
454 $target.$field = severity;
455 }
456 )+
457 };
458}
459
460impl RulesConfig {
461 #[must_use]
473 pub const fn severity_for_kind(&self, kind: IssueKind) -> Severity {
474 match kind {
475 IssueKind::UnusedFile => self.unused_files,
476 IssueKind::UnusedExport => self.unused_exports,
477 IssueKind::UnusedType => self.unused_types,
478 IssueKind::PrivateTypeLeak => self.private_type_leaks,
479 IssueKind::UnusedDependency => self.unused_dependencies,
480 IssueKind::UnusedDevDependency => self.unused_dev_dependencies,
481 IssueKind::UnusedEnumMember => self.unused_enum_members,
482 IssueKind::UnusedClassMember => self.unused_class_members,
483 IssueKind::UnusedStoreMember => self.unused_store_members,
484 IssueKind::UnprovidedInject => self.unprovided_injects,
485 IssueKind::UnresolvedImport => self.unresolved_imports,
486 IssueKind::UnlistedDependency => self.unlisted_dependencies,
487 IssueKind::DuplicateExport => self.duplicate_exports,
488 IssueKind::CircularDependency => self.circular_dependencies,
489 IssueKind::ReExportCycle => self.re_export_cycle,
490 IssueKind::TypeOnlyDependency => self.type_only_dependencies,
491 IssueKind::TestOnlyDependency => self.test_only_dependencies,
492 IssueKind::DevDependencyInProduction => self.dev_dependencies_in_production,
493 IssueKind::BoundaryViolation => self.boundary_violation,
494 IssueKind::CoverageGaps => self.coverage_gaps,
495 IssueKind::FeatureFlag => self.feature_flags,
496 IssueKind::StaleSuppression => self.stale_suppressions,
497 IssueKind::PnpmCatalogEntry => self.unused_catalog_entries,
498 IssueKind::EmptyCatalogGroup => self.empty_catalog_groups,
499 IssueKind::UnresolvedCatalogReference => self.unresolved_catalog_references,
500 IssueKind::UnusedDependencyOverride => self.unused_dependency_overrides,
501 IssueKind::MisconfiguredDependencyOverride => self.misconfigured_dependency_overrides,
502 IssueKind::SecurityClientServerLeak => self.security_client_server_leak,
503 IssueKind::SecuritySink => self.security_sink,
504 IssueKind::PolicyViolation => self.policy_violation,
505 IssueKind::InvalidClientExport => self.invalid_client_export,
506 IssueKind::MixedClientServerBarrel => self.mixed_client_server_barrel,
507 IssueKind::MisplacedDirective => self.misplaced_directive,
508 IssueKind::RouteCollision => self.route_collision,
509 IssueKind::DynamicSegmentNameConflict => self.dynamic_segment_name_conflict,
510 IssueKind::UnrenderedComponent => self.unrendered_components,
511 IssueKind::UnusedComponentProp => self.unused_component_props,
512 IssueKind::UnusedComponentEmit => self.unused_component_emits,
513 IssueKind::UnusedComponentInput => self.unused_component_inputs,
514 IssueKind::UnusedComponentOutput => self.unused_component_outputs,
515 IssueKind::UnusedSvelteEvent => self.unused_svelte_events,
516 IssueKind::UnusedServerAction => self.unused_server_actions,
517 IssueKind::UnusedLoadDataKey => self.unused_load_data_keys,
518 IssueKind::PropDrilling => self.prop_drilling,
519 IssueKind::ThinWrapper => self.thin_wrapper,
520 IssueKind::DuplicatePropShape => self.duplicate_prop_shape,
521 IssueKind::CssTokenDrift => self.css_token_drift,
522 IssueKind::CssDuplicateBlock => self.css_duplicate_block,
523 IssueKind::CssSelectorComplexity => self.css_selector_complexity,
524 IssueKind::CssDeadSurface => self.css_dead_surface,
525 IssueKind::CssBrokenReference => self.css_broken_reference,
526 IssueKind::Complexity | IssueKind::CodeDuplication => Severity::Error,
527 }
528 }
529
530 pub const fn apply_partial(&mut self, partial: &PartialRulesConfig) {
532 apply_partial_rules!(
533 self,
534 partial,
535 [
536 unused_files,
537 unused_exports,
538 unused_types,
539 private_type_leaks,
540 unused_dependencies,
541 unused_dev_dependencies,
542 unused_optional_dependencies,
543 ]
544 );
545 apply_partial_rules!(
546 self,
547 partial,
548 [
549 unused_enum_members,
550 unused_class_members,
551 unused_store_members,
552 unprovided_injects,
553 unrendered_components,
554 unused_component_props,
555 unused_component_emits,
556 unused_component_inputs,
557 unused_component_outputs,
558 unused_svelte_events,
559 unused_server_actions,
560 unused_load_data_keys,
561 prop_drilling,
562 thin_wrapper,
563 duplicate_prop_shape,
564 css_token_drift,
565 css_duplicate_block,
566 css_selector_complexity,
567 css_dead_surface,
568 css_broken_reference,
569 ]
570 );
571 apply_partial_rules!(
572 self,
573 partial,
574 [
575 unresolved_imports,
576 unlisted_dependencies,
577 duplicate_exports,
578 type_only_dependencies,
579 test_only_dependencies,
580 dev_dependencies_in_production,
581 circular_dependencies,
582 re_export_cycle,
583 boundary_violation,
584 ]
585 );
586 apply_partial_rules!(
587 self,
588 partial,
589 [
590 coverage_gaps,
591 feature_flags,
592 stale_suppressions,
593 require_suppression_reason,
594 unused_catalog_entries,
595 empty_catalog_groups,
596 unresolved_catalog_references,
597 unused_dependency_overrides,
598 misconfigured_dependency_overrides,
599 ]
600 );
601 apply_partial_rules!(
602 self,
603 partial,
604 [
605 security_client_server_leak,
606 security_sink,
607 policy_violation,
608 invalid_client_export,
609 mixed_client_server_barrel,
610 misplaced_directive,
611 route_collision,
612 dynamic_segment_name_conflict,
613 ]
614 );
615 }
616}
617
618#[must_use]
626pub fn default_severity_for_kind(kind: IssueKind) -> Severity {
627 RulesConfig::default().severity_for_kind(kind)
628}
629
630#[must_use]
637pub fn is_opt_in_kind(kind: IssueKind) -> bool {
638 matches!(default_severity_for_kind(kind), Severity::Off)
639}
640
641#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
643#[serde(rename_all = "kebab-case")]
644pub struct PartialRulesConfig {
645 #[serde(
647 default,
648 alias = "unused-file",
649 skip_serializing_if = "Option::is_none"
650 )]
651 pub unused_files: Option<Severity>,
652 #[serde(
654 default,
655 alias = "unused-export",
656 skip_serializing_if = "Option::is_none"
657 )]
658 pub unused_exports: Option<Severity>,
659 #[serde(
661 default,
662 alias = "unused-type",
663 skip_serializing_if = "Option::is_none"
664 )]
665 pub unused_types: Option<Severity>,
666 #[serde(
668 default,
669 alias = "private-type-leak",
670 skip_serializing_if = "Option::is_none"
671 )]
672 pub private_type_leaks: Option<Severity>,
673 #[serde(
675 default,
676 alias = "unused-dependency",
677 skip_serializing_if = "Option::is_none"
678 )]
679 pub unused_dependencies: Option<Severity>,
680 #[serde(
682 default,
683 alias = "unused-dev-dependency",
684 skip_serializing_if = "Option::is_none"
685 )]
686 pub unused_dev_dependencies: Option<Severity>,
687 #[serde(
689 default,
690 alias = "unused-optional-dependency",
691 skip_serializing_if = "Option::is_none"
692 )]
693 pub unused_optional_dependencies: Option<Severity>,
694 #[serde(
696 default,
697 alias = "unused-enum-member",
698 skip_serializing_if = "Option::is_none"
699 )]
700 pub unused_enum_members: Option<Severity>,
701 #[serde(
703 default,
704 alias = "unused-class-member",
705 skip_serializing_if = "Option::is_none"
706 )]
707 pub unused_class_members: Option<Severity>,
708 #[serde(
710 default,
711 alias = "unused-store-member",
712 skip_serializing_if = "Option::is_none"
713 )]
714 pub unused_store_members: Option<Severity>,
715 #[serde(
717 default,
718 alias = "unprovided-inject",
719 skip_serializing_if = "Option::is_none"
720 )]
721 pub unprovided_injects: Option<Severity>,
722 #[serde(
724 default,
725 alias = "unrendered-component",
726 skip_serializing_if = "Option::is_none"
727 )]
728 pub unrendered_components: Option<Severity>,
729 #[serde(
731 default,
732 alias = "unused-component-prop",
733 skip_serializing_if = "Option::is_none"
734 )]
735 pub unused_component_props: Option<Severity>,
736 #[serde(
738 default,
739 alias = "unused-component-emit",
740 skip_serializing_if = "Option::is_none"
741 )]
742 pub unused_component_emits: Option<Severity>,
743 #[serde(
745 default,
746 alias = "unused-component-input",
747 skip_serializing_if = "Option::is_none"
748 )]
749 pub unused_component_inputs: Option<Severity>,
750 #[serde(
752 default,
753 alias = "unused-component-output",
754 skip_serializing_if = "Option::is_none"
755 )]
756 pub unused_component_outputs: Option<Severity>,
757 #[serde(
759 default,
760 alias = "unused-svelte-event",
761 skip_serializing_if = "Option::is_none"
762 )]
763 pub unused_svelte_events: Option<Severity>,
764 #[serde(
766 default,
767 alias = "unused-server-action",
768 skip_serializing_if = "Option::is_none"
769 )]
770 pub unused_server_actions: Option<Severity>,
771 #[serde(
773 default,
774 alias = "unused-load-data-key",
775 skip_serializing_if = "Option::is_none"
776 )]
777 pub unused_load_data_keys: Option<Severity>,
778 #[serde(
780 default,
781 alias = "prop-drilling",
782 skip_serializing_if = "Option::is_none"
783 )]
784 pub prop_drilling: Option<Severity>,
785 #[serde(
787 default,
788 alias = "thin-wrapper",
789 skip_serializing_if = "Option::is_none"
790 )]
791 pub thin_wrapper: Option<Severity>,
792 #[serde(
794 default,
795 alias = "duplicate-prop-shape",
796 skip_serializing_if = "Option::is_none"
797 )]
798 pub duplicate_prop_shape: Option<Severity>,
799 #[serde(
801 default,
802 alias = "css-token-drift",
803 skip_serializing_if = "Option::is_none"
804 )]
805 pub css_token_drift: Option<Severity>,
806 #[serde(
808 default,
809 alias = "css-duplicate-block",
810 skip_serializing_if = "Option::is_none"
811 )]
812 pub css_duplicate_block: Option<Severity>,
813 #[serde(
815 default,
816 alias = "css-selector-complexity",
817 skip_serializing_if = "Option::is_none"
818 )]
819 pub css_selector_complexity: Option<Severity>,
820 #[serde(
822 default,
823 alias = "css-dead-surface",
824 skip_serializing_if = "Option::is_none"
825 )]
826 pub css_dead_surface: Option<Severity>,
827 #[serde(
829 default,
830 alias = "css-broken-reference",
831 skip_serializing_if = "Option::is_none"
832 )]
833 pub css_broken_reference: Option<Severity>,
834 #[serde(
836 default,
837 alias = "unresolved-import",
838 skip_serializing_if = "Option::is_none"
839 )]
840 pub unresolved_imports: Option<Severity>,
841 #[serde(
843 default,
844 alias = "unlisted-dependency",
845 skip_serializing_if = "Option::is_none"
846 )]
847 pub unlisted_dependencies: Option<Severity>,
848 #[serde(
850 default,
851 alias = "duplicate-export",
852 skip_serializing_if = "Option::is_none"
853 )]
854 pub duplicate_exports: Option<Severity>,
855 #[serde(
857 default,
858 alias = "type-only-dependency",
859 skip_serializing_if = "Option::is_none"
860 )]
861 pub type_only_dependencies: Option<Severity>,
862 #[serde(
864 default,
865 alias = "test-only-dependency",
866 skip_serializing_if = "Option::is_none"
867 )]
868 pub test_only_dependencies: Option<Severity>,
869 #[serde(
871 default,
872 alias = "dev-dependency-in-production",
873 skip_serializing_if = "Option::is_none"
874 )]
875 pub dev_dependencies_in_production: Option<Severity>,
876 #[serde(
878 default,
879 alias = "circular-dependency",
880 skip_serializing_if = "Option::is_none"
881 )]
882 pub circular_dependencies: Option<Severity>,
883 #[serde(
885 default,
886 alias = "re-export-cycles",
887 alias = "reexport-cycle",
888 alias = "reexport-cycles",
889 skip_serializing_if = "Option::is_none"
890 )]
891 pub re_export_cycle: Option<Severity>,
892 #[serde(
894 default,
895 alias = "boundary-violations",
896 skip_serializing_if = "Option::is_none"
897 )]
898 pub boundary_violation: Option<Severity>,
899 #[serde(
901 default,
902 alias = "coverage-gap",
903 skip_serializing_if = "Option::is_none"
904 )]
905 pub coverage_gaps: Option<Severity>,
906 #[serde(
908 default,
909 alias = "feature-flag",
910 skip_serializing_if = "Option::is_none"
911 )]
912 pub feature_flags: Option<Severity>,
913 #[serde(
915 default,
916 alias = "stale-suppression",
917 skip_serializing_if = "Option::is_none"
918 )]
919 pub stale_suppressions: Option<Severity>,
920 #[serde(
922 default,
923 alias = "suppression-reason",
924 skip_serializing_if = "Option::is_none"
925 )]
926 pub require_suppression_reason: Option<Severity>,
927 #[serde(
929 default,
930 alias = "unused-catalog-entry",
931 skip_serializing_if = "Option::is_none"
932 )]
933 pub unused_catalog_entries: Option<Severity>,
934 #[serde(
936 default,
937 alias = "empty-catalog-group",
938 skip_serializing_if = "Option::is_none"
939 )]
940 pub empty_catalog_groups: Option<Severity>,
941 #[serde(
943 default,
944 alias = "unresolved-catalog-reference",
945 skip_serializing_if = "Option::is_none"
946 )]
947 pub unresolved_catalog_references: Option<Severity>,
948 #[serde(
950 default,
951 alias = "unused-dependency-override",
952 skip_serializing_if = "Option::is_none"
953 )]
954 pub unused_dependency_overrides: Option<Severity>,
955 #[serde(
957 default,
958 alias = "misconfigured-dependency-override",
959 skip_serializing_if = "Option::is_none"
960 )]
961 pub misconfigured_dependency_overrides: Option<Severity>,
962 #[serde(default, skip_serializing_if = "Option::is_none")]
964 pub security_client_server_leak: Option<Severity>,
965 #[serde(default, skip_serializing_if = "Option::is_none")]
967 pub security_sink: Option<Severity>,
968 #[serde(
970 default,
971 alias = "policy-violations",
972 skip_serializing_if = "Option::is_none"
973 )]
974 pub policy_violation: Option<Severity>,
975 #[serde(
977 default,
978 alias = "invalid-client-exports",
979 skip_serializing_if = "Option::is_none"
980 )]
981 pub invalid_client_export: Option<Severity>,
982 #[serde(
984 default,
985 alias = "mixed-client-server-barrels",
986 skip_serializing_if = "Option::is_none"
987 )]
988 pub mixed_client_server_barrel: Option<Severity>,
989 #[serde(
991 default,
992 alias = "misplaced-directives",
993 skip_serializing_if = "Option::is_none"
994 )]
995 pub misplaced_directive: Option<Severity>,
996 #[serde(
998 default,
999 alias = "route-collisions",
1000 skip_serializing_if = "Option::is_none"
1001 )]
1002 pub route_collision: Option<Severity>,
1003 #[serde(
1005 default,
1006 alias = "dynamic-segment-name-conflicts",
1007 skip_serializing_if = "Option::is_none"
1008 )]
1009 pub dynamic_segment_name_conflict: Option<Severity>,
1010}
1011
1012pub const KNOWN_RULE_NAMES: &[&str] = &[
1023 "unused-files",
1024 "unused-exports",
1025 "unused-types",
1026 "private-type-leaks",
1027 "unused-dependencies",
1028 "unused-dev-dependencies",
1029 "unused-optional-dependencies",
1030 "unused-enum-members",
1031 "unused-class-members",
1032 "unused-store-members",
1033 "unprovided-injects",
1034 "unrendered-components",
1035 "unused-component-props",
1036 "unused-component-emits",
1037 "unused-component-inputs",
1038 "unused-component-outputs",
1039 "unused-svelte-events",
1040 "unused-server-actions",
1041 "unused-load-data-keys",
1042 "prop-drilling",
1043 "thin-wrapper",
1044 "duplicate-prop-shape",
1045 "css-token-drift",
1046 "css-duplicate-block",
1047 "css-selector-complexity",
1048 "css-dead-surface",
1049 "css-broken-reference",
1050 "unresolved-imports",
1051 "unlisted-dependencies",
1052 "duplicate-exports",
1053 "type-only-dependencies",
1054 "test-only-dependencies",
1055 "dev-dependencies-in-production",
1056 "circular-dependencies",
1057 "re-export-cycle",
1058 "boundary-violation",
1059 "coverage-gaps",
1060 "feature-flags",
1061 "stale-suppressions",
1062 "require-suppression-reason",
1063 "suppression-reason",
1064 "unused-catalog-entries",
1065 "empty-catalog-groups",
1066 "unresolved-catalog-references",
1067 "unused-dependency-overrides",
1068 "misconfigured-dependency-overrides",
1069 "security-client-server-leak",
1070 "security-sink",
1071 "policy-violation",
1072 "policy-violations",
1073 "invalid-client-export",
1074 "mixed-client-server-barrel",
1075 "misplaced-directive",
1076 "route-collision",
1077 "dynamic-segment-name-conflict",
1078 "unused-file",
1079 "unused-export",
1080 "unused-type",
1081 "private-type-leak",
1082 "unused-dependency",
1083 "unused-dev-dependency",
1084 "unused-optional-dependency",
1085 "unused-enum-member",
1086 "unused-class-member",
1087 "unused-store-member",
1088 "unprovided-inject",
1089 "unrendered-component",
1090 "unused-component-prop",
1091 "unused-component-emit",
1092 "unused-component-input",
1093 "unused-component-output",
1094 "unused-svelte-event",
1095 "unused-server-action",
1096 "unused-load-data-key",
1097 "unresolved-import",
1098 "unlisted-dependency",
1099 "duplicate-export",
1100 "type-only-dependency",
1101 "test-only-dependency",
1102 "dev-dependency-in-production",
1103 "circular-dependency",
1104 "re-export-cycles",
1105 "reexport-cycle",
1106 "reexport-cycles",
1107 "boundary-violations",
1108 "coverage-gap",
1109 "feature-flag",
1110 "stale-suppression",
1111 "unused-catalog-entry",
1112 "empty-catalog-group",
1113 "unresolved-catalog-reference",
1114 "unused-dependency-override",
1115 "misconfigured-dependency-override",
1116 "invalid-client-exports",
1117 "mixed-client-server-barrels",
1118 "misplaced-directives",
1119 "route-collisions",
1120 "dynamic-segment-name-conflicts",
1121];
1122
1123#[must_use]
1129pub fn closest_known_rule_name(input: &str) -> Option<&'static str> {
1130 let input_lower = input.to_ascii_lowercase();
1131 let candidates = KNOWN_RULE_NAMES.iter().copied();
1132 let suggestion = crate::levenshtein::closest_match(&input_lower, candidates)?;
1133 KNOWN_RULE_NAMES.iter().copied().find(|&c| c == suggestion)
1134}
1135
1136#[derive(Debug, Clone, PartialEq, Eq)]
1142pub struct UnknownRuleKey {
1143 pub context: String,
1145 pub key: String,
1147 pub suggestion: Option<&'static str>,
1149}
1150
1151#[must_use]
1159pub fn find_unknown_rule_keys(value: &serde_json::Value, context: &str) -> Vec<UnknownRuleKey> {
1160 let Some(map) = value.as_object() else {
1161 return Vec::new();
1162 };
1163
1164 map.keys()
1165 .filter(|key| !KNOWN_RULE_NAMES.contains(&key.as_str()))
1166 .map(|key| UnknownRuleKey {
1167 context: context.to_owned(),
1168 key: key.clone(),
1169 suggestion: closest_known_rule_name(key),
1170 })
1171 .collect()
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176 use super::*;
1177
1178 #[test]
1179 fn rules_default_severities() {
1180 let rules = RulesConfig::default();
1181 assert_eq!(rules.unused_files, Severity::Error);
1182 assert_eq!(rules.unused_exports, Severity::Error);
1183 assert_eq!(rules.unused_types, Severity::Error);
1184 assert_eq!(rules.private_type_leaks, Severity::Off);
1185 assert_eq!(rules.unused_dependencies, Severity::Error);
1186 assert_eq!(rules.unused_dev_dependencies, Severity::Warn);
1187 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
1188 assert_eq!(rules.unused_enum_members, Severity::Error);
1189 assert_eq!(rules.unused_class_members, Severity::Error);
1190 assert_eq!(rules.unresolved_imports, Severity::Error);
1191 assert_eq!(rules.unlisted_dependencies, Severity::Error);
1192 assert_eq!(rules.duplicate_exports, Severity::Error);
1193 assert_eq!(rules.type_only_dependencies, Severity::Warn);
1194 assert_eq!(rules.test_only_dependencies, Severity::Warn);
1195 assert_eq!(rules.circular_dependencies, Severity::Error);
1196 assert_eq!(rules.boundary_violation, Severity::Error);
1197 assert_eq!(rules.coverage_gaps, Severity::Off);
1198 assert_eq!(rules.feature_flags, Severity::Off);
1199 assert_eq!(rules.stale_suppressions, Severity::Warn);
1200 assert_eq!(rules.unused_catalog_entries, Severity::Warn);
1201 assert_eq!(rules.empty_catalog_groups, Severity::Warn);
1202 assert_eq!(rules.unresolved_catalog_references, Severity::Error);
1203 }
1204
1205 #[test]
1206 fn rules_deserialize_kebab_case() {
1207 let json_str = r#"{
1208 "unused-files": "error",
1209 "unused-exports": "warn",
1210 "unused-types": "off"
1211 }"#;
1212 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1213 assert_eq!(rules.unused_files, Severity::Error);
1214 assert_eq!(rules.unused_exports, Severity::Warn);
1215 assert_eq!(rules.unused_types, Severity::Off);
1216 assert_eq!(rules.unresolved_imports, Severity::Error);
1217 }
1218
1219 #[test]
1220 fn empty_rules_object_matches_default_impl() {
1221 let from_empty: RulesConfig = serde_json::from_str("{}").unwrap();
1229 assert_eq!(
1230 from_empty,
1231 RulesConfig::default(),
1232 "empty `rules` object must equal RulesConfig::default(); a field whose \
1233 Default is Warn/Off needs an explicit #[serde(default = ...)]"
1234 );
1235 }
1236
1237 #[test]
1238 fn empty_rules_object_preserves_non_error_defaults() {
1239 let rules: RulesConfig = serde_json::from_str("{}").unwrap();
1242 assert_eq!(rules.coverage_gaps, Severity::Off);
1243 assert_eq!(rules.unused_store_members, Severity::Warn);
1244 assert_eq!(rules.unprovided_injects, Severity::Warn);
1245 assert_eq!(rules.unrendered_components, Severity::Warn);
1246 assert_eq!(rules.unused_component_props, Severity::Warn);
1247 assert_eq!(rules.unused_component_emits, Severity::Warn);
1248 assert_eq!(rules.unused_component_inputs, Severity::Warn);
1249 assert_eq!(rules.unused_component_outputs, Severity::Warn);
1250 assert_eq!(rules.unused_server_actions, Severity::Warn);
1251 }
1252
1253 #[test]
1254 fn explicit_rule_severity_still_overrides_default() {
1255 let rules: RulesConfig =
1258 serde_json::from_str(r#"{ "coverage-gaps": "error", "unused-component-prop": "off" }"#)
1259 .unwrap();
1260 assert_eq!(rules.coverage_gaps, Severity::Error);
1261 assert_eq!(rules.unused_component_props, Severity::Off);
1262 }
1263
1264 #[test]
1265 fn rules_re_export_cycle_default_is_warn() {
1266 let rules = RulesConfig::default();
1267 assert_eq!(rules.re_export_cycle, Severity::Warn);
1268 }
1269
1270 #[test]
1271 fn rules_deserialize_re_export_cycle_aliases() {
1272 for token in [
1273 "re-export-cycle",
1274 "re-export-cycles",
1275 "reexport-cycle",
1276 "reexport-cycles",
1277 ] {
1278 let json_str = format!(r#"{{ "{token}": "error" }}"#);
1279 let rules: RulesConfig = serde_json::from_str(&json_str)
1280 .unwrap_or_else(|e| panic!("alias {token} did not deserialize: {e}"));
1281 assert_eq!(
1282 rules.re_export_cycle,
1283 Severity::Error,
1284 "alias {token} should set re_export_cycle"
1285 );
1286 }
1287 }
1288
1289 #[test]
1290 fn rules_deserialize_circular_dependency_alias() {
1291 let json_str = r#"{
1292 "circular-dependency": "off"
1293 }"#;
1294 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1295 assert_eq!(rules.circular_dependencies, Severity::Off);
1296 }
1297
1298 #[test]
1299 fn rules_deserialize_boundary_violations_alias() {
1300 let json_str = r#"{
1301 "boundary-violations": "off"
1302 }"#;
1303 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1304 assert_eq!(rules.boundary_violation, Severity::Off);
1305
1306 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
1307 assert_eq!(partial.boundary_violation, Some(Severity::Off));
1308 }
1309
1310 #[test]
1311 fn rules_deserialize_singular_aliases_for_every_plural_rule() {
1312 let json_str = r#"{
1313 "unused-file": "off",
1314 "unused-export": "off",
1315 "unused-type": "off",
1316 "private-type-leak": "warn",
1317 "unused-dependency": "off",
1318 "unused-dev-dependency": "off",
1319 "unused-optional-dependency": "off",
1320 "unused-enum-member": "off",
1321 "unused-class-member": "off",
1322 "unresolved-import": "off",
1323 "unlisted-dependency": "off",
1324 "duplicate-export": "off",
1325 "type-only-dependency": "off",
1326 "test-only-dependency": "off",
1327 "coverage-gap": "warn",
1328 "feature-flag": "warn",
1329 "stale-suppression": "off",
1330 "suppression-reason": "warn",
1331 "unused-catalog-entry": "error",
1332 "empty-catalog-group": "error",
1333 "unresolved-catalog-reference": "warn"
1334 }"#;
1335
1336 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1337 assert_eq!(rules.unused_files, Severity::Off);
1338 assert_eq!(rules.unused_exports, Severity::Off);
1339 assert_eq!(rules.unused_types, Severity::Off);
1340 assert_eq!(rules.private_type_leaks, Severity::Warn);
1341 assert_eq!(rules.unused_dependencies, Severity::Off);
1342 assert_eq!(rules.unused_dev_dependencies, Severity::Off);
1343 assert_eq!(rules.unused_optional_dependencies, Severity::Off);
1344 assert_eq!(rules.unused_enum_members, Severity::Off);
1345 assert_eq!(rules.unused_class_members, Severity::Off);
1346 assert_eq!(rules.unresolved_imports, Severity::Off);
1347 assert_eq!(rules.unlisted_dependencies, Severity::Off);
1348 assert_eq!(rules.duplicate_exports, Severity::Off);
1349 assert_eq!(rules.type_only_dependencies, Severity::Off);
1350 assert_eq!(rules.test_only_dependencies, Severity::Off);
1351 assert_eq!(rules.coverage_gaps, Severity::Warn);
1352 assert_eq!(rules.feature_flags, Severity::Warn);
1353 assert_eq!(rules.stale_suppressions, Severity::Off);
1354 assert_eq!(rules.require_suppression_reason, Severity::Warn);
1355 assert_eq!(rules.unused_catalog_entries, Severity::Error);
1356 assert_eq!(rules.empty_catalog_groups, Severity::Error);
1357 assert_eq!(rules.unresolved_catalog_references, Severity::Warn);
1358
1359 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
1360 assert_eq!(partial.unused_files, Some(Severity::Off));
1361 assert_eq!(partial.unused_exports, Some(Severity::Off));
1362 assert_eq!(partial.unused_types, Some(Severity::Off));
1363 assert_eq!(partial.private_type_leaks, Some(Severity::Warn));
1364 assert_eq!(partial.unused_dependencies, Some(Severity::Off));
1365 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Off));
1366 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
1367 assert_eq!(partial.unused_enum_members, Some(Severity::Off));
1368 assert_eq!(partial.unused_class_members, Some(Severity::Off));
1369 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
1370 assert_eq!(partial.unlisted_dependencies, Some(Severity::Off));
1371 assert_eq!(partial.duplicate_exports, Some(Severity::Off));
1372 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
1373 assert_eq!(partial.test_only_dependencies, Some(Severity::Off));
1374 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
1375 assert_eq!(partial.feature_flags, Some(Severity::Warn));
1376 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
1377 assert_eq!(partial.require_suppression_reason, Some(Severity::Warn));
1378 assert_eq!(partial.unused_catalog_entries, Some(Severity::Error));
1379 assert_eq!(partial.empty_catalog_groups, Some(Severity::Error));
1380 assert_eq!(partial.unresolved_catalog_references, Some(Severity::Warn));
1381 }
1382
1383 #[test]
1384 fn severity_from_str() {
1385 assert_eq!("error".parse::<Severity>().unwrap(), Severity::Error);
1386 assert_eq!("warn".parse::<Severity>().unwrap(), Severity::Warn);
1387 assert_eq!("warning".parse::<Severity>().unwrap(), Severity::Warn);
1388 assert_eq!("off".parse::<Severity>().unwrap(), Severity::Off);
1389 assert_eq!("none".parse::<Severity>().unwrap(), Severity::Off);
1390 assert!("invalid".parse::<Severity>().is_err());
1391 }
1392
1393 #[test]
1394 fn apply_partial_only_some_fields() {
1395 let mut rules = RulesConfig::default();
1396 let partial = PartialRulesConfig {
1397 unused_files: Some(Severity::Warn),
1398 unused_exports: Some(Severity::Off),
1399 ..Default::default()
1400 };
1401 rules.apply_partial(&partial);
1402 assert_eq!(rules.unused_files, Severity::Warn);
1403 assert_eq!(rules.unused_exports, Severity::Off);
1404 assert_eq!(rules.unused_types, Severity::Error);
1405 assert_eq!(rules.unresolved_imports, Severity::Error);
1406 assert_eq!(rules.require_suppression_reason, Severity::Off);
1407 }
1408
1409 #[test]
1410 fn require_suppression_reason_deserializes_canonical_and_alias() {
1411 let rules: RulesConfig = serde_json::from_str(
1412 r#"{
1413 "require-suppression-reason": "error"
1414 }"#,
1415 )
1416 .unwrap();
1417 assert_eq!(rules.require_suppression_reason, Severity::Error);
1418
1419 let partial: PartialRulesConfig = serde_json::from_str(
1420 r#"{
1421 "suppression-reason": "warn"
1422 }"#,
1423 )
1424 .unwrap();
1425 assert_eq!(partial.require_suppression_reason, Some(Severity::Warn));
1426 }
1427
1428 #[test]
1429 fn severity_display() {
1430 assert_eq!(Severity::Error.to_string(), "error");
1431 assert_eq!(Severity::Warn.to_string(), "warn");
1432 assert_eq!(Severity::Off.to_string(), "off");
1433 }
1434
1435 #[test]
1436 fn apply_partial_all_none_changes_nothing() {
1437 let mut rules = RulesConfig::default();
1438 let original = rules.clone();
1439 let partial = PartialRulesConfig::default(); rules.apply_partial(&partial);
1441 assert_eq!(rules.unused_files, original.unused_files);
1442 assert_eq!(rules.unused_exports, original.unused_exports);
1443 assert_eq!(
1444 rules.type_only_dependencies,
1445 original.type_only_dependencies
1446 );
1447 }
1448
1449 #[test]
1450 fn apply_partial_all_fields_set() {
1451 let mut rules = RulesConfig::default();
1452 let partial = PartialRulesConfig {
1453 unused_files: Some(Severity::Off),
1454 unused_exports: Some(Severity::Off),
1455 unused_types: Some(Severity::Off),
1456 private_type_leaks: Some(Severity::Off),
1457 unused_dependencies: Some(Severity::Off),
1458 unused_dev_dependencies: Some(Severity::Off),
1459 unused_optional_dependencies: Some(Severity::Off),
1460 unused_enum_members: Some(Severity::Off),
1461 unused_class_members: Some(Severity::Off),
1462 unused_store_members: Some(Severity::Off),
1463 unprovided_injects: Some(Severity::Off),
1464 unrendered_components: Some(Severity::Off),
1465 unused_component_props: Some(Severity::Off),
1466 unused_component_emits: Some(Severity::Off),
1467 unused_component_inputs: Some(Severity::Off),
1468 unused_component_outputs: Some(Severity::Off),
1469 unused_svelte_events: Some(Severity::Off),
1470 unused_server_actions: Some(Severity::Off),
1471 unused_load_data_keys: Some(Severity::Off),
1472 prop_drilling: Some(Severity::Off),
1473 thin_wrapper: Some(Severity::Off),
1474 duplicate_prop_shape: Some(Severity::Off),
1475 css_token_drift: Some(Severity::Off),
1476 css_duplicate_block: Some(Severity::Off),
1477 css_selector_complexity: Some(Severity::Off),
1478 css_dead_surface: Some(Severity::Off),
1479 css_broken_reference: Some(Severity::Off),
1480 unresolved_imports: Some(Severity::Off),
1481 unlisted_dependencies: Some(Severity::Off),
1482 duplicate_exports: Some(Severity::Off),
1483 type_only_dependencies: Some(Severity::Off),
1484 test_only_dependencies: Some(Severity::Off),
1485 dev_dependencies_in_production: Some(Severity::Off),
1486 circular_dependencies: Some(Severity::Off),
1487 re_export_cycle: Some(Severity::Off),
1488 boundary_violation: Some(Severity::Off),
1489 coverage_gaps: Some(Severity::Off),
1490 feature_flags: Some(Severity::Off),
1491 stale_suppressions: Some(Severity::Off),
1492 require_suppression_reason: Some(Severity::Off),
1493 unused_catalog_entries: Some(Severity::Off),
1494 empty_catalog_groups: Some(Severity::Off),
1495 unresolved_catalog_references: Some(Severity::Off),
1496 unused_dependency_overrides: Some(Severity::Off),
1497 misconfigured_dependency_overrides: Some(Severity::Off),
1498 security_client_server_leak: Some(Severity::Off),
1499 security_sink: Some(Severity::Off),
1500 policy_violation: Some(Severity::Off),
1501 invalid_client_export: Some(Severity::Off),
1502 mixed_client_server_barrel: Some(Severity::Off),
1503 misplaced_directive: Some(Severity::Off),
1504 route_collision: Some(Severity::Off),
1505 dynamic_segment_name_conflict: Some(Severity::Off),
1506 };
1507 rules.apply_partial(&partial);
1508 assert_eq!(rules.unused_files, Severity::Off);
1509 assert_eq!(rules.private_type_leaks, Severity::Off);
1510 assert_eq!(rules.circular_dependencies, Severity::Off);
1511 assert_eq!(rules.type_only_dependencies, Severity::Off);
1512 assert_eq!(rules.test_only_dependencies, Severity::Off);
1513 assert_eq!(rules.boundary_violation, Severity::Off);
1514 assert_eq!(rules.coverage_gaps, Severity::Off);
1515 assert_eq!(rules.feature_flags, Severity::Off);
1516 assert_eq!(rules.stale_suppressions, Severity::Off);
1517 assert_eq!(rules.require_suppression_reason, Severity::Off);
1518 assert_eq!(rules.security_sink, Severity::Off);
1519 assert_eq!(rules.policy_violation, Severity::Off);
1520 assert_eq!(rules.invalid_client_export, Severity::Off);
1521 assert_eq!(rules.mixed_client_server_barrel, Severity::Off);
1522 assert_eq!(rules.misplaced_directive, Severity::Off);
1523 assert_eq!(rules.unrendered_components, Severity::Off);
1524 assert_eq!(rules.unused_component_props, Severity::Off);
1525 assert_eq!(rules.unused_component_emits, Severity::Off);
1526 assert_eq!(rules.unused_component_inputs, Severity::Off);
1527 assert_eq!(rules.unused_component_outputs, Severity::Off);
1528 assert_eq!(rules.unused_svelte_events, Severity::Off);
1529 assert_eq!(rules.route_collision, Severity::Off);
1530 assert_eq!(rules.dynamic_segment_name_conflict, Severity::Off);
1531 }
1532
1533 #[test]
1534 fn rules_config_defaults_include_optional_deps() {
1535 let rules = RulesConfig::default();
1536 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
1537 }
1538
1539 #[test]
1540 fn policy_violation_defaults_to_warn() {
1541 let rules = RulesConfig::default();
1542 assert_eq!(rules.policy_violation, Severity::Warn);
1543 }
1544
1545 #[test]
1546 fn policy_violation_accepts_plural_alias() {
1547 let json = r#"{ "policy-violations": "error" }"#;
1548 let rules: RulesConfig = serde_json::from_str(json).unwrap();
1549 assert_eq!(rules.policy_violation, Severity::Error);
1550 }
1551
1552 #[test]
1553 fn severity_from_str_case_insensitive() {
1554 assert_eq!("ERROR".parse::<Severity>().unwrap(), Severity::Error);
1555 assert_eq!("Warn".parse::<Severity>().unwrap(), Severity::Warn);
1556 assert_eq!("OFF".parse::<Severity>().unwrap(), Severity::Off);
1557 assert_eq!("Warning".parse::<Severity>().unwrap(), Severity::Warn);
1558 assert_eq!("NONE".parse::<Severity>().unwrap(), Severity::Off);
1559 }
1560
1561 #[test]
1562 fn severity_from_str_invalid_returns_error() {
1563 let result = "critical".parse::<Severity>();
1564 assert!(result.is_err());
1565 let err = result.unwrap_err();
1566 assert!(
1567 err.contains("unknown severity"),
1568 "Expected descriptive error, got: {err}"
1569 );
1570 }
1571
1572 #[test]
1573 fn known_rule_names_count_matches_struct() {
1574 assert_eq!(KNOWN_RULE_NAMES.len(), 98);
1575 }
1576
1577 #[test]
1578 fn known_rule_names_has_no_duplicates() {
1579 let mut sorted: Vec<&str> = KNOWN_RULE_NAMES.to_vec();
1580 sorted.sort_unstable();
1581 let original_len = sorted.len();
1582 sorted.dedup();
1583 assert_eq!(
1584 sorted.len(),
1585 original_len,
1586 "KNOWN_RULE_NAMES contains a duplicate"
1587 );
1588 }
1589
1590 #[test]
1591 fn known_rule_names_covers_every_serde_alias_in_source() {
1592 let source = include_str!("rules.rs");
1593
1594 let mut aliases_found = Vec::new();
1595 for line in source.lines() {
1596 let trimmed = line.trim();
1597 if trimmed.starts_with("//") {
1598 continue;
1599 }
1600 let Some(after) = trimmed.split("alias = \"").nth(1) else {
1601 continue;
1602 };
1603 let Some(end) = after.find('"') else {
1604 continue;
1605 };
1606 let alias = &after[..end];
1607 if alias.is_empty() || !alias.chars().all(|c| c.is_ascii_lowercase() || c == '-') {
1608 continue;
1609 }
1610 aliases_found.push(alias.to_owned());
1611 }
1612
1613 assert_eq!(
1614 aliases_found.len(),
1615 106,
1616 "expected 106 source-level alias attrs (53 per struct); got {}: {:?}",
1617 aliases_found.len(),
1618 aliases_found
1619 );
1620
1621 for alias in &aliases_found {
1622 assert!(
1623 KNOWN_RULE_NAMES.contains(&alias.as_str()),
1624 "serde alias '{alias}' is in rules.rs source but missing from KNOWN_RULE_NAMES"
1625 );
1626 }
1627 }
1628
1629 #[test]
1630 fn re_export_cycle_aliases_all_round_trip_to_the_same_field() {
1631 for alias in [
1632 "re-export-cycle",
1633 "re-export-cycles",
1634 "reexport-cycle",
1635 "reexport-cycles",
1636 ] {
1637 let json = format!(r#"{{"{alias}": "warn"}}"#);
1638 let partial: PartialRulesConfig = serde_json::from_str(&json)
1639 .unwrap_or_else(|e| panic!("'{alias}' should deserialize: {e}"));
1640 assert_eq!(
1641 partial.re_export_cycle,
1642 Some(Severity::Warn),
1643 "'{alias}' should set re_export_cycle to Warn"
1644 );
1645 let serialized = serde_json::to_value(&partial).unwrap();
1646 let map = serialized.as_object().unwrap();
1647 assert_eq!(
1648 map.len(),
1649 1,
1650 "'{alias}' should resolve to exactly one field, got: {map:?}"
1651 );
1652 }
1653 }
1654
1655 #[test]
1656 fn every_known_rule_name_round_trips_through_partial() {
1657 for &name in KNOWN_RULE_NAMES {
1658 let json = format!(r#"{{"{name}": "warn"}}"#);
1659 let partial: PartialRulesConfig = serde_json::from_str(&json)
1660 .unwrap_or_else(|e| panic!("'{name}' should deserialize: {e}"));
1661
1662 let serialized = serde_json::to_value(&partial).unwrap();
1663 let map = serialized.as_object().unwrap();
1664 assert_eq!(
1665 map.len(),
1666 1,
1667 "'{name}' should resolve to exactly one field, got: {map:?}"
1668 );
1669 }
1670 }
1671
1672 #[test]
1673 fn known_rule_names_covers_every_struct_field() {
1674 let json = serde_json::to_value(RulesConfig::default()).unwrap();
1675 let obj = json.as_object().unwrap();
1676 for key in obj.keys() {
1677 assert!(
1678 KNOWN_RULE_NAMES.contains(&key.as_str()),
1679 "field '{key}' is serialized but missing from KNOWN_RULE_NAMES"
1680 );
1681 }
1682 }
1683
1684 #[test]
1685 fn closest_known_rule_name_suggests_for_obvious_typo() {
1686 assert_eq!(
1687 closest_known_rule_name("unsued-files"),
1688 Some("unused-files")
1689 );
1690 assert_eq!(
1691 closest_known_rule_name("circular-dependnecy"),
1692 Some("circular-dependency")
1693 );
1694 assert_eq!(
1695 closest_known_rule_name("unused-dep"),
1696 None,
1697 "too short for a confident suggestion"
1698 );
1699 }
1700
1701 #[test]
1702 fn closest_known_rule_name_returns_none_for_novel_input() {
1703 assert_eq!(closest_known_rule_name("totally-fabricated"), None);
1704 assert_eq!(closest_known_rule_name("foo"), None);
1705 }
1706
1707 #[test]
1708 fn closest_known_rule_name_is_case_insensitive() {
1709 assert_eq!(
1710 closest_known_rule_name("UNSUED-FILES"),
1711 Some("unused-files")
1712 );
1713 }
1714
1715 #[test]
1716 fn closest_known_rule_name_returns_none_for_exact_match() {
1717 assert_eq!(closest_known_rule_name("unused-files"), None);
1718 }
1719
1720 #[test]
1721 fn find_unknown_rule_keys_flags_typo() {
1722 let v = serde_json::json!({
1723 "unsued-files": "warn",
1724 "unused-exports": "off",
1725 });
1726 let unknown = find_unknown_rule_keys(&v, "rules");
1727 assert_eq!(unknown.len(), 1);
1728 assert_eq!(unknown[0].key, "unsued-files");
1729 assert_eq!(unknown[0].context, "rules");
1730 assert_eq!(unknown[0].suggestion, Some("unused-files"));
1731 }
1732
1733 #[test]
1734 fn find_unknown_rule_keys_passes_aliases() {
1735 let v = serde_json::json!({
1736 "unused-file": "warn",
1737 "circular-dependency": "off",
1738 "boundary-violations": "warn",
1739 });
1740 let unknown = find_unknown_rule_keys(&v, "rules");
1741 assert!(
1742 unknown.is_empty(),
1743 "documented aliases must not flag as unknown: {unknown:?}"
1744 );
1745 }
1746
1747 #[test]
1748 fn find_unknown_rule_keys_returns_multiple_typos() {
1749 let v = serde_json::json!({
1750 "unsued-files": "warn",
1751 "circular-dependnecy": "off",
1752 });
1753 let unknown = find_unknown_rule_keys(&v, "rules");
1754 assert_eq!(unknown.len(), 2);
1755 }
1756
1757 #[test]
1758 fn find_unknown_rule_keys_carries_context() {
1759 let v = serde_json::json!({ "unsued-files": "warn" });
1760 let unknown = find_unknown_rule_keys(&v, "overrides[2].rules");
1761 assert_eq!(unknown[0].context, "overrides[2].rules");
1762 }
1763
1764 #[test]
1765 fn find_unknown_rule_keys_empty_when_not_object() {
1766 let v = serde_json::json!(null);
1767 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
1768
1769 let v = serde_json::json!([1, 2, 3]);
1770 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
1771 }
1772
1773 #[test]
1774 fn find_unknown_rule_keys_no_suggestion_for_novel_name() {
1775 let v = serde_json::json!({ "totally-fabricated-rule": "warn" });
1776 let unknown = find_unknown_rule_keys(&v, "rules");
1777 assert_eq!(unknown.len(), 1);
1778 assert_eq!(unknown[0].suggestion, None);
1779 }
1780
1781 #[test]
1782 fn partial_rules_empty_json() {
1783 let partial: PartialRulesConfig = serde_json::from_str("{}").unwrap();
1784 assert!(partial.unused_files.is_none());
1785 assert!(partial.unused_exports.is_none());
1786 assert!(partial.unused_types.is_none());
1787 assert!(partial.unused_dependencies.is_none());
1788 assert!(partial.circular_dependencies.is_none());
1789 assert!(partial.boundary_violation.is_none());
1790 assert!(partial.coverage_gaps.is_none());
1791 assert!(partial.feature_flags.is_none());
1792 assert!(partial.stale_suppressions.is_none());
1793 }
1794
1795 #[test]
1796 fn partial_rules_subset_json() {
1797 let json = r#"{
1798 "unused-files": "warn",
1799 "circular-dependencies": "off"
1800 }"#;
1801 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1802 assert_eq!(partial.unused_files, Some(Severity::Warn));
1803 assert_eq!(partial.circular_dependencies, Some(Severity::Off));
1804 assert!(partial.unused_exports.is_none());
1805 }
1806
1807 #[test]
1808 fn partial_rules_deserialize_circular_dependency_alias() {
1809 let json = r#"{
1810 "circular-dependency": "warn"
1811 }"#;
1812 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1813 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
1814 }
1815
1816 #[test]
1817 fn partial_rules_all_fields_json() {
1818 let json = r#"{
1819 "unused-files": "error",
1820 "unused-exports": "warn",
1821 "unused-types": "off",
1822 "unused-dependencies": "error",
1823 "unused-dev-dependencies": "warn",
1824 "unused-optional-dependencies": "off",
1825 "unused-enum-members": "error",
1826 "unused-class-members": "warn",
1827 "unresolved-imports": "off",
1828 "unlisted-dependencies": "error",
1829 "duplicate-exports": "warn",
1830 "type-only-dependencies": "off",
1831 "test-only-dependencies": "error",
1832 "circular-dependencies": "warn",
1833 "boundary-violation": "off",
1834 "coverage-gaps": "warn",
1835 "feature-flags": "error",
1836 "stale-suppressions": "off"
1837 }"#;
1838 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1839 assert_eq!(partial.unused_files, Some(Severity::Error));
1840 assert_eq!(partial.unused_exports, Some(Severity::Warn));
1841 assert_eq!(partial.unused_types, Some(Severity::Off));
1842 assert_eq!(partial.unused_dependencies, Some(Severity::Error));
1843 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Warn));
1844 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
1845 assert_eq!(partial.unused_enum_members, Some(Severity::Error));
1846 assert_eq!(partial.unused_class_members, Some(Severity::Warn));
1847 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
1848 assert_eq!(partial.unlisted_dependencies, Some(Severity::Error));
1849 assert_eq!(partial.duplicate_exports, Some(Severity::Warn));
1850 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
1851 assert_eq!(partial.test_only_dependencies, Some(Severity::Error));
1852 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
1853 assert_eq!(partial.boundary_violation, Some(Severity::Off));
1854 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
1855 assert_eq!(partial.feature_flags, Some(Severity::Error));
1856 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
1857 }
1858
1859 #[test]
1860 fn partial_rules_none_fields_not_serialized() {
1861 let partial = PartialRulesConfig::default();
1862 let json = serde_json::to_string(&partial).unwrap();
1863 assert_eq!(
1864 json, "{}",
1865 "all-None partial should serialize to empty object"
1866 );
1867 }
1868
1869 #[test]
1870 fn partial_rules_some_fields_serialized() {
1871 let partial = PartialRulesConfig {
1872 unused_files: Some(Severity::Warn),
1873 ..Default::default()
1874 };
1875 let json = serde_json::to_string(&partial).unwrap();
1876 assert!(json.contains("unused-files"));
1877 assert!(!json.contains("unused-exports"));
1878 }
1879
1880 #[test]
1881 fn severity_json_deserialization() {
1882 let error: Severity = serde_json::from_str(r#""error""#).unwrap();
1883 assert_eq!(error, Severity::Error);
1884
1885 let warn: Severity = serde_json::from_str(r#""warn""#).unwrap();
1886 assert_eq!(warn, Severity::Warn);
1887
1888 let off: Severity = serde_json::from_str(r#""off""#).unwrap();
1889 assert_eq!(off, Severity::Off);
1890 }
1891
1892 #[test]
1893 fn severity_invalid_json_value_rejected() {
1894 let result: Result<Severity, _> = serde_json::from_str(r#""critical""#);
1895 assert!(result.is_err());
1896 }
1897
1898 #[test]
1899 fn severity_default_is_error() {
1900 assert_eq!(Severity::default(), Severity::Error);
1901 }
1902
1903 #[test]
1904 fn rules_config_json_roundtrip() {
1905 let rules = RulesConfig {
1906 unused_files: Severity::Warn,
1907 unused_exports: Severity::Off,
1908 type_only_dependencies: Severity::Error,
1909 ..RulesConfig::default()
1910 };
1911 let json = serde_json::to_string(&rules).unwrap();
1912 let restored: RulesConfig = serde_json::from_str(&json).unwrap();
1913 assert_eq!(restored.unused_files, Severity::Warn);
1914 assert_eq!(restored.unused_exports, Severity::Off);
1915 assert_eq!(restored.type_only_dependencies, Severity::Error);
1916 assert_eq!(restored.unused_dependencies, Severity::Error); }
1918
1919 #[test]
1920 fn apply_partial_preserves_type_only_default() {
1921 let mut rules = RulesConfig::default();
1922 let partial = PartialRulesConfig {
1923 unused_files: Some(Severity::Off),
1924 ..Default::default()
1925 };
1926 rules.apply_partial(&partial);
1927 assert_eq!(rules.type_only_dependencies, Severity::Warn);
1928 assert_eq!(rules.test_only_dependencies, Severity::Warn);
1929 }
1930}