harper_core/linting/
lint_group.rs

1use std::collections::BTreeMap;
2use std::hash::Hash;
3use std::hash::{BuildHasher, Hasher};
4use std::mem;
5use std::num::NonZero;
6use std::sync::Arc;
7
8use cached::proc_macro::cached;
9use foldhash::quality::RandomState;
10use hashbrown::HashMap;
11use lru::LruCache;
12use serde::{Deserialize, Deserializer, Serialize, Serializer};
13
14use super::a_part::APart;
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_intents_and_purposes::AllIntentsAndPurposes;
21use super::allow_to::AllowTo;
22use super::am_in_the_morning::AmInTheMorning;
23use super::amounts_for::AmountsFor;
24use super::an_a::AnA;
25use super::and_in::AndIn;
26use super::and_the_like::AndTheLike;
27use super::another_thing_coming::AnotherThingComing;
28use super::another_think_coming::AnotherThinkComing;
29use super::ask_no_preposition::AskNoPreposition;
30use super::avoid_curses::AvoidCurses;
31use super::back_in_the_day::BackInTheDay;
32use super::be_allowed::BeAllowed;
33use super::best_of_all_time::BestOfAllTime;
34use super::boring_words::BoringWords;
35use super::bought::Bought;
36use super::cant::Cant;
37use super::capitalize_personal_pronouns::CapitalizePersonalPronouns;
38use super::cautionary_tale::CautionaryTale;
39use super::change_tack::ChangeTack;
40use super::chock_full::ChockFull;
41use super::comma_fixes::CommaFixes;
42use super::compound_nouns::CompoundNouns;
43use super::compound_subject_i::CompoundSubjectI;
44use super::confident::Confident;
45use super::correct_number_suffix::CorrectNumberSuffix;
46use super::criteria_phenomena::CriteriaPhenomena;
47use super::currency_placement::CurrencyPlacement;
48use super::despite_of::DespiteOf;
49use super::didnt::Didnt;
50use super::discourse_markers::DiscourseMarkers;
51use super::dot_initialisms::DotInitialisms;
52use super::double_click::DoubleClick;
53use super::double_modal::DoubleModal;
54use super::ellipsis_length::EllipsisLength;
55use super::else_possessive::ElsePossessive;
56use super::everyday::Everyday;
57use super::expand_memory_shorthands::ExpandMemoryShorthands;
58use super::expand_time_shorthands::ExpandTimeShorthands;
59use super::expr_linter::run_on_chunk;
60use super::far_be_it::FarBeIt;
61use super::feel_fell::FeelFell;
62use super::few_units_of_time_ago::FewUnitsOfTimeAgo;
63use super::filler_words::FillerWords;
64use super::find_fine::FindFine;
65use super::first_aid_kit::FirstAidKit;
66use super::for_noun::ForNoun;
67use super::free_predicate::FreePredicate;
68use super::friend_of_me::FriendOfMe;
69use super::go_so_far_as_to::GoSoFarAsTo;
70use super::have_pronoun::HavePronoun;
71use super::have_take_a_look::HaveTakeALook;
72use super::hedging::Hedging;
73use super::hello_greeting::HelloGreeting;
74use super::hereby::Hereby;
75use super::hop_hope::HopHope;
76use super::how_to::HowTo;
77use super::hyphenate_number_day::HyphenateNumberDay;
78use super::i_am_agreement::IAmAgreement;
79use super::if_wouldve::IfWouldve;
80use super::in_on_the_cards::InOnTheCards;
81use super::inflected_verb_after_to::InflectedVerbAfterTo;
82use super::interested_in::InterestedIn;
83use super::it_looks_like_that::ItLooksLikeThat;
84use super::its_contraction::ItsContraction;
85use super::its_possessive::ItsPossessive;
86use super::left_right_hand::LeftRightHand;
87use super::less_worse::LessWorse;
88use super::let_to_do::LetToDo;
89use super::lets_confusion::LetsConfusion;
90use super::likewise::Likewise;
91use super::long_sentences::LongSentences;
92use super::looking_forward_to::LookingForwardTo;
93use super::mass_plurals::MassPlurals;
94use super::merge_words::MergeWords;
95use super::missing_preposition::MissingPreposition;
96use super::missing_to::MissingTo;
97use super::misspell::Misspell;
98use super::mixed_bag::MixedBag;
99use super::modal_of::ModalOf;
100use super::modal_seem::ModalSeem;
101use super::months::Months;
102use super::more_better::MoreBetter;
103use super::most_number::MostNumber;
104use super::most_of_the_times::MostOfTheTimes;
105use super::multiple_sequential_pronouns::MultipleSequentialPronouns;
106use super::nail_on_the_head::NailOnTheHead;
107use super::need_to_noun::NeedToNoun;
108use super::no_french_spaces::NoFrenchSpaces;
109use super::no_match_for::NoMatchFor;
110use super::no_oxford_comma::NoOxfordComma;
111use super::nobody::Nobody;
112use super::nominal_wants::NominalWants;
113use super::noun_countability::NounCountability;
114use super::noun_verb_confusion::NounVerbConfusion;
115use super::number_suffix_capitalization::NumberSuffixCapitalization;
116use super::of_course::OfCourse;
117use super::on_floor::OnFloor;
118use super::once_or_twice::OnceOrTwice;
119use super::one_and_the_same::OneAndTheSame;
120use super::open_the_light::OpenTheLight;
121use super::orthographic_consistency::OrthographicConsistency;
122use super::ought_to_be::OughtToBe;
123use super::out_of_date::OutOfDate;
124use super::oxford_comma::OxfordComma;
125use super::oxymorons::Oxymorons;
126use super::phrasal_verb_as_compound_noun::PhrasalVerbAsCompoundNoun;
127use super::pique_interest::PiqueInterest;
128use super::possessive_noun::PossessiveNoun;
129use super::possessive_your::PossessiveYour;
130use super::progressive_needs_be::ProgressiveNeedsBe;
131use super::pronoun_are::PronounAre;
132use super::pronoun_contraction::PronounContraction;
133use super::pronoun_inflection_be::PronounInflectionBe;
134use super::pronoun_knew::PronounKnew;
135use super::proper_noun_capitalization_linters;
136use super::quantifier_needs_of::QuantifierNeedsOf;
137use super::quantifier_numeral_conflict::QuantifierNumeralConflict;
138use super::quite_quiet::QuiteQuiet;
139use super::quote_spacing::QuoteSpacing;
140use super::redundant_additive_adverbs::RedundantAdditiveAdverbs;
141use super::regionalisms::Regionalisms;
142use super::repeated_words::RepeatedWords;
143use super::roller_skated::RollerSkated;
144use super::safe_to_save::SafeToSave;
145use super::save_to_safe::SaveToSafe;
146use super::semicolon_apostrophe::SemicolonApostrophe;
147use super::sentence_capitalization::SentenceCapitalization;
148use super::shoot_oneself_in_the_foot::ShootOneselfInTheFoot;
149use super::simple_past_to_past_participle::SimplePastToPastParticiple;
150use super::since_duration::SinceDuration;
151use super::single_be::SingleBe;
152use super::some_without_article::SomeWithoutArticle;
153use super::something_is::SomethingIs;
154use super::somewhat_something::SomewhatSomething;
155use super::sought_after::SoughtAfter;
156use super::spaces::Spaces;
157use super::spell_check::SpellCheck;
158use super::spelled_numbers::SpelledNumbers;
159use super::split_words::SplitWords;
160use super::subject_pronoun::SubjectPronoun;
161use super::that_than::ThatThan;
162use super::that_which::ThatWhich;
163use super::the_how_why::TheHowWhy;
164use super::the_my::TheMy;
165use super::then_than::ThenThan;
166use super::theres::Theres;
167use super::theses_these::ThesesThese;
168use super::thing_think::ThingThink;
169use super::though_thought::ThoughThought;
170use super::throw_away::ThrowAway;
171use super::throw_rubbish::ThrowRubbish;
172use super::to_adverb::ToAdverb;
173use super::to_two_too::ToTwoToo;
174use super::touristic::Touristic;
175use super::unclosed_quotes::UnclosedQuotes;
176use super::update_place_names::UpdatePlaceNames;
177use super::use_genitive::UseGenitive;
178use super::verb_to_adjective::VerbToAdjective;
179use super::very_unique::VeryUnique;
180use super::vice_versa::ViceVersa;
181use super::was_aloud::WasAloud;
182use super::way_too_adjective::WayTooAdjective;
183use super::well_educated::WellEducated;
184use super::whereas::Whereas;
185use super::widely_accepted::WidelyAccepted;
186use super::win_prize::WinPrize;
187use super::wish_could::WishCould;
188use super::wordpress_dotcom::WordPressDotcom;
189use super::would_never_have::WouldNeverHave;
190use super::{ExprLinter, Lint};
191use super::{HtmlDescriptionLinter, Linter};
192use crate::linting::dashes::Dashes;
193use crate::linting::expr_linter::Chunk;
194use crate::linting::open_compounds::OpenCompounds;
195use crate::linting::{closed_compounds, initialisms, phrase_corrections, phrase_set_corrections};
196use crate::spell::{Dictionary, MutableDictionary};
197use crate::{CharString, Dialect, Document, TokenStringExt};
198
199fn ser_ordered<S>(map: &HashMap<String, Option<bool>>, ser: S) -> Result<S::Ok, S::Error>
200where
201    S: Serializer,
202{
203    let ordered: BTreeMap<_, _> = map.iter().map(|(k, v)| (k.clone(), *v)).collect();
204    ordered.serialize(ser)
205}
206
207fn de_hashbrown<'de, D>(de: D) -> Result<HashMap<String, Option<bool>>, D::Error>
208where
209    D: Deserializer<'de>,
210{
211    let ordered: BTreeMap<String, Option<bool>> = BTreeMap::deserialize(de)?;
212    Ok(ordered.into_iter().collect())
213}
214
215/// The configuration for a [`LintGroup`].
216/// Each child linter can be enabled, disabled, or set to a curated value.
217#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq)]
218#[serde(transparent)]
219pub struct LintGroupConfig {
220    /// We do this shenanigans with the [`BTreeMap`] to keep the serialized format consistent.
221    #[serde(serialize_with = "ser_ordered", deserialize_with = "de_hashbrown")]
222    inner: HashMap<String, Option<bool>>,
223}
224
225#[cached]
226fn curated_config() -> LintGroupConfig {
227    // The Dictionary and Dialect do not matter, we're just after the config.
228    let group = LintGroup::new_curated(MutableDictionary::new().into(), Dialect::American);
229    group.config
230}
231
232impl LintGroupConfig {
233    /// Check if a rule exists in the configuration.
234    pub fn has_rule(&self, key: impl AsRef<str>) -> bool {
235        self.inner.contains_key(key.as_ref())
236    }
237
238    pub fn set_rule_enabled(&mut self, key: impl ToString, val: bool) {
239        self.inner.insert(key.to_string(), Some(val));
240    }
241
242    /// Remove any configuration attached to a rule.
243    /// This allows it to assume its default (curated) state.
244    pub fn unset_rule_enabled(&mut self, key: impl AsRef<str>) {
245        self.inner.remove(key.as_ref());
246    }
247
248    pub fn set_rule_enabled_if_unset(&mut self, key: impl AsRef<str>, val: bool) {
249        if !self.inner.contains_key(key.as_ref()) {
250            self.set_rule_enabled(key.as_ref().to_string(), val);
251        }
252    }
253
254    pub fn is_rule_enabled(&self, key: &str) -> bool {
255        self.inner.get(key).cloned().flatten().unwrap_or(false)
256    }
257
258    /// Clear all config options.
259    /// This will reset them all to disable them.
260    pub fn clear(&mut self) {
261        for val in self.inner.values_mut() {
262            *val = None
263        }
264    }
265
266    /// Merge the contents of another [`LintGroupConfig`] into this one.
267    /// The other config will be left empty after this operation.
268    ///
269    /// Conflicting keys will be overridden by the value in the other group.
270    pub fn merge_from(&mut self, other: &mut LintGroupConfig) {
271        for (key, val) in other.inner.iter() {
272            if val.is_none() {
273                continue;
274            }
275
276            self.inner.insert(key.to_string(), *val);
277        }
278
279        other.clear();
280    }
281
282    /// Fill the group with the values for the curated lint group.
283    pub fn fill_with_curated(&mut self) {
284        let mut temp = Self::new_curated();
285        mem::swap(self, &mut temp);
286        self.merge_from(&mut temp);
287    }
288
289    pub fn new_curated() -> Self {
290        curated_config()
291    }
292}
293
294impl Hash for LintGroupConfig {
295    fn hash<H: Hasher>(&self, hasher: &mut H) {
296        for (key, value) in &self.inner {
297            hasher.write(key.as_bytes());
298            if let Some(value) = value {
299                hasher.write_u8(1);
300                hasher.write_u8(*value as u8);
301            } else {
302                // Do it twice so we fill the same number of bytes as the other branch.
303                hasher.write_u8(0);
304                hasher.write_u8(0);
305            }
306        }
307    }
308}
309
310/// A struct for collecting the output of a number of individual [Linter]s.
311/// Each child can be toggled via the public, mutable `Self::config` object.
312pub struct LintGroup {
313    pub config: LintGroupConfig,
314    /// We use a binary map here so the ordering is stable.
315    linters: BTreeMap<String, Box<dyn Linter>>,
316    /// We use a binary map here so the ordering is stable.
317    chunk_expr_linters: BTreeMap<String, Box<dyn ExprLinter<Unit = Chunk>>>,
318    /// Since [`ExprLinter`]s operate on a chunk-basis, we can store a
319    /// mapping of `Chunk -> Lint` and only re-run the expr linters
320    /// when a chunk changes.
321    ///
322    /// Since the expr linter results also depend on the config, we hash it and pass it as part
323    /// of the key.
324    chunk_expr_cache: LruCache<(CharString, u64), BTreeMap<String, Vec<Lint>>>,
325    hasher_builder: RandomState,
326}
327
328impl LintGroup {
329    pub fn empty() -> Self {
330        Self {
331            config: LintGroupConfig::default(),
332            linters: BTreeMap::new(),
333            chunk_expr_linters: BTreeMap::new(),
334            chunk_expr_cache: LruCache::new(NonZero::new(1000).unwrap()),
335            hasher_builder: RandomState::default(),
336        }
337    }
338
339    /// Check if the group already contains a linter with a given name.
340    pub fn contains_key(&self, name: impl AsRef<str>) -> bool {
341        self.linters.contains_key(name.as_ref())
342            || self.chunk_expr_linters.contains_key(name.as_ref())
343    }
344
345    /// Add a [`Linter`] to the group, returning whether the operation was successful.
346    /// If it returns `false`, it is because a linter with that key already existed in the group.
347    pub fn add(&mut self, name: impl AsRef<str>, linter: impl Linter + 'static) -> bool {
348        if self.contains_key(&name) {
349            false
350        } else {
351            self.linters
352                .insert(name.as_ref().to_string(), Box::new(linter));
353            true
354        }
355    }
356
357    /// Add a chunk-based [`ExprLinter`] to the group, returning whether the operation was successful.
358    /// If it returns `false`, it is because a linter with that key already existed in the group.
359    ///
360    /// This function is not significantly different from [`Self::add`], but allows us to take
361    /// advantage of some properties of chunk-based [`ExprLinter`]s for cache optimization.
362    pub fn add_chunk_expr_linter(
363        &mut self,
364        name: impl AsRef<str>,
365        // linter: impl ExprLinter + 'static,
366        linter: impl ExprLinter<Unit = Chunk> + 'static,
367    ) -> bool {
368        if self.contains_key(&name) {
369            false
370        } else {
371            self.chunk_expr_linters
372                .insert(name.as_ref().to_string(), Box::new(linter) as _);
373            true
374        }
375    }
376
377    /// Merge the contents of another [`LintGroup`] into this one.
378    /// The other lint group will be left empty after this operation.
379    pub fn merge_from(&mut self, other: &mut LintGroup) {
380        self.config.merge_from(&mut other.config);
381
382        let other_linters = std::mem::take(&mut other.linters);
383        self.linters.extend(other_linters);
384
385        let other_expr_linters = std::mem::take(&mut other.chunk_expr_linters);
386        self.chunk_expr_linters.extend(other_expr_linters);
387    }
388
389    pub fn iter_keys(&self) -> impl Iterator<Item = &str> {
390        self.linters
391            .keys()
392            .chain(self.chunk_expr_linters.keys())
393            .map(|v| v.as_str())
394    }
395
396    /// Set all contained rules to a specific value.
397    /// Passing `None` will unset that rule, allowing it to assume its default state.
398    pub fn set_all_rules_to(&mut self, enabled: Option<bool>) {
399        let keys = self.iter_keys().map(|v| v.to_string()).collect::<Vec<_>>();
400
401        for key in keys {
402            match enabled {
403                Some(v) => self.config.set_rule_enabled(key, v),
404                None => self.config.unset_rule_enabled(key),
405            }
406        }
407    }
408
409    /// Get map from each contained linter's name to its associated description.
410    pub fn all_descriptions(&self) -> HashMap<&str, &str> {
411        self.linters
412            .iter()
413            .map(|(key, value)| (key.as_str(), value.description()))
414            .chain(
415                self.chunk_expr_linters
416                    .iter()
417                    .map(|(key, value)| (key.as_str(), ExprLinter::description(value))),
418            )
419            .collect()
420    }
421
422    /// Get map from each contained linter's name to its associated description, rendered to HTML.
423    pub fn all_descriptions_html(&self) -> HashMap<&str, String> {
424        self.linters
425            .iter()
426            .map(|(key, value)| (key.as_str(), value.description_html()))
427            .chain(
428                self.chunk_expr_linters
429                    .iter()
430                    .map(|(key, value)| (key.as_str(), value.description_html())),
431            )
432            .collect()
433    }
434
435    /// Swap out [`Self::config`] with another [`LintGroupConfig`].
436    pub fn with_lint_config(mut self, config: LintGroupConfig) -> Self {
437        self.config = config;
438        self
439    }
440
441    pub fn new_curated(dictionary: Arc<impl Dictionary + 'static>, dialect: Dialect) -> Self {
442        let mut out = Self::empty();
443
444        /// Add a `Linter` to the group, setting it to be enabled by default.
445        macro_rules! insert_struct_rule {
446            ($rule:ident, $default_config:expr) => {
447                out.add(stringify!($rule), $rule::default());
448                out.config
449                    .set_rule_enabled(stringify!($rule), $default_config);
450            };
451        }
452
453        /// Add a chunk-based `ExprLinter` to the group, setting it to be enabled by default.
454        /// While you _can_ pass an `ExprLinter` to `insert_struct_rule`, using this macro instead
455        /// will allow it to use more aggressive caching strategies.
456        macro_rules! insert_expr_rule {
457            ($rule:ident, $default_config:expr) => {
458                out.add_chunk_expr_linter(stringify!($rule), $rule::default());
459                out.config
460                    .set_rule_enabled(stringify!($rule), $default_config);
461            };
462        }
463
464        out.merge_from(&mut phrase_corrections::lint_group());
465        out.merge_from(&mut phrase_set_corrections::lint_group());
466        out.merge_from(&mut proper_noun_capitalization_linters::lint_group(
467            dictionary.clone(),
468        ));
469        out.merge_from(&mut closed_compounds::lint_group());
470        out.merge_from(&mut initialisms::lint_group());
471        // out.merge_from(&mut update_place_names::lint_group());
472
473        // Add all the more complex rules to the group.
474        // Please maintain alphabetical order.
475        // On *nix you can maintain sort order with `sort -t'(' -k2`
476        insert_expr_rule!(APart, true);
477        insert_expr_rule!(AWhile, true);
478        insert_expr_rule!(Addicting, true);
479        insert_expr_rule!(AdjectiveDoubleDegree, true);
480        insert_struct_rule!(AdjectiveOfA, true);
481        insert_expr_rule!(AfterLater, true);
482        insert_expr_rule!(AllIntentsAndPurposes, true);
483        insert_expr_rule!(AllowTo, true);
484        insert_expr_rule!(AmInTheMorning, true);
485        insert_expr_rule!(AmountsFor, true);
486        insert_struct_rule!(AnA, true);
487        insert_expr_rule!(AndIn, true);
488        insert_expr_rule!(AndTheLike, true);
489        insert_expr_rule!(AnotherThingComing, true);
490        insert_expr_rule!(AnotherThinkComing, false);
491        insert_expr_rule!(AskNoPreposition, true);
492        insert_expr_rule!(AvoidCurses, true);
493        insert_expr_rule!(BackInTheDay, true);
494        insert_expr_rule!(BeAllowed, true);
495        insert_expr_rule!(BestOfAllTime, true);
496        insert_expr_rule!(BoringWords, false);
497        insert_expr_rule!(Bought, true);
498        insert_expr_rule!(Cant, true);
499        insert_struct_rule!(CapitalizePersonalPronouns, true);
500        insert_expr_rule!(CautionaryTale, true);
501        insert_expr_rule!(ChangeTack, true);
502        insert_expr_rule!(ChockFull, true);
503        insert_struct_rule!(CommaFixes, true);
504        insert_struct_rule!(CompoundNouns, true);
505        insert_expr_rule!(CompoundSubjectI, true);
506        insert_expr_rule!(Confident, true);
507        insert_struct_rule!(CorrectNumberSuffix, true);
508        insert_expr_rule!(CriteriaPhenomena, true);
509        insert_struct_rule!(CurrencyPlacement, true);
510        insert_expr_rule!(Dashes, true);
511        insert_expr_rule!(DespiteOf, true);
512        insert_expr_rule!(Didnt, true);
513        insert_struct_rule!(DiscourseMarkers, true);
514        insert_expr_rule!(DotInitialisms, true);
515        insert_expr_rule!(DoubleClick, true);
516        insert_expr_rule!(DoubleModal, true);
517        insert_struct_rule!(EllipsisLength, true);
518        insert_expr_rule!(ElsePossessive, true);
519        insert_expr_rule!(Everyday, true);
520        insert_expr_rule!(ExpandMemoryShorthands, true);
521        insert_expr_rule!(ExpandTimeShorthands, true);
522        insert_expr_rule!(FarBeIt, true);
523        insert_expr_rule!(FeelFell, true);
524        insert_expr_rule!(FewUnitsOfTimeAgo, true);
525        insert_expr_rule!(FillerWords, true);
526        insert_struct_rule!(FindFine, true);
527        insert_expr_rule!(FirstAidKit, true);
528        insert_expr_rule!(ForNoun, true);
529        insert_expr_rule!(FreePredicate, true);
530        insert_expr_rule!(FriendOfMe, true);
531        insert_expr_rule!(GoSoFarAsTo, true);
532        insert_expr_rule!(HavePronoun, true);
533        insert_expr_rule!(Hedging, true);
534        insert_expr_rule!(HelloGreeting, true);
535        insert_expr_rule!(Hereby, true);
536        insert_struct_rule!(HopHope, true);
537        insert_expr_rule!(HowTo, true);
538        insert_expr_rule!(HyphenateNumberDay, true);
539        insert_expr_rule!(IAmAgreement, true);
540        insert_expr_rule!(IfWouldve, true);
541        insert_expr_rule!(InterestedIn, true);
542        insert_expr_rule!(ItLooksLikeThat, true);
543        insert_struct_rule!(ItsContraction, true);
544        insert_expr_rule!(ItsPossessive, true);
545        insert_expr_rule!(LeftRightHand, true);
546        insert_expr_rule!(LessWorse, true);
547        insert_expr_rule!(LetToDo, true);
548        insert_struct_rule!(LetsConfusion, true);
549        insert_expr_rule!(Likewise, true);
550        insert_struct_rule!(LongSentences, true);
551        insert_expr_rule!(LookingForwardTo, true);
552        insert_struct_rule!(MergeWords, true);
553        insert_expr_rule!(MissingPreposition, true);
554        insert_expr_rule!(MissingTo, true);
555        insert_expr_rule!(Misspell, true);
556        insert_expr_rule!(MixedBag, true);
557        insert_expr_rule!(ModalOf, true);
558        insert_expr_rule!(ModalSeem, true);
559        insert_expr_rule!(Months, true);
560        insert_expr_rule!(MoreBetter, true);
561        insert_expr_rule!(MostNumber, true);
562        insert_expr_rule!(MostOfTheTimes, true);
563        insert_expr_rule!(MultipleSequentialPronouns, true);
564        insert_expr_rule!(NailOnTheHead, true);
565        insert_expr_rule!(NeedToNoun, true);
566        insert_struct_rule!(NoFrenchSpaces, true);
567        insert_expr_rule!(NoMatchFor, true);
568        insert_struct_rule!(NoOxfordComma, false);
569        insert_expr_rule!(Nobody, true);
570        insert_expr_rule!(NominalWants, true);
571        insert_expr_rule!(NounCountability, true);
572        insert_struct_rule!(NounVerbConfusion, true);
573        insert_struct_rule!(NumberSuffixCapitalization, true);
574        insert_expr_rule!(OfCourse, true);
575        insert_expr_rule!(OnFloor, true);
576        insert_expr_rule!(OnceOrTwice, true);
577        insert_expr_rule!(OneAndTheSame, true);
578        insert_expr_rule!(OpenCompounds, true);
579        insert_expr_rule!(OpenTheLight, true);
580        insert_expr_rule!(OrthographicConsistency, true);
581        insert_expr_rule!(OughtToBe, true);
582        insert_expr_rule!(OutOfDate, true);
583        insert_struct_rule!(OxfordComma, true);
584        insert_expr_rule!(Oxymorons, true);
585        insert_struct_rule!(PhrasalVerbAsCompoundNoun, true);
586        insert_expr_rule!(PiqueInterest, true);
587        insert_expr_rule!(PossessiveYour, true);
588        insert_expr_rule!(ProgressiveNeedsBe, true);
589        insert_expr_rule!(PronounAre, true);
590        insert_struct_rule!(PronounContraction, true);
591        insert_expr_rule!(PronounInflectionBe, true);
592        insert_expr_rule!(PronounKnew, true);
593        insert_expr_rule!(QuantifierNeedsOf, true);
594        insert_expr_rule!(QuantifierNumeralConflict, true);
595        insert_expr_rule!(QuiteQuiet, true);
596        insert_struct_rule!(QuoteSpacing, true);
597        insert_expr_rule!(RedundantAdditiveAdverbs, true);
598        insert_struct_rule!(RepeatedWords, true);
599        insert_expr_rule!(RollerSkated, true);
600        insert_expr_rule!(SafeToSave, true);
601        insert_expr_rule!(SaveToSafe, true);
602        insert_expr_rule!(SemicolonApostrophe, true);
603        insert_expr_rule!(ShootOneselfInTheFoot, true);
604        insert_expr_rule!(SimplePastToPastParticiple, true);
605        insert_expr_rule!(SinceDuration, true);
606        insert_expr_rule!(SingleBe, true);
607        insert_expr_rule!(SomeWithoutArticle, true);
608        insert_expr_rule!(SomethingIs, true);
609        insert_expr_rule!(SomewhatSomething, true);
610        insert_expr_rule!(SoughtAfter, true);
611        insert_struct_rule!(Spaces, true);
612        insert_struct_rule!(SpelledNumbers, false);
613        insert_expr_rule!(SplitWords, true);
614        insert_struct_rule!(SubjectPronoun, true);
615        insert_expr_rule!(ThatThan, true);
616        insert_expr_rule!(ThatWhich, true);
617        insert_expr_rule!(TheHowWhy, true);
618        insert_expr_rule!(TheMy, true);
619        insert_expr_rule!(ThenThan, true);
620        insert_expr_rule!(Theres, true);
621        insert_expr_rule!(ThesesThese, true);
622        insert_expr_rule!(ThingThink, true);
623        insert_expr_rule!(ThoughThought, true);
624        insert_expr_rule!(ThrowAway, true);
625        insert_struct_rule!(ThrowRubbish, true);
626        insert_expr_rule!(ToAdverb, true);
627        insert_struct_rule!(ToTwoToo, true);
628        insert_expr_rule!(Touristic, true);
629        insert_struct_rule!(UnclosedQuotes, true);
630        insert_expr_rule!(UpdatePlaceNames, true);
631        insert_expr_rule!(UseGenitive, false);
632        insert_expr_rule!(VerbToAdjective, true);
633        insert_expr_rule!(VeryUnique, true);
634        insert_expr_rule!(ViceVersa, true);
635        insert_expr_rule!(WasAloud, true);
636        insert_expr_rule!(WayTooAdjective, true);
637        insert_expr_rule!(WellEducated, true);
638        insert_expr_rule!(Whereas, true);
639        insert_expr_rule!(WidelyAccepted, true);
640        insert_expr_rule!(WinPrize, true);
641        insert_expr_rule!(WishCould, true);
642        insert_struct_rule!(WordPressDotcom, true);
643        insert_expr_rule!(WouldNeverHave, true);
644
645        out.add("SpellCheck", SpellCheck::new(dictionary.clone(), dialect));
646        out.config.set_rule_enabled("SpellCheck", true);
647
648        out.add(
649            "InflectedVerbAfterTo",
650            InflectedVerbAfterTo::new(dictionary.clone()),
651        );
652        out.config.set_rule_enabled("InflectedVerbAfterTo", true);
653
654        out.add("InOnTheCards", InOnTheCards::new(dialect));
655        out.config.set_rule_enabled("InOnTheCards", true);
656
657        out.add(
658            "SentenceCapitalization",
659            SentenceCapitalization::new(dictionary.clone()),
660        );
661        out.config.set_rule_enabled("SentenceCapitalization", true);
662
663        out.add("PossessiveNoun", PossessiveNoun::new(dictionary.clone()));
664        out.config.set_rule_enabled("PossessiveNoun", false);
665
666        out.add("Regionalisms", Regionalisms::new(dialect));
667        out.config.set_rule_enabled("Regionalisms", true);
668
669        out.add("HaveTakeALook", HaveTakeALook::new(dialect));
670        out.config.set_rule_enabled("HaveTakeALook", true);
671
672        out.add("MassPlurals", MassPlurals::new(dictionary.clone()));
673        out.config.set_rule_enabled("MassPlurals", true);
674
675        out
676    }
677
678    /// Create a new curated group with all config values cleared out.
679    pub fn new_curated_empty_config(
680        dictionary: Arc<impl Dictionary + 'static>,
681        dialect: Dialect,
682    ) -> Self {
683        let mut group = Self::new_curated(dictionary, dialect);
684        group.config.clear();
685        group
686    }
687
688    pub fn organized_lints(&mut self, document: &Document) -> BTreeMap<String, Vec<Lint>> {
689        let mut results = BTreeMap::new();
690
691        // Normal linters
692        for (key, linter) in &mut self.linters {
693            if self.config.is_rule_enabled(key) {
694                results.insert(key.clone(), linter.lint(document));
695            }
696        }
697
698        // Expr linters
699        for chunk in document.iter_chunks() {
700            let Some(chunk_span) = chunk.span() else {
701                continue;
702            };
703
704            let chunk_chars = document.get_span_content(&chunk_span);
705            let config_hash = self.hasher_builder.hash_one(&self.config);
706            let cache_key = (chunk_chars.into(), config_hash);
707
708            let mut chunk_results = if let Some(hit) = self.chunk_expr_cache.get(&cache_key) {
709                hit.clone()
710            } else {
711                let mut pattern_lints = BTreeMap::new();
712
713                for (key, linter) in &mut self.chunk_expr_linters {
714                    if self.config.is_rule_enabled(key) {
715                        let lints =
716                            run_on_chunk(linter, chunk, document.get_source()).map(|mut l| {
717                                l.span.pull_by(chunk_span.start);
718                                l
719                            });
720
721                        pattern_lints.insert(key.clone(), lints.collect());
722                    }
723                }
724
725                self.chunk_expr_cache.put(cache_key, pattern_lints.clone());
726                pattern_lints
727            };
728
729            // Bring the spans back into document-space
730            for value in chunk_results.values_mut() {
731                for lint in value {
732                    lint.span.push_by(chunk_span.start);
733                }
734            }
735
736            for (key, mut vec) in chunk_results {
737                results.entry(key).or_default().append(&mut vec);
738            }
739        }
740
741        results
742    }
743}
744
745impl Default for LintGroup {
746    fn default() -> Self {
747        Self::empty()
748    }
749}
750
751impl Linter for LintGroup {
752    fn lint(&mut self, document: &Document) -> Vec<Lint> {
753        self.organized_lints(document)
754            .into_values()
755            .flatten()
756            .collect()
757    }
758
759    fn description(&self) -> &str {
760        "A collection of linters that can be run as one."
761    }
762}
763
764#[cfg(test)]
765mod tests {
766    use std::sync::Arc;
767
768    use super::LintGroup;
769    use crate::linting::tests::assert_no_lints;
770    use crate::spell::{FstDictionary, MutableDictionary};
771    use crate::{Dialect, Document, linting::Linter};
772
773    fn test_group() -> LintGroup {
774        LintGroup::new_curated(Arc::new(MutableDictionary::curated()), Dialect::American)
775    }
776
777    #[test]
778    fn clean_interjection() {
779        assert_no_lints(
780            "Although I only saw the need to interject once, I still saw it.",
781            test_group(),
782        );
783    }
784
785    #[test]
786    fn clean_consensus() {
787        assert_no_lints("But there is less consensus on this.", test_group());
788    }
789
790    #[test]
791    fn can_get_all_descriptions() {
792        let group =
793            LintGroup::new_curated(Arc::new(MutableDictionary::default()), Dialect::American);
794        group.all_descriptions();
795    }
796
797    #[test]
798    fn can_get_all_descriptions_as_html() {
799        let group =
800            LintGroup::new_curated(Arc::new(MutableDictionary::default()), Dialect::American);
801        group.all_descriptions_html();
802    }
803
804    #[test]
805    fn dont_flag_low_hanging_fruit_msg() {
806        assert_no_lints(
807            "The standard form is low-hanging fruit with a hyphen and singular form.",
808            test_group(),
809        );
810    }
811
812    #[test]
813    fn dont_flag_low_hanging_fruit_desc() {
814        assert_no_lints(
815            "Corrects non-standard variants of low-hanging fruit.",
816            test_group(),
817        );
818    }
819
820    #[test]
821    fn lint_descriptions_are_clean() {
822        let mut group = LintGroup::new_curated(FstDictionary::curated(), Dialect::American);
823        let pairs: Vec<_> = group
824            .all_descriptions()
825            .into_iter()
826            .map(|(a, b)| (a.to_string(), b.to_string()))
827            .collect();
828
829        for (key, value) in pairs {
830            let doc = Document::new_markdown_default_curated(&value);
831            eprintln!("{key}: {value}");
832
833            if !group.lint(&doc).is_empty() {
834                dbg!(&group.lint(&doc));
835                panic!();
836            }
837        }
838    }
839}