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
27impl std::fmt::Display for Severity {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 Self::Error => write!(f, "error"),
31 Self::Warn => write!(f, "warn"),
32 Self::Off => write!(f, "off"),
33 }
34 }
35}
36
37impl std::str::FromStr for Severity {
38 type Err = String;
39
40 fn from_str(s: &str) -> Result<Self, Self::Err> {
41 match s.to_lowercase().as_str() {
42 "error" => Ok(Self::Error),
43 "warn" | "warning" => Ok(Self::Warn),
44 "off" | "none" => Ok(Self::Off),
45 other => Err(format!(
46 "unknown severity: '{other}' (expected error, warn, or off)"
47 )),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
59#[serde(rename_all = "kebab-case")]
60pub struct RulesConfig {
61 #[serde(default)]
62 pub unused_files: Severity,
63 #[serde(default)]
64 pub unused_exports: Severity,
65 #[serde(default)]
66 pub unused_types: Severity,
67 #[serde(default)]
68 pub unused_dependencies: Severity,
69 #[serde(default)]
70 pub unused_dev_dependencies: Severity,
71 #[serde(default)]
72 pub unused_optional_dependencies: Severity,
73 #[serde(default)]
74 pub unused_enum_members: Severity,
75 #[serde(default)]
76 pub unused_class_members: Severity,
77 #[serde(default)]
78 pub unresolved_imports: Severity,
79 #[serde(default)]
80 pub unlisted_dependencies: Severity,
81 #[serde(default)]
82 pub duplicate_exports: Severity,
83 #[serde(default = "Severity::default_warn")]
84 pub type_only_dependencies: Severity,
85 #[serde(default = "Severity::default_warn")]
86 pub test_only_dependencies: Severity,
87 #[serde(default)]
88 pub circular_dependencies: Severity,
89}
90
91impl Default for RulesConfig {
92 fn default() -> Self {
93 Self {
94 unused_files: Severity::Error,
95 unused_exports: Severity::Error,
96 unused_types: Severity::Error,
97 unused_dependencies: Severity::Error,
98 unused_dev_dependencies: Severity::Error,
99 unused_optional_dependencies: Severity::Error,
100 unused_enum_members: Severity::Error,
101 unused_class_members: Severity::Error,
102 unresolved_imports: Severity::Error,
103 unlisted_dependencies: Severity::Error,
104 duplicate_exports: Severity::Error,
105 type_only_dependencies: Severity::Warn,
106 test_only_dependencies: Severity::Warn,
107 circular_dependencies: Severity::Error,
108 }
109 }
110}
111
112impl RulesConfig {
113 pub const fn apply_partial(&mut self, partial: &PartialRulesConfig) {
115 if let Some(s) = partial.unused_files {
116 self.unused_files = s;
117 }
118 if let Some(s) = partial.unused_exports {
119 self.unused_exports = s;
120 }
121 if let Some(s) = partial.unused_types {
122 self.unused_types = s;
123 }
124 if let Some(s) = partial.unused_dependencies {
125 self.unused_dependencies = s;
126 }
127 if let Some(s) = partial.unused_dev_dependencies {
128 self.unused_dev_dependencies = s;
129 }
130 if let Some(s) = partial.unused_optional_dependencies {
131 self.unused_optional_dependencies = s;
132 }
133 if let Some(s) = partial.unused_enum_members {
134 self.unused_enum_members = s;
135 }
136 if let Some(s) = partial.unused_class_members {
137 self.unused_class_members = s;
138 }
139 if let Some(s) = partial.unresolved_imports {
140 self.unresolved_imports = s;
141 }
142 if let Some(s) = partial.unlisted_dependencies {
143 self.unlisted_dependencies = s;
144 }
145 if let Some(s) = partial.duplicate_exports {
146 self.duplicate_exports = s;
147 }
148 if let Some(s) = partial.type_only_dependencies {
149 self.type_only_dependencies = s;
150 }
151 if let Some(s) = partial.test_only_dependencies {
152 self.test_only_dependencies = s;
153 }
154 if let Some(s) = partial.circular_dependencies {
155 self.circular_dependencies = s;
156 }
157 }
158}
159
160#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)]
162#[serde(rename_all = "kebab-case")]
163pub struct PartialRulesConfig {
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub unused_files: Option<Severity>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub unused_exports: Option<Severity>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub unused_types: Option<Severity>,
170 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub unused_dependencies: Option<Severity>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub unused_dev_dependencies: Option<Severity>,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub unused_optional_dependencies: Option<Severity>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub unused_enum_members: Option<Severity>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub unused_class_members: Option<Severity>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub unresolved_imports: Option<Severity>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub unlisted_dependencies: Option<Severity>,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub duplicate_exports: Option<Severity>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub type_only_dependencies: Option<Severity>,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub test_only_dependencies: Option<Severity>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub circular_dependencies: Option<Severity>,
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn rules_default_all_error_except_type_only() {
200 let rules = RulesConfig::default();
201 assert_eq!(rules.unused_files, Severity::Error);
202 assert_eq!(rules.unused_exports, Severity::Error);
203 assert_eq!(rules.unused_types, Severity::Error);
204 assert_eq!(rules.unused_dependencies, Severity::Error);
205 assert_eq!(rules.unused_dev_dependencies, Severity::Error);
206 assert_eq!(rules.unused_enum_members, Severity::Error);
207 assert_eq!(rules.unused_class_members, Severity::Error);
208 assert_eq!(rules.unresolved_imports, Severity::Error);
209 assert_eq!(rules.unlisted_dependencies, Severity::Error);
210 assert_eq!(rules.duplicate_exports, Severity::Error);
211 assert_eq!(rules.type_only_dependencies, Severity::Warn);
212 assert_eq!(rules.test_only_dependencies, Severity::Warn);
213 assert_eq!(rules.circular_dependencies, Severity::Error);
214 }
215
216 #[test]
217 fn rules_deserialize_kebab_case() {
218 let json_str = r#"{
219 "unused-files": "error",
220 "unused-exports": "warn",
221 "unused-types": "off"
222 }"#;
223 let rules: RulesConfig = serde_json::from_str(json_str).unwrap();
224 assert_eq!(rules.unused_files, Severity::Error);
225 assert_eq!(rules.unused_exports, Severity::Warn);
226 assert_eq!(rules.unused_types, Severity::Off);
227 assert_eq!(rules.unresolved_imports, Severity::Error);
229 }
230
231 #[test]
232 fn severity_from_str() {
233 assert_eq!("error".parse::<Severity>().unwrap(), Severity::Error);
234 assert_eq!("warn".parse::<Severity>().unwrap(), Severity::Warn);
235 assert_eq!("warning".parse::<Severity>().unwrap(), Severity::Warn);
236 assert_eq!("off".parse::<Severity>().unwrap(), Severity::Off);
237 assert_eq!("none".parse::<Severity>().unwrap(), Severity::Off);
238 assert!("invalid".parse::<Severity>().is_err());
239 }
240
241 #[test]
242 fn apply_partial_only_some_fields() {
243 let mut rules = RulesConfig::default();
244 let partial = PartialRulesConfig {
245 unused_files: Some(Severity::Warn),
246 unused_exports: Some(Severity::Off),
247 ..Default::default()
248 };
249 rules.apply_partial(&partial);
250 assert_eq!(rules.unused_files, Severity::Warn);
251 assert_eq!(rules.unused_exports, Severity::Off);
252 assert_eq!(rules.unused_types, Severity::Error);
254 assert_eq!(rules.unresolved_imports, Severity::Error);
255 }
256
257 #[test]
258 fn severity_display() {
259 assert_eq!(Severity::Error.to_string(), "error");
260 assert_eq!(Severity::Warn.to_string(), "warn");
261 assert_eq!(Severity::Off.to_string(), "off");
262 }
263
264 #[test]
265 fn apply_partial_all_none_changes_nothing() {
266 let mut rules = RulesConfig::default();
267 let original = rules.clone();
268 let partial = PartialRulesConfig::default(); rules.apply_partial(&partial);
270 assert_eq!(rules.unused_files, original.unused_files);
271 assert_eq!(rules.unused_exports, original.unused_exports);
272 assert_eq!(
273 rules.type_only_dependencies,
274 original.type_only_dependencies
275 );
276 }
277
278 #[test]
279 fn apply_partial_all_fields_set() {
280 let mut rules = RulesConfig::default();
281 let partial = PartialRulesConfig {
282 unused_files: Some(Severity::Off),
283 unused_exports: Some(Severity::Off),
284 unused_types: Some(Severity::Off),
285 unused_dependencies: Some(Severity::Off),
286 unused_dev_dependencies: Some(Severity::Off),
287 unused_optional_dependencies: Some(Severity::Off),
288 unused_enum_members: Some(Severity::Off),
289 unused_class_members: Some(Severity::Off),
290 unresolved_imports: Some(Severity::Off),
291 unlisted_dependencies: Some(Severity::Off),
292 duplicate_exports: Some(Severity::Off),
293 type_only_dependencies: Some(Severity::Off),
294 test_only_dependencies: Some(Severity::Off),
295 circular_dependencies: Some(Severity::Off),
296 };
297 rules.apply_partial(&partial);
298 assert_eq!(rules.unused_files, Severity::Off);
299 assert_eq!(rules.circular_dependencies, Severity::Off);
300 assert_eq!(rules.type_only_dependencies, Severity::Off);
301 assert_eq!(rules.test_only_dependencies, Severity::Off);
302 }
303
304 #[test]
305 fn rules_config_defaults_include_optional_deps() {
306 let rules = RulesConfig::default();
307 assert_eq!(rules.unused_optional_dependencies, Severity::Error);
308 }
309
310 #[test]
311 fn severity_from_str_case_insensitive() {
312 assert_eq!("ERROR".parse::<Severity>().unwrap(), Severity::Error);
313 assert_eq!("Warn".parse::<Severity>().unwrap(), Severity::Warn);
314 assert_eq!("OFF".parse::<Severity>().unwrap(), Severity::Off);
315 assert_eq!("Warning".parse::<Severity>().unwrap(), Severity::Warn);
316 assert_eq!("NONE".parse::<Severity>().unwrap(), Severity::Off);
317 }
318
319 #[test]
320 fn severity_from_str_invalid_returns_error() {
321 let result = "critical".parse::<Severity>();
322 assert!(result.is_err());
323 let err = result.unwrap_err();
324 assert!(
325 err.contains("unknown severity"),
326 "Expected descriptive error, got: {err}"
327 );
328 }
329}