1use std::collections::HashMap;
28use std::path::Path;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct Directive {
33 pub line: usize,
35 pub reason: Option<String>,
37}
38
39#[derive(Debug, Clone)]
41pub struct FileDirectives {
42 directives: HashMap<usize, Directive>,
46}
47
48impl FileDirectives {
49 pub fn new() -> Self {
51 Self {
52 directives: HashMap::new(),
53 }
54 }
55
56 pub fn is_suppressed(&self, start_line: usize, _end_line: usize) -> Option<&Directive> {
69 for offset in 0..=3 {
72 if start_line > offset {
73 let check_line = start_line - offset;
74 if let Some(directive) = self.directives.get(&check_line) {
75 return Some(directive);
76 }
77 }
78 }
79
80 None
81 }
82
83 fn add_directive(&mut self, line: usize, directive: Directive) {
85 self.directives.insert(line, directive);
86 }
87
88 pub fn len(&self) -> usize {
90 self.directives.len()
91 }
92
93 pub fn is_empty(&self) -> bool {
95 self.directives.is_empty()
96 }
97}
98
99impl Default for FileDirectives {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105pub fn detect_directives(source: &str) -> FileDirectives {
121 let mut directives = FileDirectives::new();
122 let lines: Vec<&str> = source.lines().collect();
123
124 for (i, line) in lines.iter().enumerate() {
125 let line_num = i + 1; let trimmed = line.trim();
127
128 if let Some(directive) = parse_directive_line(trimmed) {
130 directives.add_directive(line_num, directive);
133 }
134 }
135
136 directives
137}
138
139fn parse_directive_line(line: &str) -> Option<Directive> {
148 if let Some(rest) = line.strip_prefix("//") {
150 return parse_comment_content(rest, line.len());
151 }
152
153 if let Some(rest) = line.strip_prefix('#') {
155 return parse_comment_content(rest, line.len());
156 }
157
158 None
159}
160
161fn parse_comment_content(content: &str, _line_len: usize) -> Option<Directive> {
163 let content = content.trim();
164
165 if let Some(rest) = content.strip_prefix("polydup-ignore") {
167 let rest = rest.trim();
168
169 let reason = if let Some(after_colon) = rest.strip_prefix(':') {
171 let r = after_colon.trim();
172 if r.is_empty() {
173 None
174 } else {
175 Some(r.to_string())
176 }
177 } else if rest.is_empty() {
178 None
179 } else {
180 Some(rest.to_string())
182 };
183
184 return Some(Directive {
185 line: 0, reason,
187 });
188 }
189
190 None
191}
192
193pub fn detect_directives_in_file(path: &Path) -> anyhow::Result<FileDirectives> {
201 let source = std::fs::read_to_string(path)?;
202 Ok(detect_directives(&source))
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_detect_javascript_directive_with_reason() {
211 let source = r#"
212// polydup-ignore: intentional code reuse
213function duplicate() {
214 console.log("test");
215}
216"#;
217 let directives = detect_directives(source);
218 assert_eq!(directives.len(), 1);
219
220 assert!(directives.is_suppressed(2, 5).is_some());
222 assert!(directives.is_suppressed(3, 5).is_some()); }
224
225 #[test]
226 fn test_detect_python_directive() {
227 let source = r#"
228# polydup-ignore: framework requirement
229def duplicate_function():
230 pass
231"#;
232 let directives = detect_directives(source);
233 assert_eq!(directives.len(), 1);
234 assert!(directives.is_suppressed(2, 4).is_some());
235 }
236
237 #[test]
238 fn test_directive_without_reason() {
239 let source = "// polydup-ignore\nfunction test() {}";
240 let directives = detect_directives(source);
241 assert_eq!(directives.len(), 1);
242
243 let directive = directives.is_suppressed(1, 2).unwrap();
244 assert!(directive.reason.is_none());
245 }
246
247 #[test]
248 fn test_directive_with_colon_but_no_reason() {
249 let source = "// polydup-ignore:\nfunction test() {}";
250 let directives = detect_directives(source);
251 assert_eq!(directives.len(), 1);
252
253 let directive = directives.is_suppressed(1, 2).unwrap();
254 assert!(directive.reason.is_none());
255 }
256
257 #[test]
258 fn test_no_directive() {
259 let source = r#"
260// This is just a regular comment
261function not_ignored() {
262 return 42;
263}
264"#;
265 let directives = detect_directives(source);
266 assert_eq!(directives.len(), 0);
267 assert!(directives.is_suppressed(2, 5).is_none());
268 }
269
270 #[test]
271 fn test_multiple_directives() {
272 let source = r#"
273// polydup-ignore: reason 1
274function fn1() {}
275
276// polydup-ignore: reason 2
277function fn2() {}
278"#;
279 let directives = detect_directives(source);
280 assert_eq!(directives.len(), 2);
281
282 assert!(directives.is_suppressed(2, 3).is_some());
283 assert!(directives.is_suppressed(5, 6).is_some());
284 }
285
286 #[test]
287 fn test_rust_directive() {
288 let source = r#"
289// polydup-ignore: generated code
290fn duplicate() -> i32 {
291 42
292}
293"#;
294 let directives = detect_directives(source);
295 assert_eq!(directives.len(), 1);
296 assert!(directives.is_suppressed(2, 5).is_some());
297 }
298}