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