Skip to main content

covguard_directives/
lib.rs

1//! Directive utilities for covguard.
2//!
3//! The module provides reusable, pure logic for ignore directive parsing and
4//! changed-range scanning that is shared across app and domain adapters.
5
6use std::collections::{BTreeMap, BTreeSet};
7
8use covguard_ports::{ChangedRanges, RepoReader};
9
10/// Check if a line contains a `covguard: ignore` directive.
11///
12/// The directive can appear in comment contexts and supports:
13///
14/// - `// covguard: ignore` (Rust, C, JS, etc.)
15/// - `# covguard: ignore` (Python, Shell, YAML, etc.)
16/// - `-- covguard: ignore` (SQL, Haskell, Lua)
17/// - `/* covguard: ignore */` (block comments)
18/// - `covguard-ignore`
19///
20/// Matching is case-insensitive and tolerant of whitespace.
21pub fn has_ignore_directive(line: &str) -> bool {
22    let line_lower = line.to_lowercase();
23
24    if let Some(pos) = line_lower.find("covguard:") {
25        let after = &line_lower[pos + 9..]; // len("covguard:") = 9
26        let trimmed = after.trim_start();
27        return trimmed.starts_with("ignore");
28    }
29
30    if let Some(pos) = line_lower.find("covguard-ignore") {
31        let before = &line_lower[..pos];
32        return before.contains("//")
33            || before.contains('#')
34            || before.contains("--")
35            || before.contains("/*");
36    }
37
38    false
39}
40
41/// Detect lines with `covguard: ignore` directives in changed ranges.
42///
43/// For each file in `changed_ranges`, reads the relevant lines from `reader` and
44/// records the line numbers that contain ignore directives.
45pub fn detect_ignored_lines<R: RepoReader>(
46    changed_ranges: &ChangedRanges,
47    reader: &R,
48) -> BTreeMap<String, BTreeSet<u32>> {
49    let mut ignored = BTreeMap::new();
50
51    for (path, ranges) in changed_ranges {
52        let mut file_ignored = BTreeSet::new();
53
54        for range in ranges {
55            for line_no in range.clone() {
56                if let Some(line_content) = reader.read_line(path, line_no)
57                    && has_ignore_directive(&line_content)
58                {
59                    file_ignored.insert(line_no);
60                }
61            }
62        }
63
64        if !file_ignored.is_empty() {
65            ignored.insert(path.clone(), file_ignored);
66        }
67    }
68
69    ignored
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use covguard_ports::RepoReader;
76    use std::collections::BTreeMap;
77
78    struct MapReader {
79        lines: BTreeMap<(String, u32), String>,
80    }
81
82    impl MapReader {
83        fn new(entries: Vec<(&str, u32, &str)>) -> Self {
84            let mut lines = BTreeMap::new();
85            for (path, line_no, content) in entries {
86                lines.insert((path.to_string(), line_no), content.to_string());
87            }
88            Self { lines }
89        }
90    }
91
92    impl RepoReader for MapReader {
93        fn read_line(&self, path: &str, line_no: u32) -> Option<String> {
94            self.lines.get(&(path.to_string(), line_no)).cloned()
95        }
96    }
97
98    #[test]
99    fn test_has_ignore_directive_rust_comment() {
100        assert!(has_ignore_directive("let x = 1; // covguard: ignore"));
101        assert!(has_ignore_directive("// covguard: ignore"));
102        assert!(has_ignore_directive("    // covguard: ignore"));
103        assert!(has_ignore_directive("// COVGUARD: IGNORE"));
104        assert!(has_ignore_directive("// covguard:ignore"));
105    }
106
107    #[test]
108    fn test_has_ignore_directive_python_comment() {
109        assert!(has_ignore_directive("x = 1  # covguard: ignore"));
110        assert!(has_ignore_directive("# covguard: ignore"));
111        assert!(has_ignore_directive("#covguard:ignore"));
112    }
113
114    #[test]
115    fn test_has_ignore_directive_block_comment() {
116        assert!(has_ignore_directive("/* covguard: ignore */"));
117        assert!(has_ignore_directive("int x = 1; /* covguard: ignore */"));
118    }
119
120    #[test]
121    fn test_has_ignore_directive_sql_comment() {
122        assert!(has_ignore_directive("-- covguard: ignore"));
123        assert!(has_ignore_directive("SELECT 1; -- covguard: ignore"));
124    }
125
126    #[test]
127    fn test_has_ignore_directive_hyphen_syntax() {
128        assert!(has_ignore_directive("// covguard-ignore"));
129        assert!(has_ignore_directive("# covguard-ignore"));
130        assert!(has_ignore_directive("-- covguard-ignore"));
131        assert!(has_ignore_directive("/* covguard-ignore */"));
132    }
133
134    #[test]
135    fn test_has_ignore_directive_negative_cases() {
136        assert!(!has_ignore_directive("let x = 1;"));
137        assert!(!has_ignore_directive("// some other comment"));
138        assert!(!has_ignore_directive("// covguard")); // missing ignore
139        assert!(!has_ignore_directive("// ignore covguard")); // wrong order
140    }
141
142    #[test]
143    fn test_detect_ignored_lines_with_reader() {
144        let mut changed_ranges = BTreeMap::new();
145        changed_ranges.insert("src/lib.rs".to_string(), vec![1..=3]);
146        changed_ranges.insert("src/main.rs".to_string(), vec![10..=11]);
147
148        let reader = MapReader::new(vec![
149            ("src/lib.rs", 2, "let x = 1; // covguard: ignore"),
150            ("src/main.rs", 11, "# covguard: ignore"),
151        ]);
152
153        let ignored = detect_ignored_lines(&changed_ranges, &reader);
154        assert_eq!(
155            ignored.get("src/lib.rs").cloned(),
156            Some(BTreeSet::from([2]))
157        );
158        assert_eq!(
159            ignored.get("src/main.rs").cloned(),
160            Some(BTreeSet::from([11]))
161        );
162    }
163}