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 UnusedEffectLayer,
215 UnusedTaggedError,
218 UnproducedCatchTag,
221 WeakenedTests,
225 AddedSuppression,
228 TestOnlyBranch,
232 DivergedCloneSibling,
235 AddedDependency,
238}
239
240impl IssueKind {
241 pub const ALL: &'static [Self] = &[
243 Self::UnusedFile,
244 Self::UnusedExport,
245 Self::UnusedType,
246 Self::PrivateTypeLeak,
247 Self::UnusedDependency,
248 Self::UnusedDevDependency,
249 Self::UnusedEnumMember,
250 Self::UnusedClassMember,
251 Self::UnresolvedImport,
252 Self::UnlistedDependency,
253 Self::DuplicateExport,
254 Self::CodeDuplication,
255 Self::CircularDependency,
256 Self::ReExportCycle,
257 Self::TypeOnlyDependency,
258 Self::TestOnlyDependency,
259 Self::BoundaryViolation,
260 Self::CoverageGaps,
261 Self::FeatureFlag,
262 Self::Complexity,
263 Self::StaleSuppression,
264 Self::PnpmCatalogEntry,
265 Self::EmptyCatalogGroup,
266 Self::UnresolvedCatalogReference,
267 Self::UnusedDependencyOverride,
268 Self::MisconfiguredDependencyOverride,
269 Self::SecurityClientServerLeak,
270 Self::SecuritySink,
271 Self::PolicyViolation,
272 Self::InvalidClientExport,
273 Self::MixedClientServerBarrel,
274 Self::MisplacedDirective,
275 Self::UnusedStoreMember,
276 Self::UnprovidedInject,
277 Self::RouteCollision,
278 Self::DynamicSegmentNameConflict,
279 Self::UnrenderedComponent,
280 Self::UnusedComponentProp,
281 Self::AbsentComponentProp,
282 Self::UnusedComponentEmit,
283 Self::UnusedComponentInput,
284 Self::UnusedComponentOutput,
285 Self::UnusedServerAction,
286 Self::UnusedLoadDataKey,
287 Self::PropDrilling,
288 Self::ThinWrapper,
289 Self::DuplicatePropShape,
290 Self::UnusedSvelteEvent,
291 Self::CssTokenDrift,
292 Self::CssDuplicateBlock,
293 Self::CssSelectorComplexity,
294 Self::CssDeadSurface,
295 Self::CssBrokenReference,
296 Self::DevDependencyInProduction,
297 Self::DeprecatedExportInUse,
298 Self::PackageCycle,
299 Self::UnusedEffectLayer,
300 Self::UnusedTaggedError,
301 Self::UnproducedCatchTag,
302 Self::WeakenedTests,
303 Self::AddedSuppression,
304 Self::TestOnlyBranch,
305 Self::DivergedCloneSibling,
306 Self::AddedDependency,
307 ];
308
309 #[must_use]
311 pub fn parse(s: &str) -> Option<Self> {
312 crate::issue_meta::issue_meta_for_token(s).and_then(|meta| meta.kind)
313 }
314
315 #[must_use]
317 pub const fn to_discriminant(self) -> u8 {
318 match self {
319 Self::UnusedFile => 1,
320 Self::UnusedExport => 2,
321 Self::UnusedType => 3,
322 Self::PrivateTypeLeak => 4,
323 Self::UnusedDependency => 5,
324 Self::UnusedDevDependency => 6,
325 Self::UnusedEnumMember => 7,
326 Self::UnusedClassMember => 8,
327 Self::UnresolvedImport => 9,
328 Self::UnlistedDependency => 10,
329 Self::DuplicateExport => 11,
330 Self::CodeDuplication => 12,
331 Self::CircularDependency => 13,
332 Self::TypeOnlyDependency => 14,
333 Self::TestOnlyDependency => 15,
334 Self::BoundaryViolation => 16,
335 Self::CoverageGaps => 17,
336 Self::FeatureFlag => 18,
337 Self::Complexity => 19,
338 Self::StaleSuppression => 20,
339 Self::PnpmCatalogEntry => 21,
340 Self::UnresolvedCatalogReference => 22,
341 Self::UnusedDependencyOverride => 23,
342 Self::MisconfiguredDependencyOverride => 24,
343 Self::EmptyCatalogGroup => 25,
344 Self::ReExportCycle => 26,
345 Self::SecurityClientServerLeak => 27,
346 Self::SecuritySink => 28,
347 Self::PolicyViolation => 29,
348 Self::InvalidClientExport => 30,
349 Self::MixedClientServerBarrel => 31,
350 Self::MisplacedDirective => 32,
351 Self::UnusedStoreMember => 33,
352 Self::UnprovidedInject => 34,
353 Self::RouteCollision => 35,
354 Self::DynamicSegmentNameConflict => 36,
355 Self::UnrenderedComponent => 37,
356 Self::UnusedComponentProp => 38,
357 Self::UnusedComponentEmit => 39,
358 Self::UnusedServerAction => 40,
359 Self::UnusedLoadDataKey => 41,
360 Self::PropDrilling => 42,
361 Self::ThinWrapper => 43,
362 Self::DuplicatePropShape => 44,
363 Self::UnusedComponentInput => 45,
364 Self::UnusedComponentOutput => 46,
365 Self::UnusedSvelteEvent => 47,
366 Self::CssTokenDrift => 48,
367 Self::CssDuplicateBlock => 49,
368 Self::CssSelectorComplexity => 50,
369 Self::CssDeadSurface => 51,
370 Self::CssBrokenReference => 52,
371 Self::DevDependencyInProduction => 53,
372 Self::DeprecatedExportInUse => 54,
373 Self::PackageCycle => 55,
374 Self::AbsentComponentProp => 56,
375 Self::UnusedEffectLayer => 57,
376 Self::UnusedTaggedError => 58,
377 Self::UnproducedCatchTag => 59,
378 Self::WeakenedTests => 60,
379 Self::AddedSuppression => 61,
380 Self::TestOnlyBranch => 62,
381 Self::DivergedCloneSibling => 63,
382 Self::AddedDependency => 64,
383 }
384 }
385
386 #[must_use]
388 pub const fn from_discriminant(d: u8) -> Option<Self> {
389 match d {
390 1 => Some(Self::UnusedFile),
391 2 => Some(Self::UnusedExport),
392 3 => Some(Self::UnusedType),
393 4 => Some(Self::PrivateTypeLeak),
394 5 => Some(Self::UnusedDependency),
395 6 => Some(Self::UnusedDevDependency),
396 7 => Some(Self::UnusedEnumMember),
397 8 => Some(Self::UnusedClassMember),
398 9 => Some(Self::UnresolvedImport),
399 10 => Some(Self::UnlistedDependency),
400 11 => Some(Self::DuplicateExport),
401 12 => Some(Self::CodeDuplication),
402 13 => Some(Self::CircularDependency),
403 14 => Some(Self::TypeOnlyDependency),
404 15 => Some(Self::TestOnlyDependency),
405 16 => Some(Self::BoundaryViolation),
406 17 => Some(Self::CoverageGaps),
407 18 => Some(Self::FeatureFlag),
408 19 => Some(Self::Complexity),
409 20 => Some(Self::StaleSuppression),
410 21 => Some(Self::PnpmCatalogEntry),
411 22 => Some(Self::UnresolvedCatalogReference),
412 23 => Some(Self::UnusedDependencyOverride),
413 24 => Some(Self::MisconfiguredDependencyOverride),
414 25 => Some(Self::EmptyCatalogGroup),
415 26 => Some(Self::ReExportCycle),
416 27 => Some(Self::SecurityClientServerLeak),
417 28 => Some(Self::SecuritySink),
418 29 => Some(Self::PolicyViolation),
419 30 => Some(Self::InvalidClientExport),
420 31 => Some(Self::MixedClientServerBarrel),
421 32 => Some(Self::MisplacedDirective),
422 33 => Some(Self::UnusedStoreMember),
423 34 => Some(Self::UnprovidedInject),
424 35 => Some(Self::RouteCollision),
425 36 => Some(Self::DynamicSegmentNameConflict),
426 37 => Some(Self::UnrenderedComponent),
427 38 => Some(Self::UnusedComponentProp),
428 39 => Some(Self::UnusedComponentEmit),
429 40 => Some(Self::UnusedServerAction),
430 41 => Some(Self::UnusedLoadDataKey),
431 42 => Some(Self::PropDrilling),
432 43 => Some(Self::ThinWrapper),
433 44 => Some(Self::DuplicatePropShape),
434 45 => Some(Self::UnusedComponentInput),
435 46 => Some(Self::UnusedComponentOutput),
436 47 => Some(Self::UnusedSvelteEvent),
437 48 => Some(Self::CssTokenDrift),
438 49 => Some(Self::CssDuplicateBlock),
439 50 => Some(Self::CssSelectorComplexity),
440 51 => Some(Self::CssDeadSurface),
441 52 => Some(Self::CssBrokenReference),
442 53 => Some(Self::DevDependencyInProduction),
443 54 => Some(Self::DeprecatedExportInUse),
444 55 => Some(Self::PackageCycle),
445 56 => Some(Self::AbsentComponentProp),
446 57 => Some(Self::UnusedEffectLayer),
447 58 => Some(Self::UnusedTaggedError),
448 59 => Some(Self::UnproducedCatchTag),
449 60 => Some(Self::WeakenedTests),
450 61 => Some(Self::AddedSuppression),
451 62 => Some(Self::TestOnlyBranch),
452 63 => Some(Self::DivergedCloneSibling),
453 64 => Some(Self::AddedDependency),
454 _ => None,
455 }
456 }
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, Hash)]
461pub struct PolicyRuleSuppression {
462 pub pack: String,
464 pub rule_id: String,
466}
467
468impl PolicyRuleSuppression {
469 #[must_use]
471 pub fn new(pack: impl Into<String>, rule_id: impl Into<String>) -> Self {
472 Self {
473 pack: pack.into(),
474 rule_id: rule_id.into(),
475 }
476 }
477
478 #[must_use]
480 pub fn token(&self) -> String {
481 format!("policy-violation:{}/{}", self.pack, self.rule_id)
482 }
483}
484
485#[derive(Debug, Clone, PartialEq, Eq)]
487pub enum SuppressionTarget {
488 Issue(IssueKind),
491 PolicyRule(PolicyRuleSuppression),
494}
495
496impl SuppressionTarget {
497 #[must_use]
500 pub const fn issue_kind(&self) -> Option<IssueKind> {
501 match self {
502 Self::Issue(kind) => Some(*kind),
503 Self::PolicyRule(_) => None,
504 }
505 }
506
507 #[must_use]
509 pub fn token(&self) -> String {
510 match self {
511 Self::Issue(kind) => issue_kind_to_kebab(*kind).to_owned(),
512 Self::PolicyRule(rule) => rule.token(),
513 }
514 }
515}
516
517#[must_use]
519pub fn issue_kind_to_kebab(kind: IssueKind) -> &'static str {
520 let Some(meta) = crate::issue_meta::issue_meta_by_kind(kind) else {
521 unreachable!("IssueKind {kind:?} has no metadata row");
522 };
523 meta.suppress_token.unwrap_or(meta.code)
524}
525
526#[must_use]
528pub fn parse_suppression_target(token: &str) -> Option<SuppressionTarget> {
529 parse_policy_rule_suppression_token(token)
530 .map(SuppressionTarget::PolicyRule)
531 .or_else(|| IssueKind::parse(token).map(SuppressionTarget::Issue))
532}
533
534#[must_use]
539pub fn parse_policy_rule_suppression_token(token: &str) -> Option<PolicyRuleSuppression> {
540 let identity = token
541 .strip_prefix("policy-violation:")
542 .or_else(|| token.strip_prefix("policy-violations:"))?;
543 let (pack, rule_id) = identity.split_once('/')?;
544 if rule_id.contains('/') {
545 return None;
546 }
547 if !is_valid_policy_identifier(pack) || !is_valid_policy_identifier(rule_id) {
548 return None;
549 }
550 Some(PolicyRuleSuppression::new(pack, rule_id))
551}
552
553#[must_use]
556pub fn is_valid_policy_identifier(value: &str) -> bool {
557 !value.is_empty()
558 && value
559 .bytes()
560 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
561}
562
563#[derive(Debug, Clone)]
579pub struct Suppression {
580 pub line: u32,
582 pub comment_line: u32,
586 pub target: Option<SuppressionTarget>,
588 pub reason: Option<String>,
590}
591
592impl Suppression {
593 #[must_use]
595 pub const fn all(line: u32, comment_line: u32) -> Self {
596 Self {
597 line,
598 comment_line,
599 target: None,
600 reason: None,
601 }
602 }
603
604 #[must_use]
606 pub const fn issue(line: u32, comment_line: u32, kind: IssueKind) -> Self {
607 Self {
608 line,
609 comment_line,
610 target: Some(SuppressionTarget::Issue(kind)),
611 reason: None,
612 }
613 }
614
615 #[must_use]
617 pub fn policy_rule(
618 line: u32,
619 comment_line: u32,
620 pack: impl Into<String>,
621 rule_id: impl Into<String>,
622 ) -> Self {
623 Self {
624 line,
625 comment_line,
626 target: Some(SuppressionTarget::PolicyRule(PolicyRuleSuppression::new(
627 pack, rule_id,
628 ))),
629 reason: None,
630 }
631 }
632
633 #[must_use]
635 pub fn with_reason(mut self, reason: Option<String>) -> Self {
636 self.reason = reason;
637 self
638 }
639
640 #[must_use]
642 pub const fn issue_kind_target(&self) -> Option<IssueKind> {
643 match &self.target {
644 Some(SuppressionTarget::Issue(kind)) => Some(*kind),
645 Some(SuppressionTarget::PolicyRule(_)) | None => None,
646 }
647 }
648
649 #[must_use]
651 pub const fn policy_rule_target(&self) -> Option<&PolicyRuleSuppression> {
652 match &self.target {
653 Some(SuppressionTarget::PolicyRule(rule)) => Some(rule),
654 Some(SuppressionTarget::Issue(_)) | None => None,
655 }
656 }
657
658 #[must_use]
660 pub fn target_token(&self) -> Option<String> {
661 self.target.as_ref().map(SuppressionTarget::token)
662 }
663
664 #[must_use]
666 pub const fn applies_to_line(&self, line: u32) -> bool {
667 self.line == 0 || self.line == line
668 }
669
670 #[must_use]
676 pub fn matches_issue_kind(&self, line: u32, kind: IssueKind) -> bool {
677 self.applies_to_line(line)
678 && match &self.target {
679 None => true,
680 Some(SuppressionTarget::Issue(target_kind)) => *target_kind == kind,
681 Some(SuppressionTarget::PolicyRule(_)) => false,
682 }
683 }
684
685 #[must_use]
687 pub fn matches_policy_rule(&self, line: u32, pack: &str, rule_id: &str) -> bool {
688 self.applies_to_line(line)
689 && match &self.target {
690 None | Some(SuppressionTarget::Issue(IssueKind::PolicyViolation)) => true,
691 Some(SuppressionTarget::Issue(_)) => false,
692 Some(SuppressionTarget::PolicyRule(target)) => {
693 target.pack == pack && target.rule_id == rule_id
694 }
695 }
696 }
697}
698
699#[must_use]
701pub fn is_suppressed(suppressions: &[Suppression], line: u32, kind: IssueKind) -> bool {
702 suppressions
703 .iter()
704 .any(|suppression| suppression.matches_issue_kind(line, kind))
705}
706
707#[must_use]
709pub fn is_file_suppressed(suppressions: &[Suppression], kind: IssueKind) -> bool {
710 suppressions
711 .iter()
712 .any(|suppression| suppression.line == 0 && suppression.matches_issue_kind(0, kind))
713}
714
715#[derive(Debug, Clone)]
724pub struct UnknownSuppressionKind {
725 pub comment_line: u32,
727 pub is_file_level: bool,
730 pub token: String,
732 pub reason: Option<String>,
734}
735
736#[must_use]
740pub fn closest_known_kind_name(input: &str) -> Option<&'static str> {
741 crate::levenshtein::closest_match(input, KNOWN_ISSUE_KIND_NAMES.iter().copied())
742}
743
744const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
745
746#[cfg(test)]
747mod tests {
748 use super::*;
749
750 #[test]
751 fn issue_kind_parse_accepts_registry_codes_and_aliases() {
752 for meta in crate::issue_meta::ISSUE_KIND_META
753 .iter()
754 .filter(|meta| meta.kind.is_some())
755 {
756 let expected = meta.kind;
757 assert_eq!(
758 IssueKind::parse(meta.code),
759 expected,
760 "canonical registry token {} must parse",
761 meta.code
762 );
763 for alias in meta.aliases {
764 assert_eq!(
765 IssueKind::parse(alias),
766 expected,
767 "registry alias {alias} must parse as {}",
768 meta.code
769 );
770 }
771 }
772 }
773
774 #[test]
775 fn issue_kind_parse_accepts_registry_suppression_tokens() {
776 for meta in crate::issue_meta::ISSUE_KIND_META {
777 let (Some(kind), Some(token)) = (meta.kind, meta.suppress_token) else {
778 continue;
779 };
780 assert_eq!(
781 IssueKind::parse(token),
782 Some(kind),
783 "registry suppression token {token} must parse as {}",
784 meta.code
785 );
786 }
787 }
788
789 #[test]
790 fn issue_kind_parse_accepts_diff_risk_tokens() {
791 let cases = [
792 ("weakened-tests", IssueKind::WeakenedTests),
793 ("added-suppression", IssueKind::AddedSuppression),
794 ("test-only-branch", IssueKind::TestOnlyBranch),
795 ("diverged-clone-sibling", IssueKind::DivergedCloneSibling),
796 ("added-dependency", IssueKind::AddedDependency),
797 ];
798 for (token, kind) in cases {
799 assert_eq!(IssueKind::parse(token), Some(kind), "{token}");
800 assert_eq!(issue_kind_to_kebab(kind), token);
801 }
802 }
803
804 #[test]
805 fn issue_kind_from_str_unknown() {
806 assert_eq!(IssueKind::parse("foo"), None);
807 assert_eq!(IssueKind::parse(""), None);
808 }
809
810 #[test]
811 fn issue_kind_from_str_near_misses() {
812 assert_eq!(IssueKind::parse("Unused-File"), None);
813 assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
814 assert_eq!(IssueKind::parse("unused_file"), None);
815 assert_eq!(IssueKind::parse("unused-files"), None);
816 }
817
818 #[test]
819 fn discriminant_out_of_range() {
820 let cases: &[(u8, IssueKind)] = &[
825 (1, IssueKind::UnusedFile),
826 (2, IssueKind::UnusedExport),
827 (3, IssueKind::UnusedType),
828 (4, IssueKind::PrivateTypeLeak),
829 (5, IssueKind::UnusedDependency),
830 (6, IssueKind::UnusedDevDependency),
831 (7, IssueKind::UnusedEnumMember),
832 (8, IssueKind::UnusedClassMember),
833 (9, IssueKind::UnresolvedImport),
834 (10, IssueKind::UnlistedDependency),
835 (11, IssueKind::DuplicateExport),
836 (12, IssueKind::CodeDuplication),
837 (13, IssueKind::CircularDependency),
838 (14, IssueKind::TypeOnlyDependency),
839 (15, IssueKind::TestOnlyDependency),
840 (16, IssueKind::BoundaryViolation),
841 (17, IssueKind::CoverageGaps),
842 (18, IssueKind::FeatureFlag),
843 (19, IssueKind::Complexity),
844 (20, IssueKind::StaleSuppression),
845 (21, IssueKind::PnpmCatalogEntry),
846 (22, IssueKind::UnresolvedCatalogReference),
847 (23, IssueKind::UnusedDependencyOverride),
848 (24, IssueKind::MisconfiguredDependencyOverride),
849 (25, IssueKind::EmptyCatalogGroup),
850 (26, IssueKind::ReExportCycle),
851 (27, IssueKind::SecurityClientServerLeak),
852 (28, IssueKind::SecuritySink),
853 (29, IssueKind::PolicyViolation),
854 (30, IssueKind::InvalidClientExport),
855 (31, IssueKind::MixedClientServerBarrel),
856 (32, IssueKind::MisplacedDirective),
857 (33, IssueKind::UnusedStoreMember),
858 (34, IssueKind::UnprovidedInject),
859 (35, IssueKind::RouteCollision),
860 (36, IssueKind::DynamicSegmentNameConflict),
861 (37, IssueKind::UnrenderedComponent),
862 (38, IssueKind::UnusedComponentProp),
863 (39, IssueKind::UnusedComponentEmit),
864 (40, IssueKind::UnusedServerAction),
865 (41, IssueKind::UnusedLoadDataKey),
866 (42, IssueKind::PropDrilling),
867 (43, IssueKind::ThinWrapper),
868 (44, IssueKind::DuplicatePropShape),
869 (45, IssueKind::UnusedComponentInput),
870 (46, IssueKind::UnusedComponentOutput),
871 (47, IssueKind::UnusedSvelteEvent),
872 (48, IssueKind::CssTokenDrift),
873 (49, IssueKind::CssDuplicateBlock),
874 (50, IssueKind::CssSelectorComplexity),
875 (51, IssueKind::CssDeadSurface),
876 (52, IssueKind::CssBrokenReference),
877 (53, IssueKind::DevDependencyInProduction),
878 (54, IssueKind::DeprecatedExportInUse),
879 (55, IssueKind::PackageCycle),
880 (56, IssueKind::AbsentComponentProp),
881 (57, IssueKind::UnusedEffectLayer),
882 (58, IssueKind::UnusedTaggedError),
883 (59, IssueKind::UnproducedCatchTag),
884 (60, IssueKind::WeakenedTests),
885 (61, IssueKind::AddedSuppression),
886 (62, IssueKind::TestOnlyBranch),
887 (63, IssueKind::DivergedCloneSibling),
888 (64, IssueKind::AddedDependency),
889 ];
890 for &(discriminant, kind) in cases {
891 assert_eq!(kind.to_discriminant(), discriminant, "{kind:?} drifted");
892 assert_eq!(IssueKind::from_discriminant(discriminant), Some(kind));
893 }
894 assert_eq!(IssueKind::from_discriminant(0), None);
895 let max_discriminant = IssueKind::ALL
896 .iter()
897 .map(|kind| kind.to_discriminant())
898 .max()
899 .expect("IssueKind::ALL should not be empty");
900 assert_eq!(IssueKind::from_discriminant(max_discriminant + 1), None);
901 assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
902 }
903
904 #[test]
905 fn discriminant_roundtrip() {
906 for &kind in IssueKind::ALL {
907 assert_eq!(
908 IssueKind::from_discriminant(kind.to_discriminant()),
909 Some(kind)
910 );
911 }
912 assert_eq!(IssueKind::from_discriminant(0), None);
913 let max_discriminant = IssueKind::ALL
914 .iter()
915 .map(|kind| kind.to_discriminant())
916 .max()
917 .expect("IssueKind::ALL should not be empty");
918 assert_eq!(IssueKind::from_discriminant(max_discriminant + 1), None);
919 }
920
921 #[test]
922 fn discriminant_values_are_unique() {
923 let discriminants: Vec<u8> = IssueKind::ALL
924 .iter()
925 .map(|kind| kind.to_discriminant())
926 .collect();
927 let mut sorted = discriminants.clone();
928 sorted.sort_unstable();
929 sorted.dedup();
930 assert_eq!(
931 discriminants.len(),
932 sorted.len(),
933 "discriminant values must be unique"
934 );
935 }
936
937 #[test]
938 fn discriminant_starts_at_one() {
939 assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
940 }
941
942 #[test]
943 fn issue_kind_to_kebab_uses_registry_suppression_token() {
944 for &kind in IssueKind::ALL {
945 let meta = crate::issue_meta::issue_meta_by_kind(kind)
946 .unwrap_or_else(|| panic!("IssueKind {kind:?} has no metadata row"));
947 let token = issue_kind_to_kebab(kind);
948 assert_eq!(token, meta.suppress_token.unwrap_or(meta.code));
949 assert_eq!(IssueKind::parse(token), Some(kind));
950 }
951 }
952
953 #[test]
954 fn suppression_line_zero_is_file_wide() {
955 let s = Suppression::all(0, 1);
956 assert_eq!(s.line, 0);
957 assert!(s.issue_kind_target().is_none());
958 }
959
960 #[test]
961 fn suppression_with_specific_kind_and_line() {
962 let s = Suppression::issue(42, 41, IssueKind::UnusedExport);
963 assert_eq!(s.line, 42);
964 assert_eq!(s.comment_line, 41);
965 assert_eq!(s.issue_kind_target(), Some(IssueKind::UnusedExport));
966 }
967
968 #[test]
969 fn suppression_predicates_match_lines_and_file_wide_markers() {
970 let suppressions = vec![
971 Suppression::issue(42, 41, IssueKind::UnusedExport),
972 Suppression::all(0, 1),
973 ];
974
975 assert!(is_suppressed(&suppressions, 42, IssueKind::UnusedExport));
976 assert!(is_suppressed(&suppressions, 10, IssueKind::UnusedType));
977 assert!(is_file_suppressed(&suppressions, IssueKind::UnusedFile));
978 }
979
980 #[test]
981 fn parses_scoped_policy_suppression_token() {
982 let target =
983 parse_policy_rule_suppression_token("policy-violation:team-policy/no-child-process")
984 .expect("scoped token should parse");
985 assert_eq!(target.pack, "team-policy");
986 assert_eq!(target.rule_id, "no-child-process");
987 assert_eq!(
988 target.token(),
989 "policy-violation:team-policy/no-child-process"
990 );
991 }
992
993 #[test]
994 fn rejects_malformed_scoped_policy_suppression_tokens() {
995 for token in [
996 "policy-violation:",
997 "policy-violation:team-policy",
998 "policy-violation:/no-child-process",
999 "policy-violation:team-policy/",
1000 "policy-violation:team-policy/no/child-process",
1001 "policy-violation:team policy/no-child-process",
1002 "policy-violation:team-policy/no:child-process",
1003 ] {
1004 assert!(
1005 parse_policy_rule_suppression_token(token).is_none(),
1006 "{token} should be rejected"
1007 );
1008 }
1009 }
1010
1011 #[test]
1012 fn scoped_policy_suppression_matches_exact_policy_rule_only() {
1013 let suppression = Suppression::policy_rule(7, 6, "team-policy", "no-child-process");
1014 assert!(suppression.matches_policy_rule(7, "team-policy", "no-child-process"));
1015 assert!(!suppression.matches_policy_rule(7, "team-policy", "no-fs"));
1016 assert!(!suppression.matches_policy_rule(8, "team-policy", "no-child-process"));
1017 assert!(!suppression.matches_issue_kind(7, IssueKind::PolicyViolation));
1018 }
1019
1020 #[test]
1021 fn known_issue_kind_names_parses_each_entry() {
1022 for &name in KNOWN_ISSUE_KIND_NAMES.iter() {
1023 assert!(
1024 IssueKind::parse(name).is_some(),
1025 "KNOWN_ISSUE_KIND_NAMES contains '{name}' but IssueKind::parse rejects it"
1026 );
1027 }
1028 }
1029
1030 #[test]
1031 fn closest_known_kind_name_finds_near_misses() {
1032 assert_eq!(
1033 closest_known_kind_name("unused-exports"),
1034 Some("unused-export")
1035 );
1036 assert_eq!(closest_known_kind_name("unused-files"), Some("unused-file"));
1037 assert_eq!(closest_known_kind_name("complxity"), Some("complexity"));
1038 }
1039
1040 #[test]
1041 fn closest_known_kind_name_rejects_novel_strings() {
1042 assert_eq!(closest_known_kind_name("xyzzy"), None);
1043 assert_eq!(closest_known_kind_name("foo"), None);
1044 assert_eq!(closest_known_kind_name(""), None);
1045 }
1046
1047 #[test]
1048 fn closest_known_kind_name_skips_exact_match() {
1049 assert_eq!(closest_known_kind_name("unused-export"), None);
1050 }
1051}