1use fallow_types::suppress::IssueKind;
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
10#[serde(rename_all = "lowercase")]
11pub enum Severity {
12 #[default]
14 Error,
15 Warn,
17 Off,
19}
20
21impl Severity {
22 const fn default_warn() -> Self {
24 Self::Warn
25 }
26
27 const fn default_off() -> Self {
29 Self::Off
30 }
31}
32
33impl std::fmt::Display for Severity {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 Self::Error => write!(f, "error"),
37 Self::Warn => write!(f, "warn"),
38 Self::Off => write!(f, "off"),
39 }
40 }
41}
42
43impl std::str::FromStr for Severity {
44 type Err = String;
45
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 match s.to_lowercase().as_str() {
48 "error" => Ok(Self::Error),
49 "warn" | "warning" => Ok(Self::Warn),
50 "off" | "none" => Ok(Self::Off),
51 other => Err(format!(
52 "unknown severity: '{other}' (expected error, warn, or off)"
53 )),
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
65#[serde(rename_all = "kebab-case")]
66pub struct RulesConfig {
67 #[serde(default, alias = "unused-file")]
68 pub unused_files: Severity,
69 #[serde(default, alias = "unused-export")]
70 pub unused_exports: Severity,
71 #[serde(default, alias = "unused-type")]
72 pub unused_types: Severity,
73 #[serde(default = "Severity::default_off", alias = "private-type-leak")]
74 pub private_type_leaks: Severity,
75 #[serde(default, alias = "unused-dependency")]
76 pub unused_dependencies: Severity,
77 #[serde(default = "Severity::default_warn", alias = "unused-dev-dependency")]
78 pub unused_dev_dependencies: Severity,
79 #[serde(
80 default = "Severity::default_warn",
81 alias = "unused-optional-dependency"
82 )]
83 pub unused_optional_dependencies: Severity,
84 #[serde(default, alias = "unused-enum-member")]
85 pub unused_enum_members: Severity,
86 #[serde(default, alias = "unused-class-member")]
87 pub unused_class_members: Severity,
88 #[serde(default = "Severity::default_warn", alias = "unused-store-member")]
96 pub unused_store_members: Severity,
97 #[serde(default = "Severity::default_warn", alias = "unprovided-inject")]
103 pub unprovided_injects: Severity,
104 #[serde(default = "Severity::default_warn", alias = "unrendered-component")]
110 pub unrendered_components: Severity,
111 #[serde(default = "Severity::default_warn", alias = "unused-component-prop")]
117 pub unused_component_props: Severity,
118 #[serde(default = "Severity::default_warn", alias = "unused-component-emit")]
124 pub unused_component_emits: Severity,
125 #[serde(default = "Severity::default_warn", alias = "unused-component-input")]
132 pub unused_component_inputs: Severity,
133 #[serde(default = "Severity::default_warn", 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 = "Severity::default_warn", 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 = "Severity::default_warn", alias = "css-token-drift")]
194 pub css_token_drift: Severity,
195 #[serde(default = "Severity::default_warn", alias = "css-duplicate-block")]
199 pub css_duplicate_block: Severity,
200 #[serde(default = "Severity::default_warn", alias = "css-selector-complexity")]
204 pub css_selector_complexity: Severity,
205 #[serde(default = "Severity::default_warn", alias = "css-dead-surface")]
209 pub css_dead_surface: Severity,
210 #[serde(default = "Severity::default_warn", alias = "css-broken-reference")]
214 pub css_broken_reference: Severity,
215 #[serde(default, alias = "unresolved-import")]
216 pub unresolved_imports: Severity,
217 #[serde(default, alias = "unlisted-dependency")]
218 pub unlisted_dependencies: Severity,
219 #[serde(default, alias = "duplicate-export")]
220 pub duplicate_exports: Severity,
221 #[serde(default = "Severity::default_warn", alias = "type-only-dependency")]
222 pub type_only_dependencies: Severity,
223 #[serde(default = "Severity::default_warn", alias = "test-only-dependency")]
224 pub test_only_dependencies: Severity,
225 #[serde(
226 default = "Severity::default_warn",
227 alias = "dev-dependency-in-production"
228 )]
229 pub dev_dependencies_in_production: Severity,
230 #[serde(default, alias = "circular-dependency")]
231 pub circular_dependencies: Severity,
232 #[serde(
233 default = "Severity::default_warn",
234 alias = "re-export-cycles",
235 alias = "reexport-cycle",
236 alias = "reexport-cycles"
237 )]
238 pub re_export_cycle: Severity,
239 #[serde(default, alias = "boundary-violations")]
240 pub boundary_violation: Severity,
241 #[serde(default = "Severity::default_off", alias = "coverage-gap")]
242 pub coverage_gaps: Severity,
243 #[serde(default = "Severity::default_off", alias = "feature-flag")]
244 pub feature_flags: Severity,
245 #[serde(default = "Severity::default_warn", alias = "stale-suppression")]
246 pub stale_suppressions: Severity,
247 #[serde(default = "Severity::default_off", alias = "suppression-reason")]
250 pub require_suppression_reason: Severity,
251 #[serde(default = "Severity::default_warn", alias = "unused-catalog-entry")]
252 pub unused_catalog_entries: Severity,
253 #[serde(default = "Severity::default_warn", alias = "empty-catalog-group")]
254 pub empty_catalog_groups: Severity,
255 #[serde(default, alias = "unresolved-catalog-reference")]
256 pub unresolved_catalog_references: Severity,
257 #[serde(
258 default = "Severity::default_warn",
259 alias = "unused-dependency-override"
260 )]
261 pub unused_dependency_overrides: Severity,
262 #[serde(default, alias = "misconfigured-dependency-override")]
263 pub misconfigured_dependency_overrides: Severity,
264 #[serde(default = "Severity::default_off")]
268 pub security_client_server_leak: Severity,
269 #[serde(default = "Severity::default_off")]
274 pub security_sink: Severity,
275 #[serde(default = "Severity::default_warn", alias = "policy-violations")]
281 pub policy_violation: Severity,
282 #[serde(default = "Severity::default_warn", alias = "invalid-client-exports")]
287 pub invalid_client_export: Severity,
288 #[serde(
293 default = "Severity::default_warn",
294 alias = "mixed-client-server-barrels"
295 )]
296 pub mixed_client_server_barrel: Severity,
297 #[serde(default = "Severity::default_warn", alias = "misplaced-directives")]
303 pub misplaced_directive: Severity,
304 #[serde(default, alias = "route-collisions")]
311 pub route_collision: Severity,
312 #[serde(default, alias = "dynamic-segment-name-conflicts")]
322 pub dynamic_segment_name_conflict: Severity,
323}
324
325impl Default for RulesConfig {
326 fn default() -> Self {
327 Self {
328 unused_files: Severity::Error,
329 unused_exports: Severity::Error,
330 unused_types: Severity::Error,
331 private_type_leaks: Severity::Off,
332 unused_dependencies: Severity::Error,
333 unused_dev_dependencies: Severity::Warn,
334 unused_optional_dependencies: Severity::Warn,
335 unused_enum_members: Severity::Error,
336 unused_class_members: Severity::Error,
337 unused_store_members: Severity::Warn,
338 unprovided_injects: Severity::Warn,
339 unrendered_components: Severity::Warn,
340 unused_component_props: Severity::Warn,
341 unused_component_emits: Severity::Warn,
342 unused_component_inputs: Severity::Warn,
343 unused_component_outputs: Severity::Warn,
344 unused_svelte_events: Severity::Warn,
345 unused_server_actions: Severity::Warn,
346 unused_load_data_keys: Severity::Warn,
347 prop_drilling: Severity::Off,
348 thin_wrapper: Severity::Off,
349 duplicate_prop_shape: Severity::Off,
350 css_token_drift: Severity::Warn,
351 css_duplicate_block: Severity::Warn,
352 css_selector_complexity: Severity::Warn,
353 css_dead_surface: Severity::Warn,
354 css_broken_reference: Severity::Warn,
355 unresolved_imports: Severity::Error,
356 unlisted_dependencies: Severity::Error,
357 duplicate_exports: Severity::Error,
358 type_only_dependencies: Severity::Warn,
359 test_only_dependencies: Severity::Warn,
360 dev_dependencies_in_production: Severity::Warn,
361 circular_dependencies: Severity::Error,
362 re_export_cycle: Severity::Warn,
363 boundary_violation: Severity::Error,
364 coverage_gaps: Severity::Off,
365 feature_flags: Severity::Off,
366 stale_suppressions: Severity::Warn,
367 require_suppression_reason: Severity::Off,
368 unused_catalog_entries: Severity::Warn,
369 empty_catalog_groups: Severity::Warn,
370 unresolved_catalog_references: Severity::Error,
371 unused_dependency_overrides: Severity::Warn,
372 misconfigured_dependency_overrides: Severity::Error,
373 security_client_server_leak: Severity::Off,
374 security_sink: Severity::Off,
375 policy_violation: Severity::Warn,
376 invalid_client_export: Severity::Warn,
377 mixed_client_server_barrel: Severity::Warn,
378 misplaced_directive: Severity::Warn,
379 route_collision: Severity::Error,
380 dynamic_segment_name_conflict: Severity::Error,
381 }
382 }
383}
384
385macro_rules! apply_partial_rules {
386 ($target:expr, $partial:expr, [$($field:ident),+ $(,)?]) => {
387 $(
388 if let Some(severity) = $partial.$field {
389 $target.$field = severity;
390 }
391 )+
392 };
393}
394
395impl RulesConfig {
396 #[must_use]
408 pub const fn severity_for_kind(&self, kind: IssueKind) -> Severity {
409 match kind {
410 IssueKind::UnusedFile => self.unused_files,
411 IssueKind::UnusedExport => self.unused_exports,
412 IssueKind::UnusedType => self.unused_types,
413 IssueKind::PrivateTypeLeak => self.private_type_leaks,
414 IssueKind::UnusedDependency => self.unused_dependencies,
415 IssueKind::UnusedDevDependency => self.unused_dev_dependencies,
416 IssueKind::UnusedEnumMember => self.unused_enum_members,
417 IssueKind::UnusedClassMember => self.unused_class_members,
418 IssueKind::UnusedStoreMember => self.unused_store_members,
419 IssueKind::UnprovidedInject => self.unprovided_injects,
420 IssueKind::UnresolvedImport => self.unresolved_imports,
421 IssueKind::UnlistedDependency => self.unlisted_dependencies,
422 IssueKind::DuplicateExport => self.duplicate_exports,
423 IssueKind::CircularDependency => self.circular_dependencies,
424 IssueKind::ReExportCycle => self.re_export_cycle,
425 IssueKind::TypeOnlyDependency => self.type_only_dependencies,
426 IssueKind::TestOnlyDependency => self.test_only_dependencies,
427 IssueKind::DevDependencyInProduction => self.dev_dependencies_in_production,
428 IssueKind::BoundaryViolation => self.boundary_violation,
429 IssueKind::CoverageGaps => self.coverage_gaps,
430 IssueKind::FeatureFlag => self.feature_flags,
431 IssueKind::StaleSuppression => self.stale_suppressions,
432 IssueKind::PnpmCatalogEntry => self.unused_catalog_entries,
433 IssueKind::EmptyCatalogGroup => self.empty_catalog_groups,
434 IssueKind::UnresolvedCatalogReference => self.unresolved_catalog_references,
435 IssueKind::UnusedDependencyOverride => self.unused_dependency_overrides,
436 IssueKind::MisconfiguredDependencyOverride => self.misconfigured_dependency_overrides,
437 IssueKind::SecurityClientServerLeak => self.security_client_server_leak,
438 IssueKind::SecuritySink => self.security_sink,
439 IssueKind::PolicyViolation => self.policy_violation,
440 IssueKind::InvalidClientExport => self.invalid_client_export,
441 IssueKind::MixedClientServerBarrel => self.mixed_client_server_barrel,
442 IssueKind::MisplacedDirective => self.misplaced_directive,
443 IssueKind::RouteCollision => self.route_collision,
444 IssueKind::DynamicSegmentNameConflict => self.dynamic_segment_name_conflict,
445 IssueKind::UnrenderedComponent => self.unrendered_components,
446 IssueKind::UnusedComponentProp => self.unused_component_props,
447 IssueKind::UnusedComponentEmit => self.unused_component_emits,
448 IssueKind::UnusedComponentInput => self.unused_component_inputs,
449 IssueKind::UnusedComponentOutput => self.unused_component_outputs,
450 IssueKind::UnusedSvelteEvent => self.unused_svelte_events,
451 IssueKind::UnusedServerAction => self.unused_server_actions,
452 IssueKind::UnusedLoadDataKey => self.unused_load_data_keys,
453 IssueKind::PropDrilling => self.prop_drilling,
454 IssueKind::ThinWrapper => self.thin_wrapper,
455 IssueKind::DuplicatePropShape => self.duplicate_prop_shape,
456 IssueKind::CssTokenDrift => self.css_token_drift,
457 IssueKind::CssDuplicateBlock => self.css_duplicate_block,
458 IssueKind::CssSelectorComplexity => self.css_selector_complexity,
459 IssueKind::CssDeadSurface => self.css_dead_surface,
460 IssueKind::CssBrokenReference => self.css_broken_reference,
461 IssueKind::Complexity | IssueKind::CodeDuplication => Severity::Error,
462 }
463 }
464
465 pub const fn apply_partial(&mut self, partial: &PartialRulesConfig) {
467 apply_partial_rules!(
468 self,
469 partial,
470 [
471 unused_files,
472 unused_exports,
473 unused_types,
474 private_type_leaks,
475 unused_dependencies,
476 unused_dev_dependencies,
477 unused_optional_dependencies,
478 ]
479 );
480 apply_partial_rules!(
481 self,
482 partial,
483 [
484 unused_enum_members,
485 unused_class_members,
486 unused_store_members,
487 unprovided_injects,
488 unrendered_components,
489 unused_component_props,
490 unused_component_emits,
491 unused_component_inputs,
492 unused_component_outputs,
493 unused_svelte_events,
494 unused_server_actions,
495 unused_load_data_keys,
496 prop_drilling,
497 thin_wrapper,
498 duplicate_prop_shape,
499 css_token_drift,
500 css_duplicate_block,
501 css_selector_complexity,
502 css_dead_surface,
503 css_broken_reference,
504 ]
505 );
506 apply_partial_rules!(
507 self,
508 partial,
509 [
510 unresolved_imports,
511 unlisted_dependencies,
512 duplicate_exports,
513 type_only_dependencies,
514 test_only_dependencies,
515 dev_dependencies_in_production,
516 circular_dependencies,
517 re_export_cycle,
518 boundary_violation,
519 ]
520 );
521 apply_partial_rules!(
522 self,
523 partial,
524 [
525 coverage_gaps,
526 feature_flags,
527 stale_suppressions,
528 require_suppression_reason,
529 unused_catalog_entries,
530 empty_catalog_groups,
531 unresolved_catalog_references,
532 unused_dependency_overrides,
533 misconfigured_dependency_overrides,
534 ]
535 );
536 apply_partial_rules!(
537 self,
538 partial,
539 [
540 security_client_server_leak,
541 security_sink,
542 policy_violation,
543 invalid_client_export,
544 mixed_client_server_barrel,
545 misplaced_directive,
546 route_collision,
547 dynamic_segment_name_conflict,
548 ]
549 );
550 }
551}
552
553#[must_use]
561pub fn default_severity_for_kind(kind: IssueKind) -> Severity {
562 RulesConfig::default().severity_for_kind(kind)
563}
564
565#[must_use]
572pub fn is_opt_in_kind(kind: IssueKind) -> bool {
573 matches!(default_severity_for_kind(kind), Severity::Off)
574}
575
576#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
578#[serde(rename_all = "kebab-case")]
579pub struct PartialRulesConfig {
580 #[serde(
581 default,
582 alias = "unused-file",
583 skip_serializing_if = "Option::is_none"
584 )]
585 pub unused_files: Option<Severity>,
586 #[serde(
587 default,
588 alias = "unused-export",
589 skip_serializing_if = "Option::is_none"
590 )]
591 pub unused_exports: Option<Severity>,
592 #[serde(
593 default,
594 alias = "unused-type",
595 skip_serializing_if = "Option::is_none"
596 )]
597 pub unused_types: Option<Severity>,
598 #[serde(
599 default,
600 alias = "private-type-leak",
601 skip_serializing_if = "Option::is_none"
602 )]
603 pub private_type_leaks: Option<Severity>,
604 #[serde(
605 default,
606 alias = "unused-dependency",
607 skip_serializing_if = "Option::is_none"
608 )]
609 pub unused_dependencies: Option<Severity>,
610 #[serde(
611 default,
612 alias = "unused-dev-dependency",
613 skip_serializing_if = "Option::is_none"
614 )]
615 pub unused_dev_dependencies: Option<Severity>,
616 #[serde(
617 default,
618 alias = "unused-optional-dependency",
619 skip_serializing_if = "Option::is_none"
620 )]
621 pub unused_optional_dependencies: Option<Severity>,
622 #[serde(
623 default,
624 alias = "unused-enum-member",
625 skip_serializing_if = "Option::is_none"
626 )]
627 pub unused_enum_members: Option<Severity>,
628 #[serde(
629 default,
630 alias = "unused-class-member",
631 skip_serializing_if = "Option::is_none"
632 )]
633 pub unused_class_members: Option<Severity>,
634 #[serde(
635 default,
636 alias = "unused-store-member",
637 skip_serializing_if = "Option::is_none"
638 )]
639 pub unused_store_members: Option<Severity>,
640 #[serde(
641 default,
642 alias = "unprovided-inject",
643 skip_serializing_if = "Option::is_none"
644 )]
645 pub unprovided_injects: Option<Severity>,
646 #[serde(
647 default,
648 alias = "unrendered-component",
649 skip_serializing_if = "Option::is_none"
650 )]
651 pub unrendered_components: Option<Severity>,
652 #[serde(
653 default,
654 alias = "unused-component-prop",
655 skip_serializing_if = "Option::is_none"
656 )]
657 pub unused_component_props: Option<Severity>,
658 #[serde(
659 default,
660 alias = "unused-component-emit",
661 skip_serializing_if = "Option::is_none"
662 )]
663 pub unused_component_emits: Option<Severity>,
664 #[serde(
665 default,
666 alias = "unused-component-input",
667 skip_serializing_if = "Option::is_none"
668 )]
669 pub unused_component_inputs: Option<Severity>,
670 #[serde(
671 default,
672 alias = "unused-component-output",
673 skip_serializing_if = "Option::is_none"
674 )]
675 pub unused_component_outputs: Option<Severity>,
676 #[serde(
677 default,
678 alias = "unused-svelte-event",
679 skip_serializing_if = "Option::is_none"
680 )]
681 pub unused_svelte_events: Option<Severity>,
682 #[serde(
683 default,
684 alias = "unused-server-action",
685 skip_serializing_if = "Option::is_none"
686 )]
687 pub unused_server_actions: Option<Severity>,
688 #[serde(
689 default,
690 alias = "unused-load-data-key",
691 skip_serializing_if = "Option::is_none"
692 )]
693 pub unused_load_data_keys: Option<Severity>,
694 #[serde(
695 default,
696 alias = "prop-drilling",
697 skip_serializing_if = "Option::is_none"
698 )]
699 pub prop_drilling: Option<Severity>,
700 #[serde(
701 default,
702 alias = "thin-wrapper",
703 skip_serializing_if = "Option::is_none"
704 )]
705 pub thin_wrapper: Option<Severity>,
706 #[serde(
707 default,
708 alias = "duplicate-prop-shape",
709 skip_serializing_if = "Option::is_none"
710 )]
711 pub duplicate_prop_shape: Option<Severity>,
712 #[serde(
713 default,
714 alias = "css-token-drift",
715 skip_serializing_if = "Option::is_none"
716 )]
717 pub css_token_drift: Option<Severity>,
718 #[serde(
719 default,
720 alias = "css-duplicate-block",
721 skip_serializing_if = "Option::is_none"
722 )]
723 pub css_duplicate_block: Option<Severity>,
724 #[serde(
725 default,
726 alias = "css-selector-complexity",
727 skip_serializing_if = "Option::is_none"
728 )]
729 pub css_selector_complexity: Option<Severity>,
730 #[serde(
731 default,
732 alias = "css-dead-surface",
733 skip_serializing_if = "Option::is_none"
734 )]
735 pub css_dead_surface: Option<Severity>,
736 #[serde(
737 default,
738 alias = "css-broken-reference",
739 skip_serializing_if = "Option::is_none"
740 )]
741 pub css_broken_reference: Option<Severity>,
742 #[serde(
743 default,
744 alias = "unresolved-import",
745 skip_serializing_if = "Option::is_none"
746 )]
747 pub unresolved_imports: Option<Severity>,
748 #[serde(
749 default,
750 alias = "unlisted-dependency",
751 skip_serializing_if = "Option::is_none"
752 )]
753 pub unlisted_dependencies: Option<Severity>,
754 #[serde(
755 default,
756 alias = "duplicate-export",
757 skip_serializing_if = "Option::is_none"
758 )]
759 pub duplicate_exports: Option<Severity>,
760 #[serde(
761 default,
762 alias = "type-only-dependency",
763 skip_serializing_if = "Option::is_none"
764 )]
765 pub type_only_dependencies: Option<Severity>,
766 #[serde(
767 default,
768 alias = "test-only-dependency",
769 skip_serializing_if = "Option::is_none"
770 )]
771 pub test_only_dependencies: Option<Severity>,
772 #[serde(
773 default,
774 alias = "dev-dependency-in-production",
775 skip_serializing_if = "Option::is_none"
776 )]
777 pub dev_dependencies_in_production: Option<Severity>,
778 #[serde(
779 default,
780 alias = "circular-dependency",
781 skip_serializing_if = "Option::is_none"
782 )]
783 pub circular_dependencies: Option<Severity>,
784 #[serde(
785 default,
786 alias = "re-export-cycles",
787 alias = "reexport-cycle",
788 alias = "reexport-cycles",
789 skip_serializing_if = "Option::is_none"
790 )]
791 pub re_export_cycle: Option<Severity>,
792 #[serde(
793 default,
794 alias = "boundary-violations",
795 skip_serializing_if = "Option::is_none"
796 )]
797 pub boundary_violation: Option<Severity>,
798 #[serde(
799 default,
800 alias = "coverage-gap",
801 skip_serializing_if = "Option::is_none"
802 )]
803 pub coverage_gaps: Option<Severity>,
804 #[serde(
805 default,
806 alias = "feature-flag",
807 skip_serializing_if = "Option::is_none"
808 )]
809 pub feature_flags: Option<Severity>,
810 #[serde(
811 default,
812 alias = "stale-suppression",
813 skip_serializing_if = "Option::is_none"
814 )]
815 pub stale_suppressions: Option<Severity>,
816 #[serde(
817 default,
818 alias = "suppression-reason",
819 skip_serializing_if = "Option::is_none"
820 )]
821 pub require_suppression_reason: Option<Severity>,
822 #[serde(
823 default,
824 alias = "unused-catalog-entry",
825 skip_serializing_if = "Option::is_none"
826 )]
827 pub unused_catalog_entries: Option<Severity>,
828 #[serde(
829 default,
830 alias = "empty-catalog-group",
831 skip_serializing_if = "Option::is_none"
832 )]
833 pub empty_catalog_groups: Option<Severity>,
834 #[serde(
835 default,
836 alias = "unresolved-catalog-reference",
837 skip_serializing_if = "Option::is_none"
838 )]
839 pub unresolved_catalog_references: Option<Severity>,
840 #[serde(
841 default,
842 alias = "unused-dependency-override",
843 skip_serializing_if = "Option::is_none"
844 )]
845 pub unused_dependency_overrides: Option<Severity>,
846 #[serde(
847 default,
848 alias = "misconfigured-dependency-override",
849 skip_serializing_if = "Option::is_none"
850 )]
851 pub misconfigured_dependency_overrides: Option<Severity>,
852 #[serde(default, skip_serializing_if = "Option::is_none")]
853 pub security_client_server_leak: Option<Severity>,
854 #[serde(default, skip_serializing_if = "Option::is_none")]
855 pub security_sink: Option<Severity>,
856 #[serde(
857 default,
858 alias = "policy-violations",
859 skip_serializing_if = "Option::is_none"
860 )]
861 pub policy_violation: Option<Severity>,
862 #[serde(
863 default,
864 alias = "invalid-client-exports",
865 skip_serializing_if = "Option::is_none"
866 )]
867 pub invalid_client_export: Option<Severity>,
868 #[serde(
869 default,
870 alias = "mixed-client-server-barrels",
871 skip_serializing_if = "Option::is_none"
872 )]
873 pub mixed_client_server_barrel: Option<Severity>,
874 #[serde(
875 default,
876 alias = "misplaced-directives",
877 skip_serializing_if = "Option::is_none"
878 )]
879 pub misplaced_directive: Option<Severity>,
880 #[serde(
881 default,
882 alias = "route-collisions",
883 skip_serializing_if = "Option::is_none"
884 )]
885 pub route_collision: Option<Severity>,
886 #[serde(
887 default,
888 alias = "dynamic-segment-name-conflicts",
889 skip_serializing_if = "Option::is_none"
890 )]
891 pub dynamic_segment_name_conflict: Option<Severity>,
892}
893
894pub const KNOWN_RULE_NAMES: &[&str] = &[
905 "unused-files",
906 "unused-exports",
907 "unused-types",
908 "private-type-leaks",
909 "unused-dependencies",
910 "unused-dev-dependencies",
911 "unused-optional-dependencies",
912 "unused-enum-members",
913 "unused-class-members",
914 "unused-store-members",
915 "unprovided-injects",
916 "unrendered-components",
917 "unused-component-props",
918 "unused-component-emits",
919 "unused-component-inputs",
920 "unused-component-outputs",
921 "unused-svelte-events",
922 "unused-server-actions",
923 "unused-load-data-keys",
924 "prop-drilling",
925 "thin-wrapper",
926 "duplicate-prop-shape",
927 "css-token-drift",
928 "css-duplicate-block",
929 "css-selector-complexity",
930 "css-dead-surface",
931 "css-broken-reference",
932 "unresolved-imports",
933 "unlisted-dependencies",
934 "duplicate-exports",
935 "type-only-dependencies",
936 "test-only-dependencies",
937 "dev-dependencies-in-production",
938 "circular-dependencies",
939 "re-export-cycle",
940 "boundary-violation",
941 "coverage-gaps",
942 "feature-flags",
943 "stale-suppressions",
944 "require-suppression-reason",
945 "suppression-reason",
946 "unused-catalog-entries",
947 "empty-catalog-groups",
948 "unresolved-catalog-references",
949 "unused-dependency-overrides",
950 "misconfigured-dependency-overrides",
951 "security-client-server-leak",
952 "security-sink",
953 "policy-violation",
954 "policy-violations",
955 "invalid-client-export",
956 "mixed-client-server-barrel",
957 "misplaced-directive",
958 "route-collision",
959 "dynamic-segment-name-conflict",
960 "unused-file",
961 "unused-export",
962 "unused-type",
963 "private-type-leak",
964 "unused-dependency",
965 "unused-dev-dependency",
966 "unused-optional-dependency",
967 "unused-enum-member",
968 "unused-class-member",
969 "unused-store-member",
970 "unprovided-inject",
971 "unrendered-component",
972 "unused-component-prop",
973 "unused-component-emit",
974 "unused-component-input",
975 "unused-component-output",
976 "unused-svelte-event",
977 "unused-server-action",
978 "unused-load-data-key",
979 "unresolved-import",
980 "unlisted-dependency",
981 "duplicate-export",
982 "type-only-dependency",
983 "test-only-dependency",
984 "dev-dependency-in-production",
985 "circular-dependency",
986 "re-export-cycles",
987 "reexport-cycle",
988 "reexport-cycles",
989 "boundary-violations",
990 "coverage-gap",
991 "feature-flag",
992 "stale-suppression",
993 "unused-catalog-entry",
994 "empty-catalog-group",
995 "unresolved-catalog-reference",
996 "unused-dependency-override",
997 "misconfigured-dependency-override",
998 "invalid-client-exports",
999 "mixed-client-server-barrels",
1000 "misplaced-directives",
1001 "route-collisions",
1002 "dynamic-segment-name-conflicts",
1003];
1004
1005#[must_use]
1011pub fn closest_known_rule_name(input: &str) -> Option<&'static str> {
1012 let input_lower = input.to_ascii_lowercase();
1013 let candidates = KNOWN_RULE_NAMES.iter().copied();
1014 let suggestion = crate::levenshtein::closest_match(&input_lower, candidates)?;
1015 KNOWN_RULE_NAMES.iter().copied().find(|&c| c == suggestion)
1016}
1017
1018#[derive(Debug, Clone, PartialEq, Eq)]
1024pub struct UnknownRuleKey {
1025 pub context: String,
1027 pub key: String,
1029 pub suggestion: Option<&'static str>,
1031}
1032
1033#[must_use]
1041pub fn find_unknown_rule_keys(value: &serde_json::Value, context: &str) -> Vec<UnknownRuleKey> {
1042 let Some(map) = value.as_object() else {
1043 return Vec::new();
1044 };
1045
1046 map.keys()
1047 .filter(|key| !KNOWN_RULE_NAMES.contains(&key.as_str()))
1048 .map(|key| UnknownRuleKey {
1049 context: context.to_owned(),
1050 key: key.clone(),
1051 suggestion: closest_known_rule_name(key),
1052 })
1053 .collect()
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058 use super::*;
1059
1060 #[test]
1061 fn rules_default_severities() {
1062 let rules = RulesConfig::default();
1063 assert_eq!(rules.unused_files, Severity::Error);
1064 assert_eq!(rules.unused_exports, Severity::Error);
1065 assert_eq!(rules.unused_types, Severity::Error);
1066 assert_eq!(rules.private_type_leaks, Severity::Off);
1067 assert_eq!(rules.unused_dependencies, Severity::Error);
1068 assert_eq!(rules.unused_dev_dependencies, Severity::Warn);
1069 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
1070 assert_eq!(rules.unused_enum_members, Severity::Error);
1071 assert_eq!(rules.unused_class_members, Severity::Error);
1072 assert_eq!(rules.unresolved_imports, Severity::Error);
1073 assert_eq!(rules.unlisted_dependencies, Severity::Error);
1074 assert_eq!(rules.duplicate_exports, Severity::Error);
1075 assert_eq!(rules.type_only_dependencies, Severity::Warn);
1076 assert_eq!(rules.test_only_dependencies, Severity::Warn);
1077 assert_eq!(rules.circular_dependencies, Severity::Error);
1078 assert_eq!(rules.boundary_violation, Severity::Error);
1079 assert_eq!(rules.coverage_gaps, Severity::Off);
1080 assert_eq!(rules.feature_flags, Severity::Off);
1081 assert_eq!(rules.stale_suppressions, Severity::Warn);
1082 assert_eq!(rules.unused_catalog_entries, Severity::Warn);
1083 assert_eq!(rules.empty_catalog_groups, Severity::Warn);
1084 assert_eq!(rules.unresolved_catalog_references, Severity::Error);
1085 }
1086
1087 #[test]
1088 fn rules_deserialize_kebab_case() {
1089 let json_str = r#"{
1090 "unused-files": "error",
1091 "unused-exports": "warn",
1092 "unused-types": "off"
1093 }"#;
1094 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1095 assert_eq!(rules.unused_files, Severity::Error);
1096 assert_eq!(rules.unused_exports, Severity::Warn);
1097 assert_eq!(rules.unused_types, Severity::Off);
1098 assert_eq!(rules.unresolved_imports, Severity::Error);
1099 }
1100
1101 #[test]
1102 fn empty_rules_object_matches_default_impl() {
1103 let from_empty: RulesConfig = serde_json::from_str("{}").unwrap();
1111 assert_eq!(
1112 from_empty,
1113 RulesConfig::default(),
1114 "empty `rules` object must equal RulesConfig::default(); a field whose \
1115 Default is Warn/Off needs an explicit #[serde(default = ...)]"
1116 );
1117 }
1118
1119 #[test]
1120 fn empty_rules_object_preserves_non_error_defaults() {
1121 let rules: RulesConfig = serde_json::from_str("{}").unwrap();
1124 assert_eq!(rules.coverage_gaps, Severity::Off);
1125 assert_eq!(rules.unused_store_members, Severity::Warn);
1126 assert_eq!(rules.unprovided_injects, Severity::Warn);
1127 assert_eq!(rules.unrendered_components, Severity::Warn);
1128 assert_eq!(rules.unused_component_props, Severity::Warn);
1129 assert_eq!(rules.unused_component_emits, Severity::Warn);
1130 assert_eq!(rules.unused_component_inputs, Severity::Warn);
1131 assert_eq!(rules.unused_component_outputs, Severity::Warn);
1132 assert_eq!(rules.unused_server_actions, Severity::Warn);
1133 }
1134
1135 #[test]
1136 fn explicit_rule_severity_still_overrides_default() {
1137 let rules: RulesConfig =
1140 serde_json::from_str(r#"{ "coverage-gaps": "error", "unused-component-prop": "off" }"#)
1141 .unwrap();
1142 assert_eq!(rules.coverage_gaps, Severity::Error);
1143 assert_eq!(rules.unused_component_props, Severity::Off);
1144 }
1145
1146 #[test]
1147 fn rules_re_export_cycle_default_is_warn() {
1148 let rules = RulesConfig::default();
1149 assert_eq!(rules.re_export_cycle, Severity::Warn);
1150 }
1151
1152 #[test]
1153 fn rules_deserialize_re_export_cycle_aliases() {
1154 for token in [
1155 "re-export-cycle",
1156 "re-export-cycles",
1157 "reexport-cycle",
1158 "reexport-cycles",
1159 ] {
1160 let json_str = format!(r#"{{ "{token}": "error" }}"#);
1161 let rules: RulesConfig = serde_json::from_str(&json_str)
1162 .unwrap_or_else(|e| panic!("alias {token} did not deserialize: {e}"));
1163 assert_eq!(
1164 rules.re_export_cycle,
1165 Severity::Error,
1166 "alias {token} should set re_export_cycle"
1167 );
1168 }
1169 }
1170
1171 #[test]
1172 fn rules_deserialize_circular_dependency_alias() {
1173 let json_str = r#"{
1174 "circular-dependency": "off"
1175 }"#;
1176 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1177 assert_eq!(rules.circular_dependencies, Severity::Off);
1178 }
1179
1180 #[test]
1181 fn rules_deserialize_boundary_violations_alias() {
1182 let json_str = r#"{
1183 "boundary-violations": "off"
1184 }"#;
1185 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1186 assert_eq!(rules.boundary_violation, Severity::Off);
1187
1188 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
1189 assert_eq!(partial.boundary_violation, Some(Severity::Off));
1190 }
1191
1192 #[test]
1193 fn rules_deserialize_singular_aliases_for_every_plural_rule() {
1194 let json_str = r#"{
1195 "unused-file": "off",
1196 "unused-export": "off",
1197 "unused-type": "off",
1198 "private-type-leak": "warn",
1199 "unused-dependency": "off",
1200 "unused-dev-dependency": "off",
1201 "unused-optional-dependency": "off",
1202 "unused-enum-member": "off",
1203 "unused-class-member": "off",
1204 "unresolved-import": "off",
1205 "unlisted-dependency": "off",
1206 "duplicate-export": "off",
1207 "type-only-dependency": "off",
1208 "test-only-dependency": "off",
1209 "coverage-gap": "warn",
1210 "feature-flag": "warn",
1211 "stale-suppression": "off",
1212 "suppression-reason": "warn",
1213 "unused-catalog-entry": "error",
1214 "empty-catalog-group": "error",
1215 "unresolved-catalog-reference": "warn"
1216 }"#;
1217
1218 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
1219 assert_eq!(rules.unused_files, Severity::Off);
1220 assert_eq!(rules.unused_exports, Severity::Off);
1221 assert_eq!(rules.unused_types, Severity::Off);
1222 assert_eq!(rules.private_type_leaks, Severity::Warn);
1223 assert_eq!(rules.unused_dependencies, Severity::Off);
1224 assert_eq!(rules.unused_dev_dependencies, Severity::Off);
1225 assert_eq!(rules.unused_optional_dependencies, Severity::Off);
1226 assert_eq!(rules.unused_enum_members, Severity::Off);
1227 assert_eq!(rules.unused_class_members, Severity::Off);
1228 assert_eq!(rules.unresolved_imports, Severity::Off);
1229 assert_eq!(rules.unlisted_dependencies, Severity::Off);
1230 assert_eq!(rules.duplicate_exports, Severity::Off);
1231 assert_eq!(rules.type_only_dependencies, Severity::Off);
1232 assert_eq!(rules.test_only_dependencies, Severity::Off);
1233 assert_eq!(rules.coverage_gaps, Severity::Warn);
1234 assert_eq!(rules.feature_flags, Severity::Warn);
1235 assert_eq!(rules.stale_suppressions, Severity::Off);
1236 assert_eq!(rules.require_suppression_reason, Severity::Warn);
1237 assert_eq!(rules.unused_catalog_entries, Severity::Error);
1238 assert_eq!(rules.empty_catalog_groups, Severity::Error);
1239 assert_eq!(rules.unresolved_catalog_references, Severity::Warn);
1240
1241 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
1242 assert_eq!(partial.unused_files, Some(Severity::Off));
1243 assert_eq!(partial.unused_exports, Some(Severity::Off));
1244 assert_eq!(partial.unused_types, Some(Severity::Off));
1245 assert_eq!(partial.private_type_leaks, Some(Severity::Warn));
1246 assert_eq!(partial.unused_dependencies, Some(Severity::Off));
1247 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Off));
1248 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
1249 assert_eq!(partial.unused_enum_members, Some(Severity::Off));
1250 assert_eq!(partial.unused_class_members, Some(Severity::Off));
1251 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
1252 assert_eq!(partial.unlisted_dependencies, Some(Severity::Off));
1253 assert_eq!(partial.duplicate_exports, Some(Severity::Off));
1254 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
1255 assert_eq!(partial.test_only_dependencies, Some(Severity::Off));
1256 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
1257 assert_eq!(partial.feature_flags, Some(Severity::Warn));
1258 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
1259 assert_eq!(partial.require_suppression_reason, Some(Severity::Warn));
1260 assert_eq!(partial.unused_catalog_entries, Some(Severity::Error));
1261 assert_eq!(partial.empty_catalog_groups, Some(Severity::Error));
1262 assert_eq!(partial.unresolved_catalog_references, Some(Severity::Warn));
1263 }
1264
1265 #[test]
1266 fn severity_from_str() {
1267 assert_eq!("error".parse::<Severity>().unwrap(), Severity::Error);
1268 assert_eq!("warn".parse::<Severity>().unwrap(), Severity::Warn);
1269 assert_eq!("warning".parse::<Severity>().unwrap(), Severity::Warn);
1270 assert_eq!("off".parse::<Severity>().unwrap(), Severity::Off);
1271 assert_eq!("none".parse::<Severity>().unwrap(), Severity::Off);
1272 assert!("invalid".parse::<Severity>().is_err());
1273 }
1274
1275 #[test]
1276 fn apply_partial_only_some_fields() {
1277 let mut rules = RulesConfig::default();
1278 let partial = PartialRulesConfig {
1279 unused_files: Some(Severity::Warn),
1280 unused_exports: Some(Severity::Off),
1281 ..Default::default()
1282 };
1283 rules.apply_partial(&partial);
1284 assert_eq!(rules.unused_files, Severity::Warn);
1285 assert_eq!(rules.unused_exports, Severity::Off);
1286 assert_eq!(rules.unused_types, Severity::Error);
1287 assert_eq!(rules.unresolved_imports, Severity::Error);
1288 assert_eq!(rules.require_suppression_reason, Severity::Off);
1289 }
1290
1291 #[test]
1292 fn require_suppression_reason_deserializes_canonical_and_alias() {
1293 let rules: RulesConfig = serde_json::from_str(
1294 r#"{
1295 "require-suppression-reason": "error"
1296 }"#,
1297 )
1298 .unwrap();
1299 assert_eq!(rules.require_suppression_reason, Severity::Error);
1300
1301 let partial: PartialRulesConfig = serde_json::from_str(
1302 r#"{
1303 "suppression-reason": "warn"
1304 }"#,
1305 )
1306 .unwrap();
1307 assert_eq!(partial.require_suppression_reason, Some(Severity::Warn));
1308 }
1309
1310 #[test]
1311 fn severity_display() {
1312 assert_eq!(Severity::Error.to_string(), "error");
1313 assert_eq!(Severity::Warn.to_string(), "warn");
1314 assert_eq!(Severity::Off.to_string(), "off");
1315 }
1316
1317 #[test]
1318 fn apply_partial_all_none_changes_nothing() {
1319 let mut rules = RulesConfig::default();
1320 let original = rules.clone();
1321 let partial = PartialRulesConfig::default(); rules.apply_partial(&partial);
1323 assert_eq!(rules.unused_files, original.unused_files);
1324 assert_eq!(rules.unused_exports, original.unused_exports);
1325 assert_eq!(
1326 rules.type_only_dependencies,
1327 original.type_only_dependencies
1328 );
1329 }
1330
1331 #[test]
1332 fn apply_partial_all_fields_set() {
1333 let mut rules = RulesConfig::default();
1334 let partial = PartialRulesConfig {
1335 unused_files: Some(Severity::Off),
1336 unused_exports: Some(Severity::Off),
1337 unused_types: Some(Severity::Off),
1338 private_type_leaks: Some(Severity::Off),
1339 unused_dependencies: Some(Severity::Off),
1340 unused_dev_dependencies: Some(Severity::Off),
1341 unused_optional_dependencies: Some(Severity::Off),
1342 unused_enum_members: Some(Severity::Off),
1343 unused_class_members: Some(Severity::Off),
1344 unused_store_members: Some(Severity::Off),
1345 unprovided_injects: Some(Severity::Off),
1346 unrendered_components: Some(Severity::Off),
1347 unused_component_props: Some(Severity::Off),
1348 unused_component_emits: Some(Severity::Off),
1349 unused_component_inputs: Some(Severity::Off),
1350 unused_component_outputs: Some(Severity::Off),
1351 unused_svelte_events: Some(Severity::Off),
1352 unused_server_actions: Some(Severity::Off),
1353 unused_load_data_keys: Some(Severity::Off),
1354 prop_drilling: Some(Severity::Off),
1355 thin_wrapper: Some(Severity::Off),
1356 duplicate_prop_shape: Some(Severity::Off),
1357 css_token_drift: Some(Severity::Off),
1358 css_duplicate_block: Some(Severity::Off),
1359 css_selector_complexity: Some(Severity::Off),
1360 css_dead_surface: Some(Severity::Off),
1361 css_broken_reference: Some(Severity::Off),
1362 unresolved_imports: Some(Severity::Off),
1363 unlisted_dependencies: Some(Severity::Off),
1364 duplicate_exports: Some(Severity::Off),
1365 type_only_dependencies: Some(Severity::Off),
1366 test_only_dependencies: Some(Severity::Off),
1367 dev_dependencies_in_production: Some(Severity::Off),
1368 circular_dependencies: Some(Severity::Off),
1369 re_export_cycle: Some(Severity::Off),
1370 boundary_violation: Some(Severity::Off),
1371 coverage_gaps: Some(Severity::Off),
1372 feature_flags: Some(Severity::Off),
1373 stale_suppressions: Some(Severity::Off),
1374 require_suppression_reason: Some(Severity::Off),
1375 unused_catalog_entries: Some(Severity::Off),
1376 empty_catalog_groups: Some(Severity::Off),
1377 unresolved_catalog_references: Some(Severity::Off),
1378 unused_dependency_overrides: Some(Severity::Off),
1379 misconfigured_dependency_overrides: Some(Severity::Off),
1380 security_client_server_leak: Some(Severity::Off),
1381 security_sink: Some(Severity::Off),
1382 policy_violation: Some(Severity::Off),
1383 invalid_client_export: Some(Severity::Off),
1384 mixed_client_server_barrel: Some(Severity::Off),
1385 misplaced_directive: Some(Severity::Off),
1386 route_collision: Some(Severity::Off),
1387 dynamic_segment_name_conflict: Some(Severity::Off),
1388 };
1389 rules.apply_partial(&partial);
1390 assert_eq!(rules.unused_files, Severity::Off);
1391 assert_eq!(rules.private_type_leaks, Severity::Off);
1392 assert_eq!(rules.circular_dependencies, Severity::Off);
1393 assert_eq!(rules.type_only_dependencies, Severity::Off);
1394 assert_eq!(rules.test_only_dependencies, Severity::Off);
1395 assert_eq!(rules.boundary_violation, Severity::Off);
1396 assert_eq!(rules.coverage_gaps, Severity::Off);
1397 assert_eq!(rules.feature_flags, Severity::Off);
1398 assert_eq!(rules.stale_suppressions, Severity::Off);
1399 assert_eq!(rules.require_suppression_reason, Severity::Off);
1400 assert_eq!(rules.security_sink, Severity::Off);
1401 assert_eq!(rules.policy_violation, Severity::Off);
1402 assert_eq!(rules.invalid_client_export, Severity::Off);
1403 assert_eq!(rules.mixed_client_server_barrel, Severity::Off);
1404 assert_eq!(rules.misplaced_directive, Severity::Off);
1405 assert_eq!(rules.unrendered_components, Severity::Off);
1406 assert_eq!(rules.unused_component_props, Severity::Off);
1407 assert_eq!(rules.unused_component_emits, Severity::Off);
1408 assert_eq!(rules.unused_component_inputs, Severity::Off);
1409 assert_eq!(rules.unused_component_outputs, Severity::Off);
1410 assert_eq!(rules.unused_svelte_events, Severity::Off);
1411 assert_eq!(rules.route_collision, Severity::Off);
1412 assert_eq!(rules.dynamic_segment_name_conflict, Severity::Off);
1413 }
1414
1415 #[test]
1416 fn rules_config_defaults_include_optional_deps() {
1417 let rules = RulesConfig::default();
1418 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
1419 }
1420
1421 #[test]
1422 fn policy_violation_defaults_to_warn() {
1423 let rules = RulesConfig::default();
1424 assert_eq!(rules.policy_violation, Severity::Warn);
1425 }
1426
1427 #[test]
1428 fn policy_violation_accepts_plural_alias() {
1429 let json = r#"{ "policy-violations": "error" }"#;
1430 let rules: RulesConfig = serde_json::from_str(json).unwrap();
1431 assert_eq!(rules.policy_violation, Severity::Error);
1432 }
1433
1434 #[test]
1435 fn severity_from_str_case_insensitive() {
1436 assert_eq!("ERROR".parse::<Severity>().unwrap(), Severity::Error);
1437 assert_eq!("Warn".parse::<Severity>().unwrap(), Severity::Warn);
1438 assert_eq!("OFF".parse::<Severity>().unwrap(), Severity::Off);
1439 assert_eq!("Warning".parse::<Severity>().unwrap(), Severity::Warn);
1440 assert_eq!("NONE".parse::<Severity>().unwrap(), Severity::Off);
1441 }
1442
1443 #[test]
1444 fn severity_from_str_invalid_returns_error() {
1445 let result = "critical".parse::<Severity>();
1446 assert!(result.is_err());
1447 let err = result.unwrap_err();
1448 assert!(
1449 err.contains("unknown severity"),
1450 "Expected descriptive error, got: {err}"
1451 );
1452 }
1453
1454 #[test]
1455 fn known_rule_names_count_matches_struct() {
1456 assert_eq!(KNOWN_RULE_NAMES.len(), 98);
1457 }
1458
1459 #[test]
1460 fn known_rule_names_has_no_duplicates() {
1461 let mut sorted: Vec<&str> = KNOWN_RULE_NAMES.to_vec();
1462 sorted.sort_unstable();
1463 let original_len = sorted.len();
1464 sorted.dedup();
1465 assert_eq!(
1466 sorted.len(),
1467 original_len,
1468 "KNOWN_RULE_NAMES contains a duplicate"
1469 );
1470 }
1471
1472 #[test]
1473 fn known_rule_names_covers_every_serde_alias_in_source() {
1474 let source = include_str!("rules.rs");
1475
1476 let mut aliases_found = Vec::new();
1477 for line in source.lines() {
1478 let trimmed = line.trim();
1479 if trimmed.starts_with("//") {
1480 continue;
1481 }
1482 let Some(after) = trimmed.split("alias = \"").nth(1) else {
1483 continue;
1484 };
1485 let Some(end) = after.find('"') else {
1486 continue;
1487 };
1488 let alias = &after[..end];
1489 if alias.is_empty() || !alias.chars().all(|c| c.is_ascii_lowercase() || c == '-') {
1490 continue;
1491 }
1492 aliases_found.push(alias.to_owned());
1493 }
1494
1495 assert_eq!(
1496 aliases_found.len(),
1497 106,
1498 "expected 106 source-level alias attrs (53 per struct); got {}: {:?}",
1499 aliases_found.len(),
1500 aliases_found
1501 );
1502
1503 for alias in &aliases_found {
1504 assert!(
1505 KNOWN_RULE_NAMES.contains(&alias.as_str()),
1506 "serde alias '{alias}' is in rules.rs source but missing from KNOWN_RULE_NAMES"
1507 );
1508 }
1509 }
1510
1511 #[test]
1512 fn re_export_cycle_aliases_all_round_trip_to_the_same_field() {
1513 for alias in [
1514 "re-export-cycle",
1515 "re-export-cycles",
1516 "reexport-cycle",
1517 "reexport-cycles",
1518 ] {
1519 let json = format!(r#"{{"{alias}": "warn"}}"#);
1520 let partial: PartialRulesConfig = serde_json::from_str(&json)
1521 .unwrap_or_else(|e| panic!("'{alias}' should deserialize: {e}"));
1522 assert_eq!(
1523 partial.re_export_cycle,
1524 Some(Severity::Warn),
1525 "'{alias}' should set re_export_cycle to Warn"
1526 );
1527 let serialized = serde_json::to_value(&partial).unwrap();
1528 let map = serialized.as_object().unwrap();
1529 assert_eq!(
1530 map.len(),
1531 1,
1532 "'{alias}' should resolve to exactly one field, got: {map:?}"
1533 );
1534 }
1535 }
1536
1537 #[test]
1538 fn every_known_rule_name_round_trips_through_partial() {
1539 for &name in KNOWN_RULE_NAMES {
1540 let json = format!(r#"{{"{name}": "warn"}}"#);
1541 let partial: PartialRulesConfig = serde_json::from_str(&json)
1542 .unwrap_or_else(|e| panic!("'{name}' should deserialize: {e}"));
1543
1544 let serialized = serde_json::to_value(&partial).unwrap();
1545 let map = serialized.as_object().unwrap();
1546 assert_eq!(
1547 map.len(),
1548 1,
1549 "'{name}' should resolve to exactly one field, got: {map:?}"
1550 );
1551 }
1552 }
1553
1554 #[test]
1555 fn known_rule_names_covers_every_struct_field() {
1556 let json = serde_json::to_value(RulesConfig::default()).unwrap();
1557 let obj = json.as_object().unwrap();
1558 for key in obj.keys() {
1559 assert!(
1560 KNOWN_RULE_NAMES.contains(&key.as_str()),
1561 "field '{key}' is serialized but missing from KNOWN_RULE_NAMES"
1562 );
1563 }
1564 }
1565
1566 #[test]
1567 fn closest_known_rule_name_suggests_for_obvious_typo() {
1568 assert_eq!(
1569 closest_known_rule_name("unsued-files"),
1570 Some("unused-files")
1571 );
1572 assert_eq!(
1573 closest_known_rule_name("circular-dependnecy"),
1574 Some("circular-dependency")
1575 );
1576 assert_eq!(
1577 closest_known_rule_name("unused-dep"),
1578 None,
1579 "too short for a confident suggestion"
1580 );
1581 }
1582
1583 #[test]
1584 fn closest_known_rule_name_returns_none_for_novel_input() {
1585 assert_eq!(closest_known_rule_name("totally-fabricated"), None);
1586 assert_eq!(closest_known_rule_name("foo"), None);
1587 }
1588
1589 #[test]
1590 fn closest_known_rule_name_is_case_insensitive() {
1591 assert_eq!(
1592 closest_known_rule_name("UNSUED-FILES"),
1593 Some("unused-files")
1594 );
1595 }
1596
1597 #[test]
1598 fn closest_known_rule_name_returns_none_for_exact_match() {
1599 assert_eq!(closest_known_rule_name("unused-files"), None);
1600 }
1601
1602 #[test]
1603 fn find_unknown_rule_keys_flags_typo() {
1604 let v = serde_json::json!({
1605 "unsued-files": "warn",
1606 "unused-exports": "off",
1607 });
1608 let unknown = find_unknown_rule_keys(&v, "rules");
1609 assert_eq!(unknown.len(), 1);
1610 assert_eq!(unknown[0].key, "unsued-files");
1611 assert_eq!(unknown[0].context, "rules");
1612 assert_eq!(unknown[0].suggestion, Some("unused-files"));
1613 }
1614
1615 #[test]
1616 fn find_unknown_rule_keys_passes_aliases() {
1617 let v = serde_json::json!({
1618 "unused-file": "warn",
1619 "circular-dependency": "off",
1620 "boundary-violations": "warn",
1621 });
1622 let unknown = find_unknown_rule_keys(&v, "rules");
1623 assert!(
1624 unknown.is_empty(),
1625 "documented aliases must not flag as unknown: {unknown:?}"
1626 );
1627 }
1628
1629 #[test]
1630 fn find_unknown_rule_keys_returns_multiple_typos() {
1631 let v = serde_json::json!({
1632 "unsued-files": "warn",
1633 "circular-dependnecy": "off",
1634 });
1635 let unknown = find_unknown_rule_keys(&v, "rules");
1636 assert_eq!(unknown.len(), 2);
1637 }
1638
1639 #[test]
1640 fn find_unknown_rule_keys_carries_context() {
1641 let v = serde_json::json!({ "unsued-files": "warn" });
1642 let unknown = find_unknown_rule_keys(&v, "overrides[2].rules");
1643 assert_eq!(unknown[0].context, "overrides[2].rules");
1644 }
1645
1646 #[test]
1647 fn find_unknown_rule_keys_empty_when_not_object() {
1648 let v = serde_json::json!(null);
1649 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
1650
1651 let v = serde_json::json!([1, 2, 3]);
1652 assert!(find_unknown_rule_keys(&v, "rules").is_empty());
1653 }
1654
1655 #[test]
1656 fn find_unknown_rule_keys_no_suggestion_for_novel_name() {
1657 let v = serde_json::json!({ "totally-fabricated-rule": "warn" });
1658 let unknown = find_unknown_rule_keys(&v, "rules");
1659 assert_eq!(unknown.len(), 1);
1660 assert_eq!(unknown[0].suggestion, None);
1661 }
1662
1663 #[test]
1664 fn partial_rules_empty_json() {
1665 let partial: PartialRulesConfig = serde_json::from_str("{}").unwrap();
1666 assert!(partial.unused_files.is_none());
1667 assert!(partial.unused_exports.is_none());
1668 assert!(partial.unused_types.is_none());
1669 assert!(partial.unused_dependencies.is_none());
1670 assert!(partial.circular_dependencies.is_none());
1671 assert!(partial.boundary_violation.is_none());
1672 assert!(partial.coverage_gaps.is_none());
1673 assert!(partial.feature_flags.is_none());
1674 assert!(partial.stale_suppressions.is_none());
1675 }
1676
1677 #[test]
1678 fn partial_rules_subset_json() {
1679 let json = r#"{
1680 "unused-files": "warn",
1681 "circular-dependencies": "off"
1682 }"#;
1683 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1684 assert_eq!(partial.unused_files, Some(Severity::Warn));
1685 assert_eq!(partial.circular_dependencies, Some(Severity::Off));
1686 assert!(partial.unused_exports.is_none());
1687 }
1688
1689 #[test]
1690 fn partial_rules_deserialize_circular_dependency_alias() {
1691 let json = r#"{
1692 "circular-dependency": "warn"
1693 }"#;
1694 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1695 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
1696 }
1697
1698 #[test]
1699 fn partial_rules_all_fields_json() {
1700 let json = r#"{
1701 "unused-files": "error",
1702 "unused-exports": "warn",
1703 "unused-types": "off",
1704 "unused-dependencies": "error",
1705 "unused-dev-dependencies": "warn",
1706 "unused-optional-dependencies": "off",
1707 "unused-enum-members": "error",
1708 "unused-class-members": "warn",
1709 "unresolved-imports": "off",
1710 "unlisted-dependencies": "error",
1711 "duplicate-exports": "warn",
1712 "type-only-dependencies": "off",
1713 "test-only-dependencies": "error",
1714 "circular-dependencies": "warn",
1715 "boundary-violation": "off",
1716 "coverage-gaps": "warn",
1717 "feature-flags": "error",
1718 "stale-suppressions": "off"
1719 }"#;
1720 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
1721 assert_eq!(partial.unused_files, Some(Severity::Error));
1722 assert_eq!(partial.unused_exports, Some(Severity::Warn));
1723 assert_eq!(partial.unused_types, Some(Severity::Off));
1724 assert_eq!(partial.unused_dependencies, Some(Severity::Error));
1725 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Warn));
1726 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
1727 assert_eq!(partial.unused_enum_members, Some(Severity::Error));
1728 assert_eq!(partial.unused_class_members, Some(Severity::Warn));
1729 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
1730 assert_eq!(partial.unlisted_dependencies, Some(Severity::Error));
1731 assert_eq!(partial.duplicate_exports, Some(Severity::Warn));
1732 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
1733 assert_eq!(partial.test_only_dependencies, Some(Severity::Error));
1734 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
1735 assert_eq!(partial.boundary_violation, Some(Severity::Off));
1736 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
1737 assert_eq!(partial.feature_flags, Some(Severity::Error));
1738 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
1739 }
1740
1741 #[test]
1742 fn partial_rules_none_fields_not_serialized() {
1743 let partial = PartialRulesConfig::default();
1744 let json = serde_json::to_string(&partial).unwrap();
1745 assert_eq!(
1746 json, "{}",
1747 "all-None partial should serialize to empty object"
1748 );
1749 }
1750
1751 #[test]
1752 fn partial_rules_some_fields_serialized() {
1753 let partial = PartialRulesConfig {
1754 unused_files: Some(Severity::Warn),
1755 ..Default::default()
1756 };
1757 let json = serde_json::to_string(&partial).unwrap();
1758 assert!(json.contains("unused-files"));
1759 assert!(!json.contains("unused-exports"));
1760 }
1761
1762 #[test]
1763 fn severity_json_deserialization() {
1764 let error: Severity = serde_json::from_str(r#""error""#).unwrap();
1765 assert_eq!(error, Severity::Error);
1766
1767 let warn: Severity = serde_json::from_str(r#""warn""#).unwrap();
1768 assert_eq!(warn, Severity::Warn);
1769
1770 let off: Severity = serde_json::from_str(r#""off""#).unwrap();
1771 assert_eq!(off, Severity::Off);
1772 }
1773
1774 #[test]
1775 fn severity_invalid_json_value_rejected() {
1776 let result: Result<Severity, _> = serde_json::from_str(r#""critical""#);
1777 assert!(result.is_err());
1778 }
1779
1780 #[test]
1781 fn severity_default_is_error() {
1782 assert_eq!(Severity::default(), Severity::Error);
1783 }
1784
1785 #[test]
1786 fn rules_config_json_roundtrip() {
1787 let rules = RulesConfig {
1788 unused_files: Severity::Warn,
1789 unused_exports: Severity::Off,
1790 type_only_dependencies: Severity::Error,
1791 ..RulesConfig::default()
1792 };
1793 let json = serde_json::to_string(&rules).unwrap();
1794 let restored: RulesConfig = serde_json::from_str(&json).unwrap();
1795 assert_eq!(restored.unused_files, Severity::Warn);
1796 assert_eq!(restored.unused_exports, Severity::Off);
1797 assert_eq!(restored.type_only_dependencies, Severity::Error);
1798 assert_eq!(restored.unused_dependencies, Severity::Error); }
1800
1801 #[test]
1802 fn apply_partial_preserves_type_only_default() {
1803 let mut rules = RulesConfig::default();
1804 let partial = PartialRulesConfig {
1805 unused_files: Some(Severity::Off),
1806 ..Default::default()
1807 };
1808 rules.apply_partial(&partial);
1809 assert_eq!(rules.type_only_dependencies, Severity::Warn);
1810 assert_eq!(rules.test_only_dependencies, Severity::Warn);
1811 }
1812}