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///
5/// # Examples
6///
7/// ```
8/// use fallow_types::suppress::IssueKind;
9///
10/// let kind = IssueKind::parse("unused-export");
11/// assert_eq!(kind, Some(IssueKind::UnusedExport));
12///
13/// // Round-trip through discriminant
14/// let d = IssueKind::UnusedFile.to_discriminant();
15/// assert_eq!(IssueKind::from_discriminant(d), Some(IssueKind::UnusedFile));
16///
17/// // Unknown strings return None
18/// assert_eq!(IssueKind::parse("not-a-kind"), None);
19/// ```
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum IssueKind {
22    /// An unused file.
23    UnusedFile,
24    /// An unused export.
25    UnusedExport,
26    /// An unused type export.
27    UnusedType,
28    /// An unused dependency.
29    UnusedDependency,
30    /// An unused dev dependency.
31    UnusedDevDependency,
32    /// An unused enum member.
33    UnusedEnumMember,
34    /// An unused class member.
35    UnusedClassMember,
36    /// An unresolved import.
37    UnresolvedImport,
38    /// An unlisted dependency.
39    UnlistedDependency,
40    /// A duplicate export name across modules.
41    DuplicateExport,
42    /// Code duplication.
43    CodeDuplication,
44    /// A circular dependency chain.
45    CircularDependency,
46}
47
48impl IssueKind {
49    /// Parse an issue kind from the string tokens used in CLI output and suppression comments.
50    #[must_use]
51    pub fn parse(s: &str) -> Option<Self> {
52        match s {
53            "unused-file" => Some(Self::UnusedFile),
54            "unused-export" => Some(Self::UnusedExport),
55            "unused-type" => Some(Self::UnusedType),
56            "unused-dependency" => Some(Self::UnusedDependency),
57            "unused-dev-dependency" => Some(Self::UnusedDevDependency),
58            "unused-enum-member" => Some(Self::UnusedEnumMember),
59            "unused-class-member" => Some(Self::UnusedClassMember),
60            "unresolved-import" => Some(Self::UnresolvedImport),
61            "unlisted-dependency" => Some(Self::UnlistedDependency),
62            "duplicate-export" => Some(Self::DuplicateExport),
63            "code-duplication" => Some(Self::CodeDuplication),
64            "circular-dependency" => Some(Self::CircularDependency),
65            _ => None,
66        }
67    }
68
69    /// Convert to a u8 discriminant for compact cache storage.
70    #[must_use]
71    pub const fn to_discriminant(self) -> u8 {
72        match self {
73            Self::UnusedFile => 1,
74            Self::UnusedExport => 2,
75            Self::UnusedType => 3,
76            Self::UnusedDependency => 4,
77            Self::UnusedDevDependency => 5,
78            Self::UnusedEnumMember => 6,
79            Self::UnusedClassMember => 7,
80            Self::UnresolvedImport => 8,
81            Self::UnlistedDependency => 9,
82            Self::DuplicateExport => 10,
83            Self::CodeDuplication => 11,
84            Self::CircularDependency => 12,
85        }
86    }
87
88    /// Reconstruct from a cache discriminant.
89    #[must_use]
90    pub const fn from_discriminant(d: u8) -> Option<Self> {
91        match d {
92            1 => Some(Self::UnusedFile),
93            2 => Some(Self::UnusedExport),
94            3 => Some(Self::UnusedType),
95            4 => Some(Self::UnusedDependency),
96            5 => Some(Self::UnusedDevDependency),
97            6 => Some(Self::UnusedEnumMember),
98            7 => Some(Self::UnusedClassMember),
99            8 => Some(Self::UnresolvedImport),
100            9 => Some(Self::UnlistedDependency),
101            10 => Some(Self::DuplicateExport),
102            11 => Some(Self::CodeDuplication),
103            12 => Some(Self::CircularDependency),
104            _ => None,
105        }
106    }
107}
108
109/// A suppression directive parsed from a source comment.
110///
111/// # Examples
112///
113/// ```
114/// use fallow_types::suppress::{Suppression, IssueKind};
115///
116/// // File-wide suppression (line 0, no specific kind)
117/// let file_wide = Suppression { line: 0, kind: None };
118/// assert_eq!(file_wide.line, 0);
119///
120/// // Line-specific suppression for unused exports
121/// let line_suppress = Suppression {
122///     line: 42,
123///     kind: Some(IssueKind::UnusedExport),
124/// };
125/// assert_eq!(line_suppress.kind, Some(IssueKind::UnusedExport));
126/// ```
127#[derive(Debug, Clone)]
128pub struct Suppression {
129    /// 1-based line this suppression applies to. 0 = file-wide suppression.
130    pub line: u32,
131    /// None = suppress all issue kinds on this line.
132    pub kind: Option<IssueKind>,
133}
134
135// Size assertions to prevent memory regressions.
136// `Suppression` is stored in a Vec per file; `IssueKind` appears in every suppression.
137const _: () = assert!(std::mem::size_of::<Suppression>() == 8);
138const _: () = assert!(std::mem::size_of::<IssueKind>() == 1);
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn issue_kind_from_str_all_variants() {
146        assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
147        assert_eq!(
148            IssueKind::parse("unused-export"),
149            Some(IssueKind::UnusedExport)
150        );
151        assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
152        assert_eq!(
153            IssueKind::parse("unused-dependency"),
154            Some(IssueKind::UnusedDependency)
155        );
156        assert_eq!(
157            IssueKind::parse("unused-dev-dependency"),
158            Some(IssueKind::UnusedDevDependency)
159        );
160        assert_eq!(
161            IssueKind::parse("unused-enum-member"),
162            Some(IssueKind::UnusedEnumMember)
163        );
164        assert_eq!(
165            IssueKind::parse("unused-class-member"),
166            Some(IssueKind::UnusedClassMember)
167        );
168        assert_eq!(
169            IssueKind::parse("unresolved-import"),
170            Some(IssueKind::UnresolvedImport)
171        );
172        assert_eq!(
173            IssueKind::parse("unlisted-dependency"),
174            Some(IssueKind::UnlistedDependency)
175        );
176        assert_eq!(
177            IssueKind::parse("duplicate-export"),
178            Some(IssueKind::DuplicateExport)
179        );
180        assert_eq!(
181            IssueKind::parse("code-duplication"),
182            Some(IssueKind::CodeDuplication)
183        );
184        assert_eq!(
185            IssueKind::parse("circular-dependency"),
186            Some(IssueKind::CircularDependency)
187        );
188    }
189
190    #[test]
191    fn issue_kind_from_str_unknown() {
192        assert_eq!(IssueKind::parse("foo"), None);
193        assert_eq!(IssueKind::parse(""), None);
194    }
195
196    #[test]
197    fn issue_kind_from_str_near_misses() {
198        // Case sensitivity — these should NOT match
199        assert_eq!(IssueKind::parse("Unused-File"), None);
200        assert_eq!(IssueKind::parse("UNUSED-EXPORT"), None);
201        // Typos / near-misses
202        assert_eq!(IssueKind::parse("unused_file"), None);
203        assert_eq!(IssueKind::parse("unused-files"), None);
204    }
205
206    #[test]
207    fn discriminant_out_of_range() {
208        assert_eq!(IssueKind::from_discriminant(0), None);
209        assert_eq!(IssueKind::from_discriminant(13), None);
210        assert_eq!(IssueKind::from_discriminant(u8::MAX), None);
211    }
212
213    #[test]
214    fn discriminant_roundtrip() {
215        for kind in [
216            IssueKind::UnusedFile,
217            IssueKind::UnusedExport,
218            IssueKind::UnusedType,
219            IssueKind::UnusedDependency,
220            IssueKind::UnusedDevDependency,
221            IssueKind::UnusedEnumMember,
222            IssueKind::UnusedClassMember,
223            IssueKind::UnresolvedImport,
224            IssueKind::UnlistedDependency,
225            IssueKind::DuplicateExport,
226            IssueKind::CodeDuplication,
227            IssueKind::CircularDependency,
228        ] {
229            assert_eq!(
230                IssueKind::from_discriminant(kind.to_discriminant()),
231                Some(kind)
232            );
233        }
234        assert_eq!(IssueKind::from_discriminant(0), None);
235        assert_eq!(IssueKind::from_discriminant(13), None);
236    }
237
238    // ── Discriminant uniqueness ─────────────────────────────────
239
240    #[test]
241    fn discriminant_values_are_unique() {
242        let all_kinds = [
243            IssueKind::UnusedFile,
244            IssueKind::UnusedExport,
245            IssueKind::UnusedType,
246            IssueKind::UnusedDependency,
247            IssueKind::UnusedDevDependency,
248            IssueKind::UnusedEnumMember,
249            IssueKind::UnusedClassMember,
250            IssueKind::UnresolvedImport,
251            IssueKind::UnlistedDependency,
252            IssueKind::DuplicateExport,
253            IssueKind::CodeDuplication,
254            IssueKind::CircularDependency,
255        ];
256        let discriminants: Vec<u8> = all_kinds.iter().map(|k| k.to_discriminant()).collect();
257        let mut sorted = discriminants.clone();
258        sorted.sort_unstable();
259        sorted.dedup();
260        assert_eq!(
261            discriminants.len(),
262            sorted.len(),
263            "discriminant values must be unique"
264        );
265    }
266
267    // ── Discriminant starts at 1 ────────────────────────────────
268
269    #[test]
270    fn discriminant_starts_at_one() {
271        assert_eq!(IssueKind::UnusedFile.to_discriminant(), 1);
272    }
273
274    // ── Suppression struct ──────────────────────────────────────
275
276    #[test]
277    fn suppression_line_zero_is_file_wide() {
278        let s = Suppression {
279            line: 0,
280            kind: None,
281        };
282        assert_eq!(s.line, 0);
283        assert!(s.kind.is_none());
284    }
285
286    #[test]
287    fn suppression_with_specific_kind_and_line() {
288        let s = Suppression {
289            line: 42,
290            kind: Some(IssueKind::UnusedExport),
291        };
292        assert_eq!(s.line, 42);
293        assert_eq!(s.kind, Some(IssueKind::UnusedExport));
294    }
295}