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#[derive(Debug, Clone)]
591pub struct UnknownSuppressionKind {
592 pub comment_line: u32,
594 pub is_file_level: bool,
597 pub token: String,
599 pub reason: Option<String>,
601}
602
603fn levenshtein(a: &str, b: &str) -> usize {
611 let a_bytes = a.as_bytes();
612 let b_bytes = b.as_bytes();
613 let (a_len, b_len) = (a_bytes.len(), b_bytes.len());
614
615 if a_len == 0 {
616 return b_len;
617 }
618 if b_len == 0 {
619 return a_len;
620 }
621
622 let mut prev: Vec<usize> = (0..=b_len).collect();
623 let mut curr: Vec<usize> = vec![0; b_len + 1];
624
625 for i in 1..=a_len {
626 curr[0] = i;
627 for j in 1..=b_len {
628 let cost = usize::from(a_bytes[i - 1] != b_bytes[j - 1]);
629 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
630 }
631 std::mem::swap(&mut prev, &mut curr);
632 }
633
634 prev[b_len]
635}
636
637#[must_use]
644pub fn closest_known_kind_name(input: &str) -> Option<&'static str> {
645 let input_lower = input.to_ascii_lowercase();
646 let mut best: Option<(&'static str, usize)> = None;
647
648 for &candidate in KNOWN_ISSUE_KIND_NAMES {
649 let d = levenshtein(&input_lower, candidate);
650 if best.is_none_or(|(_, b_dist)| d < b_dist) {
651 best = Some((candidate, d));
652 }
653 }
654
655 best.filter(|&(_, d)| d > 0 && d <= 2 && input_lower.len() / 2 > d)
656 .map(|(name, _)| name)
657}
658
659const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664
665 #[test]
666 #[expect(
667 clippy::too_many_lines,
668 reason = "exhaustive per-variant parse assertions; one block per issue kind"
669 )]
670 fn issue_kind_from_str_all_variants() {
671 assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
672 assert_eq!(
673 IssueKind::parse("unused-export"),
674 Some(IssueKind::UnusedExport)
675 );
676 assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
677 assert_eq!(
678 IssueKind::parse("private-type-leak"),
679 Some(IssueKind::PrivateTypeLeak)
680 );
681 assert_eq!(
682 IssueKind::parse("unused-dependency"),
683 Some(IssueKind::UnusedDependency)
684 );
685 assert_eq!(
686 IssueKind::parse("unused-dev-dependency"),
687 Some(IssueKind::UnusedDevDependency)
688 );
689 assert_eq!(
690 IssueKind::parse("unused-enum-member"),
691 Some(IssueKind::UnusedEnumMember)
692 );
693 assert_eq!(
694 IssueKind::parse("unused-class-member"),
695 Some(IssueKind::UnusedClassMember)
696 );
697 assert_eq!(
698 IssueKind::parse("unresolved-import"),
699 Some(IssueKind::UnresolvedImport)
700 );
701 assert_eq!(
702 IssueKind::parse("unlisted-dependency"),
703 Some(IssueKind::UnlistedDependency)
704 );
705 assert_eq!(
706 IssueKind::parse("duplicate-export"),
707 Some(IssueKind::DuplicateExport)
708 );
709 assert_eq!(
710 IssueKind::parse("code-duplication"),
711 Some(IssueKind::CodeDuplication)
712 );
713 assert_eq!(
714 IssueKind::parse("circular-dependency"),
715 Some(IssueKind::CircularDependency)
716 );
717 assert_eq!(
718 IssueKind::parse("circular-dependencies"),
719 Some(IssueKind::CircularDependency)
720 );
721 assert_eq!(
722 IssueKind::parse("type-only-dependency"),
723 Some(IssueKind::TypeOnlyDependency)
724 );
725 assert_eq!(
726 IssueKind::parse("test-only-dependency"),
727 Some(IssueKind::TestOnlyDependency)
728 );
729 assert_eq!(
730 IssueKind::parse("boundary-violation"),
731 Some(IssueKind::BoundaryViolation)
732 );
733 assert_eq!(
737 IssueKind::parse("boundary-call-violation"),
738 Some(IssueKind::BoundaryViolation)
739 );
740 assert_eq!(
741 IssueKind::parse("boundary-call-violations"),
742 Some(IssueKind::BoundaryViolation)
743 );
744 assert_eq!(
745 IssueKind::parse("coverage-gaps"),
746 Some(IssueKind::CoverageGaps)
747 );
748 assert_eq!(
749 IssueKind::parse("feature-flag"),
750 Some(IssueKind::FeatureFlag)
751 );
752 assert_eq!(IssueKind::parse("complexity"), Some(IssueKind::Complexity));
753 assert_eq!(
754 IssueKind::parse("stale-suppression"),
755 Some(IssueKind::StaleSuppression)
756 );
757 assert_eq!(
758 IssueKind::parse("unused-catalog-entry"),
759 Some(IssueKind::PnpmCatalogEntry)
760 );
761 assert_eq!(
762 IssueKind::parse("unused-catalog-entries"),
763 Some(IssueKind::PnpmCatalogEntry)
764 );
765 assert_eq!(
766 IssueKind::parse("empty-catalog-group"),
767 Some(IssueKind::EmptyCatalogGroup)
768 );
769 assert_eq!(
770 IssueKind::parse("empty-catalog-groups"),
771 Some(IssueKind::EmptyCatalogGroup)
772 );
773 assert_eq!(
774 IssueKind::parse("unresolved-catalog-reference"),
775 Some(IssueKind::UnresolvedCatalogReference)
776 );
777 assert_eq!(
778 IssueKind::parse("unresolved-catalog-references"),
779 Some(IssueKind::UnresolvedCatalogReference)
780 );
781 assert_eq!(
782 IssueKind::parse("unused-dependency-override"),
783 Some(IssueKind::UnusedDependencyOverride)
784 );
785 assert_eq!(
786 IssueKind::parse("unused-dependency-overrides"),
787 Some(IssueKind::UnusedDependencyOverride)
788 );
789 assert_eq!(
790 IssueKind::parse("misconfigured-dependency-override"),
791 Some(IssueKind::MisconfiguredDependencyOverride)
792 );
793 assert_eq!(
794 IssueKind::parse("misconfigured-dependency-overrides"),
795 Some(IssueKind::MisconfiguredDependencyOverride)
796 );
797 assert_eq!(
798 IssueKind::parse("security-client-server-leak"),
799 Some(IssueKind::SecurityClientServerLeak)
800 );
801 assert_eq!(
802 IssueKind::parse("security-sink"),
803 Some(IssueKind::SecuritySink)
804 );
805 assert_eq!(
806 IssueKind::parse("policy-violation"),
807 Some(IssueKind::PolicyViolation)
808 );
809 assert_eq!(
810 IssueKind::parse("policy-violations"),
811 Some(IssueKind::PolicyViolation)
812 );
813 assert_eq!(
814 IssueKind::parse("invalid-client-export"),
815 Some(IssueKind::InvalidClientExport)
816 );
817 assert_eq!(
818 IssueKind::parse("invalid-client-exports"),
819 Some(IssueKind::InvalidClientExport)
820 );
821 assert_eq!(
822 IssueKind::parse("mixed-client-server-barrel"),
823 Some(IssueKind::MixedClientServerBarrel)
824 );
825 assert_eq!(
826 IssueKind::parse("mixed-client-server-barrels"),
827 Some(IssueKind::MixedClientServerBarrel)
828 );
829 assert_eq!(
830 IssueKind::parse("misplaced-directive"),
831 Some(IssueKind::MisplacedDirective)
832 );
833 assert_eq!(
834 IssueKind::parse("misplaced-directives"),
835 Some(IssueKind::MisplacedDirective)
836 );
837 assert_eq!(
838 IssueKind::parse("route-collision"),
839 Some(IssueKind::RouteCollision)
840 );
841 assert_eq!(
842 IssueKind::parse("route-collisions"),
843 Some(IssueKind::RouteCollision)
844 );
845 assert_eq!(
846 IssueKind::parse("dynamic-segment-name-conflict"),
847 Some(IssueKind::DynamicSegmentNameConflict)
848 );
849 assert_eq!(
850 IssueKind::parse("dynamic-segment-name-conflicts"),
851 Some(IssueKind::DynamicSegmentNameConflict)
852 );
853 assert_eq!(
854 IssueKind::parse("unrendered-component"),
855 Some(IssueKind::UnrenderedComponent)
856 );
857 assert_eq!(
858 IssueKind::parse("unrendered-components"),
859 Some(IssueKind::UnrenderedComponent)
860 );
861 assert_eq!(
862 IssueKind::parse("unused-component-prop"),
863 Some(IssueKind::UnusedComponentProp)
864 );
865 assert_eq!(
866 IssueKind::parse("unused-component-props"),
867 Some(IssueKind::UnusedComponentProp)
868 );
869 assert_eq!(
870 IssueKind::parse("unused-component-emit"),
871 Some(IssueKind::UnusedComponentEmit)
872 );
873 assert_eq!(
874 IssueKind::parse("unused-component-emits"),
875 Some(IssueKind::UnusedComponentEmit)
876 );
877 assert_eq!(
878 IssueKind::parse("unused-component-input"),
879 Some(IssueKind::UnusedComponentInput)
880 );
881 assert_eq!(
882 IssueKind::parse("unused-component-inputs"),
883 Some(IssueKind::UnusedComponentInput)
884 );
885 assert_eq!(
886 IssueKind::parse("unused-component-output"),
887 Some(IssueKind::UnusedComponentOutput)
888 );
889 assert_eq!(
890 IssueKind::parse("unused-component-outputs"),
891 Some(IssueKind::UnusedComponentOutput)
892 );
893 assert_eq!(
894 IssueKind::parse("unused-load-data-key"),
895 Some(IssueKind::UnusedLoadDataKey)
896 );
897 assert_eq!(
898 IssueKind::parse("unused-load-data-keys"),
899 Some(IssueKind::UnusedLoadDataKey)
900 );
901 assert_eq!(
902 IssueKind::parse("prop-drilling"),
903 Some(IssueKind::PropDrilling)
904 );
905 assert_eq!(
906 IssueKind::parse("unused-svelte-event"),
907 Some(IssueKind::UnusedSvelteEvent)
908 );
909 assert_eq!(
910 IssueKind::parse("unused-svelte-events"),
911 Some(IssueKind::UnusedSvelteEvent)
912 );
913 }
914
915 #[test]
916 fn issue_kind_from_str_unknown() {
917 assert_eq!(IssueKind::parse("foo"), None);
918 assert_eq!(IssueKind::parse(""), None);
919 }
920
921 #[test]
922 fn issue_kind_from_str_near_misses() {
923 assert_eq!(IssueKind::parse("Unused-File"), None);
924 assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
925 assert_eq!(IssueKind::parse("unused_file"), None);
926 assert_eq!(IssueKind::parse("unused-files"), None);
927 }
928
929 #[test]
930 fn discriminant_out_of_range() {
931 assert_eq!(IssueKind::from_discriminant(0), None);
932 assert_eq!(
933 IssueKind::from_discriminant(29),
934 Some(IssueKind::PolicyViolation)
935 );
936 assert_eq!(
937 IssueKind::from_discriminant(30),
938 Some(IssueKind::InvalidClientExport)
939 );
940 assert_eq!(
941 IssueKind::from_discriminant(31),
942 Some(IssueKind::MixedClientServerBarrel)
943 );
944 assert_eq!(
945 IssueKind::from_discriminant(32),
946 Some(IssueKind::MisplacedDirective)
947 );
948 assert_eq!(
949 IssueKind::from_discriminant(33),
950 Some(IssueKind::UnusedStoreMember)
951 );
952 assert_eq!(
953 IssueKind::from_discriminant(34),
954 Some(IssueKind::UnprovidedInject)
955 );
956 assert_eq!(
957 IssueKind::from_discriminant(35),
958 Some(IssueKind::RouteCollision)
959 );
960 assert_eq!(
961 IssueKind::from_discriminant(36),
962 Some(IssueKind::DynamicSegmentNameConflict)
963 );
964 assert_eq!(
965 IssueKind::from_discriminant(37),
966 Some(IssueKind::UnrenderedComponent)
967 );
968 assert_eq!(
969 IssueKind::from_discriminant(38),
970 Some(IssueKind::UnusedComponentProp)
971 );
972 assert_eq!(
973 IssueKind::from_discriminant(39),
974 Some(IssueKind::UnusedComponentEmit)
975 );
976 assert_eq!(
977 IssueKind::from_discriminant(40),
978 Some(IssueKind::UnusedServerAction)
979 );
980 assert_eq!(
981 IssueKind::from_discriminant(41),
982 Some(IssueKind::UnusedLoadDataKey)
983 );
984 assert_eq!(
985 IssueKind::from_discriminant(42),
986 Some(IssueKind::PropDrilling)
987 );
988 assert_eq!(
989 IssueKind::from_discriminant(43),
990 Some(IssueKind::ThinWrapper)
991 );
992 assert_eq!(
993 IssueKind::from_discriminant(44),
994 Some(IssueKind::DuplicatePropShape)
995 );
996 assert_eq!(
997 IssueKind::from_discriminant(45),
998 Some(IssueKind::UnusedComponentInput)
999 );
1000 assert_eq!(
1001 IssueKind::from_discriminant(46),
1002 Some(IssueKind::UnusedComponentOutput)
1003 );
1004 assert_eq!(
1005 IssueKind::from_discriminant(47),
1006 Some(IssueKind::UnusedSvelteEvent)
1007 );
1008 let max_discriminant = IssueKind::ALL
1009 .iter()
1010 .map(|kind| kind.to_discriminant())
1011 .max()
1012 .expect("IssueKind::ALL should not be empty");
1013 assert_eq!(IssueKind::from_discriminant(max_discriminant + 1), None);
1014 assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
1015 }
1016
1017 #[test]
1018 fn discriminant_roundtrip() {
1019 for &kind in IssueKind::ALL {
1020 assert_eq!(
1021 IssueKind::from_discriminant(kind.to_discriminant()),
1022 Some(kind)
1023 );
1024 }
1025 assert_eq!(IssueKind::from_discriminant(0), None);
1026 let max_discriminant = IssueKind::ALL
1027 .iter()
1028 .map(|kind| kind.to_discriminant())
1029 .max()
1030 .expect("IssueKind::ALL should not be empty");
1031 assert_eq!(IssueKind::from_discriminant(max_discriminant + 1), None);
1032 }
1033
1034 #[test]
1035 fn discriminant_values_are_unique() {
1036 let discriminants: Vec<u8> = IssueKind::ALL
1037 .iter()
1038 .map(|kind| kind.to_discriminant())
1039 .collect();
1040 let mut sorted = discriminants.clone();
1041 sorted.sort_unstable();
1042 sorted.dedup();
1043 assert_eq!(
1044 discriminants.len(),
1045 sorted.len(),
1046 "discriminant values must be unique"
1047 );
1048 }
1049
1050 #[test]
1051 fn discriminant_starts_at_one() {
1052 assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
1053 }
1054
1055 #[test]
1056 fn issue_kind_to_kebab_uses_registry_suppression_token() {
1057 for &kind in IssueKind::ALL {
1058 let meta = crate::issue_meta::issue_meta_by_kind(kind)
1059 .unwrap_or_else(|| panic!("IssueKind {kind:?} has no metadata row"));
1060 let token = issue_kind_to_kebab(kind);
1061 assert_eq!(token, meta.suppress_token.unwrap_or(meta.code));
1062 assert_eq!(IssueKind::parse(token), Some(kind));
1063 }
1064 }
1065
1066 #[test]
1067 fn suppression_line_zero_is_file_wide() {
1068 let s = Suppression::all(0, 1);
1069 assert_eq!(s.line, 0);
1070 assert!(s.issue_kind_target().is_none());
1071 }
1072
1073 #[test]
1074 fn suppression_with_specific_kind_and_line() {
1075 let s = Suppression::issue(42, 41, IssueKind::UnusedExport);
1076 assert_eq!(s.line, 42);
1077 assert_eq!(s.comment_line, 41);
1078 assert_eq!(s.issue_kind_target(), Some(IssueKind::UnusedExport));
1079 }
1080
1081 #[test]
1082 fn parses_scoped_policy_suppression_token() {
1083 let target =
1084 parse_policy_rule_suppression_token("policy-violation:team-policy/no-child-process")
1085 .expect("scoped token should parse");
1086 assert_eq!(target.pack, "team-policy");
1087 assert_eq!(target.rule_id, "no-child-process");
1088 assert_eq!(
1089 target.token(),
1090 "policy-violation:team-policy/no-child-process"
1091 );
1092 }
1093
1094 #[test]
1095 fn rejects_malformed_scoped_policy_suppression_tokens() {
1096 for token in [
1097 "policy-violation:",
1098 "policy-violation:team-policy",
1099 "policy-violation:/no-child-process",
1100 "policy-violation:team-policy/",
1101 "policy-violation:team-policy/no/child-process",
1102 "policy-violation:team policy/no-child-process",
1103 "policy-violation:team-policy/no:child-process",
1104 ] {
1105 assert!(
1106 parse_policy_rule_suppression_token(token).is_none(),
1107 "{token} should be rejected"
1108 );
1109 }
1110 }
1111
1112 #[test]
1113 fn scoped_policy_suppression_matches_exact_policy_rule_only() {
1114 let suppression = Suppression::policy_rule(7, 6, "team-policy", "no-child-process");
1115 assert!(suppression.matches_policy_rule(7, "team-policy", "no-child-process"));
1116 assert!(!suppression.matches_policy_rule(7, "team-policy", "no-fs"));
1117 assert!(!suppression.matches_policy_rule(8, "team-policy", "no-child-process"));
1118 assert!(!suppression.matches_issue_kind(7, IssueKind::PolicyViolation));
1119 }
1120
1121 #[test]
1122 fn known_issue_kind_names_parses_each_entry() {
1123 for &name in KNOWN_ISSUE_KIND_NAMES {
1124 assert!(
1125 IssueKind::parse(name).is_some(),
1126 "KNOWN_ISSUE_KIND_NAMES contains '{name}' but IssueKind::parse rejects it"
1127 );
1128 }
1129 }
1130
1131 #[test]
1132 fn closest_known_kind_name_finds_near_misses() {
1133 assert_eq!(
1134 closest_known_kind_name("unused-exports"),
1135 Some("unused-export")
1136 );
1137 assert_eq!(closest_known_kind_name("unused-files"), Some("unused-file"));
1138 assert_eq!(closest_known_kind_name("complxity"), Some("complexity"));
1139 }
1140
1141 #[test]
1142 fn closest_known_kind_name_rejects_novel_strings() {
1143 assert_eq!(closest_known_kind_name("xyzzy"), None);
1144 assert_eq!(closest_known_kind_name("foo"), None);
1145 assert_eq!(closest_known_kind_name(""), None);
1146 }
1147
1148 #[test]
1149 fn closest_known_kind_name_skips_exact_match() {
1150 assert_eq!(closest_known_kind_name("unused-export"), None);
1151 }
1152}