1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum IssueKind {
22 UnusedFile,
24 UnusedExport,
26 UnusedType,
28 PrivateTypeLeak,
30 UnusedDependency,
32 UnusedDevDependency,
34 UnusedEnumMember,
36 UnusedClassMember,
38 UnresolvedImport,
40 UnlistedDependency,
42 DuplicateExport,
44 CodeDuplication,
46 CircularDependency,
48 ReExportCycle,
52 TypeOnlyDependency,
54 TestOnlyDependency,
56 BoundaryViolation,
58 CoverageGaps,
60 FeatureFlag,
62 Complexity,
64 StaleSuppression,
66 PnpmCatalogEntry,
68 EmptyCatalogGroup,
70 UnresolvedCatalogReference,
73 UnusedDependencyOverride,
76 MisconfiguredDependencyOverride,
79 SecurityClientServerLeak,
82 SecuritySink,
86 PolicyViolation,
90 InvalidClientExport,
93 MixedClientServerBarrel,
97 MisplacedDirective,
102 UnusedStoreMember,
107 UnprovidedInject,
111 RouteCollision,
114 DynamicSegmentNameConflict,
118 UnrenderedComponent,
121 UnusedComponentProp,
125 UnusedComponentEmit,
129 UnusedComponentInput,
134 UnusedComponentOutput,
139 UnusedServerAction,
144 UnusedLoadDataKey,
149 PropDrilling,
154 ThinWrapper,
159 DuplicatePropShape,
165 UnusedSvelteEvent,
170}
171
172impl IssueKind {
173 #[must_use]
175 pub fn parse(s: &str) -> Option<Self> {
176 match s {
177 "unused-file" => Some(Self::UnusedFile),
178 "unused-export" => Some(Self::UnusedExport),
179 "unused-type" => Some(Self::UnusedType),
180 "private-type-leak" => Some(Self::PrivateTypeLeak),
181 "unused-dependency" => Some(Self::UnusedDependency),
182 "unused-dev-dependency" => Some(Self::UnusedDevDependency),
183 "unused-enum-member" => Some(Self::UnusedEnumMember),
184 "unused-class-member" => Some(Self::UnusedClassMember),
185 "unresolved-import" => Some(Self::UnresolvedImport),
186 "unlisted-dependency" => Some(Self::UnlistedDependency),
187 "duplicate-export" => Some(Self::DuplicateExport),
188 "code-duplication" => Some(Self::CodeDuplication),
189 "circular-dependency" | "circular-dependencies" => Some(Self::CircularDependency),
190 "re-export-cycle" | "re-export-cycles" | "reexport-cycle" | "reexport-cycles" => {
191 Some(Self::ReExportCycle)
192 }
193 "type-only-dependency" => Some(Self::TypeOnlyDependency),
194 "test-only-dependency" => Some(Self::TestOnlyDependency),
195 "boundary-violation" | "boundary-call-violation" | "boundary-call-violations" => {
196 Some(Self::BoundaryViolation)
197 }
198 "coverage-gaps" => Some(Self::CoverageGaps),
199 "feature-flag" => Some(Self::FeatureFlag),
200 "complexity" => Some(Self::Complexity),
201 "stale-suppression" => Some(Self::StaleSuppression),
202 "unused-catalog-entry" | "unused-catalog-entries" => Some(Self::PnpmCatalogEntry),
203 "empty-catalog-group" | "empty-catalog-groups" => Some(Self::EmptyCatalogGroup),
204 "unresolved-catalog-reference" | "unresolved-catalog-references" => {
205 Some(Self::UnresolvedCatalogReference)
206 }
207 "unused-dependency-override" | "unused-dependency-overrides" => {
208 Some(Self::UnusedDependencyOverride)
209 }
210 "misconfigured-dependency-override" | "misconfigured-dependency-overrides" => {
211 Some(Self::MisconfiguredDependencyOverride)
212 }
213 "security-client-server-leak" => Some(Self::SecurityClientServerLeak),
214 "security-sink" => Some(Self::SecuritySink),
215 "policy-violation" | "policy-violations" => Some(Self::PolicyViolation),
216 "invalid-client-export" | "invalid-client-exports" => Some(Self::InvalidClientExport),
217 "mixed-client-server-barrel" | "mixed-client-server-barrels" => {
218 Some(Self::MixedClientServerBarrel)
219 }
220 "misplaced-directive" | "misplaced-directives" => Some(Self::MisplacedDirective),
221 "unused-store-member" | "unused-store-members" => Some(Self::UnusedStoreMember),
222 "unprovided-inject" | "unprovided-injects" => Some(Self::UnprovidedInject),
223 "route-collision" | "route-collisions" => Some(Self::RouteCollision),
224 "dynamic-segment-name-conflict" | "dynamic-segment-name-conflicts" => {
225 Some(Self::DynamicSegmentNameConflict)
226 }
227 "unrendered-component" | "unrendered-components" => Some(Self::UnrenderedComponent),
228 "unused-component-prop" | "unused-component-props" => Some(Self::UnusedComponentProp),
229 "unused-component-emit" | "unused-component-emits" => Some(Self::UnusedComponentEmit),
230 "unused-component-input" | "unused-component-inputs" => {
231 Some(Self::UnusedComponentInput)
232 }
233 "unused-component-output" | "unused-component-outputs" => {
234 Some(Self::UnusedComponentOutput)
235 }
236 "unused-server-action" | "unused-server-actions" => Some(Self::UnusedServerAction),
237 "unused-load-data-key" | "unused-load-data-keys" => Some(Self::UnusedLoadDataKey),
238 "prop-drilling" => Some(Self::PropDrilling),
239 "thin-wrapper" | "thin-wrappers" => Some(Self::ThinWrapper),
240 "duplicate-prop-shape" | "duplicate-prop-shapes" => Some(Self::DuplicatePropShape),
241 "unused-svelte-event" | "unused-svelte-events" => Some(Self::UnusedSvelteEvent),
242 _ => None,
243 }
244 }
245
246 #[must_use]
248 pub const fn to_discriminant(self) -> u8 {
249 match self {
250 Self::UnusedFile => 1,
251 Self::UnusedExport => 2,
252 Self::UnusedType => 3,
253 Self::PrivateTypeLeak => 4,
254 Self::UnusedDependency => 5,
255 Self::UnusedDevDependency => 6,
256 Self::UnusedEnumMember => 7,
257 Self::UnusedClassMember => 8,
258 Self::UnresolvedImport => 9,
259 Self::UnlistedDependency => 10,
260 Self::DuplicateExport => 11,
261 Self::CodeDuplication => 12,
262 Self::CircularDependency => 13,
263 Self::TypeOnlyDependency => 14,
264 Self::TestOnlyDependency => 15,
265 Self::BoundaryViolation => 16,
266 Self::CoverageGaps => 17,
267 Self::FeatureFlag => 18,
268 Self::Complexity => 19,
269 Self::StaleSuppression => 20,
270 Self::PnpmCatalogEntry => 21,
271 Self::UnresolvedCatalogReference => 22,
272 Self::UnusedDependencyOverride => 23,
273 Self::MisconfiguredDependencyOverride => 24,
274 Self::EmptyCatalogGroup => 25,
275 Self::ReExportCycle => 26,
276 Self::SecurityClientServerLeak => 27,
277 Self::SecuritySink => 28,
278 Self::PolicyViolation => 29,
279 Self::InvalidClientExport => 30,
280 Self::MixedClientServerBarrel => 31,
281 Self::MisplacedDirective => 32,
282 Self::UnusedStoreMember => 33,
283 Self::UnprovidedInject => 34,
284 Self::RouteCollision => 35,
285 Self::DynamicSegmentNameConflict => 36,
286 Self::UnrenderedComponent => 37,
287 Self::UnusedComponentProp => 38,
288 Self::UnusedComponentEmit => 39,
289 Self::UnusedServerAction => 40,
290 Self::UnusedLoadDataKey => 41,
291 Self::PropDrilling => 42,
292 Self::ThinWrapper => 43,
293 Self::DuplicatePropShape => 44,
294 Self::UnusedComponentInput => 45,
295 Self::UnusedComponentOutput => 46,
296 Self::UnusedSvelteEvent => 47,
297 }
298 }
299
300 #[must_use]
302 pub const fn from_discriminant(d: u8) -> Option<Self> {
303 match d {
304 1 => Some(Self::UnusedFile),
305 2 => Some(Self::UnusedExport),
306 3 => Some(Self::UnusedType),
307 4 => Some(Self::PrivateTypeLeak),
308 5 => Some(Self::UnusedDependency),
309 6 => Some(Self::UnusedDevDependency),
310 7 => Some(Self::UnusedEnumMember),
311 8 => Some(Self::UnusedClassMember),
312 9 => Some(Self::UnresolvedImport),
313 10 => Some(Self::UnlistedDependency),
314 11 => Some(Self::DuplicateExport),
315 12 => Some(Self::CodeDuplication),
316 13 => Some(Self::CircularDependency),
317 14 => Some(Self::TypeOnlyDependency),
318 15 => Some(Self::TestOnlyDependency),
319 16 => Some(Self::BoundaryViolation),
320 17 => Some(Self::CoverageGaps),
321 18 => Some(Self::FeatureFlag),
322 19 => Some(Self::Complexity),
323 20 => Some(Self::StaleSuppression),
324 21 => Some(Self::PnpmCatalogEntry),
325 22 => Some(Self::UnresolvedCatalogReference),
326 23 => Some(Self::UnusedDependencyOverride),
327 24 => Some(Self::MisconfiguredDependencyOverride),
328 25 => Some(Self::EmptyCatalogGroup),
329 26 => Some(Self::ReExportCycle),
330 27 => Some(Self::SecurityClientServerLeak),
331 28 => Some(Self::SecuritySink),
332 29 => Some(Self::PolicyViolation),
333 30 => Some(Self::InvalidClientExport),
334 31 => Some(Self::MixedClientServerBarrel),
335 32 => Some(Self::MisplacedDirective),
336 33 => Some(Self::UnusedStoreMember),
337 34 => Some(Self::UnprovidedInject),
338 35 => Some(Self::RouteCollision),
339 36 => Some(Self::DynamicSegmentNameConflict),
340 37 => Some(Self::UnrenderedComponent),
341 38 => Some(Self::UnusedComponentProp),
342 39 => Some(Self::UnusedComponentEmit),
343 40 => Some(Self::UnusedServerAction),
344 41 => Some(Self::UnusedLoadDataKey),
345 42 => Some(Self::PropDrilling),
346 43 => Some(Self::ThinWrapper),
347 44 => Some(Self::DuplicatePropShape),
348 45 => Some(Self::UnusedComponentInput),
349 46 => Some(Self::UnusedComponentOutput),
350 47 => Some(Self::UnusedSvelteEvent),
351 _ => None,
352 }
353 }
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, Hash)]
358pub struct PolicyRuleSuppression {
359 pub pack: String,
361 pub rule_id: String,
363}
364
365impl PolicyRuleSuppression {
366 #[must_use]
368 pub fn new(pack: impl Into<String>, rule_id: impl Into<String>) -> Self {
369 Self {
370 pack: pack.into(),
371 rule_id: rule_id.into(),
372 }
373 }
374
375 #[must_use]
377 pub fn token(&self) -> String {
378 format!("policy-violation:{}/{}", self.pack, self.rule_id)
379 }
380}
381
382#[derive(Debug, Clone, PartialEq, Eq)]
384pub enum SuppressionTarget {
385 Issue(IssueKind),
388 PolicyRule(PolicyRuleSuppression),
391}
392
393impl SuppressionTarget {
394 #[must_use]
397 pub const fn issue_kind(&self) -> Option<IssueKind> {
398 match self {
399 Self::Issue(kind) => Some(*kind),
400 Self::PolicyRule(_) => None,
401 }
402 }
403
404 #[must_use]
406 pub fn token(&self) -> String {
407 match self {
408 Self::Issue(kind) => issue_kind_to_kebab(*kind).to_owned(),
409 Self::PolicyRule(rule) => rule.token(),
410 }
411 }
412}
413
414#[must_use]
416pub const fn issue_kind_to_kebab(kind: IssueKind) -> &'static str {
417 match kind {
418 IssueKind::UnusedFile => "unused-file",
419 IssueKind::UnusedExport => "unused-export",
420 IssueKind::UnusedType => "unused-type",
421 IssueKind::PrivateTypeLeak => "private-type-leak",
422 IssueKind::UnusedDependency => "unused-dependency",
423 IssueKind::UnusedDevDependency => "unused-dev-dependency",
424 IssueKind::UnusedEnumMember => "unused-enum-member",
425 IssueKind::UnusedClassMember => "unused-class-member",
426 IssueKind::UnresolvedImport => "unresolved-import",
427 IssueKind::UnlistedDependency => "unlisted-dependency",
428 IssueKind::DuplicateExport => "duplicate-export",
429 IssueKind::CodeDuplication => "code-duplication",
430 IssueKind::CircularDependency => "circular-dependency",
431 IssueKind::ReExportCycle => "re-export-cycle",
432 IssueKind::TypeOnlyDependency => "type-only-dependency",
433 IssueKind::TestOnlyDependency => "test-only-dependency",
434 IssueKind::BoundaryViolation => "boundary-violation",
435 IssueKind::CoverageGaps => "coverage-gaps",
436 IssueKind::FeatureFlag => "feature-flag",
437 IssueKind::Complexity => "complexity",
438 IssueKind::StaleSuppression => "stale-suppression",
439 IssueKind::PnpmCatalogEntry => "unused-catalog-entry",
440 IssueKind::EmptyCatalogGroup => "empty-catalog-group",
441 IssueKind::UnresolvedCatalogReference => "unresolved-catalog-reference",
442 IssueKind::UnusedDependencyOverride => "unused-dependency-override",
443 IssueKind::MisconfiguredDependencyOverride => "misconfigured-dependency-override",
444 IssueKind::SecurityClientServerLeak => "security-client-server-leak",
445 IssueKind::SecuritySink => "security-sink",
446 IssueKind::PolicyViolation => "policy-violation",
447 IssueKind::InvalidClientExport => "invalid-client-export",
448 IssueKind::MixedClientServerBarrel => "mixed-client-server-barrel",
449 IssueKind::MisplacedDirective => "misplaced-directive",
450 IssueKind::UnusedStoreMember => "unused-store-member",
451 IssueKind::UnprovidedInject => "unprovided-inject",
452 IssueKind::RouteCollision => "route-collision",
453 IssueKind::DynamicSegmentNameConflict => "dynamic-segment-name-conflict",
454 IssueKind::UnrenderedComponent => "unrendered-component",
455 IssueKind::UnusedComponentProp => "unused-component-prop",
456 IssueKind::UnusedComponentEmit => "unused-component-emit",
457 IssueKind::UnusedComponentInput => "unused-component-input",
458 IssueKind::UnusedComponentOutput => "unused-component-output",
459 IssueKind::UnusedServerAction => "unused-server-action",
460 IssueKind::UnusedLoadDataKey => "unused-load-data-key",
461 IssueKind::PropDrilling => "prop-drilling",
462 IssueKind::ThinWrapper => "thin-wrapper",
463 IssueKind::DuplicatePropShape => "duplicate-prop-shape",
464 IssueKind::UnusedSvelteEvent => "unused-svelte-event",
465 }
466}
467
468#[must_use]
470pub fn parse_suppression_target(token: &str) -> Option<SuppressionTarget> {
471 parse_policy_rule_suppression_token(token)
472 .map(SuppressionTarget::PolicyRule)
473 .or_else(|| IssueKind::parse(token).map(SuppressionTarget::Issue))
474}
475
476#[must_use]
481pub fn parse_policy_rule_suppression_token(token: &str) -> Option<PolicyRuleSuppression> {
482 let identity = token
483 .strip_prefix("policy-violation:")
484 .or_else(|| token.strip_prefix("policy-violations:"))?;
485 let (pack, rule_id) = identity.split_once('/')?;
486 if rule_id.contains('/') {
487 return None;
488 }
489 if !is_valid_policy_identifier(pack) || !is_valid_policy_identifier(rule_id) {
490 return None;
491 }
492 Some(PolicyRuleSuppression::new(pack, rule_id))
493}
494
495#[must_use]
498pub fn is_valid_policy_identifier(value: &str) -> bool {
499 !value.is_empty()
500 && value
501 .bytes()
502 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
503}
504
505#[derive(Debug, Clone)]
521pub struct Suppression {
522 pub line: u32,
524 pub comment_line: u32,
528 pub target: Option<SuppressionTarget>,
530}
531
532impl Suppression {
533 #[must_use]
535 pub const fn all(line: u32, comment_line: u32) -> Self {
536 Self {
537 line,
538 comment_line,
539 target: None,
540 }
541 }
542
543 #[must_use]
545 pub const fn issue(line: u32, comment_line: u32, kind: IssueKind) -> Self {
546 Self {
547 line,
548 comment_line,
549 target: Some(SuppressionTarget::Issue(kind)),
550 }
551 }
552
553 #[must_use]
555 pub fn policy_rule(
556 line: u32,
557 comment_line: u32,
558 pack: impl Into<String>,
559 rule_id: impl Into<String>,
560 ) -> Self {
561 Self {
562 line,
563 comment_line,
564 target: Some(SuppressionTarget::PolicyRule(PolicyRuleSuppression::new(
565 pack, rule_id,
566 ))),
567 }
568 }
569
570 #[must_use]
572 pub const fn issue_kind_target(&self) -> Option<IssueKind> {
573 match &self.target {
574 Some(SuppressionTarget::Issue(kind)) => Some(*kind),
575 Some(SuppressionTarget::PolicyRule(_)) | None => None,
576 }
577 }
578
579 #[must_use]
581 pub const fn policy_rule_target(&self) -> Option<&PolicyRuleSuppression> {
582 match &self.target {
583 Some(SuppressionTarget::PolicyRule(rule)) => Some(rule),
584 Some(SuppressionTarget::Issue(_)) | None => None,
585 }
586 }
587
588 #[must_use]
590 pub fn target_token(&self) -> Option<String> {
591 self.target.as_ref().map(SuppressionTarget::token)
592 }
593
594 #[must_use]
596 pub const fn applies_to_line(&self, line: u32) -> bool {
597 self.line == 0 || self.line == line
598 }
599
600 #[must_use]
606 pub fn matches_issue_kind(&self, line: u32, kind: IssueKind) -> bool {
607 self.applies_to_line(line)
608 && match &self.target {
609 None => true,
610 Some(SuppressionTarget::Issue(target_kind)) => *target_kind == kind,
611 Some(SuppressionTarget::PolicyRule(_)) => false,
612 }
613 }
614
615 #[must_use]
617 pub fn matches_policy_rule(&self, line: u32, pack: &str, rule_id: &str) -> bool {
618 self.applies_to_line(line)
619 && match &self.target {
620 None | Some(SuppressionTarget::Issue(IssueKind::PolicyViolation)) => true,
621 Some(SuppressionTarget::Issue(_)) => false,
622 Some(SuppressionTarget::PolicyRule(target)) => {
623 target.pack == pack && target.rule_id == rule_id
624 }
625 }
626 }
627}
628
629#[derive(Debug, Clone)]
638pub struct UnknownSuppressionKind {
639 pub comment_line: u32,
641 pub is_file_level: bool,
644 pub token: String,
646}
647
648pub const KNOWN_ISSUE_KIND_NAMES: &[&str] = &[
656 "unused-file",
657 "unused-export",
658 "unused-type",
659 "private-type-leak",
660 "unused-dependency",
661 "unused-dev-dependency",
662 "unused-enum-member",
663 "unused-class-member",
664 "unresolved-import",
665 "unlisted-dependency",
666 "duplicate-export",
667 "code-duplication",
668 "circular-dependency",
669 "circular-dependencies",
670 "re-export-cycle",
671 "re-export-cycles",
672 "reexport-cycle",
673 "reexport-cycles",
674 "type-only-dependency",
675 "test-only-dependency",
676 "boundary-violation",
677 "boundary-call-violation",
678 "boundary-call-violations",
679 "coverage-gaps",
680 "feature-flag",
681 "complexity",
682 "stale-suppression",
683 "unused-catalog-entry",
684 "unused-catalog-entries",
685 "empty-catalog-group",
686 "empty-catalog-groups",
687 "unresolved-catalog-reference",
688 "unresolved-catalog-references",
689 "unused-dependency-override",
690 "unused-dependency-overrides",
691 "misconfigured-dependency-override",
692 "misconfigured-dependency-overrides",
693 "security-client-server-leak",
694 "security-sink",
695 "policy-violation",
696 "policy-violations",
697 "invalid-client-export",
698 "invalid-client-exports",
699 "mixed-client-server-barrel",
700 "mixed-client-server-barrels",
701 "misplaced-directive",
702 "misplaced-directives",
703 "unused-store-member",
704 "unused-store-members",
705 "unprovided-inject",
706 "unprovided-injects",
707 "route-collision",
708 "route-collisions",
709 "dynamic-segment-name-conflict",
710 "dynamic-segment-name-conflicts",
711 "unrendered-component",
712 "unrendered-components",
713 "unused-component-prop",
714 "unused-component-props",
715 "unused-component-emit",
716 "unused-component-emits",
717 "unused-component-input",
718 "unused-component-inputs",
719 "unused-component-output",
720 "unused-component-outputs",
721 "unused-server-action",
722 "unused-server-actions",
723 "unused-load-data-key",
724 "unused-load-data-keys",
725 "prop-drilling",
726 "thin-wrapper",
727 "thin-wrappers",
728 "duplicate-prop-shape",
729 "duplicate-prop-shapes",
730 "unused-svelte-event",
731 "unused-svelte-events",
732];
733
734pub const DEAD_CODE_FILTER_FLAGS: &[&str] = &[
743 "--unused-files",
744 "--unused-exports",
745 "--unused-types",
746 "--private-type-leaks",
747 "--unused-deps",
748 "--unused-enum-members",
749 "--unused-class-members",
750 "--unused-store-members",
751 "--unprovided-injects",
752 "--unrendered-components",
753 "--unused-component-props",
754 "--unused-component-emits",
755 "--unused-component-inputs",
756 "--unused-component-outputs",
757 "--unused-svelte-events",
758 "--unused-server-actions",
759 "--unused-load-data-keys",
760 "--unresolved-imports",
761 "--unlisted-deps",
762 "--duplicate-exports",
763 "--circular-deps",
764 "--re-export-cycles",
765 "--boundary-violations",
766 "--policy-violations",
767 "--stale-suppressions",
768 "--unused-catalog-entries",
769 "--empty-catalog-groups",
770 "--unresolved-catalog-references",
771 "--unused-dependency-overrides",
772 "--misconfigured-dependency-overrides",
773];
774
775fn levenshtein(a: &str, b: &str) -> usize {
783 let a_bytes = a.as_bytes();
784 let b_bytes = b.as_bytes();
785 let (a_len, b_len) = (a_bytes.len(), b_bytes.len());
786
787 if a_len == 0 {
788 return b_len;
789 }
790 if b_len == 0 {
791 return a_len;
792 }
793
794 let mut prev: Vec<usize> = (0..=b_len).collect();
795 let mut curr: Vec<usize> = vec![0; b_len + 1];
796
797 for i in 1..=a_len {
798 curr[0] = i;
799 for j in 1..=b_len {
800 let cost = usize::from(a_bytes[i - 1] != b_bytes[j - 1]);
801 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
802 }
803 std::mem::swap(&mut prev, &mut curr);
804 }
805
806 prev[b_len]
807}
808
809#[must_use]
816pub fn closest_known_kind_name(input: &str) -> Option<&'static str> {
817 let input_lower = input.to_ascii_lowercase();
818 let mut best: Option<(&'static str, usize)> = None;
819
820 for &candidate in KNOWN_ISSUE_KIND_NAMES {
821 let d = levenshtein(&input_lower, candidate);
822 if best.is_none_or(|(_, b_dist)| d < b_dist) {
823 best = Some((candidate, d));
824 }
825 }
826
827 best.filter(|&(_, d)| d > 0 && d <= 2 && input_lower.len() / 2 > d)
828 .map(|(name, _)| name)
829}
830
831const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[test]
838 #[expect(
839 clippy::too_many_lines,
840 reason = "exhaustive per-variant parse assertions; one block per issue kind"
841 )]
842 fn issue_kind_from_str_all_variants() {
843 assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
844 assert_eq!(
845 IssueKind::parse("unused-export"),
846 Some(IssueKind::UnusedExport)
847 );
848 assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
849 assert_eq!(
850 IssueKind::parse("private-type-leak"),
851 Some(IssueKind::PrivateTypeLeak)
852 );
853 assert_eq!(
854 IssueKind::parse("unused-dependency"),
855 Some(IssueKind::UnusedDependency)
856 );
857 assert_eq!(
858 IssueKind::parse("unused-dev-dependency"),
859 Some(IssueKind::UnusedDevDependency)
860 );
861 assert_eq!(
862 IssueKind::parse("unused-enum-member"),
863 Some(IssueKind::UnusedEnumMember)
864 );
865 assert_eq!(
866 IssueKind::parse("unused-class-member"),
867 Some(IssueKind::UnusedClassMember)
868 );
869 assert_eq!(
870 IssueKind::parse("unresolved-import"),
871 Some(IssueKind::UnresolvedImport)
872 );
873 assert_eq!(
874 IssueKind::parse("unlisted-dependency"),
875 Some(IssueKind::UnlistedDependency)
876 );
877 assert_eq!(
878 IssueKind::parse("duplicate-export"),
879 Some(IssueKind::DuplicateExport)
880 );
881 assert_eq!(
882 IssueKind::parse("code-duplication"),
883 Some(IssueKind::CodeDuplication)
884 );
885 assert_eq!(
886 IssueKind::parse("circular-dependency"),
887 Some(IssueKind::CircularDependency)
888 );
889 assert_eq!(
890 IssueKind::parse("circular-dependencies"),
891 Some(IssueKind::CircularDependency)
892 );
893 assert_eq!(
894 IssueKind::parse("type-only-dependency"),
895 Some(IssueKind::TypeOnlyDependency)
896 );
897 assert_eq!(
898 IssueKind::parse("test-only-dependency"),
899 Some(IssueKind::TestOnlyDependency)
900 );
901 assert_eq!(
902 IssueKind::parse("boundary-violation"),
903 Some(IssueKind::BoundaryViolation)
904 );
905 assert_eq!(
909 IssueKind::parse("boundary-call-violation"),
910 Some(IssueKind::BoundaryViolation)
911 );
912 assert_eq!(
913 IssueKind::parse("boundary-call-violations"),
914 Some(IssueKind::BoundaryViolation)
915 );
916 assert_eq!(
917 IssueKind::parse("coverage-gaps"),
918 Some(IssueKind::CoverageGaps)
919 );
920 assert_eq!(
921 IssueKind::parse("feature-flag"),
922 Some(IssueKind::FeatureFlag)
923 );
924 assert_eq!(IssueKind::parse("complexity"), Some(IssueKind::Complexity));
925 assert_eq!(
926 IssueKind::parse("stale-suppression"),
927 Some(IssueKind::StaleSuppression)
928 );
929 assert_eq!(
930 IssueKind::parse("unused-catalog-entry"),
931 Some(IssueKind::PnpmCatalogEntry)
932 );
933 assert_eq!(
934 IssueKind::parse("unused-catalog-entries"),
935 Some(IssueKind::PnpmCatalogEntry)
936 );
937 assert_eq!(
938 IssueKind::parse("empty-catalog-group"),
939 Some(IssueKind::EmptyCatalogGroup)
940 );
941 assert_eq!(
942 IssueKind::parse("empty-catalog-groups"),
943 Some(IssueKind::EmptyCatalogGroup)
944 );
945 assert_eq!(
946 IssueKind::parse("unresolved-catalog-reference"),
947 Some(IssueKind::UnresolvedCatalogReference)
948 );
949 assert_eq!(
950 IssueKind::parse("unresolved-catalog-references"),
951 Some(IssueKind::UnresolvedCatalogReference)
952 );
953 assert_eq!(
954 IssueKind::parse("unused-dependency-override"),
955 Some(IssueKind::UnusedDependencyOverride)
956 );
957 assert_eq!(
958 IssueKind::parse("unused-dependency-overrides"),
959 Some(IssueKind::UnusedDependencyOverride)
960 );
961 assert_eq!(
962 IssueKind::parse("misconfigured-dependency-override"),
963 Some(IssueKind::MisconfiguredDependencyOverride)
964 );
965 assert_eq!(
966 IssueKind::parse("misconfigured-dependency-overrides"),
967 Some(IssueKind::MisconfiguredDependencyOverride)
968 );
969 assert_eq!(
970 IssueKind::parse("security-client-server-leak"),
971 Some(IssueKind::SecurityClientServerLeak)
972 );
973 assert_eq!(
974 IssueKind::parse("security-sink"),
975 Some(IssueKind::SecuritySink)
976 );
977 assert_eq!(
978 IssueKind::parse("policy-violation"),
979 Some(IssueKind::PolicyViolation)
980 );
981 assert_eq!(
982 IssueKind::parse("policy-violations"),
983 Some(IssueKind::PolicyViolation)
984 );
985 assert_eq!(
986 IssueKind::parse("invalid-client-export"),
987 Some(IssueKind::InvalidClientExport)
988 );
989 assert_eq!(
990 IssueKind::parse("invalid-client-exports"),
991 Some(IssueKind::InvalidClientExport)
992 );
993 assert_eq!(
994 IssueKind::parse("mixed-client-server-barrel"),
995 Some(IssueKind::MixedClientServerBarrel)
996 );
997 assert_eq!(
998 IssueKind::parse("mixed-client-server-barrels"),
999 Some(IssueKind::MixedClientServerBarrel)
1000 );
1001 assert_eq!(
1002 IssueKind::parse("misplaced-directive"),
1003 Some(IssueKind::MisplacedDirective)
1004 );
1005 assert_eq!(
1006 IssueKind::parse("misplaced-directives"),
1007 Some(IssueKind::MisplacedDirective)
1008 );
1009 assert_eq!(
1010 IssueKind::parse("route-collision"),
1011 Some(IssueKind::RouteCollision)
1012 );
1013 assert_eq!(
1014 IssueKind::parse("route-collisions"),
1015 Some(IssueKind::RouteCollision)
1016 );
1017 assert_eq!(
1018 IssueKind::parse("dynamic-segment-name-conflict"),
1019 Some(IssueKind::DynamicSegmentNameConflict)
1020 );
1021 assert_eq!(
1022 IssueKind::parse("dynamic-segment-name-conflicts"),
1023 Some(IssueKind::DynamicSegmentNameConflict)
1024 );
1025 assert_eq!(
1026 IssueKind::parse("unrendered-component"),
1027 Some(IssueKind::UnrenderedComponent)
1028 );
1029 assert_eq!(
1030 IssueKind::parse("unrendered-components"),
1031 Some(IssueKind::UnrenderedComponent)
1032 );
1033 assert_eq!(
1034 IssueKind::parse("unused-component-prop"),
1035 Some(IssueKind::UnusedComponentProp)
1036 );
1037 assert_eq!(
1038 IssueKind::parse("unused-component-props"),
1039 Some(IssueKind::UnusedComponentProp)
1040 );
1041 assert_eq!(
1042 IssueKind::parse("unused-component-emit"),
1043 Some(IssueKind::UnusedComponentEmit)
1044 );
1045 assert_eq!(
1046 IssueKind::parse("unused-component-emits"),
1047 Some(IssueKind::UnusedComponentEmit)
1048 );
1049 assert_eq!(
1050 IssueKind::parse("unused-component-input"),
1051 Some(IssueKind::UnusedComponentInput)
1052 );
1053 assert_eq!(
1054 IssueKind::parse("unused-component-inputs"),
1055 Some(IssueKind::UnusedComponentInput)
1056 );
1057 assert_eq!(
1058 IssueKind::parse("unused-component-output"),
1059 Some(IssueKind::UnusedComponentOutput)
1060 );
1061 assert_eq!(
1062 IssueKind::parse("unused-component-outputs"),
1063 Some(IssueKind::UnusedComponentOutput)
1064 );
1065 assert_eq!(
1066 IssueKind::parse("unused-load-data-key"),
1067 Some(IssueKind::UnusedLoadDataKey)
1068 );
1069 assert_eq!(
1070 IssueKind::parse("unused-load-data-keys"),
1071 Some(IssueKind::UnusedLoadDataKey)
1072 );
1073 assert_eq!(
1074 IssueKind::parse("prop-drilling"),
1075 Some(IssueKind::PropDrilling)
1076 );
1077 assert_eq!(
1078 IssueKind::parse("unused-svelte-event"),
1079 Some(IssueKind::UnusedSvelteEvent)
1080 );
1081 assert_eq!(
1082 IssueKind::parse("unused-svelte-events"),
1083 Some(IssueKind::UnusedSvelteEvent)
1084 );
1085 }
1086
1087 #[test]
1088 fn issue_kind_from_str_unknown() {
1089 assert_eq!(IssueKind::parse("foo"), None);
1090 assert_eq!(IssueKind::parse(""), None);
1091 }
1092
1093 #[test]
1094 fn issue_kind_from_str_near_misses() {
1095 assert_eq!(IssueKind::parse("Unused-File"), None);
1096 assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
1097 assert_eq!(IssueKind::parse("unused_file"), None);
1098 assert_eq!(IssueKind::parse("unused-files"), None);
1099 }
1100
1101 #[test]
1102 fn discriminant_out_of_range() {
1103 assert_eq!(IssueKind::from_discriminant(0), None);
1104 assert_eq!(
1105 IssueKind::from_discriminant(29),
1106 Some(IssueKind::PolicyViolation)
1107 );
1108 assert_eq!(
1109 IssueKind::from_discriminant(30),
1110 Some(IssueKind::InvalidClientExport)
1111 );
1112 assert_eq!(
1113 IssueKind::from_discriminant(31),
1114 Some(IssueKind::MixedClientServerBarrel)
1115 );
1116 assert_eq!(
1117 IssueKind::from_discriminant(32),
1118 Some(IssueKind::MisplacedDirective)
1119 );
1120 assert_eq!(
1121 IssueKind::from_discriminant(33),
1122 Some(IssueKind::UnusedStoreMember)
1123 );
1124 assert_eq!(
1125 IssueKind::from_discriminant(34),
1126 Some(IssueKind::UnprovidedInject)
1127 );
1128 assert_eq!(
1129 IssueKind::from_discriminant(35),
1130 Some(IssueKind::RouteCollision)
1131 );
1132 assert_eq!(
1133 IssueKind::from_discriminant(36),
1134 Some(IssueKind::DynamicSegmentNameConflict)
1135 );
1136 assert_eq!(
1137 IssueKind::from_discriminant(37),
1138 Some(IssueKind::UnrenderedComponent)
1139 );
1140 assert_eq!(
1141 IssueKind::from_discriminant(38),
1142 Some(IssueKind::UnusedComponentProp)
1143 );
1144 assert_eq!(
1145 IssueKind::from_discriminant(39),
1146 Some(IssueKind::UnusedComponentEmit)
1147 );
1148 assert_eq!(
1149 IssueKind::from_discriminant(40),
1150 Some(IssueKind::UnusedServerAction)
1151 );
1152 assert_eq!(
1153 IssueKind::from_discriminant(41),
1154 Some(IssueKind::UnusedLoadDataKey)
1155 );
1156 assert_eq!(
1157 IssueKind::from_discriminant(42),
1158 Some(IssueKind::PropDrilling)
1159 );
1160 assert_eq!(
1161 IssueKind::from_discriminant(43),
1162 Some(IssueKind::ThinWrapper)
1163 );
1164 assert_eq!(
1165 IssueKind::from_discriminant(44),
1166 Some(IssueKind::DuplicatePropShape)
1167 );
1168 assert_eq!(
1169 IssueKind::from_discriminant(45),
1170 Some(IssueKind::UnusedComponentInput)
1171 );
1172 assert_eq!(
1173 IssueKind::from_discriminant(46),
1174 Some(IssueKind::UnusedComponentOutput)
1175 );
1176 assert_eq!(
1177 IssueKind::from_discriminant(47),
1178 Some(IssueKind::UnusedSvelteEvent)
1179 );
1180 assert_eq!(IssueKind::from_discriminant(48), None);
1181 assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
1182 }
1183
1184 #[test]
1185 fn discriminant_roundtrip() {
1186 for kind in [
1187 IssueKind::UnusedFile,
1188 IssueKind::UnusedExport,
1189 IssueKind::UnusedType,
1190 IssueKind::PrivateTypeLeak,
1191 IssueKind::UnusedDependency,
1192 IssueKind::UnusedDevDependency,
1193 IssueKind::UnusedEnumMember,
1194 IssueKind::UnusedClassMember,
1195 IssueKind::UnresolvedImport,
1196 IssueKind::UnlistedDependency,
1197 IssueKind::DuplicateExport,
1198 IssueKind::CodeDuplication,
1199 IssueKind::CircularDependency,
1200 IssueKind::ReExportCycle,
1201 IssueKind::TypeOnlyDependency,
1202 IssueKind::TestOnlyDependency,
1203 IssueKind::BoundaryViolation,
1204 IssueKind::CoverageGaps,
1205 IssueKind::FeatureFlag,
1206 IssueKind::Complexity,
1207 IssueKind::StaleSuppression,
1208 IssueKind::PnpmCatalogEntry,
1209 IssueKind::EmptyCatalogGroup,
1210 IssueKind::UnresolvedCatalogReference,
1211 IssueKind::UnusedDependencyOverride,
1212 IssueKind::MisconfiguredDependencyOverride,
1213 IssueKind::SecurityClientServerLeak,
1214 IssueKind::SecuritySink,
1215 IssueKind::PolicyViolation,
1216 IssueKind::InvalidClientExport,
1217 IssueKind::MixedClientServerBarrel,
1218 IssueKind::MisplacedDirective,
1219 IssueKind::UnusedStoreMember,
1220 IssueKind::UnprovidedInject,
1221 IssueKind::RouteCollision,
1222 IssueKind::DynamicSegmentNameConflict,
1223 IssueKind::UnrenderedComponent,
1224 IssueKind::UnusedComponentProp,
1225 IssueKind::UnusedComponentEmit,
1226 IssueKind::UnusedServerAction,
1227 IssueKind::UnusedLoadDataKey,
1228 IssueKind::PropDrilling,
1229 IssueKind::ThinWrapper,
1230 IssueKind::DuplicatePropShape,
1231 IssueKind::UnusedComponentInput,
1232 IssueKind::UnusedComponentOutput,
1233 IssueKind::UnusedSvelteEvent,
1234 ] {
1235 assert_eq!(
1236 IssueKind::from_discriminant(kind.to_discriminant()),
1237 Some(kind)
1238 );
1239 }
1240 assert_eq!(IssueKind::from_discriminant(0), None);
1241 assert_eq!(IssueKind::from_discriminant(48), None);
1242 }
1243
1244 #[test]
1245 fn discriminant_values_are_unique() {
1246 let all_kinds = [
1247 IssueKind::UnusedFile,
1248 IssueKind::UnusedExport,
1249 IssueKind::UnusedType,
1250 IssueKind::PrivateTypeLeak,
1251 IssueKind::UnusedDependency,
1252 IssueKind::UnusedDevDependency,
1253 IssueKind::UnusedEnumMember,
1254 IssueKind::UnusedClassMember,
1255 IssueKind::UnresolvedImport,
1256 IssueKind::UnlistedDependency,
1257 IssueKind::DuplicateExport,
1258 IssueKind::CodeDuplication,
1259 IssueKind::CircularDependency,
1260 IssueKind::ReExportCycle,
1261 IssueKind::TypeOnlyDependency,
1262 IssueKind::TestOnlyDependency,
1263 IssueKind::BoundaryViolation,
1264 IssueKind::CoverageGaps,
1265 IssueKind::FeatureFlag,
1266 IssueKind::Complexity,
1267 IssueKind::StaleSuppression,
1268 IssueKind::PnpmCatalogEntry,
1269 IssueKind::EmptyCatalogGroup,
1270 IssueKind::UnresolvedCatalogReference,
1271 IssueKind::UnusedDependencyOverride,
1272 IssueKind::MisconfiguredDependencyOverride,
1273 IssueKind::SecurityClientServerLeak,
1274 IssueKind::SecuritySink,
1275 IssueKind::PolicyViolation,
1276 IssueKind::InvalidClientExport,
1277 IssueKind::MixedClientServerBarrel,
1278 IssueKind::MisplacedDirective,
1279 IssueKind::UnusedStoreMember,
1280 IssueKind::UnprovidedInject,
1281 IssueKind::RouteCollision,
1282 IssueKind::DynamicSegmentNameConflict,
1283 IssueKind::UnrenderedComponent,
1284 IssueKind::UnusedComponentProp,
1285 IssueKind::UnusedComponentEmit,
1286 IssueKind::UnusedServerAction,
1287 IssueKind::UnusedLoadDataKey,
1288 IssueKind::PropDrilling,
1289 IssueKind::ThinWrapper,
1290 IssueKind::DuplicatePropShape,
1291 IssueKind::UnusedComponentInput,
1292 IssueKind::UnusedComponentOutput,
1293 IssueKind::UnusedSvelteEvent,
1294 ];
1295 let discriminants: Vec<u8> = all_kinds.iter().map(|k| k.to_discriminant()).collect();
1296 let mut sorted = discriminants.clone();
1297 sorted.sort_unstable();
1298 sorted.dedup();
1299 assert_eq!(
1300 discriminants.len(),
1301 sorted.len(),
1302 "discriminant values must be unique"
1303 );
1304 }
1305
1306 #[test]
1307 fn discriminant_starts_at_one() {
1308 assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
1309 }
1310
1311 #[test]
1312 fn suppression_line_zero_is_file_wide() {
1313 let s = Suppression::all(0, 1);
1314 assert_eq!(s.line, 0);
1315 assert!(s.issue_kind_target().is_none());
1316 }
1317
1318 #[test]
1319 fn suppression_with_specific_kind_and_line() {
1320 let s = Suppression::issue(42, 41, IssueKind::UnusedExport);
1321 assert_eq!(s.line, 42);
1322 assert_eq!(s.comment_line, 41);
1323 assert_eq!(s.issue_kind_target(), Some(IssueKind::UnusedExport));
1324 }
1325
1326 #[test]
1327 fn parses_scoped_policy_suppression_token() {
1328 let target =
1329 parse_policy_rule_suppression_token("policy-violation:team-policy/no-child-process")
1330 .expect("scoped token should parse");
1331 assert_eq!(target.pack, "team-policy");
1332 assert_eq!(target.rule_id, "no-child-process");
1333 assert_eq!(
1334 target.token(),
1335 "policy-violation:team-policy/no-child-process"
1336 );
1337 }
1338
1339 #[test]
1340 fn rejects_malformed_scoped_policy_suppression_tokens() {
1341 for token in [
1342 "policy-violation:",
1343 "policy-violation:team-policy",
1344 "policy-violation:/no-child-process",
1345 "policy-violation:team-policy/",
1346 "policy-violation:team-policy/no/child-process",
1347 "policy-violation:team policy/no-child-process",
1348 "policy-violation:team-policy/no:child-process",
1349 ] {
1350 assert!(
1351 parse_policy_rule_suppression_token(token).is_none(),
1352 "{token} should be rejected"
1353 );
1354 }
1355 }
1356
1357 #[test]
1358 fn scoped_policy_suppression_matches_exact_policy_rule_only() {
1359 let suppression = Suppression::policy_rule(7, 6, "team-policy", "no-child-process");
1360 assert!(suppression.matches_policy_rule(7, "team-policy", "no-child-process"));
1361 assert!(!suppression.matches_policy_rule(7, "team-policy", "no-fs"));
1362 assert!(!suppression.matches_policy_rule(8, "team-policy", "no-child-process"));
1363 assert!(!suppression.matches_issue_kind(7, IssueKind::PolicyViolation));
1364 }
1365
1366 #[test]
1367 fn known_issue_kind_names_parses_each_entry() {
1368 for &name in KNOWN_ISSUE_KIND_NAMES {
1369 assert!(
1370 IssueKind::parse(name).is_some(),
1371 "KNOWN_ISSUE_KIND_NAMES contains '{name}' but IssueKind::parse rejects it"
1372 );
1373 }
1374 }
1375
1376 #[test]
1377 fn closest_known_kind_name_finds_near_misses() {
1378 assert_eq!(
1379 closest_known_kind_name("unused-exports"),
1380 Some("unused-export")
1381 );
1382 assert_eq!(closest_known_kind_name("unused-files"), Some("unused-file"));
1383 assert_eq!(closest_known_kind_name("complxity"), Some("complexity"));
1384 }
1385
1386 #[test]
1387 fn closest_known_kind_name_rejects_novel_strings() {
1388 assert_eq!(closest_known_kind_name("xyzzy"), None);
1389 assert_eq!(closest_known_kind_name("foo"), None);
1390 assert_eq!(closest_known_kind_name(""), None);
1391 }
1392
1393 #[test]
1394 fn closest_known_kind_name_skips_exact_match() {
1395 assert_eq!(closest_known_kind_name("unused-export"), None);
1396 }
1397}