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