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