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