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