1mod flat_config;
2mod structured_config;
3
4use std::{collections::BTreeMap, hash::BuildHasher, num::NonZero, sync::Arc};
5
6use {foldhash::quality::RandomState, hashbrown::HashMap, lru::LruCache};
7
8use super::a_part::APart;
10use super::a_some_time::ASomeTime;
11use super::a_ways_to_go::AWaysToGo;
12use super::a_while::AWhile;
13use super::accuse_of::AccuseOf;
14use super::addicting::Addicting;
15use super::adjective_double_degree::AdjectiveDoubleDegree;
16use super::adjective_of_a::AdjectiveOfA;
17use super::after_later::AfterLater;
18use super::all_hell_break_loose::AllHellBreakLoose;
19use super::all_intents_and_purposes::AllIntentsAndPurposes;
20use super::allow_to::AllowTo;
21use super::am_in_the_morning::AmInTheMorning;
22use super::amounts_for::AmountsFor;
23use super::an_a::AnA;
24use super::analog_acoustic_bike::AnalogAcousticBike;
25use super::and_the_like::AndTheLike;
26use super::another_thing_coming::AnotherThingComing;
27use super::another_think_coming::AnotherThinkComing;
28use super::apart_from::ApartFrom;
29use super::arrive_to::ArriveTo;
30use super::as_how::AsHow;
31use super::as_to_interrogative::AsToInterrogative;
32use super::ask_no_preposition::AskNoPreposition;
33use super::aspire_to::AspireTo;
34use super::avoid_contractions::AvoidContractions;
35use super::avoid_curses::AvoidCurses;
36use super::back_in_the_day::BackInTheDay;
37use super::barely_un::BarelyUn;
38use super::be_allowed::BeAllowed;
39use super::behind_the_scenes::BehindTheScenes;
40use super::best_of_all_time::BestOfAllTime;
41use super::boring_words::BoringWords;
42use super::bought::Bought;
43use super::brand_brandish::BrandBrandish;
44use super::by_accident::ByAccident;
45use super::by_ones_own::ByOnesOwn;
46use super::by_the_book::ByTheBook;
47use super::call_it_quits::CallItQuits;
48use super::call_them::CallThem;
49use super::cant::Cant;
50use super::capitalize_personal_pronouns::CapitalizePersonalPronouns;
51use super::catch_22::Catch22;
52use super::cautionary_tale::CautionaryTale;
53use super::change_tack::ChangeTack;
54use super::chock_full::ChockFull;
55use super::close_tight_knit::CloseTightKnit;
56use super::code_in_write_in::CodeInWriteIn;
57use super::comma_fixes::CommaFixes;
58use super::complain_as_noun::ComplainAsNoun;
59use super::compound_nouns::CompoundNouns;
60use super::compound_subject_i::CompoundSubjectI;
61use super::confident::Confident;
62use super::convenient_store::ConvenientStore;
63use super::correct_number_suffix::CorrectNumberSuffix;
64use super::crave_for::CraveFor;
65use super::criteria_phenomena::CriteriaPhenomena;
66use super::cure_for::CureFor;
67use super::currency_placement::CurrencyPlacement;
68use super::damages::Damages;
69use super::day_and_age::DayAndAge;
70use super::despite_it_is::DespiteItIs;
71use super::despite_of::DespiteOf;
72use super::did_past::DidPast;
73use super::didnt::Didnt;
74use super::discourse_markers::DiscourseMarkers;
75use super::disjoint_prefixes::DisjointPrefixes;
76use super::do_mistake::DoMistake;
77use super::dot_initialisms::DotInitialisms;
78use super::double_click::DoubleClick;
79use super::double_modal::DoubleModal;
80use super::ellipsis_length::EllipsisLength;
81use super::else_possessive::ElsePossessive;
82use super::ever_every::EverEvery;
83use super::everyday::Everyday;
84use super::except_of::ExceptOf;
85use super::expand_memory_shorthands::ExpandMemoryShorthands;
86use super::expand_people::ExpandPeople;
87use super::expand_time_shorthands::ExpandTimeShorthands;
88use super::fall_below::FallBelow;
89use super::far_be_it::FarBeIt;
90use super::fascinated_by::FascinatedBy;
91use super::fed_up_with::FedUpWith;
92use super::feel_fell::FeelFell;
93use super::fellow_co_redundancy::FellowCoRedundancy;
94use super::few_units_of_time_ago::FewUnitsOfTimeAgo;
95use super::filler_words::FillerWords;
96use super::find_fine::FindFine;
97use super::first_aid_kit::FirstAidKit;
98use super::fish_nor_fowl::FishNorFowl;
99use super::flesh_out_vs_full_fledged::FleshOutVsFullFledged;
100use super::foot_inch_minute_second_symbols::FootInchMinuteSecondSymbols;
101use super::for_free_of_charge::ForFreeOfCharge;
102use super::for_noun::ForNoun;
103use super::for_the_nth_time::ForTheNthTime;
104use super::free_predicate::FreePredicate;
105use super::friend_of_me::FriendOfMe;
106use super::full_to_the_brim::FullToTheBrim;
107use super::go_so_far_as_to::GoSoFarAsTo;
108use super::go_to_war::GoToWar;
109use super::good_at::GoodAt;
110use super::handful::Handful;
111use super::handful_of_more::HandfulOfMore;
112use super::have_a_hard_time::HaveAHardTime;
113use super::have_pronoun::HavePronoun;
114use super::have_take_a_look::HaveTakeALook;
115use super::hedging::Hedging;
116use super::hello_greeting::HelloGreeting;
117use super::helped_past::HelpedPast;
118use super::hereby::Hereby;
119use super::hop_hope::HopHope;
120use super::how_does_compared::HowDoesCompared;
121use super::how_to::HowTo;
122use super::hyphenate_number_day::HyphenateNumberDay;
123use super::i_am_agreement::IAmAgreement;
124use super::if_wouldve::IfWouldve;
125use super::implement_in::ImplementIn;
126use super::in_demand_in_depth::InDemandInDepth;
127use super::in_favour_of_doing::InFavourOfDoing;
128use super::in_on_the_cards::InOnTheCards;
129use super::in_stock::InStock;
130use super::in_time_from_now::InTimeFromNow;
131use super::inflected_verb_after_to::InflectedVerbAfterTo;
132use super::interested_in::InterestedIn;
133use super::it_looks_like_that::ItLooksLikeThat;
134use super::its_contraction::ItsContraction;
135use super::its_possessive::ItsPossessive;
136use super::jealous_of::JealousOf;
137use super::johns_hopkins::JohnsHopkins;
138use super::jump_the_gun::JumpTheGun;
139use super::lead_rise_to::LeadRiseTo;
140use super::leaving_in_droves::LeavingInDroves;
141use super::left_right_hand::LeftRightHand;
142use super::less_worse::LessWorse;
143use super::let_to_do::LetToDo;
144use super::lets_confusion::LetsConfusion;
145use super::likewise::Likewise;
146use super::little_known::LittleKnown;
147use super::long_sentences::LongSentences;
148use super::long_time_ago::LongTimeAgo;
149use super::look_down_ones_nose::LookDownOnesNose;
150use super::looking_forward_to::LookingForwardTo;
151use super::mass_nouns::MassNouns;
152use super::means_a_lot_to::MeansALotTo;
153use super::merge_words::MergeWords;
154use super::missing_preposition::MissingPreposition;
155use super::missing_to::MissingTo;
156use super::misspell::Misspell;
157use super::mixed_bag::MixedBag;
158use super::modal_be_adjective::ModalBeAdjective;
159use super::modal_of::ModalOf;
160use super::modal_seem::ModalSeem;
161use super::months::Months;
162use super::more_adjective::MoreAdjective;
163use super::more_better::MoreBetter;
164use super::most_number::MostNumber;
165use super::most_of_the_times::MostOfTheTimes;
166use super::multiple_frequency_adverbs::MultipleFrequencyAdverbs;
167use super::multiple_sequential_pronouns::MultipleSequentialPronouns;
168use super::nail_on_the_head::NailOnTheHead;
169use super::naked_eye::NakedEye;
170use super::need_to_noun::NeedToNoun;
171use super::no_french_spaces::NoFrenchSpaces;
172use super::no_harm_no_foul::NoHarmNoFoul;
173use super::no_longer::NoLonger;
174use super::no_longer_pronoun::NoLongerPronoun;
175use super::no_match_for::NoMatchFor;
176use super::no_oxford_comma::NoOxfordComma;
177use super::nobody::Nobody;
178use super::nominal_wants::NominalWants;
179use super::nor_modal_pronoun::NorModalPronoun;
180use super::not_only_inversion::NotOnlyInversion;
181use super::noun_verb_confusion::NounVerbConfusion;
182use super::number_suffix_capitalization::NumberSuffixCapitalization;
183use super::numeric_range_en_dash::NumericRangeEnDash;
184use super::obsess_preposition::ObsessPreposition;
185use super::of_course::OfCourse;
186use super::oldest_in_the_book::OldestInTheBook;
187use super::on_floor::OnFloor;
188use super::once_or_twice::OnceOrTwice;
189use super::one_and_the_same::OneAndTheSame;
190use super::one_of_the_singular::OneOfTheSingular;
191use super::ones_own_accord::OnesOwnAccord;
192use super::open_compounds::OpenCompounds;
193use super::open_the_light::OpenTheLight;
194use super::orthographic_consistency::OrthographicConsistency;
195use super::ought_to_be::OughtToBe;
196use super::out_of_date::OutOfDate;
197use super::out_of_the_window::OutOfTheWindow;
198use super::over_plus::OverPlus;
199use super::oxford_comma::OxfordComma;
200use super::oxymorons::Oxymorons;
201use super::pale_by_comparison::PaleByComparison;
202use super::passionate_about::PassionateAbout;
203use super::pay_for_price::PayForPrice;
204use super::phrasal_verb_as_compound_noun::PhrasalVerbAsCompoundNoun;
205use super::pique_interest::PiqueInterest;
206use super::plural_decades::PluralDecades;
207use super::plural_wrong_word_of_phrase::PluralWrongWordOfPhrase;
208use super::possessive_noun::PossessiveNoun;
209use super::possessive_your::PossessiveYour;
210use super::progressive_needs_be::ProgressiveNeedsBe;
211use super::pronoun_are::PronounAre;
212use super::pronoun_contraction::PronounContraction;
213use super::pronoun_inflection_be::PronounInflectionBe;
214use super::pronoun_knew::PronounKnew;
215use super::pronoun_verb_agreement::PronounVerbAgreement;
216use super::quantifier_needs_of::QuantifierNeedsOf;
217use super::quantifier_numeral_conflict::QuantifierNumeralConflict;
218use super::quite_quiet::QuiteQuiet;
219use super::quote_spacing::QuoteSpacing;
220use super::reason_for_doing::ReasonForDoing;
221use super::redundant_acronyms::RedundantAcronyms;
222use super::redundant_additive_adverbs::RedundantAdditiveAdverbs;
223use super::redundant_firsts::RedundantFirsts;
224use super::redundant_progressive_comparative::RedundantProgressiveComparative;
225use super::redundant_self::RedundantSelf;
226use super::regimen_regiment::RegimenRegiment;
227use super::regionalisms::Regionalisms;
228use super::regular_irregulars::RegularIrregulars;
229use super::repeated_words::RepeatedWords;
230use super::respond::Respond;
231use super::right_click::RightClick;
232use super::rise_the_ranks::RiseTheRanks;
233use super::roller_skated::RollerSkated;
234use super::run_into_problems_or_trouble::RunIntoProblemsOrTrouble;
235use super::safe_to_save::SafeToSave;
236use super::save_to_safe::SaveToSafe;
237use super::sentence_capitalization::SentenceCapitalization;
238use super::shoot_oneself_in_the_foot::ShootOneselfInTheFoot;
239use super::simple_past_to_past_participle::SimplePastToPastParticiple;
240use super::since_duration::SinceDuration;
241use super::single_be::SingleBe;
242use super::sneaked_snuck::SneakedSnuck;
243use super::some_without_article::SomeWithoutArticle;
244use super::something_is::SomethingIs;
245use super::somewhat_something::SomewhatSomething;
246use super::soon_to_be::SoonToBe;
247use super::sought_after::SoughtAfter;
248use super::spaces::Spaces;
249use super::spell_check::SpellCheck;
250use super::spelled_numbers::SpelledNumbers;
251use super::split_words::SplitWords;
252use super::subject_pronoun::SubjectPronoun;
253use super::take_a_look_to::TakeALookTo;
254use super::take_care_of::TakeCareOf;
255use super::take_medicine::TakeMedicine;
256use super::that_than::ThatThan;
257use super::that_which::ThatWhich;
258use super::the_how_why::TheHowWhy;
259use super::the_last_days::TheLastDays;
260use super::the_my::TheMy;
261use super::the_point_for::ThePointFor;
262use super::the_proper_noun_possessive::TheProperNounPossessive;
263use super::the_the_to_that_the::TheTheToThatThe;
264use super::then_than::ThenThan;
265use super::there_is_agreement::ThereIsAgreement;
266use super::there_own::ThereOwn;
267use super::theres::Theres;
268use super::theses_these::ThesesThese;
269use super::theyre_confusions::TheyreConfusions;
270use super::thing_think::ThingThink;
271use super::this_type_of_thing::ThisTypeOfThing;
272use super::though_thought::ThoughThought;
273use super::thrive_on::ThriveOn;
274use super::throw_away::ThrowAway;
275use super::throw_baby_with_bathwater::ThrowBabyWithBathwater;
276use super::throw_rubbish::ThrowRubbish;
277use super::till_date::TillDate;
278use super::to_adverb::ToAdverb;
279use super::to_two_too::ToTwoToo;
280use super::touristic::Touristic;
281use super::transposed_space::TransposedSpace;
282use super::try_ones_hand_at::TryOnesHandAt;
283use super::try_ones_luck::TryOnesLuck;
284use super::unclosed_quotes::UnclosedQuotes;
285use super::update_place_names::UpdatePlaceNames;
286use super::use_ellipsis_character::UseEllipsisCharacter;
287use super::use_title_case::UseTitleCase;
288use super::verb_to_adjective::VerbToAdjective;
289use super::very_unique::VeryUnique;
290use super::vice_versa::ViceVersa;
291use super::vicious_loop::ViciousCircle;
292use super::vicious_loop::ViciousCircleOrCycle;
293use super::vicious_loop::ViciousCycle;
294use super::was_aloud::WasAloud;
295use super::way_too_adjective::WayTooAdjective;
296use super::web_scraping::WebScraping;
297use super::well_educated::WellEducated;
298use super::were_where::WereWhere;
299use super::whereas::Whereas;
300use super::whom_subject_of_verb::WhomSubjectOfVerb;
301use super::widely_accepted::WidelyAccepted;
302use super::will_non_lemma::WillNonLemma;
303use super::win_prize::WinPrize;
304use super::wish_could::WishCould;
305use super::wordpress_dotcom::WordPressDotcom;
306use super::worth_to_do::WorthToDo;
307use super::would_never_have::WouldNeverHave;
308use super::wrong_apostrophe::WrongApostrophe;
309use super::wrong_negative::WrongNegative;
310
311use super::be_adjective_confusions;
313use super::closed_compounds;
314use super::initialisms;
315use super::phrase_set_corrections;
316use super::proper_noun_capitalization_linters;
317use super::weir_rules;
318
319use crate::{
320 linting::{
321 dashes::Dashes,
322 expr_linter::{Chunk, Sentence, run_on_chunk},
323 {ExprLinter, HtmlDescriptionLinter, Lint, Linter},
324 },
325 spell::Dictionary,
326 {Dialect, Document, Span, TokenStringExt},
327};
328
329pub use flat_config::FlatConfig;
330pub use structured_config::{
331 HumanReadableSetting, HumanReadableStructuredConfig, StructuredConfig,
332};
333
334pub struct LintGroup {
337 pub config: FlatConfig,
338 linters: BTreeMap<String, Box<dyn Linter>>,
340 chunk_expr_linters: BTreeMap<String, Box<dyn ExprLinter<Unit = Chunk>>>,
342 sentence_expr_linters: BTreeMap<String, Box<dyn ExprLinter<Unit = Sentence>>>,
344 chunk_expr_cache: LruCache<(u64, u64), BTreeMap<String, Vec<Lint>>>,
351 sentence_expr_cache: LruCache<(u64, u64), BTreeMap<String, Vec<Lint>>>,
352 hasher_builder: RandomState,
353 clashing_linter_names: Option<Vec<String>>,
354}
355
356impl LintGroup {
357 pub fn empty() -> Self {
360 Self {
361 config: FlatConfig::default(),
362 linters: BTreeMap::new(),
363 chunk_expr_linters: BTreeMap::new(),
364 sentence_expr_linters: BTreeMap::new(),
365 chunk_expr_cache: LruCache::new(NonZero::new(1000).unwrap()),
366 sentence_expr_cache: LruCache::new(NonZero::new(1000).unwrap()),
367 hasher_builder: RandomState::default(),
368 clashing_linter_names: None,
369 }
370 }
371
372 pub fn contains_key(&self, name: impl AsRef<str>) -> bool {
374 self.linters.contains_key(name.as_ref())
375 || self.chunk_expr_linters.contains_key(name.as_ref())
376 || self.sentence_expr_linters.contains_key(name.as_ref())
377 }
378
379 pub fn add(&mut self, name: impl AsRef<str>, linter: impl Linter + 'static) -> bool {
382 if self.contains_key(&name) {
383 if self.clashing_linter_names.is_none() {
384 self.clashing_linter_names = Some(vec![name.as_ref().to_string()]);
385 } else if let Some(clashing_names) = &mut self.clashing_linter_names {
386 clashing_names.push(name.as_ref().to_string());
387 }
388 false
389 } else {
390 self.linters
391 .insert(name.as_ref().to_string(), Box::new(linter));
392 true
393 }
394 }
395
396 pub fn add_chunk_expr_linter(
402 &mut self,
403 name: impl AsRef<str>,
404 linter: impl ExprLinter<Unit = Chunk> + 'static,
405 ) -> bool {
406 if self.contains_key(&name) {
407 if self.clashing_linter_names.is_none() {
408 self.clashing_linter_names = Some(vec![name.as_ref().to_string()]);
409 } else if let Some(clashing_names) = &mut self.clashing_linter_names {
410 clashing_names.push(name.as_ref().to_string());
411 }
412 false
413 } else {
414 self.chunk_expr_linters
415 .insert(name.as_ref().to_string(), Box::new(linter) as _);
416 true
417 }
418 }
419
420 pub fn add_sentence_expr_linter(
423 &mut self,
424 name: impl AsRef<str>,
425 linter: impl ExprLinter<Unit = Sentence> + 'static,
426 ) -> bool {
427 if self.contains_key(&name) {
428 if self.clashing_linter_names.is_none() {
429 self.clashing_linter_names = Some(vec![name.as_ref().to_string()]);
430 } else if let Some(clashing_names) = &mut self.clashing_linter_names {
431 clashing_names.push(name.as_ref().to_string());
432 }
433 false
434 } else {
435 self.sentence_expr_linters
436 .insert(name.as_ref().to_string(), Box::new(linter) as _);
437 true
438 }
439 }
440
441 pub fn merge_from(&mut self, other: LintGroup) {
443 self.config.merge_from(other.config);
444
445 if let Some((conflicting_key, _)) = other.linters.iter().find(|(k, _)| self.contains_key(k))
446 {
447 if self.clashing_linter_names.is_none() {
448 self.clashing_linter_names = Some(vec![conflicting_key.clone()]);
449 } else if let Some(clashing_names) = &mut self.clashing_linter_names {
450 clashing_names.push(conflicting_key.clone());
451 }
452 }
453 self.linters.extend(other.linters);
454
455 if let Some((conflicting_key, _)) = other
456 .chunk_expr_linters
457 .iter()
458 .find(|(k, _)| self.contains_key(k))
459 {
460 if self.clashing_linter_names.is_none() {
461 self.clashing_linter_names = Some(vec![conflicting_key.clone()]);
462 } else if let Some(clashing_names) = &mut self.clashing_linter_names {
463 clashing_names.push(conflicting_key.clone());
464 }
465 }
466 self.chunk_expr_linters.extend(other.chunk_expr_linters);
467
468 if let Some((conflicting_key, _)) = other
469 .sentence_expr_linters
470 .iter()
471 .find(|(k, _)| self.contains_key(k))
472 {
473 if self.clashing_linter_names.is_none() {
474 self.clashing_linter_names = Some(vec![conflicting_key.clone()]);
475 } else if let Some(clashing_names) = &mut self.clashing_linter_names {
476 clashing_names.push(conflicting_key.clone());
477 }
478 }
479 self.sentence_expr_linters
480 .extend(other.sentence_expr_linters);
481 }
482
483 pub fn iter_keys(&self) -> impl Iterator<Item = &str> {
484 self.linters
485 .keys()
486 .chain(self.chunk_expr_linters.keys())
487 .chain(self.sentence_expr_linters.keys())
488 .map(|v| v.as_str())
489 }
490
491 pub fn set_all_rules_to(&mut self, enabled: Option<bool>) {
494 let keys = self.iter_keys().map(|v| v.to_string()).collect::<Vec<_>>();
495
496 for key in keys {
497 match enabled {
498 Some(v) => self.config.set_rule_enabled(key, v),
499 None => self.config.unset_rule_enabled(key),
500 }
501 }
502 }
503
504 pub fn all_descriptions(&self) -> HashMap<&str, &str> {
506 self.linters
507 .iter()
508 .map(|(key, value)| (key.as_str(), value.description()))
509 .chain(
510 self.chunk_expr_linters
511 .iter()
512 .map(|(key, value)| (key.as_str(), ExprLinter::description(value))),
513 )
514 .chain(
515 self.sentence_expr_linters
516 .iter()
517 .map(|(key, value)| (key.as_str(), ExprLinter::description(value))),
518 )
519 .collect()
520 }
521
522 pub fn all_descriptions_html(&self) -> HashMap<&str, String> {
524 self.linters
525 .iter()
526 .map(|(key, value)| (key.as_str(), value.description_html()))
527 .chain(
528 self.chunk_expr_linters
529 .iter()
530 .map(|(key, value)| (key.as_str(), value.description_html())),
531 )
532 .chain(
533 self.sentence_expr_linters
534 .iter()
535 .map(|(key, value)| (key.as_str(), value.description_html())),
536 )
537 .collect()
538 }
539
540 pub fn with_lint_config(mut self, config: FlatConfig) -> Self {
542 self.config = config;
543 self
544 }
545
546 pub fn new_curated(dictionary: Arc<impl Dictionary + 'static>, dialect: Dialect) -> Self {
547 let mut out = Self::empty();
548
549 macro_rules! insert_struct_rule {
551 ($rule:ident) => {
552 out.add(stringify!($rule), $rule::default());
553 };
554 }
555
556 macro_rules! insert_struct_rule_with_dict {
558 ($rule:ident) => {
559 out.add(stringify!($rule), $rule::new(dictionary.clone()));
560 };
561 }
562
563 macro_rules! insert_struct_rule_with_dialect {
565 ($rule:ident) => {
566 out.add(stringify!($rule), $rule::new(dialect));
567 };
568 }
569
570 macro_rules! insert_expr_rule {
574 ($rule:ident) => {
575 out.add_chunk_expr_linter(stringify!($rule), $rule::default());
576 };
577 }
578
579 macro_rules! insert_expr_rule_with_dict {
581 ($rule:ident) => {
582 out.add_chunk_expr_linter(stringify!($rule), $rule::new(dictionary.clone()));
583 };
584 }
585
586 macro_rules! insert_expr_rule_with_dialect {
588 ($rule:ident) => {
589 out.add_chunk_expr_linter(stringify!($rule), $rule::new(dialect));
590 };
591 }
592
593 out.merge_from(weir_rules::lint_group());
594 out.merge_from(phrase_set_corrections::lint_group());
595 out.merge_from(proper_noun_capitalization_linters::lint_group());
596 out.merge_from(closed_compounds::lint_group());
597 out.merge_from(initialisms::lint_group());
598 out.merge_from(be_adjective_confusions::lint_group());
599
600 insert_expr_rule!(APart);
604 insert_expr_rule!(ASomeTime);
605 insert_expr_rule!(AWaysToGo);
606 insert_expr_rule!(AWhile);
607 insert_expr_rule!(AccuseOf);
608 insert_expr_rule!(Addicting);
609 insert_expr_rule!(AdjectiveDoubleDegree);
610 insert_struct_rule!(AdjectiveOfA);
611 insert_expr_rule!(AfterLater);
612 insert_expr_rule!(AllHellBreakLoose);
613 insert_expr_rule!(AllIntentsAndPurposes);
614 insert_expr_rule!(AllowTo);
615 insert_expr_rule!(AmInTheMorning);
616 insert_expr_rule!(AmountsFor);
617 insert_struct_rule_with_dialect!(AnA);
618 insert_expr_rule_with_dialect!(AnalogAcousticBike);
619 insert_expr_rule!(AndTheLike);
620 insert_expr_rule!(AnotherThingComing);
621 insert_expr_rule!(AnotherThinkComing);
622 insert_expr_rule!(ApartFrom);
623 insert_expr_rule!(ArriveTo);
624 insert_expr_rule!(AsHow);
625 insert_expr_rule!(AsToInterrogative);
626 insert_expr_rule!(AskNoPreposition);
627 insert_expr_rule!(AvoidContractions);
628 insert_expr_rule!(AvoidCurses);
629 insert_expr_rule!(BackInTheDay);
630 insert_expr_rule_with_dict!(BarelyUn);
631 insert_expr_rule!(BeAllowed);
632 insert_expr_rule!(BehindTheScenes);
633 insert_struct_rule!(BestOfAllTime);
634 insert_expr_rule!(BoringWords);
635 insert_expr_rule!(Bought);
636 insert_expr_rule!(BrandBrandish);
637 insert_expr_rule!(ByAccident);
638 insert_expr_rule!(ByOnesOwn);
639 insert_expr_rule!(ByTheBook);
640 insert_expr_rule!(CallItQuits);
641 insert_expr_rule!(CallThem);
642 insert_expr_rule!(Cant);
643 insert_struct_rule!(CapitalizePersonalPronouns);
644 insert_expr_rule!(Catch22);
645 insert_expr_rule!(CautionaryTale);
646 insert_expr_rule!(ChangeTack);
647 insert_expr_rule!(ChockFull);
648 insert_expr_rule!(CloseTightKnit);
649 insert_expr_rule!(CodeInWriteIn);
650 insert_struct_rule!(CommaFixes);
651 insert_expr_rule!(ComplainAsNoun);
652 insert_struct_rule!(CompoundNouns);
653 insert_expr_rule!(CompoundSubjectI);
654 insert_expr_rule!(Confident);
655 insert_struct_rule!(CorrectNumberSuffix);
656 insert_expr_rule!(CraveFor);
657 insert_struct_rule!(CriteriaPhenomena);
658 insert_expr_rule!(CureFor);
659 insert_struct_rule!(CurrencyPlacement);
660 insert_expr_rule!(Dashes);
661 insert_expr_rule!(DayAndAge);
662 insert_expr_rule!(DespiteItIs);
663 insert_expr_rule!(DespiteOf);
664 insert_expr_rule_with_dict!(DidPast);
665 insert_expr_rule!(Didnt);
666 insert_struct_rule!(DiscourseMarkers);
667 insert_expr_rule_with_dict!(DisjointPrefixes);
668 insert_expr_rule!(DoMistake);
669 insert_expr_rule!(DotInitialisms);
670 insert_expr_rule!(DoubleClick);
671 insert_expr_rule!(DoubleModal);
672 insert_struct_rule!(EllipsisLength);
673 insert_expr_rule!(ElsePossessive);
674 insert_expr_rule!(EverEvery);
675 insert_expr_rule!(Everyday);
676 insert_expr_rule!(ExceptOf);
677 insert_expr_rule!(ExpandMemoryShorthands);
678 insert_expr_rule!(ExpandPeople);
679 insert_expr_rule!(ExpandTimeShorthands);
680 insert_expr_rule!(FallBelow);
681 insert_expr_rule!(FarBeIt);
682 insert_expr_rule!(FascinatedBy);
683 insert_expr_rule_with_dialect!(FedUpWith);
684 insert_expr_rule!(FeelFell);
685 insert_expr_rule!(FellowCoRedundancy);
686 insert_expr_rule!(FewUnitsOfTimeAgo);
687 insert_expr_rule!(FillerWords);
688 insert_struct_rule!(FindFine);
689 insert_expr_rule!(FirstAidKit);
690 insert_expr_rule!(FishNorFowl);
691 insert_expr_rule!(FleshOutVsFullFledged);
692 insert_expr_rule!(FootInchMinuteSecondSymbols);
693 insert_expr_rule!(ForFreeOfCharge);
694 insert_expr_rule!(ForNoun);
695 insert_expr_rule!(ForTheNthTime);
696 insert_expr_rule!(FreePredicate);
697 insert_expr_rule!(FriendOfMe);
698 insert_expr_rule!(FullToTheBrim);
699 insert_expr_rule!(GoSoFarAsTo);
700 insert_expr_rule!(GoToWar);
701 insert_expr_rule!(GoodAt);
702 insert_expr_rule!(Handful);
703 insert_expr_rule!(HandfulOfMore);
704 insert_expr_rule_with_dialect!(HaveAHardTime);
705 insert_expr_rule!(HavePronoun);
706 insert_struct_rule_with_dialect!(HaveTakeALook);
707 insert_expr_rule!(Hedging);
708 insert_expr_rule!(HelloGreeting);
709 insert_expr_rule_with_dict!(HelpedPast);
710 insert_expr_rule!(Hereby);
711 insert_struct_rule!(HopHope);
712 insert_expr_rule!(HowDoesCompared);
713 insert_expr_rule!(HowTo);
714 insert_expr_rule!(HyphenateNumberDay);
715 insert_expr_rule!(IAmAgreement);
716 insert_expr_rule!(IfWouldve);
717 insert_expr_rule!(ImplementIn);
718 insert_expr_rule!(InDemandInDepth);
719 insert_expr_rule!(InFavourOfDoing);
720 insert_struct_rule_with_dialect!(InOnTheCards);
721 insert_expr_rule!(InStock);
722 insert_expr_rule!(InTimeFromNow);
723 insert_struct_rule_with_dict!(InflectedVerbAfterTo);
724 insert_expr_rule!(InterestedIn);
725 insert_expr_rule!(ItLooksLikeThat);
726 insert_struct_rule!(ItsContraction);
727 insert_expr_rule!(ItsPossessive);
728 insert_expr_rule!(JealousOf);
729 insert_expr_rule!(JohnsHopkins);
730 insert_expr_rule!(JumpTheGun);
731 insert_expr_rule!(LeadRiseTo);
732 insert_expr_rule!(LeavingInDroves);
733 insert_expr_rule!(LeftRightHand);
734 insert_expr_rule!(LessWorse);
735 insert_expr_rule!(LetToDo);
736 insert_struct_rule!(LetsConfusion);
737 insert_expr_rule!(Likewise);
738 insert_struct_rule!(LittleKnown);
739 insert_struct_rule!(LongSentences);
740 insert_expr_rule!(LongTimeAgo);
741 insert_expr_rule!(LookDownOnesNose);
742 insert_expr_rule!(LookingForwardTo);
743 insert_struct_rule_with_dict!(MassNouns);
744 insert_expr_rule!(MeansALotTo);
745 insert_struct_rule!(MergeWords);
746 insert_expr_rule!(MissingPreposition);
747 insert_expr_rule!(MissingTo);
748 insert_expr_rule!(Misspell);
749 insert_expr_rule!(MixedBag);
750 insert_expr_rule!(ModalBeAdjective);
751 insert_expr_rule!(ModalOf);
752 insert_expr_rule!(ModalSeem);
753 insert_expr_rule!(Months);
754 insert_expr_rule_with_dict!(MoreAdjective);
755 insert_expr_rule!(MoreBetter);
756 insert_expr_rule!(MostNumber);
757 insert_expr_rule!(MostOfTheTimes);
758 insert_expr_rule!(MultipleSequentialPronouns);
759 insert_expr_rule!(NailOnTheHead);
760 insert_expr_rule!(NakedEye);
761 insert_expr_rule!(NeedToNoun);
762 insert_struct_rule!(NoFrenchSpaces);
763 insert_expr_rule!(NoLonger);
764 insert_expr_rule!(NoLongerPronoun);
765 insert_expr_rule!(NoMatchFor);
766 insert_struct_rule!(NoOxfordComma);
767 insert_expr_rule!(Nobody);
768 insert_expr_rule!(NominalWants);
769 insert_expr_rule!(NorModalPronoun);
770 insert_expr_rule!(NotOnlyInversion);
771 insert_struct_rule!(NounVerbConfusion);
772 insert_struct_rule!(NumberSuffixCapitalization);
773 insert_expr_rule!(NumericRangeEnDash);
774 insert_expr_rule!(ObsessPreposition);
775 insert_expr_rule!(OfCourse);
776 insert_expr_rule!(OldestInTheBook);
777 insert_expr_rule!(OnFloor);
778 insert_expr_rule!(OnceOrTwice);
779 insert_expr_rule!(OneAndTheSame);
780 insert_expr_rule_with_dict!(OneOfTheSingular);
781 insert_expr_rule!(OnesOwnAccord);
782 insert_expr_rule!(OpenCompounds);
783 insert_expr_rule!(OpenTheLight);
784 insert_expr_rule!(OrthographicConsistency);
785 insert_expr_rule!(OughtToBe);
786 insert_expr_rule!(OutOfDate);
787 insert_expr_rule_with_dialect!(OutOfTheWindow);
788 insert_expr_rule!(OverPlus);
789 insert_struct_rule!(OxfordComma);
790 insert_expr_rule!(Oxymorons);
791 insert_expr_rule!(PaleByComparison);
792 insert_expr_rule!(PassionateAbout);
793 insert_expr_rule!(PayForPrice);
794 insert_struct_rule!(PhrasalVerbAsCompoundNoun);
795 insert_expr_rule!(PiqueInterest);
796 insert_expr_rule!(PluralWrongWordOfPhrase);
797 insert_struct_rule_with_dict!(PossessiveNoun);
798 insert_expr_rule!(PossessiveYour);
799 insert_expr_rule!(ProgressiveNeedsBe);
800 insert_expr_rule!(PronounAre);
801 insert_struct_rule!(PronounContraction);
802 insert_expr_rule!(PronounInflectionBe);
803 insert_expr_rule!(PronounKnew);
804 insert_expr_rule_with_dict!(PronounVerbAgreement);
805 insert_expr_rule!(QuantifierNeedsOf);
806 insert_expr_rule!(QuantifierNumeralConflict);
807 insert_expr_rule!(QuiteQuiet);
808 insert_struct_rule!(QuoteSpacing);
809 insert_expr_rule!(ReasonForDoing);
810 insert_expr_rule!(RedundantAcronyms);
811 insert_expr_rule!(RedundantAdditiveAdverbs);
812 insert_expr_rule!(RedundantFirsts);
813 insert_expr_rule!(RedundantProgressiveComparative);
814 insert_expr_rule!(RedundantSelf);
815 insert_expr_rule!(RegimenRegiment);
816 insert_struct_rule_with_dialect!(Regionalisms);
817 insert_expr_rule_with_dict!(RegularIrregulars);
818 insert_struct_rule!(RepeatedWords);
819 insert_expr_rule!(Respond);
820 insert_expr_rule!(RightClick);
821 insert_expr_rule!(RiseTheRanks);
822 insert_expr_rule!(RollerSkated);
823 insert_expr_rule!(RunIntoProblemsOrTrouble);
824 insert_expr_rule!(SafeToSave);
825 insert_expr_rule!(SaveToSafe);
826 insert_struct_rule_with_dict!(SentenceCapitalization);
827 insert_expr_rule!(ShootOneselfInTheFoot);
828 insert_expr_rule!(SimplePastToPastParticiple);
829 insert_expr_rule!(SinceDuration);
830 insert_expr_rule!(SingleBe);
831 insert_struct_rule!(SneakedSnuck);
832 insert_expr_rule!(SomeWithoutArticle);
833 insert_expr_rule!(SomethingIs);
834 insert_expr_rule!(SomewhatSomething);
835 insert_expr_rule!(SoonToBe);
836 insert_expr_rule!(SoughtAfter);
837 insert_struct_rule!(Spaces);
838 insert_struct_rule!(SpelledNumbers);
839 insert_expr_rule!(SplitWords);
840 insert_struct_rule!(SubjectPronoun);
841 insert_expr_rule!(TakeALookTo);
842 insert_expr_rule!(TakeCareOf);
843 insert_expr_rule!(TakeMedicine);
844 insert_expr_rule!(ThatThan);
845 insert_expr_rule!(ThatWhich);
846 insert_expr_rule!(TheHowWhy);
847 insert_expr_rule!(TheLastDays);
848 insert_expr_rule!(TheMy);
849 insert_expr_rule!(ThePointFor);
850 insert_expr_rule!(TheProperNounPossessive);
851 insert_expr_rule!(TheTheToThatThe);
852 insert_expr_rule!(ThenThan);
853 insert_expr_rule!(ThereOwn);
854 insert_expr_rule!(Theres);
855 insert_expr_rule!(ThesesThese);
856 insert_struct_rule!(TheyreConfusions);
857 insert_expr_rule!(ThingThink);
858 insert_expr_rule!(ThisTypeOfThing);
859 insert_expr_rule!(ThoughThought);
860 insert_expr_rule!(ThriveOn);
861 insert_expr_rule!(ThrowAway);
862 insert_expr_rule!(ThrowBabyWithBathwater);
863 insert_struct_rule!(ThrowRubbish);
864 insert_expr_rule_with_dialect!(TillDate);
865 insert_expr_rule!(ToAdverb);
866 insert_struct_rule!(ToTwoToo);
867 insert_expr_rule!(Touristic);
868 insert_expr_rule_with_dict!(TransposedSpace);
869 insert_expr_rule!(TryOnesHandAt);
870 insert_expr_rule!(TryOnesLuck);
871 insert_struct_rule!(UnclosedQuotes);
872 insert_expr_rule!(UpdatePlaceNames);
873 insert_struct_rule!(UseEllipsisCharacter);
874 insert_struct_rule_with_dict!(UseTitleCase);
875 insert_expr_rule!(VerbToAdjective);
876 insert_expr_rule!(VeryUnique);
877 insert_expr_rule!(ViceVersa);
878 insert_expr_rule!(ViciousCircle);
879 insert_expr_rule!(ViciousCircleOrCycle);
880 insert_expr_rule!(ViciousCycle);
881 insert_expr_rule!(WasAloud);
882 insert_expr_rule!(WayTooAdjective);
883 insert_expr_rule!(WellEducated);
884 insert_expr_rule!(Whereas);
885 insert_expr_rule!(WhomSubjectOfVerb);
886 insert_expr_rule!(WidelyAccepted);
887 insert_expr_rule_with_dict!(WillNonLemma);
888 insert_expr_rule!(WinPrize);
889 insert_expr_rule!(WishCould);
890 insert_struct_rule!(WordPressDotcom);
891 insert_expr_rule_with_dict!(WorthToDo);
892 insert_expr_rule!(WouldNeverHave);
893 insert_expr_rule_with_dict!(WrongNegative);
894
895 out.add("AspireTo", AspireTo::default());
897
898 out.add("ConvenientStore", ConvenientStore::default());
900
901 out.add("Damages", Damages::default());
903
904 out.add(
906 "MultipleFrequencyAdverbs",
907 MultipleFrequencyAdverbs::default(),
908 );
909
910 out.add("NoHarmNoFoul", NoHarmNoFoul::default());
912
913 out.add("PluralDecades", PluralDecades::default());
915
916 out.add("SpellCheck", SpellCheck::new(dictionary.clone(), dialect));
918
919 out.add(
921 "ThereIsAgreement",
922 ThereIsAgreement::new(dictionary.clone()),
923 );
924
925 out.add("WebScraping", WebScraping::default());
927
928 out.add("WereWhere", WereWhere::default());
930
931 out.add("WrongApostrophe", WrongApostrophe::default());
933
934 out.with_lint_config(StructuredConfig::curated().to_flat_config().unwrap())
935 }
936
937 pub fn new_curated_empty_config(
939 dictionary: Arc<impl Dictionary + 'static>,
940 dialect: Dialect,
941 ) -> Self {
942 let mut group = Self::new_curated(dictionary, dialect);
943 group.config.clear();
944 group
945 }
946
947 pub fn organized_lints(&mut self, document: &Document) -> BTreeMap<String, Vec<Lint>> {
948 let mut results = BTreeMap::new();
949
950 for (key, linter) in &mut self.linters {
952 if self.config.is_rule_enabled(key) {
953 results.insert(key.to_owned(), linter.lint(document));
954 }
955 }
956
957 let mut insert_items_to_results =
958 |items: &BTreeMap<String, Vec<Lint>>, chunk_span: Span<char>| {
959 for (key, vec) in items.iter() {
960 results
961 .entry(key.to_owned())
962 .or_default()
963 .extend(vec.iter().cloned().map(|mut lint| {
964 lint.span.push_by(chunk_span.start);
966 lint
967 }));
968 }
969 };
970
971 for chunk in document.iter_chunks() {
973 let Some(chunk_span) = chunk.span() else {
974 continue;
975 };
976
977 let chunk_chars = document.get_span_content(&chunk_span);
978 let config_hash = self.hasher_builder.hash_one(&self.config);
979 let char_hash = self.hasher_builder.hash_one(chunk_chars);
980 let cache_key = (char_hash, config_hash);
981
982 if let Some(hit) = self.chunk_expr_cache.get(&cache_key) {
983 insert_items_to_results(hit, chunk_span);
984 } else {
985 let mut pattern_lints = BTreeMap::new();
986
987 for (key, linter) in &mut self.chunk_expr_linters {
988 if self.config.is_rule_enabled(key) {
989 let lints =
990 run_on_chunk(linter, chunk, document.get_source()).map(|mut l| {
991 l.span.pull_by(chunk_span.start);
992 l
993 });
994
995 pattern_lints.insert(key.clone(), lints.collect());
996 }
997 }
998
999 self.chunk_expr_cache.put(cache_key, pattern_lints.clone());
1000 insert_items_to_results(&pattern_lints, chunk_span);
1001 }
1002 }
1003
1004 for sentence in document.iter_sentences() {
1006 let Some(sentence_span) = sentence.span() else {
1007 continue;
1008 };
1009
1010 let sentence_chars = document.get_span_content(&sentence_span);
1011 let config_hash = self.hasher_builder.hash_one(&self.config);
1012 let char_hash = self.hasher_builder.hash_one(sentence_chars);
1013 let cache_key = (char_hash, config_hash);
1014
1015 if let Some(hit) = self.sentence_expr_cache.get(&cache_key) {
1016 insert_items_to_results(hit, sentence_span);
1017 } else {
1018 let mut pattern_lints = BTreeMap::new();
1019
1020 for (key, linter) in &mut self.sentence_expr_linters {
1021 if self.config.is_rule_enabled(key) {
1022 let lints =
1023 run_on_chunk(linter, sentence, document.get_source()).map(|mut l| {
1024 l.span.pull_by(sentence_span.start);
1025 l
1026 });
1027
1028 pattern_lints.insert(key.clone(), lints.collect());
1029 }
1030 }
1031
1032 self.sentence_expr_cache
1033 .put(cache_key, pattern_lints.clone());
1034 insert_items_to_results(&pattern_lints, sentence_span);
1035 }
1036 }
1037
1038 results
1039 }
1040}
1041
1042impl Default for LintGroup {
1043 fn default() -> Self {
1044 Self::empty()
1045 }
1046}
1047
1048impl Linter for LintGroup {
1049 fn lint(&mut self, document: &Document) -> Vec<Lint> {
1050 self.organized_lints(document)
1051 .into_values()
1052 .flatten()
1053 .collect()
1054 }
1055
1056 fn description(&self) -> &str {
1057 "A collection of linters that can be run as one."
1058 }
1059}
1060
1061#[cfg(test)]
1062mod tests {
1063 use std::sync::Arc;
1064
1065 use super::LintGroup;
1066 use crate::linting::LintKind;
1067 use crate::linting::pooled_linter::for_tests::create_test_pool;
1068 use crate::linting::tests::{assert_no_lints, assert_suggestion_result};
1069 use crate::spell::{FstDictionary, MutableDictionary};
1070 use crate::weir::WeirLinter;
1071 use crate::{Dialect, Document, linting::Linter};
1072
1073 create_test_pool!(
1074 LintGroup,
1075 LintGroup,
1076 LintGroup::new_curated(FstDictionary::curated(), Dialect::American)
1077 );
1078
1079 #[test]
1080 fn clean_interjection() {
1081 assert_no_lints(
1082 "Although I only saw the need to interject once, I still saw it.",
1083 test_linter(),
1084 );
1085 }
1086
1087 #[test]
1088 fn clean_consensus() {
1089 assert_no_lints("But there is less consensus on this.", test_linter());
1090 }
1091
1092 #[test]
1093 fn ive_corrects_to_single_word() {
1094 assert_suggestion_result(
1095 "ive never seen that before",
1096 test_linter(),
1097 "I've never seen that before",
1098 );
1099 }
1100
1101 #[test]
1102 fn worthchecking_is_split() {
1103 assert_suggestion_result("It is worthchecking", test_linter(), "It is worth checking");
1104 }
1105
1106 #[test]
1107 fn its_not_perfect_keeps_apostrophe() {
1108 assert_no_lints("It's not perfect", test_linter());
1109 }
1110
1111 #[test]
1112 fn corrects_extention() {
1113 let group = test_linter();
1114 let document = Document::new_plain_english_curated("I love this extention!");
1115 let organized = group.run_with_inner(|l| l.organized_lints(&document));
1116
1117 let spellcheck_lints = organized
1118 .get("SpellCheck")
1119 .expect("SpellCheck should produce a lint for extention");
1120 assert_eq!(spellcheck_lints.len(), 1);
1121 assert!(
1122 spellcheck_lints[0]
1123 .suggestions
1124 .iter()
1125 .any(|suggestion| suggestion.to_string() == "Replace with: “extension”")
1126 );
1127
1128 assert!(
1129 organized.get("SplitWords").is_none_or(Vec::is_empty),
1130 "expected no lints from SplitWords, but found {:?}",
1131 organized.get("SplitWords")
1132 );
1133 }
1134
1135 #[test]
1136 fn ok_becomes_okay() {
1137 assert_suggestion_result("This is ok.", test_linter(), "This is okay.");
1138 }
1139
1140 #[test]
1141 fn weir_linter_uses_configured_sentence_scope() {
1142 let source = r#"
1143 expr main one**two
1144 let message "Use three."
1145 let description "Test sentence-scoped Weir."
1146 let kind "Miscellaneous"
1147 let becomes "three"
1148 let strategy "Exact"
1149 let scope "Sentence"
1150 "#;
1151
1152 let mut group = LintGroup::empty();
1153 group.add_sentence_expr_linter(
1154 "TestSentenceWeir",
1155 WeirLinter::new(source)
1156 .unwrap()
1157 .into_sentence_linter()
1158 .unwrap_or_else(|_| unreachable!()),
1159 );
1160 group.config.set_rule_enabled("TestSentenceWeir", true);
1161
1162 assert_suggestion_result("one, two.", group, "three.");
1163 }
1164
1165 #[test]
1166 fn can_get_all_descriptions() {
1167 let group =
1168 LintGroup::new_curated(Arc::new(MutableDictionary::default()), Dialect::American);
1169 group.all_descriptions();
1170 }
1171
1172 #[test]
1173 fn can_get_all_descriptions_as_html() {
1174 let group =
1175 LintGroup::new_curated(Arc::new(MutableDictionary::default()), Dialect::American);
1176 group.all_descriptions_html();
1177 }
1178
1179 #[test]
1180 fn dont_flag_low_hanging_fruit_msg() {
1181 assert_no_lints(
1182 "The standard form is low-hanging fruit with a hyphen and singular form.",
1183 test_linter(),
1184 );
1185 }
1186
1187 #[test]
1188 fn dont_flag_low_hanging_fruit_desc() {
1189 assert_no_lints(
1190 "Corrects nonstandard variants of low-hanging fruit.",
1191 test_linter(),
1192 );
1193 }
1194
1195 #[test]
1196 fn fix_stoped() {
1197 assert_suggestion_result("He stoped to think.", test_linter(), "He stopped to think.");
1198 }
1199
1200 #[test]
1201 fn allow_playthrough() {
1202 assert_no_lints("I recently did a playthrough.", test_linter());
1203 }
1204
1205 #[test]
1206 fn fixes_politicans() {
1207 assert_suggestion_result(
1208 "I guess politicans like to complain.",
1209 test_linter(),
1210 "I guess politicians like to complain.",
1211 );
1212 }
1213
1214 #[test]
1215 fn fixes_benefitting() {
1216 assert_suggestion_result(
1217 "Who is really benefitting from it?",
1218 test_linter(),
1219 "Who is really benefiting from it?",
1220 );
1221 }
1222
1223 #[test]
1224 fn fixes_extention_correctly() {
1225 assert_suggestion_result(
1226 "There is no extention quite like this one.",
1227 test_linter(),
1228 "There is no extention quite like this one.",
1229 );
1230 }
1231
1232 #[test]
1233 fn fixes_easir() {
1234 assert_suggestion_result(
1235 "It makes it easir to select it.",
1236 test_linter(),
1237 "It makes it easier to select it.",
1238 );
1239 }
1240
1241 #[test]
1242 fn fixes_breakfest() {
1243 assert_suggestion_result(
1244 "Ice cream for breakfest?",
1245 test_linter(),
1246 "Ice cream for breakfast?",
1247 );
1248 }
1249
1250 #[test]
1251 fn fixes_buget() {
1252 assert_suggestion_result(
1253 "It was in their buget range.",
1254 test_linter(),
1255 "It was in their budget range.",
1256 );
1257 }
1258
1259 #[test]
1260 fn allows_chest_compressions() {
1261 assert_no_lints("Please continue chest compressions.", test_linter());
1262 }
1263
1264 #[test]
1278 fn lint_descriptions_are_clean() {
1279 let lints_to_check = LintGroup::new_curated(FstDictionary::curated(), Dialect::American);
1280
1281 let name_description_pairs: Vec<_> = lints_to_check
1282 .all_descriptions()
1283 .into_iter()
1284 .map(|(n, d)| (n.to_string(), d.to_string()))
1285 .collect();
1286
1287 name_description_pairs
1288 .iter()
1289 .for_each(|(lint_name, description)| {
1290 let doc = Document::new_markdown_default_curated(&description);
1291 eprintln!("{lint_name}: {description}");
1292
1293 let mut lints = test_linter().lint(&doc);
1294
1295 lints.retain(|l| l.lint_kind != LintKind::Style);
1297
1298 if !lints.is_empty() {
1299 dbg!(lints);
1300 panic!();
1301 }
1302 });
1303 }
1304
1305 #[test]
1306 fn no_linter_names_clash() {
1307 let group =
1308 LintGroup::new_curated(Arc::new(MutableDictionary::default()), Dialect::American);
1309
1310 if let Some(names) = &group.clashing_linter_names {
1311 if !names.is_empty() {
1312 panic!(
1313 "⚠️ Found {} clashing linter names: {}",
1314 names.len(),
1315 names.join(", ")
1316 );
1317 }
1318 }
1319 }
1320}