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