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}
173
174impl IssueKind {
175 pub const ALL: &'static [Self] = &[
177 Self::UnusedFile,
178 Self::UnusedExport,
179 Self::UnusedType,
180 Self::PrivateTypeLeak,
181 Self::UnusedDependency,
182 Self::UnusedDevDependency,
183 Self::UnusedEnumMember,
184 Self::UnusedClassMember,
185 Self::UnresolvedImport,
186 Self::UnlistedDependency,
187 Self::DuplicateExport,
188 Self::CodeDuplication,
189 Self::CircularDependency,
190 Self::ReExportCycle,
191 Self::TypeOnlyDependency,
192 Self::TestOnlyDependency,
193 Self::BoundaryViolation,
194 Self::CoverageGaps,
195 Self::FeatureFlag,
196 Self::Complexity,
197 Self::StaleSuppression,
198 Self::PnpmCatalogEntry,
199 Self::EmptyCatalogGroup,
200 Self::UnresolvedCatalogReference,
201 Self::UnusedDependencyOverride,
202 Self::MisconfiguredDependencyOverride,
203 Self::SecurityClientServerLeak,
204 Self::SecuritySink,
205 Self::PolicyViolation,
206 Self::InvalidClientExport,
207 Self::MixedClientServerBarrel,
208 Self::MisplacedDirective,
209 Self::UnusedStoreMember,
210 Self::UnprovidedInject,
211 Self::RouteCollision,
212 Self::DynamicSegmentNameConflict,
213 Self::UnrenderedComponent,
214 Self::UnusedComponentProp,
215 Self::UnusedComponentEmit,
216 Self::UnusedComponentInput,
217 Self::UnusedComponentOutput,
218 Self::UnusedServerAction,
219 Self::UnusedLoadDataKey,
220 Self::PropDrilling,
221 Self::ThinWrapper,
222 Self::DuplicatePropShape,
223 Self::UnusedSvelteEvent,
224 ];
225
226 #[must_use]
228 pub fn parse(s: &str) -> Option<Self> {
229 crate::issue_meta::issue_meta_for_token(s).and_then(|meta| meta.kind)
230 }
231
232 #[must_use]
234 pub const fn to_discriminant(self) -> u8 {
235 match self {
236 Self::UnusedFile => 1,
237 Self::UnusedExport => 2,
238 Self::UnusedType => 3,
239 Self::PrivateTypeLeak => 4,
240 Self::UnusedDependency => 5,
241 Self::UnusedDevDependency => 6,
242 Self::UnusedEnumMember => 7,
243 Self::UnusedClassMember => 8,
244 Self::UnresolvedImport => 9,
245 Self::UnlistedDependency => 10,
246 Self::DuplicateExport => 11,
247 Self::CodeDuplication => 12,
248 Self::CircularDependency => 13,
249 Self::TypeOnlyDependency => 14,
250 Self::TestOnlyDependency => 15,
251 Self::BoundaryViolation => 16,
252 Self::CoverageGaps => 17,
253 Self::FeatureFlag => 18,
254 Self::Complexity => 19,
255 Self::StaleSuppression => 20,
256 Self::PnpmCatalogEntry => 21,
257 Self::UnresolvedCatalogReference => 22,
258 Self::UnusedDependencyOverride => 23,
259 Self::MisconfiguredDependencyOverride => 24,
260 Self::EmptyCatalogGroup => 25,
261 Self::ReExportCycle => 26,
262 Self::SecurityClientServerLeak => 27,
263 Self::SecuritySink => 28,
264 Self::PolicyViolation => 29,
265 Self::InvalidClientExport => 30,
266 Self::MixedClientServerBarrel => 31,
267 Self::MisplacedDirective => 32,
268 Self::UnusedStoreMember => 33,
269 Self::UnprovidedInject => 34,
270 Self::RouteCollision => 35,
271 Self::DynamicSegmentNameConflict => 36,
272 Self::UnrenderedComponent => 37,
273 Self::UnusedComponentProp => 38,
274 Self::UnusedComponentEmit => 39,
275 Self::UnusedServerAction => 40,
276 Self::UnusedLoadDataKey => 41,
277 Self::PropDrilling => 42,
278 Self::ThinWrapper => 43,
279 Self::DuplicatePropShape => 44,
280 Self::UnusedComponentInput => 45,
281 Self::UnusedComponentOutput => 46,
282 Self::UnusedSvelteEvent => 47,
283 }
284 }
285
286 #[must_use]
288 pub const fn from_discriminant(d: u8) -> Option<Self> {
289 match d {
290 1 => Some(Self::UnusedFile),
291 2 => Some(Self::UnusedExport),
292 3 => Some(Self::UnusedType),
293 4 => Some(Self::PrivateTypeLeak),
294 5 => Some(Self::UnusedDependency),
295 6 => Some(Self::UnusedDevDependency),
296 7 => Some(Self::UnusedEnumMember),
297 8 => Some(Self::UnusedClassMember),
298 9 => Some(Self::UnresolvedImport),
299 10 => Some(Self::UnlistedDependency),
300 11 => Some(Self::DuplicateExport),
301 12 => Some(Self::CodeDuplication),
302 13 => Some(Self::CircularDependency),
303 14 => Some(Self::TypeOnlyDependency),
304 15 => Some(Self::TestOnlyDependency),
305 16 => Some(Self::BoundaryViolation),
306 17 => Some(Self::CoverageGaps),
307 18 => Some(Self::FeatureFlag),
308 19 => Some(Self::Complexity),
309 20 => Some(Self::StaleSuppression),
310 21 => Some(Self::PnpmCatalogEntry),
311 22 => Some(Self::UnresolvedCatalogReference),
312 23 => Some(Self::UnusedDependencyOverride),
313 24 => Some(Self::MisconfiguredDependencyOverride),
314 25 => Some(Self::EmptyCatalogGroup),
315 26 => Some(Self::ReExportCycle),
316 27 => Some(Self::SecurityClientServerLeak),
317 28 => Some(Self::SecuritySink),
318 29 => Some(Self::PolicyViolation),
319 30 => Some(Self::InvalidClientExport),
320 31 => Some(Self::MixedClientServerBarrel),
321 32 => Some(Self::MisplacedDirective),
322 33 => Some(Self::UnusedStoreMember),
323 34 => Some(Self::UnprovidedInject),
324 35 => Some(Self::RouteCollision),
325 36 => Some(Self::DynamicSegmentNameConflict),
326 37 => Some(Self::UnrenderedComponent),
327 38 => Some(Self::UnusedComponentProp),
328 39 => Some(Self::UnusedComponentEmit),
329 40 => Some(Self::UnusedServerAction),
330 41 => Some(Self::UnusedLoadDataKey),
331 42 => Some(Self::PropDrilling),
332 43 => Some(Self::ThinWrapper),
333 44 => Some(Self::DuplicatePropShape),
334 45 => Some(Self::UnusedComponentInput),
335 46 => Some(Self::UnusedComponentOutput),
336 47 => Some(Self::UnusedSvelteEvent),
337 _ => None,
338 }
339 }
340}
341
342#[derive(Debug, Clone, PartialEq, Eq, Hash)]
344pub struct PolicyRuleSuppression {
345 pub pack: String,
347 pub rule_id: String,
349}
350
351impl PolicyRuleSuppression {
352 #[must_use]
354 pub fn new(pack: impl Into<String>, rule_id: impl Into<String>) -> Self {
355 Self {
356 pack: pack.into(),
357 rule_id: rule_id.into(),
358 }
359 }
360
361 #[must_use]
363 pub fn token(&self) -> String {
364 format!("policy-violation:{}/{}", self.pack, self.rule_id)
365 }
366}
367
368#[derive(Debug, Clone, PartialEq, Eq)]
370pub enum SuppressionTarget {
371 Issue(IssueKind),
374 PolicyRule(PolicyRuleSuppression),
377}
378
379impl SuppressionTarget {
380 #[must_use]
383 pub const fn issue_kind(&self) -> Option<IssueKind> {
384 match self {
385 Self::Issue(kind) => Some(*kind),
386 Self::PolicyRule(_) => None,
387 }
388 }
389
390 #[must_use]
392 pub fn token(&self) -> String {
393 match self {
394 Self::Issue(kind) => issue_kind_to_kebab(*kind).to_owned(),
395 Self::PolicyRule(rule) => rule.token(),
396 }
397 }
398}
399
400#[must_use]
402pub fn issue_kind_to_kebab(kind: IssueKind) -> &'static str {
403 let Some(meta) = crate::issue_meta::issue_meta_by_kind(kind) else {
404 unreachable!("IssueKind {kind:?} has no metadata row");
405 };
406 meta.suppress_token.unwrap_or(meta.code)
407}
408
409#[must_use]
411pub fn parse_suppression_target(token: &str) -> Option<SuppressionTarget> {
412 parse_policy_rule_suppression_token(token)
413 .map(SuppressionTarget::PolicyRule)
414 .or_else(|| IssueKind::parse(token).map(SuppressionTarget::Issue))
415}
416
417#[must_use]
422pub fn parse_policy_rule_suppression_token(token: &str) -> Option<PolicyRuleSuppression> {
423 let identity = token
424 .strip_prefix("policy-violation:")
425 .or_else(|| token.strip_prefix("policy-violations:"))?;
426 let (pack, rule_id) = identity.split_once('/')?;
427 if rule_id.contains('/') {
428 return None;
429 }
430 if !is_valid_policy_identifier(pack) || !is_valid_policy_identifier(rule_id) {
431 return None;
432 }
433 Some(PolicyRuleSuppression::new(pack, rule_id))
434}
435
436#[must_use]
439pub fn is_valid_policy_identifier(value: &str) -> bool {
440 !value.is_empty()
441 && value
442 .bytes()
443 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
444}
445
446#[derive(Debug, Clone)]
462pub struct Suppression {
463 pub line: u32,
465 pub comment_line: u32,
469 pub target: Option<SuppressionTarget>,
471 pub reason: Option<String>,
473}
474
475impl Suppression {
476 #[must_use]
478 pub const fn all(line: u32, comment_line: u32) -> Self {
479 Self {
480 line,
481 comment_line,
482 target: None,
483 reason: None,
484 }
485 }
486
487 #[must_use]
489 pub const fn issue(line: u32, comment_line: u32, kind: IssueKind) -> Self {
490 Self {
491 line,
492 comment_line,
493 target: Some(SuppressionTarget::Issue(kind)),
494 reason: None,
495 }
496 }
497
498 #[must_use]
500 pub fn policy_rule(
501 line: u32,
502 comment_line: u32,
503 pack: impl Into<String>,
504 rule_id: impl Into<String>,
505 ) -> Self {
506 Self {
507 line,
508 comment_line,
509 target: Some(SuppressionTarget::PolicyRule(PolicyRuleSuppression::new(
510 pack, rule_id,
511 ))),
512 reason: None,
513 }
514 }
515
516 #[must_use]
518 pub fn with_reason(mut self, reason: Option<String>) -> Self {
519 self.reason = reason;
520 self
521 }
522
523 #[must_use]
525 pub const fn issue_kind_target(&self) -> Option<IssueKind> {
526 match &self.target {
527 Some(SuppressionTarget::Issue(kind)) => Some(*kind),
528 Some(SuppressionTarget::PolicyRule(_)) | None => None,
529 }
530 }
531
532 #[must_use]
534 pub const fn policy_rule_target(&self) -> Option<&PolicyRuleSuppression> {
535 match &self.target {
536 Some(SuppressionTarget::PolicyRule(rule)) => Some(rule),
537 Some(SuppressionTarget::Issue(_)) | None => None,
538 }
539 }
540
541 #[must_use]
543 pub fn target_token(&self) -> Option<String> {
544 self.target.as_ref().map(SuppressionTarget::token)
545 }
546
547 #[must_use]
549 pub const fn applies_to_line(&self, line: u32) -> bool {
550 self.line == 0 || self.line == line
551 }
552
553 #[must_use]
559 pub fn matches_issue_kind(&self, line: u32, kind: IssueKind) -> bool {
560 self.applies_to_line(line)
561 && match &self.target {
562 None => true,
563 Some(SuppressionTarget::Issue(target_kind)) => *target_kind == kind,
564 Some(SuppressionTarget::PolicyRule(_)) => false,
565 }
566 }
567
568 #[must_use]
570 pub fn matches_policy_rule(&self, line: u32, pack: &str, rule_id: &str) -> bool {
571 self.applies_to_line(line)
572 && match &self.target {
573 None | Some(SuppressionTarget::Issue(IssueKind::PolicyViolation)) => true,
574 Some(SuppressionTarget::Issue(_)) => false,
575 Some(SuppressionTarget::PolicyRule(target)) => {
576 target.pack == pack && target.rule_id == rule_id
577 }
578 }
579 }
580}
581
582#[must_use]
584pub fn is_suppressed(suppressions: &[Suppression], line: u32, kind: IssueKind) -> bool {
585 suppressions
586 .iter()
587 .any(|suppression| suppression.matches_issue_kind(line, kind))
588}
589
590#[must_use]
592pub fn is_file_suppressed(suppressions: &[Suppression], kind: IssueKind) -> bool {
593 suppressions
594 .iter()
595 .any(|suppression| suppression.line == 0 && suppression.matches_issue_kind(0, kind))
596}
597
598#[derive(Debug, Clone)]
607pub struct UnknownSuppressionKind {
608 pub comment_line: u32,
610 pub is_file_level: bool,
613 pub token: String,
615 pub reason: Option<String>,
617}
618
619fn levenshtein(a: &str, b: &str) -> usize {
627 let a_bytes = a.as_bytes();
628 let b_bytes = b.as_bytes();
629 let (a_len, b_len) = (a_bytes.len(), b_bytes.len());
630
631 if a_len == 0 {
632 return b_len;
633 }
634 if b_len == 0 {
635 return a_len;
636 }
637
638 let mut prev: Vec<usize> = (0..=b_len).collect();
639 let mut curr: Vec<usize> = vec![0; b_len + 1];
640
641 for i in 1..=a_len {
642 curr[0] = i;
643 for j in 1..=b_len {
644 let cost = usize::from(a_bytes[i - 1] != b_bytes[j - 1]);
645 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
646 }
647 std::mem::swap(&mut prev, &mut curr);
648 }
649
650 prev[b_len]
651}
652
653#[must_use]
660pub fn closest_known_kind_name(input: &str) -> Option<&'static str> {
661 let input_lower = input.to_ascii_lowercase();
662 let mut best: Option<(&'static str, usize)> = None;
663
664 for &candidate in KNOWN_ISSUE_KIND_NAMES {
665 let d = levenshtein(&input_lower, candidate);
666 if best.is_none_or(|(_, b_dist)| d < b_dist) {
667 best = Some((candidate, d));
668 }
669 }
670
671 best.filter(|&(_, d)| d > 0 && d <= 2 && input_lower.len() / 2 > d)
672 .map(|(name, _)| name)
673}
674
675const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680
681 #[test]
682 #[expect(
683 clippy::too_many_lines,
684 reason = "exhaustive per-variant parse assertions; one block per issue kind"
685 )]
686 fn issue_kind_from_str_all_variants() {
687 assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
688 assert_eq!(
689 IssueKind::parse("unused-export"),
690 Some(IssueKind::UnusedExport)
691 );
692 assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
693 assert_eq!(
694 IssueKind::parse("private-type-leak"),
695 Some(IssueKind::PrivateTypeLeak)
696 );
697 assert_eq!(
698 IssueKind::parse("unused-dependency"),
699 Some(IssueKind::UnusedDependency)
700 );
701 assert_eq!(
702 IssueKind::parse("unused-dev-dependency"),
703 Some(IssueKind::UnusedDevDependency)
704 );
705 assert_eq!(
706 IssueKind::parse("unused-enum-member"),
707 Some(IssueKind::UnusedEnumMember)
708 );
709 assert_eq!(
710 IssueKind::parse("unused-class-member"),
711 Some(IssueKind::UnusedClassMember)
712 );
713 assert_eq!(
714 IssueKind::parse("unresolved-import"),
715 Some(IssueKind::UnresolvedImport)
716 );
717 assert_eq!(
718 IssueKind::parse("unlisted-dependency"),
719 Some(IssueKind::UnlistedDependency)
720 );
721 assert_eq!(
722 IssueKind::parse("duplicate-export"),
723 Some(IssueKind::DuplicateExport)
724 );
725 assert_eq!(
726 IssueKind::parse("code-duplication"),
727 Some(IssueKind::CodeDuplication)
728 );
729 assert_eq!(
730 IssueKind::parse("circular-dependency"),
731 Some(IssueKind::CircularDependency)
732 );
733 assert_eq!(
734 IssueKind::parse("circular-dependencies"),
735 Some(IssueKind::CircularDependency)
736 );
737 assert_eq!(
738 IssueKind::parse("type-only-dependency"),
739 Some(IssueKind::TypeOnlyDependency)
740 );
741 assert_eq!(
742 IssueKind::parse("test-only-dependency"),
743 Some(IssueKind::TestOnlyDependency)
744 );
745 assert_eq!(
746 IssueKind::parse("boundary-violation"),
747 Some(IssueKind::BoundaryViolation)
748 );
749 assert_eq!(
753 IssueKind::parse("boundary-call-violation"),
754 Some(IssueKind::BoundaryViolation)
755 );
756 assert_eq!(
757 IssueKind::parse("boundary-call-violations"),
758 Some(IssueKind::BoundaryViolation)
759 );
760 assert_eq!(
761 IssueKind::parse("coverage-gaps"),
762 Some(IssueKind::CoverageGaps)
763 );
764 assert_eq!(
765 IssueKind::parse("feature-flag"),
766 Some(IssueKind::FeatureFlag)
767 );
768 assert_eq!(IssueKind::parse("complexity"), Some(IssueKind::Complexity));
769 assert_eq!(
770 IssueKind::parse("stale-suppression"),
771 Some(IssueKind::StaleSuppression)
772 );
773 assert_eq!(
774 IssueKind::parse("unused-catalog-entry"),
775 Some(IssueKind::PnpmCatalogEntry)
776 );
777 assert_eq!(
778 IssueKind::parse("unused-catalog-entries"),
779 Some(IssueKind::PnpmCatalogEntry)
780 );
781 assert_eq!(
782 IssueKind::parse("empty-catalog-group"),
783 Some(IssueKind::EmptyCatalogGroup)
784 );
785 assert_eq!(
786 IssueKind::parse("empty-catalog-groups"),
787 Some(IssueKind::EmptyCatalogGroup)
788 );
789 assert_eq!(
790 IssueKind::parse("unresolved-catalog-reference"),
791 Some(IssueKind::UnresolvedCatalogReference)
792 );
793 assert_eq!(
794 IssueKind::parse("unresolved-catalog-references"),
795 Some(IssueKind::UnresolvedCatalogReference)
796 );
797 assert_eq!(
798 IssueKind::parse("unused-dependency-override"),
799 Some(IssueKind::UnusedDependencyOverride)
800 );
801 assert_eq!(
802 IssueKind::parse("unused-dependency-overrides"),
803 Some(IssueKind::UnusedDependencyOverride)
804 );
805 assert_eq!(
806 IssueKind::parse("misconfigured-dependency-override"),
807 Some(IssueKind::MisconfiguredDependencyOverride)
808 );
809 assert_eq!(
810 IssueKind::parse("misconfigured-dependency-overrides"),
811 Some(IssueKind::MisconfiguredDependencyOverride)
812 );
813 assert_eq!(
814 IssueKind::parse("security-client-server-leak"),
815 Some(IssueKind::SecurityClientServerLeak)
816 );
817 assert_eq!(
818 IssueKind::parse("security-sink"),
819 Some(IssueKind::SecuritySink)
820 );
821 assert_eq!(
822 IssueKind::parse("policy-violation"),
823 Some(IssueKind::PolicyViolation)
824 );
825 assert_eq!(
826 IssueKind::parse("policy-violations"),
827 Some(IssueKind::PolicyViolation)
828 );
829 assert_eq!(
830 IssueKind::parse("invalid-client-export"),
831 Some(IssueKind::InvalidClientExport)
832 );
833 assert_eq!(
834 IssueKind::parse("invalid-client-exports"),
835 Some(IssueKind::InvalidClientExport)
836 );
837 assert_eq!(
838 IssueKind::parse("mixed-client-server-barrel"),
839 Some(IssueKind::MixedClientServerBarrel)
840 );
841 assert_eq!(
842 IssueKind::parse("mixed-client-server-barrels"),
843 Some(IssueKind::MixedClientServerBarrel)
844 );
845 assert_eq!(
846 IssueKind::parse("misplaced-directive"),
847 Some(IssueKind::MisplacedDirective)
848 );
849 assert_eq!(
850 IssueKind::parse("misplaced-directives"),
851 Some(IssueKind::MisplacedDirective)
852 );
853 assert_eq!(
854 IssueKind::parse("route-collision"),
855 Some(IssueKind::RouteCollision)
856 );
857 assert_eq!(
858 IssueKind::parse("route-collisions"),
859 Some(IssueKind::RouteCollision)
860 );
861 assert_eq!(
862 IssueKind::parse("dynamic-segment-name-conflict"),
863 Some(IssueKind::DynamicSegmentNameConflict)
864 );
865 assert_eq!(
866 IssueKind::parse("dynamic-segment-name-conflicts"),
867 Some(IssueKind::DynamicSegmentNameConflict)
868 );
869 assert_eq!(
870 IssueKind::parse("unrendered-component"),
871 Some(IssueKind::UnrenderedComponent)
872 );
873 assert_eq!(
874 IssueKind::parse("unrendered-components"),
875 Some(IssueKind::UnrenderedComponent)
876 );
877 assert_eq!(
878 IssueKind::parse("unused-component-prop"),
879 Some(IssueKind::UnusedComponentProp)
880 );
881 assert_eq!(
882 IssueKind::parse("unused-component-props"),
883 Some(IssueKind::UnusedComponentProp)
884 );
885 assert_eq!(
886 IssueKind::parse("unused-component-emit"),
887 Some(IssueKind::UnusedComponentEmit)
888 );
889 assert_eq!(
890 IssueKind::parse("unused-component-emits"),
891 Some(IssueKind::UnusedComponentEmit)
892 );
893 assert_eq!(
894 IssueKind::parse("unused-component-input"),
895 Some(IssueKind::UnusedComponentInput)
896 );
897 assert_eq!(
898 IssueKind::parse("unused-component-inputs"),
899 Some(IssueKind::UnusedComponentInput)
900 );
901 assert_eq!(
902 IssueKind::parse("unused-component-output"),
903 Some(IssueKind::UnusedComponentOutput)
904 );
905 assert_eq!(
906 IssueKind::parse("unused-component-outputs"),
907 Some(IssueKind::UnusedComponentOutput)
908 );
909 assert_eq!(
910 IssueKind::parse("unused-load-data-key"),
911 Some(IssueKind::UnusedLoadDataKey)
912 );
913 assert_eq!(
914 IssueKind::parse("unused-load-data-keys"),
915 Some(IssueKind::UnusedLoadDataKey)
916 );
917 assert_eq!(
918 IssueKind::parse("prop-drilling"),
919 Some(IssueKind::PropDrilling)
920 );
921 assert_eq!(
922 IssueKind::parse("unused-svelte-event"),
923 Some(IssueKind::UnusedSvelteEvent)
924 );
925 assert_eq!(
926 IssueKind::parse("unused-svelte-events"),
927 Some(IssueKind::UnusedSvelteEvent)
928 );
929 }
930
931 #[test]
932 fn issue_kind_from_str_unknown() {
933 assert_eq!(IssueKind::parse("foo"), None);
934 assert_eq!(IssueKind::parse(""), None);
935 }
936
937 #[test]
938 fn issue_kind_from_str_near_misses() {
939 assert_eq!(IssueKind::parse("Unused-File"), None);
940 assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
941 assert_eq!(IssueKind::parse("unused_file"), None);
942 assert_eq!(IssueKind::parse("unused-files"), None);
943 }
944
945 #[test]
946 fn discriminant_out_of_range() {
947 assert_eq!(IssueKind::from_discriminant(0), None);
948 assert_eq!(
949 IssueKind::from_discriminant(29),
950 Some(IssueKind::PolicyViolation)
951 );
952 assert_eq!(
953 IssueKind::from_discriminant(30),
954 Some(IssueKind::InvalidClientExport)
955 );
956 assert_eq!(
957 IssueKind::from_discriminant(31),
958 Some(IssueKind::MixedClientServerBarrel)
959 );
960 assert_eq!(
961 IssueKind::from_discriminant(32),
962 Some(IssueKind::MisplacedDirective)
963 );
964 assert_eq!(
965 IssueKind::from_discriminant(33),
966 Some(IssueKind::UnusedStoreMember)
967 );
968 assert_eq!(
969 IssueKind::from_discriminant(34),
970 Some(IssueKind::UnprovidedInject)
971 );
972 assert_eq!(
973 IssueKind::from_discriminant(35),
974 Some(IssueKind::RouteCollision)
975 );
976 assert_eq!(
977 IssueKind::from_discriminant(36),
978 Some(IssueKind::DynamicSegmentNameConflict)
979 );
980 assert_eq!(
981 IssueKind::from_discriminant(37),
982 Some(IssueKind::UnrenderedComponent)
983 );
984 assert_eq!(
985 IssueKind::from_discriminant(38),
986 Some(IssueKind::UnusedComponentProp)
987 );
988 assert_eq!(
989 IssueKind::from_discriminant(39),
990 Some(IssueKind::UnusedComponentEmit)
991 );
992 assert_eq!(
993 IssueKind::from_discriminant(40),
994 Some(IssueKind::UnusedServerAction)
995 );
996 assert_eq!(
997 IssueKind::from_discriminant(41),
998 Some(IssueKind::UnusedLoadDataKey)
999 );
1000 assert_eq!(
1001 IssueKind::from_discriminant(42),
1002 Some(IssueKind::PropDrilling)
1003 );
1004 assert_eq!(
1005 IssueKind::from_discriminant(43),
1006 Some(IssueKind::ThinWrapper)
1007 );
1008 assert_eq!(
1009 IssueKind::from_discriminant(44),
1010 Some(IssueKind::DuplicatePropShape)
1011 );
1012 assert_eq!(
1013 IssueKind::from_discriminant(45),
1014 Some(IssueKind::UnusedComponentInput)
1015 );
1016 assert_eq!(
1017 IssueKind::from_discriminant(46),
1018 Some(IssueKind::UnusedComponentOutput)
1019 );
1020 assert_eq!(
1021 IssueKind::from_discriminant(47),
1022 Some(IssueKind::UnusedSvelteEvent)
1023 );
1024 let max_discriminant = IssueKind::ALL
1025 .iter()
1026 .map(|kind| kind.to_discriminant())
1027 .max()
1028 .expect("IssueKind::ALL should not be empty");
1029 assert_eq!(IssueKind::from_discriminant(max_discriminant + 1), None);
1030 assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
1031 }
1032
1033 #[test]
1034 fn discriminant_roundtrip() {
1035 for &kind in IssueKind::ALL {
1036 assert_eq!(
1037 IssueKind::from_discriminant(kind.to_discriminant()),
1038 Some(kind)
1039 );
1040 }
1041 assert_eq!(IssueKind::from_discriminant(0), None);
1042 let max_discriminant = IssueKind::ALL
1043 .iter()
1044 .map(|kind| kind.to_discriminant())
1045 .max()
1046 .expect("IssueKind::ALL should not be empty");
1047 assert_eq!(IssueKind::from_discriminant(max_discriminant + 1), None);
1048 }
1049
1050 #[test]
1051 fn discriminant_values_are_unique() {
1052 let discriminants: Vec<u8> = IssueKind::ALL
1053 .iter()
1054 .map(|kind| kind.to_discriminant())
1055 .collect();
1056 let mut sorted = discriminants.clone();
1057 sorted.sort_unstable();
1058 sorted.dedup();
1059 assert_eq!(
1060 discriminants.len(),
1061 sorted.len(),
1062 "discriminant values must be unique"
1063 );
1064 }
1065
1066 #[test]
1067 fn discriminant_starts_at_one() {
1068 assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
1069 }
1070
1071 #[test]
1072 fn issue_kind_to_kebab_uses_registry_suppression_token() {
1073 for &kind in IssueKind::ALL {
1074 let meta = crate::issue_meta::issue_meta_by_kind(kind)
1075 .unwrap_or_else(|| panic!("IssueKind {kind:?} has no metadata row"));
1076 let token = issue_kind_to_kebab(kind);
1077 assert_eq!(token, meta.suppress_token.unwrap_or(meta.code));
1078 assert_eq!(IssueKind::parse(token), Some(kind));
1079 }
1080 }
1081
1082 #[test]
1083 fn suppression_line_zero_is_file_wide() {
1084 let s = Suppression::all(0, 1);
1085 assert_eq!(s.line, 0);
1086 assert!(s.issue_kind_target().is_none());
1087 }
1088
1089 #[test]
1090 fn suppression_with_specific_kind_and_line() {
1091 let s = Suppression::issue(42, 41, IssueKind::UnusedExport);
1092 assert_eq!(s.line, 42);
1093 assert_eq!(s.comment_line, 41);
1094 assert_eq!(s.issue_kind_target(), Some(IssueKind::UnusedExport));
1095 }
1096
1097 #[test]
1098 fn suppression_predicates_match_lines_and_file_wide_markers() {
1099 let suppressions = vec![
1100 Suppression::issue(42, 41, IssueKind::UnusedExport),
1101 Suppression::all(0, 1),
1102 ];
1103
1104 assert!(is_suppressed(&suppressions, 42, IssueKind::UnusedExport));
1105 assert!(is_suppressed(&suppressions, 10, IssueKind::UnusedType));
1106 assert!(is_file_suppressed(&suppressions, IssueKind::UnusedFile));
1107 }
1108
1109 #[test]
1110 fn parses_scoped_policy_suppression_token() {
1111 let target =
1112 parse_policy_rule_suppression_token("policy-violation:team-policy/no-child-process")
1113 .expect("scoped token should parse");
1114 assert_eq!(target.pack, "team-policy");
1115 assert_eq!(target.rule_id, "no-child-process");
1116 assert_eq!(
1117 target.token(),
1118 "policy-violation:team-policy/no-child-process"
1119 );
1120 }
1121
1122 #[test]
1123 fn rejects_malformed_scoped_policy_suppression_tokens() {
1124 for token in [
1125 "policy-violation:",
1126 "policy-violation:team-policy",
1127 "policy-violation:/no-child-process",
1128 "policy-violation:team-policy/",
1129 "policy-violation:team-policy/no/child-process",
1130 "policy-violation:team policy/no-child-process",
1131 "policy-violation:team-policy/no:child-process",
1132 ] {
1133 assert!(
1134 parse_policy_rule_suppression_token(token).is_none(),
1135 "{token} should be rejected"
1136 );
1137 }
1138 }
1139
1140 #[test]
1141 fn scoped_policy_suppression_matches_exact_policy_rule_only() {
1142 let suppression = Suppression::policy_rule(7, 6, "team-policy", "no-child-process");
1143 assert!(suppression.matches_policy_rule(7, "team-policy", "no-child-process"));
1144 assert!(!suppression.matches_policy_rule(7, "team-policy", "no-fs"));
1145 assert!(!suppression.matches_policy_rule(8, "team-policy", "no-child-process"));
1146 assert!(!suppression.matches_issue_kind(7, IssueKind::PolicyViolation));
1147 }
1148
1149 #[test]
1150 fn known_issue_kind_names_parses_each_entry() {
1151 for &name in KNOWN_ISSUE_KIND_NAMES {
1152 assert!(
1153 IssueKind::parse(name).is_some(),
1154 "KNOWN_ISSUE_KIND_NAMES contains '{name}' but IssueKind::parse rejects it"
1155 );
1156 }
1157 }
1158
1159 #[test]
1160 fn closest_known_kind_name_finds_near_misses() {
1161 assert_eq!(
1162 closest_known_kind_name("unused-exports"),
1163 Some("unused-export")
1164 );
1165 assert_eq!(closest_known_kind_name("unused-files"), Some("unused-file"));
1166 assert_eq!(closest_known_kind_name("complxity"), Some("complexity"));
1167 }
1168
1169 #[test]
1170 fn closest_known_kind_name_rejects_novel_strings() {
1171 assert_eq!(closest_known_kind_name("xyzzy"), None);
1172 assert_eq!(closest_known_kind_name("foo"), None);
1173 assert_eq!(closest_known_kind_name(""), None);
1174 }
1175
1176 #[test]
1177 fn closest_known_kind_name_skips_exact_match() {
1178 assert_eq!(closest_known_kind_name("unused-export"), None);
1179 }
1180}