code_moniker_cli/check/
suppress.rs1use regex::Regex;
2
3use crate::check::eval::Violation;
4use code_moniker_core::core::code_graph::{CodeGraph, DefRecord};
5use code_moniker_core::core::kinds::KIND_COMMENT;
6
7pub fn apply(graph: &CodeGraph, source: &str, violations: Vec<Violation>) -> Vec<Violation> {
15 let directives = collect_directives(graph, source);
16 if directives.is_empty() {
17 return violations;
18 }
19
20 let file_scope: Vec<&Directive> = directives.iter().filter(|d| d.file_scope).collect();
21 let line_scope: Vec<(&Directive, Option<(u32, u32)>)> = directives
22 .iter()
23 .filter(|d| !d.file_scope)
24 .map(|d| (d, target_lines_for(graph, source, d)))
25 .collect();
26
27 violations
28 .into_iter()
29 .filter(|v| {
30 !file_scope.iter().any(|d| matches_id(d, &v.rule_id))
31 && !line_scope.iter().any(|(d, target)| {
32 matches_id(d, &v.rule_id)
33 && target.is_some_and(|(s, e)| v.lines.0 >= s && v.lines.0 <= e)
34 })
35 })
36 .collect()
37}
38
39#[derive(Debug)]
40struct Directive {
41 comment_end_byte: u32,
42 file_scope: bool,
43 rule_filters: Vec<String>,
44}
45
46fn directive_re() -> &'static Regex {
47 use std::sync::OnceLock;
48 static RE: OnceLock<Regex> = OnceLock::new();
49 RE.get_or_init(|| {
50 Regex::new(r"(?://|#|--)\s*code-moniker:\s*ignore(-file)?(?:\[([^\]]+)\])?").unwrap()
51 })
52}
53
54fn collect_directives(graph: &CodeGraph, source: &str) -> Vec<Directive> {
55 let mut out = Vec::new();
56 for d in graph.defs() {
57 if d.kind.as_slice() != KIND_COMMENT {
58 continue;
59 }
60 let Some((s, e)) = d.position else { continue };
61 let Some(text) = source.get(s as usize..e as usize) else {
62 continue;
63 };
64 let Some(caps) = directive_re().captures(text) else {
65 continue;
66 };
67 let file_scope = caps.get(1).is_some();
68 let rule_filters = caps
69 .get(2)
70 .map(|m| {
71 m.as_str()
72 .split(',')
73 .map(|s| s.trim().to_string())
74 .filter(|s| !s.is_empty())
75 .collect()
76 })
77 .unwrap_or_default();
78 out.push(Directive {
79 comment_end_byte: e,
80 file_scope,
81 rule_filters,
82 });
83 }
84 out
85}
86
87fn target_lines_for(graph: &CodeGraph, source: &str, dir: &Directive) -> Option<(u32, u32)> {
88 let target = next_def_after(graph, dir.comment_end_byte)?;
89 let (s, e) = target.position?;
90 Some(crate::lines::line_range(source, s, e))
91}
92
93fn next_def_after(graph: &CodeGraph, after_byte: u32) -> Option<&DefRecord> {
94 let mut best: Option<&DefRecord> = None;
95 for d in graph.defs() {
96 if d.kind.as_slice() == KIND_COMMENT {
97 continue;
98 }
99 let Some((s, _)) = d.position else { continue };
100 if s < after_byte {
101 continue;
102 }
103 match best {
104 None => best = Some(d),
105 Some(b) => {
106 let bs = b.position.map(|p| p.0).unwrap_or(u32::MAX);
107 if s < bs {
108 best = Some(d);
109 }
110 }
111 }
112 }
113 best
114}
115
116fn matches_id(dir: &Directive, rule_id: &str) -> bool {
117 if dir.rule_filters.is_empty() {
118 return true;
119 }
120 dir.rule_filters
121 .iter()
122 .any(|f| rule_id == f || rule_id.ends_with(&format!(".{f}")))
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::check::config::Config;
129 use crate::check::evaluate;
130 use crate::extract;
131 use code_moniker_core::lang::Lang;
132
133 fn run(source: &str, cfg: &Config) -> Vec<Violation> {
134 let graph = extract::extract(Lang::Ts, source, std::path::Path::new("test.ts"));
135 let violations = evaluate(&graph, source, Lang::Ts, cfg, "code+moniker://")
136 .expect("test config compiles");
137 apply(&graph, source, violations)
138 }
139
140 fn cfg(s: &str) -> Config {
141 toml::from_str(s).expect("test config must parse")
142 }
143
144 #[test]
145 fn ignore_without_filter_drops_next_def_violations() {
146 let cfg = cfg(r#"
147 [[ts.class.where]]
148 id = "name-pascal"
149 expr = "name =~ ^[A-Z][A-Za-z0-9]*$"
150 "#);
151 let source = "// code-moniker: ignore\nclass lower_bad {}\n";
152 assert!(run(source, &cfg).is_empty());
153 }
154
155 #[test]
156 fn ignore_with_specific_id_only_drops_matching_violations() {
157 let cfg = cfg(r#"
158 [[ts.class.where]]
159 id = "name-pascal"
160 expr = "name =~ ^[A-Z][A-Za-z0-9]*$"
161
162 [[ts.class.where]]
163 id = "max-lines"
164 expr = "lines <= 1"
165 "#);
166 let source = "// code-moniker: ignore[name-pascal]\nclass lower_bad {\n}\n";
167 let v = run(source, &cfg);
168 let ids: Vec<&str> = v.iter().map(|x| x.rule_id.as_str()).collect();
169 assert!(!ids.contains(&"ts.class.name-pascal"), "{ids:?}");
170 assert!(
171 ids.contains(&"ts.class.max-lines"),
172 "max-lines should remain: {ids:?}"
173 );
174 }
175
176 #[test]
177 fn ignore_with_other_id_does_not_drop_violation() {
178 let cfg = cfg(r#"
179 [[ts.class.where]]
180 id = "name-pascal"
181 expr = "name =~ ^[A-Z][A-Za-z0-9]*$"
182 "#);
183 let source = "// code-moniker: ignore[max-lines]\nclass lower_bad {}\n";
184 let v = run(source, &cfg);
185 assert_eq!(v.len(), 1);
186 assert_eq!(v[0].rule_id, "ts.class.name-pascal");
187 }
188
189 #[test]
190 fn ignore_file_drops_violations_anywhere() {
191 let cfg = cfg(r#"
192 [[ts.class.where]]
193 id = "name-pascal"
194 expr = "name =~ ^[A-Z][A-Za-z0-9]*$"
195 "#);
196 let source = "// code-moniker: ignore-file\nclass lower_one {}\nclass another_lower {}\n";
197 assert!(run(source, &cfg).is_empty());
198 }
199
200 #[test]
201 fn ignore_file_with_filter_only_drops_listed_rules() {
202 let cfg = cfg(r#"
203 [[ts.class.where]]
204 id = "name-pascal"
205 expr = "name =~ ^[A-Z][A-Za-z0-9]*$"
206
207 [[ts.class.where]]
208 id = "max-lines"
209 expr = "lines <= 1"
210 "#);
211 let source = "// code-moniker: ignore-file[name-pascal]\nclass lower_one {\n}\n";
212 let v = run(source, &cfg);
213 let ids: Vec<&str> = v.iter().map(|x| x.rule_id.as_str()).collect();
214 assert!(!ids.contains(&"ts.class.name-pascal"), "{ids:?}");
215 assert!(ids.contains(&"ts.class.max-lines"), "{ids:?}");
216 }
217
218 #[test]
219 fn ignore_only_applies_to_immediate_next_def() {
220 let cfg = cfg(r#"
221 [[ts.class.where]]
222 id = "name-pascal"
223 expr = "name =~ ^[A-Z][A-Za-z0-9]*$"
224 "#);
225 let source = "// code-moniker: ignore\nclass lower_one {}\nclass lower_two {}\n";
226 let v = run(source, &cfg);
227 let ids: Vec<&str> = v.iter().map(|x| x.rule_id.as_str()).collect();
228 assert_eq!(v.len(), 1, "second class still flagged: {ids:?}");
229 }
230
231 #[test]
232 fn ignore_directives_dont_self_flag_as_prose() {
233 let cfg = cfg(r#"
234 [[ts.comment.where]]
235 id = "allow-only"
236 expr = '''text =~ ^\s*//\s*code-moniker:'''
237 "#);
238 let source = "// code-moniker: ignore\nclass Whatever {}\n";
239 assert!(run(source, &cfg).is_empty());
240 }
241}