1mod a_part;
6mod a_while;
7mod addicting;
8mod adjective_double_degree;
9mod adjective_of_a;
10mod after_later;
11mod all_hell_break_loose;
12mod all_intents_and_purposes;
13mod allow_to;
14mod am_in_the_morning;
15mod amounts_for;
16mod an_a;
17mod and_the_like;
18mod another_thing_coming;
19mod another_think_coming;
20mod apart_from;
21mod arrive_to;
22mod ask_no_preposition;
23mod aspire_to;
24mod avoid_curses;
25mod back_in_the_day;
26mod be_adjective_confusions;
27mod be_allowed;
28mod behind_the_scenes;
29mod best_of_all_time;
30mod boring_words;
31mod bought;
32mod brand_brandish;
33mod by_accident;
34mod call_them;
35mod cant;
36mod capitalize_personal_pronouns;
37mod cautionary_tale;
38mod change_tack;
39mod chock_full;
40mod closed_compounds;
41mod comma_fixes;
42mod compound_nouns;
43mod compound_subject_i;
44mod confident;
45mod correct_number_suffix;
46mod criteria_phenomena;
47mod cure_for;
48mod currency_placement;
49mod damages;
50mod dashes;
51mod day_and_age;
52mod despite_it_is;
53mod despite_of;
54mod determiner_without_noun;
55mod did_past;
56mod didnt;
57mod discourse_markers;
58mod disjoint_prefixes;
59mod do_mistake;
60mod dot_initialisms;
61mod double_click;
62mod double_modal;
63mod ellipsis_length;
64mod else_possessive;
65mod ever_every;
66mod everyday;
67mod except_of;
68mod expand_memory_shorthands;
69mod expand_people;
70mod expand_time_shorthands;
71mod expr_linter;
72mod far_be_it;
73mod fascinated_by;
74mod fed_up_with;
75mod feel_fell;
76mod few_units_of_time_ago;
77mod filler_words;
78mod find_fine;
79mod first_aid_kit;
80mod flesh_out_vs_full_fledged;
81mod for_noun;
82mod free_predicate;
83mod friend_of_me;
84mod go_so_far_as_to;
85mod go_to_war;
86mod good_at;
87mod handful;
88mod have_pronoun;
89mod have_take_a_look;
90mod hedging;
91mod hello_greeting;
92mod hereby;
93mod hop_hope;
94mod hope_youre;
95mod how_to;
96mod hyphenate_number_day;
97mod i_am_agreement;
98mod if_wouldve;
99mod in_on_the_cards;
100mod in_time_from_now;
101mod inflected_verb_after_to;
102mod initialism_linter;
103mod initialisms;
104mod interested_in;
105mod it_is;
106mod it_looks_like_that;
107mod it_would_be;
108mod its_contraction;
109mod its_possessive;
110mod jealous_of;
111mod johns_hopkins;
112mod lead_rise_to;
113mod left_right_hand;
114mod less_worse;
115mod let_to_do;
116mod lets_confusion;
117mod likewise;
118mod lint;
119mod lint_group;
120mod lint_kind;
121mod long_sentences;
122mod look_down_ones_nose;
123mod looking_forward_to;
124mod map_phrase_linter;
125mod map_phrase_set_linter;
126mod mass_nouns;
127mod means_a_lot_to;
128mod merge_linters;
129mod merge_words;
130mod missing_preposition;
131mod missing_space;
132mod missing_to;
133mod misspell;
134mod mixed_bag;
135mod modal_be_adjective;
136mod modal_of;
137mod modal_seem;
138mod months;
139mod more_adjective;
140mod more_better;
141mod most_number;
142mod most_of_the_times;
143mod multiple_frequency_adverbs;
144mod multiple_sequential_pronouns;
145mod nail_on_the_head;
146mod need_to_noun;
147mod no_french_spaces;
148mod no_longer;
149mod no_match_for;
150mod no_oxford_comma;
151mod nobody;
152mod nominal_wants;
153mod nor_modal_pronoun;
154mod not_only_inversion;
155mod noun_verb_confusion;
156mod number_suffix_capitalization;
157mod obsess_preposition;
158mod of_course;
159mod oldest_in_the_book;
160mod on_floor;
161mod once_or_twice;
162mod one_and_the_same;
163mod one_of_the_singular;
164mod open_compounds;
165mod open_the_light;
166mod orthographic_consistency;
167mod ought_to_be;
168mod out_of_date;
169mod oxford_comma;
170mod oxymorons;
171mod phrasal_verb_as_compound_noun;
172mod phrase_set_corrections;
173mod pique_interest;
174mod plural_decades;
175mod plural_wrong_word_of_phrase;
176mod possessive_noun;
177mod possessive_your;
178mod progressive_needs_be;
179mod pronoun_are;
180mod pronoun_contraction;
181mod pronoun_inflection_be;
182mod pronoun_knew;
183mod pronoun_verb_agreement;
184mod proper_noun_capitalization_linters;
185mod quantifier_needs_of;
186mod quantifier_numeral_conflict;
187mod quite_quiet;
188mod quote_spacing;
189mod reason_for_doing;
190mod redundant_acronyms;
191mod redundant_additive_adverbs;
192mod redundant_progressive_comparative;
193mod regionalisms;
194mod regular_irregulars;
195mod repeated_words;
196mod respond;
197mod right_click;
198mod rise_the_ranks;
199mod roller_skated;
200mod safe_to_save;
201mod save_to_safe;
202mod sentence_capitalization;
203mod shoot_oneself_in_the_foot;
204mod simple_past_to_past_participle;
205mod since_duration;
206mod single_be;
207mod sneaked_snuck;
208mod some_without_article;
209mod something_is;
210mod somewhat_something;
211mod soon_to_be;
212mod sought_after;
213mod spaces;
214mod spell_check;
215mod spelled_numbers;
216mod split_words;
217mod subject_pronoun;
218mod suggestion;
219mod take_a_look_to;
220mod take_medicine;
221mod take_serious;
222mod that_than;
223mod that_which;
224mod the_how_why;
225mod the_my;
226mod the_point_for;
227mod the_proper_noun_possessive;
228mod then_than;
229mod theres;
230mod theses_these;
231mod theyre_confusions;
232mod thing_think;
233mod this_type_of_thing;
234mod though_thought;
235mod thrive_on;
236mod throw_away;
237mod throw_rubbish;
238mod to_adverb;
239mod to_two_too;
240mod touristic;
241mod transposed_space;
242mod try_ones_hand_at;
243mod try_ones_luck;
244mod unclosed_quotes;
245mod update_place_names;
246mod use_ellipsis_character;
247mod use_title_case;
248mod verb_to_adjective;
249mod very_unique;
250mod vice_versa;
251mod vicious_loop;
252mod was_aloud;
253mod way_too_adjective;
254mod web_scraping;
255mod weir_rules;
256mod well_educated;
257mod were_where;
258mod whereas;
259mod whom_subject_of_verb;
260mod widely_accepted;
261mod will_non_lemma;
262mod win_prize;
263mod wish_could;
264mod wordpress_dotcom;
265mod worth_to_do;
266mod would_never_have;
267mod wrong_apostrophe;
268
269pub use expr_linter::{Chunk, ExprLinter};
270pub use initialism_linter::InitialismLinter;
271pub use lint::Lint;
272pub use lint_group::{
273 FlatConfig, HumanReadableSetting, HumanReadableStructuredConfig, LintGroup, StructuredConfig,
274};
275pub use lint_kind::LintKind;
276pub use map_phrase_linter::MapPhraseLinter;
277pub use map_phrase_set_linter::MapPhraseSetLinter;
278pub use suggestion::{Suggestion, SuggestionCollectionExt};
279
280use crate::{Document, LSend, render_markdown};
281
282pub trait Linter: LSend {
288 fn lint(&mut self, document: &Document) -> Vec<Lint>;
291 fn description(&self) -> &str;
294}
295
296pub trait HtmlDescriptionLinter {
298 fn description_html(&self) -> String;
299}
300
301impl<L: ?Sized> HtmlDescriptionLinter for L
302where
303 L: Linter,
304{
305 fn description_html(&self) -> String {
306 let desc = self.description();
307 render_markdown(desc)
308 }
309}
310
311pub mod debug {
312 use crate::Token;
313
314 pub fn format_lint_match(
329 log: &[Token],
330 ctx: Option<(&[Token], &[Token])>,
331 src: &[char],
332 ) -> String {
333 let fmt = |tokens: &[Token]| {
334 tokens
335 .iter()
336 .filter(|t| !t.kind.is_unlintable())
337 .map(|t| t.get_str(src))
338 .collect::<String>()
339 };
340
341 if let Some((pro, epi)) = ctx {
342 format!(
343 "\x1b[2m{}\x1b[0m{}\x1b[2m{}\x1b[0m",
344 fmt(pro),
345 fmt(log),
346 fmt(epi)
347 )
348 } else {
349 fmt(log)
350 }
351 }
352}
353
354#[cfg(test)]
355pub mod tests {
356 use crate::{Document, Span, Token, linting::Linter};
357 use hashbrown::HashSet;
358
359 pub trait SpanVecExt {
361 fn to_strings(&self, doc: &Document) -> Vec<String>;
362 }
363
364 impl SpanVecExt for Vec<Span<Token>> {
365 fn to_strings(&self, doc: &Document) -> Vec<String> {
366 self.iter()
367 .map(|sp| {
368 doc.get_tokens()[sp.start..sp.end]
369 .iter()
370 .map(|tok| doc.get_span_content_str(&tok.span))
371 .collect::<String>()
372 })
373 .collect()
374 }
375 }
376
377 use crate::{
379 CharStringExt, Lint, TokenStringExt,
380 linting::{LintKind, Suggestion},
381 };
382
383 pub type TestLinterMap<'a> = &'a [(&'a [&'a str], &'a [&'a str])];
386
387 #[derive(Clone)]
388 pub struct TestLinter<'a> {
389 map: TestLinterMap<'a>,
390 }
391 impl<'a> TestLinter<'a> {
392 pub fn new(map: TestLinterMap<'a>) -> Self {
393 Self { map }
394 }
395 }
396 impl<'a> Linter for TestLinter<'a> {
397 fn lint(&mut self, doc: &Document) -> Vec<Lint> {
398 let mut corr: Vec<(Span<char>, &[char], &[&str])> = Vec::new();
399 for wordtok in doc.iter_words() {
400 let wordspan = wordtok.span;
401 let word_chars = wordspan.get_content(doc.get_source());
402 for (errors, fixes) in self.map {
404 if errors.iter().any(|&e| word_chars.eq_str(e)) {
406 corr.push((wordspan, word_chars, fixes))
407 }
408 }
409 }
410 corr.iter()
411 .map(|(ws, wch, cstr)| {
412 let suggestions: Vec<Suggestion> = cstr
414 .iter()
415 .map(|&suggestion_str| {
416 Suggestion::replace_with_match_case(
417 suggestion_str.chars().collect(),
418 wch.to_owned(),
419 )
420 })
421 .collect();
422
423 Lint {
424 span: *ws,
425 lint_kind: LintKind::Spelling,
426 suggestions,
427 message: "Test linter for 'linting assertion' tests".to_string(),
428 ..Default::default()
429 }
430 })
431 .collect()
432 }
433 fn description(&self) -> &str {
434 "Test linter for 'linting assertion' tests"
435 }
436 }
437
438 mod linter_tests {
440 use super::{TestLinter, assert_suggestion_result};
441
442 #[test]
443 fn test_1_to_1_error_to_fix() {
444 assert_suggestion_result("bad", TestLinter::new(&[(&["bad"], &["good"])]), "good");
445 }
446
447 #[test]
448 fn test_1_to_2_error_to_fixes() {
449 let linter = TestLinter::new(&[(&["bad"], &["good1", "good2"])]);
450 assert_suggestion_result("bad", linter.clone(), "good1");
451 assert_suggestion_result("bad", linter, "good2");
452 }
453
454 #[test]
455 fn test_2_to_1_errors_to_fix() {
456 let linter = TestLinter::new(&[(&["bad1", "bad2"], &["good"])]);
457 assert_suggestion_result("bad1", linter.clone(), "good");
458 assert_suggestion_result("bad2", linter, "good");
459 }
460
461 #[test]
462 fn test_2_to_2_errors_to_fixes() {
463 let linter = TestLinter::new(&[(&["bad1", "bad2"], &["good1", "good2"])]);
464 assert_suggestion_result("bad1", linter.clone(), "good1");
465 assert_suggestion_result("bad2", linter.clone(), "good2");
466 assert_suggestion_result("bad1", linter.clone(), "good2");
467 assert_suggestion_result("bad2", linter, "good1");
468 }
469 }
470
471 #[track_caller]
472 pub fn assert_no_lints(text: &str, linter: impl Linter) {
473 assert_lint_count(text, linter, 0);
474 }
475
476 #[test]
477 fn verify_no_lints() {
478 assert_no_lints("hello world", TestLinter::new(&[]));
479 }
480
481 #[track_caller]
482 pub fn assert_lint_count(text: &str, mut linter: impl Linter, count: usize) {
483 let test = Document::new_plain_english_curated(text);
484 let lints = linter.lint(&test);
485 if lints.len() != count {
487 panic!(
488 "Expected \"{text}\" to create {count} lints, but it created {}.",
489 lints.len()
490 );
491 }
492 }
493
494 #[test]
495 fn verify_1_lint() {
496 assert_lint_count(
497 "heloo world",
498 TestLinter::new(&[(&["heloo"], &["hello"])]),
499 1,
500 );
501 }
502
503 #[test]
504 fn verify_2_lints() {
505 assert_lint_count(
506 "heloo wolrd",
507 TestLinter::new(&[(&["heloo"], &["hello"]), (&["wolrd"], &["world"])]),
508 2,
509 );
510 }
511
512 #[track_caller]
515 pub fn assert_suggestion_count(text: &str, mut linter: impl Linter, count: usize) {
516 let test = Document::new_plain_english_curated(text);
517 let lints = linter.lint(&test);
518 eprintln!(
519 "{}",
520 lints
521 .iter()
522 .map(|l| l
523 .suggestions
524 .iter()
525 .map(|s| s.to_string())
526 .collect::<Vec<_>>()
527 .join(", "))
528 .collect::<Vec<_>>()
529 .join("\n")
530 );
531 assert_eq!(
532 lints.iter().map(|l| l.suggestions.len()).sum::<usize>(),
533 count
534 );
535 }
536
537 #[test]
538 fn verify_no_suggestions() {
539 assert_suggestion_count("afjehwkf", TestLinter::new(&[]), 0);
540 }
541
542 #[test]
543 fn verify_1_suggestion() {
544 assert_suggestion_count(
545 "dictionery",
546 TestLinter::new(&[(&["dictionery"], &["dictionary"])]),
547 1,
548 );
549 }
550
551 #[derive(Debug, Clone, Copy)]
553 enum DocumentType {
554 PlainEnglish,
555 Markdown,
556 }
557
558 fn create_document(chars: &[char], doc_type: DocumentType) -> Document {
560 match doc_type {
561 DocumentType::PlainEnglish => Document::new_plain_english_curated_chars(chars),
562 DocumentType::Markdown => Document::new_markdown_default_curated_chars(chars),
563 }
564 }
565
566 #[track_caller]
576 pub fn assert_suggestion_result(text: &str, mut linter: impl Linter, needle: &str) {
577 if search_for_suggestion(DocumentType::PlainEnglish, text, &mut linter, needle, 0) {
578 return;
579 }
580
581 panic!(
582 "No suggestion sequence produced the expected result.\n\
583 Expected: \"{needle}\""
584 );
585 }
586
587 #[track_caller]
589 pub fn assert_markdown_suggestion_result(text: &str, mut linter: impl Linter, needle: &str) {
590 if !search_for_suggestion(DocumentType::Markdown, text, &mut linter, needle, 0) {
591 panic!("No suggestion sequence produced the expected result.\nExpected: {needle}");
592 }
593 }
594
595 fn search_for_suggestion(
598 doc_type: DocumentType,
599 text: &str,
600 linter: &mut impl Linter,
601 needle: &str,
602 depth: usize,
603 ) -> bool {
604 if depth > 100 {
606 eprintln!("⚠️ Reached depth limit (100)");
607 return false;
608 }
609
610 if text == needle {
612 return true;
613 }
614
615 let chars: Vec<char> = text.chars().collect();
617 let document = create_document(&chars, doc_type);
618 let lints = linter.lint(&document);
619
620 if let Some(lint) = lints.first() {
621 for sug in lint.suggestions.iter() {
622 let mut chars_copy = chars.clone();
623 sug.apply(lint.span, &mut chars_copy);
624 let next: String = chars_copy.iter().collect();
625
626 if search_for_suggestion(doc_type, &next, linter, needle, depth + 1) {
628 return true;
629 }
630 }
631 }
632
633 false
634 }
635
636 #[test]
637 fn verify_fix_one_lint() {
638 assert_suggestion_result(
639 "find the misstake and fix it",
640 TestLinter::new(&[(&["misstake"], &["mistake"])]),
641 "find the mistake and fix it",
642 );
643 }
644
645 #[test]
646 #[should_panic]
647 fn verify_unable_to_fix_one_spanish_lint() {
648 assert_suggestion_result("Hay una orrrer", TestLinter::new(&[]), "Hay una error");
649 }
650
651 #[test]
652 fn verify_fix_two_lints() {
653 assert_suggestion_result(
654 "find two misstakes and fix theem",
655 TestLinter::new(&[(&["misstakes"], &["mistakes"]), (&["theem"], &["them"])]),
656 "find two mistakes and fix them",
657 );
658 }
659
660 #[test]
665 fn verify_fix_five_typos() {
666 assert_suggestion_result(
667 "Please recieve teh payment untill thier authorization occured",
668 TestLinter::new(&[
669 (&["recieve"], &["receive"]),
670 (&["teh"], &["the"]),
671 (&["untill"], &["until"]),
672 (&["thier"], &["their"]),
673 (&["occured"], &["occurred"]),
674 ]),
675 "Please receive the payment until their authorization occurred",
676 );
677 }
678
679 #[track_caller]
681 pub fn assert_not_in_suggestion_result(
682 text: &str,
683 mut linter: impl Linter,
684 bad_suggestion: &str,
685 ) {
686 if !search_for_suggestion(
687 DocumentType::PlainEnglish,
688 text,
689 &mut linter,
690 bad_suggestion,
691 0,
692 ) {
693 return;
694 }
695
696 panic!(
697 "A suggestion sequence produced the undesired result.\n\
698 Undesired: \"{bad_suggestion}\""
699 );
700 }
701
702 #[test]
703 fn verify_sole_suggestion_is_the_one_we_wanted() {
704 assert_not_in_suggestion_result(
705 "Baby cats are called kitens",
706 TestLinter::new(&[]),
707 "Baby cats are called puppies",
708 );
709 }
710
711 #[test]
714 #[should_panic]
715 fn verify_sole_suggestion_not_in_result_fails() {
716 assert_not_in_suggestion_result(
717 "heloo",
718 TestLinter::new(&[(&["heloo"], &["hello"])]),
719 "hello",
720 );
721 }
722
723 #[track_caller]
730 pub fn assert_good_and_bad_suggestions(
731 text: &str,
732 mut linter: impl Linter,
733 good: &[&str],
734 bad: &[&str],
735 ) {
736 let test = Document::new_plain_english_curated(text);
737 let lints = linter.lint(&test);
738
739 let mut unseen_good: HashSet<_> = good.iter().cloned().collect();
740 let mut found_bad = Vec::new();
741 let mut found_good = Vec::new();
742
743 for (i, lint) in lints.into_iter().enumerate() {
744 for (j, suggestion) in lint.suggestions.into_iter().enumerate() {
745 let mut text_chars: Vec<char> = text.chars().collect();
746 suggestion.apply(lint.span, &mut text_chars);
747 let suggestion_text: String = text_chars.into_iter().collect();
748
749 if bad.contains(&&*suggestion_text) {
751 found_bad.push((i, j, suggestion_text.clone()));
752 eprintln!(
753 " ❌ Found bad suggestion at lint[{i}].suggestions[{j}]: \"{suggestion_text}\""
754 );
755 }
756 else if good.contains(&&*suggestion_text) {
758 found_good.push((i, j, suggestion_text.clone()));
759 eprintln!(
760 " ✅ Found good suggestion at lint[{i}].suggestions[{j}]: \"{suggestion_text}\""
761 );
762 unseen_good.remove(suggestion_text.as_str());
763 }
764 }
765 }
766
767 if !found_bad.is_empty() || !unseen_good.is_empty() {
769 eprintln!("\n=== Test Summary ===");
770
771 if !found_bad.is_empty() {
772 eprintln!("\n❌ Found {} bad suggestions:", found_bad.len());
773 for (i, j, text) in &found_bad {
774 eprintln!(" - lint[{i}].suggestions[{j}]: \"{text}\"");
775 }
776 }
777
778 if !unseen_good.is_empty() {
779 eprintln!(
780 "\n❌ Missing {} expected good suggestions:",
781 unseen_good.len()
782 );
783 for text in &unseen_good {
784 eprintln!(" - \"{text}\"");
785 }
786 }
787
788 eprintln!("\n✅ Found {} good suggestions", found_good.len());
789 eprintln!("==================\n");
790
791 if !found_bad.is_empty() || !unseen_good.is_empty() {
792 panic!("Test failed - see error output above");
793 }
794 } else {
795 eprintln!(
796 "\n✅ All {} good suggestions found, no bad suggestions\n",
797 found_good.len()
798 );
799 }
800 }
801
802 #[test]
807 #[should_panic]
808 fn verify_mutal_corrections_cause_failure() {
809 assert_suggestion_result(
810 "gooder",
811 TestLinter::new(&[(&["gooder"], &["more good"])]),
812 "better",
813 );
814 }
815
816 #[track_caller]
818 pub fn assert_lint_message(text: &str, mut linter: impl Linter, expected_message: &str) {
819 let test = Document::new_plain_english_curated(text);
820 let lints = linter.lint(&test);
821
822 if let Some(lint) = lints.first()
824 && lint.message != expected_message
825 {
826 panic!(
827 "Expected lint message \"{expected_message}\", but got \"{}\"",
828 lint.message
829 );
830 }
831 }
832}