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