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, PartialEq, Eq, Hash)]
216pub struct PolicyRuleSuppression {
217 pub pack: String,
219 pub rule_id: String,
221}
222
223impl PolicyRuleSuppression {
224 #[must_use]
226 pub fn new(pack: impl Into<String>, rule_id: impl Into<String>) -> Self {
227 Self {
228 pack: pack.into(),
229 rule_id: rule_id.into(),
230 }
231 }
232
233 #[must_use]
235 pub fn token(&self) -> String {
236 format!("policy-violation:{}/{}", self.pack, self.rule_id)
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq)]
242pub enum SuppressionTarget {
243 Issue(IssueKind),
246 PolicyRule(PolicyRuleSuppression),
249}
250
251impl SuppressionTarget {
252 #[must_use]
255 pub const fn issue_kind(&self) -> Option<IssueKind> {
256 match self {
257 Self::Issue(kind) => Some(*kind),
258 Self::PolicyRule(_) => None,
259 }
260 }
261
262 #[must_use]
264 pub fn token(&self) -> String {
265 match self {
266 Self::Issue(kind) => issue_kind_to_kebab(*kind).to_owned(),
267 Self::PolicyRule(rule) => rule.token(),
268 }
269 }
270}
271
272#[must_use]
274pub const fn issue_kind_to_kebab(kind: IssueKind) -> &'static str {
275 match kind {
276 IssueKind::UnusedFile => "unused-file",
277 IssueKind::UnusedExport => "unused-export",
278 IssueKind::UnusedType => "unused-type",
279 IssueKind::PrivateTypeLeak => "private-type-leak",
280 IssueKind::UnusedDependency => "unused-dependency",
281 IssueKind::UnusedDevDependency => "unused-dev-dependency",
282 IssueKind::UnusedEnumMember => "unused-enum-member",
283 IssueKind::UnusedClassMember => "unused-class-member",
284 IssueKind::UnresolvedImport => "unresolved-import",
285 IssueKind::UnlistedDependency => "unlisted-dependency",
286 IssueKind::DuplicateExport => "duplicate-export",
287 IssueKind::CodeDuplication => "code-duplication",
288 IssueKind::CircularDependency => "circular-dependency",
289 IssueKind::ReExportCycle => "re-export-cycle",
290 IssueKind::TypeOnlyDependency => "type-only-dependency",
291 IssueKind::TestOnlyDependency => "test-only-dependency",
292 IssueKind::BoundaryViolation => "boundary-violation",
293 IssueKind::CoverageGaps => "coverage-gaps",
294 IssueKind::FeatureFlag => "feature-flag",
295 IssueKind::Complexity => "complexity",
296 IssueKind::StaleSuppression => "stale-suppression",
297 IssueKind::PnpmCatalogEntry => "unused-catalog-entry",
298 IssueKind::EmptyCatalogGroup => "empty-catalog-group",
299 IssueKind::UnresolvedCatalogReference => "unresolved-catalog-reference",
300 IssueKind::UnusedDependencyOverride => "unused-dependency-override",
301 IssueKind::MisconfiguredDependencyOverride => "misconfigured-dependency-override",
302 IssueKind::SecurityClientServerLeak => "security-client-server-leak",
303 IssueKind::SecuritySink => "security-sink",
304 IssueKind::PolicyViolation => "policy-violation",
305 }
306}
307
308#[must_use]
310pub fn parse_suppression_target(token: &str) -> Option<SuppressionTarget> {
311 parse_policy_rule_suppression_token(token)
312 .map(SuppressionTarget::PolicyRule)
313 .or_else(|| IssueKind::parse(token).map(SuppressionTarget::Issue))
314}
315
316#[must_use]
321pub fn parse_policy_rule_suppression_token(token: &str) -> Option<PolicyRuleSuppression> {
322 let identity = token
323 .strip_prefix("policy-violation:")
324 .or_else(|| token.strip_prefix("policy-violations:"))?;
325 let (pack, rule_id) = identity.split_once('/')?;
326 if rule_id.contains('/') {
327 return None;
328 }
329 if !is_valid_policy_identifier(pack) || !is_valid_policy_identifier(rule_id) {
330 return None;
331 }
332 Some(PolicyRuleSuppression::new(pack, rule_id))
333}
334
335#[must_use]
338pub fn is_valid_policy_identifier(value: &str) -> bool {
339 !value.is_empty()
340 && value
341 .bytes()
342 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
343}
344
345#[derive(Debug, Clone)]
361pub struct Suppression {
362 pub line: u32,
364 pub comment_line: u32,
368 pub target: Option<SuppressionTarget>,
370}
371
372impl Suppression {
373 #[must_use]
375 pub const fn all(line: u32, comment_line: u32) -> Self {
376 Self {
377 line,
378 comment_line,
379 target: None,
380 }
381 }
382
383 #[must_use]
385 pub const fn issue(line: u32, comment_line: u32, kind: IssueKind) -> Self {
386 Self {
387 line,
388 comment_line,
389 target: Some(SuppressionTarget::Issue(kind)),
390 }
391 }
392
393 #[must_use]
395 pub fn policy_rule(
396 line: u32,
397 comment_line: u32,
398 pack: impl Into<String>,
399 rule_id: impl Into<String>,
400 ) -> Self {
401 Self {
402 line,
403 comment_line,
404 target: Some(SuppressionTarget::PolicyRule(PolicyRuleSuppression::new(
405 pack, rule_id,
406 ))),
407 }
408 }
409
410 #[must_use]
412 pub const fn issue_kind_target(&self) -> Option<IssueKind> {
413 match &self.target {
414 Some(SuppressionTarget::Issue(kind)) => Some(*kind),
415 Some(SuppressionTarget::PolicyRule(_)) | None => None,
416 }
417 }
418
419 #[must_use]
421 pub const fn policy_rule_target(&self) -> Option<&PolicyRuleSuppression> {
422 match &self.target {
423 Some(SuppressionTarget::PolicyRule(rule)) => Some(rule),
424 Some(SuppressionTarget::Issue(_)) | None => None,
425 }
426 }
427
428 #[must_use]
430 pub fn target_token(&self) -> Option<String> {
431 self.target.as_ref().map(SuppressionTarget::token)
432 }
433
434 #[must_use]
436 pub const fn applies_to_line(&self, line: u32) -> bool {
437 self.line == 0 || self.line == line
438 }
439
440 #[must_use]
446 pub fn matches_issue_kind(&self, line: u32, kind: IssueKind) -> bool {
447 self.applies_to_line(line)
448 && match &self.target {
449 None => true,
450 Some(SuppressionTarget::Issue(target_kind)) => *target_kind == kind,
451 Some(SuppressionTarget::PolicyRule(_)) => false,
452 }
453 }
454
455 #[must_use]
457 pub fn matches_policy_rule(&self, line: u32, pack: &str, rule_id: &str) -> bool {
458 self.applies_to_line(line)
459 && match &self.target {
460 None | Some(SuppressionTarget::Issue(IssueKind::PolicyViolation)) => true,
461 Some(SuppressionTarget::Issue(_)) => false,
462 Some(SuppressionTarget::PolicyRule(target)) => {
463 target.pack == pack && target.rule_id == rule_id
464 }
465 }
466 }
467}
468
469#[derive(Debug, Clone)]
478pub struct UnknownSuppressionKind {
479 pub comment_line: u32,
481 pub is_file_level: bool,
484 pub token: String,
486}
487
488pub const KNOWN_ISSUE_KIND_NAMES: &[&str] = &[
496 "unused-file",
497 "unused-export",
498 "unused-type",
499 "private-type-leak",
500 "unused-dependency",
501 "unused-dev-dependency",
502 "unused-enum-member",
503 "unused-class-member",
504 "unresolved-import",
505 "unlisted-dependency",
506 "duplicate-export",
507 "code-duplication",
508 "circular-dependency",
509 "circular-dependencies",
510 "re-export-cycle",
511 "re-export-cycles",
512 "reexport-cycle",
513 "reexport-cycles",
514 "type-only-dependency",
515 "test-only-dependency",
516 "boundary-violation",
517 "boundary-call-violation",
518 "boundary-call-violations",
519 "coverage-gaps",
520 "feature-flag",
521 "complexity",
522 "stale-suppression",
523 "unused-catalog-entry",
524 "unused-catalog-entries",
525 "empty-catalog-group",
526 "empty-catalog-groups",
527 "unresolved-catalog-reference",
528 "unresolved-catalog-references",
529 "unused-dependency-override",
530 "unused-dependency-overrides",
531 "misconfigured-dependency-override",
532 "misconfigured-dependency-overrides",
533 "security-client-server-leak",
534 "security-sink",
535 "policy-violation",
536 "policy-violations",
537];
538
539pub const DEAD_CODE_FILTER_FLAGS: &[&str] = &[
548 "--unused-files",
549 "--unused-exports",
550 "--unused-types",
551 "--private-type-leaks",
552 "--unused-deps",
553 "--unused-enum-members",
554 "--unused-class-members",
555 "--unresolved-imports",
556 "--unlisted-deps",
557 "--duplicate-exports",
558 "--circular-deps",
559 "--re-export-cycles",
560 "--boundary-violations",
561 "--policy-violations",
562 "--stale-suppressions",
563 "--unused-catalog-entries",
564 "--empty-catalog-groups",
565 "--unresolved-catalog-references",
566 "--unused-dependency-overrides",
567 "--misconfigured-dependency-overrides",
568];
569
570fn levenshtein(a: &str, b: &str) -> usize {
578 let a_bytes = a.as_bytes();
579 let b_bytes = b.as_bytes();
580 let (a_len, b_len) = (a_bytes.len(), b_bytes.len());
581
582 if a_len == 0 {
583 return b_len;
584 }
585 if b_len == 0 {
586 return a_len;
587 }
588
589 let mut prev: Vec<usize> = (0..=b_len).collect();
590 let mut curr: Vec<usize> = vec![0; b_len + 1];
591
592 for i in 1..=a_len {
593 curr[0] = i;
594 for j in 1..=b_len {
595 let cost = usize::from(a_bytes[i - 1] != b_bytes[j - 1]);
596 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
597 }
598 std::mem::swap(&mut prev, &mut curr);
599 }
600
601 prev[b_len]
602}
603
604#[must_use]
611pub fn closest_known_kind_name(input: &str) -> Option<&'static str> {
612 let input_lower = input.to_ascii_lowercase();
613 let mut best: Option<(&'static str, usize)> = None;
614
615 for &candidate in KNOWN_ISSUE_KIND_NAMES {
616 let d = levenshtein(&input_lower, candidate);
617 if best.is_none_or(|(_, b_dist)| d < b_dist) {
618 best = Some((candidate, d));
619 }
620 }
621
622 best.filter(|&(_, d)| d > 0 && d <= 2 && input_lower.len() / 2 > d)
623 .map(|(name, _)| name)
624}
625
626const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 #[test]
633 fn issue_kind_from_str_all_variants() {
634 assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
635 assert_eq!(
636 IssueKind::parse("unused-export"),
637 Some(IssueKind::UnusedExport)
638 );
639 assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
640 assert_eq!(
641 IssueKind::parse("private-type-leak"),
642 Some(IssueKind::PrivateTypeLeak)
643 );
644 assert_eq!(
645 IssueKind::parse("unused-dependency"),
646 Some(IssueKind::UnusedDependency)
647 );
648 assert_eq!(
649 IssueKind::parse("unused-dev-dependency"),
650 Some(IssueKind::UnusedDevDependency)
651 );
652 assert_eq!(
653 IssueKind::parse("unused-enum-member"),
654 Some(IssueKind::UnusedEnumMember)
655 );
656 assert_eq!(
657 IssueKind::parse("unused-class-member"),
658 Some(IssueKind::UnusedClassMember)
659 );
660 assert_eq!(
661 IssueKind::parse("unresolved-import"),
662 Some(IssueKind::UnresolvedImport)
663 );
664 assert_eq!(
665 IssueKind::parse("unlisted-dependency"),
666 Some(IssueKind::UnlistedDependency)
667 );
668 assert_eq!(
669 IssueKind::parse("duplicate-export"),
670 Some(IssueKind::DuplicateExport)
671 );
672 assert_eq!(
673 IssueKind::parse("code-duplication"),
674 Some(IssueKind::CodeDuplication)
675 );
676 assert_eq!(
677 IssueKind::parse("circular-dependency"),
678 Some(IssueKind::CircularDependency)
679 );
680 assert_eq!(
681 IssueKind::parse("circular-dependencies"),
682 Some(IssueKind::CircularDependency)
683 );
684 assert_eq!(
685 IssueKind::parse("type-only-dependency"),
686 Some(IssueKind::TypeOnlyDependency)
687 );
688 assert_eq!(
689 IssueKind::parse("test-only-dependency"),
690 Some(IssueKind::TestOnlyDependency)
691 );
692 assert_eq!(
693 IssueKind::parse("boundary-violation"),
694 Some(IssueKind::BoundaryViolation)
695 );
696 assert_eq!(
700 IssueKind::parse("boundary-call-violation"),
701 Some(IssueKind::BoundaryViolation)
702 );
703 assert_eq!(
704 IssueKind::parse("boundary-call-violations"),
705 Some(IssueKind::BoundaryViolation)
706 );
707 assert_eq!(
708 IssueKind::parse("coverage-gaps"),
709 Some(IssueKind::CoverageGaps)
710 );
711 assert_eq!(
712 IssueKind::parse("feature-flag"),
713 Some(IssueKind::FeatureFlag)
714 );
715 assert_eq!(IssueKind::parse("complexity"), Some(IssueKind::Complexity));
716 assert_eq!(
717 IssueKind::parse("stale-suppression"),
718 Some(IssueKind::StaleSuppression)
719 );
720 assert_eq!(
721 IssueKind::parse("unused-catalog-entry"),
722 Some(IssueKind::PnpmCatalogEntry)
723 );
724 assert_eq!(
725 IssueKind::parse("unused-catalog-entries"),
726 Some(IssueKind::PnpmCatalogEntry)
727 );
728 assert_eq!(
729 IssueKind::parse("empty-catalog-group"),
730 Some(IssueKind::EmptyCatalogGroup)
731 );
732 assert_eq!(
733 IssueKind::parse("empty-catalog-groups"),
734 Some(IssueKind::EmptyCatalogGroup)
735 );
736 assert_eq!(
737 IssueKind::parse("unresolved-catalog-reference"),
738 Some(IssueKind::UnresolvedCatalogReference)
739 );
740 assert_eq!(
741 IssueKind::parse("unresolved-catalog-references"),
742 Some(IssueKind::UnresolvedCatalogReference)
743 );
744 assert_eq!(
745 IssueKind::parse("unused-dependency-override"),
746 Some(IssueKind::UnusedDependencyOverride)
747 );
748 assert_eq!(
749 IssueKind::parse("unused-dependency-overrides"),
750 Some(IssueKind::UnusedDependencyOverride)
751 );
752 assert_eq!(
753 IssueKind::parse("misconfigured-dependency-override"),
754 Some(IssueKind::MisconfiguredDependencyOverride)
755 );
756 assert_eq!(
757 IssueKind::parse("misconfigured-dependency-overrides"),
758 Some(IssueKind::MisconfiguredDependencyOverride)
759 );
760 assert_eq!(
761 IssueKind::parse("security-client-server-leak"),
762 Some(IssueKind::SecurityClientServerLeak)
763 );
764 assert_eq!(
765 IssueKind::parse("security-sink"),
766 Some(IssueKind::SecuritySink)
767 );
768 assert_eq!(
769 IssueKind::parse("policy-violation"),
770 Some(IssueKind::PolicyViolation)
771 );
772 assert_eq!(
773 IssueKind::parse("policy-violations"),
774 Some(IssueKind::PolicyViolation)
775 );
776 }
777
778 #[test]
779 fn issue_kind_from_str_unknown() {
780 assert_eq!(IssueKind::parse("foo"), None);
781 assert_eq!(IssueKind::parse(""), None);
782 }
783
784 #[test]
785 fn issue_kind_from_str_near_misses() {
786 assert_eq!(IssueKind::parse("Unused-File"), None);
787 assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
788 assert_eq!(IssueKind::parse("unused_file"), None);
789 assert_eq!(IssueKind::parse("unused-files"), None);
790 }
791
792 #[test]
793 fn discriminant_out_of_range() {
794 assert_eq!(IssueKind::from_discriminant(0), None);
795 assert_eq!(
796 IssueKind::from_discriminant(29),
797 Some(IssueKind::PolicyViolation)
798 );
799 assert_eq!(IssueKind::from_discriminant(30), None);
800 assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
801 }
802
803 #[test]
804 fn discriminant_roundtrip() {
805 for kind in [
806 IssueKind::UnusedFile,
807 IssueKind::UnusedExport,
808 IssueKind::UnusedType,
809 IssueKind::PrivateTypeLeak,
810 IssueKind::UnusedDependency,
811 IssueKind::UnusedDevDependency,
812 IssueKind::UnusedEnumMember,
813 IssueKind::UnusedClassMember,
814 IssueKind::UnresolvedImport,
815 IssueKind::UnlistedDependency,
816 IssueKind::DuplicateExport,
817 IssueKind::CodeDuplication,
818 IssueKind::CircularDependency,
819 IssueKind::ReExportCycle,
820 IssueKind::TypeOnlyDependency,
821 IssueKind::TestOnlyDependency,
822 IssueKind::BoundaryViolation,
823 IssueKind::CoverageGaps,
824 IssueKind::FeatureFlag,
825 IssueKind::Complexity,
826 IssueKind::StaleSuppression,
827 IssueKind::PnpmCatalogEntry,
828 IssueKind::EmptyCatalogGroup,
829 IssueKind::UnresolvedCatalogReference,
830 IssueKind::UnusedDependencyOverride,
831 IssueKind::MisconfiguredDependencyOverride,
832 IssueKind::SecurityClientServerLeak,
833 IssueKind::SecuritySink,
834 IssueKind::PolicyViolation,
835 ] {
836 assert_eq!(
837 IssueKind::from_discriminant(kind.to_discriminant()),
838 Some(kind)
839 );
840 }
841 assert_eq!(IssueKind::from_discriminant(0), None);
842 assert_eq!(IssueKind::from_discriminant(30), None);
843 }
844
845 #[test]
846 fn discriminant_values_are_unique() {
847 let all_kinds = [
848 IssueKind::UnusedFile,
849 IssueKind::UnusedExport,
850 IssueKind::UnusedType,
851 IssueKind::PrivateTypeLeak,
852 IssueKind::UnusedDependency,
853 IssueKind::UnusedDevDependency,
854 IssueKind::UnusedEnumMember,
855 IssueKind::UnusedClassMember,
856 IssueKind::UnresolvedImport,
857 IssueKind::UnlistedDependency,
858 IssueKind::DuplicateExport,
859 IssueKind::CodeDuplication,
860 IssueKind::CircularDependency,
861 IssueKind::ReExportCycle,
862 IssueKind::TypeOnlyDependency,
863 IssueKind::TestOnlyDependency,
864 IssueKind::BoundaryViolation,
865 IssueKind::CoverageGaps,
866 IssueKind::FeatureFlag,
867 IssueKind::Complexity,
868 IssueKind::StaleSuppression,
869 IssueKind::PnpmCatalogEntry,
870 IssueKind::EmptyCatalogGroup,
871 IssueKind::UnresolvedCatalogReference,
872 IssueKind::UnusedDependencyOverride,
873 IssueKind::MisconfiguredDependencyOverride,
874 IssueKind::SecurityClientServerLeak,
875 IssueKind::SecuritySink,
876 IssueKind::PolicyViolation,
877 ];
878 let discriminants: Vec<u8> = all_kinds.iter().map(|k| k.to_discriminant()).collect();
879 let mut sorted = discriminants.clone();
880 sorted.sort_unstable();
881 sorted.dedup();
882 assert_eq!(
883 discriminants.len(),
884 sorted.len(),
885 "discriminant values must be unique"
886 );
887 }
888
889 #[test]
890 fn discriminant_starts_at_one() {
891 assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
892 }
893
894 #[test]
895 fn suppression_line_zero_is_file_wide() {
896 let s = Suppression::all(0, 1);
897 assert_eq!(s.line, 0);
898 assert!(s.issue_kind_target().is_none());
899 }
900
901 #[test]
902 fn suppression_with_specific_kind_and_line() {
903 let s = Suppression::issue(42, 41, IssueKind::UnusedExport);
904 assert_eq!(s.line, 42);
905 assert_eq!(s.comment_line, 41);
906 assert_eq!(s.issue_kind_target(), Some(IssueKind::UnusedExport));
907 }
908
909 #[test]
910 fn parses_scoped_policy_suppression_token() {
911 let target =
912 parse_policy_rule_suppression_token("policy-violation:team-policy/no-child-process")
913 .expect("scoped token should parse");
914 assert_eq!(target.pack, "team-policy");
915 assert_eq!(target.rule_id, "no-child-process");
916 assert_eq!(
917 target.token(),
918 "policy-violation:team-policy/no-child-process"
919 );
920 }
921
922 #[test]
923 fn rejects_malformed_scoped_policy_suppression_tokens() {
924 for token in [
925 "policy-violation:",
926 "policy-violation:team-policy",
927 "policy-violation:/no-child-process",
928 "policy-violation:team-policy/",
929 "policy-violation:team-policy/no/child-process",
930 "policy-violation:team policy/no-child-process",
931 "policy-violation:team-policy/no:child-process",
932 ] {
933 assert!(
934 parse_policy_rule_suppression_token(token).is_none(),
935 "{token} should be rejected"
936 );
937 }
938 }
939
940 #[test]
941 fn scoped_policy_suppression_matches_exact_policy_rule_only() {
942 let suppression = Suppression::policy_rule(7, 6, "team-policy", "no-child-process");
943 assert!(suppression.matches_policy_rule(7, "team-policy", "no-child-process"));
944 assert!(!suppression.matches_policy_rule(7, "team-policy", "no-fs"));
945 assert!(!suppression.matches_policy_rule(8, "team-policy", "no-child-process"));
946 assert!(!suppression.matches_issue_kind(7, IssueKind::PolicyViolation));
947 }
948
949 #[test]
950 fn known_issue_kind_names_parses_each_entry() {
951 for &name in KNOWN_ISSUE_KIND_NAMES {
952 assert!(
953 IssueKind::parse(name).is_some(),
954 "KNOWN_ISSUE_KIND_NAMES contains '{name}' but IssueKind::parse rejects it"
955 );
956 }
957 }
958
959 #[test]
960 fn closest_known_kind_name_finds_near_misses() {
961 assert_eq!(
962 closest_known_kind_name("unused-exports"),
963 Some("unused-export")
964 );
965 assert_eq!(closest_known_kind_name("unused-files"), Some("unused-file"));
966 assert_eq!(closest_known_kind_name("complxity"), Some("complexity"));
967 }
968
969 #[test]
970 fn closest_known_kind_name_rejects_novel_strings() {
971 assert_eq!(closest_known_kind_name("xyzzy"), None);
972 assert_eq!(closest_known_kind_name("foo"), None);
973 assert_eq!(closest_known_kind_name(""), None);
974 }
975
976 #[test]
977 fn closest_known_kind_name_skips_exact_match() {
978 assert_eq!(closest_known_kind_name("unused-export"), None);
979 }
980}