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