Skip to main content

slopguard_core/
disable.rs

1use crate::finding::Finding;
2use crate::rule::RuleId;
3
4const DISABLE_MARKER: &str = "slopguard-disable-next-line";
5
6struct DisableDirective {
7    target_line: usize,
8    rule_id: Option<RuleId>,
9}
10
11fn parse_directives(source: &str) -> Vec<DisableDirective> {
12    source
13        .lines()
14        .enumerate()
15        .filter_map(|(idx, line)| {
16            let rest = line
17                .trim()
18                .strip_prefix("//")?
19                .trim()
20                .strip_prefix(DISABLE_MARKER)?;
21            let rest = rest.trim();
22            Some(DisableDirective {
23                target_line: idx + 2,
24                rule_id: (!rest.is_empty()).then(|| RuleId::from(rest)),
25            })
26        })
27        .collect()
28}
29
30/// Filter out findings that are suppressed by `// slopguard-disable-next-line`
31/// comments in the source code.
32pub fn filter_disabled(findings: Vec<Finding>, source: &str) -> Vec<Finding> {
33    let directives = parse_directives(source);
34    if directives.is_empty() {
35        return findings;
36    }
37    findings
38        .into_iter()
39        .filter(|finding| {
40            !directives.iter().any(|d| {
41                d.target_line == finding.line
42                    && d.rule_id.as_ref().is_none_or(|id| *id == finding.rule_id)
43            })
44        })
45        .collect()
46}
47
48#[cfg(test)]
49mod tests {
50    use std::path::PathBuf;
51
52    use crate::finding::Finding;
53    use crate::rule::{Category, RuleId, Severity};
54
55    use super::filter_disabled;
56
57    fn make_finding(rule_id: &str, line: usize) -> Finding {
58        Finding {
59            rule_id: RuleId::from(rule_id),
60            severity: Severity::Error,
61            category: Category::Correctness,
62            message: "test".to_string(),
63            note: None,
64            fix: None,
65            file: PathBuf::from("test.rs"),
66            line,
67            column: 1,
68            end_line: line,
69            end_column: 10,
70            matched_text: "test".to_string(),
71            confidence: None,
72            escalated: false,
73        }
74    }
75
76    #[test]
77    fn disable_without_rule_id_suppresses_all_findings() {
78        let source = "fn main() {\n    // slopguard-disable-next-line\n    foo().unwrap();\n}\n";
79        let findings = vec![
80            make_finding("no-unwrap-in-prod", 3),
81            make_finding("no-expect-in-prod", 3),
82        ];
83        let result = filter_disabled(findings, source);
84        assert!(
85            result.is_empty(),
86            "all findings on the disabled line should be suppressed"
87        );
88    }
89
90    #[test]
91    fn disable_with_rule_id_suppresses_only_that_rule() {
92        let source = "fn main() {\n    // slopguard-disable-next-line no-unwrap-in-prod\n    foo().unwrap();\n}\n";
93        let findings = vec![
94            make_finding("no-unwrap-in-prod", 3),
95            make_finding("other-rule", 3),
96        ];
97        let result = filter_disabled(findings, source);
98        assert_eq!(result.len(), 1);
99        assert_eq!(result[0].rule_id, RuleId::from("other-rule"));
100    }
101
102    #[test]
103    fn disable_with_unknown_rule_id_suppresses_nothing() {
104        let source = "fn main() {\n    // slopguard-disable-next-line nonexistent-rule\n    foo().unwrap();\n}\n";
105        let findings = vec![make_finding("no-unwrap-in-prod", 3)];
106        let result = filter_disabled(findings, source);
107        assert_eq!(result.len(), 1);
108        assert_eq!(result[0].rule_id, RuleId::from("no-unwrap-in-prod"));
109    }
110
111    #[test]
112    fn disable_on_wrong_line_suppresses_nothing() {
113        let source = "// slopguard-disable-next-line\nfn main() {\n    foo().unwrap();\n}\n";
114        let findings = vec![make_finding("no-unwrap-in-prod", 3)];
115        let result = filter_disabled(findings, source);
116        assert_eq!(result.len(), 1);
117    }
118
119    #[test]
120    fn disable_with_spacing_variants() {
121        let source_no_space =
122            "fn main() {\n    //slopguard-disable-next-line\n    foo().unwrap();\n}\n";
123        let findings1 = vec![make_finding("no-unwrap-in-prod", 3)];
124        assert!(filter_disabled(findings1, source_no_space).is_empty());
125
126        let source_extra_spaces =
127            "fn main() {\n    //  slopguard-disable-next-line\n    foo().unwrap();\n}\n";
128        let findings2 = vec![make_finding("no-unwrap-in-prod", 3)];
129        assert!(filter_disabled(findings2, source_extra_spaces).is_empty());
130
131        let source_leading_space =
132            "fn main() {\n      // slopguard-disable-next-line\n    foo().unwrap();\n}\n";
133        let findings3 = vec![make_finding("no-unwrap-in-prod", 3)];
134        assert!(filter_disabled(findings3, source_leading_space).is_empty());
135    }
136
137    #[test]
138    fn disable_without_rule_id_multiple_findings_same_line() {
139        let source = "fn main() {\n    // slopguard-disable-next-line\n    foo().unwrap();\n}\n";
140        let findings = vec![
141            make_finding("rule-a", 3),
142            make_finding("rule-b", 3),
143            make_finding("rule-c", 3),
144        ];
145        let result = filter_disabled(findings, source);
146        assert!(result.is_empty());
147    }
148
149    #[test]
150    fn disable_with_rule_id_multiple_findings_same_line() {
151        let source =
152            "fn main() {\n    // slopguard-disable-next-line rule-b\n    foo().unwrap();\n}\n";
153        let findings = vec![
154            make_finding("rule-a", 3),
155            make_finding("rule-b", 3),
156            make_finding("rule-c", 3),
157        ];
158        let result = filter_disabled(findings, source);
159        assert_eq!(result.len(), 2);
160        assert_eq!(result[0].rule_id, RuleId::from("rule-a"));
161        assert_eq!(result[1].rule_id, RuleId::from("rule-c"));
162    }
163
164    #[test]
165    fn no_disable_comments_returns_all_findings() {
166        let source = "fn main() {\n    foo().unwrap();\n}\n";
167        let findings = vec![make_finding("no-unwrap-in-prod", 2)];
168        let result = filter_disabled(findings, source);
169        assert_eq!(result.len(), 1);
170    }
171
172    #[test]
173    fn disable_on_last_line_no_panic() {
174        let source = "fn main() {}\n// slopguard-disable-next-line";
175        let findings: Vec<Finding> = vec![];
176        let result = filter_disabled(findings, source);
177        assert!(result.is_empty());
178    }
179}