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