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 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}