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 = "Severity::default_off", alias = "deprecated-export-in-use")]
98 pub deprecated_exports_in_use: Severity,
99 #[serde(default, alias = "unused-dependency")]
102 pub unused_dependencies: Severity,
103 #[serde(default = "Severity::default_warn", alias = "unused-dev-dependency")]
106 pub unused_dev_dependencies: Severity,
107 #[serde(
110 default = "Severity::default_warn",
111 alias = "unused-optional-dependency"
112 )]
113 pub unused_optional_dependencies: Severity,
114 #[serde(default, alias = "unused-enum-member")]
116 pub unused_enum_members: Severity,
117 #[serde(default, alias = "unused-class-member")]
120 pub unused_class_members: Severity,
121 #[serde(default = "Severity::default_warn", alias = "unused-store-member")]
129 pub unused_store_members: Severity,
130 #[serde(default = "Severity::default_warn", alias = "unprovided-inject")]
136 pub unprovided_injects: Severity,
137 #[serde(default = "Severity::default_warn", alias = "unrendered-component")]
143 pub unrendered_components: Severity,
144 #[serde(default = "Severity::default_warn", alias = "unused-component-prop")]
150 pub unused_component_props: Severity,
151 #[serde(default = "Severity::default_warn", alias = "unused-component-emit")]
157 pub unused_component_emits: Severity,
158 #[serde(default = "Severity::default_warn", alias = "unused-component-input")]
165 pub unused_component_inputs: Severity,
166 #[serde(default = "Severity::default_warn", alias = "unused-component-output")]
173 pub unused_component_outputs: Severity,
174 #[serde(default = "Severity::default_warn", alias = "unused-svelte-event")]
182 pub unused_svelte_events: Severity,
183 #[serde(default = "Severity::default_warn", alias = "unused-server-action")]
191 pub unused_server_actions: Severity,
192 #[serde(default = "Severity::default_warn", alias = "unused-load-data-key")]
200 pub unused_load_data_keys: Severity,
201 #[serde(default = "Severity::default_off", alias = "prop-drilling")]
207 pub prop_drilling: Severity,
208 #[serde(default = "Severity::default_off", alias = "thin-wrapper")]
213 pub thin_wrapper: Severity,
214 #[serde(default = "Severity::default_off", alias = "duplicate-prop-shape")]
221 pub duplicate_prop_shape: Severity,
222 #[serde(default = "Severity::default_warn", alias = "css-token-drift")]
227 pub css_token_drift: Severity,
228 #[serde(default = "Severity::default_warn", alias = "css-duplicate-block")]
232 pub css_duplicate_block: Severity,
233 #[serde(default = "Severity::default_warn", alias = "css-selector-complexity")]
237 pub css_selector_complexity: Severity,
238 #[serde(default = "Severity::default_warn", alias = "css-dead-surface")]
242 pub css_dead_surface: Severity,
243 #[serde(default = "Severity::default_warn", alias = "css-broken-reference")]
247 pub css_broken_reference: Severity,
248 #[serde(default)]
257 pub complexity_cyclomatic: Severity,
258 #[serde(default)]
262 pub complexity_cognitive: Severity,
263 #[serde(default)]
269 pub complexity_crap: Severity,
270 #[serde(default, alias = "unresolved-import")]
273 pub unresolved_imports: Severity,
274 #[serde(default, alias = "unlisted-dependency")]
277 pub unlisted_dependencies: Severity,
278 #[serde(default, alias = "duplicate-export")]
281 pub duplicate_exports: Severity,
282 #[serde(default = "Severity::default_warn", alias = "type-only-dependency")]
285 pub type_only_dependencies: Severity,
286 #[serde(default = "Severity::default_warn", alias = "test-only-dependency")]
289 pub test_only_dependencies: Severity,
290 #[serde(
294 default = "Severity::default_warn",
295 alias = "dev-dependency-in-production"
296 )]
297 pub dev_dependencies_in_production: Severity,
298 #[serde(default, alias = "circular-dependency")]
300 pub circular_dependencies: Severity,
301 #[serde(
304 default = "Severity::default_warn",
305 alias = "re-export-cycles",
306 alias = "reexport-cycle",
307 alias = "reexport-cycles"
308 )]
309 pub re_export_cycle: Severity,
310 #[serde(default, alias = "boundary-violations")]
313 pub boundary_violation: Severity,
314 #[serde(default = "Severity::default_off", alias = "coverage-gap")]
317 pub coverage_gaps: Severity,
318 #[serde(default = "Severity::default_off", alias = "feature-flag")]
321 pub feature_flags: Severity,
322 #[serde(default = "Severity::default_warn", alias = "stale-suppression")]
325 pub stale_suppressions: Severity,
326 #[serde(default = "Severity::default_off", alias = "suppression-reason")]
329 pub require_suppression_reason: Severity,
330 #[serde(default = "Severity::default_warn", alias = "unused-catalog-entry")]
333 pub unused_catalog_entries: Severity,
334 #[serde(default = "Severity::default_warn", alias = "empty-catalog-group")]
336 pub empty_catalog_groups: Severity,
337 #[serde(default, alias = "unresolved-catalog-reference")]
341 pub unresolved_catalog_references: Severity,
342 #[serde(
346 default = "Severity::default_warn",
347 alias = "unused-dependency-override"
348 )]
349 pub unused_dependency_overrides: Severity,
350 #[serde(default, alias = "misconfigured-dependency-override")]
354 pub misconfigured_dependency_overrides: Severity,
355 #[serde(default = "Severity::default_off")]
359 pub security_client_server_leak: Severity,
360 #[serde(default = "Severity::default_off")]
365 pub security_sink: Severity,
366 #[serde(default = "Severity::default_warn", alias = "policy-violations")]
372 pub policy_violation: Severity,
373 #[serde(default = "Severity::default_warn", alias = "invalid-client-exports")]
378 pub invalid_client_export: Severity,
379 #[serde(
384 default = "Severity::default_warn",
385 alias = "mixed-client-server-barrels"
386 )]
387 pub mixed_client_server_barrel: Severity,
388 #[serde(default = "Severity::default_warn", alias = "misplaced-directives")]
394 pub misplaced_directive: Severity,
395 #[serde(default, alias = "route-collisions")]
402 pub route_collision: Severity,
403 #[serde(default, alias = "dynamic-segment-name-conflicts")]
413 pub dynamic_segment_name_conflict: Severity,
414}
415
416impl Default for RulesConfig {
417 fn default() -> Self {
418 Self {
419 unused_files: Severity::Error,
420 unused_exports: Severity::Error,
421 unused_types: Severity::Error,
422 private_type_leaks: Severity::Off,
423 private_type_leaks_configured: false,
424 deprecated_exports_in_use: Severity::Off,
425 unused_dependencies: Severity::Error,
426 unused_dev_dependencies: Severity::Warn,
427 unused_optional_dependencies: Severity::Warn,
428 unused_enum_members: Severity::Error,
429 unused_class_members: Severity::Error,
430 unused_store_members: Severity::Warn,
431 unprovided_injects: Severity::Warn,
432 unrendered_components: Severity::Warn,
433 unused_component_props: Severity::Warn,
434 unused_component_emits: Severity::Warn,
435 unused_component_inputs: Severity::Warn,
436 unused_component_outputs: Severity::Warn,
437 unused_svelte_events: Severity::Warn,
438 unused_server_actions: Severity::Warn,
439 unused_load_data_keys: Severity::Warn,
440 prop_drilling: Severity::Off,
441 thin_wrapper: Severity::Off,
442 duplicate_prop_shape: Severity::Off,
443 css_token_drift: Severity::Warn,
444 css_duplicate_block: Severity::Warn,
445 css_selector_complexity: Severity::Warn,
446 css_dead_surface: Severity::Warn,
447 css_broken_reference: Severity::Warn,
448 complexity_cyclomatic: Severity::Error,
449 complexity_cognitive: Severity::Error,
450 complexity_crap: Severity::Error,
451 unresolved_imports: Severity::Error,
452 unlisted_dependencies: Severity::Error,
453 duplicate_exports: Severity::Error,
454 type_only_dependencies: Severity::Warn,
455 test_only_dependencies: Severity::Warn,
456 dev_dependencies_in_production: Severity::Warn,
457 circular_dependencies: Severity::Error,
458 re_export_cycle: Severity::Warn,
459 boundary_violation: Severity::Error,
460 coverage_gaps: Severity::Off,
461 feature_flags: Severity::Off,
462 stale_suppressions: Severity::Warn,
463 require_suppression_reason: Severity::Off,
464 unused_catalog_entries: Severity::Warn,
465 empty_catalog_groups: Severity::Warn,
466 unresolved_catalog_references: Severity::Error,
467 unused_dependency_overrides: Severity::Warn,
468 misconfigured_dependency_overrides: Severity::Error,
469 security_client_server_leak: Severity::Off,
470 security_sink: Severity::Off,
471 policy_violation: Severity::Warn,
472 invalid_client_export: Severity::Warn,
473 mixed_client_server_barrel: Severity::Warn,
474 misplaced_directive: Severity::Warn,
475 route_collision: Severity::Error,
476 dynamic_segment_name_conflict: Severity::Error,
477 }
478 }
479}
480
481macro_rules! apply_partial_rules {
482 ($target:expr, $partial:expr, [$($field:ident),+ $(,)?]) => {
483 $(
484 if let Some(severity) = $partial.$field {
485 $target.$field = severity;
486 }
487 )+
488 };
489}
490
491impl RulesConfig {
492 #[must_use]
504 pub const fn severity_for_kind(&self, kind: IssueKind) -> Severity {
505 match kind {
506 IssueKind::UnusedFile => self.unused_files,
507 IssueKind::UnusedExport => self.unused_exports,
508 IssueKind::UnusedType => self.unused_types,
509 IssueKind::PrivateTypeLeak => self.private_type_leaks,
510 IssueKind::DeprecatedExportInUse => self.deprecated_exports_in_use,
511 IssueKind::UnusedDependency => self.unused_dependencies,
512 IssueKind::UnusedDevDependency => self.unused_dev_dependencies,
513 IssueKind::UnusedEnumMember => self.unused_enum_members,
514 IssueKind::UnusedClassMember => self.unused_class_members,
515 IssueKind::UnusedStoreMember => self.unused_store_members,
516 IssueKind::UnprovidedInject => self.unprovided_injects,
517 IssueKind::UnresolvedImport => self.unresolved_imports,
518 IssueKind::UnlistedDependency => self.unlisted_dependencies,
519 IssueKind::DuplicateExport => self.duplicate_exports,
520 IssueKind::CircularDependency => self.circular_dependencies,
521 IssueKind::ReExportCycle => self.re_export_cycle,
522 IssueKind::TypeOnlyDependency => self.type_only_dependencies,
523 IssueKind::TestOnlyDependency => self.test_only_dependencies,
524 IssueKind::DevDependencyInProduction => self.dev_dependencies_in_production,
525 IssueKind::BoundaryViolation => self.boundary_violation,
526 IssueKind::CoverageGaps => self.coverage_gaps,
527 IssueKind::FeatureFlag => self.feature_flags,
528 IssueKind::StaleSuppression => self.stale_suppressions,
529 IssueKind::PnpmCatalogEntry => self.unused_catalog_entries,
530 IssueKind::EmptyCatalogGroup => self.empty_catalog_groups,
531 IssueKind::UnresolvedCatalogReference => self.unresolved_catalog_references,
532 IssueKind::UnusedDependencyOverride => self.unused_dependency_overrides,
533 IssueKind::MisconfiguredDependencyOverride => self.misconfigured_dependency_overrides,
534 IssueKind::SecurityClientServerLeak => self.security_client_server_leak,
535 IssueKind::SecuritySink => self.security_sink,
536 IssueKind::PolicyViolation => self.policy_violation,
537 IssueKind::InvalidClientExport => self.invalid_client_export,
538 IssueKind::MixedClientServerBarrel => self.mixed_client_server_barrel,
539 IssueKind::MisplacedDirective => self.misplaced_directive,
540 IssueKind::RouteCollision => self.route_collision,
541 IssueKind::DynamicSegmentNameConflict => self.dynamic_segment_name_conflict,
542 IssueKind::UnrenderedComponent => self.unrendered_components,
543 IssueKind::UnusedComponentProp => self.unused_component_props,
544 IssueKind::UnusedComponentEmit => self.unused_component_emits,
545 IssueKind::UnusedComponentInput => self.unused_component_inputs,
546 IssueKind::UnusedComponentOutput => self.unused_component_outputs,
547 IssueKind::UnusedSvelteEvent => self.unused_svelte_events,
548 IssueKind::UnusedServerAction => self.unused_server_actions,
549 IssueKind::UnusedLoadDataKey => self.unused_load_data_keys,
550 IssueKind::PropDrilling => self.prop_drilling,
551 IssueKind::ThinWrapper => self.thin_wrapper,
552 IssueKind::DuplicatePropShape => self.duplicate_prop_shape,
553 IssueKind::CssTokenDrift => self.css_token_drift,
554 IssueKind::CssDuplicateBlock => self.css_duplicate_block,
555 IssueKind::CssSelectorComplexity => self.css_selector_complexity,
556 IssueKind::CssDeadSurface => self.css_dead_surface,
557 IssueKind::CssBrokenReference => self.css_broken_reference,
558 IssueKind::Complexity | IssueKind::CodeDuplication => Severity::Error,
559 }
560 }
561
562 #[must_use]
569 pub fn complexity_severity(&self, cyclomatic: bool, cognitive: bool, crap: bool) -> Severity {
570 [
571 (cyclomatic, self.complexity_cyclomatic),
572 (cognitive, self.complexity_cognitive),
573 (crap, self.complexity_crap),
574 ]
575 .into_iter()
576 .filter_map(|(contributed, severity)| contributed.then_some(severity))
577 .max_by_key(|severity| match severity {
578 Severity::Error => 2,
579 Severity::Warn => 1,
580 Severity::Off => 0,
581 })
582 .unwrap_or(Severity::Off)
583 }
584
585 pub const fn apply_partial(&mut self, partial: &PartialRulesConfig) {
587 apply_partial_rules!(
588 self,
589 partial,
590 [
591 unused_files,
592 unused_exports,
593 unused_types,
594 private_type_leaks,
595 deprecated_exports_in_use,
596 unused_dependencies,
597 unused_dev_dependencies,
598 unused_optional_dependencies,
599 ]
600 );
601 apply_partial_rules!(
602 self,
603 partial,
604 [
605 unused_enum_members,
606 unused_class_members,
607 unused_store_members,
608 unprovided_injects,
609 unrendered_components,
610 unused_component_props,
611 unused_component_emits,
612 unused_component_inputs,
613 unused_component_outputs,
614 unused_svelte_events,
615 unused_server_actions,
616 unused_load_data_keys,
617 prop_drilling,
618 thin_wrapper,
619 duplicate_prop_shape,
620 css_token_drift,
621 css_duplicate_block,
622 css_selector_complexity,
623 css_dead_surface,
624 css_broken_reference,
625 complexity_cyclomatic,
626 complexity_cognitive,
627 complexity_crap,
628 ]
629 );
630 apply_partial_rules!(
631 self,
632 partial,
633 [
634 unresolved_imports,
635 unlisted_dependencies,
636 duplicate_exports,
637 type_only_dependencies,
638 test_only_dependencies,
639 dev_dependencies_in_production,
640 circular_dependencies,
641 re_export_cycle,
642 boundary_violation,
643 ]
644 );
645 apply_partial_rules!(
646 self,
647 partial,
648 [
649 coverage_gaps,
650 feature_flags,
651 stale_suppressions,
652 require_suppression_reason,
653 unused_catalog_entries,
654 empty_catalog_groups,
655 unresolved_catalog_references,
656 unused_dependency_overrides,
657 misconfigured_dependency_overrides,
658 ]
659 );
660 apply_partial_rules!(
661 self,
662 partial,
663 [
664 security_client_server_leak,
665 security_sink,
666 policy_violation,
667 invalid_client_export,
668 mixed_client_server_barrel,
669 misplaced_directive,
670 route_collision,
671 dynamic_segment_name_conflict,
672 ]
673 );
674 }
675}
676
677#[must_use]
685pub fn default_severity_for_kind(kind: IssueKind) -> Severity {
686 RulesConfig::default().severity_for_kind(kind)
687}
688
689#[must_use]
696pub fn is_opt_in_kind(kind: IssueKind) -> bool {
697 matches!(default_severity_for_kind(kind), Severity::Off)
698}
699
700#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
702#[serde(rename_all = "kebab-case")]
703pub struct PartialRulesConfig {
704 #[serde(
706 default,
707 alias = "unused-file",
708 skip_serializing_if = "Option::is_none"
709 )]
710 pub unused_files: Option<Severity>,
711 #[serde(
713 default,
714 alias = "unused-export",
715 skip_serializing_if = "Option::is_none"
716 )]
717 pub unused_exports: Option<Severity>,
718 #[serde(
720 default,
721 alias = "unused-type",
722 skip_serializing_if = "Option::is_none"
723 )]
724 pub unused_types: Option<Severity>,
725 #[serde(
727 default,
728 alias = "private-type-leak",
729 skip_serializing_if = "Option::is_none"
730 )]
731 pub private_type_leaks: Option<Severity>,
732 #[serde(
734 default,
735 alias = "deprecated-export-in-use",
736 skip_serializing_if = "Option::is_none"
737 )]
738 pub deprecated_exports_in_use: Option<Severity>,
739 #[serde(
741 default,
742 alias = "unused-dependency",
743 skip_serializing_if = "Option::is_none"
744 )]
745 pub unused_dependencies: Option<Severity>,
746 #[serde(
748 default,
749 alias = "unused-dev-dependency",
750 skip_serializing_if = "Option::is_none"
751 )]
752 pub unused_dev_dependencies: Option<Severity>,
753 #[serde(
755 default,
756 alias = "unused-optional-dependency",
757 skip_serializing_if = "Option::is_none"
758 )]
759 pub unused_optional_dependencies: Option<Severity>,
760 #[serde(
762 default,
763 alias = "unused-enum-member",
764 skip_serializing_if = "Option::is_none"
765 )]
766 pub unused_enum_members: Option<Severity>,
767 #[serde(
769 default,
770 alias = "unused-class-member",
771 skip_serializing_if = "Option::is_none"
772 )]
773 pub unused_class_members: Option<Severity>,
774 #[serde(
776 default,
777 alias = "unused-store-member",
778 skip_serializing_if = "Option::is_none"
779 )]
780 pub unused_store_members: Option<Severity>,
781 #[serde(
783 default,
784 alias = "unprovided-inject",
785 skip_serializing_if = "Option::is_none"
786 )]
787 pub unprovided_injects: Option<Severity>,
788 #[serde(
790 default,
791 alias = "unrendered-component",
792 skip_serializing_if = "Option::is_none"
793 )]
794 pub unrendered_components: Option<Severity>,
795 #[serde(
797 default,
798 alias = "unused-component-prop",
799 skip_serializing_if = "Option::is_none"
800 )]
801 pub unused_component_props: Option<Severity>,
802 #[serde(
804 default,
805 alias = "unused-component-emit",
806 skip_serializing_if = "Option::is_none"
807 )]
808 pub unused_component_emits: Option<Severity>,
809 #[serde(
811 default,
812 alias = "unused-component-input",
813 skip_serializing_if = "Option::is_none"
814 )]
815 pub unused_component_inputs: Option<Severity>,
816 #[serde(
818 default,
819 alias = "unused-component-output",
820 skip_serializing_if = "Option::is_none"
821 )]
822 pub unused_component_outputs: Option<Severity>,
823 #[serde(
825 default,
826 alias = "unused-svelte-event",
827 skip_serializing_if = "Option::is_none"
828 )]
829 pub unused_svelte_events: Option<Severity>,
830 #[serde(
832 default,
833 alias = "unused-server-action",
834 skip_serializing_if = "Option::is_none"
835 )]
836 pub unused_server_actions: Option<Severity>,
837 #[serde(
839 default,
840 alias = "unused-load-data-key",
841 skip_serializing_if = "Option::is_none"
842 )]
843 pub unused_load_data_keys: Option<Severity>,
844 #[serde(
846 default,
847 alias = "prop-drilling",
848 skip_serializing_if = "Option::is_none"
849 )]
850 pub prop_drilling: Option<Severity>,
851 #[serde(
853 default,
854 alias = "thin-wrapper",
855 skip_serializing_if = "Option::is_none"
856 )]
857 pub thin_wrapper: Option<Severity>,
858 #[serde(
860 default,
861 alias = "duplicate-prop-shape",
862 skip_serializing_if = "Option::is_none"
863 )]
864 pub duplicate_prop_shape: Option<Severity>,
865 #[serde(
867 default,
868 alias = "css-token-drift",
869 skip_serializing_if = "Option::is_none"
870 )]
871 pub css_token_drift: Option<Severity>,
872 #[serde(
874 default,
875 alias = "css-duplicate-block",
876 skip_serializing_if = "Option::is_none"
877 )]
878 pub css_duplicate_block: Option<Severity>,
879 #[serde(
881 default,
882 alias = "css-selector-complexity",
883 skip_serializing_if = "Option::is_none"
884 )]
885 pub css_selector_complexity: Option<Severity>,
886 #[serde(
888 default,
889 alias = "css-dead-surface",
890 skip_serializing_if = "Option::is_none"
891 )]
892 pub css_dead_surface: Option<Severity>,
893 #[serde(
895 default,
896 alias = "css-broken-reference",
897 skip_serializing_if = "Option::is_none"
898 )]
899 pub css_broken_reference: Option<Severity>,
900 #[serde(default, skip_serializing_if = "Option::is_none")]
902 pub complexity_cyclomatic: Option<Severity>,
903 #[serde(default, skip_serializing_if = "Option::is_none")]
905 pub complexity_cognitive: Option<Severity>,
906 #[serde(default, skip_serializing_if = "Option::is_none")]
908 pub complexity_crap: Option<Severity>,
909 #[serde(
911 default,
912 alias = "unresolved-import",
913 skip_serializing_if = "Option::is_none"
914 )]
915 pub unresolved_imports: Option<Severity>,
916 #[serde(
918 default,
919 alias = "unlisted-dependency",
920 skip_serializing_if = "Option::is_none"
921 )]
922 pub unlisted_dependencies: Option<Severity>,
923 #[serde(
925 default,
926 alias = "duplicate-export",
927 skip_serializing_if = "Option::is_none"
928 )]
929 pub duplicate_exports: Option<Severity>,
930 #[serde(
932 default,
933 alias = "type-only-dependency",
934 skip_serializing_if = "Option::is_none"
935 )]
936 pub type_only_dependencies: Option<Severity>,
937 #[serde(
939 default,
940 alias = "test-only-dependency",
941 skip_serializing_if = "Option::is_none"
942 )]
943 pub test_only_dependencies: Option<Severity>,
944 #[serde(
946 default,
947 alias = "dev-dependency-in-production",
948 skip_serializing_if = "Option::is_none"
949 )]
950 pub dev_dependencies_in_production: Option<Severity>,
951 #[serde(
953 default,
954 alias = "circular-dependency",
955 skip_serializing_if = "Option::is_none"
956 )]
957 pub circular_dependencies: Option<Severity>,
958 #[serde(
960 default,
961 alias = "re-export-cycles",
962 alias = "reexport-cycle",
963 alias = "reexport-cycles",
964 skip_serializing_if = "Option::is_none"
965 )]
966 pub re_export_cycle: Option<Severity>,
967 #[serde(
969 default,
970 alias = "boundary-violations",
971 skip_serializing_if = "Option::is_none"
972 )]
973 pub boundary_violation: Option<Severity>,
974 #[serde(
976 default,
977 alias = "coverage-gap",
978 skip_serializing_if = "Option::is_none"
979 )]
980 pub coverage_gaps: Option<Severity>,
981 #[serde(
983 default,
984 alias = "feature-flag",
985 skip_serializing_if = "Option::is_none"
986 )]
987 pub feature_flags: Option<Severity>,
988 #[serde(
990 default,
991 alias = "stale-suppression",
992 skip_serializing_if = "Option::is_none"
993 )]
994 pub stale_suppressions: Option<Severity>,
995 #[serde(
997 default,
998 alias = "suppression-reason",
999 skip_serializing_if = "Option::is_none"
1000 )]
1001 pub require_suppression_reason: Option<Severity>,
1002 #[serde(
1004 default,
1005 alias = "unused-catalog-entry",
1006 skip_serializing_if = "Option::is_none"
1007 )]
1008 pub unused_catalog_entries: Option<Severity>,
1009 #[serde(
1011 default,
1012 alias = "empty-catalog-group",
1013 skip_serializing_if = "Option::is_none"
1014 )]
1015 pub empty_catalog_groups: Option<Severity>,
1016 #[serde(
1018 default,
1019 alias = "unresolved-catalog-reference",
1020 skip_serializing_if = "Option::is_none"
1021 )]
1022 pub unresolved_catalog_references: Option<Severity>,
1023 #[serde(
1025 default,
1026 alias = "unused-dependency-override",
1027 skip_serializing_if = "Option::is_none"
1028 )]
1029 pub unused_dependency_overrides: Option<Severity>,
1030 #[serde(
1032 default,
1033 alias = "misconfigured-dependency-override",
1034 skip_serializing_if = "Option::is_none"
1035 )]
1036 pub misconfigured_dependency_overrides: Option<Severity>,
1037 #[serde(default, skip_serializing_if = "Option::is_none")]
1039 pub security_client_server_leak: Option<Severity>,
1040 #[serde(default, skip_serializing_if = "Option::is_none")]
1042 pub security_sink: Option<Severity>,
1043 #[serde(
1045 default,
1046 alias = "policy-violations",
1047 skip_serializing_if = "Option::is_none"
1048 )]
1049 pub policy_violation: Option<Severity>,
1050 #[serde(
1052 default,
1053 alias = "invalid-client-exports",
1054 skip_serializing_if = "Option::is_none"
1055 )]
1056 pub invalid_client_export: Option<Severity>,
1057 #[serde(
1059 default,
1060 alias = "mixed-client-server-barrels",
1061 skip_serializing_if = "Option::is_none"
1062 )]
1063 pub mixed_client_server_barrel: Option<Severity>,
1064 #[serde(
1066 default,
1067 alias = "misplaced-directives",
1068 skip_serializing_if = "Option::is_none"
1069 )]
1070 pub misplaced_directive: Option<Severity>,
1071 #[serde(
1073 default,
1074 alias = "route-collisions",
1075 skip_serializing_if = "Option::is_none"
1076 )]
1077 pub route_collision: Option<Severity>,
1078 #[serde(
1080 default,
1081 alias = "dynamic-segment-name-conflicts",
1082 skip_serializing_if = "Option::is_none"
1083 )]
1084 pub dynamic_segment_name_conflict: Option<Severity>,
1085}
1086
1087pub const KNOWN_RULE_NAMES: &[&str] = &[
1102 "unused-files",
1103 "unused-exports",
1104 "unused-types",
1105 "private-type-leaks",
1106 "deprecated-exports-in-use",
1107 "unused-dependencies",
1108 "unused-dev-dependencies",
1109 "unused-optional-dependencies",
1110 "unused-enum-members",
1111 "unused-class-members",
1112 "unused-store-members",
1113 "unprovided-injects",
1114 "unrendered-components",
1115 "unused-component-props",
1116 "unused-component-emits",
1117 "unused-component-inputs",
1118 "unused-component-outputs",
1119 "unused-svelte-events",
1120 "unused-server-actions",
1121 "unused-load-data-keys",
1122 "prop-drilling",
1123 "thin-wrapper",
1124 "duplicate-prop-shape",
1125 "css-token-drift",
1126 "css-duplicate-block",
1127 "css-selector-complexity",
1128 "css-dead-surface",
1129 "css-broken-reference",
1130 "complexity-cyclomatic",
1131 "complexity-cognitive",
1132 "complexity-crap",
1133 "unresolved-imports",
1134 "unlisted-dependencies",
1135 "duplicate-exports",
1136 "type-only-dependencies",
1137 "test-only-dependencies",
1138 "dev-dependencies-in-production",
1139 "circular-dependencies",
1140 "re-export-cycle",
1141 "boundary-violation",
1142 "coverage-gaps",
1143 "feature-flags",
1144 "stale-suppressions",
1145 "require-suppression-reason",
1146 "suppression-reason",
1147 "unused-catalog-entries",
1148 "empty-catalog-groups",
1149 "unresolved-catalog-references",
1150 "unused-dependency-overrides",
1151 "misconfigured-dependency-overrides",
1152 "security-client-server-leak",
1153 "security-sink",
1154 "policy-violation",
1155 "policy-violations",
1156 "invalid-client-export",
1157 "mixed-client-server-barrel",
1158 "misplaced-directive",
1159 "route-collision",
1160 "dynamic-segment-name-conflict",
1161 "unused-file",
1162 "unused-export",
1163 "unused-type",
1164 "private-type-leak",
1165 "deprecated-export-in-use",
1166 "unused-dependency",
1167 "unused-dev-dependency",
1168 "unused-optional-dependency",
1169 "unused-enum-member",
1170 "unused-class-member",
1171 "unused-store-member",
1172 "unprovided-inject",
1173 "unrendered-component",
1174 "unused-component-prop",
1175 "unused-component-emit",
1176 "unused-component-input",
1177 "unused-component-output",
1178 "unused-svelte-event",
1179 "unused-server-action",
1180 "unused-load-data-key",
1181 "unresolved-import",
1182 "unlisted-dependency",
1183 "duplicate-export",
1184 "type-only-dependency",
1185 "test-only-dependency",
1186 "dev-dependency-in-production",
1187 "circular-dependency",
1188 "re-export-cycles",
1189 "reexport-cycle",
1190 "reexport-cycles",
1191 "boundary-violations",
1192 "coverage-gap",
1193 "feature-flag",
1194 "stale-suppression",
1195 "unused-catalog-entry",
1196 "empty-catalog-group",
1197 "unresolved-catalog-reference",
1198 "unused-dependency-override",
1199 "misconfigured-dependency-override",
1200 "invalid-client-exports",
1201 "mixed-client-server-barrels",
1202 "misplaced-directives",
1203 "route-collisions",
1204 "dynamic-segment-name-conflicts",
1205];
1206
1207#[must_use]
1214pub fn closest_known_rule_name(input: &str) -> Option<&'static str> {
1215 crate::levenshtein::closest_match(input, KNOWN_RULE_NAMES.iter().copied())
1216}
1217
1218#[derive(Debug, Clone, PartialEq, Eq)]
1224pub struct UnknownRuleKey {
1225 pub context: String,
1227 pub key: String,
1229 pub suggestion: Option<&'static str>,
1231}
1232
1233#[must_use]
1241pub fn find_unknown_rule_keys(value: &serde_json::Value, context: &str) -> Vec<UnknownRuleKey> {
1242 let Some(map) = value.as_object() else {
1243 return Vec::new();
1244 };
1245
1246 map.keys()
1247 .filter(|key| !KNOWN_RULE_NAMES.contains(&key.as_str()))
1248 .map(|key| UnknownRuleKey {
1249 context: context.to_owned(),
1250 key: key.clone(),
1251 suggestion: closest_known_rule_name(key),
1252 })
1253 .collect()
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258 use super::*;
1259
1260 #[test]
1261 fn severity_for_kind_maps_configured_fields_and_default() {
1262 let rules = RulesConfig {
1263 unused_exports: Severity::Warn,
1264 unused_types: Severity::Off,
1265 unresolved_imports: Severity::Warn,
1266 boundary_violation: Severity::Off,
1267 ..RulesConfig::default()
1268 };
1269
1270 assert_eq!(
1271 rules.severity_for_kind(IssueKind::UnusedExport),
1272 Severity::Warn
1273 );
1274 assert_eq!(
1275 rules.severity_for_kind(IssueKind::UnusedType),
1276 Severity::Off
1277 );
1278 assert_eq!(
1279 rules.severity_for_kind(IssueKind::UnresolvedImport),
1280 Severity::Warn
1281 );
1282 assert_eq!(
1283 rules.severity_for_kind(IssueKind::BoundaryViolation),
1284 Severity::Off
1285 );
1286 assert_eq!(
1287 rules.severity_for_kind(IssueKind::UnusedFile),
1288 Severity::Error
1289 );
1290 assert_eq!(
1291 rules.severity_for_kind(IssueKind::PrivateTypeLeak),
1292 Severity::Off
1293 );
1294 assert_eq!(
1295 rules.severity_for_kind(IssueKind::Complexity),
1296 Severity::Error
1297 );
1298 assert_eq!(
1299 rules.severity_for_kind(IssueKind::CodeDuplication),
1300 Severity::Error
1301 );
1302 }
1303
1304 #[test]
1305 fn rules_default_severities() {
1306 let rules = RulesConfig::default();
1307 assert_eq!(rules.unused_files, Severity::Error);
1308 assert_eq!(rules.unused_exports, Severity::Error);
1309 assert_eq!(rules.unused_types, Severity::Error);
1310 assert_eq!(rules.private_type_leaks, Severity::Off);
1311 assert_eq!(rules.unused_dependencies, Severity::Error);
1312 assert_eq!(rules.unused_dev_dependencies, Severity::Warn);
1313 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
1314 assert_eq!(rules.unused_enum_members, Severity::Error);
1315 assert_eq!(rules.unused_class_members, Severity::Error);
1316 assert_eq!(rules.unresolved_imports, Severity::Error);
1317 assert_eq!(rules.unlisted_dependencies, Severity::Error);
1318 assert_eq!(rules.duplicate_exports, Severity::Error);
1319 assert_eq!(rules.type_only_dependencies, Severity::Warn);
1320 assert_eq!(rules.test_only_dependencies, Severity::Warn);
1321 assert_eq!(rules.circular_dependencies, Severity::Error);
1322 assert_eq!(rules.boundary_violation, Severity::Error);
1323 assert_eq!(rules.coverage_gaps, Severity::Off);
1324 assert_eq!(rules.feature_flags, Severity::Off);
1325 assert_eq!(rules.stale_suppressions, Severity::Warn);
1326 assert_eq!(rules.unused_catalog_entries, Severity::Warn);
1327 assert_eq!(rules.empty_catalog_groups, Severity::Warn);
1328 assert_eq!(rules.unresolved_catalog_references, Severity::Error);
1329 }
1330
1331 #[test]
1332 fn rules_deserialize_kebab_case() {
1333 let json_str = r#"{
1334 "unused-files": "error",
1335 "unused-exports": "warn",
1336 "unused-types": "off"
1337 }"#;
1338 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1339 assert_eq!(rules.unused_files, Severity::Error);
1340 assert_eq!(rules.unused_exports, Severity::Warn);
1341 assert_eq!(rules.unused_types, Severity::Off);
1342 assert_eq!(rules.unresolved_imports, Severity::Error);
1343 }
1344
1345 #[test]
1346 fn empty_rules_object_matches_default_impl() {
1347 let from_empty: RulesConfig = serde_json::from_str("{}").unwrap();
1355 assert_eq!(
1356 from_empty,
1357 RulesConfig::default(),
1358 "empty `rules` object must equal RulesConfig::default(); a field whose \
1359 Default is Warn/Off needs an explicit #[serde(default = ...)]"
1360 );
1361 }
1362
1363 #[test]
1364 fn empty_rules_object_preserves_non_error_defaults() {
1365 let rules: RulesConfig = serde_json::from_str("{}").unwrap();
1368 assert_eq!(rules.coverage_gaps, Severity::Off);
1369 assert_eq!(rules.unused_store_members, Severity::Warn);
1370 assert_eq!(rules.unprovided_injects, Severity::Warn);
1371 assert_eq!(rules.unrendered_components, Severity::Warn);
1372 assert_eq!(rules.unused_component_props, Severity::Warn);
1373 assert_eq!(rules.unused_component_emits, Severity::Warn);
1374 assert_eq!(rules.unused_component_inputs, Severity::Warn);
1375 assert_eq!(rules.unused_component_outputs, Severity::Warn);
1376 assert_eq!(rules.unused_server_actions, Severity::Warn);
1377 }
1378
1379 #[test]
1380 fn explicit_rule_severity_still_overrides_default() {
1381 let rules: RulesConfig =
1384 serde_json::from_str(r#"{ "coverage-gaps": "error", "unused-component-prop": "off" }"#)
1385 .unwrap();
1386 assert_eq!(rules.coverage_gaps, Severity::Error);
1387 assert_eq!(rules.unused_component_props, Severity::Off);
1388 }
1389
1390 #[test]
1391 fn rules_re_export_cycle_default_is_warn() {
1392 let rules = RulesConfig::default();
1393 assert_eq!(rules.re_export_cycle, Severity::Warn);
1394 }
1395
1396 #[test]
1397 fn rules_deserialize_re_export_cycle_aliases() {
1398 for token in [
1399 "re-export-cycle",
1400 "re-export-cycles",
1401 "reexport-cycle",
1402 "reexport-cycles",
1403 ] {
1404 let json_str = format!(r#"{{ "{token}": "error" }}"#);
1405 let rules: RulesConfig = serde_json::from_str(&json_str)
1406 .unwrap_or_else(|e| panic!("alias {token} did not deserialize: {e}"));
1407 assert_eq!(
1408 rules.re_export_cycle,
1409 Severity::Error,
1410 "alias {token} should set re_export_cycle"
1411 );
1412 }
1413 }
1414
1415 #[test]
1416 fn rules_deserialize_circular_dependency_alias() {
1417 let json_str = r#"{
1418 "circular-dependency": "off"
1419 }"#;
1420 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1421 assert_eq!(rules.circular_dependencies, Severity::Off);
1422 }
1423
1424 #[test]
1425 fn rules_deserialize_boundary_violations_alias() {
1426 let json_str = r#"{
1427 "boundary-violations": "off"
1428 }"#;
1429 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1430 assert_eq!(rules.boundary_violation, Severity::Off);
1431
1432 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
1433 assert_eq!(partial.boundary_violation, Some(Severity::Off));
1434 }
1435
1436 #[test]
1437 fn rules_deserialize_singular_aliases_for_every_plural_rule() {
1438 let json_str = r#"{
1439 "unused-file": "off",
1440 "unused-export": "off",
1441 "unused-type": "off",
1442 "private-type-leak": "warn",
1443 "unused-dependency": "off",
1444 "unused-dev-dependency": "off",
1445 "unused-optional-dependency": "off",
1446 "unused-enum-member": "off",
1447 "unused-class-member": "off",
1448 "unresolved-import": "off",
1449 "unlisted-dependency": "off",
1450 "duplicate-export": "off",
1451 "type-only-dependency": "off",
1452 "test-only-dependency": "off",
1453 "coverage-gap": "warn",
1454 "feature-flag": "warn",
1455 "stale-suppression": "off",
1456 "suppression-reason": "warn",
1457 "unused-catalog-entry": "error",
1458 "empty-catalog-group": "error",
1459 "unresolved-catalog-reference": "warn"
1460 }"#;
1461
1462 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1463 assert_eq!(rules.unused_files, Severity::Off);
1464 assert_eq!(rules.unused_exports, Severity::Off);
1465 assert_eq!(rules.unused_types, Severity::Off);
1466 assert_eq!(rules.private_type_leaks, Severity::Warn);
1467 assert_eq!(rules.unused_dependencies, Severity::Off);
1468 assert_eq!(rules.unused_dev_dependencies, Severity::Off);
1469 assert_eq!(rules.unused_optional_dependencies, Severity::Off);
1470 assert_eq!(rules.unused_enum_members, Severity::Off);
1471 assert_eq!(rules.unused_class_members, Severity::Off);
1472 assert_eq!(rules.unresolved_imports, Severity::Off);
1473 assert_eq!(rules.unlisted_dependencies, Severity::Off);
1474 assert_eq!(rules.duplicate_exports, Severity::Off);
1475 assert_eq!(rules.type_only_dependencies, Severity::Off);
1476 assert_eq!(rules.test_only_dependencies, Severity::Off);
1477 assert_eq!(rules.coverage_gaps, Severity::Warn);
1478 assert_eq!(rules.feature_flags, Severity::Warn);
1479 assert_eq!(rules.stale_suppressions, Severity::Off);
1480 assert_eq!(rules.require_suppression_reason, Severity::Warn);
1481 assert_eq!(rules.unused_catalog_entries, Severity::Error);
1482 assert_eq!(rules.empty_catalog_groups, Severity::Error);
1483 assert_eq!(rules.unresolved_catalog_references, Severity::Warn);
1484
1485 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
1486 assert_eq!(partial.unused_files, Some(Severity::Off));
1487 assert_eq!(partial.unused_exports, Some(Severity::Off));
1488 assert_eq!(partial.unused_types, Some(Severity::Off));
1489 assert_eq!(partial.private_type_leaks, Some(Severity::Warn));
1490 assert_eq!(partial.unused_dependencies, Some(Severity::Off));
1491 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Off));
1492 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
1493 assert_eq!(partial.unused_enum_members, Some(Severity::Off));
1494 assert_eq!(partial.unused_class_members, Some(Severity::Off));
1495 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
1496 assert_eq!(partial.unlisted_dependencies, Some(Severity::Off));
1497 assert_eq!(partial.duplicate_exports, Some(Severity::Off));
1498 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
1499 assert_eq!(partial.test_only_dependencies, Some(Severity::Off));
1500 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
1501 assert_eq!(partial.feature_flags, Some(Severity::Warn));
1502 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
1503 assert_eq!(partial.require_suppression_reason, Some(Severity::Warn));
1504 assert_eq!(partial.unused_catalog_entries, Some(Severity::Error));
1505 assert_eq!(partial.empty_catalog_groups, Some(Severity::Error));
1506 assert_eq!(partial.unresolved_catalog_references, Some(Severity::Warn));
1507 }
1508
1509 #[test]
1510 fn severity_from_str() {
1511 assert_eq!("error".parse::<Severity>().unwrap(), Severity::Error);
1512 assert_eq!("warn".parse::<Severity>().unwrap(), Severity::Warn);
1513 assert_eq!("warning".parse::<Severity>().unwrap(), Severity::Warn);
1514 assert_eq!("off".parse::<Severity>().unwrap(), Severity::Off);
1515 assert_eq!("none".parse::<Severity>().unwrap(), Severity::Off);
1516 assert!("invalid".parse::<Severity>().is_err());
1517 }
1518
1519 #[test]
1520 fn apply_partial_only_some_fields() {
1521 let mut rules = RulesConfig::default();
1522 let partial = PartialRulesConfig {
1523 unused_files: Some(Severity::Warn),
1524 unused_exports: Some(Severity::Off),
1525 ..Default::default()
1526 };
1527 rules.apply_partial(&partial);
1528 assert_eq!(rules.unused_files, Severity::Warn);
1529 assert_eq!(rules.unused_exports, Severity::Off);
1530 assert_eq!(rules.unused_types, Severity::Error);
1531 assert_eq!(rules.unresolved_imports, Severity::Error);
1532 assert_eq!(rules.require_suppression_reason, Severity::Off);
1533 }
1534
1535 #[test]
1536 fn require_suppression_reason_deserializes_canonical_and_alias() {
1537 let rules: RulesConfig = serde_json::from_str(
1538 r#"{
1539 "require-suppression-reason": "error"
1540 }"#,
1541 )
1542 .unwrap();
1543 assert_eq!(rules.require_suppression_reason, Severity::Error);
1544
1545 let partial: PartialRulesConfig = serde_json::from_str(
1546 r#"{
1547 "suppression-reason": "warn"
1548 }"#,
1549 )
1550 .unwrap();
1551 assert_eq!(partial.require_suppression_reason, Some(Severity::Warn));
1552 }
1553
1554 #[test]
1555 fn complexity_rules_default_to_error_and_deserialize() {
1556 let rules = RulesConfig::default();
1557 assert_eq!(rules.complexity_cyclomatic, Severity::Error);
1558 assert_eq!(rules.complexity_cognitive, Severity::Error);
1559 assert_eq!(rules.complexity_crap, Severity::Error);
1560
1561 let partial: PartialRulesConfig =
1562 serde_json::from_str(r#"{ "complexity-crap": "warn", "complexity-cognitive": "off" }"#)
1563 .unwrap();
1564 assert_eq!(partial.complexity_crap, Some(Severity::Warn));
1565 assert_eq!(partial.complexity_cognitive, Some(Severity::Off));
1566 assert_eq!(partial.complexity_cyclomatic, None);
1567 }
1568
1569 #[test]
1570 fn complexity_severity_takes_the_most_severe_contributing_rule() {
1571 let rules = RulesConfig {
1572 complexity_cyclomatic: Severity::Warn,
1573 complexity_cognitive: Severity::Off,
1574 complexity_crap: Severity::Error,
1575 ..RulesConfig::default()
1576 };
1577 assert_eq!(
1578 rules.complexity_severity(true, false, true),
1579 Severity::Error
1580 );
1581 assert_eq!(rules.complexity_severity(true, true, false), Severity::Warn);
1582 assert_eq!(rules.complexity_severity(false, true, false), Severity::Off);
1583 assert_eq!(
1584 rules.complexity_severity(true, false, false),
1585 Severity::Warn
1586 );
1587 assert_eq!(
1588 rules.complexity_severity(false, false, false),
1589 Severity::Off
1590 );
1591 }
1592
1593 #[test]
1594 fn severity_display() {
1595 assert_eq!(Severity::Error.to_string(), "error");
1596 assert_eq!(Severity::Warn.to_string(), "warn");
1597 assert_eq!(Severity::Off.to_string(), "off");
1598 }
1599
1600 #[test]
1601 fn apply_partial_all_none_changes_nothing() {
1602 let mut rules = RulesConfig::default();
1603 let original = rules.clone();
1604 let partial = PartialRulesConfig::default(); rules.apply_partial(&partial);
1606 assert_eq!(rules.unused_files, original.unused_files);
1607 assert_eq!(rules.unused_exports, original.unused_exports);
1608 assert_eq!(
1609 rules.type_only_dependencies,
1610 original.type_only_dependencies
1611 );
1612 }
1613
1614 #[test]
1615 fn apply_partial_all_fields_set() {
1616 let mut rules = RulesConfig::default();
1617 let partial = PartialRulesConfig {
1618 unused_files: Some(Severity::Off),
1619 unused_exports: Some(Severity::Off),
1620 unused_types: Some(Severity::Off),
1621 private_type_leaks: Some(Severity::Off),
1622 unused_dependencies: Some(Severity::Off),
1623 unused_dev_dependencies: Some(Severity::Off),
1624 unused_optional_dependencies: Some(Severity::Off),
1625 unused_enum_members: Some(Severity::Off),
1626 unused_class_members: Some(Severity::Off),
1627 unused_store_members: Some(Severity::Off),
1628 unprovided_injects: Some(Severity::Off),
1629 unrendered_components: Some(Severity::Off),
1630 unused_component_props: Some(Severity::Off),
1631 unused_component_emits: Some(Severity::Off),
1632 unused_component_inputs: Some(Severity::Off),
1633 unused_component_outputs: Some(Severity::Off),
1634 unused_svelte_events: Some(Severity::Off),
1635 unused_server_actions: Some(Severity::Off),
1636 unused_load_data_keys: Some(Severity::Off),
1637 prop_drilling: Some(Severity::Off),
1638 thin_wrapper: Some(Severity::Off),
1639 duplicate_prop_shape: Some(Severity::Off),
1640 css_token_drift: Some(Severity::Off),
1641 css_duplicate_block: Some(Severity::Off),
1642 css_selector_complexity: Some(Severity::Off),
1643 css_dead_surface: Some(Severity::Off),
1644 css_broken_reference: Some(Severity::Off),
1645 complexity_cyclomatic: Some(Severity::Off),
1646 complexity_cognitive: Some(Severity::Off),
1647 complexity_crap: Some(Severity::Off),
1648 unresolved_imports: Some(Severity::Off),
1649 unlisted_dependencies: Some(Severity::Off),
1650 duplicate_exports: Some(Severity::Off),
1651 type_only_dependencies: Some(Severity::Off),
1652 test_only_dependencies: Some(Severity::Off),
1653 dev_dependencies_in_production: Some(Severity::Off),
1654 circular_dependencies: Some(Severity::Off),
1655 re_export_cycle: Some(Severity::Off),
1656 boundary_violation: Some(Severity::Off),
1657 coverage_gaps: Some(Severity::Off),
1658 feature_flags: Some(Severity::Off),
1659 stale_suppressions: Some(Severity::Off),
1660 require_suppression_reason: Some(Severity::Off),
1661 unused_catalog_entries: Some(Severity::Off),
1662 empty_catalog_groups: Some(Severity::Off),
1663 unresolved_catalog_references: Some(Severity::Off),
1664 unused_dependency_overrides: Some(Severity::Off),
1665 misconfigured_dependency_overrides: Some(Severity::Off),
1666 security_client_server_leak: Some(Severity::Off),
1667 security_sink: Some(Severity::Off),
1668 policy_violation: Some(Severity::Off),
1669 invalid_client_export: Some(Severity::Off),
1670 mixed_client_server_barrel: Some(Severity::Off),
1671 misplaced_directive: Some(Severity::Off),
1672 route_collision: Some(Severity::Off),
1673 dynamic_segment_name_conflict: Some(Severity::Off),
1674 deprecated_exports_in_use: Some(Severity::Off),
1675 };
1676 rules.apply_partial(&partial);
1677 assert_eq!(rules.unused_files, Severity::Off);
1678 assert_eq!(rules.private_type_leaks, Severity::Off);
1679 assert_eq!(rules.circular_dependencies, Severity::Off);
1680 assert_eq!(rules.type_only_dependencies, Severity::Off);
1681 assert_eq!(rules.test_only_dependencies, Severity::Off);
1682 assert_eq!(rules.boundary_violation, Severity::Off);
1683 assert_eq!(rules.coverage_gaps, Severity::Off);
1684 assert_eq!(rules.feature_flags, Severity::Off);
1685 assert_eq!(rules.stale_suppressions, Severity::Off);
1686 assert_eq!(rules.require_suppression_reason, Severity::Off);
1687 assert_eq!(rules.security_sink, Severity::Off);
1688 assert_eq!(rules.policy_violation, Severity::Off);
1689 assert_eq!(rules.invalid_client_export, Severity::Off);
1690 assert_eq!(rules.mixed_client_server_barrel, Severity::Off);
1691 assert_eq!(rules.misplaced_directive, Severity::Off);
1692 assert_eq!(rules.unrendered_components, Severity::Off);
1693 assert_eq!(rules.unused_component_props, Severity::Off);
1694 assert_eq!(rules.unused_component_emits, Severity::Off);
1695 assert_eq!(rules.unused_component_inputs, Severity::Off);
1696 assert_eq!(rules.unused_component_outputs, Severity::Off);
1697 assert_eq!(rules.unused_svelte_events, Severity::Off);
1698 assert_eq!(rules.route_collision, Severity::Off);
1699 assert_eq!(rules.dynamic_segment_name_conflict, Severity::Off);
1700 }
1701
1702 #[test]
1703 fn policy_violation_defaults_to_warn() {
1704 let rules = RulesConfig::default();
1705 assert_eq!(rules.policy_violation, Severity::Warn);
1706 }
1707
1708 #[test]
1709 fn policy_violation_accepts_plural_alias() {
1710 let json = r#"{ "policy-violations": "error" }"#;
1711 let rules: RulesConfig = serde_json::from_str(json).unwrap();
1712 assert_eq!(rules.policy_violation, Severity::Error);
1713 }
1714
1715 #[test]
1716 fn severity_from_str_case_insensitive() {
1717 assert_eq!("ERROR".parse::<Severity>().unwrap(), Severity::Error);
1718 assert_eq!("Warn".parse::<Severity>().unwrap(), Severity::Warn);
1719 assert_eq!("OFF".parse::<Severity>().unwrap(), Severity::Off);
1720 assert_eq!("Warning".parse::<Severity>().unwrap(), Severity::Warn);
1721 assert_eq!("NONE".parse::<Severity>().unwrap(), Severity::Off);
1722 }
1723
1724 #[test]
1725 fn severity_from_str_invalid_returns_error() {
1726 let result = "critical".parse::<Severity>();
1727 assert!(result.is_err());
1728 let err = result.unwrap_err();
1729 assert!(
1730 err.contains("unknown severity"),
1731 "Expected descriptive error, got: {err}"
1732 );
1733 }
1734
1735 #[test]
1740 fn known_rule_names_list_length_is_pinned() {
1741 assert_eq!(KNOWN_RULE_NAMES.len(), 103);
1742 }
1743
1744 #[test]
1750 fn known_rule_names_holds_no_unreachable_entry() {
1751 let serialized =
1752 serde_json::to_value(RulesConfig::default()).expect("RulesConfig serializes");
1753 let canonical = serialized
1754 .as_object()
1755 .expect("RulesConfig serializes to an object");
1756 let source = include_str!("rules.rs");
1757
1758 let unreachable: Vec<&&str> = KNOWN_RULE_NAMES
1759 .iter()
1760 .filter(|name| !canonical.contains_key(**name))
1761 .filter(|name| !source.contains(&format!("alias = \"{name}\"")))
1762 .collect();
1763
1764 assert!(
1765 unreachable.is_empty(),
1766 "KNOWN_RULE_NAMES entries that are neither a serialized rule nor a declared \
1767 serde alias: {unreachable:?}. closest_known_rule_name suggests from this list, \
1768 so each one can be offered to a user as a rule that does not exist."
1769 );
1770 }
1771
1772 #[test]
1773 fn known_rule_names_has_no_duplicates() {
1774 let mut sorted: Vec<&str> = KNOWN_RULE_NAMES.to_vec();
1775 sorted.sort_unstable();
1776 let original_len = sorted.len();
1777 sorted.dedup();
1778 assert_eq!(
1779 sorted.len(),
1780 original_len,
1781 "KNOWN_RULE_NAMES contains a duplicate"
1782 );
1783 }
1784
1785 #[test]
1786 fn known_rule_names_covers_every_serde_alias_in_source() {
1787 let source = include_str!("rules.rs");
1788
1789 let mut aliases_found = Vec::new();
1790 for line in source.lines() {
1791 let trimmed = line.trim();
1792 if trimmed.starts_with("//") {
1793 continue;
1794 }
1795 let Some(after) = trimmed.split("alias = \"").nth(1) else {
1796 continue;
1797 };
1798 let Some(end) = after.find('"') else {
1799 continue;
1800 };
1801 let alias = &after[..end];
1802 if alias.is_empty() || !alias.chars().all(|c| c.is_ascii_lowercase() || c == '-') {
1803 continue;
1804 }
1805 aliases_found.push(alias.to_owned());
1806 }
1807
1808 assert_eq!(
1809 aliases_found.len(),
1810 108,
1811 "expected 108 source-level alias attrs (54 per struct); got {}: {:?}",
1812 aliases_found.len(),
1813 aliases_found
1814 );
1815
1816 for alias in &aliases_found {
1817 assert!(
1818 KNOWN_RULE_NAMES.contains(&alias.as_str()),
1819 "serde alias '{alias}' is in rules.rs source but missing from KNOWN_RULE_NAMES"
1820 );
1821 }
1822 }
1823
1824 #[test]
1825 fn re_export_cycle_aliases_all_round_trip_to_the_same_field() {
1826 for alias in [
1827 "re-export-cycle",
1828 "re-export-cycles",
1829 "reexport-cycle",
1830 "reexport-cycles",
1831 ] {
1832 let json = format!(r#"{{"{alias}": "warn"}}"#);
1833 let partial: PartialRulesConfig = serde_json::from_str(&json)
1834 .unwrap_or_else(|e| panic!("'{alias}' should deserialize: {e}"));
1835 assert_eq!(
1836 partial.re_export_cycle,
1837 Some(Severity::Warn),
1838 "'{alias}' should set re_export_cycle to Warn"
1839 );
1840 let serialized = serde_json::to_value(&partial).unwrap();
1841 let map = serialized.as_object().unwrap();
1842 assert_eq!(
1843 map.len(),
1844 1,
1845 "'{alias}' should resolve to exactly one field, got: {map:?}"
1846 );
1847 }
1848 }
1849
1850 #[test]
1851 fn every_known_rule_name_round_trips_through_partial() {
1852 for &name in KNOWN_RULE_NAMES {
1853 let json = format!(r#"{{"{name}": "warn"}}"#);
1854 let partial: PartialRulesConfig = serde_json::from_str(&json)
1855 .unwrap_or_else(|e| panic!("'{name}' should deserialize: {e}"));
1856
1857 let serialized = serde_json::to_value(&partial).unwrap();
1858 let map = serialized.as_object().unwrap();
1859 assert_eq!(
1860 map.len(),
1861 1,
1862 "'{name}' should resolve to exactly one field, got: {map:?}"
1863 );
1864 }
1865 }
1866
1867 #[test]
1868 fn known_rule_names_covers_every_struct_field() {
1869 let json = serde_json::to_value(RulesConfig::default()).unwrap();
1870 let obj = json.as_object().unwrap();
1871 for key in obj.keys() {
1872 assert!(
1873 KNOWN_RULE_NAMES.contains(&key.as_str()),
1874 "field '{key}' is serialized but missing from KNOWN_RULE_NAMES"
1875 );
1876 }
1877 }
1878
1879 #[test]
1880 fn closest_known_rule_name_suggests_for_obvious_typo() {
1881 assert_eq!(
1882 closest_known_rule_name("unsued-files"),
1883 Some("unused-files")
1884 );
1885 assert_eq!(
1886 closest_known_rule_name("circular-dependnecy"),
1887 Some("circular-dependency")
1888 );
1889 assert_eq!(
1890 closest_known_rule_name("unused-dep"),
1891 None,
1892 "too short for a confident suggestion"
1893 );
1894 }
1895
1896 #[test]
1897 fn closest_known_rule_name_returns_none_for_novel_input() {
1898 assert_eq!(closest_known_rule_name("totally-fabricated"), None);
1899 assert_eq!(closest_known_rule_name("foo"), None);
1900 }
1901
1902 #[test]
1903 fn closest_known_rule_name_is_case_insensitive() {
1904 assert_eq!(
1905 closest_known_rule_name("UNSUED-FILES"),
1906 Some("unused-files")
1907 );
1908 }
1909
1910 #[test]
1911 fn closest_known_rule_name_returns_none_for_exact_match() {
1912 assert_eq!(closest_known_rule_name("unused-files"), None);
1913 }
1914
1915 #[test]
1916 fn find_unknown_rule_keys_flags_typo() {
1917 let v = serde_json::json!({
1918 "unsued-files": "warn",
1919 "unused-exports": "off",
1920 });
1921 let unknown = find_unknown_rule_keys(&v, "rules");
1922 assert_eq!(unknown.len(), 1);
1923 assert_eq!(unknown[0].key, "unsued-files");
1924 assert_eq!(unknown[0].context, "rules");
1925 assert_eq!(unknown[0].suggestion, Some("unused-files"));
1926 }
1927
1928 #[test]
1929 fn find_unknown_rule_keys_passes_aliases() {
1930 let v = serde_json::json!({
1931 "unused-file": "warn",
1932 "circular-dependency": "off",
1933 "boundary-violations": "warn",
1934 });
1935 let unknown = find_unknown_rule_keys(&v, "rules");
1936 assert!(
1937 unknown.is_empty(),
1938 "documented aliases must not flag as unknown: {unknown:?}"
1939 );
1940 }
1941
1942 #[test]
1943 fn find_unknown_rule_keys_returns_multiple_typos() {
1944 let v = serde_json::json!({
1945 "unsued-files": "warn",
1946 "circular-dependnecy": "off",
1947 });
1948 let unknown = find_unknown_rule_keys(&v, "rules");
1949 assert_eq!(unknown.len(), 2);
1950 }
1951
1952 #[test]
1953 fn find_unknown_rule_keys_carries_context() {
1954 let v = serde_json::json!({ "unsued-files": "warn" });
1955 let unknown = find_unknown_rule_keys(&v, "overrides[2].rules");
1956 assert_eq!(unknown[0].context, "overrides[2].rules");
1957 }
1958
1959 #[test]
1960 fn find_unknown_rule_keys_empty_when_not_object() {
1961 let v = serde_json::json!(null);
1962 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
1963
1964 let v = serde_json::json!([1, 2, 3]);
1965 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
1966 }
1967
1968 #[test]
1969 fn find_unknown_rule_keys_no_suggestion_for_novel_name() {
1970 let v = serde_json::json!({ "totally-fabricated-rule": "warn" });
1971 let unknown = find_unknown_rule_keys(&v, "rules");
1972 assert_eq!(unknown.len(), 1);
1973 assert_eq!(unknown[0].suggestion, None);
1974 }
1975
1976 #[test]
1977 fn partial_rules_empty_json() {
1978 let partial: PartialRulesConfig = serde_json::from_str("{}").unwrap();
1979 assert!(partial.unused_files.is_none());
1980 assert!(partial.unused_exports.is_none());
1981 assert!(partial.unused_types.is_none());
1982 assert!(partial.unused_dependencies.is_none());
1983 assert!(partial.circular_dependencies.is_none());
1984 assert!(partial.boundary_violation.is_none());
1985 assert!(partial.coverage_gaps.is_none());
1986 assert!(partial.feature_flags.is_none());
1987 assert!(partial.stale_suppressions.is_none());
1988 }
1989
1990 #[test]
1991 fn partial_rules_subset_json() {
1992 let json = r#"{
1993 "unused-files": "warn",
1994 "circular-dependencies": "off"
1995 }"#;
1996 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1997 assert_eq!(partial.unused_files, Some(Severity::Warn));
1998 assert_eq!(partial.circular_dependencies, Some(Severity::Off));
1999 assert!(partial.unused_exports.is_none());
2000 }
2001
2002 #[test]
2003 fn partial_rules_deserialize_circular_dependency_alias() {
2004 let json = r#"{
2005 "circular-dependency": "warn"
2006 }"#;
2007 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
2008 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
2009 }
2010
2011 #[test]
2012 fn partial_rules_all_fields_json() {
2013 let json = r#"{
2014 "unused-files": "error",
2015 "unused-exports": "warn",
2016 "unused-types": "off",
2017 "unused-dependencies": "error",
2018 "unused-dev-dependencies": "warn",
2019 "unused-optional-dependencies": "off",
2020 "unused-enum-members": "error",
2021 "unused-class-members": "warn",
2022 "unresolved-imports": "off",
2023 "unlisted-dependencies": "error",
2024 "duplicate-exports": "warn",
2025 "type-only-dependencies": "off",
2026 "test-only-dependencies": "error",
2027 "circular-dependencies": "warn",
2028 "boundary-violation": "off",
2029 "coverage-gaps": "warn",
2030 "feature-flags": "error",
2031 "stale-suppressions": "off"
2032 }"#;
2033 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
2034 assert_eq!(partial.unused_files, Some(Severity::Error));
2035 assert_eq!(partial.unused_exports, Some(Severity::Warn));
2036 assert_eq!(partial.unused_types, Some(Severity::Off));
2037 assert_eq!(partial.unused_dependencies, Some(Severity::Error));
2038 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Warn));
2039 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
2040 assert_eq!(partial.unused_enum_members, Some(Severity::Error));
2041 assert_eq!(partial.unused_class_members, Some(Severity::Warn));
2042 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
2043 assert_eq!(partial.unlisted_dependencies, Some(Severity::Error));
2044 assert_eq!(partial.duplicate_exports, Some(Severity::Warn));
2045 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
2046 assert_eq!(partial.test_only_dependencies, Some(Severity::Error));
2047 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
2048 assert_eq!(partial.boundary_violation, Some(Severity::Off));
2049 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
2050 assert_eq!(partial.feature_flags, Some(Severity::Error));
2051 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
2052 }
2053
2054 #[test]
2055 fn partial_rules_none_fields_not_serialized() {
2056 let partial = PartialRulesConfig::default();
2057 let json = serde_json::to_string(&partial).unwrap();
2058 assert_eq!(
2059 json, "{}",
2060 "all-None partial should serialize to empty object"
2061 );
2062 }
2063
2064 #[test]
2065 fn partial_rules_some_fields_serialized() {
2066 let partial = PartialRulesConfig {
2067 unused_files: Some(Severity::Warn),
2068 ..Default::default()
2069 };
2070 let json = serde_json::to_string(&partial).unwrap();
2071 assert!(json.contains("unused-files"));
2072 assert!(!json.contains("unused-exports"));
2073 }
2074
2075 #[test]
2076 fn severity_json_deserialization() {
2077 let error: Severity = serde_json::from_str(r#""error""#).unwrap();
2078 assert_eq!(error, Severity::Error);
2079
2080 let warn: Severity = serde_json::from_str(r#""warn""#).unwrap();
2081 assert_eq!(warn, Severity::Warn);
2082
2083 let off: Severity = serde_json::from_str(r#""off""#).unwrap();
2084 assert_eq!(off, Severity::Off);
2085 }
2086
2087 #[test]
2088 fn severity_invalid_json_value_rejected() {
2089 let result: Result<Severity, _> = serde_json::from_str(r#""critical""#);
2090 assert!(result.is_err());
2091 }
2092
2093 #[test]
2094 fn severity_default_is_error() {
2095 assert_eq!(Severity::default(), Severity::Error);
2096 }
2097
2098 #[test]
2099 fn rules_config_json_roundtrip() {
2100 let rules = RulesConfig {
2101 unused_files: Severity::Warn,
2102 unused_exports: Severity::Off,
2103 type_only_dependencies: Severity::Error,
2104 ..RulesConfig::default()
2105 };
2106 let json = serde_json::to_string(&rules).unwrap();
2107 let restored: RulesConfig = serde_json::from_str(&json).unwrap();
2108 assert_eq!(restored.unused_files, Severity::Warn);
2109 assert_eq!(restored.unused_exports, Severity::Off);
2110 assert_eq!(restored.type_only_dependencies, Severity::Error);
2111 assert_eq!(restored.unused_dependencies, Severity::Error); }
2113
2114 #[test]
2115 fn apply_partial_preserves_type_only_default() {
2116 let mut rules = RulesConfig::default();
2117 let partial = PartialRulesConfig {
2118 unused_files: Some(Severity::Off),
2119 ..Default::default()
2120 };
2121 rules.apply_partial(&partial);
2122 assert_eq!(rules.type_only_dependencies, Severity::Warn);
2123 assert_eq!(rules.test_only_dependencies, Severity::Warn);
2124 }
2125}