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