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 test_that::prelude::*;
102 use ytil_noxi::buffer::mock::MockBuffer;
103
104 use super::*;
105 use crate::diagnostics::filters::BufferWithPath;
106
107 #[test]
108 fn test_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 assert_that!(filter.skip_diagnostic(&buf, &diag), ok(eq(false)));
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 assert_that!(filter.skip_diagnostic(&buf, &diag), ok(eq(false)));
145 }
146
147 #[test]
148 fn test_skip_diagnostic_when_diagnosed_text_not_in_blacklist_returns_false() {
149 let test_blacklist = map! {"stdout": set!["instead of"]};
150 let filter = HarperLsFilter {
151 source: "Harper",
152 blacklist: &test_blacklist,
153 path_substring: None,
154 };
155 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["stderr"]);
156 let diag = dict! {
157 source: "Harper",
158 message: "some message",
159 lnum: 0,
160 col: 0,
161 end_lnum: 0,
162 end_col: 6,
163 };
164 assert_that!(filter.skip_diagnostic(&buf, &diag), ok(eq(false)));
165 }
166
167 #[test]
168 fn test_skip_diagnostic_when_diagnosed_text_in_blacklist_but_message_no_match_returns_false() {
169 let test_blacklist = map! {"stderr": set!["instead of"]};
170 let filter = HarperLsFilter {
171 source: "Harper",
172 blacklist: &test_blacklist,
173 path_substring: None,
174 };
175 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["stderr"]);
176 let diag = dict! {
177 source: "Harper",
178 message: "some other message",
179 lnum: 0,
180 col: 0,
181 end_lnum: 0,
182 end_col: 6,
183 };
184 assert_that!(filter.skip_diagnostic(&buf, &diag), ok(eq(false)));
185 }
186
187 #[test]
188 fn test_skip_diagnostic_when_all_conditions_met_returns_true() {
189 let test_blacklist = map! {"stderr": set!["instead of"]};
190 let filter = HarperLsFilter {
191 source: "Harper",
192 blacklist: &test_blacklist,
193 path_substring: None,
194 };
195 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["stderr"]);
196 let diag = dict! {
197 source: "Harper",
198 message: "instead of something",
199 lnum: 0,
200 col: 0,
201 end_lnum: 0,
202 end_col: 6,
203 };
204 assert_that!(filter.skip_diagnostic(&buf, &diag), ok(eq(true)));
205 }
206
207 #[test]
208 fn test_skip_diagnostic_when_diagnosed_text_cannot_be_extracted_returns_error() {
209 let test_blacklist = map! {"stderr": set!["instead of"]};
210 let filter = HarperLsFilter {
211 source: "Harper",
212 blacklist: &test_blacklist,
213 path_substring: None,
214 };
215 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["short"]);
216 let diag = dict! {
217 source: "Harper",
218 message: "instead of something",
219 lnum: 1,
220 col: 1,
221 end_col: 7,
222 };
223 assert_that!(
224 (filter.skip_diagnostic(&buf, &diag)).map(|_| ()),
225 err(displays_as(all!(
226 contains_substring("missing dict value"),
227 contains_substring(r#""end_lnum""#)
228 )))
229 );
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 assert_that!(
250 (filter.skip_diagnostic(&buf, &diag)).map(|_| ()),
251 err(displays_as(all!(
252 contains_substring("inconsistent line boundaries"),
253 contains_substring("lnum 1 > end_lnum 0")
254 )))
255 );
256 }
257
258 #[test]
259 fn test_skip_diagnostic_when_col_greater_than_end_col_returns_error() {
260 let test_blacklist = map! {"stderr": set!["instead of"]};
261 let filter = HarperLsFilter {
262 source: "Harper",
263 blacklist: &test_blacklist,
264 path_substring: None,
265 };
266 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["hello world"]);
267 let diag = dict! {
268 source: "Harper",
269 message: "some message",
270 lnum: 0,
271 col: 5,
272 end_lnum: 0,
273 end_col: 0,
274 };
275 assert_that!(
276 (filter.skip_diagnostic(&buf, &diag)).map(|_| ()),
277 err(displays_as(all!(
278 contains_substring("inconsistent col boundaries"),
279 contains_substring("col 5 > end_col 0")
280 )))
281 );
282 }
283
284 #[test]
285 fn test_skip_diagnostic_when_start_col_out_of_bounds_returns_error() {
286 let test_blacklist = map! {"stderr": set!["instead of"]};
287 let filter = HarperLsFilter {
288 source: "Harper",
289 blacklist: &test_blacklist,
290 path_substring: None,
291 };
292 let buf = create_buffer_with_path_and_content("src/lib.rs", vec!["hi"]);
293 let diag = dict! {
294 source: "Harper",
295 message: "some message",
296 lnum: 0,
297 col: 10,
298 end_lnum: 0,
299 end_col: 15,
300 };
301 assert_that!(
302 (filter.skip_diagnostic(&buf, &diag)).map(|_| ()),
303 err(displays_as(contains_substring("cannot extract substring")))
304 );
305 }
306
307 #[test]
308 fn test_skip_diagnostic_when_empty_lines_returns_false() {
309 let test_blacklist = map! {"stderr": set!["instead of"]};
310 let filter = HarperLsFilter {
311 source: "Harper",
312 blacklist: &test_blacklist,
313 path_substring: None,
314 };
315 let buf = create_buffer_with_path_and_content("src/lib.rs", vec![]);
316 let diag = dict! {
317 source: "Harper",
318 message: "some message",
319 lnum: 0,
320 col: 0,
321 end_lnum: 0,
322 end_col: 5,
323 };
324 assert_that!(filter.skip_diagnostic(&buf, &diag), ok(eq(false)));
325 }
326
327 fn create_buffer_with_path_and_content(path: &str, content: Vec<&str>) -> BufferWithPath {
328 BufferWithPath {
329 buffer: Box::new(MockBuffer::new(content.into_iter().map(str::to_string).collect())),
330 path: path.to_string(),
331 }
332 }
333}