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