nvrim/diagnostics/filters/lsps/
harper_ls.rs1use std::collections::HashMap;
6use std::collections::HashSet;
7use std::sync::LazyLock;
8
9use lit2::map;
10use lit2::set;
11use nvim_oxi::Dictionary;
12use ytil_noxi::buffer::TextBoundary;
13
14use crate::diagnostics::filters::BufferWithPath;
15use crate::diagnostics::filters::DiagnosticLocation;
16use crate::diagnostics::filters::DiagnosticsFilter;
17use crate::diagnostics::filters::lsps::GetDiagMsgOutput;
18use crate::diagnostics::filters::lsps::LspFilter;
19
20static HARPER_BLACKLIST: LazyLock<HashMap<&'static str, HashSet<&'static str>>> = LazyLock::new(|| {
23 map! {
24 "has ": set!["You may be missing a preposition here"],
25 "stderr": set!["instead of"],
26 "stdout": set!["instead of"],
27 "stdin": set!["instead of"],
28 "deduper": set!["Did you mean to spell"],
29 "TODO": set!["Hyphenate"],
30 "FIXME": set!["Did you mean `IME`"],
31 "Resolve": set!["Insert `to` to complete the infinitive"],
32 "foreground": set!["This sentence does not start with a capital letter"],
33 "build": set!["This sentence does not start with a capital letter"],
34 "args": set!["Use `argument` instead of `arg`"],
35 "stack overflow": set!["Ensure proper capitalization of companies"],
36 "over all": set!["closed compound `overall`"],
37 "checkout": set!["not a compound noun"]
38 }
39});
40
41pub struct HarperLsFilter<'a> {
42 pub source: &'a str,
44 pub blacklist: &'a HashMap<&'static str, HashSet<&'static str>>,
46 pub path_substring: Option<&'a str>,
48}
49
50impl HarperLsFilter<'_> {
51 pub fn filters() -> Vec<Box<dyn DiagnosticsFilter>> {
56 vec![Box::new(HarperLsFilter {
57 source: "Harper",
58 path_substring: None,
59 blacklist: &HARPER_BLACKLIST,
60 })]
61 }
62}
63
64impl LspFilter for HarperLsFilter<'_> {
65 fn path_substring(&self) -> Option<&str> {
66 self.path_substring
67 }
68
69 fn source(&self) -> &str {
70 self.source
71 }
72}
73
74impl DiagnosticsFilter for HarperLsFilter<'_> {
75 fn skip_diagnostic(&self, buf: &BufferWithPath, lsp_diag: &Dictionary) -> rootcause::Result<bool> {
76 let diag_msg = match self.get_diag_msg_or_skip(&buf.path, lsp_diag)? {
77 GetDiagMsgOutput::Msg(diag_msg) => diag_msg,
78 GetDiagMsgOutput::Skip => return Ok(false),
79 };
80
81 let diag_location = DiagnosticLocation::try_from(lsp_diag)?;
82
83 let diag_text = buf
84 .buffer
85 .get_text_between(diag_location.start(), diag_location.end(), TextBoundary::Exact)?;
86
87 Ok(self
88 .blacklist
89 .get(diag_text.as_str())
90 .map(|blacklisted_msgs| {
91 blacklisted_msgs
92 .iter()
93 .any(|blacklisted_msg| diag_msg.contains(blacklisted_msg))
94 })
95 .is_some_and(std::convert::identity))
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use ytil_noxi::buffer::mock::MockBuffer;
102
103 use super::*;
104 use crate::diagnostics::filters::BufferWithPath;
105
106 #[test]
107 fn test_skip_diagnostic_when_path_substring_pattern_not_matched_returns_false() {
108 let test_blacklist = map! {"stderr": set!["instead of"]};
109 let filter = HarperLsFilter {
110 source: "Harper",
111 blacklist: &test_blacklist,
112 path_substring: Some("src/"),
113 };
114 let buf = create_buffer_with_path_and_content("tests/main.rs", vec!["stderr"]);
115 let diag = dict! {
116 source: "Harper",
117 message: "instead of something",
118 lnum: 0,
119 col: 0,
120 end_lnum: 0,
121 end_col: 6,
122 };
123 assert2::assert!(let Ok(res) = filter.skip_diagnostic(&buf, &diag));
124 assert!(!res);
125 }
126
127 #[test]
128 fn test_skip_diagnostic_when_source_mismatch_returns_false() {
129 let test_blacklist = map! {"stderr": set!["instead of"]};
130 let filter = HarperLsFilter {
131 source: "Harper",
132 blacklist: &test_blacklist,
133 path_substring: None,
134 };
135 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["stderr"]);
136 let diag = dict! {
137 source: "Other",
138 message: "instead of something",
139 lnum: 0,
140 col: 0,
141 end_lnum: 0,
142 end_col: 6,
143 };
144 assert2::assert!(let Ok(res) = filter.skip_diagnostic(&buf, &diag));
145 assert!(!res);
146 }
147
148 #[test]
149 fn test_skip_diagnostic_when_diagnosed_text_not_in_blacklist_returns_false() {
150 let test_blacklist = map! {"stdout": set!["instead of"]};
151 let filter = HarperLsFilter {
152 source: "Harper",
153 blacklist: &test_blacklist,
154 path_substring: None,
155 };
156 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["stderr"]);
157 let diag = dict! {
158 source: "Harper",
159 message: "some message",
160 lnum: 0,
161 col: 0,
162 end_lnum: 0,
163 end_col: 6,
164 };
165 assert2::assert!(let Ok(res) = filter.skip_diagnostic(&buf, &diag));
166 assert!(!res);
167 }
168
169 #[test]
170 fn test_skip_diagnostic_when_diagnosed_text_in_blacklist_but_message_no_match_returns_false() {
171 let test_blacklist = map! {"stderr": set!["instead of"]};
172 let filter = HarperLsFilter {
173 source: "Harper",
174 blacklist: &test_blacklist,
175 path_substring: None,
176 };
177 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["stderr"]);
178 let diag = dict! {
179 source: "Harper",
180 message: "some other message",
181 lnum: 0,
182 col: 0,
183 end_lnum: 0,
184 end_col: 6,
185 };
186 assert2::assert!(let Ok(res) = filter.skip_diagnostic(&buf, &diag));
187 assert!(!res);
188 }
189
190 #[test]
191 fn test_skip_diagnostic_when_all_conditions_met_returns_true() {
192 let test_blacklist = map! {"stderr": set!["instead of"]};
193 let filter = HarperLsFilter {
194 source: "Harper",
195 blacklist: &test_blacklist,
196 path_substring: None,
197 };
198 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["stderr"]);
199 let diag = dict! {
200 source: "Harper",
201 message: "instead of something",
202 lnum: 0,
203 col: 0,
204 end_lnum: 0,
205 end_col: 6,
206 };
207 assert2::assert!(let Ok(res) = filter.skip_diagnostic(&buf, &diag));
208 assert!(res);
209 }
210
211 #[test]
212 fn test_skip_diagnostic_when_diagnosed_text_cannot_be_extracted_returns_error() {
213 let test_blacklist = map! {"stderr": set!["instead of"]};
214 let filter = HarperLsFilter {
215 source: "Harper",
216 blacklist: &test_blacklist,
217 path_substring: None,
218 };
219 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["short"]);
220 let diag = dict! {
221 source: "Harper",
222 message: "instead of something",
223 lnum: 1,
224 col: 1,
225 end_col: 7,
226 };
227 assert2::assert!(let Err(err) = filter.skip_diagnostic(&buf, &diag));
228 assert!(err.to_string().contains("missing dict value"));
229 assert!(err.to_string().contains(r#""end_lnum""#));
230 }
231
232 #[test]
233 fn test_skip_diagnostic_when_lnum_greater_than_end_lnum_returns_error() {
234 let test_blacklist = map! {"stderr": set!["instead of"]};
235 let filter = HarperLsFilter {
236 source: "Harper",
237 blacklist: &test_blacklist,
238 path_substring: None,
239 };
240 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["hello world"]);
241 let diag = dict! {
242 source: "Harper",
243 message: "some message",
244 lnum: 1,
245 col: 0,
246 end_lnum: 0,
247 end_col: 5,
248 };
249 assert2::assert!(let Err(err) = filter.skip_diagnostic(&buf, &diag));
250 assert!(err.to_string().contains("inconsistent line boundaries"));
251 assert!(err.to_string().contains("lnum 1 > end_lnum 0"));
252 }
253
254 #[test]
255 fn test_skip_diagnostic_when_col_greater_than_end_col_returns_error() {
256 let test_blacklist = map! {"stderr": set!["instead of"]};
257 let filter = HarperLsFilter {
258 source: "Harper",
259 blacklist: &test_blacklist,
260 path_substring: None,
261 };
262 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["hello world"]);
263 let diag = dict! {
264 source: "Harper",
265 message: "some message",
266 lnum: 0,
267 col: 5,
268 end_lnum: 0,
269 end_col: 0,
270 };
271 assert2::assert!(let Err(err) = filter.skip_diagnostic(&buf, &diag));
272 assert!(err.to_string().contains("inconsistent col boundaries"));
273 assert!(err.to_string().contains("col 5 > end_col 0"));
274 }
275
276 #[test]
277 fn test_skip_diagnostic_when_start_col_out_of_bounds_returns_error() {
278 let test_blacklist = map! {"stderr": set!["instead of"]};
279 let filter = HarperLsFilter {
280 source: "Harper",
281 blacklist: &test_blacklist,
282 path_substring: None,
283 };
284 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["hi"]);
285 let diag = dict! {
286 source: "Harper",
287 message: "some message",
288 lnum: 0,
289 col: 10,
290 end_lnum: 0,
291 end_col: 15,
292 };
293 assert2::assert!(let Err(err) = filter.skip_diagnostic(&buf, &diag));
294 assert!(err.to_string().contains("cannot extract substring"));
295 }
296
297 #[test]
298 fn test_skip_diagnostic_when_empty_lines_returns_false() {
299 let test_blacklist = map! {"stderr": set!["instead of"]};
300 let filter = HarperLsFilter {
301 source: "Harper",
302 blacklist: &test_blacklist,
303 path_substring: None,
304 };
305 let buf = create_buffer_with_path_and_content("src/lib.rs", vec![]);
306 let diag = dict! {
307 source: "Harper",
308 message: "some message",
309 lnum: 0,
310 col: 0,
311 end_lnum: 0,
312 end_col: 5,
313 };
314 assert2::assert!(let Ok(res) = filter.skip_diagnostic(&buf, &diag));
315 assert!(!res);
316 }
317
318 fn create_buffer_with_path_and_content(path: &str, content: Vec<&str>) -> BufferWithPath {
319 BufferWithPath {
320 buffer: Box::new(MockBuffer::new(content.into_iter().map(str::to_string).collect())),
321 path: path.to_string(),
322 }
323 }
324}