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