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 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 }
73 }
74
75 #[test]
76 fn disable_without_rule_id_suppresses_all_findings() {
77 let source = "fn main() {\n // slopguard-disable-next-line\n foo().unwrap();\n}\n";
78 let findings = vec![
79 make_finding("no-unwrap-in-prod", 3),
80 make_finding("no-expect-in-prod", 3),
81 ];
82 let result = filter_disabled(findings, source);
83 assert!(
84 result.is_empty(),
85 "all findings on the disabled line should be suppressed"
86 );
87 }
88
89 #[test]
90 fn disable_with_rule_id_suppresses_only_that_rule() {
91 let source = "fn main() {\n // slopguard-disable-next-line no-unwrap-in-prod\n foo().unwrap();\n}\n";
92 let findings = vec![
93 make_finding("no-unwrap-in-prod", 3),
94 make_finding("other-rule", 3),
95 ];
96 let result = filter_disabled(findings, source);
97 assert_eq!(result.len(), 1);
98 assert_eq!(result[0].rule_id, RuleId::from("other-rule"));
99 }
100
101 #[test]
102 fn disable_with_unknown_rule_id_suppresses_nothing() {
103 let source = "fn main() {\n // slopguard-disable-next-line nonexistent-rule\n foo().unwrap();\n}\n";
104 let findings = vec![make_finding("no-unwrap-in-prod", 3)];
105 let result = filter_disabled(findings, source);
106 assert_eq!(result.len(), 1);
107 assert_eq!(result[0].rule_id, RuleId::from("no-unwrap-in-prod"));
108 }
109
110 #[test]
111 fn disable_on_wrong_line_suppresses_nothing() {
112 let source = "// slopguard-disable-next-line\nfn main() {\n foo().unwrap();\n}\n";
113 let findings = vec![make_finding("no-unwrap-in-prod", 3)];
114 let result = filter_disabled(findings, source);
115 assert_eq!(result.len(), 1);
116 }
117
118 #[test]
119 fn disable_with_spacing_variants() {
120 let source_no_space =
121 "fn main() {\n //slopguard-disable-next-line\n foo().unwrap();\n}\n";
122 let findings1 = vec![make_finding("no-unwrap-in-prod", 3)];
123 assert!(filter_disabled(findings1, source_no_space).is_empty());
124
125 let source_extra_spaces =
126 "fn main() {\n // slopguard-disable-next-line\n foo().unwrap();\n}\n";
127 let findings2 = vec![make_finding("no-unwrap-in-prod", 3)];
128 assert!(filter_disabled(findings2, source_extra_spaces).is_empty());
129
130 let source_leading_space =
131 "fn main() {\n // slopguard-disable-next-line\n foo().unwrap();\n}\n";
132 let findings3 = vec![make_finding("no-unwrap-in-prod", 3)];
133 assert!(filter_disabled(findings3, source_leading_space).is_empty());
134 }
135
136 #[test]
137 fn disable_without_rule_id_multiple_findings_same_line() {
138 let source = "fn main() {\n // slopguard-disable-next-line\n foo().unwrap();\n}\n";
139 let findings = vec![
140 make_finding("rule-a", 3),
141 make_finding("rule-b", 3),
142 make_finding("rule-c", 3),
143 ];
144 let result = filter_disabled(findings, source);
145 assert!(result.is_empty());
146 }
147
148 #[test]
149 fn disable_with_rule_id_multiple_findings_same_line() {
150 let source =
151 "fn main() {\n // slopguard-disable-next-line rule-b\n foo().unwrap();\n}\n";
152 let findings = vec![
153 make_finding("rule-a", 3),
154 make_finding("rule-b", 3),
155 make_finding("rule-c", 3),
156 ];
157 let result = filter_disabled(findings, source);
158 assert_eq!(result.len(), 2);
159 assert_eq!(result[0].rule_id, RuleId::from("rule-a"));
160 assert_eq!(result[1].rule_id, RuleId::from("rule-c"));
161 }
162
163 #[test]
164 fn no_disable_comments_returns_all_findings() {
165 let source = "fn main() {\n foo().unwrap();\n}\n";
166 let findings = vec![make_finding("no-unwrap-in-prod", 2)];
167 let result = filter_disabled(findings, source);
168 assert_eq!(result.len(), 1);
169 }
170
171 #[test]
172 fn disable_on_last_line_no_panic() {
173 let source = "fn main() {}\n// slopguard-disable-next-line";
174 let findings: Vec<Finding> = vec![];
175 let result = filter_disabled(findings, source);
176 assert!(result.is_empty());
177 }
178}