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