1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
9#[serde(rename_all = "lowercase")]
10pub enum Severity {
11 #[default]
13 Error,
14 Warn,
16 Off,
18}
19
20impl Severity {
21 const fn default_warn() -> Self {
23 Self::Warn
24 }
25
26 const fn default_off() -> Self {
28 Self::Off
29 }
30}
31
32impl std::fmt::Display for Severity {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 match self {
35 Self::Error => write!(f, "error"),
36 Self::Warn => write!(f, "warn"),
37 Self::Off => write!(f, "off"),
38 }
39 }
40}
41
42impl std::str::FromStr for Severity {
43 type Err = String;
44
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 match s.to_lowercase().as_str() {
47 "error" => Ok(Self::Error),
48 "warn" | "warning" => Ok(Self::Warn),
49 "off" | "none" => Ok(Self::Off),
50 other => Err(format!(
51 "unknown severity: '{other}' (expected error, warn, or off)"
52 )),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
64#[serde(rename_all = "kebab-case")]
65pub struct RulesConfig {
66 #[serde(default, alias = "unused-file")]
67 pub unused_files: Severity,
68 #[serde(default, alias = "unused-export")]
69 pub unused_exports: Severity,
70 #[serde(default, alias = "unused-type")]
71 pub unused_types: Severity,
72 #[serde(default = "Severity::default_off", alias = "private-type-leak")]
73 pub private_type_leaks: Severity,
74 #[serde(default, alias = "unused-dependency")]
75 pub unused_dependencies: Severity,
76 #[serde(default = "Severity::default_warn", alias = "unused-dev-dependency")]
77 pub unused_dev_dependencies: Severity,
78 #[serde(
79 default = "Severity::default_warn",
80 alias = "unused-optional-dependency"
81 )]
82 pub unused_optional_dependencies: Severity,
83 #[serde(default, alias = "unused-enum-member")]
84 pub unused_enum_members: Severity,
85 #[serde(default, alias = "unused-class-member")]
86 pub unused_class_members: Severity,
87 #[serde(default = "Severity::default_warn", alias = "unused-store-member")]
95 pub unused_store_members: Severity,
96 #[serde(default = "Severity::default_warn", alias = "unprovided-inject")]
102 pub unprovided_injects: Severity,
103 #[serde(default = "Severity::default_warn", alias = "unrendered-component")]
109 pub unrendered_components: Severity,
110 #[serde(default = "Severity::default_warn", alias = "unused-component-prop")]
116 pub unused_component_props: Severity,
117 #[serde(default = "Severity::default_warn", alias = "unused-component-emit")]
123 pub unused_component_emits: Severity,
124 #[serde(default = "Severity::default_warn", alias = "unused-component-input")]
131 pub unused_component_inputs: Severity,
132 #[serde(default = "Severity::default_warn", alias = "unused-component-output")]
139 pub unused_component_outputs: Severity,
140 #[serde(default = "Severity::default_warn", alias = "unused-svelte-event")]
148 pub unused_svelte_events: Severity,
149 #[serde(default = "Severity::default_warn", alias = "unused-server-action")]
157 pub unused_server_actions: Severity,
158 #[serde(default = "Severity::default_warn", alias = "unused-load-data-key")]
166 pub unused_load_data_keys: Severity,
167 #[serde(default = "Severity::default_off", alias = "prop-drilling")]
173 pub prop_drilling: Severity,
174 #[serde(default = "Severity::default_off", alias = "thin-wrapper")]
179 pub thin_wrapper: Severity,
180 #[serde(default = "Severity::default_off", alias = "duplicate-prop-shape")]
187 pub duplicate_prop_shape: Severity,
188 #[serde(default = "Severity::default_warn", alias = "css-token-drift")]
193 pub css_token_drift: Severity,
194 #[serde(default = "Severity::default_warn", alias = "css-duplicate-block")]
198 pub css_duplicate_block: Severity,
199 #[serde(default = "Severity::default_warn", alias = "css-selector-complexity")]
203 pub css_selector_complexity: Severity,
204 #[serde(default = "Severity::default_warn", alias = "css-dead-surface")]
208 pub css_dead_surface: Severity,
209 #[serde(default = "Severity::default_warn", alias = "css-broken-reference")]
213 pub css_broken_reference: Severity,
214 #[serde(default, alias = "unresolved-import")]
215 pub unresolved_imports: Severity,
216 #[serde(default, alias = "unlisted-dependency")]
217 pub unlisted_dependencies: Severity,
218 #[serde(default, alias = "duplicate-export")]
219 pub duplicate_exports: Severity,
220 #[serde(default = "Severity::default_warn", alias = "type-only-dependency")]
221 pub type_only_dependencies: Severity,
222 #[serde(default = "Severity::default_warn", alias = "test-only-dependency")]
223 pub test_only_dependencies: Severity,
224 #[serde(
225 default = "Severity::default_warn",
226 alias = "dev-dependency-in-production"
227 )]
228 pub dev_dependencies_in_production: Severity,
229 #[serde(default, alias = "circular-dependency")]
230 pub circular_dependencies: Severity,
231 #[serde(
232 default = "Severity::default_warn",
233 alias = "re-export-cycles",
234 alias = "reexport-cycle",
235 alias = "reexport-cycles"
236 )]
237 pub re_export_cycle: Severity,
238 #[serde(default, alias = "boundary-violations")]
239 pub boundary_violation: Severity,
240 #[serde(default = "Severity::default_off", alias = "coverage-gap")]
241 pub coverage_gaps: Severity,
242 #[serde(default = "Severity::default_off", alias = "feature-flag")]
243 pub feature_flags: Severity,
244 #[serde(default = "Severity::default_warn", alias = "stale-suppression")]
245 pub stale_suppressions: Severity,
246 #[serde(default = "Severity::default_off", alias = "suppression-reason")]
249 pub require_suppression_reason: Severity,
250 #[serde(default = "Severity::default_warn", alias = "unused-catalog-entry")]
251 pub unused_catalog_entries: Severity,
252 #[serde(default = "Severity::default_warn", alias = "empty-catalog-group")]
253 pub empty_catalog_groups: Severity,
254 #[serde(default, alias = "unresolved-catalog-reference")]
255 pub unresolved_catalog_references: Severity,
256 #[serde(
257 default = "Severity::default_warn",
258 alias = "unused-dependency-override"
259 )]
260 pub unused_dependency_overrides: Severity,
261 #[serde(default, alias = "misconfigured-dependency-override")]
262 pub misconfigured_dependency_overrides: Severity,
263 #[serde(default = "Severity::default_off")]
267 pub security_client_server_leak: Severity,
268 #[serde(default = "Severity::default_off")]
273 pub security_sink: Severity,
274 #[serde(default = "Severity::default_warn", alias = "policy-violations")]
280 pub policy_violation: Severity,
281 #[serde(default = "Severity::default_warn", alias = "invalid-client-exports")]
286 pub invalid_client_export: Severity,
287 #[serde(
292 default = "Severity::default_warn",
293 alias = "mixed-client-server-barrels"
294 )]
295 pub mixed_client_server_barrel: Severity,
296 #[serde(default = "Severity::default_warn", alias = "misplaced-directives")]
302 pub misplaced_directive: Severity,
303 #[serde(default, alias = "route-collisions")]
310 pub route_collision: Severity,
311 #[serde(default, alias = "dynamic-segment-name-conflicts")]
321 pub dynamic_segment_name_conflict: Severity,
322}
323
324impl Default for RulesConfig {
325 fn default() -> Self {
326 Self {
327 unused_files: Severity::Error,
328 unused_exports: Severity::Error,
329 unused_types: Severity::Error,
330 private_type_leaks: Severity::Off,
331 unused_dependencies: Severity::Error,
332 unused_dev_dependencies: Severity::Warn,
333 unused_optional_dependencies: Severity::Warn,
334 unused_enum_members: Severity::Error,
335 unused_class_members: Severity::Error,
336 unused_store_members: Severity::Warn,
337 unprovided_injects: Severity::Warn,
338 unrendered_components: Severity::Warn,
339 unused_component_props: Severity::Warn,
340 unused_component_emits: Severity::Warn,
341 unused_component_inputs: Severity::Warn,
342 unused_component_outputs: Severity::Warn,
343 unused_svelte_events: Severity::Warn,
344 unused_server_actions: Severity::Warn,
345 unused_load_data_keys: Severity::Warn,
346 prop_drilling: Severity::Off,
347 thin_wrapper: Severity::Off,
348 duplicate_prop_shape: Severity::Off,
349 css_token_drift: Severity::Warn,
350 css_duplicate_block: Severity::Warn,
351 css_selector_complexity: Severity::Warn,
352 css_dead_surface: Severity::Warn,
353 css_broken_reference: Severity::Warn,
354 unresolved_imports: Severity::Error,
355 unlisted_dependencies: Severity::Error,
356 duplicate_exports: Severity::Error,
357 type_only_dependencies: Severity::Warn,
358 test_only_dependencies: Severity::Warn,
359 dev_dependencies_in_production: Severity::Warn,
360 circular_dependencies: Severity::Error,
361 re_export_cycle: Severity::Warn,
362 boundary_violation: Severity::Error,
363 coverage_gaps: Severity::Off,
364 feature_flags: Severity::Off,
365 stale_suppressions: Severity::Warn,
366 require_suppression_reason: Severity::Off,
367 unused_catalog_entries: Severity::Warn,
368 empty_catalog_groups: Severity::Warn,
369 unresolved_catalog_references: Severity::Error,
370 unused_dependency_overrides: Severity::Warn,
371 misconfigured_dependency_overrides: Severity::Error,
372 security_client_server_leak: Severity::Off,
373 security_sink: Severity::Off,
374 policy_violation: Severity::Warn,
375 invalid_client_export: Severity::Warn,
376 mixed_client_server_barrel: Severity::Warn,
377 misplaced_directive: Severity::Warn,
378 route_collision: Severity::Error,
379 dynamic_segment_name_conflict: Severity::Error,
380 }
381 }
382}
383
384macro_rules! apply_partial_rules {
385 ($target:expr, $partial:expr, [$($field:ident),+ $(,)?]) => {
386 $(
387 if let Some(severity) = $partial.$field {
388 $target.$field = severity;
389 }
390 )+
391 };
392}
393
394impl RulesConfig {
395 pub const fn apply_partial(&mut self, partial: &PartialRulesConfig) {
397 apply_partial_rules!(
398 self,
399 partial,
400 [
401 unused_files,
402 unused_exports,
403 unused_types,
404 private_type_leaks,
405 unused_dependencies,
406 unused_dev_dependencies,
407 unused_optional_dependencies,
408 ]
409 );
410 apply_partial_rules!(
411 self,
412 partial,
413 [
414 unused_enum_members,
415 unused_class_members,
416 unused_store_members,
417 unprovided_injects,
418 unrendered_components,
419 unused_component_props,
420 unused_component_emits,
421 unused_component_inputs,
422 unused_component_outputs,
423 unused_svelte_events,
424 unused_server_actions,
425 unused_load_data_keys,
426 prop_drilling,
427 thin_wrapper,
428 duplicate_prop_shape,
429 css_token_drift,
430 css_duplicate_block,
431 css_selector_complexity,
432 css_dead_surface,
433 css_broken_reference,
434 ]
435 );
436 apply_partial_rules!(
437 self,
438 partial,
439 [
440 unresolved_imports,
441 unlisted_dependencies,
442 duplicate_exports,
443 type_only_dependencies,
444 test_only_dependencies,
445 dev_dependencies_in_production,
446 circular_dependencies,
447 re_export_cycle,
448 boundary_violation,
449 ]
450 );
451 apply_partial_rules!(
452 self,
453 partial,
454 [
455 coverage_gaps,
456 feature_flags,
457 stale_suppressions,
458 require_suppression_reason,
459 unused_catalog_entries,
460 empty_catalog_groups,
461 unresolved_catalog_references,
462 unused_dependency_overrides,
463 misconfigured_dependency_overrides,
464 ]
465 );
466 apply_partial_rules!(
467 self,
468 partial,
469 [
470 security_client_server_leak,
471 security_sink,
472 policy_violation,
473 invalid_client_export,
474 mixed_client_server_barrel,
475 misplaced_directive,
476 route_collision,
477 dynamic_segment_name_conflict,
478 ]
479 );
480 }
481}
482
483#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
485#[serde(rename_all = "kebab-case")]
486pub struct PartialRulesConfig {
487 #[serde(
488 default,
489 alias = "unused-file",
490 skip_serializing_if = "Option::is_none"
491 )]
492 pub unused_files: Option<Severity>,
493 #[serde(
494 default,
495 alias = "unused-export",
496 skip_serializing_if = "Option::is_none"
497 )]
498 pub unused_exports: Option<Severity>,
499 #[serde(
500 default,
501 alias = "unused-type",
502 skip_serializing_if = "Option::is_none"
503 )]
504 pub unused_types: Option<Severity>,
505 #[serde(
506 default,
507 alias = "private-type-leak",
508 skip_serializing_if = "Option::is_none"
509 )]
510 pub private_type_leaks: Option<Severity>,
511 #[serde(
512 default,
513 alias = "unused-dependency",
514 skip_serializing_if = "Option::is_none"
515 )]
516 pub unused_dependencies: Option<Severity>,
517 #[serde(
518 default,
519 alias = "unused-dev-dependency",
520 skip_serializing_if = "Option::is_none"
521 )]
522 pub unused_dev_dependencies: Option<Severity>,
523 #[serde(
524 default,
525 alias = "unused-optional-dependency",
526 skip_serializing_if = "Option::is_none"
527 )]
528 pub unused_optional_dependencies: Option<Severity>,
529 #[serde(
530 default,
531 alias = "unused-enum-member",
532 skip_serializing_if = "Option::is_none"
533 )]
534 pub unused_enum_members: Option<Severity>,
535 #[serde(
536 default,
537 alias = "unused-class-member",
538 skip_serializing_if = "Option::is_none"
539 )]
540 pub unused_class_members: Option<Severity>,
541 #[serde(
542 default,
543 alias = "unused-store-member",
544 skip_serializing_if = "Option::is_none"
545 )]
546 pub unused_store_members: Option<Severity>,
547 #[serde(
548 default,
549 alias = "unprovided-inject",
550 skip_serializing_if = "Option::is_none"
551 )]
552 pub unprovided_injects: Option<Severity>,
553 #[serde(
554 default,
555 alias = "unrendered-component",
556 skip_serializing_if = "Option::is_none"
557 )]
558 pub unrendered_components: Option<Severity>,
559 #[serde(
560 default,
561 alias = "unused-component-prop",
562 skip_serializing_if = "Option::is_none"
563 )]
564 pub unused_component_props: Option<Severity>,
565 #[serde(
566 default,
567 alias = "unused-component-emit",
568 skip_serializing_if = "Option::is_none"
569 )]
570 pub unused_component_emits: Option<Severity>,
571 #[serde(
572 default,
573 alias = "unused-component-input",
574 skip_serializing_if = "Option::is_none"
575 )]
576 pub unused_component_inputs: Option<Severity>,
577 #[serde(
578 default,
579 alias = "unused-component-output",
580 skip_serializing_if = "Option::is_none"
581 )]
582 pub unused_component_outputs: Option<Severity>,
583 #[serde(
584 default,
585 alias = "unused-svelte-event",
586 skip_serializing_if = "Option::is_none"
587 )]
588 pub unused_svelte_events: Option<Severity>,
589 #[serde(
590 default,
591 alias = "unused-server-action",
592 skip_serializing_if = "Option::is_none"
593 )]
594 pub unused_server_actions: Option<Severity>,
595 #[serde(
596 default,
597 alias = "unused-load-data-key",
598 skip_serializing_if = "Option::is_none"
599 )]
600 pub unused_load_data_keys: Option<Severity>,
601 #[serde(
602 default,
603 alias = "prop-drilling",
604 skip_serializing_if = "Option::is_none"
605 )]
606 pub prop_drilling: Option<Severity>,
607 #[serde(
608 default,
609 alias = "thin-wrapper",
610 skip_serializing_if = "Option::is_none"
611 )]
612 pub thin_wrapper: Option<Severity>,
613 #[serde(
614 default,
615 alias = "duplicate-prop-shape",
616 skip_serializing_if = "Option::is_none"
617 )]
618 pub duplicate_prop_shape: Option<Severity>,
619 #[serde(
620 default,
621 alias = "css-token-drift",
622 skip_serializing_if = "Option::is_none"
623 )]
624 pub css_token_drift: Option<Severity>,
625 #[serde(
626 default,
627 alias = "css-duplicate-block",
628 skip_serializing_if = "Option::is_none"
629 )]
630 pub css_duplicate_block: Option<Severity>,
631 #[serde(
632 default,
633 alias = "css-selector-complexity",
634 skip_serializing_if = "Option::is_none"
635 )]
636 pub css_selector_complexity: Option<Severity>,
637 #[serde(
638 default,
639 alias = "css-dead-surface",
640 skip_serializing_if = "Option::is_none"
641 )]
642 pub css_dead_surface: Option<Severity>,
643 #[serde(
644 default,
645 alias = "css-broken-reference",
646 skip_serializing_if = "Option::is_none"
647 )]
648 pub css_broken_reference: Option<Severity>,
649 #[serde(
650 default,
651 alias = "unresolved-import",
652 skip_serializing_if = "Option::is_none"
653 )]
654 pub unresolved_imports: Option<Severity>,
655 #[serde(
656 default,
657 alias = "unlisted-dependency",
658 skip_serializing_if = "Option::is_none"
659 )]
660 pub unlisted_dependencies: Option<Severity>,
661 #[serde(
662 default,
663 alias = "duplicate-export",
664 skip_serializing_if = "Option::is_none"
665 )]
666 pub duplicate_exports: Option<Severity>,
667 #[serde(
668 default,
669 alias = "type-only-dependency",
670 skip_serializing_if = "Option::is_none"
671 )]
672 pub type_only_dependencies: Option<Severity>,
673 #[serde(
674 default,
675 alias = "test-only-dependency",
676 skip_serializing_if = "Option::is_none"
677 )]
678 pub test_only_dependencies: Option<Severity>,
679 #[serde(
680 default,
681 alias = "dev-dependency-in-production",
682 skip_serializing_if = "Option::is_none"
683 )]
684 pub dev_dependencies_in_production: Option<Severity>,
685 #[serde(
686 default,
687 alias = "circular-dependency",
688 skip_serializing_if = "Option::is_none"
689 )]
690 pub circular_dependencies: Option<Severity>,
691 #[serde(
692 default,
693 alias = "re-export-cycles",
694 alias = "reexport-cycle",
695 alias = "reexport-cycles",
696 skip_serializing_if = "Option::is_none"
697 )]
698 pub re_export_cycle: Option<Severity>,
699 #[serde(
700 default,
701 alias = "boundary-violations",
702 skip_serializing_if = "Option::is_none"
703 )]
704 pub boundary_violation: Option<Severity>,
705 #[serde(
706 default,
707 alias = "coverage-gap",
708 skip_serializing_if = "Option::is_none"
709 )]
710 pub coverage_gaps: Option<Severity>,
711 #[serde(
712 default,
713 alias = "feature-flag",
714 skip_serializing_if = "Option::is_none"
715 )]
716 pub feature_flags: Option<Severity>,
717 #[serde(
718 default,
719 alias = "stale-suppression",
720 skip_serializing_if = "Option::is_none"
721 )]
722 pub stale_suppressions: Option<Severity>,
723 #[serde(
724 default,
725 alias = "suppression-reason",
726 skip_serializing_if = "Option::is_none"
727 )]
728 pub require_suppression_reason: Option<Severity>,
729 #[serde(
730 default,
731 alias = "unused-catalog-entry",
732 skip_serializing_if = "Option::is_none"
733 )]
734 pub unused_catalog_entries: Option<Severity>,
735 #[serde(
736 default,
737 alias = "empty-catalog-group",
738 skip_serializing_if = "Option::is_none"
739 )]
740 pub empty_catalog_groups: Option<Severity>,
741 #[serde(
742 default,
743 alias = "unresolved-catalog-reference",
744 skip_serializing_if = "Option::is_none"
745 )]
746 pub unresolved_catalog_references: Option<Severity>,
747 #[serde(
748 default,
749 alias = "unused-dependency-override",
750 skip_serializing_if = "Option::is_none"
751 )]
752 pub unused_dependency_overrides: Option<Severity>,
753 #[serde(
754 default,
755 alias = "misconfigured-dependency-override",
756 skip_serializing_if = "Option::is_none"
757 )]
758 pub misconfigured_dependency_overrides: Option<Severity>,
759 #[serde(default, skip_serializing_if = "Option::is_none")]
760 pub security_client_server_leak: Option<Severity>,
761 #[serde(default, skip_serializing_if = "Option::is_none")]
762 pub security_sink: Option<Severity>,
763 #[serde(
764 default,
765 alias = "policy-violations",
766 skip_serializing_if = "Option::is_none"
767 )]
768 pub policy_violation: Option<Severity>,
769 #[serde(
770 default,
771 alias = "invalid-client-exports",
772 skip_serializing_if = "Option::is_none"
773 )]
774 pub invalid_client_export: Option<Severity>,
775 #[serde(
776 default,
777 alias = "mixed-client-server-barrels",
778 skip_serializing_if = "Option::is_none"
779 )]
780 pub mixed_client_server_barrel: Option<Severity>,
781 #[serde(
782 default,
783 alias = "misplaced-directives",
784 skip_serializing_if = "Option::is_none"
785 )]
786 pub misplaced_directive: Option<Severity>,
787 #[serde(
788 default,
789 alias = "route-collisions",
790 skip_serializing_if = "Option::is_none"
791 )]
792 pub route_collision: Option<Severity>,
793 #[serde(
794 default,
795 alias = "dynamic-segment-name-conflicts",
796 skip_serializing_if = "Option::is_none"
797 )]
798 pub dynamic_segment_name_conflict: Option<Severity>,
799}
800
801pub const KNOWN_RULE_NAMES: &[&str] = &[
812 "unused-files",
813 "unused-exports",
814 "unused-types",
815 "private-type-leaks",
816 "unused-dependencies",
817 "unused-dev-dependencies",
818 "unused-optional-dependencies",
819 "unused-enum-members",
820 "unused-class-members",
821 "unused-store-members",
822 "unprovided-injects",
823 "unrendered-components",
824 "unused-component-props",
825 "unused-component-emits",
826 "unused-component-inputs",
827 "unused-component-outputs",
828 "unused-svelte-events",
829 "unused-server-actions",
830 "unused-load-data-keys",
831 "prop-drilling",
832 "thin-wrapper",
833 "duplicate-prop-shape",
834 "css-token-drift",
835 "css-duplicate-block",
836 "css-selector-complexity",
837 "css-dead-surface",
838 "css-broken-reference",
839 "unresolved-imports",
840 "unlisted-dependencies",
841 "duplicate-exports",
842 "type-only-dependencies",
843 "test-only-dependencies",
844 "dev-dependencies-in-production",
845 "circular-dependencies",
846 "re-export-cycle",
847 "boundary-violation",
848 "coverage-gaps",
849 "feature-flags",
850 "stale-suppressions",
851 "require-suppression-reason",
852 "suppression-reason",
853 "unused-catalog-entries",
854 "empty-catalog-groups",
855 "unresolved-catalog-references",
856 "unused-dependency-overrides",
857 "misconfigured-dependency-overrides",
858 "security-client-server-leak",
859 "security-sink",
860 "policy-violation",
861 "policy-violations",
862 "invalid-client-export",
863 "mixed-client-server-barrel",
864 "misplaced-directive",
865 "route-collision",
866 "dynamic-segment-name-conflict",
867 "unused-file",
868 "unused-export",
869 "unused-type",
870 "private-type-leak",
871 "unused-dependency",
872 "unused-dev-dependency",
873 "unused-optional-dependency",
874 "unused-enum-member",
875 "unused-class-member",
876 "unused-store-member",
877 "unprovided-inject",
878 "unrendered-component",
879 "unused-component-prop",
880 "unused-component-emit",
881 "unused-component-input",
882 "unused-component-output",
883 "unused-svelte-event",
884 "unused-server-action",
885 "unused-load-data-key",
886 "unresolved-import",
887 "unlisted-dependency",
888 "duplicate-export",
889 "type-only-dependency",
890 "test-only-dependency",
891 "dev-dependency-in-production",
892 "circular-dependency",
893 "re-export-cycles",
894 "reexport-cycle",
895 "reexport-cycles",
896 "boundary-violations",
897 "coverage-gap",
898 "feature-flag",
899 "stale-suppression",
900 "unused-catalog-entry",
901 "empty-catalog-group",
902 "unresolved-catalog-reference",
903 "unused-dependency-override",
904 "misconfigured-dependency-override",
905 "invalid-client-exports",
906 "mixed-client-server-barrels",
907 "misplaced-directives",
908 "route-collisions",
909 "dynamic-segment-name-conflicts",
910];
911
912#[must_use]
918pub fn closest_known_rule_name(input: &str) -> Option<&'static str> {
919 let input_lower = input.to_ascii_lowercase();
920 let candidates = KNOWN_RULE_NAMES.iter().copied();
921 let suggestion = crate::levenshtein::closest_match(&input_lower, candidates)?;
922 KNOWN_RULE_NAMES.iter().copied().find(|&c| c == suggestion)
923}
924
925#[derive(Debug, Clone, PartialEq, Eq)]
931pub struct UnknownRuleKey {
932 pub context: String,
934 pub key: String,
936 pub suggestion: Option<&'static str>,
938}
939
940#[must_use]
948pub fn find_unknown_rule_keys(value: &serde_json::Value, context: &str) -> Vec<UnknownRuleKey> {
949 let Some(map) = value.as_object() else {
950 return Vec::new();
951 };
952
953 map.keys()
954 .filter(|key| !KNOWN_RULE_NAMES.contains(&key.as_str()))
955 .map(|key| UnknownRuleKey {
956 context: context.to_owned(),
957 key: key.clone(),
958 suggestion: closest_known_rule_name(key),
959 })
960 .collect()
961}
962
963#[cfg(test)]
964mod tests {
965 use super::*;
966
967 #[test]
968 fn rules_default_severities() {
969 let rules = RulesConfig::default();
970 assert_eq!(rules.unused_files, Severity::Error);
971 assert_eq!(rules.unused_exports, Severity::Error);
972 assert_eq!(rules.unused_types, Severity::Error);
973 assert_eq!(rules.private_type_leaks, Severity::Off);
974 assert_eq!(rules.unused_dependencies, Severity::Error);
975 assert_eq!(rules.unused_dev_dependencies, Severity::Warn);
976 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
977 assert_eq!(rules.unused_enum_members, Severity::Error);
978 assert_eq!(rules.unused_class_members, Severity::Error);
979 assert_eq!(rules.unresolved_imports, Severity::Error);
980 assert_eq!(rules.unlisted_dependencies, Severity::Error);
981 assert_eq!(rules.duplicate_exports, Severity::Error);
982 assert_eq!(rules.type_only_dependencies, Severity::Warn);
983 assert_eq!(rules.test_only_dependencies, Severity::Warn);
984 assert_eq!(rules.circular_dependencies, Severity::Error);
985 assert_eq!(rules.boundary_violation, Severity::Error);
986 assert_eq!(rules.coverage_gaps, Severity::Off);
987 assert_eq!(rules.feature_flags, Severity::Off);
988 assert_eq!(rules.stale_suppressions, Severity::Warn);
989 assert_eq!(rules.unused_catalog_entries, Severity::Warn);
990 assert_eq!(rules.empty_catalog_groups, Severity::Warn);
991 assert_eq!(rules.unresolved_catalog_references, Severity::Error);
992 }
993
994 #[test]
995 fn rules_deserialize_kebab_case() {
996 let json_str = r#"{
997 "unused-files": "error",
998 "unused-exports": "warn",
999 "unused-types": "off"
1000 }"#;
1001 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1002 assert_eq!(rules.unused_files, Severity::Error);
1003 assert_eq!(rules.unused_exports, Severity::Warn);
1004 assert_eq!(rules.unused_types, Severity::Off);
1005 assert_eq!(rules.unresolved_imports, Severity::Error);
1006 }
1007
1008 #[test]
1009 fn empty_rules_object_matches_default_impl() {
1010 let from_empty: RulesConfig = serde_json::from_str("{}").unwrap();
1018 assert_eq!(
1019 from_empty,
1020 RulesConfig::default(),
1021 "empty `rules` object must equal RulesConfig::default(); a field whose \
1022 Default is Warn/Off needs an explicit #[serde(default = ...)]"
1023 );
1024 }
1025
1026 #[test]
1027 fn empty_rules_object_preserves_non_error_defaults() {
1028 let rules: RulesConfig = serde_json::from_str("{}").unwrap();
1031 assert_eq!(rules.coverage_gaps, Severity::Off);
1032 assert_eq!(rules.unused_store_members, Severity::Warn);
1033 assert_eq!(rules.unprovided_injects, Severity::Warn);
1034 assert_eq!(rules.unrendered_components, Severity::Warn);
1035 assert_eq!(rules.unused_component_props, Severity::Warn);
1036 assert_eq!(rules.unused_component_emits, Severity::Warn);
1037 assert_eq!(rules.unused_component_inputs, Severity::Warn);
1038 assert_eq!(rules.unused_component_outputs, Severity::Warn);
1039 assert_eq!(rules.unused_server_actions, Severity::Warn);
1040 }
1041
1042 #[test]
1043 fn explicit_rule_severity_still_overrides_default() {
1044 let rules: RulesConfig =
1047 serde_json::from_str(r#"{ "coverage-gaps": "error", "unused-component-prop": "off" }"#)
1048 .unwrap();
1049 assert_eq!(rules.coverage_gaps, Severity::Error);
1050 assert_eq!(rules.unused_component_props, Severity::Off);
1051 }
1052
1053 #[test]
1054 fn rules_re_export_cycle_default_is_warn() {
1055 let rules = RulesConfig::default();
1056 assert_eq!(rules.re_export_cycle, Severity::Warn);
1057 }
1058
1059 #[test]
1060 fn rules_deserialize_re_export_cycle_aliases() {
1061 for token in [
1062 "re-export-cycle",
1063 "re-export-cycles",
1064 "reexport-cycle",
1065 "reexport-cycles",
1066 ] {
1067 let json_str = format!(r#"{{ "{token}": "error" }}"#);
1068 let rules: RulesConfig = serde_json::from_str(&json_str)
1069 .unwrap_or_else(|e| panic!("alias {token} did not deserialize: {e}"));
1070 assert_eq!(
1071 rules.re_export_cycle,
1072 Severity::Error,
1073 "alias {token} should set re_export_cycle"
1074 );
1075 }
1076 }
1077
1078 #[test]
1079 fn rules_deserialize_circular_dependency_alias() {
1080 let json_str = r#"{
1081 "circular-dependency": "off"
1082 }"#;
1083 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1084 assert_eq!(rules.circular_dependencies, Severity::Off);
1085 }
1086
1087 #[test]
1088 fn rules_deserialize_boundary_violations_alias() {
1089 let json_str = r#"{
1090 "boundary-violations": "off"
1091 }"#;
1092 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1093 assert_eq!(rules.boundary_violation, Severity::Off);
1094
1095 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
1096 assert_eq!(partial.boundary_violation, Some(Severity::Off));
1097 }
1098
1099 #[test]
1100 fn rules_deserialize_singular_aliases_for_every_plural_rule() {
1101 let json_str = r#"{
1102 "unused-file": "off",
1103 "unused-export": "off",
1104 "unused-type": "off",
1105 "private-type-leak": "warn",
1106 "unused-dependency": "off",
1107 "unused-dev-dependency": "off",
1108 "unused-optional-dependency": "off",
1109 "unused-enum-member": "off",
1110 "unused-class-member": "off",
1111 "unresolved-import": "off",
1112 "unlisted-dependency": "off",
1113 "duplicate-export": "off",
1114 "type-only-dependency": "off",
1115 "test-only-dependency": "off",
1116 "coverage-gap": "warn",
1117 "feature-flag": "warn",
1118 "stale-suppression": "off",
1119 "suppression-reason": "warn",
1120 "unused-catalog-entry": "error",
1121 "empty-catalog-group": "error",
1122 "unresolved-catalog-reference": "warn"
1123 }"#;
1124
1125 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1126 assert_eq!(rules.unused_files, Severity::Off);
1127 assert_eq!(rules.unused_exports, Severity::Off);
1128 assert_eq!(rules.unused_types, Severity::Off);
1129 assert_eq!(rules.private_type_leaks, Severity::Warn);
1130 assert_eq!(rules.unused_dependencies, Severity::Off);
1131 assert_eq!(rules.unused_dev_dependencies, Severity::Off);
1132 assert_eq!(rules.unused_optional_dependencies, Severity::Off);
1133 assert_eq!(rules.unused_enum_members, Severity::Off);
1134 assert_eq!(rules.unused_class_members, Severity::Off);
1135 assert_eq!(rules.unresolved_imports, Severity::Off);
1136 assert_eq!(rules.unlisted_dependencies, Severity::Off);
1137 assert_eq!(rules.duplicate_exports, Severity::Off);
1138 assert_eq!(rules.type_only_dependencies, Severity::Off);
1139 assert_eq!(rules.test_only_dependencies, Severity::Off);
1140 assert_eq!(rules.coverage_gaps, Severity::Warn);
1141 assert_eq!(rules.feature_flags, Severity::Warn);
1142 assert_eq!(rules.stale_suppressions, Severity::Off);
1143 assert_eq!(rules.require_suppression_reason, Severity::Warn);
1144 assert_eq!(rules.unused_catalog_entries, Severity::Error);
1145 assert_eq!(rules.empty_catalog_groups, Severity::Error);
1146 assert_eq!(rules.unresolved_catalog_references, Severity::Warn);
1147
1148 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
1149 assert_eq!(partial.unused_files, Some(Severity::Off));
1150 assert_eq!(partial.unused_exports, Some(Severity::Off));
1151 assert_eq!(partial.unused_types, Some(Severity::Off));
1152 assert_eq!(partial.private_type_leaks, Some(Severity::Warn));
1153 assert_eq!(partial.unused_dependencies, Some(Severity::Off));
1154 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Off));
1155 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
1156 assert_eq!(partial.unused_enum_members, Some(Severity::Off));
1157 assert_eq!(partial.unused_class_members, Some(Severity::Off));
1158 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
1159 assert_eq!(partial.unlisted_dependencies, Some(Severity::Off));
1160 assert_eq!(partial.duplicate_exports, Some(Severity::Off));
1161 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
1162 assert_eq!(partial.test_only_dependencies, Some(Severity::Off));
1163 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
1164 assert_eq!(partial.feature_flags, Some(Severity::Warn));
1165 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
1166 assert_eq!(partial.require_suppression_reason, Some(Severity::Warn));
1167 assert_eq!(partial.unused_catalog_entries, Some(Severity::Error));
1168 assert_eq!(partial.empty_catalog_groups, Some(Severity::Error));
1169 assert_eq!(partial.unresolved_catalog_references, Some(Severity::Warn));
1170 }
1171
1172 #[test]
1173 fn severity_from_str() {
1174 assert_eq!("error".parse::<Severity>().unwrap(), Severity::Error);
1175 assert_eq!("warn".parse::<Severity>().unwrap(), Severity::Warn);
1176 assert_eq!("warning".parse::<Severity>().unwrap(), Severity::Warn);
1177 assert_eq!("off".parse::<Severity>().unwrap(), Severity::Off);
1178 assert_eq!("none".parse::<Severity>().unwrap(), Severity::Off);
1179 assert!("invalid".parse::<Severity>().is_err());
1180 }
1181
1182 #[test]
1183 fn apply_partial_only_some_fields() {
1184 let mut rules = RulesConfig::default();
1185 let partial = PartialRulesConfig {
1186 unused_files: Some(Severity::Warn),
1187 unused_exports: Some(Severity::Off),
1188 ..Default::default()
1189 };
1190 rules.apply_partial(&partial);
1191 assert_eq!(rules.unused_files, Severity::Warn);
1192 assert_eq!(rules.unused_exports, Severity::Off);
1193 assert_eq!(rules.unused_types, Severity::Error);
1194 assert_eq!(rules.unresolved_imports, Severity::Error);
1195 assert_eq!(rules.require_suppression_reason, Severity::Off);
1196 }
1197
1198 #[test]
1199 fn require_suppression_reason_deserializes_canonical_and_alias() {
1200 let rules: RulesConfig = serde_json::from_str(
1201 r#"{
1202 "require-suppression-reason": "error"
1203 }"#,
1204 )
1205 .unwrap();
1206 assert_eq!(rules.require_suppression_reason, Severity::Error);
1207
1208 let partial: PartialRulesConfig = serde_json::from_str(
1209 r#"{
1210 "suppression-reason": "warn"
1211 }"#,
1212 )
1213 .unwrap();
1214 assert_eq!(partial.require_suppression_reason, Some(Severity::Warn));
1215 }
1216
1217 #[test]
1218 fn severity_display() {
1219 assert_eq!(Severity::Error.to_string(), "error");
1220 assert_eq!(Severity::Warn.to_string(), "warn");
1221 assert_eq!(Severity::Off.to_string(), "off");
1222 }
1223
1224 #[test]
1225 fn apply_partial_all_none_changes_nothing() {
1226 let mut rules = RulesConfig::default();
1227 let original = rules.clone();
1228 let partial = PartialRulesConfig::default(); rules.apply_partial(&partial);
1230 assert_eq!(rules.unused_files, original.unused_files);
1231 assert_eq!(rules.unused_exports, original.unused_exports);
1232 assert_eq!(
1233 rules.type_only_dependencies,
1234 original.type_only_dependencies
1235 );
1236 }
1237
1238 #[test]
1239 fn apply_partial_all_fields_set() {
1240 let mut rules = RulesConfig::default();
1241 let partial = PartialRulesConfig {
1242 unused_files: Some(Severity::Off),
1243 unused_exports: Some(Severity::Off),
1244 unused_types: Some(Severity::Off),
1245 private_type_leaks: Some(Severity::Off),
1246 unused_dependencies: Some(Severity::Off),
1247 unused_dev_dependencies: Some(Severity::Off),
1248 unused_optional_dependencies: Some(Severity::Off),
1249 unused_enum_members: Some(Severity::Off),
1250 unused_class_members: Some(Severity::Off),
1251 unused_store_members: Some(Severity::Off),
1252 unprovided_injects: Some(Severity::Off),
1253 unrendered_components: Some(Severity::Off),
1254 unused_component_props: Some(Severity::Off),
1255 unused_component_emits: Some(Severity::Off),
1256 unused_component_inputs: Some(Severity::Off),
1257 unused_component_outputs: Some(Severity::Off),
1258 unused_svelte_events: Some(Severity::Off),
1259 unused_server_actions: Some(Severity::Off),
1260 unused_load_data_keys: Some(Severity::Off),
1261 prop_drilling: Some(Severity::Off),
1262 thin_wrapper: Some(Severity::Off),
1263 duplicate_prop_shape: Some(Severity::Off),
1264 css_token_drift: Some(Severity::Off),
1265 css_duplicate_block: Some(Severity::Off),
1266 css_selector_complexity: Some(Severity::Off),
1267 css_dead_surface: Some(Severity::Off),
1268 css_broken_reference: Some(Severity::Off),
1269 unresolved_imports: Some(Severity::Off),
1270 unlisted_dependencies: Some(Severity::Off),
1271 duplicate_exports: Some(Severity::Off),
1272 type_only_dependencies: Some(Severity::Off),
1273 test_only_dependencies: Some(Severity::Off),
1274 dev_dependencies_in_production: Some(Severity::Off),
1275 circular_dependencies: Some(Severity::Off),
1276 re_export_cycle: Some(Severity::Off),
1277 boundary_violation: Some(Severity::Off),
1278 coverage_gaps: Some(Severity::Off),
1279 feature_flags: Some(Severity::Off),
1280 stale_suppressions: Some(Severity::Off),
1281 require_suppression_reason: Some(Severity::Off),
1282 unused_catalog_entries: Some(Severity::Off),
1283 empty_catalog_groups: Some(Severity::Off),
1284 unresolved_catalog_references: Some(Severity::Off),
1285 unused_dependency_overrides: Some(Severity::Off),
1286 misconfigured_dependency_overrides: Some(Severity::Off),
1287 security_client_server_leak: Some(Severity::Off),
1288 security_sink: Some(Severity::Off),
1289 policy_violation: Some(Severity::Off),
1290 invalid_client_export: Some(Severity::Off),
1291 mixed_client_server_barrel: Some(Severity::Off),
1292 misplaced_directive: Some(Severity::Off),
1293 route_collision: Some(Severity::Off),
1294 dynamic_segment_name_conflict: Some(Severity::Off),
1295 };
1296 rules.apply_partial(&partial);
1297 assert_eq!(rules.unused_files, Severity::Off);
1298 assert_eq!(rules.private_type_leaks, Severity::Off);
1299 assert_eq!(rules.circular_dependencies, Severity::Off);
1300 assert_eq!(rules.type_only_dependencies, Severity::Off);
1301 assert_eq!(rules.test_only_dependencies, Severity::Off);
1302 assert_eq!(rules.boundary_violation, Severity::Off);
1303 assert_eq!(rules.coverage_gaps, Severity::Off);
1304 assert_eq!(rules.feature_flags, Severity::Off);
1305 assert_eq!(rules.stale_suppressions, Severity::Off);
1306 assert_eq!(rules.require_suppression_reason, Severity::Off);
1307 assert_eq!(rules.security_sink, Severity::Off);
1308 assert_eq!(rules.policy_violation, Severity::Off);
1309 assert_eq!(rules.invalid_client_export, Severity::Off);
1310 assert_eq!(rules.mixed_client_server_barrel, Severity::Off);
1311 assert_eq!(rules.misplaced_directive, Severity::Off);
1312 assert_eq!(rules.unrendered_components, Severity::Off);
1313 assert_eq!(rules.unused_component_props, Severity::Off);
1314 assert_eq!(rules.unused_component_emits, Severity::Off);
1315 assert_eq!(rules.unused_component_inputs, Severity::Off);
1316 assert_eq!(rules.unused_component_outputs, Severity::Off);
1317 assert_eq!(rules.unused_svelte_events, Severity::Off);
1318 assert_eq!(rules.route_collision, Severity::Off);
1319 assert_eq!(rules.dynamic_segment_name_conflict, Severity::Off);
1320 }
1321
1322 #[test]
1323 fn rules_config_defaults_include_optional_deps() {
1324 let rules = RulesConfig::default();
1325 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
1326 }
1327
1328 #[test]
1329 fn policy_violation_defaults_to_warn() {
1330 let rules = RulesConfig::default();
1331 assert_eq!(rules.policy_violation, Severity::Warn);
1332 }
1333
1334 #[test]
1335 fn policy_violation_accepts_plural_alias() {
1336 let json = r#"{ "policy-violations": "error" }"#;
1337 let rules: RulesConfig = serde_json::from_str(json).unwrap();
1338 assert_eq!(rules.policy_violation, Severity::Error);
1339 }
1340
1341 #[test]
1342 fn severity_from_str_case_insensitive() {
1343 assert_eq!("ERROR".parse::<Severity>().unwrap(), Severity::Error);
1344 assert_eq!("Warn".parse::<Severity>().unwrap(), Severity::Warn);
1345 assert_eq!("OFF".parse::<Severity>().unwrap(), Severity::Off);
1346 assert_eq!("Warning".parse::<Severity>().unwrap(), Severity::Warn);
1347 assert_eq!("NONE".parse::<Severity>().unwrap(), Severity::Off);
1348 }
1349
1350 #[test]
1351 fn severity_from_str_invalid_returns_error() {
1352 let result = "critical".parse::<Severity>();
1353 assert!(result.is_err());
1354 let err = result.unwrap_err();
1355 assert!(
1356 err.contains("unknown severity"),
1357 "Expected descriptive error, got: {err}"
1358 );
1359 }
1360
1361 #[test]
1362 fn known_rule_names_count_matches_struct() {
1363 assert_eq!(KNOWN_RULE_NAMES.len(), 98);
1364 }
1365
1366 #[test]
1367 fn known_rule_names_has_no_duplicates() {
1368 let mut sorted: Vec<&str> = KNOWN_RULE_NAMES.to_vec();
1369 sorted.sort_unstable();
1370 let original_len = sorted.len();
1371 sorted.dedup();
1372 assert_eq!(
1373 sorted.len(),
1374 original_len,
1375 "KNOWN_RULE_NAMES contains a duplicate"
1376 );
1377 }
1378
1379 #[test]
1380 fn known_rule_names_covers_every_serde_alias_in_source() {
1381 let source = include_str!("rules.rs");
1382
1383 let mut aliases_found = Vec::new();
1384 for line in source.lines() {
1385 let trimmed = line.trim();
1386 if trimmed.starts_with("//") {
1387 continue;
1388 }
1389 let Some(after) = trimmed.split("alias = \"").nth(1) else {
1390 continue;
1391 };
1392 let Some(end) = after.find('"') else {
1393 continue;
1394 };
1395 let alias = &after[..end];
1396 if alias.is_empty() || !alias.chars().all(|c| c.is_ascii_lowercase() || c == '-') {
1397 continue;
1398 }
1399 aliases_found.push(alias.to_owned());
1400 }
1401
1402 assert_eq!(
1403 aliases_found.len(),
1404 106,
1405 "expected 106 source-level alias attrs (53 per struct); got {}: {:?}",
1406 aliases_found.len(),
1407 aliases_found
1408 );
1409
1410 for alias in &aliases_found {
1411 assert!(
1412 KNOWN_RULE_NAMES.contains(&alias.as_str()),
1413 "serde alias '{alias}' is in rules.rs source but missing from KNOWN_RULE_NAMES"
1414 );
1415 }
1416 }
1417
1418 #[test]
1419 fn re_export_cycle_aliases_all_round_trip_to_the_same_field() {
1420 for alias in [
1421 "re-export-cycle",
1422 "re-export-cycles",
1423 "reexport-cycle",
1424 "reexport-cycles",
1425 ] {
1426 let json = format!(r#"{{"{alias}": "warn"}}"#);
1427 let partial: PartialRulesConfig = serde_json::from_str(&json)
1428 .unwrap_or_else(|e| panic!("'{alias}' should deserialize: {e}"));
1429 assert_eq!(
1430 partial.re_export_cycle,
1431 Some(Severity::Warn),
1432 "'{alias}' should set re_export_cycle to Warn"
1433 );
1434 let serialized = serde_json::to_value(&partial).unwrap();
1435 let map = serialized.as_object().unwrap();
1436 assert_eq!(
1437 map.len(),
1438 1,
1439 "'{alias}' should resolve to exactly one field, got: {map:?}"
1440 );
1441 }
1442 }
1443
1444 #[test]
1445 fn every_known_rule_name_round_trips_through_partial() {
1446 for &name in KNOWN_RULE_NAMES {
1447 let json = format!(r#"{{"{name}": "warn"}}"#);
1448 let partial: PartialRulesConfig = serde_json::from_str(&json)
1449 .unwrap_or_else(|e| panic!("'{name}' should deserialize: {e}"));
1450
1451 let serialized = serde_json::to_value(&partial).unwrap();
1452 let map = serialized.as_object().unwrap();
1453 assert_eq!(
1454 map.len(),
1455 1,
1456 "'{name}' should resolve to exactly one field, got: {map:?}"
1457 );
1458 }
1459 }
1460
1461 #[test]
1462 fn known_rule_names_covers_every_struct_field() {
1463 let json = serde_json::to_value(RulesConfig::default()).unwrap();
1464 let obj = json.as_object().unwrap();
1465 for key in obj.keys() {
1466 assert!(
1467 KNOWN_RULE_NAMES.contains(&key.as_str()),
1468 "field '{key}' is serialized but missing from KNOWN_RULE_NAMES"
1469 );
1470 }
1471 }
1472
1473 #[test]
1474 fn closest_known_rule_name_suggests_for_obvious_typo() {
1475 assert_eq!(
1476 closest_known_rule_name("unsued-files"),
1477 Some("unused-files")
1478 );
1479 assert_eq!(
1480 closest_known_rule_name("circular-dependnecy"),
1481 Some("circular-dependency")
1482 );
1483 assert_eq!(
1484 closest_known_rule_name("unused-dep"),
1485 None,
1486 "too short for a confident suggestion"
1487 );
1488 }
1489
1490 #[test]
1491 fn closest_known_rule_name_returns_none_for_novel_input() {
1492 assert_eq!(closest_known_rule_name("totally-fabricated"), None);
1493 assert_eq!(closest_known_rule_name("foo"), None);
1494 }
1495
1496 #[test]
1497 fn closest_known_rule_name_is_case_insensitive() {
1498 assert_eq!(
1499 closest_known_rule_name("UNSUED-FILES"),
1500 Some("unused-files")
1501 );
1502 }
1503
1504 #[test]
1505 fn closest_known_rule_name_returns_none_for_exact_match() {
1506 assert_eq!(closest_known_rule_name("unused-files"), None);
1507 }
1508
1509 #[test]
1510 fn find_unknown_rule_keys_flags_typo() {
1511 let v = serde_json::json!({
1512 "unsued-files": "warn",
1513 "unused-exports": "off",
1514 });
1515 let unknown = find_unknown_rule_keys(&v, "rules");
1516 assert_eq!(unknown.len(), 1);
1517 assert_eq!(unknown[0].key, "unsued-files");
1518 assert_eq!(unknown[0].context, "rules");
1519 assert_eq!(unknown[0].suggestion, Some("unused-files"));
1520 }
1521
1522 #[test]
1523 fn find_unknown_rule_keys_passes_aliases() {
1524 let v = serde_json::json!({
1525 "unused-file": "warn",
1526 "circular-dependency": "off",
1527 "boundary-violations": "warn",
1528 });
1529 let unknown = find_unknown_rule_keys(&v, "rules");
1530 assert!(
1531 unknown.is_empty(),
1532 "documented aliases must not flag as unknown: {unknown:?}"
1533 );
1534 }
1535
1536 #[test]
1537 fn find_unknown_rule_keys_returns_multiple_typos() {
1538 let v = serde_json::json!({
1539 "unsued-files": "warn",
1540 "circular-dependnecy": "off",
1541 });
1542 let unknown = find_unknown_rule_keys(&v, "rules");
1543 assert_eq!(unknown.len(), 2);
1544 }
1545
1546 #[test]
1547 fn find_unknown_rule_keys_carries_context() {
1548 let v = serde_json::json!({ "unsued-files": "warn" });
1549 let unknown = find_unknown_rule_keys(&v, "overrides[2].rules");
1550 assert_eq!(unknown[0].context, "overrides[2].rules");
1551 }
1552
1553 #[test]
1554 fn find_unknown_rule_keys_empty_when_not_object() {
1555 let v = serde_json::json!(null);
1556 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
1557
1558 let v = serde_json::json!([1, 2, 3]);
1559 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
1560 }
1561
1562 #[test]
1563 fn find_unknown_rule_keys_no_suggestion_for_novel_name() {
1564 let v = serde_json::json!({ "totally-fabricated-rule": "warn" });
1565 let unknown = find_unknown_rule_keys(&v, "rules");
1566 assert_eq!(unknown.len(), 1);
1567 assert_eq!(unknown[0].suggestion, None);
1568 }
1569
1570 #[test]
1571 fn partial_rules_empty_json() {
1572 let partial: PartialRulesConfig = serde_json::from_str("{}").unwrap();
1573 assert!(partial.unused_files.is_none());
1574 assert!(partial.unused_exports.is_none());
1575 assert!(partial.unused_types.is_none());
1576 assert!(partial.unused_dependencies.is_none());
1577 assert!(partial.circular_dependencies.is_none());
1578 assert!(partial.boundary_violation.is_none());
1579 assert!(partial.coverage_gaps.is_none());
1580 assert!(partial.feature_flags.is_none());
1581 assert!(partial.stale_suppressions.is_none());
1582 }
1583
1584 #[test]
1585 fn partial_rules_subset_json() {
1586 let json = r#"{
1587 "unused-files": "warn",
1588 "circular-dependencies": "off"
1589 }"#;
1590 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1591 assert_eq!(partial.unused_files, Some(Severity::Warn));
1592 assert_eq!(partial.circular_dependencies, Some(Severity::Off));
1593 assert!(partial.unused_exports.is_none());
1594 }
1595
1596 #[test]
1597 fn partial_rules_deserialize_circular_dependency_alias() {
1598 let json = r#"{
1599 "circular-dependency": "warn"
1600 }"#;
1601 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1602 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
1603 }
1604
1605 #[test]
1606 fn partial_rules_all_fields_json() {
1607 let json = r#"{
1608 "unused-files": "error",
1609 "unused-exports": "warn",
1610 "unused-types": "off",
1611 "unused-dependencies": "error",
1612 "unused-dev-dependencies": "warn",
1613 "unused-optional-dependencies": "off",
1614 "unused-enum-members": "error",
1615 "unused-class-members": "warn",
1616 "unresolved-imports": "off",
1617 "unlisted-dependencies": "error",
1618 "duplicate-exports": "warn",
1619 "type-only-dependencies": "off",
1620 "test-only-dependencies": "error",
1621 "circular-dependencies": "warn",
1622 "boundary-violation": "off",
1623 "coverage-gaps": "warn",
1624 "feature-flags": "error",
1625 "stale-suppressions": "off"
1626 }"#;
1627 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1628 assert_eq!(partial.unused_files, Some(Severity::Error));
1629 assert_eq!(partial.unused_exports, Some(Severity::Warn));
1630 assert_eq!(partial.unused_types, Some(Severity::Off));
1631 assert_eq!(partial.unused_dependencies, Some(Severity::Error));
1632 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Warn));
1633 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
1634 assert_eq!(partial.unused_enum_members, Some(Severity::Error));
1635 assert_eq!(partial.unused_class_members, Some(Severity::Warn));
1636 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
1637 assert_eq!(partial.unlisted_dependencies, Some(Severity::Error));
1638 assert_eq!(partial.duplicate_exports, Some(Severity::Warn));
1639 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
1640 assert_eq!(partial.test_only_dependencies, Some(Severity::Error));
1641 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
1642 assert_eq!(partial.boundary_violation, Some(Severity::Off));
1643 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
1644 assert_eq!(partial.feature_flags, Some(Severity::Error));
1645 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
1646 }
1647
1648 #[test]
1649 fn partial_rules_none_fields_not_serialized() {
1650 let partial = PartialRulesConfig::default();
1651 let json = serde_json::to_string(&partial).unwrap();
1652 assert_eq!(
1653 json, "{}",
1654 "all-None partial should serialize to empty object"
1655 );
1656 }
1657
1658 #[test]
1659 fn partial_rules_some_fields_serialized() {
1660 let partial = PartialRulesConfig {
1661 unused_files: Some(Severity::Warn),
1662 ..Default::default()
1663 };
1664 let json = serde_json::to_string(&partial).unwrap();
1665 assert!(json.contains("unused-files"));
1666 assert!(!json.contains("unused-exports"));
1667 }
1668
1669 #[test]
1670 fn severity_json_deserialization() {
1671 let error: Severity = serde_json::from_str(r#""error""#).unwrap();
1672 assert_eq!(error, Severity::Error);
1673
1674 let warn: Severity = serde_json::from_str(r#""warn""#).unwrap();
1675 assert_eq!(warn, Severity::Warn);
1676
1677 let off: Severity = serde_json::from_str(r#""off""#).unwrap();
1678 assert_eq!(off, Severity::Off);
1679 }
1680
1681 #[test]
1682 fn severity_invalid_json_value_rejected() {
1683 let result: Result<Severity, _> = serde_json::from_str(r#""critical""#);
1684 assert!(result.is_err());
1685 }
1686
1687 #[test]
1688 fn severity_default_is_error() {
1689 assert_eq!(Severity::default(), Severity::Error);
1690 }
1691
1692 #[test]
1693 fn rules_config_json_roundtrip() {
1694 let rules = RulesConfig {
1695 unused_files: Severity::Warn,
1696 unused_exports: Severity::Off,
1697 type_only_dependencies: Severity::Error,
1698 ..RulesConfig::default()
1699 };
1700 let json = serde_json::to_string(&rules).unwrap();
1701 let restored: RulesConfig = serde_json::from_str(&json).unwrap();
1702 assert_eq!(restored.unused_files, Severity::Warn);
1703 assert_eq!(restored.unused_exports, Severity::Off);
1704 assert_eq!(restored.type_only_dependencies, Severity::Error);
1705 assert_eq!(restored.unused_dependencies, Severity::Error); }
1707
1708 #[test]
1709 fn apply_partial_preserves_type_only_default() {
1710 let mut rules = RulesConfig::default();
1711 let partial = PartialRulesConfig {
1712 unused_files: Some(Severity::Off),
1713 ..Default::default()
1714 };
1715 rules.apply_partial(&partial);
1716 assert_eq!(rules.type_only_dependencies, Severity::Warn);
1717 assert_eq!(rules.test_only_dependencies, Severity::Warn);
1718 }
1719}