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