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