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