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")]
117 pub unused_component_props: Severity,
118 #[serde(default, alias = "unused-component-emit")]
124 pub unused_component_emits: Severity,
125 #[serde(default, alias = "unused-server-action")]
133 pub unused_server_actions: Severity,
134 #[serde(default = "Severity::default_warn", alias = "unused-load-data-key")]
142 pub unused_load_data_keys: Severity,
143 #[serde(default = "Severity::default_off", alias = "prop-drilling")]
149 pub prop_drilling: Severity,
150 #[serde(default = "Severity::default_off", alias = "thin-wrapper")]
155 pub thin_wrapper: Severity,
156 #[serde(default = "Severity::default_off", alias = "duplicate-prop-shape")]
163 pub duplicate_prop_shape: Severity,
164 #[serde(default, alias = "unresolved-import")]
165 pub unresolved_imports: Severity,
166 #[serde(default, alias = "unlisted-dependency")]
167 pub unlisted_dependencies: Severity,
168 #[serde(default, alias = "duplicate-export")]
169 pub duplicate_exports: Severity,
170 #[serde(default = "Severity::default_warn", alias = "type-only-dependency")]
171 pub type_only_dependencies: Severity,
172 #[serde(default = "Severity::default_warn", alias = "test-only-dependency")]
173 pub test_only_dependencies: Severity,
174 #[serde(default, alias = "circular-dependency")]
175 pub circular_dependencies: Severity,
176 #[serde(
177 default = "Severity::default_warn",
178 alias = "re-export-cycles",
179 alias = "reexport-cycle",
180 alias = "reexport-cycles"
181 )]
182 pub re_export_cycle: Severity,
183 #[serde(default, alias = "boundary-violations")]
184 pub boundary_violation: Severity,
185 #[serde(default, alias = "coverage-gap")]
186 pub coverage_gaps: Severity,
187 #[serde(default = "Severity::default_off", alias = "feature-flag")]
188 pub feature_flags: Severity,
189 #[serde(default = "Severity::default_warn", alias = "stale-suppression")]
190 pub stale_suppressions: Severity,
191 #[serde(default = "Severity::default_warn", alias = "unused-catalog-entry")]
192 pub unused_catalog_entries: Severity,
193 #[serde(default = "Severity::default_warn", alias = "empty-catalog-group")]
194 pub empty_catalog_groups: Severity,
195 #[serde(default, alias = "unresolved-catalog-reference")]
196 pub unresolved_catalog_references: Severity,
197 #[serde(
198 default = "Severity::default_warn",
199 alias = "unused-dependency-override"
200 )]
201 pub unused_dependency_overrides: Severity,
202 #[serde(default, alias = "misconfigured-dependency-override")]
203 pub misconfigured_dependency_overrides: Severity,
204 #[serde(default = "Severity::default_off")]
208 pub security_client_server_leak: Severity,
209 #[serde(default = "Severity::default_off")]
214 pub security_sink: Severity,
215 #[serde(default = "Severity::default_warn", alias = "policy-violations")]
221 pub policy_violation: Severity,
222 #[serde(default = "Severity::default_warn", alias = "invalid-client-exports")]
227 pub invalid_client_export: Severity,
228 #[serde(
233 default = "Severity::default_warn",
234 alias = "mixed-client-server-barrels"
235 )]
236 pub mixed_client_server_barrel: Severity,
237 #[serde(default = "Severity::default_warn", alias = "misplaced-directives")]
243 pub misplaced_directive: Severity,
244 #[serde(default, alias = "route-collisions")]
251 pub route_collision: Severity,
252 #[serde(default, alias = "dynamic-segment-name-conflicts")]
262 pub dynamic_segment_name_conflict: Severity,
263}
264
265impl Default for RulesConfig {
266 fn default() -> Self {
267 Self {
268 unused_files: Severity::Error,
269 unused_exports: Severity::Error,
270 unused_types: Severity::Error,
271 private_type_leaks: Severity::Off,
272 unused_dependencies: Severity::Error,
273 unused_dev_dependencies: Severity::Warn,
274 unused_optional_dependencies: Severity::Warn,
275 unused_enum_members: Severity::Error,
276 unused_class_members: Severity::Error,
277 unused_store_members: Severity::Warn,
278 unprovided_injects: Severity::Warn,
279 unrendered_components: Severity::Warn,
280 unused_component_props: Severity::Warn,
281 unused_component_emits: Severity::Warn,
282 unused_server_actions: Severity::Warn,
283 unused_load_data_keys: Severity::Warn,
284 prop_drilling: Severity::Off,
285 thin_wrapper: Severity::Off,
286 duplicate_prop_shape: Severity::Off,
287 unresolved_imports: Severity::Error,
288 unlisted_dependencies: Severity::Error,
289 duplicate_exports: Severity::Error,
290 type_only_dependencies: Severity::Warn,
291 test_only_dependencies: Severity::Warn,
292 circular_dependencies: Severity::Error,
293 re_export_cycle: Severity::Warn,
294 boundary_violation: Severity::Error,
295 coverage_gaps: Severity::Off,
296 feature_flags: Severity::Off,
297 stale_suppressions: Severity::Warn,
298 unused_catalog_entries: Severity::Warn,
299 empty_catalog_groups: Severity::Warn,
300 unresolved_catalog_references: Severity::Error,
301 unused_dependency_overrides: Severity::Warn,
302 misconfigured_dependency_overrides: Severity::Error,
303 security_client_server_leak: Severity::Off,
304 security_sink: Severity::Off,
305 policy_violation: Severity::Warn,
306 invalid_client_export: Severity::Warn,
307 mixed_client_server_barrel: Severity::Warn,
308 misplaced_directive: Severity::Warn,
309 route_collision: Severity::Error,
310 dynamic_segment_name_conflict: Severity::Error,
311 }
312 }
313}
314
315macro_rules! apply_partial_rules {
316 ($target:expr, $partial:expr, [$($field:ident),+ $(,)?]) => {
317 $(
318 if let Some(severity) = $partial.$field {
319 $target.$field = severity;
320 }
321 )+
322 };
323}
324
325impl RulesConfig {
326 pub const fn apply_partial(&mut self, partial: &PartialRulesConfig) {
328 apply_partial_rules!(
329 self,
330 partial,
331 [
332 unused_files,
333 unused_exports,
334 unused_types,
335 private_type_leaks,
336 unused_dependencies,
337 unused_dev_dependencies,
338 unused_optional_dependencies,
339 ]
340 );
341 apply_partial_rules!(
342 self,
343 partial,
344 [
345 unused_enum_members,
346 unused_class_members,
347 unused_store_members,
348 unprovided_injects,
349 unrendered_components,
350 unused_component_props,
351 unused_component_emits,
352 unused_server_actions,
353 unused_load_data_keys,
354 prop_drilling,
355 thin_wrapper,
356 duplicate_prop_shape,
357 ]
358 );
359 apply_partial_rules!(
360 self,
361 partial,
362 [
363 unresolved_imports,
364 unlisted_dependencies,
365 duplicate_exports,
366 type_only_dependencies,
367 test_only_dependencies,
368 circular_dependencies,
369 re_export_cycle,
370 boundary_violation,
371 ]
372 );
373 apply_partial_rules!(
374 self,
375 partial,
376 [
377 coverage_gaps,
378 feature_flags,
379 stale_suppressions,
380 unused_catalog_entries,
381 empty_catalog_groups,
382 unresolved_catalog_references,
383 unused_dependency_overrides,
384 misconfigured_dependency_overrides,
385 ]
386 );
387 apply_partial_rules!(
388 self,
389 partial,
390 [
391 security_client_server_leak,
392 security_sink,
393 policy_violation,
394 invalid_client_export,
395 mixed_client_server_barrel,
396 misplaced_directive,
397 route_collision,
398 dynamic_segment_name_conflict,
399 ]
400 );
401 }
402}
403
404#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
406#[serde(rename_all = "kebab-case")]
407pub struct PartialRulesConfig {
408 #[serde(
409 default,
410 alias = "unused-file",
411 skip_serializing_if = "Option::is_none"
412 )]
413 pub unused_files: Option<Severity>,
414 #[serde(
415 default,
416 alias = "unused-export",
417 skip_serializing_if = "Option::is_none"
418 )]
419 pub unused_exports: Option<Severity>,
420 #[serde(
421 default,
422 alias = "unused-type",
423 skip_serializing_if = "Option::is_none"
424 )]
425 pub unused_types: Option<Severity>,
426 #[serde(
427 default,
428 alias = "private-type-leak",
429 skip_serializing_if = "Option::is_none"
430 )]
431 pub private_type_leaks: Option<Severity>,
432 #[serde(
433 default,
434 alias = "unused-dependency",
435 skip_serializing_if = "Option::is_none"
436 )]
437 pub unused_dependencies: Option<Severity>,
438 #[serde(
439 default,
440 alias = "unused-dev-dependency",
441 skip_serializing_if = "Option::is_none"
442 )]
443 pub unused_dev_dependencies: Option<Severity>,
444 #[serde(
445 default,
446 alias = "unused-optional-dependency",
447 skip_serializing_if = "Option::is_none"
448 )]
449 pub unused_optional_dependencies: Option<Severity>,
450 #[serde(
451 default,
452 alias = "unused-enum-member",
453 skip_serializing_if = "Option::is_none"
454 )]
455 pub unused_enum_members: Option<Severity>,
456 #[serde(
457 default,
458 alias = "unused-class-member",
459 skip_serializing_if = "Option::is_none"
460 )]
461 pub unused_class_members: Option<Severity>,
462 #[serde(
463 default,
464 alias = "unused-store-member",
465 skip_serializing_if = "Option::is_none"
466 )]
467 pub unused_store_members: Option<Severity>,
468 #[serde(
469 default,
470 alias = "unprovided-inject",
471 skip_serializing_if = "Option::is_none"
472 )]
473 pub unprovided_injects: Option<Severity>,
474 #[serde(
475 default,
476 alias = "unrendered-component",
477 skip_serializing_if = "Option::is_none"
478 )]
479 pub unrendered_components: Option<Severity>,
480 #[serde(
481 default,
482 alias = "unused-component-prop",
483 skip_serializing_if = "Option::is_none"
484 )]
485 pub unused_component_props: Option<Severity>,
486 #[serde(
487 default,
488 alias = "unused-component-emit",
489 skip_serializing_if = "Option::is_none"
490 )]
491 pub unused_component_emits: Option<Severity>,
492 #[serde(
493 default,
494 alias = "unused-server-action",
495 skip_serializing_if = "Option::is_none"
496 )]
497 pub unused_server_actions: Option<Severity>,
498 #[serde(
499 default,
500 alias = "unused-load-data-key",
501 skip_serializing_if = "Option::is_none"
502 )]
503 pub unused_load_data_keys: Option<Severity>,
504 #[serde(
505 default,
506 alias = "prop-drilling",
507 skip_serializing_if = "Option::is_none"
508 )]
509 pub prop_drilling: Option<Severity>,
510 #[serde(
511 default,
512 alias = "thin-wrapper",
513 skip_serializing_if = "Option::is_none"
514 )]
515 pub thin_wrapper: Option<Severity>,
516 #[serde(
517 default,
518 alias = "duplicate-prop-shape",
519 skip_serializing_if = "Option::is_none"
520 )]
521 pub duplicate_prop_shape: Option<Severity>,
522 #[serde(
523 default,
524 alias = "unresolved-import",
525 skip_serializing_if = "Option::is_none"
526 )]
527 pub unresolved_imports: Option<Severity>,
528 #[serde(
529 default,
530 alias = "unlisted-dependency",
531 skip_serializing_if = "Option::is_none"
532 )]
533 pub unlisted_dependencies: Option<Severity>,
534 #[serde(
535 default,
536 alias = "duplicate-export",
537 skip_serializing_if = "Option::is_none"
538 )]
539 pub duplicate_exports: Option<Severity>,
540 #[serde(
541 default,
542 alias = "type-only-dependency",
543 skip_serializing_if = "Option::is_none"
544 )]
545 pub type_only_dependencies: Option<Severity>,
546 #[serde(
547 default,
548 alias = "test-only-dependency",
549 skip_serializing_if = "Option::is_none"
550 )]
551 pub test_only_dependencies: Option<Severity>,
552 #[serde(
553 default,
554 alias = "circular-dependency",
555 skip_serializing_if = "Option::is_none"
556 )]
557 pub circular_dependencies: Option<Severity>,
558 #[serde(
559 default,
560 alias = "re-export-cycles",
561 alias = "reexport-cycle",
562 alias = "reexport-cycles",
563 skip_serializing_if = "Option::is_none"
564 )]
565 pub re_export_cycle: Option<Severity>,
566 #[serde(
567 default,
568 alias = "boundary-violations",
569 skip_serializing_if = "Option::is_none"
570 )]
571 pub boundary_violation: Option<Severity>,
572 #[serde(
573 default,
574 alias = "coverage-gap",
575 skip_serializing_if = "Option::is_none"
576 )]
577 pub coverage_gaps: Option<Severity>,
578 #[serde(
579 default,
580 alias = "feature-flag",
581 skip_serializing_if = "Option::is_none"
582 )]
583 pub feature_flags: Option<Severity>,
584 #[serde(
585 default,
586 alias = "stale-suppression",
587 skip_serializing_if = "Option::is_none"
588 )]
589 pub stale_suppressions: Option<Severity>,
590 #[serde(
591 default,
592 alias = "unused-catalog-entry",
593 skip_serializing_if = "Option::is_none"
594 )]
595 pub unused_catalog_entries: Option<Severity>,
596 #[serde(
597 default,
598 alias = "empty-catalog-group",
599 skip_serializing_if = "Option::is_none"
600 )]
601 pub empty_catalog_groups: Option<Severity>,
602 #[serde(
603 default,
604 alias = "unresolved-catalog-reference",
605 skip_serializing_if = "Option::is_none"
606 )]
607 pub unresolved_catalog_references: Option<Severity>,
608 #[serde(
609 default,
610 alias = "unused-dependency-override",
611 skip_serializing_if = "Option::is_none"
612 )]
613 pub unused_dependency_overrides: Option<Severity>,
614 #[serde(
615 default,
616 alias = "misconfigured-dependency-override",
617 skip_serializing_if = "Option::is_none"
618 )]
619 pub misconfigured_dependency_overrides: Option<Severity>,
620 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub security_client_server_leak: Option<Severity>,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
623 pub security_sink: Option<Severity>,
624 #[serde(
625 default,
626 alias = "policy-violations",
627 skip_serializing_if = "Option::is_none"
628 )]
629 pub policy_violation: Option<Severity>,
630 #[serde(
631 default,
632 alias = "invalid-client-exports",
633 skip_serializing_if = "Option::is_none"
634 )]
635 pub invalid_client_export: Option<Severity>,
636 #[serde(
637 default,
638 alias = "mixed-client-server-barrels",
639 skip_serializing_if = "Option::is_none"
640 )]
641 pub mixed_client_server_barrel: Option<Severity>,
642 #[serde(
643 default,
644 alias = "misplaced-directives",
645 skip_serializing_if = "Option::is_none"
646 )]
647 pub misplaced_directive: Option<Severity>,
648 #[serde(
649 default,
650 alias = "route-collisions",
651 skip_serializing_if = "Option::is_none"
652 )]
653 pub route_collision: Option<Severity>,
654 #[serde(
655 default,
656 alias = "dynamic-segment-name-conflicts",
657 skip_serializing_if = "Option::is_none"
658 )]
659 pub dynamic_segment_name_conflict: Option<Severity>,
660}
661
662pub const KNOWN_RULE_NAMES: &[&str] = &[
673 "unused-files",
674 "unused-exports",
675 "unused-types",
676 "private-type-leaks",
677 "unused-dependencies",
678 "unused-dev-dependencies",
679 "unused-optional-dependencies",
680 "unused-enum-members",
681 "unused-class-members",
682 "unused-store-members",
683 "unprovided-injects",
684 "unrendered-components",
685 "unused-component-props",
686 "unused-component-emits",
687 "unused-server-actions",
688 "unused-load-data-keys",
689 "prop-drilling",
690 "thin-wrapper",
691 "duplicate-prop-shape",
692 "unresolved-imports",
693 "unlisted-dependencies",
694 "duplicate-exports",
695 "type-only-dependencies",
696 "test-only-dependencies",
697 "circular-dependencies",
698 "re-export-cycle",
699 "boundary-violation",
700 "coverage-gaps",
701 "feature-flags",
702 "stale-suppressions",
703 "unused-catalog-entries",
704 "empty-catalog-groups",
705 "unresolved-catalog-references",
706 "unused-dependency-overrides",
707 "misconfigured-dependency-overrides",
708 "security-client-server-leak",
709 "security-sink",
710 "policy-violation",
711 "policy-violations",
712 "invalid-client-export",
713 "mixed-client-server-barrel",
714 "misplaced-directive",
715 "route-collision",
716 "dynamic-segment-name-conflict",
717 "unused-file",
718 "unused-export",
719 "unused-type",
720 "private-type-leak",
721 "unused-dependency",
722 "unused-dev-dependency",
723 "unused-optional-dependency",
724 "unused-enum-member",
725 "unused-class-member",
726 "unused-store-member",
727 "unprovided-inject",
728 "unrendered-component",
729 "unused-component-prop",
730 "unused-component-emit",
731 "unused-server-action",
732 "unused-load-data-key",
733 "unresolved-import",
734 "unlisted-dependency",
735 "duplicate-export",
736 "type-only-dependency",
737 "test-only-dependency",
738 "circular-dependency",
739 "re-export-cycles",
740 "reexport-cycle",
741 "reexport-cycles",
742 "boundary-violations",
743 "coverage-gap",
744 "feature-flag",
745 "stale-suppression",
746 "unused-catalog-entry",
747 "empty-catalog-group",
748 "unresolved-catalog-reference",
749 "unused-dependency-override",
750 "misconfigured-dependency-override",
751 "invalid-client-exports",
752 "mixed-client-server-barrels",
753 "misplaced-directives",
754 "route-collisions",
755 "dynamic-segment-name-conflicts",
756];
757
758#[must_use]
764pub fn closest_known_rule_name(input: &str) -> Option<&'static str> {
765 let input_lower = input.to_ascii_lowercase();
766 let candidates = KNOWN_RULE_NAMES.iter().copied();
767 let suggestion = crate::levenshtein::closest_match(&input_lower, candidates)?;
768 KNOWN_RULE_NAMES.iter().copied().find(|&c| c == suggestion)
769}
770
771#[derive(Debug, Clone, PartialEq, Eq)]
777pub struct UnknownRuleKey {
778 pub context: String,
780 pub key: String,
782 pub suggestion: Option<&'static str>,
784}
785
786#[must_use]
794pub fn find_unknown_rule_keys(value: &serde_json::Value, context: &str) -> Vec<UnknownRuleKey> {
795 let Some(map) = value.as_object() else {
796 return Vec::new();
797 };
798
799 map.keys()
800 .filter(|key| !KNOWN_RULE_NAMES.contains(&key.as_str()))
801 .map(|key| UnknownRuleKey {
802 context: context.to_owned(),
803 key: key.clone(),
804 suggestion: closest_known_rule_name(key),
805 })
806 .collect()
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812
813 #[test]
814 fn rules_default_severities() {
815 let rules = RulesConfig::default();
816 assert_eq!(rules.unused_files, Severity::Error);
817 assert_eq!(rules.unused_exports, Severity::Error);
818 assert_eq!(rules.unused_types, Severity::Error);
819 assert_eq!(rules.private_type_leaks, Severity::Off);
820 assert_eq!(rules.unused_dependencies, Severity::Error);
821 assert_eq!(rules.unused_dev_dependencies, Severity::Warn);
822 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
823 assert_eq!(rules.unused_enum_members, Severity::Error);
824 assert_eq!(rules.unused_class_members, Severity::Error);
825 assert_eq!(rules.unresolved_imports, Severity::Error);
826 assert_eq!(rules.unlisted_dependencies, Severity::Error);
827 assert_eq!(rules.duplicate_exports, Severity::Error);
828 assert_eq!(rules.type_only_dependencies, Severity::Warn);
829 assert_eq!(rules.test_only_dependencies, Severity::Warn);
830 assert_eq!(rules.circular_dependencies, Severity::Error);
831 assert_eq!(rules.boundary_violation, Severity::Error);
832 assert_eq!(rules.coverage_gaps, Severity::Off);
833 assert_eq!(rules.feature_flags, Severity::Off);
834 assert_eq!(rules.stale_suppressions, Severity::Warn);
835 assert_eq!(rules.unused_catalog_entries, Severity::Warn);
836 assert_eq!(rules.empty_catalog_groups, Severity::Warn);
837 assert_eq!(rules.unresolved_catalog_references, Severity::Error);
838 }
839
840 #[test]
841 fn rules_deserialize_kebab_case() {
842 let json_str = r#"{
843 "unused-files": "error",
844 "unused-exports": "warn",
845 "unused-types": "off"
846 }"#;
847 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
848 assert_eq!(rules.unused_files, Severity::Error);
849 assert_eq!(rules.unused_exports, Severity::Warn);
850 assert_eq!(rules.unused_types, Severity::Off);
851 assert_eq!(rules.unresolved_imports, Severity::Error);
852 }
853
854 #[test]
855 fn rules_re_export_cycle_default_is_warn() {
856 let rules = RulesConfig::default();
857 assert_eq!(rules.re_export_cycle, Severity::Warn);
858 }
859
860 #[test]
861 fn rules_deserialize_re_export_cycle_aliases() {
862 for token in [
863 "re-export-cycle",
864 "re-export-cycles",
865 "reexport-cycle",
866 "reexport-cycles",
867 ] {
868 let json_str = format!(r#"{{ "{token}": "error" }}"#);
869 let rules: RulesConfig = serde_json::from_str(&json_str)
870 .unwrap_or_else(|e| panic!("alias {token} did not deserialize: {e}"));
871 assert_eq!(
872 rules.re_export_cycle,
873 Severity::Error,
874 "alias {token} should set re_export_cycle"
875 );
876 }
877 }
878
879 #[test]
880 fn rules_deserialize_circular_dependency_alias() {
881 let json_str = r#"{
882 "circular-dependency": "off"
883 }"#;
884 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
885 assert_eq!(rules.circular_dependencies, Severity::Off);
886 }
887
888 #[test]
889 fn rules_deserialize_boundary_violations_alias() {
890 let json_str = r#"{
891 "boundary-violations": "off"
892 }"#;
893 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
894 assert_eq!(rules.boundary_violation, Severity::Off);
895
896 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
897 assert_eq!(partial.boundary_violation, Some(Severity::Off));
898 }
899
900 #[test]
901 fn rules_deserialize_singular_aliases_for_every_plural_rule() {
902 let json_str = r#"{
903 "unused-file": "off",
904 "unused-export": "off",
905 "unused-type": "off",
906 "private-type-leak": "warn",
907 "unused-dependency": "off",
908 "unused-dev-dependency": "off",
909 "unused-optional-dependency": "off",
910 "unused-enum-member": "off",
911 "unused-class-member": "off",
912 "unresolved-import": "off",
913 "unlisted-dependency": "off",
914 "duplicate-export": "off",
915 "type-only-dependency": "off",
916 "test-only-dependency": "off",
917 "coverage-gap": "warn",
918 "feature-flag": "warn",
919 "stale-suppression": "off",
920 "unused-catalog-entry": "error",
921 "empty-catalog-group": "error",
922 "unresolved-catalog-reference": "warn"
923 }"#;
924
925 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
926 assert_eq!(rules.unused_files, Severity::Off);
927 assert_eq!(rules.unused_exports, Severity::Off);
928 assert_eq!(rules.unused_types, Severity::Off);
929 assert_eq!(rules.private_type_leaks, Severity::Warn);
930 assert_eq!(rules.unused_dependencies, Severity::Off);
931 assert_eq!(rules.unused_dev_dependencies, Severity::Off);
932 assert_eq!(rules.unused_optional_dependencies, Severity::Off);
933 assert_eq!(rules.unused_enum_members, Severity::Off);
934 assert_eq!(rules.unused_class_members, Severity::Off);
935 assert_eq!(rules.unresolved_imports, Severity::Off);
936 assert_eq!(rules.unlisted_dependencies, Severity::Off);
937 assert_eq!(rules.duplicate_exports, Severity::Off);
938 assert_eq!(rules.type_only_dependencies, Severity::Off);
939 assert_eq!(rules.test_only_dependencies, Severity::Off);
940 assert_eq!(rules.coverage_gaps, Severity::Warn);
941 assert_eq!(rules.feature_flags, Severity::Warn);
942 assert_eq!(rules.stale_suppressions, Severity::Off);
943 assert_eq!(rules.unused_catalog_entries, Severity::Error);
944 assert_eq!(rules.empty_catalog_groups, Severity::Error);
945 assert_eq!(rules.unresolved_catalog_references, Severity::Warn);
946
947 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
948 assert_eq!(partial.unused_files, Some(Severity::Off));
949 assert_eq!(partial.unused_exports, Some(Severity::Off));
950 assert_eq!(partial.unused_types, Some(Severity::Off));
951 assert_eq!(partial.private_type_leaks, Some(Severity::Warn));
952 assert_eq!(partial.unused_dependencies, Some(Severity::Off));
953 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Off));
954 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
955 assert_eq!(partial.unused_enum_members, Some(Severity::Off));
956 assert_eq!(partial.unused_class_members, Some(Severity::Off));
957 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
958 assert_eq!(partial.unlisted_dependencies, Some(Severity::Off));
959 assert_eq!(partial.duplicate_exports, Some(Severity::Off));
960 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
961 assert_eq!(partial.test_only_dependencies, Some(Severity::Off));
962 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
963 assert_eq!(partial.feature_flags, Some(Severity::Warn));
964 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
965 assert_eq!(partial.unused_catalog_entries, Some(Severity::Error));
966 assert_eq!(partial.empty_catalog_groups, Some(Severity::Error));
967 assert_eq!(partial.unresolved_catalog_references, Some(Severity::Warn));
968 }
969
970 #[test]
971 fn severity_from_str() {
972 assert_eq!("error".parse::<Severity>().unwrap(), Severity::Error);
973 assert_eq!("warn".parse::<Severity>().unwrap(), Severity::Warn);
974 assert_eq!("warning".parse::<Severity>().unwrap(), Severity::Warn);
975 assert_eq!("off".parse::<Severity>().unwrap(), Severity::Off);
976 assert_eq!("none".parse::<Severity>().unwrap(), Severity::Off);
977 assert!("invalid".parse::<Severity>().is_err());
978 }
979
980 #[test]
981 fn apply_partial_only_some_fields() {
982 let mut rules = RulesConfig::default();
983 let partial = PartialRulesConfig {
984 unused_files: Some(Severity::Warn),
985 unused_exports: Some(Severity::Off),
986 ..Default::default()
987 };
988 rules.apply_partial(&partial);
989 assert_eq!(rules.unused_files, Severity::Warn);
990 assert_eq!(rules.unused_exports, Severity::Off);
991 assert_eq!(rules.unused_types, Severity::Error);
992 assert_eq!(rules.unresolved_imports, Severity::Error);
993 }
994
995 #[test]
996 fn severity_display() {
997 assert_eq!(Severity::Error.to_string(), "error");
998 assert_eq!(Severity::Warn.to_string(), "warn");
999 assert_eq!(Severity::Off.to_string(), "off");
1000 }
1001
1002 #[test]
1003 fn apply_partial_all_none_changes_nothing() {
1004 let mut rules = RulesConfig::default();
1005 let original = rules.clone();
1006 let partial = PartialRulesConfig::default(); rules.apply_partial(&partial);
1008 assert_eq!(rules.unused_files, original.unused_files);
1009 assert_eq!(rules.unused_exports, original.unused_exports);
1010 assert_eq!(
1011 rules.type_only_dependencies,
1012 original.type_only_dependencies
1013 );
1014 }
1015
1016 #[test]
1017 fn apply_partial_all_fields_set() {
1018 let mut rules = RulesConfig::default();
1019 let partial = PartialRulesConfig {
1020 unused_files: Some(Severity::Off),
1021 unused_exports: Some(Severity::Off),
1022 unused_types: Some(Severity::Off),
1023 private_type_leaks: Some(Severity::Off),
1024 unused_dependencies: Some(Severity::Off),
1025 unused_dev_dependencies: Some(Severity::Off),
1026 unused_optional_dependencies: Some(Severity::Off),
1027 unused_enum_members: Some(Severity::Off),
1028 unused_class_members: Some(Severity::Off),
1029 unused_store_members: Some(Severity::Off),
1030 unprovided_injects: Some(Severity::Off),
1031 unrendered_components: Some(Severity::Off),
1032 unused_component_props: Some(Severity::Off),
1033 unused_component_emits: Some(Severity::Off),
1034 unused_server_actions: Some(Severity::Off),
1035 unused_load_data_keys: Some(Severity::Off),
1036 prop_drilling: Some(Severity::Off),
1037 thin_wrapper: Some(Severity::Off),
1038 duplicate_prop_shape: Some(Severity::Off),
1039 unresolved_imports: Some(Severity::Off),
1040 unlisted_dependencies: Some(Severity::Off),
1041 duplicate_exports: Some(Severity::Off),
1042 type_only_dependencies: Some(Severity::Off),
1043 test_only_dependencies: Some(Severity::Off),
1044 circular_dependencies: Some(Severity::Off),
1045 re_export_cycle: Some(Severity::Off),
1046 boundary_violation: Some(Severity::Off),
1047 coverage_gaps: Some(Severity::Off),
1048 feature_flags: Some(Severity::Off),
1049 stale_suppressions: Some(Severity::Off),
1050 unused_catalog_entries: Some(Severity::Off),
1051 empty_catalog_groups: Some(Severity::Off),
1052 unresolved_catalog_references: Some(Severity::Off),
1053 unused_dependency_overrides: Some(Severity::Off),
1054 misconfigured_dependency_overrides: Some(Severity::Off),
1055 security_client_server_leak: Some(Severity::Off),
1056 security_sink: Some(Severity::Off),
1057 policy_violation: Some(Severity::Off),
1058 invalid_client_export: Some(Severity::Off),
1059 mixed_client_server_barrel: Some(Severity::Off),
1060 misplaced_directive: Some(Severity::Off),
1061 route_collision: Some(Severity::Off),
1062 dynamic_segment_name_conflict: Some(Severity::Off),
1063 };
1064 rules.apply_partial(&partial);
1065 assert_eq!(rules.unused_files, Severity::Off);
1066 assert_eq!(rules.private_type_leaks, Severity::Off);
1067 assert_eq!(rules.circular_dependencies, Severity::Off);
1068 assert_eq!(rules.type_only_dependencies, Severity::Off);
1069 assert_eq!(rules.test_only_dependencies, Severity::Off);
1070 assert_eq!(rules.boundary_violation, Severity::Off);
1071 assert_eq!(rules.coverage_gaps, Severity::Off);
1072 assert_eq!(rules.feature_flags, Severity::Off);
1073 assert_eq!(rules.stale_suppressions, Severity::Off);
1074 assert_eq!(rules.security_sink, Severity::Off);
1075 assert_eq!(rules.policy_violation, Severity::Off);
1076 assert_eq!(rules.invalid_client_export, Severity::Off);
1077 assert_eq!(rules.mixed_client_server_barrel, Severity::Off);
1078 assert_eq!(rules.misplaced_directive, Severity::Off);
1079 assert_eq!(rules.unrendered_components, Severity::Off);
1080 assert_eq!(rules.unused_component_props, Severity::Off);
1081 assert_eq!(rules.unused_component_emits, Severity::Off);
1082 assert_eq!(rules.route_collision, Severity::Off);
1083 assert_eq!(rules.dynamic_segment_name_conflict, Severity::Off);
1084 }
1085
1086 #[test]
1087 fn rules_config_defaults_include_optional_deps() {
1088 let rules = RulesConfig::default();
1089 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
1090 }
1091
1092 #[test]
1093 fn policy_violation_defaults_to_warn() {
1094 let rules = RulesConfig::default();
1095 assert_eq!(rules.policy_violation, Severity::Warn);
1096 }
1097
1098 #[test]
1099 fn policy_violation_accepts_plural_alias() {
1100 let json = r#"{ "policy-violations": "error" }"#;
1101 let rules: RulesConfig = serde_json::from_str(json).unwrap();
1102 assert_eq!(rules.policy_violation, Severity::Error);
1103 }
1104
1105 #[test]
1106 fn severity_from_str_case_insensitive() {
1107 assert_eq!("ERROR".parse::<Severity>().unwrap(), Severity::Error);
1108 assert_eq!("Warn".parse::<Severity>().unwrap(), Severity::Warn);
1109 assert_eq!("OFF".parse::<Severity>().unwrap(), Severity::Off);
1110 assert_eq!("Warning".parse::<Severity>().unwrap(), Severity::Warn);
1111 assert_eq!("NONE".parse::<Severity>().unwrap(), Severity::Off);
1112 }
1113
1114 #[test]
1115 fn severity_from_str_invalid_returns_error() {
1116 let result = "critical".parse::<Severity>();
1117 assert!(result.is_err());
1118 let err = result.unwrap_err();
1119 assert!(
1120 err.contains("unknown severity"),
1121 "Expected descriptive error, got: {err}"
1122 );
1123 }
1124
1125 #[test]
1126 fn known_rule_names_count_matches_struct() {
1127 assert_eq!(KNOWN_RULE_NAMES.len(), 83);
1128 }
1129
1130 #[test]
1131 fn known_rule_names_has_no_duplicates() {
1132 let mut sorted: Vec<&str> = KNOWN_RULE_NAMES.to_vec();
1133 sorted.sort_unstable();
1134 let original_len = sorted.len();
1135 sorted.dedup();
1136 assert_eq!(
1137 sorted.len(),
1138 original_len,
1139 "KNOWN_RULE_NAMES contains a duplicate"
1140 );
1141 }
1142
1143 #[test]
1144 fn known_rule_names_covers_every_serde_alias_in_source() {
1145 let source = include_str!("rules.rs");
1146
1147 let mut aliases_found = Vec::new();
1148 for line in source.lines() {
1149 let trimmed = line.trim();
1150 if trimmed.starts_with("//") {
1151 continue;
1152 }
1153 let Some(after) = trimmed.split("alias = \"").nth(1) else {
1154 continue;
1155 };
1156 let Some(end) = after.find('"') else {
1157 continue;
1158 };
1159 let alias = &after[..end];
1160 if alias.is_empty() || !alias.chars().all(|c| c.is_ascii_lowercase() || c == '-') {
1161 continue;
1162 }
1163 aliases_found.push(alias.to_owned());
1164 }
1165
1166 assert_eq!(
1167 aliases_found.len(),
1168 86,
1169 "expected 86 source-level alias attrs (43 per struct); got {}: {:?}",
1170 aliases_found.len(),
1171 aliases_found
1172 );
1173
1174 for alias in &aliases_found {
1175 assert!(
1176 KNOWN_RULE_NAMES.contains(&alias.as_str()),
1177 "serde alias '{alias}' is in rules.rs source but missing from KNOWN_RULE_NAMES"
1178 );
1179 }
1180 }
1181
1182 #[test]
1183 fn re_export_cycle_aliases_all_round_trip_to_the_same_field() {
1184 for alias in [
1185 "re-export-cycle",
1186 "re-export-cycles",
1187 "reexport-cycle",
1188 "reexport-cycles",
1189 ] {
1190 let json = format!(r#"{{"{alias}": "warn"}}"#);
1191 let partial: PartialRulesConfig = serde_json::from_str(&json)
1192 .unwrap_or_else(|e| panic!("'{alias}' should deserialize: {e}"));
1193 assert_eq!(
1194 partial.re_export_cycle,
1195 Some(Severity::Warn),
1196 "'{alias}' should set re_export_cycle to Warn"
1197 );
1198 let serialized = serde_json::to_value(&partial).unwrap();
1199 let map = serialized.as_object().unwrap();
1200 assert_eq!(
1201 map.len(),
1202 1,
1203 "'{alias}' should resolve to exactly one field, got: {map:?}"
1204 );
1205 }
1206 }
1207
1208 #[test]
1209 fn every_known_rule_name_round_trips_through_partial() {
1210 for &name in KNOWN_RULE_NAMES {
1211 let json = format!(r#"{{"{name}": "warn"}}"#);
1212 let partial: PartialRulesConfig = serde_json::from_str(&json)
1213 .unwrap_or_else(|e| panic!("'{name}' should deserialize: {e}"));
1214
1215 let serialized = serde_json::to_value(&partial).unwrap();
1216 let map = serialized.as_object().unwrap();
1217 assert_eq!(
1218 map.len(),
1219 1,
1220 "'{name}' should resolve to exactly one field, got: {map:?}"
1221 );
1222 }
1223 }
1224
1225 #[test]
1226 fn known_rule_names_covers_every_struct_field() {
1227 let json = serde_json::to_value(RulesConfig::default()).unwrap();
1228 let obj = json.as_object().unwrap();
1229 for key in obj.keys() {
1230 assert!(
1231 KNOWN_RULE_NAMES.contains(&key.as_str()),
1232 "field '{key}' is serialized but missing from KNOWN_RULE_NAMES"
1233 );
1234 }
1235 }
1236
1237 #[test]
1238 fn closest_known_rule_name_suggests_for_obvious_typo() {
1239 assert_eq!(
1240 closest_known_rule_name("unsued-files"),
1241 Some("unused-files")
1242 );
1243 assert_eq!(
1244 closest_known_rule_name("circular-dependnecy"),
1245 Some("circular-dependency")
1246 );
1247 assert_eq!(
1248 closest_known_rule_name("unused-dep"),
1249 None,
1250 "too short for a confident suggestion"
1251 );
1252 }
1253
1254 #[test]
1255 fn closest_known_rule_name_returns_none_for_novel_input() {
1256 assert_eq!(closest_known_rule_name("totally-fabricated"), None);
1257 assert_eq!(closest_known_rule_name("foo"), None);
1258 }
1259
1260 #[test]
1261 fn closest_known_rule_name_is_case_insensitive() {
1262 assert_eq!(
1263 closest_known_rule_name("UNSUED-FILES"),
1264 Some("unused-files")
1265 );
1266 }
1267
1268 #[test]
1269 fn closest_known_rule_name_returns_none_for_exact_match() {
1270 assert_eq!(closest_known_rule_name("unused-files"), None);
1271 }
1272
1273 #[test]
1274 fn find_unknown_rule_keys_flags_typo() {
1275 let v = serde_json::json!({
1276 "unsued-files": "warn",
1277 "unused-exports": "off",
1278 });
1279 let unknown = find_unknown_rule_keys(&v, "rules");
1280 assert_eq!(unknown.len(), 1);
1281 assert_eq!(unknown[0].key, "unsued-files");
1282 assert_eq!(unknown[0].context, "rules");
1283 assert_eq!(unknown[0].suggestion, Some("unused-files"));
1284 }
1285
1286 #[test]
1287 fn find_unknown_rule_keys_passes_aliases() {
1288 let v = serde_json::json!({
1289 "unused-file": "warn",
1290 "circular-dependency": "off",
1291 "boundary-violations": "warn",
1292 });
1293 let unknown = find_unknown_rule_keys(&v, "rules");
1294 assert!(
1295 unknown.is_empty(),
1296 "documented aliases must not flag as unknown: {unknown:?}"
1297 );
1298 }
1299
1300 #[test]
1301 fn find_unknown_rule_keys_returns_multiple_typos() {
1302 let v = serde_json::json!({
1303 "unsued-files": "warn",
1304 "circular-dependnecy": "off",
1305 });
1306 let unknown = find_unknown_rule_keys(&v, "rules");
1307 assert_eq!(unknown.len(), 2);
1308 }
1309
1310 #[test]
1311 fn find_unknown_rule_keys_carries_context() {
1312 let v = serde_json::json!({ "unsued-files": "warn" });
1313 let unknown = find_unknown_rule_keys(&v, "overrides[2].rules");
1314 assert_eq!(unknown[0].context, "overrides[2].rules");
1315 }
1316
1317 #[test]
1318 fn find_unknown_rule_keys_empty_when_not_object() {
1319 let v = serde_json::json!(null);
1320 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
1321
1322 let v = serde_json::json!([1, 2, 3]);
1323 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
1324 }
1325
1326 #[test]
1327 fn find_unknown_rule_keys_no_suggestion_for_novel_name() {
1328 let v = serde_json::json!({ "totally-fabricated-rule": "warn" });
1329 let unknown = find_unknown_rule_keys(&v, "rules");
1330 assert_eq!(unknown.len(), 1);
1331 assert_eq!(unknown[0].suggestion, None);
1332 }
1333
1334 #[test]
1335 fn partial_rules_empty_json() {
1336 let partial: PartialRulesConfig = serde_json::from_str("{}").unwrap();
1337 assert!(partial.unused_files.is_none());
1338 assert!(partial.unused_exports.is_none());
1339 assert!(partial.unused_types.is_none());
1340 assert!(partial.unused_dependencies.is_none());
1341 assert!(partial.circular_dependencies.is_none());
1342 assert!(partial.boundary_violation.is_none());
1343 assert!(partial.coverage_gaps.is_none());
1344 assert!(partial.feature_flags.is_none());
1345 assert!(partial.stale_suppressions.is_none());
1346 }
1347
1348 #[test]
1349 fn partial_rules_subset_json() {
1350 let json = r#"{
1351 "unused-files": "warn",
1352 "circular-dependencies": "off"
1353 }"#;
1354 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1355 assert_eq!(partial.unused_files, Some(Severity::Warn));
1356 assert_eq!(partial.circular_dependencies, Some(Severity::Off));
1357 assert!(partial.unused_exports.is_none());
1358 }
1359
1360 #[test]
1361 fn partial_rules_deserialize_circular_dependency_alias() {
1362 let json = r#"{
1363 "circular-dependency": "warn"
1364 }"#;
1365 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1366 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
1367 }
1368
1369 #[test]
1370 fn partial_rules_all_fields_json() {
1371 let json = r#"{
1372 "unused-files": "error",
1373 "unused-exports": "warn",
1374 "unused-types": "off",
1375 "unused-dependencies": "error",
1376 "unused-dev-dependencies": "warn",
1377 "unused-optional-dependencies": "off",
1378 "unused-enum-members": "error",
1379 "unused-class-members": "warn",
1380 "unresolved-imports": "off",
1381 "unlisted-dependencies": "error",
1382 "duplicate-exports": "warn",
1383 "type-only-dependencies": "off",
1384 "test-only-dependencies": "error",
1385 "circular-dependencies": "warn",
1386 "boundary-violation": "off",
1387 "coverage-gaps": "warn",
1388 "feature-flags": "error",
1389 "stale-suppressions": "off"
1390 }"#;
1391 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1392 assert_eq!(partial.unused_files, Some(Severity::Error));
1393 assert_eq!(partial.unused_exports, Some(Severity::Warn));
1394 assert_eq!(partial.unused_types, Some(Severity::Off));
1395 assert_eq!(partial.unused_dependencies, Some(Severity::Error));
1396 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Warn));
1397 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
1398 assert_eq!(partial.unused_enum_members, Some(Severity::Error));
1399 assert_eq!(partial.unused_class_members, Some(Severity::Warn));
1400 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
1401 assert_eq!(partial.unlisted_dependencies, Some(Severity::Error));
1402 assert_eq!(partial.duplicate_exports, Some(Severity::Warn));
1403 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
1404 assert_eq!(partial.test_only_dependencies, Some(Severity::Error));
1405 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
1406 assert_eq!(partial.boundary_violation, Some(Severity::Off));
1407 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
1408 assert_eq!(partial.feature_flags, Some(Severity::Error));
1409 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
1410 }
1411
1412 #[test]
1413 fn partial_rules_none_fields_not_serialized() {
1414 let partial = PartialRulesConfig::default();
1415 let json = serde_json::to_string(&partial).unwrap();
1416 assert_eq!(
1417 json, "{}",
1418 "all-None partial should serialize to empty object"
1419 );
1420 }
1421
1422 #[test]
1423 fn partial_rules_some_fields_serialized() {
1424 let partial = PartialRulesConfig {
1425 unused_files: Some(Severity::Warn),
1426 ..Default::default()
1427 };
1428 let json = serde_json::to_string(&partial).unwrap();
1429 assert!(json.contains("unused-files"));
1430 assert!(!json.contains("unused-exports"));
1431 }
1432
1433 #[test]
1434 fn severity_json_deserialization() {
1435 let error: Severity = serde_json::from_str(r#""error""#).unwrap();
1436 assert_eq!(error, Severity::Error);
1437
1438 let warn: Severity = serde_json::from_str(r#""warn""#).unwrap();
1439 assert_eq!(warn, Severity::Warn);
1440
1441 let off: Severity = serde_json::from_str(r#""off""#).unwrap();
1442 assert_eq!(off, Severity::Off);
1443 }
1444
1445 #[test]
1446 fn severity_invalid_json_value_rejected() {
1447 let result: Result<Severity, _> = serde_json::from_str(r#""critical""#);
1448 assert!(result.is_err());
1449 }
1450
1451 #[test]
1452 fn severity_default_is_error() {
1453 assert_eq!(Severity::default(), Severity::Error);
1454 }
1455
1456 #[test]
1457 fn rules_config_json_roundtrip() {
1458 let rules = RulesConfig {
1459 unused_files: Severity::Warn,
1460 unused_exports: Severity::Off,
1461 type_only_dependencies: Severity::Error,
1462 ..RulesConfig::default()
1463 };
1464 let json = serde_json::to_string(&rules).unwrap();
1465 let restored: RulesConfig = serde_json::from_str(&json).unwrap();
1466 assert_eq!(restored.unused_files, Severity::Warn);
1467 assert_eq!(restored.unused_exports, Severity::Off);
1468 assert_eq!(restored.type_only_dependencies, Severity::Error);
1469 assert_eq!(restored.unused_dependencies, Severity::Error); }
1471
1472 #[test]
1473 fn apply_partial_preserves_type_only_default() {
1474 let mut rules = RulesConfig::default();
1475 let partial = PartialRulesConfig {
1476 unused_files: Some(Severity::Off),
1477 ..Default::default()
1478 };
1479 rules.apply_partial(&partial);
1480 assert_eq!(rules.type_only_dependencies, Severity::Warn);
1481 assert_eq!(rules.test_only_dependencies, Severity::Warn);
1482 }
1483}