Skip to main content

harper_core/linting/lint_group/
mod.rs

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