slopguard_core/
disable.rs1use 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
30pub 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
41pub 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}