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/// Whether a `// slopguard-disable-next-line` directive suppresses `rule_id`
31/// on 1-based `line` of `source`. A directive with no rule id suppresses every
32/// rule; one naming a rule suppresses only that rule. Mirrors the filtering in
33/// [`filter_disabled`] for callers that hold a position rather than a finding
34/// (the `--fix` pass).
35pub fn is_disabled(source: &str, line: usize, rule_id: &RuleId) -> bool {
36    parse_directives(source)
37        .iter()
38        .any(|d| d.target_line == line && d.rule_id.as_ref().is_none_or(|id| id == rule_id))
39}
40
41/// Filter out findings that are suppressed by `// slopguard-disable-next-line`
42/// comments in the source code.
43pub fn filter_disabled(findings: Vec<Finding>, source: &str) -> Vec<Finding> {
44    let directives = parse_directives(source);
45    if directives.is_empty() {
46        return findings;
47    }
48    findings
49        .into_iter()
50        .filter(|finding| {
51            !directives.iter().any(|d| {
52                d.target_line == finding.line
53                    && d.rule_id.as_ref().is_none_or(|id| *id == finding.rule_id)
54            })
55        })
56        .collect()
57}
58
59#[cfg(test)]
60mod tests {
61    use std::path::PathBuf;
62
63    use crate::finding::Finding;
64    use crate::rule::{Category, RuleId, Severity};
65
66    use super::filter_disabled;
67
68    fn make_finding(rule_id: &str, line: usize) -> Finding {
69        Finding {
70            rule_id: RuleId::from(rule_id),
71            severity: Severity::Error,
72            category: Category::Correctness,
73            message: "test".to_string(),
74            note: None,
75            fix: None,
76            file: PathBuf::from("test.rs"),
77            line,
78            column: 1,
79            end_line: line,
80            end_column: 10,
81            matched_text: "test".to_string(),
82            confidence: None,
83            escalated: false,
84        }
85    }
86
87    #[test]
88    fn disable_without_rule_id_suppresses_all_findings() {
89        let source = "fn main() {\n    // slopguard-disable-next-line\n    foo().unwrap();\n}\n";
90        let findings = vec![
91            make_finding("no-unwrap-in-prod", 3),
92            make_finding("no-expect-in-prod", 3),
93        ];
94        let result = filter_disabled(findings, source);
95        assert!(
96            result.is_empty(),
97            "all findings on the disabled line should be suppressed"
98        );
99    }
100
101    #[test]
102    fn disable_with_rule_id_suppresses_only_that_rule() {
103        let source = "fn main() {\n    // slopguard-disable-next-line no-unwrap-in-prod\n    foo().unwrap();\n}\n";
104        let findings = vec![
105            make_finding("no-unwrap-in-prod", 3),
106            make_finding("other-rule", 3),
107        ];
108        let result = filter_disabled(findings, source);
109        assert_eq!(result.len(), 1);
110        assert_eq!(result[0].rule_id, RuleId::from("other-rule"));
111    }
112
113    #[test]
114    fn disable_with_unknown_rule_id_suppresses_nothing() {
115        let source = "fn main() {\n    // slopguard-disable-next-line nonexistent-rule\n    foo().unwrap();\n}\n";
116        let findings = vec![make_finding("no-unwrap-in-prod", 3)];
117        let result = filter_disabled(findings, source);
118        assert_eq!(result.len(), 1);
119        assert_eq!(result[0].rule_id, RuleId::from("no-unwrap-in-prod"));
120    }
121
122    #[test]
123    fn disable_on_wrong_line_suppresses_nothing() {
124        let source = "// slopguard-disable-next-line\nfn main() {\n    foo().unwrap();\n}\n";
125        let findings = vec![make_finding("no-unwrap-in-prod", 3)];
126        let result = filter_disabled(findings, source);
127        assert_eq!(result.len(), 1);
128    }
129
130    #[test]
131    fn disable_with_spacing_variants() {
132        let source_no_space =
133            "fn main() {\n    //slopguard-disable-next-line\n    foo().unwrap();\n}\n";
134        let findings1 = vec![make_finding("no-unwrap-in-prod", 3)];
135        assert!(filter_disabled(findings1, source_no_space).is_empty());
136
137        let source_extra_spaces =
138            "fn main() {\n    //  slopguard-disable-next-line\n    foo().unwrap();\n}\n";
139        let findings2 = vec![make_finding("no-unwrap-in-prod", 3)];
140        assert!(filter_disabled(findings2, source_extra_spaces).is_empty());
141
142        let source_leading_space =
143            "fn main() {\n      // slopguard-disable-next-line\n    foo().unwrap();\n}\n";
144        let findings3 = vec![make_finding("no-unwrap-in-prod", 3)];
145        assert!(filter_disabled(findings3, source_leading_space).is_empty());
146    }
147
148    #[test]
149    fn disable_without_rule_id_multiple_findings_same_line() {
150        let source = "fn main() {\n    // slopguard-disable-next-line\n    foo().unwrap();\n}\n";
151        let findings = vec![
152            make_finding("rule-a", 3),
153            make_finding("rule-b", 3),
154            make_finding("rule-c", 3),
155        ];
156        let result = filter_disabled(findings, source);
157        assert!(result.is_empty());
158    }
159
160    #[test]
161    fn disable_with_rule_id_multiple_findings_same_line() {
162        let source =
163            "fn main() {\n    // slopguard-disable-next-line rule-b\n    foo().unwrap();\n}\n";
164        let findings = vec![
165            make_finding("rule-a", 3),
166            make_finding("rule-b", 3),
167            make_finding("rule-c", 3),
168        ];
169        let result = filter_disabled(findings, source);
170        assert_eq!(result.len(), 2);
171        assert_eq!(result[0].rule_id, RuleId::from("rule-a"));
172        assert_eq!(result[1].rule_id, RuleId::from("rule-c"));
173    }
174
175    #[test]
176    fn no_disable_comments_returns_all_findings() {
177        let source = "fn main() {\n    foo().unwrap();\n}\n";
178        let findings = vec![make_finding("no-unwrap-in-prod", 2)];
179        let result = filter_disabled(findings, source);
180        assert_eq!(result.len(), 1);
181    }
182
183    #[test]
184    fn disable_on_last_line_no_panic() {
185        let source = "fn main() {}\n// slopguard-disable-next-line";
186        let findings: Vec<Finding> = vec![];
187        let result = filter_disabled(findings, source);
188        assert!(result.is_empty());
189    }
190}