#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IssueKind {
UnusedFile,
UnusedExport,
UnusedType,
UnusedDependency,
UnusedDevDependency,
UnusedEnumMember,
UnusedClassMember,
UnresolvedImport,
UnlistedDependency,
DuplicateExport,
CodeDuplication,
CircularDependency,
TypeOnlyDependency,
TestOnlyDependency,
BoundaryViolation,
CoverageGaps,
FeatureFlag,
Complexity,
StaleSuppression,
}
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),
"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" => Some(Self::CircularDependency),
"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),
_ => None,
}
}
#[must_use]
pub const fn to_discriminant(self) -> u8 {
match self {
Self::UnusedFile => 1,
Self::UnusedExport => 2,
Self::UnusedType => 3,
Self::UnusedDependency => 4,
Self::UnusedDevDependency => 5,
Self::UnusedEnumMember => 6,
Self::UnusedClassMember => 7,
Self::UnresolvedImport => 8,
Self::UnlistedDependency => 9,
Self::DuplicateExport => 10,
Self::CodeDuplication => 11,
Self::CircularDependency => 12,
Self::TypeOnlyDependency => 13,
Self::TestOnlyDependency => 14,
Self::BoundaryViolation => 15,
Self::CoverageGaps => 16,
Self::FeatureFlag => 17,
Self::Complexity => 18,
Self::StaleSuppression => 19,
}
}
#[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::UnusedDependency),
5 => Some(Self::UnusedDevDependency),
6 => Some(Self::UnusedEnumMember),
7 => Some(Self::UnusedClassMember),
8 => Some(Self::UnresolvedImport),
9 => Some(Self::UnlistedDependency),
10 => Some(Self::DuplicateExport),
11 => Some(Self::CodeDuplication),
12 => Some(Self::CircularDependency),
13 => Some(Self::TypeOnlyDependency),
14 => Some(Self::TestOnlyDependency),
15 => Some(Self::BoundaryViolation),
16 => Some(Self::CoverageGaps),
17 => Some(Self::FeatureFlag),
18 => Some(Self::Complexity),
19 => Some(Self::StaleSuppression),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Suppression {
pub line: u32,
pub comment_line: u32,
pub kind: Option<IssueKind>,
}
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("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("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)
);
}
#[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(20), None);
assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
}
#[test]
fn discriminant_roundtrip() {
for kind in [
IssueKind::UnusedFile,
IssueKind::UnusedExport,
IssueKind::UnusedType,
IssueKind::UnusedDependency,
IssueKind::UnusedDevDependency,
IssueKind::UnusedEnumMember,
IssueKind::UnusedClassMember,
IssueKind::UnresolvedImport,
IssueKind::UnlistedDependency,
IssueKind::DuplicateExport,
IssueKind::CodeDuplication,
IssueKind::CircularDependency,
IssueKind::TypeOnlyDependency,
IssueKind::TestOnlyDependency,
IssueKind::BoundaryViolation,
IssueKind::CoverageGaps,
IssueKind::FeatureFlag,
IssueKind::Complexity,
IssueKind::StaleSuppression,
] {
assert_eq!(
IssueKind::from_discriminant(kind.to_discriminant()),
Some(kind)
);
}
assert_eq!(IssueKind::from_discriminant(0), None);
assert_eq!(IssueKind::from_discriminant(20), None);
}
#[test]
fn discriminant_values_are_unique() {
let all_kinds = [
IssueKind::UnusedFile,
IssueKind::UnusedExport,
IssueKind::UnusedType,
IssueKind::UnusedDependency,
IssueKind::UnusedDevDependency,
IssueKind::UnusedEnumMember,
IssueKind::UnusedClassMember,
IssueKind::UnresolvedImport,
IssueKind::UnlistedDependency,
IssueKind::DuplicateExport,
IssueKind::CodeDuplication,
IssueKind::CircularDependency,
IssueKind::TypeOnlyDependency,
IssueKind::TestOnlyDependency,
IssueKind::BoundaryViolation,
IssueKind::CoverageGaps,
IssueKind::FeatureFlag,
IssueKind::Complexity,
IssueKind::StaleSuppression,
];
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));
}
}