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(
204 context: Option<(&[Token], &[Token])>,
205 predicate: impl Fn(&Token) -> bool,
206) -> bool {
207 if let Some((before, _)) = context
208 && let [.., word, ws] = before
209 && ws.kind.is_whitespace()
210 {
211 return predicate(word);
212 }
213 false
214}
215
216#[cfg(test)]
217mod tests_context {
218 use crate::expr::{Expr, FixedPhrase};
219 use crate::linting::expr_linter::{Chunk, Sentence};
220 use crate::linting::tests::assert_suggestion_result;
221 use crate::linting::{ExprLinter, Suggestion};
222 use crate::token_string_ext::TokenStringExt;
223 use crate::{Lint, Token};
224
225 pub struct TestSimpleLinter {
226 expr: Box<dyn Expr>,
227 }
228
229 impl Default for TestSimpleLinter {
230 fn default() -> Self {
231 Self {
232 expr: Box::new(FixedPhrase::from_phrase("two")),
233 }
234 }
235 }
236
237 impl ExprLinter for TestSimpleLinter {
238 type Unit = Chunk;
239
240 fn expr(&self) -> &dyn Expr {
241 &*self.expr
242 }
243
244 fn match_to_lint(&self, toks: &[Token], _src: &[char]) -> Option<Lint> {
245 Some(Lint {
246 span: toks.span()?,
247 message: "simple".to_owned(),
248 suggestions: vec![Suggestion::ReplaceWith(vec!['2'])],
249 ..Default::default()
250 })
251 }
252
253 fn description(&self) -> &str {
254 "test linter"
255 }
256 }
257
258 pub struct TestContextLinter {
259 expr: Box<dyn Expr>,
260 }
261
262 impl Default for TestContextLinter {
263 fn default() -> Self {
264 Self {
265 expr: Box::new(FixedPhrase::from_phrase("two")),
266 }
267 }
268 }
269
270 impl ExprLinter for TestContextLinter {
271 type Unit = Chunk;
272
273 fn expr(&self) -> &dyn Expr {
274 &*self.expr
275 }
276
277 fn match_to_lint_with_context(
278 &self,
279 toks: &[Token],
280 src: &[char],
281 context: Option<(&[Token], &[Token])>,
282 ) -> Option<Lint> {
283 if let Some((before, after)) = context {
284 let before = before.span()?.get_content_string(src);
285 let after = after.span()?.get_content_string(src);
286
287 let (message, suggestions) = if before.eq_ignore_ascii_case("one ")
288 && after.eq_ignore_ascii_case(" three")
289 {
290 (
291 "ascending".to_owned(),
292 vec![Suggestion::ReplaceWith(vec!['>'])],
293 )
294 } else if before.eq_ignore_ascii_case("three ")
295 && after.eq_ignore_ascii_case(" one")
296 {
297 (
298 "descending".to_owned(),
299 vec![Suggestion::ReplaceWith(vec!['<'])],
300 )
301 } else {
302 ("dunno".to_owned(), vec![Suggestion::ReplaceWith(vec!['?'])])
303 };
304
305 return Some(Lint {
306 span: toks.span()?,
307 message,
308 suggestions,
309 ..Default::default()
310 });
311 } else {
312 None
313 }
314 }
315
316 fn description(&self) -> &str {
317 "context linter"
318 }
319 }
320
321 pub struct TestSentenceLinter {
322 expr: Box<dyn Expr>,
323 }
324
325 impl Default for TestSentenceLinter {
326 fn default() -> Self {
327 Self {
328 expr: Box::new(FixedPhrase::from_phrase("two, two")),
329 }
330 }
331 }
332
333 impl ExprLinter for TestSentenceLinter {
334 type Unit = Sentence;
335
336 fn expr(&self) -> &dyn Expr {
337 self.expr.as_ref()
338 }
339
340 fn match_to_lint(&self, toks: &[Token], _src: &[char]) -> Option<Lint> {
341 Some(Lint {
342 span: toks.span()?,
343 message: "sentence".to_owned(),
344 suggestions: vec![Suggestion::ReplaceWith(vec!['2', '&', '2'])],
345 ..Default::default()
346 })
347 }
348
349 fn description(&self) -> &str {
350 "sentence linter"
351 }
352 }
353
354 #[test]
355 fn simple_test_123() {
356 assert_suggestion_result("one two three", TestSimpleLinter::default(), "one 2 three");
357 }
358
359 #[test]
360 fn context_test_123() {
361 assert_suggestion_result("one two three", TestContextLinter::default(), "one > three");
362 }
363
364 #[test]
365 fn context_test_321() {
366 assert_suggestion_result("three two one", TestContextLinter::default(), "three < one");
367 }
368
369 #[test]
370 fn sentence_test_123() {
371 assert_suggestion_result(
372 "one, two, two, three",
373 TestSentenceLinter::default(),
374 "one, 2&2, three",
375 );
376 }
377}