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 SecurityClientServerLeak,
82 SecuritySink,
86 PolicyViolation,
90}
91
92impl IssueKind {
93 #[must_use]
95 pub fn parse(s: &str) -> Option<Self> {
96 match s {
97 "unused-file" => Some(Self::UnusedFile),
98 "unused-export" => Some(Self::UnusedExport),
99 "unused-type" => Some(Self::UnusedType),
100 "private-type-leak" => Some(Self::PrivateTypeLeak),
101 "unused-dependency" => Some(Self::UnusedDependency),
102 "unused-dev-dependency" => Some(Self::UnusedDevDependency),
103 "unused-enum-member" => Some(Self::UnusedEnumMember),
104 "unused-class-member" => Some(Self::UnusedClassMember),
105 "unresolved-import" => Some(Self::UnresolvedImport),
106 "unlisted-dependency" => Some(Self::UnlistedDependency),
107 "duplicate-export" => Some(Self::DuplicateExport),
108 "code-duplication" => Some(Self::CodeDuplication),
109 "circular-dependency" | "circular-dependencies" => Some(Self::CircularDependency),
110 "re-export-cycle" | "re-export-cycles" | "reexport-cycle" | "reexport-cycles" => {
111 Some(Self::ReExportCycle)
112 }
113 "type-only-dependency" => Some(Self::TypeOnlyDependency),
114 "test-only-dependency" => Some(Self::TestOnlyDependency),
115 "boundary-violation" | "boundary-call-violation" | "boundary-call-violations" => {
116 Some(Self::BoundaryViolation)
117 }
118 "coverage-gaps" => Some(Self::CoverageGaps),
119 "feature-flag" => Some(Self::FeatureFlag),
120 "complexity" => Some(Self::Complexity),
121 "stale-suppression" => Some(Self::StaleSuppression),
122 "unused-catalog-entry" | "unused-catalog-entries" => Some(Self::PnpmCatalogEntry),
123 "empty-catalog-group" | "empty-catalog-groups" => Some(Self::EmptyCatalogGroup),
124 "unresolved-catalog-reference" | "unresolved-catalog-references" => {
125 Some(Self::UnresolvedCatalogReference)
126 }
127 "unused-dependency-override" | "unused-dependency-overrides" => {
128 Some(Self::UnusedDependencyOverride)
129 }
130 "misconfigured-dependency-override" | "misconfigured-dependency-overrides" => {
131 Some(Self::MisconfiguredDependencyOverride)
132 }
133 "security-client-server-leak" => Some(Self::SecurityClientServerLeak),
134 "security-sink" => Some(Self::SecuritySink),
135 "policy-violation" | "policy-violations" => Some(Self::PolicyViolation),
136 _ => None,
137 }
138 }
139
140 #[must_use]
142 pub const fn to_discriminant(self) -> u8 {
143 match self {
144 Self::UnusedFile => 1,
145 Self::UnusedExport => 2,
146 Self::UnusedType => 3,
147 Self::PrivateTypeLeak => 4,
148 Self::UnusedDependency => 5,
149 Self::UnusedDevDependency => 6,
150 Self::UnusedEnumMember => 7,
151 Self::UnusedClassMember => 8,
152 Self::UnresolvedImport => 9,
153 Self::UnlistedDependency => 10,
154 Self::DuplicateExport => 11,
155 Self::CodeDuplication => 12,
156 Self::CircularDependency => 13,
157 Self::TypeOnlyDependency => 14,
158 Self::TestOnlyDependency => 15,
159 Self::BoundaryViolation => 16,
160 Self::CoverageGaps => 17,
161 Self::FeatureFlag => 18,
162 Self::Complexity => 19,
163 Self::StaleSuppression => 20,
164 Self::PnpmCatalogEntry => 21,
165 Self::UnresolvedCatalogReference => 22,
166 Self::UnusedDependencyOverride => 23,
167 Self::MisconfiguredDependencyOverride => 24,
168 Self::EmptyCatalogGroup => 25,
169 Self::ReExportCycle => 26,
170 Self::SecurityClientServerLeak => 27,
171 Self::SecuritySink => 28,
172 Self::PolicyViolation => 29,
173 }
174 }
175
176 #[must_use]
178 pub const fn from_discriminant(d: u8) -> Option<Self> {
179 match d {
180 1 => Some(Self::UnusedFile),
181 2 => Some(Self::UnusedExport),
182 3 => Some(Self::UnusedType),
183 4 => Some(Self::PrivateTypeLeak),
184 5 => Some(Self::UnusedDependency),
185 6 => Some(Self::UnusedDevDependency),
186 7 => Some(Self::UnusedEnumMember),
187 8 => Some(Self::UnusedClassMember),
188 9 => Some(Self::UnresolvedImport),
189 10 => Some(Self::UnlistedDependency),
190 11 => Some(Self::DuplicateExport),
191 12 => Some(Self::CodeDuplication),
192 13 => Some(Self::CircularDependency),
193 14 => Some(Self::TypeOnlyDependency),
194 15 => Some(Self::TestOnlyDependency),
195 16 => Some(Self::BoundaryViolation),
196 17 => Some(Self::CoverageGaps),
197 18 => Some(Self::FeatureFlag),
198 19 => Some(Self::Complexity),
199 20 => Some(Self::StaleSuppression),
200 21 => Some(Self::PnpmCatalogEntry),
201 22 => Some(Self::UnresolvedCatalogReference),
202 23 => Some(Self::UnusedDependencyOverride),
203 24 => Some(Self::MisconfiguredDependencyOverride),
204 25 => Some(Self::EmptyCatalogGroup),
205 26 => Some(Self::ReExportCycle),
206 27 => Some(Self::SecurityClientServerLeak),
207 28 => Some(Self::SecuritySink),
208 29 => Some(Self::PolicyViolation),
209 _ => None,
210 }
211 }
212}
213
214#[derive(Debug, Clone)]
234pub struct Suppression {
235 pub line: u32,
237 pub comment_line: u32,
241 pub kind: Option<IssueKind>,
243}
244
245#[derive(Debug, Clone)]
254pub struct UnknownSuppressionKind {
255 pub comment_line: u32,
257 pub is_file_level: bool,
260 pub token: String,
262}
263
264pub const KNOWN_ISSUE_KIND_NAMES: &[&str] = &[
272 "unused-file",
273 "unused-export",
274 "unused-type",
275 "private-type-leak",
276 "unused-dependency",
277 "unused-dev-dependency",
278 "unused-enum-member",
279 "unused-class-member",
280 "unresolved-import",
281 "unlisted-dependency",
282 "duplicate-export",
283 "code-duplication",
284 "circular-dependency",
285 "circular-dependencies",
286 "re-export-cycle",
287 "re-export-cycles",
288 "reexport-cycle",
289 "reexport-cycles",
290 "type-only-dependency",
291 "test-only-dependency",
292 "boundary-violation",
293 "boundary-call-violation",
294 "boundary-call-violations",
295 "coverage-gaps",
296 "feature-flag",
297 "complexity",
298 "stale-suppression",
299 "unused-catalog-entry",
300 "unused-catalog-entries",
301 "empty-catalog-group",
302 "empty-catalog-groups",
303 "unresolved-catalog-reference",
304 "unresolved-catalog-references",
305 "unused-dependency-override",
306 "unused-dependency-overrides",
307 "misconfigured-dependency-override",
308 "misconfigured-dependency-overrides",
309 "security-client-server-leak",
310 "security-sink",
311 "policy-violation",
312 "policy-violations",
313];
314
315pub const DEAD_CODE_FILTER_FLAGS: &[&str] = &[
324 "--unused-files",
325 "--unused-exports",
326 "--unused-types",
327 "--private-type-leaks",
328 "--unused-deps",
329 "--unused-enum-members",
330 "--unused-class-members",
331 "--unresolved-imports",
332 "--unlisted-deps",
333 "--duplicate-exports",
334 "--circular-deps",
335 "--re-export-cycles",
336 "--boundary-violations",
337 "--policy-violations",
338 "--stale-suppressions",
339 "--unused-catalog-entries",
340 "--empty-catalog-groups",
341 "--unresolved-catalog-references",
342 "--unused-dependency-overrides",
343 "--misconfigured-dependency-overrides",
344];
345
346fn levenshtein(a: &str, b: &str) -> usize {
354 let a_bytes = a.as_bytes();
355 let b_bytes = b.as_bytes();
356 let (a_len, b_len) = (a_bytes.len(), b_bytes.len());
357
358 if a_len == 0 {
359 return b_len;
360 }
361 if b_len == 0 {
362 return a_len;
363 }
364
365 let mut prev: Vec<usize> = (0..=b_len).collect();
366 let mut curr: Vec<usize> = vec![0; b_len + 1];
367
368 for i in 1..=a_len {
369 curr[0] = i;
370 for j in 1..=b_len {
371 let cost = usize::from(a_bytes[i - 1] != b_bytes[j - 1]);
372 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
373 }
374 std::mem::swap(&mut prev, &mut curr);
375 }
376
377 prev[b_len]
378}
379
380#[must_use]
387pub fn closest_known_kind_name(input: &str) -> Option<&'static str> {
388 let input_lower = input.to_ascii_lowercase();
389 let mut best: Option<(&'static str, usize)> = None;
390
391 for &candidate in KNOWN_ISSUE_KIND_NAMES {
392 let d = levenshtein(&input_lower, candidate);
393 if best.is_none_or(|(_, b_dist)| d < b_dist) {
394 best = Some((candidate, d));
395 }
396 }
397
398 best.filter(|&(_, d)| d > 0 && d <= 2 && input_lower.len() / 2 > d)
399 .map(|(name, _)| name)
400}
401
402const _: () = assert!(std::mem::size_of::<Suppression>() == 12);
403const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn issue_kind_from_str_all_variants() {
411 assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
412 assert_eq!(
413 IssueKind::parse("unused-export"),
414 Some(IssueKind::UnusedExport)
415 );
416 assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
417 assert_eq!(
418 IssueKind::parse("private-type-leak"),
419 Some(IssueKind::PrivateTypeLeak)
420 );
421 assert_eq!(
422 IssueKind::parse("unused-dependency"),
423 Some(IssueKind::UnusedDependency)
424 );
425 assert_eq!(
426 IssueKind::parse("unused-dev-dependency"),
427 Some(IssueKind::UnusedDevDependency)
428 );
429 assert_eq!(
430 IssueKind::parse("unused-enum-member"),
431 Some(IssueKind::UnusedEnumMember)
432 );
433 assert_eq!(
434 IssueKind::parse("unused-class-member"),
435 Some(IssueKind::UnusedClassMember)
436 );
437 assert_eq!(
438 IssueKind::parse("unresolved-import"),
439 Some(IssueKind::UnresolvedImport)
440 );
441 assert_eq!(
442 IssueKind::parse("unlisted-dependency"),
443 Some(IssueKind::UnlistedDependency)
444 );
445 assert_eq!(
446 IssueKind::parse("duplicate-export"),
447 Some(IssueKind::DuplicateExport)
448 );
449 assert_eq!(
450 IssueKind::parse("code-duplication"),
451 Some(IssueKind::CodeDuplication)
452 );
453 assert_eq!(
454 IssueKind::parse("circular-dependency"),
455 Some(IssueKind::CircularDependency)
456 );
457 assert_eq!(
458 IssueKind::parse("circular-dependencies"),
459 Some(IssueKind::CircularDependency)
460 );
461 assert_eq!(
462 IssueKind::parse("type-only-dependency"),
463 Some(IssueKind::TypeOnlyDependency)
464 );
465 assert_eq!(
466 IssueKind::parse("test-only-dependency"),
467 Some(IssueKind::TestOnlyDependency)
468 );
469 assert_eq!(
470 IssueKind::parse("boundary-violation"),
471 Some(IssueKind::BoundaryViolation)
472 );
473 assert_eq!(
477 IssueKind::parse("boundary-call-violation"),
478 Some(IssueKind::BoundaryViolation)
479 );
480 assert_eq!(
481 IssueKind::parse("boundary-call-violations"),
482 Some(IssueKind::BoundaryViolation)
483 );
484 assert_eq!(
485 IssueKind::parse("coverage-gaps"),
486 Some(IssueKind::CoverageGaps)
487 );
488 assert_eq!(
489 IssueKind::parse("feature-flag"),
490 Some(IssueKind::FeatureFlag)
491 );
492 assert_eq!(IssueKind::parse("complexity"), Some(IssueKind::Complexity));
493 assert_eq!(
494 IssueKind::parse("stale-suppression"),
495 Some(IssueKind::StaleSuppression)
496 );
497 assert_eq!(
498 IssueKind::parse("unused-catalog-entry"),
499 Some(IssueKind::PnpmCatalogEntry)
500 );
501 assert_eq!(
502 IssueKind::parse("unused-catalog-entries"),
503 Some(IssueKind::PnpmCatalogEntry)
504 );
505 assert_eq!(
506 IssueKind::parse("empty-catalog-group"),
507 Some(IssueKind::EmptyCatalogGroup)
508 );
509 assert_eq!(
510 IssueKind::parse("empty-catalog-groups"),
511 Some(IssueKind::EmptyCatalogGroup)
512 );
513 assert_eq!(
514 IssueKind::parse("unresolved-catalog-reference"),
515 Some(IssueKind::UnresolvedCatalogReference)
516 );
517 assert_eq!(
518 IssueKind::parse("unresolved-catalog-references"),
519 Some(IssueKind::UnresolvedCatalogReference)
520 );
521 assert_eq!(
522 IssueKind::parse("unused-dependency-override"),
523 Some(IssueKind::UnusedDependencyOverride)
524 );
525 assert_eq!(
526 IssueKind::parse("unused-dependency-overrides"),
527 Some(IssueKind::UnusedDependencyOverride)
528 );
529 assert_eq!(
530 IssueKind::parse("misconfigured-dependency-override"),
531 Some(IssueKind::MisconfiguredDependencyOverride)
532 );
533 assert_eq!(
534 IssueKind::parse("misconfigured-dependency-overrides"),
535 Some(IssueKind::MisconfiguredDependencyOverride)
536 );
537 assert_eq!(
538 IssueKind::parse("security-client-server-leak"),
539 Some(IssueKind::SecurityClientServerLeak)
540 );
541 assert_eq!(
542 IssueKind::parse("security-sink"),
543 Some(IssueKind::SecuritySink)
544 );
545 assert_eq!(
546 IssueKind::parse("policy-violation"),
547 Some(IssueKind::PolicyViolation)
548 );
549 assert_eq!(
550 IssueKind::parse("policy-violations"),
551 Some(IssueKind::PolicyViolation)
552 );
553 }
554
555 #[test]
556 fn issue_kind_from_str_unknown() {
557 assert_eq!(IssueKind::parse("foo"), None);
558 assert_eq!(IssueKind::parse(""), None);
559 }
560
561 #[test]
562 fn issue_kind_from_str_near_misses() {
563 assert_eq!(IssueKind::parse("Unused-File"), None);
564 assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
565 assert_eq!(IssueKind::parse("unused_file"), None);
566 assert_eq!(IssueKind::parse("unused-files"), None);
567 }
568
569 #[test]
570 fn discriminant_out_of_range() {
571 assert_eq!(IssueKind::from_discriminant(0), None);
572 assert_eq!(
573 IssueKind::from_discriminant(29),
574 Some(IssueKind::PolicyViolation)
575 );
576 assert_eq!(IssueKind::from_discriminant(30), None);
577 assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
578 }
579
580 #[test]
581 fn discriminant_roundtrip() {
582 for kind in [
583 IssueKind::UnusedFile,
584 IssueKind::UnusedExport,
585 IssueKind::UnusedType,
586 IssueKind::PrivateTypeLeak,
587 IssueKind::UnusedDependency,
588 IssueKind::UnusedDevDependency,
589 IssueKind::UnusedEnumMember,
590 IssueKind::UnusedClassMember,
591 IssueKind::UnresolvedImport,
592 IssueKind::UnlistedDependency,
593 IssueKind::DuplicateExport,
594 IssueKind::CodeDuplication,
595 IssueKind::CircularDependency,
596 IssueKind::ReExportCycle,
597 IssueKind::TypeOnlyDependency,
598 IssueKind::TestOnlyDependency,
599 IssueKind::BoundaryViolation,
600 IssueKind::CoverageGaps,
601 IssueKind::FeatureFlag,
602 IssueKind::Complexity,
603 IssueKind::StaleSuppression,
604 IssueKind::PnpmCatalogEntry,
605 IssueKind::EmptyCatalogGroup,
606 IssueKind::UnresolvedCatalogReference,
607 IssueKind::UnusedDependencyOverride,
608 IssueKind::MisconfiguredDependencyOverride,
609 IssueKind::SecurityClientServerLeak,
610 IssueKind::SecuritySink,
611 IssueKind::PolicyViolation,
612 ] {
613 assert_eq!(
614 IssueKind::from_discriminant(kind.to_discriminant()),
615 Some(kind)
616 );
617 }
618 assert_eq!(IssueKind::from_discriminant(0), None);
619 assert_eq!(IssueKind::from_discriminant(30), None);
620 }
621
622 #[test]
623 fn discriminant_values_are_unique() {
624 let all_kinds = [
625 IssueKind::UnusedFile,
626 IssueKind::UnusedExport,
627 IssueKind::UnusedType,
628 IssueKind::PrivateTypeLeak,
629 IssueKind::UnusedDependency,
630 IssueKind::UnusedDevDependency,
631 IssueKind::UnusedEnumMember,
632 IssueKind::UnusedClassMember,
633 IssueKind::UnresolvedImport,
634 IssueKind::UnlistedDependency,
635 IssueKind::DuplicateExport,
636 IssueKind::CodeDuplication,
637 IssueKind::CircularDependency,
638 IssueKind::ReExportCycle,
639 IssueKind::TypeOnlyDependency,
640 IssueKind::TestOnlyDependency,
641 IssueKind::BoundaryViolation,
642 IssueKind::CoverageGaps,
643 IssueKind::FeatureFlag,
644 IssueKind::Complexity,
645 IssueKind::StaleSuppression,
646 IssueKind::PnpmCatalogEntry,
647 IssueKind::EmptyCatalogGroup,
648 IssueKind::UnresolvedCatalogReference,
649 IssueKind::UnusedDependencyOverride,
650 IssueKind::MisconfiguredDependencyOverride,
651 IssueKind::SecurityClientServerLeak,
652 IssueKind::SecuritySink,
653 IssueKind::PolicyViolation,
654 ];
655 let discriminants: Vec<u8> = all_kinds.iter().map(|k| k.to_discriminant()).collect();
656 let mut sorted = discriminants.clone();
657 sorted.sort_unstable();
658 sorted.dedup();
659 assert_eq!(
660 discriminants.len(),
661 sorted.len(),
662 "discriminant values must be unique"
663 );
664 }
665
666 #[test]
667 fn discriminant_starts_at_one() {
668 assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
669 }
670
671 #[test]
672 fn suppression_line_zero_is_file_wide() {
673 let s = Suppression {
674 line: 0,
675 comment_line: 1,
676 kind: None,
677 };
678 assert_eq!(s.line, 0);
679 assert!(s.kind.is_none());
680 }
681
682 #[test]
683 fn suppression_with_specific_kind_and_line() {
684 let s = Suppression {
685 line: 42,
686 comment_line: 41,
687 kind: Some(IssueKind::UnusedExport),
688 };
689 assert_eq!(s.line, 42);
690 assert_eq!(s.comment_line, 41);
691 assert_eq!(s.kind, Some(IssueKind::UnusedExport));
692 }
693
694 #[test]
695 fn known_issue_kind_names_parses_each_entry() {
696 for &name in KNOWN_ISSUE_KIND_NAMES {
697 assert!(
698 IssueKind::parse(name).is_some(),
699 "KNOWN_ISSUE_KIND_NAMES contains '{name}' but IssueKind::parse rejects it"
700 );
701 }
702 }
703
704 #[test]
705 fn closest_known_kind_name_finds_near_misses() {
706 assert_eq!(
707 closest_known_kind_name("unused-exports"),
708 Some("unused-export")
709 );
710 assert_eq!(closest_known_kind_name("unused-files"), Some("unused-file"));
711 assert_eq!(closest_known_kind_name("complxity"), Some("complexity"));
712 }
713
714 #[test]
715 fn closest_known_kind_name_rejects_novel_strings() {
716 assert_eq!(closest_known_kind_name("xyzzy"), None);
717 assert_eq!(closest_known_kind_name("foo"), None);
718 assert_eq!(closest_known_kind_name(""), None);
719 }
720
721 #[test]
722 fn closest_known_kind_name_skips_exact_match() {
723 assert_eq!(closest_known_kind_name("unused-export"), None);
724 }
725}