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, 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 = "unresolved-import")]
88 pub unresolved_imports: Severity,
89 #[serde(default, alias = "unlisted-dependency")]
90 pub unlisted_dependencies: Severity,
91 #[serde(default, alias = "duplicate-export")]
92 pub duplicate_exports: Severity,
93 #[serde(default = "Severity::default_warn", alias = "type-only-dependency")]
94 pub type_only_dependencies: Severity,
95 #[serde(default = "Severity::default_warn", alias = "test-only-dependency")]
96 pub test_only_dependencies: Severity,
97 #[serde(default, alias = "circular-dependency")]
98 pub circular_dependencies: Severity,
99 #[serde(default, alias = "boundary-violations")]
100 pub boundary_violation: Severity,
101 #[serde(default, alias = "coverage-gap")]
102 pub coverage_gaps: Severity,
103 #[serde(default = "Severity::default_off", alias = "feature-flag")]
104 pub feature_flags: Severity,
105 #[serde(default = "Severity::default_warn", alias = "stale-suppression")]
106 pub stale_suppressions: Severity,
107 #[serde(default = "Severity::default_warn", alias = "unused-catalog-entry")]
108 pub unused_catalog_entries: Severity,
109 #[serde(default = "Severity::default_warn", alias = "empty-catalog-group")]
110 pub empty_catalog_groups: Severity,
111 #[serde(default, alias = "unresolved-catalog-reference")]
112 pub unresolved_catalog_references: Severity,
113 #[serde(
114 default = "Severity::default_warn",
115 alias = "unused-dependency-override"
116 )]
117 pub unused_dependency_overrides: Severity,
118 #[serde(default, alias = "misconfigured-dependency-override")]
119 pub misconfigured_dependency_overrides: Severity,
120}
121
122impl Default for RulesConfig {
123 fn default() -> Self {
124 Self {
125 unused_files: Severity::Error,
126 unused_exports: Severity::Error,
127 unused_types: Severity::Error,
128 private_type_leaks: Severity::Off,
129 unused_dependencies: Severity::Error,
130 unused_dev_dependencies: Severity::Warn,
131 unused_optional_dependencies: Severity::Warn,
132 unused_enum_members: Severity::Error,
133 unused_class_members: Severity::Error,
134 unresolved_imports: Severity::Error,
135 unlisted_dependencies: Severity::Error,
136 duplicate_exports: Severity::Error,
137 type_only_dependencies: Severity::Warn,
138 test_only_dependencies: Severity::Warn,
139 circular_dependencies: Severity::Error,
140 boundary_violation: Severity::Error,
141 coverage_gaps: Severity::Off,
142 feature_flags: Severity::Off,
143 stale_suppressions: Severity::Warn,
144 unused_catalog_entries: Severity::Warn,
145 empty_catalog_groups: Severity::Warn,
146 unresolved_catalog_references: Severity::Error,
147 unused_dependency_overrides: Severity::Warn,
148 misconfigured_dependency_overrides: Severity::Error,
149 }
150 }
151}
152
153impl RulesConfig {
154 pub const fn apply_partial(&mut self, partial: &PartialRulesConfig) {
156 if let Some(s) = partial.unused_files {
157 self.unused_files = s;
158 }
159 if let Some(s) = partial.unused_exports {
160 self.unused_exports = s;
161 }
162 if let Some(s) = partial.unused_types {
163 self.unused_types = s;
164 }
165 if let Some(s) = partial.private_type_leaks {
166 self.private_type_leaks = s;
167 }
168 if let Some(s) = partial.unused_dependencies {
169 self.unused_dependencies = s;
170 }
171 if let Some(s) = partial.unused_dev_dependencies {
172 self.unused_dev_dependencies = s;
173 }
174 if let Some(s) = partial.unused_optional_dependencies {
175 self.unused_optional_dependencies = s;
176 }
177 if let Some(s) = partial.unused_enum_members {
178 self.unused_enum_members = s;
179 }
180 if let Some(s) = partial.unused_class_members {
181 self.unused_class_members = s;
182 }
183 if let Some(s) = partial.unresolved_imports {
184 self.unresolved_imports = s;
185 }
186 if let Some(s) = partial.unlisted_dependencies {
187 self.unlisted_dependencies = s;
188 }
189 if let Some(s) = partial.duplicate_exports {
190 self.duplicate_exports = s;
191 }
192 if let Some(s) = partial.type_only_dependencies {
193 self.type_only_dependencies = s;
194 }
195 if let Some(s) = partial.test_only_dependencies {
196 self.test_only_dependencies = s;
197 }
198 if let Some(s) = partial.circular_dependencies {
199 self.circular_dependencies = s;
200 }
201 if let Some(s) = partial.boundary_violation {
202 self.boundary_violation = s;
203 }
204 if let Some(s) = partial.coverage_gaps {
205 self.coverage_gaps = s;
206 }
207 if let Some(s) = partial.feature_flags {
208 self.feature_flags = s;
209 }
210 if let Some(s) = partial.stale_suppressions {
211 self.stale_suppressions = s;
212 }
213 if let Some(s) = partial.unused_catalog_entries {
214 self.unused_catalog_entries = s;
215 }
216 if let Some(s) = partial.empty_catalog_groups {
217 self.empty_catalog_groups = s;
218 }
219 if let Some(s) = partial.unresolved_catalog_references {
220 self.unresolved_catalog_references = s;
221 }
222 if let Some(s) = partial.unused_dependency_overrides {
223 self.unused_dependency_overrides = s;
224 }
225 if let Some(s) = partial.misconfigured_dependency_overrides {
226 self.misconfigured_dependency_overrides = s;
227 }
228 }
229}
230
231#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
233#[serde(rename_all = "kebab-case")]
234pub struct PartialRulesConfig {
235 #[serde(
236 default,
237 alias = "unused-file",
238 skip_serializing_if = "Option::is_none"
239 )]
240 pub unused_files: Option<Severity>,
241 #[serde(
242 default,
243 alias = "unused-export",
244 skip_serializing_if = "Option::is_none"
245 )]
246 pub unused_exports: Option<Severity>,
247 #[serde(
248 default,
249 alias = "unused-type",
250 skip_serializing_if = "Option::is_none"
251 )]
252 pub unused_types: Option<Severity>,
253 #[serde(
254 default,
255 alias = "private-type-leak",
256 skip_serializing_if = "Option::is_none"
257 )]
258 pub private_type_leaks: Option<Severity>,
259 #[serde(
260 default,
261 alias = "unused-dependency",
262 skip_serializing_if = "Option::is_none"
263 )]
264 pub unused_dependencies: Option<Severity>,
265 #[serde(
266 default,
267 alias = "unused-dev-dependency",
268 skip_serializing_if = "Option::is_none"
269 )]
270 pub unused_dev_dependencies: Option<Severity>,
271 #[serde(
272 default,
273 alias = "unused-optional-dependency",
274 skip_serializing_if = "Option::is_none"
275 )]
276 pub unused_optional_dependencies: Option<Severity>,
277 #[serde(
278 default,
279 alias = "unused-enum-member",
280 skip_serializing_if = "Option::is_none"
281 )]
282 pub unused_enum_members: Option<Severity>,
283 #[serde(
284 default,
285 alias = "unused-class-member",
286 skip_serializing_if = "Option::is_none"
287 )]
288 pub unused_class_members: Option<Severity>,
289 #[serde(
290 default,
291 alias = "unresolved-import",
292 skip_serializing_if = "Option::is_none"
293 )]
294 pub unresolved_imports: Option<Severity>,
295 #[serde(
296 default,
297 alias = "unlisted-dependency",
298 skip_serializing_if = "Option::is_none"
299 )]
300 pub unlisted_dependencies: Option<Severity>,
301 #[serde(
302 default,
303 alias = "duplicate-export",
304 skip_serializing_if = "Option::is_none"
305 )]
306 pub duplicate_exports: Option<Severity>,
307 #[serde(
308 default,
309 alias = "type-only-dependency",
310 skip_serializing_if = "Option::is_none"
311 )]
312 pub type_only_dependencies: Option<Severity>,
313 #[serde(
314 default,
315 alias = "test-only-dependency",
316 skip_serializing_if = "Option::is_none"
317 )]
318 pub test_only_dependencies: Option<Severity>,
319 #[serde(
320 default,
321 alias = "circular-dependency",
322 skip_serializing_if = "Option::is_none"
323 )]
324 pub circular_dependencies: Option<Severity>,
325 #[serde(
326 default,
327 alias = "boundary-violations",
328 skip_serializing_if = "Option::is_none"
329 )]
330 pub boundary_violation: Option<Severity>,
331 #[serde(
332 default,
333 alias = "coverage-gap",
334 skip_serializing_if = "Option::is_none"
335 )]
336 pub coverage_gaps: Option<Severity>,
337 #[serde(
338 default,
339 alias = "feature-flag",
340 skip_serializing_if = "Option::is_none"
341 )]
342 pub feature_flags: Option<Severity>,
343 #[serde(
344 default,
345 alias = "stale-suppression",
346 skip_serializing_if = "Option::is_none"
347 )]
348 pub stale_suppressions: Option<Severity>,
349 #[serde(
350 default,
351 alias = "unused-catalog-entry",
352 skip_serializing_if = "Option::is_none"
353 )]
354 pub unused_catalog_entries: Option<Severity>,
355 #[serde(
356 default,
357 alias = "empty-catalog-group",
358 skip_serializing_if = "Option::is_none"
359 )]
360 pub empty_catalog_groups: Option<Severity>,
361 #[serde(
362 default,
363 alias = "unresolved-catalog-reference",
364 skip_serializing_if = "Option::is_none"
365 )]
366 pub unresolved_catalog_references: Option<Severity>,
367 #[serde(
368 default,
369 alias = "unused-dependency-override",
370 skip_serializing_if = "Option::is_none"
371 )]
372 pub unused_dependency_overrides: Option<Severity>,
373 #[serde(
374 default,
375 alias = "misconfigured-dependency-override",
376 skip_serializing_if = "Option::is_none"
377 )]
378 pub misconfigured_dependency_overrides: Option<Severity>,
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn rules_default_severities() {
387 let rules = RulesConfig::default();
388 assert_eq!(rules.unused_files, Severity::Error);
389 assert_eq!(rules.unused_exports, Severity::Error);
390 assert_eq!(rules.unused_types, Severity::Error);
391 assert_eq!(rules.private_type_leaks, Severity::Off);
392 assert_eq!(rules.unused_dependencies, Severity::Error);
393 assert_eq!(rules.unused_dev_dependencies, Severity::Warn);
394 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
395 assert_eq!(rules.unused_enum_members, Severity::Error);
396 assert_eq!(rules.unused_class_members, Severity::Error);
397 assert_eq!(rules.unresolved_imports, Severity::Error);
398 assert_eq!(rules.unlisted_dependencies, Severity::Error);
399 assert_eq!(rules.duplicate_exports, Severity::Error);
400 assert_eq!(rules.type_only_dependencies, Severity::Warn);
401 assert_eq!(rules.test_only_dependencies, Severity::Warn);
402 assert_eq!(rules.circular_dependencies, Severity::Error);
403 assert_eq!(rules.boundary_violation, Severity::Error);
404 assert_eq!(rules.coverage_gaps, Severity::Off);
405 assert_eq!(rules.feature_flags, Severity::Off);
406 assert_eq!(rules.stale_suppressions, Severity::Warn);
407 assert_eq!(rules.unused_catalog_entries, Severity::Warn);
408 assert_eq!(rules.empty_catalog_groups, Severity::Warn);
409 assert_eq!(rules.unresolved_catalog_references, Severity::Error);
410 }
411
412 #[test]
413 fn rules_deserialize_kebab_case() {
414 let json_str = r#"{
415 "unused-files": "error",
416 "unused-exports": "warn",
417 "unused-types": "off"
418 }"#;
419 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
420 assert_eq!(rules.unused_files, Severity::Error);
421 assert_eq!(rules.unused_exports, Severity::Warn);
422 assert_eq!(rules.unused_types, Severity::Off);
423 assert_eq!(rules.unresolved_imports, Severity::Error);
425 }
426
427 #[test]
428 fn rules_deserialize_circular_dependency_alias() {
429 let json_str = r#"{
430 "circular-dependency": "off"
431 }"#;
432 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
433 assert_eq!(rules.circular_dependencies, Severity::Off);
434 }
435
436 #[test]
437 fn rules_deserialize_boundary_violations_alias() {
438 let json_str = r#"{
439 "boundary-violations": "off"
440 }"#;
441 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
442 assert_eq!(rules.boundary_violation, Severity::Off);
443
444 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
445 assert_eq!(partial.boundary_violation, Some(Severity::Off));
446 }
447
448 #[test]
449 fn rules_deserialize_singular_aliases_for_every_plural_rule() {
450 let json_str = r#"{
454 "unused-file": "off",
455 "unused-export": "off",
456 "unused-type": "off",
457 "private-type-leak": "warn",
458 "unused-dependency": "off",
459 "unused-dev-dependency": "off",
460 "unused-optional-dependency": "off",
461 "unused-enum-member": "off",
462 "unused-class-member": "off",
463 "unresolved-import": "off",
464 "unlisted-dependency": "off",
465 "duplicate-export": "off",
466 "type-only-dependency": "off",
467 "test-only-dependency": "off",
468 "coverage-gap": "warn",
469 "feature-flag": "warn",
470 "stale-suppression": "off",
471 "unused-catalog-entry": "error",
472 "empty-catalog-group": "error",
473 "unresolved-catalog-reference": "warn"
474 }"#;
475
476 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
477 assert_eq!(rules.unused_files, Severity::Off);
478 assert_eq!(rules.unused_exports, Severity::Off);
479 assert_eq!(rules.unused_types, Severity::Off);
480 assert_eq!(rules.private_type_leaks, Severity::Warn);
481 assert_eq!(rules.unused_dependencies, Severity::Off);
482 assert_eq!(rules.unused_dev_dependencies, Severity::Off);
483 assert_eq!(rules.unused_optional_dependencies, Severity::Off);
484 assert_eq!(rules.unused_enum_members, Severity::Off);
485 assert_eq!(rules.unused_class_members, Severity::Off);
486 assert_eq!(rules.unresolved_imports, Severity::Off);
487 assert_eq!(rules.unlisted_dependencies, Severity::Off);
488 assert_eq!(rules.duplicate_exports, Severity::Off);
489 assert_eq!(rules.type_only_dependencies, Severity::Off);
490 assert_eq!(rules.test_only_dependencies, Severity::Off);
491 assert_eq!(rules.coverage_gaps, Severity::Warn);
492 assert_eq!(rules.feature_flags, Severity::Warn);
493 assert_eq!(rules.stale_suppressions, Severity::Off);
494 assert_eq!(rules.unused_catalog_entries, Severity::Error);
495 assert_eq!(rules.empty_catalog_groups, Severity::Error);
496 assert_eq!(rules.unresolved_catalog_references, Severity::Warn);
497
498 let partial: PartialRulesConfig = serde_json::from_str(json_str).unwrap();
499 assert_eq!(partial.unused_files, Some(Severity::Off));
500 assert_eq!(partial.unused_exports, Some(Severity::Off));
501 assert_eq!(partial.unused_types, Some(Severity::Off));
502 assert_eq!(partial.private_type_leaks, Some(Severity::Warn));
503 assert_eq!(partial.unused_dependencies, Some(Severity::Off));
504 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Off));
505 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
506 assert_eq!(partial.unused_enum_members, Some(Severity::Off));
507 assert_eq!(partial.unused_class_members, Some(Severity::Off));
508 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
509 assert_eq!(partial.unlisted_dependencies, Some(Severity::Off));
510 assert_eq!(partial.duplicate_exports, Some(Severity::Off));
511 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
512 assert_eq!(partial.test_only_dependencies, Some(Severity::Off));
513 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
514 assert_eq!(partial.feature_flags, Some(Severity::Warn));
515 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
516 assert_eq!(partial.unused_catalog_entries, Some(Severity::Error));
517 assert_eq!(partial.empty_catalog_groups, Some(Severity::Error));
518 assert_eq!(partial.unresolved_catalog_references, Some(Severity::Warn));
519 }
520
521 #[test]
522 fn severity_from_str() {
523 assert_eq!("error".parse::<Severity>().unwrap(), Severity::Error);
524 assert_eq!("warn".parse::<Severity>().unwrap(), Severity::Warn);
525 assert_eq!("warning".parse::<Severity>().unwrap(), Severity::Warn);
526 assert_eq!("off".parse::<Severity>().unwrap(), Severity::Off);
527 assert_eq!("none".parse::<Severity>().unwrap(), Severity::Off);
528 assert!("invalid".parse::<Severity>().is_err());
529 }
530
531 #[test]
532 fn apply_partial_only_some_fields() {
533 let mut rules = RulesConfig::default();
534 let partial = PartialRulesConfig {
535 unused_files: Some(Severity::Warn),
536 unused_exports: Some(Severity::Off),
537 ..Default::default()
538 };
539 rules.apply_partial(&partial);
540 assert_eq!(rules.unused_files, Severity::Warn);
541 assert_eq!(rules.unused_exports, Severity::Off);
542 assert_eq!(rules.unused_types, Severity::Error);
544 assert_eq!(rules.unresolved_imports, Severity::Error);
545 }
546
547 #[test]
548 fn severity_display() {
549 assert_eq!(Severity::Error.to_string(), "error");
550 assert_eq!(Severity::Warn.to_string(), "warn");
551 assert_eq!(Severity::Off.to_string(), "off");
552 }
553
554 #[test]
555 fn apply_partial_all_none_changes_nothing() {
556 let mut rules = RulesConfig::default();
557 let original = rules.clone();
558 let partial = PartialRulesConfig::default(); rules.apply_partial(&partial);
560 assert_eq!(rules.unused_files, original.unused_files);
561 assert_eq!(rules.unused_exports, original.unused_exports);
562 assert_eq!(
563 rules.type_only_dependencies,
564 original.type_only_dependencies
565 );
566 }
567
568 #[test]
569 fn apply_partial_all_fields_set() {
570 let mut rules = RulesConfig::default();
571 let partial = PartialRulesConfig {
572 unused_files: Some(Severity::Off),
573 unused_exports: Some(Severity::Off),
574 unused_types: Some(Severity::Off),
575 private_type_leaks: Some(Severity::Off),
576 unused_dependencies: Some(Severity::Off),
577 unused_dev_dependencies: Some(Severity::Off),
578 unused_optional_dependencies: Some(Severity::Off),
579 unused_enum_members: Some(Severity::Off),
580 unused_class_members: Some(Severity::Off),
581 unresolved_imports: Some(Severity::Off),
582 unlisted_dependencies: Some(Severity::Off),
583 duplicate_exports: Some(Severity::Off),
584 type_only_dependencies: Some(Severity::Off),
585 test_only_dependencies: Some(Severity::Off),
586 circular_dependencies: Some(Severity::Off),
587 boundary_violation: Some(Severity::Off),
588 coverage_gaps: Some(Severity::Off),
589 feature_flags: Some(Severity::Off),
590 stale_suppressions: Some(Severity::Off),
591 unused_catalog_entries: Some(Severity::Off),
592 empty_catalog_groups: Some(Severity::Off),
593 unresolved_catalog_references: Some(Severity::Off),
594 unused_dependency_overrides: Some(Severity::Off),
595 misconfigured_dependency_overrides: Some(Severity::Off),
596 };
597 rules.apply_partial(&partial);
598 assert_eq!(rules.unused_files, Severity::Off);
599 assert_eq!(rules.private_type_leaks, Severity::Off);
600 assert_eq!(rules.circular_dependencies, Severity::Off);
601 assert_eq!(rules.type_only_dependencies, Severity::Off);
602 assert_eq!(rules.test_only_dependencies, Severity::Off);
603 assert_eq!(rules.boundary_violation, Severity::Off);
604 assert_eq!(rules.coverage_gaps, Severity::Off);
605 assert_eq!(rules.feature_flags, Severity::Off);
606 assert_eq!(rules.stale_suppressions, Severity::Off);
607 }
608
609 #[test]
610 fn rules_config_defaults_include_optional_deps() {
611 let rules = RulesConfig::default();
612 assert_eq!(rules.unused_optional_dependencies, Severity::Warn);
613 }
614
615 #[test]
616 fn severity_from_str_case_insensitive() {
617 assert_eq!("ERROR".parse::<Severity>().unwrap(), Severity::Error);
618 assert_eq!("Warn".parse::<Severity>().unwrap(), Severity::Warn);
619 assert_eq!("OFF".parse::<Severity>().unwrap(), Severity::Off);
620 assert_eq!("Warning".parse::<Severity>().unwrap(), Severity::Warn);
621 assert_eq!("NONE".parse::<Severity>().unwrap(), Severity::Off);
622 }
623
624 #[test]
625 fn severity_from_str_invalid_returns_error() {
626 let result = "critical".parse::<Severity>();
627 assert!(result.is_err());
628 let err = result.unwrap_err();
629 assert!(
630 err.contains("unknown severity"),
631 "Expected descriptive error, got: {err}"
632 );
633 }
634
635 #[test]
638 fn partial_rules_empty_json() {
639 let partial: PartialRulesConfig = serde_json::from_str("{}").unwrap();
640 assert!(partial.unused_files.is_none());
641 assert!(partial.unused_exports.is_none());
642 assert!(partial.unused_types.is_none());
643 assert!(partial.unused_dependencies.is_none());
644 assert!(partial.circular_dependencies.is_none());
645 assert!(partial.boundary_violation.is_none());
646 assert!(partial.coverage_gaps.is_none());
647 assert!(partial.feature_flags.is_none());
648 assert!(partial.stale_suppressions.is_none());
649 }
650
651 #[test]
652 fn partial_rules_subset_json() {
653 let json = r#"{
654 "unused-files": "warn",
655 "circular-dependencies": "off"
656 }"#;
657 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
658 assert_eq!(partial.unused_files, Some(Severity::Warn));
659 assert_eq!(partial.circular_dependencies, Some(Severity::Off));
660 assert!(partial.unused_exports.is_none());
661 }
662
663 #[test]
664 fn partial_rules_deserialize_circular_dependency_alias() {
665 let json = r#"{
666 "circular-dependency": "warn"
667 }"#;
668 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
669 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
670 }
671
672 #[test]
673 fn partial_rules_all_fields_json() {
674 let json = r#"{
675 "unused-files": "error",
676 "unused-exports": "warn",
677 "unused-types": "off",
678 "unused-dependencies": "error",
679 "unused-dev-dependencies": "warn",
680 "unused-optional-dependencies": "off",
681 "unused-enum-members": "error",
682 "unused-class-members": "warn",
683 "unresolved-imports": "off",
684 "unlisted-dependencies": "error",
685 "duplicate-exports": "warn",
686 "type-only-dependencies": "off",
687 "test-only-dependencies": "error",
688 "circular-dependencies": "warn",
689 "boundary-violation": "off",
690 "coverage-gaps": "warn",
691 "feature-flags": "error",
692 "stale-suppressions": "off"
693 }"#;
694 let partial: PartialRulesConfig = serde_json::from_str(json).unwrap();
695 assert_eq!(partial.unused_files, Some(Severity::Error));
696 assert_eq!(partial.unused_exports, Some(Severity::Warn));
697 assert_eq!(partial.unused_types, Some(Severity::Off));
698 assert_eq!(partial.unused_dependencies, Some(Severity::Error));
699 assert_eq!(partial.unused_dev_dependencies, Some(Severity::Warn));
700 assert_eq!(partial.unused_optional_dependencies, Some(Severity::Off));
701 assert_eq!(partial.unused_enum_members, Some(Severity::Error));
702 assert_eq!(partial.unused_class_members, Some(Severity::Warn));
703 assert_eq!(partial.unresolved_imports, Some(Severity::Off));
704 assert_eq!(partial.unlisted_dependencies, Some(Severity::Error));
705 assert_eq!(partial.duplicate_exports, Some(Severity::Warn));
706 assert_eq!(partial.type_only_dependencies, Some(Severity::Off));
707 assert_eq!(partial.test_only_dependencies, Some(Severity::Error));
708 assert_eq!(partial.circular_dependencies, Some(Severity::Warn));
709 assert_eq!(partial.boundary_violation, Some(Severity::Off));
710 assert_eq!(partial.coverage_gaps, Some(Severity::Warn));
711 assert_eq!(partial.feature_flags, Some(Severity::Error));
712 assert_eq!(partial.stale_suppressions, Some(Severity::Off));
713 }
714
715 #[test]
718 fn partial_rules_none_fields_not_serialized() {
719 let partial = PartialRulesConfig::default();
720 let json = serde_json::to_string(&partial).unwrap();
721 assert_eq!(
722 json, "{}",
723 "all-None partial should serialize to empty object"
724 );
725 }
726
727 #[test]
728 fn partial_rules_some_fields_serialized() {
729 let partial = PartialRulesConfig {
730 unused_files: Some(Severity::Warn),
731 ..Default::default()
732 };
733 let json = serde_json::to_string(&partial).unwrap();
734 assert!(json.contains("unused-files"));
735 assert!(!json.contains("unused-exports"));
736 }
737
738 #[test]
741 fn severity_json_deserialization() {
742 let error: Severity = serde_json::from_str(r#""error""#).unwrap();
743 assert_eq!(error, Severity::Error);
744
745 let warn: Severity = serde_json::from_str(r#""warn""#).unwrap();
746 assert_eq!(warn, Severity::Warn);
747
748 let off: Severity = serde_json::from_str(r#""off""#).unwrap();
749 assert_eq!(off, Severity::Off);
750 }
751
752 #[test]
753 fn severity_invalid_json_value_rejected() {
754 let result: Result<Severity, _> = serde_json::from_str(r#""critical""#);
755 assert!(result.is_err());
756 }
757
758 #[test]
761 fn severity_default_is_error() {
762 assert_eq!(Severity::default(), Severity::Error);
763 }
764
765 #[test]
768 fn rules_config_json_roundtrip() {
769 let rules = RulesConfig {
770 unused_files: Severity::Warn,
771 unused_exports: Severity::Off,
772 type_only_dependencies: Severity::Error,
773 ..RulesConfig::default()
774 };
775 let json = serde_json::to_string(&rules).unwrap();
776 let restored: RulesConfig = serde_json::from_str(&json).unwrap();
777 assert_eq!(restored.unused_files, Severity::Warn);
778 assert_eq!(restored.unused_exports, Severity::Off);
779 assert_eq!(restored.type_only_dependencies, Severity::Error);
780 assert_eq!(restored.unused_dependencies, Severity::Error); }
782
783 #[test]
786 fn apply_partial_preserves_type_only_default() {
787 let mut rules = RulesConfig::default();
788 let partial = PartialRulesConfig {
789 unused_files: Some(Severity::Off),
790 ..Default::default()
791 };
792 rules.apply_partial(&partial);
793 assert_eq!(rules.type_only_dependencies, Severity::Warn);
795 assert_eq!(rules.test_only_dependencies, Severity::Warn);
796 }
797}