1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum IssueKind {
22 UnusedFile,
24 UnusedExport,
26 UnusedType,
28 PrivateTypeLeak,
30 UnusedDependency,
32 UnusedDevDependency,
34 UnusedEnumMember,
36 UnusedClassMember,
38 UnresolvedImport,
40 UnlistedDependency,
42 DuplicateExport,
44 CodeDuplication,
46 CircularDependency,
48 TypeOnlyDependency,
50 TestOnlyDependency,
52 BoundaryViolation,
54 CoverageGaps,
56 FeatureFlag,
58 Complexity,
60 StaleSuppression,
62}
63
64impl IssueKind {
65 #[must_use]
67 pub fn parse(s: &str) -> Option<Self> {
68 match s {
69 "unused-file" => Some(Self::UnusedFile),
70 "unused-export" => Some(Self::UnusedExport),
71 "unused-type" => Some(Self::UnusedType),
72 "private-type-leak" => Some(Self::PrivateTypeLeak),
73 "unused-dependency" => Some(Self::UnusedDependency),
74 "unused-dev-dependency" => Some(Self::UnusedDevDependency),
75 "unused-enum-member" => Some(Self::UnusedEnumMember),
76 "unused-class-member" => Some(Self::UnusedClassMember),
77 "unresolved-import" => Some(Self::UnresolvedImport),
78 "unlisted-dependency" => Some(Self::UnlistedDependency),
79 "duplicate-export" => Some(Self::DuplicateExport),
80 "code-duplication" => Some(Self::CodeDuplication),
81 "circular-dependency" => Some(Self::CircularDependency),
82 "type-only-dependency" => Some(Self::TypeOnlyDependency),
83 "test-only-dependency" => Some(Self::TestOnlyDependency),
84 "boundary-violation" => Some(Self::BoundaryViolation),
85 "coverage-gaps" => Some(Self::CoverageGaps),
86 "feature-flag" => Some(Self::FeatureFlag),
87 "complexity" => Some(Self::Complexity),
88 "stale-suppression" => Some(Self::StaleSuppression),
89 _ => None,
90 }
91 }
92
93 #[must_use]
95 pub const fn to_discriminant(self) -> u8 {
96 match self {
97 Self::UnusedFile => 1,
98 Self::UnusedExport => 2,
99 Self::UnusedType => 3,
100 Self::PrivateTypeLeak => 4,
101 Self::UnusedDependency => 5,
102 Self::UnusedDevDependency => 6,
103 Self::UnusedEnumMember => 7,
104 Self::UnusedClassMember => 8,
105 Self::UnresolvedImport => 9,
106 Self::UnlistedDependency => 10,
107 Self::DuplicateExport => 11,
108 Self::CodeDuplication => 12,
109 Self::CircularDependency => 13,
110 Self::TypeOnlyDependency => 14,
111 Self::TestOnlyDependency => 15,
112 Self::BoundaryViolation => 16,
113 Self::CoverageGaps => 17,
114 Self::FeatureFlag => 18,
115 Self::Complexity => 19,
116 Self::StaleSuppression => 20,
117 }
118 }
119
120 #[must_use]
122 pub const fn from_discriminant(d: u8) -> Option<Self> {
123 match d {
124 1 => Some(Self::UnusedFile),
125 2 => Some(Self::UnusedExport),
126 3 => Some(Self::UnusedType),
127 4 => Some(Self::PrivateTypeLeak),
128 5 => Some(Self::UnusedDependency),
129 6 => Some(Self::UnusedDevDependency),
130 7 => Some(Self::UnusedEnumMember),
131 8 => Some(Self::UnusedClassMember),
132 9 => Some(Self::UnresolvedImport),
133 10 => Some(Self::UnlistedDependency),
134 11 => Some(Self::DuplicateExport),
135 12 => Some(Self::CodeDuplication),
136 13 => Some(Self::CircularDependency),
137 14 => Some(Self::TypeOnlyDependency),
138 15 => Some(Self::TestOnlyDependency),
139 16 => Some(Self::BoundaryViolation),
140 17 => Some(Self::CoverageGaps),
141 18 => Some(Self::FeatureFlag),
142 19 => Some(Self::Complexity),
143 20 => Some(Self::StaleSuppression),
144 _ => None,
145 }
146 }
147}
148
149#[derive(Debug, Clone)]
169pub struct Suppression {
170 pub line: u32,
172 pub comment_line: u32,
176 pub kind: Option<IssueKind>,
178}
179
180const _: () = assert!(std::mem::size_of::<Suppression>() == 12);
183const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn issue_kind_from_str_all_variants() {
191 assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
192 assert_eq!(
193 IssueKind::parse("unused-export"),
194 Some(IssueKind::UnusedExport)
195 );
196 assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
197 assert_eq!(
198 IssueKind::parse("private-type-leak"),
199 Some(IssueKind::PrivateTypeLeak)
200 );
201 assert_eq!(
202 IssueKind::parse("unused-dependency"),
203 Some(IssueKind::UnusedDependency)
204 );
205 assert_eq!(
206 IssueKind::parse("unused-dev-dependency"),
207 Some(IssueKind::UnusedDevDependency)
208 );
209 assert_eq!(
210 IssueKind::parse("unused-enum-member"),
211 Some(IssueKind::UnusedEnumMember)
212 );
213 assert_eq!(
214 IssueKind::parse("unused-class-member"),
215 Some(IssueKind::UnusedClassMember)
216 );
217 assert_eq!(
218 IssueKind::parse("unresolved-import"),
219 Some(IssueKind::UnresolvedImport)
220 );
221 assert_eq!(
222 IssueKind::parse("unlisted-dependency"),
223 Some(IssueKind::UnlistedDependency)
224 );
225 assert_eq!(
226 IssueKind::parse("duplicate-export"),
227 Some(IssueKind::DuplicateExport)
228 );
229 assert_eq!(
230 IssueKind::parse("code-duplication"),
231 Some(IssueKind::CodeDuplication)
232 );
233 assert_eq!(
234 IssueKind::parse("circular-dependency"),
235 Some(IssueKind::CircularDependency)
236 );
237 assert_eq!(
238 IssueKind::parse("type-only-dependency"),
239 Some(IssueKind::TypeOnlyDependency)
240 );
241 assert_eq!(
242 IssueKind::parse("test-only-dependency"),
243 Some(IssueKind::TestOnlyDependency)
244 );
245 assert_eq!(
246 IssueKind::parse("boundary-violation"),
247 Some(IssueKind::BoundaryViolation)
248 );
249 assert_eq!(
250 IssueKind::parse("coverage-gaps"),
251 Some(IssueKind::CoverageGaps)
252 );
253 assert_eq!(
254 IssueKind::parse("feature-flag"),
255 Some(IssueKind::FeatureFlag)
256 );
257 assert_eq!(IssueKind::parse("complexity"), Some(IssueKind::Complexity));
258 assert_eq!(
259 IssueKind::parse("stale-suppression"),
260 Some(IssueKind::StaleSuppression)
261 );
262 }
263
264 #[test]
265 fn issue_kind_from_str_unknown() {
266 assert_eq!(IssueKind::parse("foo"), None);
267 assert_eq!(IssueKind::parse(""), None);
268 }
269
270 #[test]
271 fn issue_kind_from_str_near_misses() {
272 assert_eq!(IssueKind::parse("Unused-File"), None);
274 assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
275 assert_eq!(IssueKind::parse("unused_file"), None);
277 assert_eq!(IssueKind::parse("unused-files"), None);
278 }
279
280 #[test]
281 fn discriminant_out_of_range() {
282 assert_eq!(IssueKind::from_discriminant(0), None);
283 assert_eq!(IssueKind::from_discriminant(21), None);
284 assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
285 }
286
287 #[test]
288 fn discriminant_roundtrip() {
289 for kind in [
290 IssueKind::UnusedFile,
291 IssueKind::UnusedExport,
292 IssueKind::UnusedType,
293 IssueKind::PrivateTypeLeak,
294 IssueKind::UnusedDependency,
295 IssueKind::UnusedDevDependency,
296 IssueKind::UnusedEnumMember,
297 IssueKind::UnusedClassMember,
298 IssueKind::UnresolvedImport,
299 IssueKind::UnlistedDependency,
300 IssueKind::DuplicateExport,
301 IssueKind::CodeDuplication,
302 IssueKind::CircularDependency,
303 IssueKind::TypeOnlyDependency,
304 IssueKind::TestOnlyDependency,
305 IssueKind::BoundaryViolation,
306 IssueKind::CoverageGaps,
307 IssueKind::FeatureFlag,
308 IssueKind::Complexity,
309 IssueKind::StaleSuppression,
310 ] {
311 assert_eq!(
312 IssueKind::from_discriminant(kind.to_discriminant()),
313 Some(kind)
314 );
315 }
316 assert_eq!(IssueKind::from_discriminant(0), None);
317 assert_eq!(IssueKind::from_discriminant(21), None);
318 }
319
320 #[test]
323 fn discriminant_values_are_unique() {
324 let all_kinds = [
325 IssueKind::UnusedFile,
326 IssueKind::UnusedExport,
327 IssueKind::UnusedType,
328 IssueKind::PrivateTypeLeak,
329 IssueKind::UnusedDependency,
330 IssueKind::UnusedDevDependency,
331 IssueKind::UnusedEnumMember,
332 IssueKind::UnusedClassMember,
333 IssueKind::UnresolvedImport,
334 IssueKind::UnlistedDependency,
335 IssueKind::DuplicateExport,
336 IssueKind::CodeDuplication,
337 IssueKind::CircularDependency,
338 IssueKind::TypeOnlyDependency,
339 IssueKind::TestOnlyDependency,
340 IssueKind::BoundaryViolation,
341 IssueKind::CoverageGaps,
342 IssueKind::FeatureFlag,
343 IssueKind::Complexity,
344 IssueKind::StaleSuppression,
345 ];
346 let discriminants: Vec<u8> = all_kinds.iter().map(|k| k.to_discriminant()).collect();
347 let mut sorted = discriminants.clone();
348 sorted.sort_unstable();
349 sorted.dedup();
350 assert_eq!(
351 discriminants.len(),
352 sorted.len(),
353 "discriminant values must be unique"
354 );
355 }
356
357 #[test]
360 fn discriminant_starts_at_one() {
361 assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
362 }
363
364 #[test]
367 fn suppression_line_zero_is_file_wide() {
368 let s = Suppression {
369 line: 0,
370 comment_line: 1,
371 kind: None,
372 };
373 assert_eq!(s.line, 0);
374 assert!(s.kind.is_none());
375 }
376
377 #[test]
378 fn suppression_with_specific_kind_and_line() {
379 let s = Suppression {
380 line: 42,
381 comment_line: 41,
382 kind: Some(IssueKind::UnusedExport),
383 };
384 assert_eq!(s.line, 42);
385 assert_eq!(s.comment_line, 41);
386 assert_eq!(s.kind, Some(IssueKind::UnusedExport));
387 }
388}