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