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