syncable_cli/analyzer/dclint/
pragma.rs1use std::collections::{HashMap, HashSet};
11
12use crate::analyzer::dclint::types::RuleCode;
13
14#[derive(Debug, Clone, Default)]
16pub struct PragmaState {
17 pub global_disabled: HashSet<String>,
19 pub all_disabled: bool,
21 pub line_disabled: HashMap<u32, HashSet<String>>,
23 pub all_disabled_lines: HashSet<u32>,
25}
26
27impl PragmaState {
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 pub fn is_ignored(&self, code: &RuleCode, line: u32) -> bool {
34 if self.all_disabled {
36 return true;
37 }
38 if self.global_disabled.contains(code.as_str()) || self.global_disabled.contains("*") {
39 return true;
40 }
41
42 if self.all_disabled_lines.contains(&line) {
44 return true;
45 }
46 if let Some(rules) = self.line_disabled.get(&line)
47 && (rules.contains("*") || rules.contains(code.as_str()))
48 {
49 return true;
50 }
51
52 false
53 }
54
55 pub fn disable_global(&mut self, rule: impl Into<String>) {
57 let rule = rule.into();
58 if rule == "*" {
59 self.all_disabled = true;
60 } else {
61 self.global_disabled.insert(rule);
62 }
63 }
64
65 pub fn disable_line(&mut self, line: u32, rules: Vec<String>) {
67 if rules.is_empty() || rules.iter().any(|r| r == "*") {
68 self.all_disabled_lines.insert(line);
69 } else {
70 self.line_disabled.entry(line).or_default().extend(rules);
71 }
72 }
73}
74
75pub fn extract_pragmas(source: &str) -> PragmaState {
77 let mut state = PragmaState::new();
78 let lines: Vec<&str> = source.lines().collect();
79
80 for (idx, line) in lines.iter().enumerate() {
81 let line_num = (idx + 1) as u32;
82 let trimmed = line.trim();
83
84 if !trimmed.starts_with('#') {
86 continue;
87 }
88
89 let comment = trimmed.trim_start_matches('#').trim();
90
91 if let Some(rest) = comment.strip_prefix("dclint-disable-file") {
93 let rules = parse_rule_list(rest);
94 if rules.is_empty() {
95 state.all_disabled = true;
96 } else {
97 for rule in rules {
98 state.disable_global(rule);
99 }
100 }
101 continue;
102 }
103
104 if let Some(rest) = comment.strip_prefix("dclint-disable-next-line") {
106 let rules = parse_rule_list(rest);
107 let next_line = line_num + 1;
108
109 if rules.is_empty() {
110 state.all_disabled_lines.insert(next_line);
111 } else {
112 state.disable_line(next_line, rules);
113 }
114 continue;
115 }
116
117 if comment.starts_with("dclint-disable") && !comment.starts_with("dclint-disable-") {
119 let rules = parse_rule_list(&comment["dclint-disable".len()..]);
120 if rules.is_empty() {
121 state.all_disabled = true;
122 } else {
123 for rule in rules {
124 state.disable_global(rule);
125 }
126 }
127 continue;
128 }
129 }
130
131 state
132}
133
134fn parse_rule_list(s: &str) -> Vec<String> {
136 let trimmed = s.trim();
137 if trimmed.is_empty() {
138 return vec![];
139 }
140
141 trimmed
142 .split(',')
143 .map(|r| r.trim().to_string())
144 .filter(|r| !r.is_empty())
145 .collect()
146}
147
148pub fn extract_global_disable_rules(source: &str) -> HashSet<String> {
151 let state = extract_pragmas(source);
152 let mut result = state.global_disabled;
153 if state.all_disabled {
154 result.insert("*".to_string());
155 }
156 result
157}
158
159pub fn extract_line_disable_rules(source: &str) -> HashMap<u32, HashSet<String>> {
162 let state = extract_pragmas(source);
163 let mut result = state.line_disabled;
164
165 for line in state.all_disabled_lines {
167 result.entry(line).or_default().insert("*".to_string());
168 }
169
170 result
171}
172
173pub fn starts_with_disable_file_comment(source: &str) -> bool {
175 for line in source.lines() {
176 let trimmed = line.trim();
177 if trimmed.is_empty() {
178 continue;
179 }
180 if trimmed.starts_with('#') {
181 let comment = trimmed.trim_start_matches('#').trim();
182 return comment.starts_with("dclint-disable-file")
183 || (comment.starts_with("dclint-disable")
184 && !comment.starts_with("dclint-disable-"));
185 }
186 return false;
188 }
189 false
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_extract_global_disable() {
198 let source = "# dclint-disable\nservices:\n web:\n image: nginx\n";
199 let state = extract_pragmas(source);
200 assert!(state.all_disabled);
201 }
202
203 #[test]
204 fn test_extract_global_disable_specific_rules() {
205 let source = "# dclint-disable DCL001, DCL002\nservices:\n web:\n image: nginx\n";
206 let state = extract_pragmas(source);
207 assert!(!state.all_disabled);
208 assert!(state.global_disabled.contains("DCL001"));
209 assert!(state.global_disabled.contains("DCL002"));
210 assert!(!state.global_disabled.contains("DCL003"));
211 }
212
213 #[test]
214 fn test_extract_disable_next_line() {
215 let source = r#"
216services:
217 # dclint-disable-next-line DCL001
218 web:
219 build: .
220 image: nginx
221"#;
222 let state = extract_pragmas(source);
223 assert!(!state.all_disabled);
224
225 assert!(state.is_ignored(&RuleCode::new("DCL001"), 4));
227 assert!(!state.is_ignored(&RuleCode::new("DCL001"), 5));
228 }
229
230 #[test]
231 fn test_extract_disable_next_line_all() {
232 let source = r#"
233services:
234 # dclint-disable-next-line
235 web:
236 build: .
237 image: nginx
238"#;
239 let state = extract_pragmas(source);
240
241 assert!(state.all_disabled_lines.contains(&4));
243 assert!(state.is_ignored(&RuleCode::new("DCL001"), 4));
244 assert!(state.is_ignored(&RuleCode::new("DCL002"), 4));
245 }
246
247 #[test]
248 fn test_extract_disable_file() {
249 let source = "# dclint-disable-file\nservices:\n web:\n image: nginx\n";
250 let state = extract_pragmas(source);
251 assert!(state.all_disabled);
252 }
253
254 #[test]
255 fn test_is_ignored() {
256 let source = "# dclint-disable DCL001\nservices:\n web:\n image: nginx\n";
257 let state = extract_pragmas(source);
258
259 assert!(state.is_ignored(&RuleCode::new("DCL001"), 1));
260 assert!(state.is_ignored(&RuleCode::new("DCL001"), 5));
261 assert!(!state.is_ignored(&RuleCode::new("DCL002"), 1));
262 }
263
264 #[test]
265 fn test_starts_with_disable_file_comment() {
266 assert!(starts_with_disable_file_comment(
267 "# dclint-disable-file\nservices:"
268 ));
269 assert!(starts_with_disable_file_comment(
270 "# dclint-disable\nservices:"
271 ));
272 assert!(!starts_with_disable_file_comment("services:\n web:"));
273 assert!(!starts_with_disable_file_comment(
274 "# Some other comment\nservices:"
275 ));
276 }
277
278 #[test]
279 fn test_parse_rule_list() {
280 assert_eq!(parse_rule_list(""), Vec::<String>::new());
281 assert_eq!(parse_rule_list("DCL001"), vec!["DCL001"]);
282 assert_eq!(parse_rule_list("DCL001, DCL002"), vec!["DCL001", "DCL002"]);
283 assert_eq!(
284 parse_rule_list(" DCL001 , DCL002 "),
285 vec!["DCL001", "DCL002"]
286 );
287 }
288}