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