1use crate::expr::{Expr, ExprExt};
2use blanket::blanket;
3
4use crate::{Document, LSend, Token, TokenStringExt};
5
6use super::{Lint, Linter};
7
8pub trait DocumentIterator {
9 type Unit;
10
11 fn iter_units<'a>(document: &'a Document) -> Box<dyn Iterator<Item = &'a [Token]> + 'a>;
12}
13
14pub struct Chunk;
16pub struct Sentence;
18
19impl DocumentIterator for Chunk {
20 type Unit = Chunk;
21
22 fn iter_units<'a>(document: &'a Document) -> Box<dyn Iterator<Item = &'a [Token]> + 'a> {
23 Box::new(document.iter_chunks())
24 }
25}
26
27impl DocumentIterator for Sentence {
28 type Unit = Sentence;
29
30 fn iter_units<'a>(document: &'a Document) -> Box<dyn Iterator<Item = &'a [Token]> + 'a> {
31 Box::new(document.iter_sentences())
32 }
33}
34
35#[blanket(derive(Box))]
40pub trait ExprLinter: LSend {
41 type Unit: DocumentIterator;
42
43 fn expr(&self) -> &dyn Expr;
45 fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
55 self.match_to_lint_with_context(matched_tokens, source, None)
56 }
57
58 fn match_to_lint_with_context(
65 &self,
66 matched_tokens: &[Token],
67 source: &[char],
68 _context: Option<(&[Token], &[Token])>,
69 ) -> Option<Lint> {
70 self.match_to_lint(matched_tokens, source)
72 }
73 fn description(&self) -> &str;
76}
77
78pub fn find_the_only_token_matching<'a, F>(
83 tokens: &'a [Token],
84 source: &[char],
85 predicate: F,
86) -> Option<&'a Token>
87where
88 F: Fn(&Token, &[char]) -> bool,
89{
90 find_the_only_token_index_matching(tokens, source, predicate).map(|idx| &tokens[idx])
91}
92
93pub fn find_the_only_token_index_matching<F>(
97 tokens: &[Token],
98 source: &[char],
99 predicate: F,
100) -> Option<usize>
101where
102 F: Fn(&Token, &[char]) -> bool,
103{
104 let mut matches = tokens
105 .iter()
106 .enumerate()
107 .filter(|&(_, tok)| predicate(tok, source));
108
109 match (matches.next(), matches.next()) {
110 (Some((idx, _)), None) => Some(idx),
111 _ => None,
112 }
113}
114
115impl<L, U> Linter for L
116where
117 L: ExprLinter<Unit = U>,
118 U: DocumentIterator,
119{
120 fn lint(&mut self, document: &Document) -> Vec<Lint> {
121 let mut lints = Vec::new();
122 let source = document.get_source();
123
124 for unit in U::iter_units(document) {
125 lints.extend(run_on_chunk(self, unit, source));
126 }
127
128 lints
129 }
130
131 fn description(&self) -> &str {
132 self.description()
133 }
134}
135
136pub fn run_on_chunk<'a>(
137 linter: &'a impl ExprLinter,
138 unit: &'a [Token],
139 source: &'a [char],
140) -> impl Iterator<Item = Lint> + 'a {
141 linter
142 .expr()
143 .iter_matches(unit, source)
144 .filter_map(|match_span| {
145 linter.match_to_lint_with_context(
146 &unit[match_span.start..match_span.end],
147 source,
148 Some((&unit[..match_span.start], &unit[match_span.end..])),
149 )
150 })
151}
152
153pub fn followed_by_word(
161 context: Option<(&[Token], &[Token])>,
162 predicate: impl Fn(&Token) -> bool,
163) -> bool {
164 if let Some((_, after)) = context
165 && let [ws, word, ..] = after
166 && ws.kind.is_whitespace()
167 {
168 return predicate(word);
169 }
170 false
171}
172
173pub fn followed_by_token(
180 context: Option<(&[Token], &[Token])>,
181 predicate: impl Fn(&Token) -> bool,
182) -> bool {
183 context
184 .and_then(|(_, after)| after.first())
185 .is_some_and(predicate)
186}
187
188pub fn followed_by_hyphen(context: Option<(&[Token], &[Token])>) -> bool {
189 followed_by_token(context, |hy| hy.kind.is_hyphen())
190}
191
192pub fn at_start_of_sentence(context: Option<(&[Token], &[Token])>) -> bool {
195 if let Some((before, _)) = context
196 && (before.is_empty() || (before.len() == 1 && before[0].kind.is_whitespace()))
197 {
198 return true;
199 }
200 false
201}
202
203pub fn preceded_by_word(
211 context: Option<(&[Token], &[Token])>,
212 predicate: impl Fn(&Token) -> bool,
213) -> bool {
214 if let Some((before, _)) = context
215 && let [.., word, ws] = before
216 && ws.kind.is_whitespace()
217 {
218 return predicate(word);
219 }
220 false
221}
222
223pub fn surrounded_by_words(
232 context: Option<(&[Token], &[Token])>,
233 predicate: impl Fn(&Token, &Token) -> bool,
234) -> bool {
235 if let Some((before, after)) = context
236 && let [.., word_before, ws_before] = before
237 && let [ws_after, word_after, ..] = after
238 && ws_before.kind.is_whitespace()
239 && ws_after.kind.is_whitespace()
240 {
241 return predicate(word_before, word_after);
242 }
243 false
244}
245
246#[cfg(test)]
247mod tests_context {
248 use crate::expr::{Expr, FixedPhrase};
249 use crate::linting::expr_linter::{Chunk, Sentence};
250 use crate::linting::tests::assert_suggestion_result;
251 use crate::linting::{ExprLinter, Suggestion};
252 use crate::token_string_ext::TokenStringExt;
253 use crate::{Lint, Token};
254
255 pub struct TestSimpleLinter {
256 expr: Box<dyn Expr>,
257 }
258
259 impl Default for TestSimpleLinter {
260 fn default() -> Self {
261 Self {
262 expr: Box::new(FixedPhrase::from_phrase("two")),
263 }
264 }
265 }
266
267 impl ExprLinter for TestSimpleLinter {
268 type Unit = Chunk;
269
270 fn expr(&self) -> &dyn Expr {
271 &*self.expr
272 }
273
274 fn match_to_lint(&self, toks: &[Token], _src: &[char]) -> Option<Lint> {
275 Some(Lint {
276 span: toks.span()?,
277 message: "simple".to_owned(),
278 suggestions: vec![Suggestion::ReplaceWith(vec!['2'])],
279 ..Default::default()
280 })
281 }
282
283 fn description(&self) -> &str {
284 "test linter"
285 }
286 }
287
288 pub struct TestContextLinter {
289 expr: Box<dyn Expr>,
290 }
291
292 impl Default for TestContextLinter {
293 fn default() -> Self {
294 Self {
295 expr: Box::new(FixedPhrase::from_phrase("two")),
296 }
297 }
298 }
299
300 impl ExprLinter for TestContextLinter {
301 type Unit = Chunk;
302
303 fn expr(&self) -> &dyn Expr {
304 &*self.expr
305 }
306
307 fn match_to_lint_with_context(
308 &self,
309 toks: &[Token],
310 src: &[char],
311 context: Option<(&[Token], &[Token])>,
312 ) -> Option<Lint> {
313 if let Some((before, after)) = context {
314 let before = before.span()?.get_content_string(src);
315 let after = after.span()?.get_content_string(src);
316
317 let (message, suggestions) = if before.eq_ignore_ascii_case("one ")
318 && after.eq_ignore_ascii_case(" three")
319 {
320 (
321 "ascending".to_owned(),
322 vec![Suggestion::ReplaceWith(vec!['>'])],
323 )
324 } else if before.eq_ignore_ascii_case("three ")
325 && after.eq_ignore_ascii_case(" one")
326 {
327 (
328 "descending".to_owned(),
329 vec![Suggestion::ReplaceWith(vec!['<'])],
330 )
331 } else {
332 ("dunno".to_owned(), vec![Suggestion::ReplaceWith(vec!['?'])])
333 };
334
335 return Some(Lint {
336 span: toks.span()?,
337 message,
338 suggestions,
339 ..Default::default()
340 });
341 } else {
342 None
343 }
344 }
345
346 fn description(&self) -> &str {
347 "context linter"
348 }
349 }
350
351 pub struct TestSentenceLinter {
352 expr: Box<dyn Expr>,
353 }
354
355 impl Default for TestSentenceLinter {
356 fn default() -> Self {
357 Self {
358 expr: Box::new(FixedPhrase::from_phrase("two, two")),
359 }
360 }
361 }
362
363 impl ExprLinter for TestSentenceLinter {
364 type Unit = Sentence;
365
366 fn expr(&self) -> &dyn Expr {
367 self.expr.as_ref()
368 }
369
370 fn match_to_lint(&self, toks: &[Token], _src: &[char]) -> Option<Lint> {
371 Some(Lint {
372 span: toks.span()?,
373 message: "sentence".to_owned(),
374 suggestions: vec![Suggestion::ReplaceWith(vec!['2', '&', '2'])],
375 ..Default::default()
376 })
377 }
378
379 fn description(&self) -> &str {
380 "sentence linter"
381 }
382 }
383
384 #[test]
385 fn simple_test_123() {
386 assert_suggestion_result("one two three", TestSimpleLinter::default(), "one 2 three");
387 }
388
389 #[test]
390 fn context_test_123() {
391 assert_suggestion_result("one two three", TestContextLinter::default(), "one > three");
392 }
393
394 #[test]
395 fn context_test_321() {
396 assert_suggestion_result("three two one", TestContextLinter::default(), "three < one");
397 }
398
399 #[test]
400 fn sentence_test_123() {
401 assert_suggestion_result(
402 "one, two, two, three",
403 TestSentenceLinter::default(),
404 "one, 2&2, three",
405 );
406 }
407}