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 ReExportCycle,
52 TypeOnlyDependency,
54 TestOnlyDependency,
56 BoundaryViolation,
58 CoverageGaps,
60 FeatureFlag,
62 Complexity,
64 StaleSuppression,
66 PnpmCatalogEntry,
68 EmptyCatalogGroup,
70 UnresolvedCatalogReference,
73 UnusedDependencyOverride,
76 MisconfiguredDependencyOverride,
79}
80
81impl IssueKind {
82 #[must_use]
84 pub fn parse(s: &str) -> Option<Self> {
85 match s {
86 "unused-file" => Some(Self::UnusedFile),
87 "unused-export" => Some(Self::UnusedExport),
88 "unused-type" => Some(Self::UnusedType),
89 "private-type-leak" => Some(Self::PrivateTypeLeak),
90 "unused-dependency" => Some(Self::UnusedDependency),
91 "unused-dev-dependency" => Some(Self::UnusedDevDependency),
92 "unused-enum-member" => Some(Self::UnusedEnumMember),
93 "unused-class-member" => Some(Self::UnusedClassMember),
94 "unresolved-import" => Some(Self::UnresolvedImport),
95 "unlisted-dependency" => Some(Self::UnlistedDependency),
96 "duplicate-export" => Some(Self::DuplicateExport),
97 "code-duplication" => Some(Self::CodeDuplication),
98 "circular-dependency" | "circular-dependencies" => Some(Self::CircularDependency),
99 "re-export-cycle" | "re-export-cycles" | "reexport-cycle" | "reexport-cycles" => {
100 Some(Self::ReExportCycle)
101 }
102 "type-only-dependency" => Some(Self::TypeOnlyDependency),
103 "test-only-dependency" => Some(Self::TestOnlyDependency),
104 "boundary-violation" => Some(Self::BoundaryViolation),
105 "coverage-gaps" => Some(Self::CoverageGaps),
106 "feature-flag" => Some(Self::FeatureFlag),
107 "complexity" => Some(Self::Complexity),
108 "stale-suppression" => Some(Self::StaleSuppression),
109 "unused-catalog-entry" | "unused-catalog-entries" => Some(Self::PnpmCatalogEntry),
110 "empty-catalog-group" | "empty-catalog-groups" => Some(Self::EmptyCatalogGroup),
111 "unresolved-catalog-reference" | "unresolved-catalog-references" => {
112 Some(Self::UnresolvedCatalogReference)
113 }
114 "unused-dependency-override" | "unused-dependency-overrides" => {
115 Some(Self::UnusedDependencyOverride)
116 }
117 "misconfigured-dependency-override" | "misconfigured-dependency-overrides" => {
118 Some(Self::MisconfiguredDependencyOverride)
119 }
120 _ => None,
121 }
122 }
123
124 #[must_use]
126 pub const fn to_discriminant(self) -> u8 {
127 match self {
128 Self::UnusedFile => 1,
129 Self::UnusedExport => 2,
130 Self::UnusedType => 3,
131 Self::PrivateTypeLeak => 4,
132 Self::UnusedDependency => 5,
133 Self::UnusedDevDependency => 6,
134 Self::UnusedEnumMember => 7,
135 Self::UnusedClassMember => 8,
136 Self::UnresolvedImport => 9,
137 Self::UnlistedDependency => 10,
138 Self::DuplicateExport => 11,
139 Self::CodeDuplication => 12,
140 Self::CircularDependency => 13,
141 Self::TypeOnlyDependency => 14,
142 Self::TestOnlyDependency => 15,
143 Self::BoundaryViolation => 16,
144 Self::CoverageGaps => 17,
145 Self::FeatureFlag => 18,
146 Self::Complexity => 19,
147 Self::StaleSuppression => 20,
148 Self::PnpmCatalogEntry => 21,
149 Self::UnresolvedCatalogReference => 22,
150 Self::UnusedDependencyOverride => 23,
151 Self::MisconfiguredDependencyOverride => 24,
152 Self::EmptyCatalogGroup => 25,
153 Self::ReExportCycle => 26,
154 }
155 }
156
157 #[must_use]
159 pub const fn from_discriminant(d: u8) -> Option<Self> {
160 match d {
161 1 => Some(Self::UnusedFile),
162 2 => Some(Self::UnusedExport),
163 3 => Some(Self::UnusedType),
164 4 => Some(Self::PrivateTypeLeak),
165 5 => Some(Self::UnusedDependency),
166 6 => Some(Self::UnusedDevDependency),
167 7 => Some(Self::UnusedEnumMember),
168 8 => Some(Self::UnusedClassMember),
169 9 => Some(Self::UnresolvedImport),
170 10 => Some(Self::UnlistedDependency),
171 11 => Some(Self::DuplicateExport),
172 12 => Some(Self::CodeDuplication),
173 13 => Some(Self::CircularDependency),
174 14 => Some(Self::TypeOnlyDependency),
175 15 => Some(Self::TestOnlyDependency),
176 16 => Some(Self::BoundaryViolation),
177 17 => Some(Self::CoverageGaps),
178 18 => Some(Self::FeatureFlag),
179 19 => Some(Self::Complexity),
180 20 => Some(Self::StaleSuppression),
181 21 => Some(Self::PnpmCatalogEntry),
182 22 => Some(Self::UnresolvedCatalogReference),
183 23 => Some(Self::UnusedDependencyOverride),
184 24 => Some(Self::MisconfiguredDependencyOverride),
185 25 => Some(Self::EmptyCatalogGroup),
186 26 => Some(Self::ReExportCycle),
187 _ => None,
188 }
189 }
190}
191
192#[derive(Debug, Clone)]
212pub struct Suppression {
213 pub line: u32,
215 pub comment_line: u32,
219 pub kind: Option<IssueKind>,
221}
222
223#[derive(Debug, Clone)]
232pub struct UnknownSuppressionKind {
233 pub comment_line: u32,
235 pub is_file_level: bool,
238 pub token: String,
240}
241
242pub const KNOWN_ISSUE_KIND_NAMES: &[&str] = &[
250 "unused-file",
251 "unused-export",
252 "unused-type",
253 "private-type-leak",
254 "unused-dependency",
255 "unused-dev-dependency",
256 "unused-enum-member",
257 "unused-class-member",
258 "unresolved-import",
259 "unlisted-dependency",
260 "duplicate-export",
261 "code-duplication",
262 "circular-dependency",
263 "circular-dependencies",
264 "re-export-cycle",
265 "re-export-cycles",
266 "reexport-cycle",
267 "reexport-cycles",
268 "type-only-dependency",
269 "test-only-dependency",
270 "boundary-violation",
271 "coverage-gaps",
272 "feature-flag",
273 "complexity",
274 "stale-suppression",
275 "unused-catalog-entry",
276 "unused-catalog-entries",
277 "empty-catalog-group",
278 "empty-catalog-groups",
279 "unresolved-catalog-reference",
280 "unresolved-catalog-references",
281 "unused-dependency-override",
282 "unused-dependency-overrides",
283 "misconfigured-dependency-override",
284 "misconfigured-dependency-overrides",
285];
286
287fn levenshtein(a: &str, b: &str) -> usize {
295 let a_bytes = a.as_bytes();
296 let b_bytes = b.as_bytes();
297 let (a_len, b_len) = (a_bytes.len(), b_bytes.len());
298
299 if a_len == 0 {
300 return b_len;
301 }
302 if b_len == 0 {
303 return a_len;
304 }
305
306 let mut prev: Vec<usize> = (0..=b_len).collect();
307 let mut curr: Vec<usize> = vec![0; b_len + 1];
308
309 for i in 1..=a_len {
310 curr[0] = i;
311 for j in 1..=b_len {
312 let cost = usize::from(a_bytes[i - 1] != b_bytes[j - 1]);
313 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
314 }
315 std::mem::swap(&mut prev, &mut curr);
316 }
317
318 prev[b_len]
319}
320
321#[must_use]
328pub fn closest_known_kind_name(input: &str) -> Option<&'static str> {
329 let input_lower = input.to_ascii_lowercase();
330 let mut best: Option<(&'static str, usize)> = None;
331
332 for &candidate in KNOWN_ISSUE_KIND_NAMES {
333 let d = levenshtein(&input_lower, candidate);
334 if best.is_none_or(|(_, b_dist)| d < b_dist) {
335 best = Some((candidate, d));
336 }
337 }
338
339 best.filter(|&(_, d)| d > 0 && d <= 2 && input_lower.len() / 2 > d)
340 .map(|(name, _)| name)
341}
342
343const _: () = assert!(std::mem::size_of::<Suppression>() == 12);
346const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn issue_kind_from_str_all_variants() {
354 assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
355 assert_eq!(
356 IssueKind::parse("unused-export"),
357 Some(IssueKind::UnusedExport)
358 );
359 assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
360 assert_eq!(
361 IssueKind::parse("private-type-leak"),
362 Some(IssueKind::PrivateTypeLeak)
363 );
364 assert_eq!(
365 IssueKind::parse("unused-dependency"),
366 Some(IssueKind::UnusedDependency)
367 );
368 assert_eq!(
369 IssueKind::parse("unused-dev-dependency"),
370 Some(IssueKind::UnusedDevDependency)
371 );
372 assert_eq!(
373 IssueKind::parse("unused-enum-member"),
374 Some(IssueKind::UnusedEnumMember)
375 );
376 assert_eq!(
377 IssueKind::parse("unused-class-member"),
378 Some(IssueKind::UnusedClassMember)
379 );
380 assert_eq!(
381 IssueKind::parse("unresolved-import"),
382 Some(IssueKind::UnresolvedImport)
383 );
384 assert_eq!(
385 IssueKind::parse("unlisted-dependency"),
386 Some(IssueKind::UnlistedDependency)
387 );
388 assert_eq!(
389 IssueKind::parse("duplicate-export"),
390 Some(IssueKind::DuplicateExport)
391 );
392 assert_eq!(
393 IssueKind::parse("code-duplication"),
394 Some(IssueKind::CodeDuplication)
395 );
396 assert_eq!(
397 IssueKind::parse("circular-dependency"),
398 Some(IssueKind::CircularDependency)
399 );
400 assert_eq!(
401 IssueKind::parse("circular-dependencies"),
402 Some(IssueKind::CircularDependency)
403 );
404 assert_eq!(
405 IssueKind::parse("type-only-dependency"),
406 Some(IssueKind::TypeOnlyDependency)
407 );
408 assert_eq!(
409 IssueKind::parse("test-only-dependency"),
410 Some(IssueKind::TestOnlyDependency)
411 );
412 assert_eq!(
413 IssueKind::parse("boundary-violation"),
414 Some(IssueKind::BoundaryViolation)
415 );
416 assert_eq!(
417 IssueKind::parse("coverage-gaps"),
418 Some(IssueKind::CoverageGaps)
419 );
420 assert_eq!(
421 IssueKind::parse("feature-flag"),
422 Some(IssueKind::FeatureFlag)
423 );
424 assert_eq!(IssueKind::parse("complexity"), Some(IssueKind::Complexity));
425 assert_eq!(
426 IssueKind::parse("stale-suppression"),
427 Some(IssueKind::StaleSuppression)
428 );
429 assert_eq!(
430 IssueKind::parse("unused-catalog-entry"),
431 Some(IssueKind::PnpmCatalogEntry)
432 );
433 assert_eq!(
434 IssueKind::parse("unused-catalog-entries"),
435 Some(IssueKind::PnpmCatalogEntry)
436 );
437 assert_eq!(
438 IssueKind::parse("empty-catalog-group"),
439 Some(IssueKind::EmptyCatalogGroup)
440 );
441 assert_eq!(
442 IssueKind::parse("empty-catalog-groups"),
443 Some(IssueKind::EmptyCatalogGroup)
444 );
445 assert_eq!(
446 IssueKind::parse("unresolved-catalog-reference"),
447 Some(IssueKind::UnresolvedCatalogReference)
448 );
449 assert_eq!(
450 IssueKind::parse("unresolved-catalog-references"),
451 Some(IssueKind::UnresolvedCatalogReference)
452 );
453 assert_eq!(
454 IssueKind::parse("unused-dependency-override"),
455 Some(IssueKind::UnusedDependencyOverride)
456 );
457 assert_eq!(
458 IssueKind::parse("unused-dependency-overrides"),
459 Some(IssueKind::UnusedDependencyOverride)
460 );
461 assert_eq!(
462 IssueKind::parse("misconfigured-dependency-override"),
463 Some(IssueKind::MisconfiguredDependencyOverride)
464 );
465 assert_eq!(
466 IssueKind::parse("misconfigured-dependency-overrides"),
467 Some(IssueKind::MisconfiguredDependencyOverride)
468 );
469 }
470
471 #[test]
472 fn issue_kind_from_str_unknown() {
473 assert_eq!(IssueKind::parse("foo"), None);
474 assert_eq!(IssueKind::parse(""), None);
475 }
476
477 #[test]
478 fn issue_kind_from_str_near_misses() {
479 assert_eq!(IssueKind::parse("Unused-File"), None);
481 assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
482 assert_eq!(IssueKind::parse("unused_file"), None);
484 assert_eq!(IssueKind::parse("unused-files"), None);
485 }
486
487 #[test]
488 fn discriminant_out_of_range() {
489 assert_eq!(IssueKind::from_discriminant(0), None);
490 assert_eq!(IssueKind::from_discriminant(27), None);
491 assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
492 }
493
494 #[test]
495 fn discriminant_roundtrip() {
496 for kind in [
497 IssueKind::UnusedFile,
498 IssueKind::UnusedExport,
499 IssueKind::UnusedType,
500 IssueKind::PrivateTypeLeak,
501 IssueKind::UnusedDependency,
502 IssueKind::UnusedDevDependency,
503 IssueKind::UnusedEnumMember,
504 IssueKind::UnusedClassMember,
505 IssueKind::UnresolvedImport,
506 IssueKind::UnlistedDependency,
507 IssueKind::DuplicateExport,
508 IssueKind::CodeDuplication,
509 IssueKind::CircularDependency,
510 IssueKind::ReExportCycle,
511 IssueKind::TypeOnlyDependency,
512 IssueKind::TestOnlyDependency,
513 IssueKind::BoundaryViolation,
514 IssueKind::CoverageGaps,
515 IssueKind::FeatureFlag,
516 IssueKind::Complexity,
517 IssueKind::StaleSuppression,
518 IssueKind::PnpmCatalogEntry,
519 IssueKind::EmptyCatalogGroup,
520 IssueKind::UnresolvedCatalogReference,
521 IssueKind::UnusedDependencyOverride,
522 IssueKind::MisconfiguredDependencyOverride,
523 ] {
524 assert_eq!(
525 IssueKind::from_discriminant(kind.to_discriminant()),
526 Some(kind)
527 );
528 }
529 assert_eq!(IssueKind::from_discriminant(0), None);
530 assert_eq!(IssueKind::from_discriminant(27), None);
531 }
532
533 #[test]
536 fn discriminant_values_are_unique() {
537 let all_kinds = [
538 IssueKind::UnusedFile,
539 IssueKind::UnusedExport,
540 IssueKind::UnusedType,
541 IssueKind::PrivateTypeLeak,
542 IssueKind::UnusedDependency,
543 IssueKind::UnusedDevDependency,
544 IssueKind::UnusedEnumMember,
545 IssueKind::UnusedClassMember,
546 IssueKind::UnresolvedImport,
547 IssueKind::UnlistedDependency,
548 IssueKind::DuplicateExport,
549 IssueKind::CodeDuplication,
550 IssueKind::CircularDependency,
551 IssueKind::ReExportCycle,
552 IssueKind::TypeOnlyDependency,
553 IssueKind::TestOnlyDependency,
554 IssueKind::BoundaryViolation,
555 IssueKind::CoverageGaps,
556 IssueKind::FeatureFlag,
557 IssueKind::Complexity,
558 IssueKind::StaleSuppression,
559 IssueKind::PnpmCatalogEntry,
560 IssueKind::EmptyCatalogGroup,
561 IssueKind::UnresolvedCatalogReference,
562 IssueKind::UnusedDependencyOverride,
563 IssueKind::MisconfiguredDependencyOverride,
564 ];
565 let discriminants: Vec<u8> = all_kinds.iter().map(|k| k.to_discriminant()).collect();
566 let mut sorted = discriminants.clone();
567 sorted.sort_unstable();
568 sorted.dedup();
569 assert_eq!(
570 discriminants.len(),
571 sorted.len(),
572 "discriminant values must be unique"
573 );
574 }
575
576 #[test]
579 fn discriminant_starts_at_one() {
580 assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
581 }
582
583 #[test]
586 fn suppression_line_zero_is_file_wide() {
587 let s = Suppression {
588 line: 0,
589 comment_line: 1,
590 kind: None,
591 };
592 assert_eq!(s.line, 0);
593 assert!(s.kind.is_none());
594 }
595
596 #[test]
597 fn suppression_with_specific_kind_and_line() {
598 let s = Suppression {
599 line: 42,
600 comment_line: 41,
601 kind: Some(IssueKind::UnusedExport),
602 };
603 assert_eq!(s.line, 42);
604 assert_eq!(s.comment_line, 41);
605 assert_eq!(s.kind, Some(IssueKind::UnusedExport));
606 }
607
608 #[test]
611 fn known_issue_kind_names_parses_each_entry() {
612 for &name in KNOWN_ISSUE_KIND_NAMES {
613 assert!(
614 IssueKind::parse(name).is_some(),
615 "KNOWN_ISSUE_KIND_NAMES contains '{name}' but IssueKind::parse rejects it"
616 );
617 }
618 }
619
620 #[test]
621 fn closest_known_kind_name_finds_near_misses() {
622 assert_eq!(
624 closest_known_kind_name("unused-exports"),
625 Some("unused-export")
626 );
627 assert_eq!(closest_known_kind_name("unused-files"), Some("unused-file"));
628 assert_eq!(closest_known_kind_name("complxity"), Some("complexity"));
629 }
630
631 #[test]
632 fn closest_known_kind_name_rejects_novel_strings() {
633 assert_eq!(closest_known_kind_name("xyzzy"), None);
635 assert_eq!(closest_known_kind_name("foo"), None);
636 assert_eq!(closest_known_kind_name(""), None);
637 }
638
639 #[test]
640 fn closest_known_kind_name_skips_exact_match() {
641 assert_eq!(closest_known_kind_name("unused-export"), None);
644 }
645}