1mod a_part;
6mod addicting;
7mod adjective_double_degree;
8mod adjective_of_a;
9mod after_later;
10mod all_intents_and_purposes;
11mod allow_to;
12mod am_in_the_morning;
13mod amounts_for;
14mod an_a;
15mod and_in;
16mod and_the_like;
17mod another_thing_coming;
18mod another_think_coming;
19mod ask_no_preposition;
20mod avoid_curses;
21mod back_in_the_day;
22mod be_allowed;
23mod best_of_all_time;
24mod boring_words;
25mod bought;
26mod call_them;
27mod cant;
28mod capitalize_personal_pronouns;
29mod cautionary_tale;
30mod change_tack;
31mod chock_full;
32mod closed_compounds;
33mod comma_fixes;
34mod compound_nouns;
35mod compound_subject_i;
36mod confident;
37mod correct_number_suffix;
38mod criteria_phenomena;
39mod currency_placement;
40mod dashes;
41mod despite_of;
42mod determiner_without_noun;
43mod didnt;
44mod discourse_markers;
45mod dot_initialisms;
46mod double_click;
47mod double_modal;
48mod ellipsis_length;
49mod else_possessive;
50mod everyday;
51mod expand_memory_shorthands;
52mod expand_time_shorthands;
53mod expr_linter;
54mod far_be_it;
55mod feel_fell;
56mod few_units_of_time_ago;
57mod filler_words;
58mod first_aid_kit;
59mod for_noun;
60mod free_predicate;
61mod friend_of_me;
62mod go_so_far_as_to;
63mod have_pronoun;
64mod have_take_a_look;
65mod hedging;
66mod hello_greeting;
67mod hereby;
68mod hop_hope;
69mod hope_youre;
70mod how_to;
71mod hyphenate_number_day;
72mod i_am_agreement;
73mod if_wouldve;
74mod in_on_the_cards;
75mod inflected_verb_after_to;
76mod initialism_linter;
77mod initialisms;
78mod interested_in;
79mod it_is;
80mod it_looks_like_that;
81mod it_would_be;
82mod its_contraction;
83mod its_possessive;
84mod left_right_hand;
85mod less_worse;
86mod let_to_do;
87mod lets_confusion;
88mod likewise;
89mod lint;
90mod lint_group;
91mod lint_kind;
92mod long_sentences;
93mod looking_forward_to;
94mod map_phrase_linter;
95mod map_phrase_set_linter;
96mod mass_plurals;
97mod merge_linters;
98mod merge_words;
99mod missing_preposition;
100mod missing_space;
101mod missing_to;
102mod misspell;
103mod mixed_bag;
104mod modal_of;
105mod modal_seem;
106mod months;
107mod more_better;
108mod most_number;
109mod most_of_the_times;
110mod multiple_sequential_pronouns;
111mod nail_on_the_head;
112mod need_to_noun;
113mod no_french_spaces;
114mod no_match_for;
115mod no_oxford_comma;
116mod nobody;
117mod nominal_wants;
118mod noun_countability;
119mod noun_verb_confusion;
120mod number_suffix_capitalization;
121mod of_course;
122mod on_floor;
123mod once_or_twice;
124mod one_and_the_same;
125mod open_compounds;
126mod open_the_light;
127mod orthographic_consistency;
128mod ought_to_be;
129mod out_of_date;
130mod oxford_comma;
131mod oxymorons;
132mod phrasal_verb_as_compound_noun;
133mod phrase_corrections;
134mod phrase_set_corrections;
135mod pique_interest;
136mod possessive_noun;
137mod possessive_your;
138mod progressive_needs_be;
139mod pronoun_are;
140mod pronoun_contraction;
141mod pronoun_inflection_be;
142mod pronoun_knew;
143mod proper_noun_capitalization_linters;
144mod quantifier_needs_of;
145mod quantifier_numeral_conflict;
146mod quite_quiet;
147mod quote_spacing;
148mod redundant_additive_adverbs;
149mod regionalisms;
150mod repeated_words;
151mod roller_skated;
152mod safe_to_save;
153mod save_to_safe;
154mod semicolon_apostrophe;
155mod sentence_capitalization;
156mod shoot_oneself_in_the_foot;
157mod simple_past_to_past_participle;
158mod since_duration;
159mod single_be;
160mod some_without_article;
161mod something_is;
162mod somewhat_something;
163mod sought_after;
164mod spaces;
165mod spell_check;
166mod spelled_numbers;
167mod split_words;
168mod suggestion;
169mod take_serious;
170mod that_than;
171mod that_which;
172mod the_how_why;
173mod the_my;
174mod then_than;
175mod theres;
176mod theses_these;
177mod thing_think;
178mod though_thought;
179mod throw_away;
180mod throw_rubbish;
181mod to_adverb;
182mod to_two_too;
183mod touristic;
184mod unclosed_quotes;
185mod update_place_names;
186mod use_genitive;
187mod verb_to_adjective;
188mod very_unique;
189mod vice_versa;
190mod was_aloud;
191mod way_too_adjective;
192mod well_educated;
193mod whereas;
194mod widely_accepted;
195mod win_prize;
196mod wordpress_dotcom;
197mod would_never_have;
198
199pub use a_part::APart;
200pub use addicting::Addicting;
201pub use adjective_double_degree::AdjectiveDoubleDegree;
202pub use adjective_of_a::AdjectiveOfA;
203pub use after_later::AfterLater;
204pub use all_intents_and_purposes::AllIntentsAndPurposes;
205pub use allow_to::AllowTo;
206pub use am_in_the_morning::AmInTheMorning;
207pub use amounts_for::AmountsFor;
208pub use an_a::AnA;
209pub use and_in::AndIn;
210pub use and_the_like::AndTheLike;
211pub use another_thing_coming::AnotherThingComing;
212pub use another_think_coming::AnotherThinkComing;
213pub use ask_no_preposition::AskNoPreposition;
214pub use avoid_curses::AvoidCurses;
215pub use back_in_the_day::BackInTheDay;
216pub use be_allowed::BeAllowed;
217pub use best_of_all_time::BestOfAllTime;
218pub use boring_words::BoringWords;
219pub use bought::Bought;
220pub use cant::Cant;
221pub use capitalize_personal_pronouns::CapitalizePersonalPronouns;
222pub use cautionary_tale::CautionaryTale;
223pub use change_tack::ChangeTack;
224pub use chock_full::ChockFull;
225pub use comma_fixes::CommaFixes;
226pub use compound_nouns::CompoundNouns;
227pub use compound_subject_i::CompoundSubjectI;
228pub use confident::Confident;
229pub use correct_number_suffix::CorrectNumberSuffix;
230pub use criteria_phenomena::CriteriaPhenomena;
231pub use currency_placement::CurrencyPlacement;
232pub use dashes::Dashes;
233pub use despite_of::DespiteOf;
234pub use didnt::Didnt;
235pub use discourse_markers::DiscourseMarkers;
236pub use dot_initialisms::DotInitialisms;
237pub use double_click::DoubleClick;
238pub use double_modal::DoubleModal;
239pub use ellipsis_length::EllipsisLength;
240pub use everyday::Everyday;
241pub use expand_memory_shorthands::ExpandMemoryShorthands;
242pub use expand_time_shorthands::ExpandTimeShorthands;
243pub use expr_linter::ExprLinter;
244pub use far_be_it::FarBeIt;
245pub use feel_fell::FeelFell;
246pub use few_units_of_time_ago::FewUnitsOfTimeAgo;
247pub use filler_words::FillerWords;
248pub use for_noun::ForNoun;
249pub use free_predicate::FreePredicate;
250pub use friend_of_me::FriendOfMe;
251pub use go_so_far_as_to::GoSoFarAsTo;
252pub use have_pronoun::HavePronoun;
253pub use have_take_a_look::HaveTakeALook;
254pub use hedging::Hedging;
255pub use hello_greeting::HelloGreeting;
256pub use hereby::Hereby;
257pub use hop_hope::HopHope;
258pub use how_to::HowTo;
259pub use hyphenate_number_day::HyphenateNumberDay;
260pub use i_am_agreement::IAmAgreement;
261pub use if_wouldve::IfWouldve;
262pub use in_on_the_cards::InOnTheCards;
263pub use inflected_verb_after_to::InflectedVerbAfterTo;
264pub use initialism_linter::InitialismLinter;
265pub use interested_in::InterestedIn;
266pub use it_looks_like_that::ItLooksLikeThat;
267pub use its_contraction::ItsContraction;
268pub use its_possessive::ItsPossessive;
269pub use left_right_hand::LeftRightHand;
270pub use less_worse::LessWorse;
271pub use let_to_do::LetToDo;
272pub use lets_confusion::LetsConfusion;
273pub use likewise::Likewise;
274pub use lint::Lint;
275pub use lint_group::{LintGroup, LintGroupConfig};
276pub use lint_kind::LintKind;
277pub use long_sentences::LongSentences;
278pub use looking_forward_to::LookingForwardTo;
279pub use map_phrase_linter::MapPhraseLinter;
280pub use map_phrase_set_linter::MapPhraseSetLinter;
281pub use mass_plurals::MassPlurals;
282pub use merge_words::MergeWords;
283pub use missing_preposition::MissingPreposition;
284pub use missing_to::MissingTo;
285pub use misspell::Misspell;
286pub use mixed_bag::MixedBag;
287pub use modal_of::ModalOf;
288pub use modal_seem::ModalSeem;
289pub use months::Months;
290pub use more_better::MoreBetter;
291pub use most_number::MostNumber;
292pub use most_of_the_times::MostOfTheTimes;
293pub use multiple_sequential_pronouns::MultipleSequentialPronouns;
294pub use nail_on_the_head::NailOnTheHead;
295pub use need_to_noun::NeedToNoun;
296pub use no_french_spaces::NoFrenchSpaces;
297pub use no_match_for::NoMatchFor;
298pub use no_oxford_comma::NoOxfordComma;
299pub use nobody::Nobody;
300pub use noun_countability::NounCountability;
301pub use noun_verb_confusion::NounVerbConfusion;
302pub use number_suffix_capitalization::NumberSuffixCapitalization;
303pub use of_course::OfCourse;
304pub use on_floor::OnFloor;
305pub use once_or_twice::OnceOrTwice;
306pub use one_and_the_same::OneAndTheSame;
307pub use open_the_light::OpenTheLight;
308pub use orthographic_consistency::OrthographicConsistency;
309pub use ought_to_be::OughtToBe;
310pub use out_of_date::OutOfDate;
311pub use oxford_comma::OxfordComma;
312pub use oxymorons::Oxymorons;
313pub use phrasal_verb_as_compound_noun::PhrasalVerbAsCompoundNoun;
314pub use pique_interest::PiqueInterest;
315pub use possessive_noun::PossessiveNoun;
316pub use possessive_your::PossessiveYour;
317pub use progressive_needs_be::ProgressiveNeedsBe;
318pub use pronoun_are::PronounAre;
319pub use pronoun_contraction::PronounContraction;
320pub use pronoun_inflection_be::PronounInflectionBe;
321pub use quantifier_needs_of::QuantifierNeedsOf;
322pub use quantifier_numeral_conflict::QuantifierNumeralConflict;
323pub use quite_quiet::QuiteQuiet;
324pub use quote_spacing::QuoteSpacing;
325pub use redundant_additive_adverbs::RedundantAdditiveAdverbs;
326pub use regionalisms::Regionalisms;
327pub use repeated_words::RepeatedWords;
328pub use roller_skated::RollerSkated;
329pub use safe_to_save::SafeToSave;
330pub use save_to_safe::SaveToSafe;
331pub use semicolon_apostrophe::SemicolonApostrophe;
332pub use sentence_capitalization::SentenceCapitalization;
333pub use shoot_oneself_in_the_foot::ShootOneselfInTheFoot;
334pub use simple_past_to_past_participle::SimplePastToPastParticiple;
335pub use since_duration::SinceDuration;
336pub use single_be::SingleBe;
337pub use some_without_article::SomeWithoutArticle;
338pub use something_is::SomethingIs;
339pub use somewhat_something::SomewhatSomething;
340pub use sought_after::SoughtAfter;
341pub use spaces::Spaces;
342pub use spell_check::SpellCheck;
343pub use spelled_numbers::SpelledNumbers;
344pub use split_words::SplitWords;
345pub use suggestion::Suggestion;
346pub use take_serious::TakeSerious;
347pub use that_than::ThatThan;
348pub use that_which::ThatWhich;
349pub use the_how_why::TheHowWhy;
350pub use the_my::TheMy;
351pub use then_than::ThenThan;
352pub use theres::Theres;
353pub use theses_these::ThesesThese;
354pub use thing_think::ThingThink;
355pub use though_thought::ThoughThought;
356pub use throw_away::ThrowAway;
357pub use throw_rubbish::ThrowRubbish;
358pub use to_adverb::ToAdverb;
359pub use to_two_too::ToTwoToo;
360pub use touristic::Touristic;
361pub use unclosed_quotes::UnclosedQuotes;
362pub use update_place_names::UpdatePlaceNames;
363pub use use_genitive::UseGenitive;
364pub use verb_to_adjective::VerbToAdjective;
365pub use very_unique::VeryUnique;
366pub use vice_versa::ViceVersa;
367pub use was_aloud::WasAloud;
368pub use way_too_adjective::WayTooAdjective;
369pub use well_educated::WellEducated;
370pub use whereas::Whereas;
371pub use widely_accepted::WidelyAccepted;
372pub use win_prize::WinPrize;
373pub use wordpress_dotcom::WordPressDotcom;
374pub use would_never_have::WouldNeverHave;
375
376use crate::{Document, LSend, render_markdown};
377
378pub trait Linter: LSend {
384 fn lint(&mut self, document: &Document) -> Vec<Lint>;
387 fn description(&self) -> &str;
390}
391
392pub trait HtmlDescriptionLinter {
394 fn description_html(&self) -> String;
395}
396
397impl<L: ?Sized> HtmlDescriptionLinter for L
398where
399 L: Linter,
400{
401 fn description_html(&self) -> String {
402 let desc = self.description();
403 render_markdown(desc)
404 }
405}
406
407#[cfg(test)]
408pub mod tests {
409 use crate::{Document, Span, Token, parsers::PlainEnglish};
410 use hashbrown::HashSet;
411
412 pub trait SpanVecExt {
414 fn to_strings(&self, doc: &Document) -> Vec<String>;
415 }
416
417 impl SpanVecExt for Vec<Span<Token>> {
418 fn to_strings(&self, doc: &Document) -> Vec<String> {
419 self.iter()
420 .map(|sp| {
421 doc.get_tokens()[sp.start..sp.end]
422 .iter()
423 .map(|tok| doc.get_span_content_str(&tok.span))
424 .collect::<String>()
425 })
426 .collect()
427 }
428 }
429
430 use super::Linter;
431 use crate::spell::FstDictionary;
432
433 #[track_caller]
434 pub fn assert_no_lints(text: &str, linter: impl Linter) {
435 assert_lint_count(text, linter, 0);
436 }
437
438 #[track_caller]
439 pub fn assert_lint_count(text: &str, mut linter: impl Linter, count: usize) {
440 let test = Document::new_markdown_default_curated(text);
441 let lints = linter.lint(&test);
442 dbg!(&lints);
443 if lints.len() != count {
444 panic!(
445 "Expected \"{text}\" to create {count} lints, but it created {}.",
446 lints.len()
447 );
448 }
449 }
450
451 #[track_caller]
454 pub fn assert_suggestion_count(text: &str, mut linter: impl Linter, count: usize) {
455 let test = Document::new_markdown_default_curated(text);
456 let lints = linter.lint(&test);
457 assert_eq!(
458 lints.iter().map(|l| l.suggestions.len()).sum::<usize>(),
459 count
460 );
461 }
462
463 #[track_caller]
466 pub fn assert_suggestion_result(text: &str, linter: impl Linter, expected_result: &str) {
467 assert_nth_suggestion_result(text, linter, expected_result, 0);
468 }
469
470 #[track_caller]
475 pub fn assert_nth_suggestion_result(
476 text: &str,
477 mut linter: impl Linter,
478 expected_result: &str,
479 n: usize,
480 ) {
481 let transformed_str = transform_nth_str(text, &mut linter, n);
482
483 if transformed_str.as_str() != expected_result {
484 panic!(
485 "Expected \"{transformed_str}\" to be \"{expected_result}\" after applying the computed suggestions."
486 );
487 }
488
489 assert_lint_count(&transformed_str, linter, 0);
491 }
492
493 #[track_caller]
494 pub fn assert_top3_suggestion_result(
495 text: &str,
496 mut linter: impl Linter,
497 expected_result: &str,
498 ) {
499 let zeroth = transform_nth_str(text, &mut linter, 0);
500 let first = transform_nth_str(text, &mut linter, 1);
501 let second = transform_nth_str(text, &mut linter, 2);
502
503 match (
504 zeroth.as_str() == expected_result,
505 first.as_str() == expected_result,
506 second.as_str() == expected_result,
507 ) {
508 (true, false, false) => assert_lint_count(&zeroth, linter, 0),
509 (false, true, false) => assert_lint_count(&first, linter, 0),
510 (false, false, true) => assert_lint_count(&second, linter, 0),
511 (false, false, false) => panic!(
512 "None of the top 3 suggestions produced the expected result:\n\
513 Expected: \"{expected_result}\"\n\
514 Got:\n\
515 [0]: \"{zeroth}\"\n\
516 [1]: \"{first}\"\n\
517 [2]: \"{second}\""
518 ),
519 _ => {}
521 }
522 }
523
524 #[track_caller]
526 pub fn assert_not_in_suggestion_result(
527 text: &str,
528 mut linter: impl Linter,
529 bad_suggestion: &str,
530 ) {
531 let test = Document::new_markdown_default_curated(text);
532 let lints = linter.lint(&test);
533
534 for (i, lint) in lints.iter().enumerate() {
535 for (j, suggestion) in lint.suggestions.iter().enumerate() {
536 let mut text_chars: Vec<char> = text.chars().collect();
537 suggestion.apply(lint.span, &mut text_chars);
538 let suggestion_text: String = text_chars.into_iter().collect();
539
540 if suggestion_text == bad_suggestion {
541 panic!(
542 "Found undesired suggestion at lint[{i}].suggestions[{j}]:\n\
543 Expected to not find suggestion: \"{bad_suggestion}\"\n\
544 But found: \"{suggestion_text}\""
545 );
546 }
547 }
548 }
549 }
550
551 #[track_caller]
554 pub fn assert_good_and_bad_suggestions(
555 text: &str,
556 mut linter: impl Linter,
557 good: &[&str],
558 bad: &[&str],
559 ) {
560 let test = Document::new_markdown_default_curated(text);
561 let lints = linter.lint(&test);
562
563 let mut unseen_good: HashSet<_> = good.iter().cloned().collect();
564 let mut found_bad = Vec::new();
565 let mut found_good = Vec::new();
566
567 for (i, lint) in lints.into_iter().enumerate() {
568 for (j, suggestion) in lint.suggestions.into_iter().enumerate() {
569 let mut text_chars: Vec<char> = text.chars().collect();
570 suggestion.apply(lint.span, &mut text_chars);
571 let suggestion_text: String = text_chars.into_iter().collect();
572
573 if bad.contains(&&*suggestion_text) {
575 found_bad.push((i, j, suggestion_text.clone()));
576 eprintln!(
577 " ❌ Found bad suggestion at lint[{i}].suggestions[{j}]: \"{suggestion_text}\""
578 );
579 }
580 else if good.contains(&&*suggestion_text) {
582 found_good.push((i, j, suggestion_text.clone()));
583 eprintln!(
584 " ✅ Found good suggestion at lint[{i}].suggestions[{j}]: \"{suggestion_text}\""
585 );
586 unseen_good.remove(suggestion_text.as_str());
587 }
588 }
589 }
590
591 if !found_bad.is_empty() || !unseen_good.is_empty() {
593 eprintln!("\n=== Test Summary ===");
594
595 if !found_bad.is_empty() {
597 eprintln!("\n❌ Found {} bad suggestions:", found_bad.len());
598 for (i, j, text) in &found_bad {
599 eprintln!(" - lint[{i}].suggestions[{j}]: \"{text}\"");
600 }
601 }
602
603 if !unseen_good.is_empty() {
605 eprintln!(
606 "\n❌ Missing {} expected good suggestions:",
607 unseen_good.len()
608 );
609 for text in &unseen_good {
610 eprintln!(" - \"{text}\"");
611 }
612 }
613
614 eprintln!("\n✅ Found {} good suggestions", found_good.len());
615 eprintln!("==================\n");
616
617 if !found_bad.is_empty() || !unseen_good.is_empty() {
618 panic!("Test failed - see error output above");
619 }
620 } else {
621 eprintln!(
622 "\n✅ All {} good suggestions found, no bad suggestions\n",
623 found_good.len()
624 );
625 }
626 }
627
628 fn transform_nth_str(text: &str, linter: &mut impl Linter, n: usize) -> String {
629 let mut text_chars: Vec<char> = text.chars().collect();
630
631 let mut iter_count = 0;
632
633 loop {
634 let test = Document::new_from_vec(
635 text_chars.clone().into(),
636 &PlainEnglish,
637 &FstDictionary::curated(),
638 );
639 let lints = linter.lint(&test);
640
641 if let Some(lint) = lints.first() {
642 if let Some(sug) = lint.suggestions.get(n) {
643 sug.apply(lint.span, &mut text_chars);
644
645 let transformed_str: String = text_chars.iter().collect();
646 dbg!(transformed_str);
647 } else {
648 break;
649 }
650 } else {
651 break;
652 }
653
654 iter_count += 1;
655
656 if iter_count == 100 {
657 break;
658 }
659 }
660
661 eprintln!("Corrected {iter_count} times.");
662
663 text_chars.iter().collect()
664 }
665}