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)] = &[
789 (1, IssueKind::UnusedFile),
790 (2, IssueKind::UnusedExport),
791 (3, IssueKind::UnusedType),
792 (4, IssueKind::PrivateTypeLeak),
793 (5, IssueKind::UnusedDependency),
794 (6, IssueKind::UnusedDevDependency),
795 (7, IssueKind::UnusedEnumMember),
796 (8, IssueKind::UnusedClassMember),
797 (9, IssueKind::UnresolvedImport),
798 (10, IssueKind::UnlistedDependency),
799 (11, IssueKind::DuplicateExport),
800 (12, IssueKind::CodeDuplication),
801 (13, IssueKind::CircularDependency),
802 (14, IssueKind::TypeOnlyDependency),
803 (15, IssueKind::TestOnlyDependency),
804 (16, IssueKind::BoundaryViolation),
805 (17, IssueKind::CoverageGaps),
806 (18, IssueKind::FeatureFlag),
807 (19, IssueKind::Complexity),
808 (20, IssueKind::StaleSuppression),
809 (21, IssueKind::PnpmCatalogEntry),
810 (22, IssueKind::UnresolvedCatalogReference),
811 (23, IssueKind::UnusedDependencyOverride),
812 (24, IssueKind::MisconfiguredDependencyOverride),
813 (25, IssueKind::EmptyCatalogGroup),
814 (26, IssueKind::ReExportCycle),
815 (27, IssueKind::SecurityClientServerLeak),
816 (28, IssueKind::SecuritySink),
817 (29, IssueKind::PolicyViolation),
818 (30, IssueKind::InvalidClientExport),
819 (31, IssueKind::MixedClientServerBarrel),
820 (32, IssueKind::MisplacedDirective),
821 (33, IssueKind::UnusedStoreMember),
822 (34, IssueKind::UnprovidedInject),
823 (35, IssueKind::RouteCollision),
824 (36, IssueKind::DynamicSegmentNameConflict),
825 (37, IssueKind::UnrenderedComponent),
826 (38, IssueKind::UnusedComponentProp),
827 (39, IssueKind::UnusedComponentEmit),
828 (40, IssueKind::UnusedServerAction),
829 (41, IssueKind::UnusedLoadDataKey),
830 (42, IssueKind::PropDrilling),
831 (43, IssueKind::ThinWrapper),
832 (44, IssueKind::DuplicatePropShape),
833 (45, IssueKind::UnusedComponentInput),
834 (46, IssueKind::UnusedComponentOutput),
835 (47, IssueKind::UnusedSvelteEvent),
836 (48, IssueKind::CssTokenDrift),
837 (49, IssueKind::CssDuplicateBlock),
838 (50, IssueKind::CssSelectorComplexity),
839 (51, IssueKind::CssDeadSurface),
840 (52, IssueKind::CssBrokenReference),
841 (53, IssueKind::DevDependencyInProduction),
842 ];
843 for &(discriminant, kind) in cases {
844 assert_eq!(kind.to_discriminant(), discriminant, "{kind:?} drifted");
845 assert_eq!(IssueKind::from_discriminant(discriminant), Some(kind));
846 }
847 assert_eq!(IssueKind::from_discriminant(0), None);
848 let max_discriminant = IssueKind::ALL
849 .iter()
850 .map(|kind| kind.to_discriminant())
851 .max()
852 .expect("IssueKind::ALL should not be empty");
853 assert_eq!(IssueKind::from_discriminant(max_discriminant + 1), None);
854 assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
855 }
856
857 #[test]
858 fn discriminant_roundtrip() {
859 for &kind in IssueKind::ALL {
860 assert_eq!(
861 IssueKind::from_discriminant(kind.to_discriminant()),
862 Some(kind)
863 );
864 }
865 assert_eq!(IssueKind::from_discriminant(0), None);
866 let max_discriminant = IssueKind::ALL
867 .iter()
868 .map(|kind| kind.to_discriminant())
869 .max()
870 .expect("IssueKind::ALL should not be empty");
871 assert_eq!(IssueKind::from_discriminant(max_discriminant + 1), None);
872 }
873
874 #[test]
875 fn discriminant_values_are_unique() {
876 let discriminants: Vec<u8> = IssueKind::ALL
877 .iter()
878 .map(|kind| kind.to_discriminant())
879 .collect();
880 let mut sorted = discriminants.clone();
881 sorted.sort_unstable();
882 sorted.dedup();
883 assert_eq!(
884 discriminants.len(),
885 sorted.len(),
886 "discriminant values must be unique"
887 );
888 }
889
890 #[test]
891 fn discriminant_starts_at_one() {
892 assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
893 }
894
895 #[test]
896 fn issue_kind_to_kebab_uses_registry_suppression_token() {
897 for &kind in IssueKind::ALL {
898 let meta = crate::issue_meta::issue_meta_by_kind(kind)
899 .unwrap_or_else(|| panic!("IssueKind {kind:?} has no metadata row"));
900 let token = issue_kind_to_kebab(kind);
901 assert_eq!(token, meta.suppress_token.unwrap_or(meta.code));
902 assert_eq!(IssueKind::parse(token), Some(kind));
903 }
904 }
905
906 #[test]
907 fn suppression_line_zero_is_file_wide() {
908 let s = Suppression::all(0, 1);
909 assert_eq!(s.line, 0);
910 assert!(s.issue_kind_target().is_none());
911 }
912
913 #[test]
914 fn suppression_with_specific_kind_and_line() {
915 let s = Suppression::issue(42, 41, IssueKind::UnusedExport);
916 assert_eq!(s.line, 42);
917 assert_eq!(s.comment_line, 41);
918 assert_eq!(s.issue_kind_target(), Some(IssueKind::UnusedExport));
919 }
920
921 #[test]
922 fn suppression_predicates_match_lines_and_file_wide_markers() {
923 let suppressions = vec![
924 Suppression::issue(42, 41, IssueKind::UnusedExport),
925 Suppression::all(0, 1),
926 ];
927
928 assert!(is_suppressed(&suppressions, 42, IssueKind::UnusedExport));
929 assert!(is_suppressed(&suppressions, 10, IssueKind::UnusedType));
930 assert!(is_file_suppressed(&suppressions, IssueKind::UnusedFile));
931 }
932
933 #[test]
934 fn parses_scoped_policy_suppression_token() {
935 let target =
936 parse_policy_rule_suppression_token("policy-violation:team-policy/no-child-process")
937 .expect("scoped token should parse");
938 assert_eq!(target.pack, "team-policy");
939 assert_eq!(target.rule_id, "no-child-process");
940 assert_eq!(
941 target.token(),
942 "policy-violation:team-policy/no-child-process"
943 );
944 }
945
946 #[test]
947 fn rejects_malformed_scoped_policy_suppression_tokens() {
948 for token in [
949 "policy-violation:",
950 "policy-violation:team-policy",
951 "policy-violation:/no-child-process",
952 "policy-violation:team-policy/",
953 "policy-violation:team-policy/no/child-process",
954 "policy-violation:team policy/no-child-process",
955 "policy-violation:team-policy/no:child-process",
956 ] {
957 assert!(
958 parse_policy_rule_suppression_token(token).is_none(),
959 "{token} should be rejected"
960 );
961 }
962 }
963
964 #[test]
965 fn scoped_policy_suppression_matches_exact_policy_rule_only() {
966 let suppression = Suppression::policy_rule(7, 6, "team-policy", "no-child-process");
967 assert!(suppression.matches_policy_rule(7, "team-policy", "no-child-process"));
968 assert!(!suppression.matches_policy_rule(7, "team-policy", "no-fs"));
969 assert!(!suppression.matches_policy_rule(8, "team-policy", "no-child-process"));
970 assert!(!suppression.matches_issue_kind(7, IssueKind::PolicyViolation));
971 }
972
973 #[test]
974 fn known_issue_kind_names_parses_each_entry() {
975 for &name in KNOWN_ISSUE_KIND_NAMES.iter() {
976 assert!(
977 IssueKind::parse(name).is_some(),
978 "KNOWN_ISSUE_KIND_NAMES contains '{name}' but IssueKind::parse rejects it"
979 );
980 }
981 }
982
983 #[test]
984 fn closest_known_kind_name_finds_near_misses() {
985 assert_eq!(
986 closest_known_kind_name("unused-exports"),
987 Some("unused-export")
988 );
989 assert_eq!(closest_known_kind_name("unused-files"), Some("unused-file"));
990 assert_eq!(closest_known_kind_name("complxity"), Some("complexity"));
991 }
992
993 #[test]
994 fn closest_known_kind_name_rejects_novel_strings() {
995 assert_eq!(closest_known_kind_name("xyzzy"), None);
996 assert_eq!(closest_known_kind_name("foo"), None);
997 assert_eq!(closest_known_kind_name(""), None);
998 }
999
1000 #[test]
1001 fn closest_known_kind_name_skips_exact_match() {
1002 assert_eq!(closest_known_kind_name("unused-export"), None);
1003 }
1004}