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