Skip to main content

fallow_types/
suppress.rs

1//! Inline suppression comment types and issue kind definitions.
2
3/// Issue kind for suppression matching.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum IssueKind {
6    /// An unused file.
7    UnusedFile,
8    /// An unused export.
9    UnusedExport,
10    /// An unused type export.
11    UnusedType,
12    /// An unused dependency.
13    UnusedDependency,
14    /// An unused dev dependency.
15    UnusedDevDependency,
16    /// An unused enum member.
17    UnusedEnumMember,
18    /// An unused class member.
19    UnusedClassMember,
20    /// An unresolved import.
21    UnresolvedImport,
22    /// An unlisted dependency.
23    UnlistedDependency,
24    /// A duplicate export name across modules.
25    DuplicateExport,
26    /// Code duplication.
27    CodeDuplication,
28    /// A circular dependency chain.
29    CircularDependency,
30}
31
32impl IssueKind {
33    /// Parse an issue kind from the string tokens used in CLI output and suppression comments.
34    #[must_use]
35    pub fn parse(s: &str) -> Option<Self> {
36        match s {
37            "unused-file" => Some(Self::UnusedFile),
38            "unused-export" => Some(Self::UnusedExport),
39            "unused-type" => Some(Self::UnusedType),
40            "unused-dependency" => Some(Self::UnusedDependency),
41            "unused-dev-dependency" => Some(Self::UnusedDevDependency),
42            "unused-enum-member" => Some(Self::UnusedEnumMember),
43            "unused-class-member" => Some(Self::UnusedClassMember),
44            "unresolved-import" => Some(Self::UnresolvedImport),
45            "unlisted-dependency" => Some(Self::UnlistedDependency),
46            "duplicate-export" => Some(Self::DuplicateExport),
47            "code-duplication" => Some(Self::CodeDuplication),
48            "circular-dependency" => Some(Self::CircularDependency),
49            _ => None,
50        }
51    }
52
53    /// Convert to a u8 discriminant for compact cache storage.
54    #[must_use]
55    pub const fn to_discriminant(self) -> u8 {
56        match self {
57            Self::UnusedFile => 1,
58            Self::UnusedExport => 2,
59            Self::UnusedType => 3,
60            Self::UnusedDependency => 4,
61            Self::UnusedDevDependency => 5,
62            Self::UnusedEnumMember => 6,
63            Self::UnusedClassMember => 7,
64            Self::UnresolvedImport => 8,
65            Self::UnlistedDependency => 9,
66            Self::DuplicateExport => 10,
67            Self::CodeDuplication => 11,
68            Self::CircularDependency => 12,
69        }
70    }
71
72    /// Reconstruct from a cache discriminant.
73    #[must_use]
74    pub const fn from_discriminant(d: u8) -> Option<Self> {
75        match d {
76            1 => Some(Self::UnusedFile),
77            2 => Some(Self::UnusedExport),
78            3 => Some(Self::UnusedType),
79            4 => Some(Self::UnusedDependency),
80            5 => Some(Self::UnusedDevDependency),
81            6 => Some(Self::UnusedEnumMember),
82            7 => Some(Self::UnusedClassMember),
83            8 => Some(Self::UnresolvedImport),
84            9 => Some(Self::UnlistedDependency),
85            10 => Some(Self::DuplicateExport),
86            11 => Some(Self::CodeDuplication),
87            12 => Some(Self::CircularDependency),
88            _ => None,
89        }
90    }
91}
92
93/// A suppression directive parsed from a source comment.
94#[derive(Debug, Clone)]
95pub struct Suppression {
96    /// 1-based line this suppression applies to. 0 = file-wide suppression.
97    pub line: u32,
98    /// None = suppress all issue kinds on this line.
99    pub kind: Option<IssueKind>,
100}
101
102// Size assertions to prevent memory regressions.
103// `Suppression` is stored in a Vec per file; `IssueKind` appears in every suppression.
104const _: () = assert!(std::mem::size_of::<Suppression>() == 8);
105const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn issue_kind_from_str_all_variants() {
113        assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
114        assert_eq!(
115            IssueKind::parse("unused-export"),
116            Some(IssueKind::UnusedExport)
117        );
118        assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
119        assert_eq!(
120            IssueKind::parse("unused-dependency"),
121            Some(IssueKind::UnusedDependency)
122        );
123        assert_eq!(
124            IssueKind::parse("unused-dev-dependency"),
125            Some(IssueKind::UnusedDevDependency)
126        );
127        assert_eq!(
128            IssueKind::parse("unused-enum-member"),
129            Some(IssueKind::UnusedEnumMember)
130        );
131        assert_eq!(
132            IssueKind::parse("unused-class-member"),
133            Some(IssueKind::UnusedClassMember)
134        );
135        assert_eq!(
136            IssueKind::parse("unresolved-import"),
137            Some(IssueKind::UnresolvedImport)
138        );
139        assert_eq!(
140            IssueKind::parse("unlisted-dependency"),
141            Some(IssueKind::UnlistedDependency)
142        );
143        assert_eq!(
144            IssueKind::parse("duplicate-export"),
145            Some(IssueKind::DuplicateExport)
146        );
147        assert_eq!(
148            IssueKind::parse("code-duplication"),
149            Some(IssueKind::CodeDuplication)
150        );
151        assert_eq!(
152            IssueKind::parse("circular-dependency"),
153            Some(IssueKind::CircularDependency)
154        );
155    }
156
157    #[test]
158    fn issue_kind_from_str_unknown() {
159        assert_eq!(IssueKind::parse("foo"), None);
160        assert_eq!(IssueKind::parse(""), None);
161    }
162
163    #[test]
164    fn issue_kind_from_str_near_misses() {
165        // Case sensitivity — these should NOT match
166        assert_eq!(IssueKind::parse("Unused-File"), None);
167        assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
168        // Typos / near-misses
169        assert_eq!(IssueKind::parse("unused_file"), None);
170        assert_eq!(IssueKind::parse("unused-files"), None);
171    }
172
173    #[test]
174    fn discriminant_out_of_range() {
175        assert_eq!(IssueKind::from_discriminant(0), None);
176        assert_eq!(IssueKind::from_discriminant(13), None);
177        assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
178    }
179
180    #[test]
181    fn discriminant_roundtrip() {
182        for kind in [
183            IssueKind::UnusedFile,
184            IssueKind::UnusedExport,
185            IssueKind::UnusedType,
186            IssueKind::UnusedDependency,
187            IssueKind::UnusedDevDependency,
188            IssueKind::UnusedEnumMember,
189            IssueKind::UnusedClassMember,
190            IssueKind::UnresolvedImport,
191            IssueKind::UnlistedDependency,
192            IssueKind::DuplicateExport,
193            IssueKind::CodeDuplication,
194            IssueKind::CircularDependency,
195        ] {
196            assert_eq!(
197                IssueKind::from_discriminant(kind.to_discriminant()),
198                Some(kind)
199            );
200        }
201        assert_eq!(IssueKind::from_discriminant(0), None);
202        assert_eq!(IssueKind::from_discriminant(13), None);
203    }
204}