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