Skip to main content

harper_core/linting/lint_group/
mod.rs

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