#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueKind {
UnusedFile,
UnusedExport,
UnusedType,
PrivateTypeLeak,
UnusedDependency,
UnusedDevDependency,
UnusedEnumMember,
UnusedClassMember,
UnresolvedImport,
UnlistedDependency,
DuplicateExport,
CodeDuplication,
CircularDependency,
ReExportCycle,
TypeOnlyDependency,
TestOnlyDependency,
BoundaryViolation,
CoverageGaps,
FeatureFlag,
Complexity,
StaleSuppression,
PnpmCatalogEntry,
EmptyCatalogGroup,
UnresolvedCatalogReference,
UnusedDependencyOverride,
MisconfiguredDependencyOverride,
}
impl IssueKind {
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s {
"unused-file" => Some(Self::UnusedFile),
"unused-export" => Some(Self::UnusedExport),
"unused-type" => Some(Self::UnusedType),
"private-type-leak" => Some(Self::PrivateTypeLeak),
"unused-dependency" => Some(Self::UnusedDependency),
"unused-dev-dependency" => Some(Self::UnusedDevDependency),
"unused-enum-member" => Some(Self::UnusedEnumMember),
"unused-class-member" => Some(Self::UnusedClassMember),
"unresolved-import" => Some(Self::UnresolvedImport),
"unlisted-dependency" => Some(Self::UnlistedDependency),
"duplicate-export" => Some(Self::DuplicateExport),
"code-duplication" => Some(Self::CodeDuplication),
"circular-dependency" | "circular-dependencies" => Some(Self::CircularDependency),
"re-export-cycle" | "re-export-cycles" | "reexport-cycle" | "reexport-cycles" => {
Some(Self::ReExportCycle)
}
"type-only-dependency" => Some(Self::TypeOnlyDependency),
"test-only-dependency" => Some(Self::TestOnlyDependency),
"boundary-violation" => Some(Self::BoundaryViolation),
"coverage-gaps" => Some(Self::CoverageGaps),
"feature-flag" => Some(Self::FeatureFlag),
"complexity" => Some(Self::Complexity),
"stale-suppression" => Some(Self::StaleSuppression),
"unused-catalog-entry" | "unused-catalog-entries" => Some(Self::PnpmCatalogEntry),
"empty-catalog-group" | "empty-catalog-groups" => Some(Self::EmptyCatalogGroup),
"unresolved-catalog-reference" | "unresolved-catalog-references" => {
Some(Self::UnresolvedCatalogReference)
}
"unused-dependency-override" | "unused-dependency-overrides" => {
Some(Self::UnusedDependencyOverride)
}
"misconfigured-dependency-override" | "misconfigured-dependency-overrides" => {
Some(Self::MisconfiguredDependencyOverride)
}
_ => None,
}
}
#[must_use]
pub const fn to_discriminant(self) -> u8 {
match self {
Self::UnusedFile => 1,
Self::UnusedExport => 2,
Self::UnusedType => 3,
Self::PrivateTypeLeak => 4,
Self::UnusedDependency => 5,
Self::UnusedDevDependency => 6,
Self::UnusedEnumMember => 7,
Self::UnusedClassMember => 8,
Self::UnresolvedImport => 9,
Self::UnlistedDependency => 10,
Self::DuplicateExport => 11,
Self::CodeDuplication => 12,
Self::CircularDependency => 13,
Self::TypeOnlyDependency => 14,
Self::TestOnlyDependency => 15,
Self::BoundaryViolation => 16,
Self::CoverageGaps => 17,
Self::FeatureFlag => 18,
Self::Complexity => 19,
Self::StaleSuppression => 20,
Self::PnpmCatalogEntry => 21,
Self::UnresolvedCatalogReference => 22,
Self::UnusedDependencyOverride => 23,
Self::MisconfiguredDependencyOverride => 24,
Self::EmptyCatalogGroup => 25,
Self::ReExportCycle => 26,
}
}
#[must_use]
pub const fn from_discriminant(d: u8) -> Option<Self> {
match d {
1 => Some(Self::UnusedFile),
2 => Some(Self::UnusedExport),
3 => Some(Self::UnusedType),
4 => Some(Self::PrivateTypeLeak),
5 => Some(Self::UnusedDependency),
6 => Some(Self::UnusedDevDependency),
7 => Some(Self::UnusedEnumMember),
8 => Some(Self::UnusedClassMember),
9 => Some(Self::UnresolvedImport),
10 => Some(Self::UnlistedDependency),
11 => Some(Self::DuplicateExport),
12 => Some(Self::CodeDuplication),
13 => Some(Self::CircularDependency),
14 => Some(Self::TypeOnlyDependency),
15 => Some(Self::TestOnlyDependency),
16 => Some(Self::BoundaryViolation),
17 => Some(Self::CoverageGaps),
18 => Some(Self::FeatureFlag),
19 => Some(Self::Complexity),
20 => Some(Self::StaleSuppression),
21 => Some(Self::PnpmCatalogEntry),
22 => Some(Self::UnresolvedCatalogReference),
23 => Some(Self::UnusedDependencyOverride),
24 => Some(Self::MisconfiguredDependencyOverride),
25 => Some(Self::EmptyCatalogGroup),
26 => Some(Self::ReExportCycle),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Suppression {
pub line: u32,
pub comment_line: u32,
pub kind: Option<IssueKind>,
}
#[derive(Debug, Clone)]
pub struct UnknownSuppressionKind {
pub comment_line: u32,
pub is_file_level: bool,
pub token: String,
}
pub const KNOWN_ISSUE_KIND_NAMES: &[&str] = &[
"unused-file",
"unused-export",
"unused-type",
"private-type-leak",
"unused-dependency",
"unused-dev-dependency",
"unused-enum-member",
"unused-class-member",
"unresolved-import",
"unlisted-dependency",
"duplicate-export",
"code-duplication",
"circular-dependency",
"circular-dependencies",
"re-export-cycle",
"re-export-cycles",
"reexport-cycle",
"reexport-cycles",
"type-only-dependency",
"test-only-dependency",
"boundary-violation",
"coverage-gaps",
"feature-flag",
"complexity",
"stale-suppression",
"unused-catalog-entry",
"unused-catalog-entries",
"empty-catalog-group",
"empty-catalog-groups",
"unresolved-catalog-reference",
"unresolved-catalog-references",
"unused-dependency-override",
"unused-dependency-overrides",
"misconfigured-dependency-override",
"misconfigured-dependency-overrides",
];
fn levenshtein(a: &str, b: &str) -> usize {
let a_bytes = a.as_bytes();
let b_bytes = b.as_bytes();
let (a_len, b_len) = (a_bytes.len(), b_bytes.len());
if a_len == 0 {
return b_len;
}
if b_len == 0 {
return a_len;
}
let mut prev: Vec<usize> = (0..=b_len).collect();
let mut curr: Vec<usize> = vec![0; b_len + 1];
for i in 1..=a_len {
curr[0] = i;
for j in 1..=b_len {
let cost = usize::from(a_bytes[i - 1] != b_bytes[j - 1]);
curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
}
std::mem::swap(&mut prev, &mut curr);
}
prev[b_len]
}
#[must_use]
pub fn closest_known_kind_name(input: &str) -> Option<&'static str> {
let input_lower = input.to_ascii_lowercase();
let mut best: Option<(&'static str, usize)> = None;
for &candidate in KNOWN_ISSUE_KIND_NAMES {
let d = levenshtein(&input_lower, candidate);
if best.is_none_or(|(_, b_dist)| d < b_dist) {
best = Some((candidate, d));
}
}
best.filter(|&(_, d)| d > 0 && d <= 2 && input_lower.len() / 2 > d)
.map(|(name, _)| name)
}
const _: () = assert!(std::mem::size_of::<Suppression>() == 12);
const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn issue_kind_from_str_all_variants() {
assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
assert_eq!(
IssueKind::parse("unused-export"),
Some(IssueKind::UnusedExport)
);
assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
assert_eq!(
IssueKind::parse("private-type-leak"),
Some(IssueKind::PrivateTypeLeak)
);
assert_eq!(
IssueKind::parse("unused-dependency"),
Some(IssueKind::UnusedDependency)
);
assert_eq!(
IssueKind::parse("unused-dev-dependency"),
Some(IssueKind::UnusedDevDependency)
);
assert_eq!(
IssueKind::parse("unused-enum-member"),
Some(IssueKind::UnusedEnumMember)
);
assert_eq!(
IssueKind::parse("unused-class-member"),
Some(IssueKind::UnusedClassMember)
);
assert_eq!(
IssueKind::parse("unresolved-import"),
Some(IssueKind::UnresolvedImport)
);
assert_eq!(
IssueKind::parse("unlisted-dependency"),
Some(IssueKind::UnlistedDependency)
);
assert_eq!(
IssueKind::parse("duplicate-export"),
Some(IssueKind::DuplicateExport)
);
assert_eq!(
IssueKind::parse("code-duplication"),
Some(IssueKind::CodeDuplication)
);
assert_eq!(
IssueKind::parse("circular-dependency"),
Some(IssueKind::CircularDependency)
);
assert_eq!(
IssueKind::parse("circular-dependencies"),
Some(IssueKind::CircularDependency)
);
assert_eq!(
IssueKind::parse("type-only-dependency"),
Some(IssueKind::TypeOnlyDependency)
);
assert_eq!(
IssueKind::parse("test-only-dependency"),
Some(IssueKind::TestOnlyDependency)
);
assert_eq!(
IssueKind::parse("boundary-violation"),
Some(IssueKind::BoundaryViolation)
);
assert_eq!(
IssueKind::parse("coverage-gaps"),
Some(IssueKind::CoverageGaps)
);
assert_eq!(
IssueKind::parse("feature-flag"),
Some(IssueKind::FeatureFlag)
);
assert_eq!(IssueKind::parse("complexity"), Some(IssueKind::Complexity));
assert_eq!(
IssueKind::parse("stale-suppression"),
Some(IssueKind::StaleSuppression)
);
assert_eq!(
IssueKind::parse("unused-catalog-entry"),
Some(IssueKind::PnpmCatalogEntry)
);
assert_eq!(
IssueKind::parse("unused-catalog-entries"),
Some(IssueKind::PnpmCatalogEntry)
);
assert_eq!(
IssueKind::parse("empty-catalog-group"),
Some(IssueKind::EmptyCatalogGroup)
);
assert_eq!(
IssueKind::parse("empty-catalog-groups"),
Some(IssueKind::EmptyCatalogGroup)
);
assert_eq!(
IssueKind::parse("unresolved-catalog-reference"),
Some(IssueKind::UnresolvedCatalogReference)
);
assert_eq!(
IssueKind::parse("unresolved-catalog-references"),
Some(IssueKind::UnresolvedCatalogReference)
);
assert_eq!(
IssueKind::parse("unused-dependency-override"),
Some(IssueKind::UnusedDependencyOverride)
);
assert_eq!(
IssueKind::parse("unused-dependency-overrides"),
Some(IssueKind::UnusedDependencyOverride)
);
assert_eq!(
IssueKind::parse("misconfigured-dependency-override"),
Some(IssueKind::MisconfiguredDependencyOverride)
);
assert_eq!(
IssueKind::parse("misconfigured-dependency-overrides"),
Some(IssueKind::MisconfiguredDependencyOverride)
);
}
#[test]
fn issue_kind_from_str_unknown() {
assert_eq!(IssueKind::parse("foo"), None);
assert_eq!(IssueKind::parse(""), None);
}
#[test]
fn issue_kind_from_str_near_misses() {
assert_eq!(IssueKind::parse("Unused-File"), None);
assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
assert_eq!(IssueKind::parse("unused_file"), None);
assert_eq!(IssueKind::parse("unused-files"), None);
}
#[test]
fn discriminant_out_of_range() {
assert_eq!(IssueKind::from_discriminant(0), None);
assert_eq!(IssueKind::from_discriminant(27), None);
assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
}
#[test]
fn discriminant_roundtrip() {
for kind in [
IssueKind::UnusedFile,
IssueKind::UnusedExport,
IssueKind::UnusedType,
IssueKind::PrivateTypeLeak,
IssueKind::UnusedDependency,
IssueKind::UnusedDevDependency,
IssueKind::UnusedEnumMember,
IssueKind::UnusedClassMember,
IssueKind::UnresolvedImport,
IssueKind::UnlistedDependency,
IssueKind::DuplicateExport,
IssueKind::CodeDuplication,
IssueKind::CircularDependency,
IssueKind::ReExportCycle,
IssueKind::TypeOnlyDependency,
IssueKind::TestOnlyDependency,
IssueKind::BoundaryViolation,
IssueKind::CoverageGaps,
IssueKind::FeatureFlag,
IssueKind::Complexity,
IssueKind::StaleSuppression,
IssueKind::PnpmCatalogEntry,
IssueKind::EmptyCatalogGroup,
IssueKind::UnresolvedCatalogReference,
IssueKind::UnusedDependencyOverride,
IssueKind::MisconfiguredDependencyOverride,
] {
assert_eq!(
IssueKind::from_discriminant(kind.to_discriminant()),
Some(kind)
);
}
assert_eq!(IssueKind::from_discriminant(0), None);
assert_eq!(IssueKind::from_discriminant(27), None);
}
#[test]
fn discriminant_values_are_unique() {
let all_kinds = [
IssueKind::UnusedFile,
IssueKind::UnusedExport,
IssueKind::UnusedType,
IssueKind::PrivateTypeLeak,
IssueKind::UnusedDependency,
IssueKind::UnusedDevDependency,
IssueKind::UnusedEnumMember,
IssueKind::UnusedClassMember,
IssueKind::UnresolvedImport,
IssueKind::UnlistedDependency,
IssueKind::DuplicateExport,
IssueKind::CodeDuplication,
IssueKind::CircularDependency,
IssueKind::ReExportCycle,
IssueKind::TypeOnlyDependency,
IssueKind::TestOnlyDependency,
IssueKind::BoundaryViolation,
IssueKind::CoverageGaps,
IssueKind::FeatureFlag,
IssueKind::Complexity,
IssueKind::StaleSuppression,
IssueKind::PnpmCatalogEntry,
IssueKind::EmptyCatalogGroup,
IssueKind::UnresolvedCatalogReference,
IssueKind::UnusedDependencyOverride,
IssueKind::MisconfiguredDependencyOverride,
];
let discriminants: Vec<u8> = all_kinds.iter().map(|k| k.to_discriminant()).collect();
let mut sorted = discriminants.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(
discriminants.len(),
sorted.len(),
"discriminant values must be unique"
);
}
#[test]
fn discriminant_starts_at_one() {
assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
}
#[test]
fn suppression_line_zero_is_file_wide() {
let s = Suppression {
line: 0,
comment_line: 1,
kind: None,
};
assert_eq!(s.line, 0);
assert!(s.kind.is_none());
}
#[test]
fn suppression_with_specific_kind_and_line() {
let s = Suppression {
line: 42,
comment_line: 41,
kind: Some(IssueKind::UnusedExport),
};
assert_eq!(s.line, 42);
assert_eq!(s.comment_line, 41);
assert_eq!(s.kind, Some(IssueKind::UnusedExport));
}
#[test]
fn known_issue_kind_names_parses_each_entry() {
for &name in KNOWN_ISSUE_KIND_NAMES {
assert!(
IssueKind::parse(name).is_some(),
"KNOWN_ISSUE_KIND_NAMES contains '{name}' but IssueKind::parse rejects it"
);
}
}
#[test]
fn closest_known_kind_name_finds_near_misses() {
assert_eq!(
closest_known_kind_name("unused-exports"),
Some("unused-export")
);
assert_eq!(closest_known_kind_name("unused-files"), Some("unused-file"));
assert_eq!(closest_known_kind_name("complxity"), Some("complexity"));
}
#[test]
fn closest_known_kind_name_rejects_novel_strings() {
assert_eq!(closest_known_kind_name("xyzzy"), None);
assert_eq!(closest_known_kind_name("foo"), None);
assert_eq!(closest_known_kind_name(""), None);
}
#[test]
fn closest_known_kind_name_skips_exact_match() {
assert_eq!(closest_known_kind_name("unused-export"), None);
}
}