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