1pub use crate::issue_meta::{DEAD_CODE_FILTER_FLAGS, KNOWN_ISSUE_KIND_NAMES};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum IssueKind {
24 UnusedFile,
26 UnusedExport,
28 UnusedType,
30 PrivateTypeLeak,
32 UnusedDependency,
34 UnusedDevDependency,
36 UnusedEnumMember,
38 UnusedClassMember,
40 UnresolvedImport,
42 UnlistedDependency,
44 DuplicateExport,
46 CodeDuplication,
48 CircularDependency,
50 ReExportCycle,
54 TypeOnlyDependency,
56 TestOnlyDependency,
58 BoundaryViolation,
60 CoverageGaps,
62 FeatureFlag,
64 Complexity,
66 StaleSuppression,
68 PnpmCatalogEntry,
70 EmptyCatalogGroup,
72 UnresolvedCatalogReference,
75 UnusedDependencyOverride,
78 MisconfiguredDependencyOverride,
81 SecurityClientServerLeak,
84 SecuritySink,
88 PolicyViolation,
92 InvalidClientExport,
95 MixedClientServerBarrel,
99 MisplacedDirective,
104 UnusedStoreMember,
109 UnprovidedInject,
113 RouteCollision,
116 DynamicSegmentNameConflict,
120 UnrenderedComponent,
123 UnusedComponentProp,
127 AbsentComponentProp,
129 UnusedComponentEmit,
133 UnusedComponentInput,
138 UnusedComponentOutput,
143 UnusedServerAction,
148 UnusedLoadDataKey,
153 PropDrilling,
158 ThinWrapper,
163 DuplicatePropShape,
169 UnusedSvelteEvent,
174 CssTokenDrift,
180 CssDuplicateBlock,
185 CssSelectorComplexity,
189 CssDeadSurface,
193 CssBrokenReference,
198 DevDependencyInProduction,
204 DeprecatedExportInUse,
208 PackageCycle,
211}
212
213impl IssueKind {
214 pub const ALL: &'static [Self] = &[
216 Self::UnusedFile,
217 Self::UnusedExport,
218 Self::UnusedType,
219 Self::PrivateTypeLeak,
220 Self::UnusedDependency,
221 Self::UnusedDevDependency,
222 Self::UnusedEnumMember,
223 Self::UnusedClassMember,
224 Self::UnresolvedImport,
225 Self::UnlistedDependency,
226 Self::DuplicateExport,
227 Self::CodeDuplication,
228 Self::CircularDependency,
229 Self::ReExportCycle,
230 Self::TypeOnlyDependency,
231 Self::TestOnlyDependency,
232 Self::BoundaryViolation,
233 Self::CoverageGaps,
234 Self::FeatureFlag,
235 Self::Complexity,
236 Self::StaleSuppression,
237 Self::PnpmCatalogEntry,
238 Self::EmptyCatalogGroup,
239 Self::UnresolvedCatalogReference,
240 Self::UnusedDependencyOverride,
241 Self::MisconfiguredDependencyOverride,
242 Self::SecurityClientServerLeak,
243 Self::SecuritySink,
244 Self::PolicyViolation,
245 Self::InvalidClientExport,
246 Self::MixedClientServerBarrel,
247 Self::MisplacedDirective,
248 Self::UnusedStoreMember,
249 Self::UnprovidedInject,
250 Self::RouteCollision,
251 Self::DynamicSegmentNameConflict,
252 Self::UnrenderedComponent,
253 Self::UnusedComponentProp,
254 Self::AbsentComponentProp,
255 Self::UnusedComponentEmit,
256 Self::UnusedComponentInput,
257 Self::UnusedComponentOutput,
258 Self::UnusedServerAction,
259 Self::UnusedLoadDataKey,
260 Self::PropDrilling,
261 Self::ThinWrapper,
262 Self::DuplicatePropShape,
263 Self::UnusedSvelteEvent,
264 Self::CssTokenDrift,
265 Self::CssDuplicateBlock,
266 Self::CssSelectorComplexity,
267 Self::CssDeadSurface,
268 Self::CssBrokenReference,
269 Self::DevDependencyInProduction,
270 Self::DeprecatedExportInUse,
271 Self::PackageCycle,
272 ];
273
274 #[must_use]
276 pub fn parse(s: &str) -> Option<Self> {
277 crate::issue_meta::issue_meta_for_token(s).and_then(|meta| meta.kind)
278 }
279
280 #[must_use]
282 pub const fn to_discriminant(self) -> u8 {
283 match self {
284 Self::UnusedFile => 1,
285 Self::UnusedExport => 2,
286 Self::UnusedType => 3,
287 Self::PrivateTypeLeak => 4,
288 Self::UnusedDependency => 5,
289 Self::UnusedDevDependency => 6,
290 Self::UnusedEnumMember => 7,
291 Self::UnusedClassMember => 8,
292 Self::UnresolvedImport => 9,
293 Self::UnlistedDependency => 10,
294 Self::DuplicateExport => 11,
295 Self::CodeDuplication => 12,
296 Self::CircularDependency => 13,
297 Self::TypeOnlyDependency => 14,
298 Self::TestOnlyDependency => 15,
299 Self::BoundaryViolation => 16,
300 Self::CoverageGaps => 17,
301 Self::FeatureFlag => 18,
302 Self::Complexity => 19,
303 Self::StaleSuppression => 20,
304 Self::PnpmCatalogEntry => 21,
305 Self::UnresolvedCatalogReference => 22,
306 Self::UnusedDependencyOverride => 23,
307 Self::MisconfiguredDependencyOverride => 24,
308 Self::EmptyCatalogGroup => 25,
309 Self::ReExportCycle => 26,
310 Self::SecurityClientServerLeak => 27,
311 Self::SecuritySink => 28,
312 Self::PolicyViolation => 29,
313 Self::InvalidClientExport => 30,
314 Self::MixedClientServerBarrel => 31,
315 Self::MisplacedDirective => 32,
316 Self::UnusedStoreMember => 33,
317 Self::UnprovidedInject => 34,
318 Self::RouteCollision => 35,
319 Self::DynamicSegmentNameConflict => 36,
320 Self::UnrenderedComponent => 37,
321 Self::UnusedComponentProp => 38,
322 Self::UnusedComponentEmit => 39,
323 Self::UnusedServerAction => 40,
324 Self::UnusedLoadDataKey => 41,
325 Self::PropDrilling => 42,
326 Self::ThinWrapper => 43,
327 Self::DuplicatePropShape => 44,
328 Self::UnusedComponentInput => 45,
329 Self::UnusedComponentOutput => 46,
330 Self::UnusedSvelteEvent => 47,
331 Self::CssTokenDrift => 48,
332 Self::CssDuplicateBlock => 49,
333 Self::CssSelectorComplexity => 50,
334 Self::CssDeadSurface => 51,
335 Self::CssBrokenReference => 52,
336 Self::DevDependencyInProduction => 53,
337 Self::DeprecatedExportInUse => 54,
338 Self::PackageCycle => 55,
339 Self::AbsentComponentProp => 56,
340 }
341 }
342
343 #[must_use]
345 pub const fn from_discriminant(d: u8) -> Option<Self> {
346 match d {
347 1 => Some(Self::UnusedFile),
348 2 => Some(Self::UnusedExport),
349 3 => Some(Self::UnusedType),
350 4 => Some(Self::PrivateTypeLeak),
351 5 => Some(Self::UnusedDependency),
352 6 => Some(Self::UnusedDevDependency),
353 7 => Some(Self::UnusedEnumMember),
354 8 => Some(Self::UnusedClassMember),
355 9 => Some(Self::UnresolvedImport),
356 10 => Some(Self::UnlistedDependency),
357 11 => Some(Self::DuplicateExport),
358 12 => Some(Self::CodeDuplication),
359 13 => Some(Self::CircularDependency),
360 14 => Some(Self::TypeOnlyDependency),
361 15 => Some(Self::TestOnlyDependency),
362 16 => Some(Self::BoundaryViolation),
363 17 => Some(Self::CoverageGaps),
364 18 => Some(Self::FeatureFlag),
365 19 => Some(Self::Complexity),
366 20 => Some(Self::StaleSuppression),
367 21 => Some(Self::PnpmCatalogEntry),
368 22 => Some(Self::UnresolvedCatalogReference),
369 23 => Some(Self::UnusedDependencyOverride),
370 24 => Some(Self::MisconfiguredDependencyOverride),
371 25 => Some(Self::EmptyCatalogGroup),
372 26 => Some(Self::ReExportCycle),
373 27 => Some(Self::SecurityClientServerLeak),
374 28 => Some(Self::SecuritySink),
375 29 => Some(Self::PolicyViolation),
376 30 => Some(Self::InvalidClientExport),
377 31 => Some(Self::MixedClientServerBarrel),
378 32 => Some(Self::MisplacedDirective),
379 33 => Some(Self::UnusedStoreMember),
380 34 => Some(Self::UnprovidedInject),
381 35 => Some(Self::RouteCollision),
382 36 => Some(Self::DynamicSegmentNameConflict),
383 37 => Some(Self::UnrenderedComponent),
384 38 => Some(Self::UnusedComponentProp),
385 39 => Some(Self::UnusedComponentEmit),
386 40 => Some(Self::UnusedServerAction),
387 41 => Some(Self::UnusedLoadDataKey),
388 42 => Some(Self::PropDrilling),
389 43 => Some(Self::ThinWrapper),
390 44 => Some(Self::DuplicatePropShape),
391 45 => Some(Self::UnusedComponentInput),
392 46 => Some(Self::UnusedComponentOutput),
393 47 => Some(Self::UnusedSvelteEvent),
394 48 => Some(Self::CssTokenDrift),
395 49 => Some(Self::CssDuplicateBlock),
396 50 => Some(Self::CssSelectorComplexity),
397 51 => Some(Self::CssDeadSurface),
398 52 => Some(Self::CssBrokenReference),
399 53 => Some(Self::DevDependencyInProduction),
400 54 => Some(Self::DeprecatedExportInUse),
401 55 => Some(Self::PackageCycle),
402 56 => Some(Self::AbsentComponentProp),
403 _ => None,
404 }
405 }
406}
407
408#[derive(Debug, Clone, PartialEq, Eq, Hash)]
410pub struct PolicyRuleSuppression {
411 pub pack: String,
413 pub rule_id: String,
415}
416
417impl PolicyRuleSuppression {
418 #[must_use]
420 pub fn new(pack: impl Into<String>, rule_id: impl Into<String>) -> Self {
421 Self {
422 pack: pack.into(),
423 rule_id: rule_id.into(),
424 }
425 }
426
427 #[must_use]
429 pub fn token(&self) -> String {
430 format!("policy-violation:{}/{}", self.pack, self.rule_id)
431 }
432}
433
434#[derive(Debug, Clone, PartialEq, Eq)]
436pub enum SuppressionTarget {
437 Issue(IssueKind),
440 PolicyRule(PolicyRuleSuppression),
443}
444
445impl SuppressionTarget {
446 #[must_use]
449 pub const fn issue_kind(&self) -> Option<IssueKind> {
450 match self {
451 Self::Issue(kind) => Some(*kind),
452 Self::PolicyRule(_) => None,
453 }
454 }
455
456 #[must_use]
458 pub fn token(&self) -> String {
459 match self {
460 Self::Issue(kind) => issue_kind_to_kebab(*kind).to_owned(),
461 Self::PolicyRule(rule) => rule.token(),
462 }
463 }
464}
465
466#[must_use]
468pub fn issue_kind_to_kebab(kind: IssueKind) -> &'static str {
469 let Some(meta) = crate::issue_meta::issue_meta_by_kind(kind) else {
470 unreachable!("IssueKind {kind:?} has no metadata row");
471 };
472 meta.suppress_token.unwrap_or(meta.code)
473}
474
475#[must_use]
477pub fn parse_suppression_target(token: &str) -> Option<SuppressionTarget> {
478 parse_policy_rule_suppression_token(token)
479 .map(SuppressionTarget::PolicyRule)
480 .or_else(|| IssueKind::parse(token).map(SuppressionTarget::Issue))
481}
482
483#[must_use]
488pub fn parse_policy_rule_suppression_token(token: &str) -> Option<PolicyRuleSuppression> {
489 let identity = token
490 .strip_prefix("policy-violation:")
491 .or_else(|| token.strip_prefix("policy-violations:"))?;
492 let (pack, rule_id) = identity.split_once('/')?;
493 if rule_id.contains('/') {
494 return None;
495 }
496 if !is_valid_policy_identifier(pack) || !is_valid_policy_identifier(rule_id) {
497 return None;
498 }
499 Some(PolicyRuleSuppression::new(pack, rule_id))
500}
501
502#[must_use]
505pub fn is_valid_policy_identifier(value: &str) -> bool {
506 !value.is_empty()
507 && value
508 .bytes()
509 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
510}
511
512#[derive(Debug, Clone)]
528pub struct Suppression {
529 pub line: u32,
531 pub comment_line: u32,
535 pub target: Option<SuppressionTarget>,
537 pub reason: Option<String>,
539}
540
541impl Suppression {
542 #[must_use]
544 pub const fn all(line: u32, comment_line: u32) -> Self {
545 Self {
546 line,
547 comment_line,
548 target: None,
549 reason: None,
550 }
551 }
552
553 #[must_use]
555 pub const fn issue(line: u32, comment_line: u32, kind: IssueKind) -> Self {
556 Self {
557 line,
558 comment_line,
559 target: Some(SuppressionTarget::Issue(kind)),
560 reason: None,
561 }
562 }
563
564 #[must_use]
566 pub fn policy_rule(
567 line: u32,
568 comment_line: u32,
569 pack: impl Into<String>,
570 rule_id: impl Into<String>,
571 ) -> Self {
572 Self {
573 line,
574 comment_line,
575 target: Some(SuppressionTarget::PolicyRule(PolicyRuleSuppression::new(
576 pack, rule_id,
577 ))),
578 reason: None,
579 }
580 }
581
582 #[must_use]
584 pub fn with_reason(mut self, reason: Option<String>) -> Self {
585 self.reason = reason;
586 self
587 }
588
589 #[must_use]
591 pub const fn issue_kind_target(&self) -> Option<IssueKind> {
592 match &self.target {
593 Some(SuppressionTarget::Issue(kind)) => Some(*kind),
594 Some(SuppressionTarget::PolicyRule(_)) | None => None,
595 }
596 }
597
598 #[must_use]
600 pub const fn policy_rule_target(&self) -> Option<&PolicyRuleSuppression> {
601 match &self.target {
602 Some(SuppressionTarget::PolicyRule(rule)) => Some(rule),
603 Some(SuppressionTarget::Issue(_)) | None => None,
604 }
605 }
606
607 #[must_use]
609 pub fn target_token(&self) -> Option<String> {
610 self.target.as_ref().map(SuppressionTarget::token)
611 }
612
613 #[must_use]
615 pub const fn applies_to_line(&self, line: u32) -> bool {
616 self.line == 0 || self.line == line
617 }
618
619 #[must_use]
625 pub fn matches_issue_kind(&self, line: u32, kind: IssueKind) -> bool {
626 self.applies_to_line(line)
627 && match &self.target {
628 None => true,
629 Some(SuppressionTarget::Issue(target_kind)) => *target_kind == kind,
630 Some(SuppressionTarget::PolicyRule(_)) => false,
631 }
632 }
633
634 #[must_use]
636 pub fn matches_policy_rule(&self, line: u32, pack: &str, rule_id: &str) -> bool {
637 self.applies_to_line(line)
638 && match &self.target {
639 None | Some(SuppressionTarget::Issue(IssueKind::PolicyViolation)) => true,
640 Some(SuppressionTarget::Issue(_)) => false,
641 Some(SuppressionTarget::PolicyRule(target)) => {
642 target.pack == pack && target.rule_id == rule_id
643 }
644 }
645 }
646}
647
648#[must_use]
650pub fn is_suppressed(suppressions: &[Suppression], line: u32, kind: IssueKind) -> bool {
651 suppressions
652 .iter()
653 .any(|suppression| suppression.matches_issue_kind(line, kind))
654}
655
656#[must_use]
658pub fn is_file_suppressed(suppressions: &[Suppression], kind: IssueKind) -> bool {
659 suppressions
660 .iter()
661 .any(|suppression| suppression.line == 0 && suppression.matches_issue_kind(0, kind))
662}
663
664#[derive(Debug, Clone)]
673pub struct UnknownSuppressionKind {
674 pub comment_line: u32,
676 pub is_file_level: bool,
679 pub token: String,
681 pub reason: Option<String>,
683}
684
685#[must_use]
689pub fn closest_known_kind_name(input: &str) -> Option<&'static str> {
690 crate::levenshtein::closest_match(input, KNOWN_ISSUE_KIND_NAMES.iter().copied())
691}
692
693const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
694
695#[cfg(test)]
696mod tests {
697 use super::*;
698
699 #[test]
700 fn issue_kind_parse_accepts_registry_codes_and_aliases() {
701 for meta in crate::issue_meta::ISSUE_KIND_META
702 .iter()
703 .filter(|meta| meta.kind.is_some())
704 {
705 let expected = meta.kind;
706 assert_eq!(
707 IssueKind::parse(meta.code),
708 expected,
709 "canonical registry token {} must parse",
710 meta.code
711 );
712 for alias in meta.aliases {
713 assert_eq!(
714 IssueKind::parse(alias),
715 expected,
716 "registry alias {alias} must parse as {}",
717 meta.code
718 );
719 }
720 }
721 }
722
723 #[test]
724 fn issue_kind_parse_accepts_registry_suppression_tokens() {
725 for meta in crate::issue_meta::ISSUE_KIND_META {
726 let (Some(kind), Some(token)) = (meta.kind, meta.suppress_token) else {
727 continue;
728 };
729 assert_eq!(
730 IssueKind::parse(token),
731 Some(kind),
732 "registry suppression token {token} must parse as {}",
733 meta.code
734 );
735 }
736 }
737
738 #[test]
739 fn issue_kind_from_str_unknown() {
740 assert_eq!(IssueKind::parse("foo"), None);
741 assert_eq!(IssueKind::parse(""), None);
742 }
743
744 #[test]
745 fn issue_kind_from_str_near_misses() {
746 assert_eq!(IssueKind::parse("Unused-File"), None);
747 assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
748 assert_eq!(IssueKind::parse("unused_file"), None);
749 assert_eq!(IssueKind::parse("unused-files"), None);
750 }
751
752 #[test]
753 fn discriminant_out_of_range() {
754 let cases: &[(u8, IssueKind)] = &[
759 (1, IssueKind::UnusedFile),
760 (2, IssueKind::UnusedExport),
761 (3, IssueKind::UnusedType),
762 (4, IssueKind::PrivateTypeLeak),
763 (5, IssueKind::UnusedDependency),
764 (6, IssueKind::UnusedDevDependency),
765 (7, IssueKind::UnusedEnumMember),
766 (8, IssueKind::UnusedClassMember),
767 (9, IssueKind::UnresolvedImport),
768 (10, IssueKind::UnlistedDependency),
769 (11, IssueKind::DuplicateExport),
770 (12, IssueKind::CodeDuplication),
771 (13, IssueKind::CircularDependency),
772 (14, IssueKind::TypeOnlyDependency),
773 (15, IssueKind::TestOnlyDependency),
774 (16, IssueKind::BoundaryViolation),
775 (17, IssueKind::CoverageGaps),
776 (18, IssueKind::FeatureFlag),
777 (19, IssueKind::Complexity),
778 (20, IssueKind::StaleSuppression),
779 (21, IssueKind::PnpmCatalogEntry),
780 (22, IssueKind::UnresolvedCatalogReference),
781 (23, IssueKind::UnusedDependencyOverride),
782 (24, IssueKind::MisconfiguredDependencyOverride),
783 (25, IssueKind::EmptyCatalogGroup),
784 (26, IssueKind::ReExportCycle),
785 (27, IssueKind::SecurityClientServerLeak),
786 (28, IssueKind::SecuritySink),
787 (29, IssueKind::PolicyViolation),
788 (30, IssueKind::InvalidClientExport),
789 (31, IssueKind::MixedClientServerBarrel),
790 (32, IssueKind::MisplacedDirective),
791 (33, IssueKind::UnusedStoreMember),
792 (34, IssueKind::UnprovidedInject),
793 (35, IssueKind::RouteCollision),
794 (36, IssueKind::DynamicSegmentNameConflict),
795 (37, IssueKind::UnrenderedComponent),
796 (38, IssueKind::UnusedComponentProp),
797 (39, IssueKind::UnusedComponentEmit),
798 (40, IssueKind::UnusedServerAction),
799 (41, IssueKind::UnusedLoadDataKey),
800 (42, IssueKind::PropDrilling),
801 (43, IssueKind::ThinWrapper),
802 (44, IssueKind::DuplicatePropShape),
803 (45, IssueKind::UnusedComponentInput),
804 (46, IssueKind::UnusedComponentOutput),
805 (47, IssueKind::UnusedSvelteEvent),
806 (48, IssueKind::CssTokenDrift),
807 (49, IssueKind::CssDuplicateBlock),
808 (50, IssueKind::CssSelectorComplexity),
809 (51, IssueKind::CssDeadSurface),
810 (52, IssueKind::CssBrokenReference),
811 (53, IssueKind::DevDependencyInProduction),
812 (54, IssueKind::DeprecatedExportInUse),
813 (55, IssueKind::PackageCycle),
814 (56, IssueKind::AbsentComponentProp),
815 ];
816 for &(discriminant, kind) in cases {
817 assert_eq!(kind.to_discriminant(), discriminant, "{kind:?} drifted");
818 assert_eq!(IssueKind::from_discriminant(discriminant), Some(kind));
819 }
820 assert_eq!(IssueKind::from_discriminant(0), None);
821 let max_discriminant = IssueKind::ALL
822 .iter()
823 .map(|kind| kind.to_discriminant())
824 .max()
825 .expect("IssueKind::ALL should not be empty");
826 assert_eq!(IssueKind::from_discriminant(max_discriminant + 1), None);
827 assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
828 }
829
830 #[test]
831 fn discriminant_roundtrip() {
832 for &kind in IssueKind::ALL {
833 assert_eq!(
834 IssueKind::from_discriminant(kind.to_discriminant()),
835 Some(kind)
836 );
837 }
838 assert_eq!(IssueKind::from_discriminant(0), None);
839 let max_discriminant = IssueKind::ALL
840 .iter()
841 .map(|kind| kind.to_discriminant())
842 .max()
843 .expect("IssueKind::ALL should not be empty");
844 assert_eq!(IssueKind::from_discriminant(max_discriminant + 1), None);
845 }
846
847 #[test]
848 fn discriminant_values_are_unique() {
849 let discriminants: Vec<u8> = IssueKind::ALL
850 .iter()
851 .map(|kind| kind.to_discriminant())
852 .collect();
853 let mut sorted = discriminants.clone();
854 sorted.sort_unstable();
855 sorted.dedup();
856 assert_eq!(
857 discriminants.len(),
858 sorted.len(),
859 "discriminant values must be unique"
860 );
861 }
862
863 #[test]
864 fn discriminant_starts_at_one() {
865 assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
866 }
867
868 #[test]
869 fn issue_kind_to_kebab_uses_registry_suppression_token() {
870 for &kind in IssueKind::ALL {
871 let meta = crate::issue_meta::issue_meta_by_kind(kind)
872 .unwrap_or_else(|| panic!("IssueKind {kind:?} has no metadata row"));
873 let token = issue_kind_to_kebab(kind);
874 assert_eq!(token, meta.suppress_token.unwrap_or(meta.code));
875 assert_eq!(IssueKind::parse(token), Some(kind));
876 }
877 }
878
879 #[test]
880 fn suppression_line_zero_is_file_wide() {
881 let s = Suppression::all(0, 1);
882 assert_eq!(s.line, 0);
883 assert!(s.issue_kind_target().is_none());
884 }
885
886 #[test]
887 fn suppression_with_specific_kind_and_line() {
888 let s = Suppression::issue(42, 41, IssueKind::UnusedExport);
889 assert_eq!(s.line, 42);
890 assert_eq!(s.comment_line, 41);
891 assert_eq!(s.issue_kind_target(), Some(IssueKind::UnusedExport));
892 }
893
894 #[test]
895 fn suppression_predicates_match_lines_and_file_wide_markers() {
896 let suppressions = vec![
897 Suppression::issue(42, 41, IssueKind::UnusedExport),
898 Suppression::all(0, 1),
899 ];
900
901 assert!(is_suppressed(&suppressions, 42, IssueKind::UnusedExport));
902 assert!(is_suppressed(&suppressions, 10, IssueKind::UnusedType));
903 assert!(is_file_suppressed(&suppressions, IssueKind::UnusedFile));
904 }
905
906 #[test]
907 fn parses_scoped_policy_suppression_token() {
908 let target =
909 parse_policy_rule_suppression_token("policy-violation:team-policy/no-child-process")
910 .expect("scoped token should parse");
911 assert_eq!(target.pack, "team-policy");
912 assert_eq!(target.rule_id, "no-child-process");
913 assert_eq!(
914 target.token(),
915 "policy-violation:team-policy/no-child-process"
916 );
917 }
918
919 #[test]
920 fn rejects_malformed_scoped_policy_suppression_tokens() {
921 for token in [
922 "policy-violation:",
923 "policy-violation:team-policy",
924 "policy-violation:/no-child-process",
925 "policy-violation:team-policy/",
926 "policy-violation:team-policy/no/child-process",
927 "policy-violation:team policy/no-child-process",
928 "policy-violation:team-policy/no:child-process",
929 ] {
930 assert!(
931 parse_policy_rule_suppression_token(token).is_none(),
932 "{token} should be rejected"
933 );
934 }
935 }
936
937 #[test]
938 fn scoped_policy_suppression_matches_exact_policy_rule_only() {
939 let suppression = Suppression::policy_rule(7, 6, "team-policy", "no-child-process");
940 assert!(suppression.matches_policy_rule(7, "team-policy", "no-child-process"));
941 assert!(!suppression.matches_policy_rule(7, "team-policy", "no-fs"));
942 assert!(!suppression.matches_policy_rule(8, "team-policy", "no-child-process"));
943 assert!(!suppression.matches_issue_kind(7, IssueKind::PolicyViolation));
944 }
945
946 #[test]
947 fn known_issue_kind_names_parses_each_entry() {
948 for &name in KNOWN_ISSUE_KIND_NAMES.iter() {
949 assert!(
950 IssueKind::parse(name).is_some(),
951 "KNOWN_ISSUE_KIND_NAMES contains '{name}' but IssueKind::parse rejects it"
952 );
953 }
954 }
955
956 #[test]
957 fn closest_known_kind_name_finds_near_misses() {
958 assert_eq!(
959 closest_known_kind_name("unused-exports"),
960 Some("unused-export")
961 );
962 assert_eq!(closest_known_kind_name("unused-files"), Some("unused-file"));
963 assert_eq!(closest_known_kind_name("complxity"), Some("complexity"));
964 }
965
966 #[test]
967 fn closest_known_kind_name_rejects_novel_strings() {
968 assert_eq!(closest_known_kind_name("xyzzy"), None);
969 assert_eq!(closest_known_kind_name("foo"), None);
970 assert_eq!(closest_known_kind_name(""), None);
971 }
972
973 #[test]
974 fn closest_known_kind_name_skips_exact_match() {
975 assert_eq!(closest_known_kind_name("unused-export"), None);
976 }
977}