From c0426a73498d093336fc32b752432b2e654838f4 Mon Sep 17 00:00:00 2001 From: Elijah Potter Date: Wed, 5 Nov 2025 08:27:25 -0700 Subject: [PATCH] feat(core): more rules (#2107) --- .../tests/language_support_sources/clean.sol | 3 +- .../javadoc_clean_simple.java | 2 +- harper-core/dictionary.dict | 3 - harper-core/proper_noun_rules.json | 1 + harper-core/src/char_ext.rs | 8 + harper-core/src/char_string.rs | 14 +- .../src/dict_word_metadata_orthography.rs | 292 ++++++++ harper-core/src/lib.rs | 1 + harper-core/src/linting/be_allowed.rs | 185 +++++ harper-core/src/linting/bought.rs | 157 ++++ harper-core/src/linting/compound_subject_i.rs | 173 +++++ harper-core/src/linting/double_click.rs | 193 +++++ harper-core/src/linting/free_predicate.rs | 203 +++++ harper-core/src/linting/hello_greeting.rs | 127 ++++ harper-core/src/linting/lint_group.rs | 22 + harper-core/src/linting/mod.rs | 22 + harper-core/src/linting/modal_seem.rs | 196 +++++ .../src/linting/orthographic_consistency.rs | 355 +++++++++ .../src/linting/phrase_corrections/mod.rs | 20 +- .../src/linting/phrase_corrections/tests.rs | 247 ++++++ harper-core/src/linting/pronoun_are.rs | 184 +++++ harper-core/src/linting/safe_to_save.rs | 186 +++++ harper-core/src/linting/theres.rs | 134 ++++ harper-core/src/linting/to_two_too/mod.rs | 5 + .../src/linting/to_two_too/to_too_adverb.rs | 2 +- harper-core/src/spell/fst_dictionary.rs | 5 +- harper-core/src/spell/rune/attribute_list.rs | 338 +-------- harper-core/tests/run_tests.rs | 8 +- .../Alice's Adventures in Wonderland.snap.yml | 135 +++- .../text/linters/Computer science.snap.yml | 28 +- .../text/linters/Difficult sentences.snap.yml | 22 +- .../linters/Part-of-speech tagging.snap.yml | 12 +- harper-core/tests/text/linters/Spell.snap.yml | 9 + ...Constitution of the United States.snap.yml | 45 ++ .../text/linters/The Great Gatsby.snap.yml | 701 +++++++++++++++++- 35 files changed, 3637 insertions(+), 401 deletions(-) create mode 100644 harper-core/src/linting/be_allowed.rs create mode 100644 harper-core/src/linting/bought.rs create mode 100644 harper-core/src/linting/compound_subject_i.rs create mode 100644 harper-core/src/linting/double_click.rs create mode 100644 harper-core/src/linting/free_predicate.rs create mode 100644 harper-core/src/linting/hello_greeting.rs create mode 100644 harper-core/src/linting/modal_seem.rs create mode 100644 harper-core/src/linting/orthographic_consistency.rs create mode 100644 harper-core/src/linting/pronoun_are.rs create mode 100644 harper-core/src/linting/safe_to_save.rs create mode 100644 harper-core/src/linting/theres.rs diff --git a/harper-comments/tests/language_support_sources/clean.sol b/harper-comments/tests/language_support_sources/clean.sol index f626cc3d..6783503f 100644 --- a/harper-comments/tests/language_support_sources/clean.sol +++ b/harper-comments/tests/language_support_sources/clean.sol @@ -11,9 +11,8 @@ contract TestContract { * @notice This is another test function. * @dev It has another [link](https://example.com) embedded inside * @param p This is a parameter - * @return fooBar The return value. */ - function testFunction2(uint256 p) external returns (address fooBar) {} + function testFunction2(uint256 p) external {} // This is some gibberish to try to trigger a lint for sentences that continue for too long // diff --git a/harper-comments/tests/language_support_sources/javadoc_clean_simple.java b/harper-comments/tests/language_support_sources/javadoc_clean_simple.java index 74125e62..54278f56 100644 --- a/harper-comments/tests/language_support_sources/javadoc_clean_simple.java +++ b/harper-comments/tests/language_support_sources/javadoc_clean_simple.java @@ -1,7 +1,7 @@ class TestClass { /** - * This is a Javadoc without any of the fancy frills that come with it. + * This is a JavaDoc without any of the fancy frills that come with it. */ public static void main(String[] args) { System.out.println("Hello world."); diff --git a/harper-core/dictionary.dict b/harper-core/dictionary.dict index c8ee4038..446f0843 100644 --- a/harper-core/dictionary.dict +++ b/harper-core/dictionary.dict @@ -28667,7 +28667,6 @@ howsoever/ hoyden/NgSJV hoydenish/J hp/~N -hr/~NS ht/~N huarache/NSg hub/~NOSg @@ -33014,7 +33013,6 @@ marital/~JY maritime/~J marjoram/Ng mark/~NgSVdGr -markdown/NgS marked/~JVtTU markedly/~R marker/~NgSV @@ -52984,7 +52982,6 @@ JWT/Ng # JSON Web Token Jacoco/Sg JavaDoc/Sg JavaScript/ONSg # programming language -Javadoc/Sg JetBrains Jetpack/Og Jira/Og # issue tracker diff --git a/harper-core/proper_noun_rules.json b/harper-core/proper_noun_rules.json index d4e90753..a72d7ad8 100644 --- a/harper-core/proper_noun_rules.json +++ b/harper-core/proper_noun_rules.json @@ -527,6 +527,7 @@ "Las Vegas", "Los Angeles", "New York", + "New York City", "Niagara Falls", "Novi Sad", "Panama Canal", diff --git a/harper-core/src/char_ext.rs b/harper-core/src/char_ext.rs index 35b84bf6..1e67e143 100644 --- a/harper-core/src/char_ext.rs +++ b/harper-core/src/char_ext.rs @@ -13,6 +13,7 @@ pub trait CharExt { /// /// Checks whether the character is in the set (A, E, I, O, U); case-insensitive. fn is_vowel(&self) -> bool; + fn normalized(self) -> Self; } impl CharExt for char { @@ -27,6 +28,13 @@ impl CharExt for char { && self.script() == Script::Latin } + fn normalized(self) -> Self { + match self { + '’' | '‘' | 'ʼ' | ''' => '\'', + _ => self, + } + } + fn is_emoji(&self) -> bool { let Some(block) = unicode_blocks::find_unicode_block(*self) else { return false; diff --git a/harper-core/src/char_string.rs b/harper-core/src/char_string.rs index d5565553..08170a81 100644 --- a/harper-core/src/char_string.rs +++ b/harper-core/src/char_string.rs @@ -1,3 +1,4 @@ +use crate::char_ext::CharExt; use std::borrow::Cow; use smallvec::SmallVec; @@ -58,12 +59,12 @@ impl CharStringExt for [char] { /// Convert a given character sequence to the standard character set /// the dictionary is in. fn normalized(&'_ self) -> Cow<'_, [char]> { - if self.as_ref().iter().any(|c| char_to_normalized(*c) != *c) { + if self.as_ref().iter().any(|c| c.normalized() != *c) { Cow::Owned( self.as_ref() .iter() .copied() - .map(char_to_normalized) + .map(|c| c.normalized()) .collect(), ) } else { @@ -120,15 +121,6 @@ impl CharStringExt for [char] { } } -fn char_to_normalized(c: char) -> char { - match c { - '’' => '\'', - '‘' => '\'', - ''' => '\'', - _ => c, - } -} - macro_rules! char_string { ($string:literal) => {{ use crate::char_string::CharString; diff --git a/harper-core/src/dict_word_metadata_orthography.rs b/harper-core/src/dict_word_metadata_orthography.rs index a77db0ab..3b0254c0 100644 --- a/harper-core/src/dict_word_metadata_orthography.rs +++ b/harper-core/src/dict_word_metadata_orthography.rs @@ -1,3 +1,5 @@ +use crate::CharStringExt; +use crate::char_ext::CharExt; use serde::{Deserialize, Serialize}; /// Orthography information. @@ -51,11 +53,301 @@ impl Default for OrthFlags { } } +impl OrthFlags { + /// Construct orthography flags for a given sequence of letters. + pub fn from_letters(letters: &[char]) -> Self { + let mut ortho_flags = Self::default(); + let mut all_lower = true; + let mut all_upper = true; + let mut first_is_upper = false; + let mut first_is_lower = false; + let mut saw_upper_after_first = false; + let mut saw_lower_after_first = false; + let mut is_first_char = true; + let mut upper_to_lower = false; + let mut lower_to_upper = false; + let letter_count = letters.iter().filter(|c| c.is_english_lingual()).count(); + + for &c in letters { + if c == ' ' { + ortho_flags |= Self::MULTIWORD; + continue; + } + + if c == '-' { + ortho_flags |= Self::HYPHENATED; + continue; + } + + if c.normalized() == '\'' { + ortho_flags |= Self::APOSTROPHE; + continue; + } + + if !c.is_english_lingual() { + continue; + } + + if c.is_lowercase() { + all_upper = false; + if is_first_char { + first_is_lower = true; + } else { + saw_lower_after_first = true; + if upper_to_lower { + lower_to_upper = true; + } + upper_to_lower = true; + } + } else if c.is_uppercase() { + all_lower = false; + if is_first_char { + first_is_upper = true; + } else { + saw_upper_after_first = true; + if lower_to_upper { + upper_to_lower = true; + } + lower_to_upper = true; + } + } else { + first_is_upper = false; + first_is_lower = false; + upper_to_lower = false; + lower_to_upper = false; + } + is_first_char = false; + } + + if letter_count > 0 { + if all_lower { + ortho_flags |= Self::LOWERCASE; + } + if all_upper { + ortho_flags |= Self::ALLCAPS; + } + if letter_count > 1 && first_is_upper && !saw_upper_after_first { + ortho_flags |= Self::TITLECASE; + } + if first_is_lower && saw_upper_after_first { + ortho_flags |= Self::LOWER_CAMEL; + } + if first_is_upper && saw_lower_after_first && saw_upper_after_first { + ortho_flags |= Self::UPPER_CAMEL; + } + } + + if looks_like_roman_numerals(letters) && is_really_roman_numerals(&letters.to_lower()) { + ortho_flags |= Self::ROMAN_NUMERALS; + } + + ortho_flags + } +} + +fn looks_like_roman_numerals(word: &[char]) -> bool { + let mut is_roman = false; + let first_char_upper; + + if let Some((&first, rest)) = word.split_first() + && "mdclxvi".contains(first.to_ascii_lowercase()) + { + first_char_upper = first.is_uppercase(); + + for &c in rest { + if !"mdclxvi".contains(c.to_ascii_lowercase()) || c.is_uppercase() != first_char_upper { + return false; + } + } + is_roman = true; + } + is_roman +} + +fn is_really_roman_numerals(word: &[char]) -> bool { + let s: String = word.iter().collect(); + let mut chars = s.chars().peekable(); + + let mut m_count = 0; + while m_count < 4 && chars.peek() == Some(&'m') { + chars.next(); + m_count += 1; + } + + if !check_roman_group(&mut chars, 'c', 'd', 'm') { + return false; + } + + if !check_roman_group(&mut chars, 'x', 'l', 'c') { + return false; + } + + if !check_roman_group(&mut chars, 'i', 'v', 'x') { + return false; + } + + if chars.next().is_some() { + return false; + } + + true +} + +fn check_roman_group>( + chars: &mut std::iter::Peekable, + one: char, + five: char, + ten: char, +) -> bool { + match chars.peek() { + Some(&c) if c == one => { + chars.next(); + match chars.peek() { + Some(&next) if next == ten || next == five => { + chars.next(); + true + } + _ => { + let mut count = 0; + while count < 2 && chars.peek() == Some(&one) { + chars.next(); + count += 1; + } + true + } + } + } + Some(&c) if c == five => { + chars.next(); + let mut count = 0; + while count < 3 && chars.peek() == Some(&one) { + chars.next(); + count += 1; + } + true + } + _ => true, + } +} + #[cfg(test)] mod tests { + use crate::CharString; use crate::dict_word_metadata::tests::md; use crate::dict_word_metadata_orthography::OrthFlags; + fn orth_flags(s: &str) -> OrthFlags { + let letters: CharString = s.chars().collect(); + OrthFlags::from_letters(&letters) + } + + #[test] + fn test_lowercase_flags() { + let flags = orth_flags("hello"); + assert!(flags.contains(OrthFlags::LOWERCASE)); + assert!(!flags.contains(OrthFlags::TITLECASE)); + assert!(!flags.contains(OrthFlags::ALLCAPS)); + assert!(!flags.contains(OrthFlags::LOWER_CAMEL)); + assert!(!flags.contains(OrthFlags::UPPER_CAMEL)); + + let flags = orth_flags("hello123"); + assert!(flags.contains(OrthFlags::LOWERCASE)); + } + + #[test] + fn test_titlecase_flags() { + let flags = orth_flags("Hello"); + assert!(!flags.contains(OrthFlags::LOWERCASE)); + assert!(flags.contains(OrthFlags::TITLECASE)); + assert!(!flags.contains(OrthFlags::ALLCAPS)); + assert!(!flags.contains(OrthFlags::LOWER_CAMEL)); + assert!(!flags.contains(OrthFlags::UPPER_CAMEL)); + + assert!(orth_flags("World").contains(OrthFlags::TITLECASE)); + assert!(orth_flags("Something").contains(OrthFlags::TITLECASE)); + assert!(!orth_flags("McDonald").contains(OrthFlags::TITLECASE)); + assert!(!orth_flags("O'Reilly").contains(OrthFlags::TITLECASE)); + assert!(!orth_flags("A").contains(OrthFlags::TITLECASE)); + } + + #[test] + fn test_allcaps_flags() { + let flags = orth_flags("HELLO"); + assert!(!flags.contains(OrthFlags::LOWERCASE)); + assert!(!flags.contains(OrthFlags::TITLECASE)); + assert!(flags.contains(OrthFlags::ALLCAPS)); + assert!(!flags.contains(OrthFlags::LOWER_CAMEL)); + assert!(!flags.contains(OrthFlags::UPPER_CAMEL)); + + assert!(orth_flags("NASA").contains(OrthFlags::ALLCAPS)); + assert!(orth_flags("I").contains(OrthFlags::ALLCAPS)); + } + + #[test] + fn test_lower_camel_flags() { + let flags = orth_flags("helloWorld"); + assert!(!flags.contains(OrthFlags::LOWERCASE)); + assert!(!flags.contains(OrthFlags::TITLECASE)); + assert!(!flags.contains(OrthFlags::ALLCAPS)); + assert!(flags.contains(OrthFlags::LOWER_CAMEL)); + assert!(!flags.contains(OrthFlags::UPPER_CAMEL)); + + assert!(orth_flags("getHTTPResponse").contains(OrthFlags::LOWER_CAMEL)); + assert!(orth_flags("eBay").contains(OrthFlags::LOWER_CAMEL)); + assert!(!orth_flags("hello").contains(OrthFlags::LOWER_CAMEL)); + assert!(!orth_flags("HelloWorld").contains(OrthFlags::LOWER_CAMEL)); + } + + #[test] + fn test_upper_camel_flags() { + let flags = orth_flags("HelloWorld"); + assert!(!flags.contains(OrthFlags::LOWERCASE)); + assert!(!flags.contains(OrthFlags::TITLECASE)); + assert!(!flags.contains(OrthFlags::ALLCAPS)); + assert!(!flags.contains(OrthFlags::LOWER_CAMEL)); + assert!(flags.contains(OrthFlags::UPPER_CAMEL)); + + assert!(orth_flags("HttpRequest").contains(OrthFlags::UPPER_CAMEL)); + assert!(orth_flags("McDonald").contains(OrthFlags::UPPER_CAMEL)); + assert!(orth_flags("O'Reilly").contains(OrthFlags::UPPER_CAMEL)); + assert!(orth_flags("XMLHttpRequest").contains(OrthFlags::UPPER_CAMEL)); + assert!(!orth_flags("Hello").contains(OrthFlags::UPPER_CAMEL)); + assert!(!orth_flags("NASA").contains(OrthFlags::UPPER_CAMEL)); + assert!(!orth_flags("Hi").contains(OrthFlags::UPPER_CAMEL)); + } + + #[test] + fn test_roman_numeral_flags() { + assert!(orth_flags("MCMXCIV").contains(OrthFlags::ROMAN_NUMERALS)); + assert!(orth_flags("mdccclxxi").contains(OrthFlags::ROMAN_NUMERALS)); + assert!(orth_flags("MMXXI").contains(OrthFlags::ROMAN_NUMERALS)); + assert!(orth_flags("mcmxciv").contains(OrthFlags::ROMAN_NUMERALS)); + assert!(orth_flags("MCMXCIV").contains(OrthFlags::ROMAN_NUMERALS)); + assert!(orth_flags("MMI").contains(OrthFlags::ROMAN_NUMERALS)); + assert!(orth_flags("MMXXV").contains(OrthFlags::ROMAN_NUMERALS)); + } + + #[test] + fn test_single_roman_numeral_flags() { + assert!(orth_flags("i").contains(OrthFlags::ROMAN_NUMERALS)); + } + + #[test] + fn empty_string_is_not_roman_numeral() { + assert!(!orth_flags("").contains(OrthFlags::ROMAN_NUMERALS)); + } + + #[test] + fn dont_allow_mixed_case_roman_numerals() { + assert!(!orth_flags("MCMlxxxVIII").contains(OrthFlags::ROMAN_NUMERALS)); + } + + #[test] + fn dont_allow_looks_like_but_isnt_roman_numeral() { + assert!(!orth_flags("mdxlivx").contains(OrthFlags::ROMAN_NUMERALS)); + assert!(!orth_flags("XIXIVV").contains(OrthFlags::ROMAN_NUMERALS)); + } + #[test] fn australia_lexeme_is_titlecase_even_when_word_is_lowercase() { assert!(md("australia").orth_info.contains(OrthFlags::TITLECASE)); diff --git a/harper-core/src/lib.rs b/harper-core/src/lib.rs index c814d044..bf5c7a66 100644 --- a/harper-core/src/lib.rs +++ b/harper-core/src/lib.rs @@ -38,6 +38,7 @@ pub use dict_word_metadata::{ AdverbData, ConjunctionData, Degree, DeterminerData, Dialect, DictWordMetadata, NounData, PronounData, VerbData, VerbForm, }; +pub use dict_word_metadata_orthography::{OrthFlags, Orthography}; pub use document::Document; pub use fat_token::{FatStringToken, FatToken}; pub use ignored_lints::{IgnoredLints, LintContext}; diff --git a/harper-core/src/linting/be_allowed.rs b/harper-core/src/linting/be_allowed.rs new file mode 100644 index 00000000..24a281dc --- /dev/null +++ b/harper-core/src/linting/be_allowed.rs @@ -0,0 +1,185 @@ +use std::sync::Arc; + +use crate::{ + Token, + expr::{Expr, ExprMap, SequenceExpr}, + linting::{ExprLinter, Lint, LintKind, Suggestion}, +}; + +pub struct BeAllowed { + expr: Box, + map: Arc>, +} + +impl Default for BeAllowed { + fn default() -> Self { + let mut map = ExprMap::default(); + + map.insert( + SequenceExpr::default() + .t_aco("will") + .t_ws() + .then_word_set(&["not"]) + .t_ws() + .t_aco("allowed") + .t_ws() + .t_aco("to") + .t_ws() + .then_verb(), + 4, + ); + + map.insert( + SequenceExpr::default() + .t_aco("won't") + .t_ws() + .t_aco("allowed") + .t_ws() + .t_aco("to") + .t_ws() + .then_verb(), + 2, + ); + + let map = Arc::new(map); + + Self { + expr: Box::new(map.clone()), + map, + } + } +} + +impl ExprLinter for BeAllowed { + fn expr(&self) -> &dyn Expr { + self.expr.as_ref() + } + + fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option { + let allowed_index = *self.map.lookup(0, matched_tokens, source)?; + let allowed_token = matched_tokens.get(allowed_index)?; + let span = allowed_token.span; + let template = span.get_content(source); + + Some(Lint { + span, + lint_kind: LintKind::Grammar, + suggestions: vec![Suggestion::replace_with_match_case( + "be allowed".chars().collect(), + template, + )], + message: "Add `be` so this reads `be allowed`.".to_owned(), + priority: 31, + }) + } + + fn description(&self) -> &'static str { + "Ensures the passive form uses `be allowed` after future negatives." + } +} + +#[cfg(test)] +mod tests { + use crate::linting::tests::{assert_lint_count, assert_suggestion_result}; + + use super::BeAllowed; + + #[test] + fn corrects_basic_sentence() { + assert_suggestion_result( + "You will not allowed to enter the lab.", + BeAllowed::default(), + "You will not be allowed to enter the lab.", + ); + } + + #[test] + fn corrects_first_person_subject() { + assert_suggestion_result( + "I will not allowed to go tonight.", + BeAllowed::default(), + "I will not be allowed to go tonight.", + ); + } + + #[test] + fn corrects_plural_subject() { + assert_suggestion_result( + "Students will not allowed to submit late work.", + BeAllowed::default(), + "Students will not be allowed to submit late work.", + ); + } + + #[test] + fn corrects_with_intro_clause() { + assert_suggestion_result( + "Because of policy, workers will not allowed to take photos.", + BeAllowed::default(), + "Because of policy, workers will not be allowed to take photos.", + ); + } + + #[test] + fn corrects_contracted_form() { + assert_suggestion_result( + "They won't allowed to park here during events.", + BeAllowed::default(), + "They won't be allowed to park here during events.", + ); + } + + #[test] + fn corrects_all_caps() { + assert_suggestion_result( + "THEY WILL NOT ALLOWED TO ENTER.", + BeAllowed::default(), + "THEY WILL NOT BE ALLOWED TO ENTER.", + ); + } + + #[test] + fn corrects_with_trailing_clause() { + assert_suggestion_result( + "Without a permit, guests will not allowed to stay overnight at the cabin.", + BeAllowed::default(), + "Without a permit, guests will not be allowed to stay overnight at the cabin.", + ); + } + + #[test] + fn corrects_with_modal_context() { + assert_suggestion_result( + "Even with approval, contractors will not allowed to access production.", + BeAllowed::default(), + "Even with approval, contractors will not be allowed to access production.", + ); + } + + #[test] + fn leaves_correct_phrase_untouched() { + assert_suggestion_result( + "They will not be allowed to park here during events.", + BeAllowed::default(), + "They will not be allowed to park here during events.", + ); + } + + #[test] + fn leaves_other_verbs_alone() { + assert_lint_count( + "We will not allow visitors after nine.", + BeAllowed::default(), + 0, + ); + } + + #[test] + fn leaves_similar_sequence_without_to() { + assert_lint_count( + "They won't be allowed to park here during events.", + BeAllowed::default(), + 0, + ); + } +} diff --git a/harper-core/src/linting/bought.rs b/harper-core/src/linting/bought.rs new file mode 100644 index 00000000..11d0f6fb --- /dev/null +++ b/harper-core/src/linting/bought.rs @@ -0,0 +1,157 @@ +use super::{ExprLinter, Lint, LintKind}; +use crate::Token; +use crate::expr::{Expr, SequenceExpr}; +use crate::linting::Suggestion; + +pub struct Bought { + expr: Box, +} + +impl Default for Bought { + fn default() -> Self { + let subject = SequenceExpr::default() + .then(Self::is_subject_pronoun_like) + .t_ws() + .then_optional(SequenceExpr::default().then_adverb().t_ws()) + .then_optional(SequenceExpr::default().then_auxiliary_verb().t_ws()) + .then_optional(SequenceExpr::default().then_adverb().t_ws()) + .then_any_capitalization_of("bough"); + + Self { + expr: Box::new(subject), + } + } +} + +impl ExprLinter for Bought { + fn expr(&self) -> &dyn Expr { + self.expr.as_ref() + } + + fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option { + let typo = matched_tokens.last()?; + + Some(Lint { + span: typo.span, + lint_kind: LintKind::WordChoice, + suggestions: vec![Suggestion::replace_with_match_case( + "bought".chars().collect(), + typo.span.get_content(source), + )], + message: "Prefer the past-tense form `bought` here.".to_owned(), + priority: 31, + }) + } + + fn description(&self) -> &'static str { + "Replaces the incorrect past-tense spelling `bough` with `bought` after subject pronouns." + } +} + +impl Bought { + fn is_subject_pronoun_like(token: &Token, source: &[char]) -> bool { + if token.kind.is_subject_pronoun() { + return true; + } + + if !token.kind.is_word() || !token.kind.is_apostrophized() { + return false; + } + + let text = token.span.get_content_string(source); + let lower = text.to_ascii_lowercase(); + + let Some((stem, suffix)) = lower.split_once('\'') else { + return false; + }; + + let is_subject_stem = matches!(stem, "i" | "you" | "we" | "they" | "he" | "she" | "it"); + let is_supported_suffix = matches!(suffix, "d" | "ve"); + + is_subject_stem && is_supported_suffix + } +} + +#[cfg(test)] +mod tests { + use super::Bought; + use crate::linting::tests::{assert_no_lints, assert_suggestion_result}; + + #[test] + fn corrects_he_bough() { + assert_suggestion_result( + "He bough a laptop yesterday.", + Bought::default(), + "He bought a laptop yesterday.", + ); + } + + #[test] + fn corrects_she_never_bough() { + assert_suggestion_result( + "She never bough fresh herbs there.", + Bought::default(), + "She never bought fresh herbs there.", + ); + } + + #[test] + fn corrects_they_already_bough() { + assert_suggestion_result( + "They already bough the train tickets.", + Bought::default(), + "They already bought the train tickets.", + ); + } + + #[test] + fn corrects_we_have_bough() { + assert_suggestion_result( + "We have bough extra paint.", + Bought::default(), + "We have bought extra paint.", + ); + } + + #[test] + fn corrects_they_have_never_bough() { + assert_suggestion_result( + "They have never bough theatre seats online.", + Bought::default(), + "They have never bought theatre seats online.", + ); + } + + #[test] + fn corrects_ive_bough() { + assert_suggestion_result( + "I've bough the ingredients already.", + Bought::default(), + "I've bought the ingredients already.", + ); + } + + #[test] + fn corrects_wed_bough() { + assert_suggestion_result( + "We'd bough snacks before the film.", + Bought::default(), + "We'd bought snacks before the film.", + ); + } + + #[test] + fn no_lint_for_tree_bough() { + assert_no_lints("The heavy bough cracked under the snow.", Bought::default()); + } + + #[test] + fn no_lint_for_he_bought() { + assert_no_lints("He bought a laptop yesterday.", Bought::default()); + } + + #[test] + fn no_lint_for_plural_boughs() { + assert_no_lints("Boughs swayed in the evening breeze.", Bought::default()); + } +} diff --git a/harper-core/src/linting/compound_subject_i.rs b/harper-core/src/linting/compound_subject_i.rs new file mode 100644 index 00000000..36b5dfc0 --- /dev/null +++ b/harper-core/src/linting/compound_subject_i.rs @@ -0,0 +1,173 @@ +use crate::{ + Token, TokenKind, + expr::{AnchorStart, Expr, SequenceExpr}, + linting::{ExprLinter, Lint, LintKind, Suggestion}, +}; + +const POSSESSIVE_DETERMINERS: &[&str] = &["my", "your", "her", "his", "their", "our"]; + +pub struct CompoundSubjectI { + expr: Box, +} + +impl Default for CompoundSubjectI { + fn default() -> Self { + let expr = SequenceExpr::default() + .then(AnchorStart) + .then_optional( + SequenceExpr::default() + .then_quote() + .then_optional(SequenceExpr::default().t_ws()), + ) + .then_optional( + SequenceExpr::default() + .then_punctuation() + .then_optional(SequenceExpr::default().t_ws()), + ) + .then_word_set(POSSESSIVE_DETERMINERS) + .t_ws() + .then_nominal() + .t_ws() + .t_aco("and") + .t_ws() + .t_aco("me") + .t_ws() + .then_kind_either(TokenKind::is_verb, TokenKind::is_auxiliary_verb); + + Self { + expr: Box::new(expr), + } + } +} + +impl ExprLinter for CompoundSubjectI { + fn expr(&self) -> &dyn Expr { + self.expr.as_ref() + } + + fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option { + let pronoun = matched_tokens.iter().find(|tok| { + tok.kind.is_word() + && tok + .span + .get_content_string(source) + .eq_ignore_ascii_case("me") + })?; + Some(Lint { + span: pronoun.span, + lint_kind: LintKind::Grammar, + suggestions: vec![Suggestion::ReplaceWith("I".chars().collect())], + message: "Use `I` when this pronoun is part of a compound subject.".to_owned(), + priority: 31, + }) + } + + fn description(&self) -> &'static str { + "Promotes `I` in compound subjects headed by a possessive determiner." + } +} + +#[cfg(test)] +mod tests { + use super::CompoundSubjectI; + use crate::linting::tests::{assert_lint_count, assert_suggestion_result}; + + #[test] + fn corrects_my_mother_and_me() { + assert_suggestion_result( + "My mother and me went to California.", + CompoundSubjectI::default(), + "My mother and I went to California.", + ); + } + + #[test] + fn corrects_my_brother_and_me() { + assert_suggestion_result( + "My brother and me would often go to the cinema.", + CompoundSubjectI::default(), + "My brother and I would often go to the cinema.", + ); + } + + #[test] + fn corrects_your_friend_and_me() { + assert_suggestion_result( + "Your friend and me are heading out.", + CompoundSubjectI::default(), + "Your friend and I are heading out.", + ); + } + + #[test] + fn corrects_her_manager_and_me() { + assert_suggestion_result( + "Her manager and me have talked about it.", + CompoundSubjectI::default(), + "Her manager and I have talked about it.", + ); + } + + #[test] + fn corrects_his_cat_and_me() { + assert_suggestion_result( + "His cat and me were inseparable.", + CompoundSubjectI::default(), + "His cat and I were inseparable.", + ); + } + + #[test] + fn corrects_their_kids_and_me() { + assert_suggestion_result( + "Their kids and me will play outside.", + CompoundSubjectI::default(), + "Their kids and I will play outside.", + ); + } + + #[test] + fn corrects_our_neighbor_and_me() { + assert_suggestion_result( + "Our neighbor and me can help tomorrow.", + CompoundSubjectI::default(), + "Our neighbor and I can help tomorrow.", + ); + } + + #[test] + fn corrects_with_quote_prefix() { + assert_suggestion_result( + "\"My mother and me went to California,\" she said.", + CompoundSubjectI::default(), + "\"My mother and I went to California,\" she said.", + ); + } + + #[test] + fn corrects_all_caps() { + assert_suggestion_result( + "MY BROTHER AND ME WILL HANDLE IT.", + CompoundSubjectI::default(), + "MY BROTHER AND I WILL HANDLE IT.", + ); + } + + #[test] + fn ignores_between_you_and_me() { + assert_lint_count( + "Between you and me, this stays here.", + CompoundSubjectI::default(), + 0, + ); + } + + #[test] + fn ignores_comma_after_me() { + assert_lint_count( + "My mother and me, as usual, went to the park.", + CompoundSubjectI::default(), + 0, + ); + } +} diff --git a/harper-core/src/linting/double_click.rs b/harper-core/src/linting/double_click.rs new file mode 100644 index 00000000..98942252 --- /dev/null +++ b/harper-core/src/linting/double_click.rs @@ -0,0 +1,193 @@ +use std::sync::Arc; + +use crate::{ + Token, TokenKind, TokenStringExt, + expr::{Expr, ExprMap, SequenceExpr}, + linting::{ExprLinter, Lint, LintKind, Suggestion}, +}; + +pub struct DoubleClick { + expr: Box, + map: Arc>, +} + +impl DoubleClick { + fn double_click_sequence() -> SequenceExpr { + SequenceExpr::default() + .t_aco("double") + .t_ws() + .then_word_set(&["click", "clicked", "clicking", "clicks"]) + } +} + +impl Default for DoubleClick { + fn default() -> Self { + let mut map = ExprMap::default(); + + map.insert( + SequenceExpr::default() + .then_seq(Self::double_click_sequence()) + .t_ws() + .then_any_word(), + 0, + ); + + map.insert( + SequenceExpr::default() + .then_seq(Self::double_click_sequence()) + .then_punctuation(), + 0, + ); + + map.insert( + SequenceExpr::default() + .then_seq(Self::double_click_sequence()) + .t_ws() + .then_kind_is_but_is_not(TokenKind::is_word, TokenKind::is_verb), + 0, + ); + + let map = Arc::new(map); + + Self { + expr: Box::new(map.clone()), + map, + } + } +} + +impl ExprLinter for DoubleClick { + fn expr(&self) -> &dyn Expr { + self.expr.as_ref() + } + + fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option { + let double_idx = *self.map.lookup(0, matched_tokens, source)?; + let click_idx = 2; + let span = matched_tokens.get(double_idx..=click_idx)?.span()?; + let template = span.get_content(source); + + let double_word = matched_tokens.get(double_idx)?.span.get_content(source); + let click_word = matched_tokens.get(click_idx)?.span.get_content(source); + + let replacement: Vec = double_word + .iter() + .copied() + .chain(['-']) + .chain(click_word.iter().copied()) + .collect(); + + Some(Lint { + span, + lint_kind: LintKind::Punctuation, + suggestions: vec![Suggestion::replace_with_match_case(replacement, template)], + message: "Add a hyphen to this command.".to_owned(), + priority: 40, + }) + } + + fn description(&self) -> &'static str { + "Encourages hyphenating `double-click` and its inflections." + } +} + +#[cfg(test)] +mod tests { + use super::DoubleClick; + use crate::linting::tests::{assert_lint_count, assert_suggestion_result}; + + #[test] + fn corrects_basic_command() { + assert_suggestion_result( + "Double click the icon.", + DoubleClick::default(), + "Double-click the icon.", + ); + } + + #[test] + fn corrects_with_preposition() { + assert_suggestion_result( + "Please double click on the link.", + DoubleClick::default(), + "Please double-click on the link.", + ); + } + + #[test] + fn corrects_with_pronoun() { + assert_suggestion_result( + "You should double click it to open.", + DoubleClick::default(), + "You should double-click it to open.", + ); + } + + #[test] + fn corrects_plural_form() { + assert_suggestion_result( + "Double clicks are recorded in the log.", + DoubleClick::default(), + "Double-clicks are recorded in the log.", + ); + } + + #[test] + fn corrects_past_tense() { + assert_suggestion_result( + "They double clicked the submit button.", + DoubleClick::default(), + "They double-clicked the submit button.", + ); + } + + #[test] + fn corrects_gerund() { + assert_suggestion_result( + "Double clicking the item highlights it.", + DoubleClick::default(), + "Double-clicking the item highlights it.", + ); + } + + #[test] + fn corrects_with_caps() { + assert_suggestion_result( + "He DOUBLE CLICKED the file.", + DoubleClick::default(), + "He DOUBLE-CLICKED the file.", + ); + } + + #[test] + fn corrects_multiline() { + assert_suggestion_result( + "Double\nclick the checkbox.", + DoubleClick::default(), + "Double-click the checkbox.", + ); + } + + #[test] + fn corrects_at_sentence_end() { + assert_suggestion_result( + "Just double click.", + DoubleClick::default(), + "Just double-click.", + ); + } + + #[test] + fn allows_hyphenated_form() { + assert_lint_count("Double-click the icon.", DoubleClick::default(), 0); + } + + #[test] + fn ignores_other_double_words() { + assert_lint_count( + "She said the double rainbow was beautiful.", + DoubleClick::default(), + 0, + ); + } +} diff --git a/harper-core/src/linting/free_predicate.rs b/harper-core/src/linting/free_predicate.rs new file mode 100644 index 00000000..b1c2b3b2 --- /dev/null +++ b/harper-core/src/linting/free_predicate.rs @@ -0,0 +1,203 @@ +use std::sync::Arc; + +use crate::Token; +use crate::TokenKind; +use crate::char_string::CharStringExt; +use crate::expr::{Expr, ExprMap, SequenceExpr}; +use crate::patterns::WhitespacePattern; + +use super::{ExprLinter, Lint, LintKind, Suggestion}; + +pub struct FreePredicate { + expr: Box, + map: Arc>, +} + +impl Default for FreePredicate { + fn default() -> Self { + let mut map = ExprMap::default(); + + let no_modifier = SequenceExpr::default() + .then(linking_like) + .t_ws() + .then(matches_fee) + .then_optional(WhitespacePattern) + .then(follows_fee); + + map.insert(no_modifier, 2); + + let with_adverb = SequenceExpr::default() + .then(linking_like) + .t_ws() + .then_adverb() + .t_ws() + .then(matches_fee) + .then_optional(WhitespacePattern) + .then(follows_fee); + + map.insert(with_adverb, 4); + + let map = Arc::new(map); + + Self { + expr: Box::new(map.clone()), + map, + } + } +} + +impl ExprLinter for FreePredicate { + fn expr(&self) -> &dyn Expr { + self.expr.as_ref() + } + + fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option { + let offending_idx = *self.map.lookup(0, matched_tokens, source)?; + let offending = matched_tokens.get(offending_idx)?; + + Some(Lint { + span: offending.span, + lint_kind: LintKind::WordChoice, + suggestions: vec![Suggestion::replace_with_match_case_str( + "free", + offending.span.get_content(source), + )], + message: "Use `free` here to show that something costs nothing.".to_owned(), + priority: 38, + }) + } + + fn description(&self) -> &'static str { + "Helps swap in `free` when a linking verb is followed by the noun `fee`." + } +} + +fn matches_fee(token: &Token, source: &[char]) -> bool { + if !token.kind.is_noun() { + return false; + } + + const FEE: [char; 3] = ['f', 'e', 'e']; + let content = token.span.get_content(source); + + content.len() == FEE.len() + && content + .iter() + .zip(FEE) + .all(|(actual, expected)| actual.eq_ignore_ascii_case(&expected)) +} + +fn follows_fee(token: &Token, _source: &[char]) -> bool { + if token.kind.is_hyphen() { + return false; + } + + token.kind.is_preposition() + || token.kind.is_conjunction() + || matches!(token.kind, TokenKind::Punctuation(_)) +} + +fn linking_like(token: &Token, source: &[char]) -> bool { + const BE_FORMS: [&str; 8] = ["be", "is", "am", "are", "was", "were", "being", "been"]; + let content = token.span.get_content(source); + + BE_FORMS + .iter() + .any(|form| content.eq_ignore_ascii_case_str(form)) +} + +#[cfg(test)] +mod tests { + use crate::linting::tests::{assert_lint_count, assert_no_lints, assert_suggestion_result}; + + use super::FreePredicate; + + #[test] + fn corrects_is_fee_for() { + assert_suggestion_result( + "The trial is fee for new members.", + FreePredicate::default(), + "The trial is free for new members.", + ); + } + + #[test] + fn corrects_totally_fee() { + assert_suggestion_result( + "Customer support is totally fee.", + FreePredicate::default(), + "Customer support is totally free.", + ); + } + + #[test] + fn corrects_really_fee_to() { + assert_suggestion_result( + "The workshop is really fee to attend.", + FreePredicate::default(), + "The workshop is really free to attend.", + ); + } + + #[test] + fn corrects_fee_with_comma() { + assert_suggestion_result( + "Our platform is fee, and always available.", + FreePredicate::default(), + "Our platform is free, and always available.", + ); + } + + #[test] + fn corrects_fee_period() { + assert_suggestion_result( + "Access is fee.", + FreePredicate::default(), + "Access is free.", + ); + } + + #[test] + fn corrects_fee_past_tense() { + assert_suggestion_result( + "The program was fee for nonprofits.", + FreePredicate::default(), + "The program was free for nonprofits.", + ); + } + + #[test] + fn allows_fee_based() { + assert_no_lints("The pricing model is fee-based.", FreePredicate::default()); + } + + #[test] + fn allows_fee_paying() { + assert_no_lints("The membership is fee-paying.", FreePredicate::default()); + } + + #[test] + fn allows_fee_schedule_statement() { + assert_no_lints( + "This plan has a fee for standard support.", + FreePredicate::default(), + ); + } + + #[test] + fn allows_fee_free_phrase() { + assert_no_lints( + "Our service is fee-free for students.", + FreePredicate::default(), + ); + } + + #[test] + fn counts_single_lint() { + assert_lint_count( + "The upgrade is fee for existing users.", + FreePredicate::default(), + 1, + ); + } +} diff --git a/harper-core/src/linting/hello_greeting.rs b/harper-core/src/linting/hello_greeting.rs new file mode 100644 index 00000000..f4ea6f22 --- /dev/null +++ b/harper-core/src/linting/hello_greeting.rs @@ -0,0 +1,127 @@ +use crate::{ + Token, + expr::{AnchorStart, Expr, SequenceExpr}, + linting::{ExprLinter, Lint, LintKind, Suggestion}, +}; + +pub struct HelloGreeting { + expr: Box, +} + +impl Default for HelloGreeting { + fn default() -> Self { + let expr = SequenceExpr::default() + .then(AnchorStart) + .then_optional(SequenceExpr::default().t_ws()) + .then_optional( + SequenceExpr::default() + .then_quote() + .then_optional(SequenceExpr::default().t_ws()), + ) + .t_aco("halo"); + + Self { + expr: Box::new(expr), + } + } +} + +impl ExprLinter for HelloGreeting { + fn expr(&self) -> &dyn Expr { + self.expr.as_ref() + } + + fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option { + let word = matched_tokens.iter().find(|tok| tok.kind.is_word())?; + let span = word.span; + let original = span.get_content(source); + + Some(Lint { + span, + lint_kind: LintKind::WordChoice, + suggestions: vec![Suggestion::replace_with_match_case( + "hello".chars().collect(), + original, + )], + message: "Prefer `hello` as a greeting; `halo` refers to the optical effect." + .to_owned(), + priority: 31, + }) + } + + fn description(&self) -> &'static str { + "Encourages greeting someone with `hello` instead of the homophone `halo`." + } +} + +#[cfg(test)] +mod tests { + use super::HelloGreeting; + use crate::linting::tests::{assert_lint_count, assert_suggestion_result}; + + #[test] + fn corrects_basic_greeting() { + assert_suggestion_result("Halo John!", HelloGreeting::default(), "Hello John!"); + } + + #[test] + fn corrects_with_comma() { + assert_suggestion_result("Halo, Jane.", HelloGreeting::default(), "Hello, Jane."); + } + + #[test] + fn corrects_with_world() { + assert_suggestion_result("Halo world!", HelloGreeting::default(), "Hello world!"); + } + + #[test] + fn corrects_without_punctuation() { + assert_suggestion_result( + "Halo there friend.", + HelloGreeting::default(), + "Hello there friend.", + ); + } + + #[test] + fn corrects_single_word_sentence() { + assert_suggestion_result("Halo!", HelloGreeting::default(), "Hello!"); + } + + #[test] + fn corrects_question() { + assert_suggestion_result("Halo?", HelloGreeting::default(), "Hello?"); + } + + #[test] + fn corrects_uppercase() { + assert_suggestion_result("HALO!", HelloGreeting::default(), "HELLO!"); + } + + #[test] + fn no_lint_for_optical_term() { + assert_lint_count( + "The halo around the moon glowed softly.", + HelloGreeting::default(), + 0, + ); + } + + #[test] + fn no_lint_mid_sentence() { + assert_lint_count( + "They shouted hello, not Halo, during rehearsal.", + HelloGreeting::default(), + 0, + ); + } + + #[test] + fn corrects_in_quotes() { + assert_suggestion_result( + "\"Halo John!\"", + HelloGreeting::default(), + "\"Hello John!\"", + ); + } +} diff --git a/harper-core/src/linting/lint_group.rs b/harper-core/src/linting/lint_group.rs index 89b70f9a..4cc6601d 100644 --- a/harper-core/src/linting/lint_group.rs +++ b/harper-core/src/linting/lint_group.rs @@ -24,8 +24,10 @@ use super::another_think_coming::AnotherThinkComing; use super::ask_no_preposition::AskNoPreposition; use super::avoid_curses::AvoidCurses; use super::back_in_the_day::BackInTheDay; +use super::be_allowed::BeAllowed; use super::best_of_all_time::BestOfAllTime; use super::boring_words::BoringWords; +use super::bought::Bought; use super::cant::Cant; use super::capitalize_personal_pronouns::CapitalizePersonalPronouns; use super::cautionary_tale::CautionaryTale; @@ -33,6 +35,7 @@ use super::change_tack::ChangeTack; use super::chock_full::ChockFull; use super::comma_fixes::CommaFixes; use super::compound_nouns::CompoundNouns; +use super::compound_subject_i::CompoundSubjectI; use super::confident::Confident; use super::correct_number_suffix::CorrectNumberSuffix; use super::criteria_phenomena::CriteriaPhenomena; @@ -40,6 +43,7 @@ use super::despite_of::DespiteOf; use super::didnt::Didnt; use super::discourse_markers::DiscourseMarkers; use super::dot_initialisms::DotInitialisms; +use super::double_click::DoubleClick; use super::double_modal::DoubleModal; use super::ellipsis_length::EllipsisLength; use super::else_possessive::ElsePossessive; @@ -52,10 +56,12 @@ use super::few_units_of_time_ago::FewUnitsOfTimeAgo; use super::filler_words::FillerWords; use super::first_aid_kit::FirstAidKit; use super::for_noun::ForNoun; +use super::free_predicate::FreePredicate; use super::friend_of_me::FriendOfMe; use super::have_pronoun::HavePronoun; use super::have_take_a_look::HaveTakeALook; use super::hedging::Hedging; +use super::hello_greeting::HelloGreeting; use super::hereby::Hereby; use super::hop_hope::HopHope; use super::how_to::HowTo; @@ -79,6 +85,7 @@ use super::missing_to::MissingTo; use super::misspell::Misspell; use super::mixed_bag::MixedBag; use super::modal_of::ModalOf; +use super::modal_seem::ModalSeem; use super::months::Months; use super::most_number::MostNumber; use super::multiple_sequential_pronouns::MultipleSequentialPronouns; @@ -94,6 +101,7 @@ use super::on_floor::OnFloor; use super::once_or_twice::OnceOrTwice; use super::one_and_the_same::OneAndTheSame; use super::open_the_light::OpenTheLight; +use super::orthographic_consistency::OrthographicConsistency; use super::ought_to_be::OughtToBe; use super::out_of_date::OutOfDate; use super::oxymorons::Oxymorons; @@ -102,6 +110,7 @@ use super::pique_interest::PiqueInterest; use super::possessive_noun::PossessiveNoun; use super::possessive_your::PossessiveYour; use super::progressive_needs_be::ProgressiveNeedsBe; +use super::pronoun_are::PronounAre; use super::pronoun_contraction::PronounContraction; use super::pronoun_inflection_be::PronounInflectionBe; use super::pronoun_knew::PronounKnew; @@ -113,6 +122,7 @@ use super::redundant_additive_adverbs::RedundantAdditiveAdverbs; use super::regionalisms::Regionalisms; use super::repeated_words::RepeatedWords; use super::roller_skated::RollerSkated; +use super::safe_to_save::SafeToSave; use super::save_to_safe::SaveToSafe; use super::semicolon_apostrophe::SemicolonApostrophe; use super::sentence_capitalization::SentenceCapitalization; @@ -132,6 +142,7 @@ use super::that_which::ThatWhich; use super::the_how_why::TheHowWhy; use super::the_my::TheMy; use super::then_than::ThenThan; +use super::theres::Theres; use super::thing_think::ThingThink; use super::though_thought::ThoughThought; use super::throw_away::ThrowAway; @@ -446,15 +457,19 @@ impl LintGroup { insert_expr_rule!(AskNoPreposition, true); insert_expr_rule!(AvoidCurses, true); insert_expr_rule!(BackInTheDay, true); + insert_expr_rule!(BeAllowed, true); insert_expr_rule!(BestOfAllTime, true); + insert_expr_rule!(Bought, true); insert_expr_rule!(BoringWords, false); insert_expr_rule!(Cant, true); insert_struct_rule!(CapitalizePersonalPronouns, true); insert_expr_rule!(CautionaryTale, true); insert_expr_rule!(ChangeTack, true); insert_expr_rule!(ChockFull, true); + insert_struct_rule!(OrthographicConsistency, true); insert_struct_rule!(CommaFixes, true); insert_struct_rule!(CompoundNouns, true); + insert_expr_rule!(CompoundSubjectI, true); insert_expr_rule!(Confident, true); insert_struct_rule!(CorrectNumberSuffix, true); insert_expr_rule!(CriteriaPhenomena, true); @@ -464,6 +479,7 @@ impl LintGroup { insert_expr_rule!(Didnt, true); insert_struct_rule!(DiscourseMarkers, true); insert_expr_rule!(DotInitialisms, true); + insert_expr_rule!(DoubleClick, true); insert_expr_rule!(DoubleModal, true); insert_struct_rule!(EllipsisLength, true); insert_struct_rule!(ElsePossessive, true); @@ -475,9 +491,11 @@ impl LintGroup { insert_expr_rule!(FillerWords, true); insert_struct_rule!(FirstAidKit, true); insert_struct_rule!(ForNoun, true); + insert_expr_rule!(FreePredicate, true); insert_expr_rule!(FriendOfMe, true); insert_expr_rule!(HavePronoun, true); insert_expr_rule!(Hedging, true); + insert_expr_rule!(HelloGreeting, true); insert_expr_rule!(Hereby, true); insert_struct_rule!(HopHope, true); insert_struct_rule!(HowTo, true); @@ -500,6 +518,7 @@ impl LintGroup { insert_expr_rule!(MissingTo, true); insert_expr_rule!(MixedBag, true); insert_expr_rule!(ModalOf, true); + insert_expr_rule!(ModalSeem, true); insert_expr_rule!(Months, true); insert_expr_rule!(MostNumber, true); insert_expr_rule!(MultipleSequentialPronouns, true); @@ -526,6 +545,7 @@ impl LintGroup { insert_expr_rule!(PiqueInterest, true); insert_expr_rule!(PossessiveYour, true); insert_expr_rule!(ProgressiveNeedsBe, true); + insert_expr_rule!(PronounAre, true); insert_struct_rule!(PronounContraction, true); insert_expr_rule!(PronounInflectionBe, true); insert_struct_rule!(PronounKnew, true); @@ -535,6 +555,7 @@ impl LintGroup { insert_expr_rule!(RedundantAdditiveAdverbs, true); insert_struct_rule!(RepeatedWords, true); insert_expr_rule!(RollerSkated, true); + insert_expr_rule!(SafeToSave, true); insert_struct_rule!(SaveToSafe, true); insert_expr_rule!(SemicolonApostrophe, true); insert_expr_rule!(ShootOneselfInTheFoot, true); @@ -550,6 +571,7 @@ impl LintGroup { insert_expr_rule!(ThatWhich, true); insert_expr_rule!(TheHowWhy, true); insert_struct_rule!(TheMy, true); + insert_expr_rule!(Theres, true); insert_expr_rule!(ThenThan, true); insert_expr_rule!(ThingThink, true); insert_expr_rule!(ThoughThought, true); diff --git a/harper-core/src/linting/mod.rs b/harper-core/src/linting/mod.rs index 952af0ad..0a958f89 100644 --- a/harper-core/src/linting/mod.rs +++ b/harper-core/src/linting/mod.rs @@ -15,8 +15,10 @@ mod another_think_coming; mod ask_no_preposition; mod avoid_curses; mod back_in_the_day; +mod be_allowed; mod best_of_all_time; mod boring_words; +mod bought; mod cant; mod capitalize_personal_pronouns; mod cautionary_tale; @@ -25,6 +27,7 @@ mod chock_full; mod closed_compounds; mod comma_fixes; mod compound_nouns; +mod compound_subject_i; mod confident; mod correct_number_suffix; mod criteria_phenomena; @@ -35,6 +38,7 @@ mod determiner_without_noun; mod didnt; mod discourse_markers; mod dot_initialisms; +mod double_click; mod double_modal; mod ellipsis_length; mod else_possessive; @@ -47,10 +51,12 @@ mod few_units_of_time_ago; mod filler_words; mod first_aid_kit; mod for_noun; +mod free_predicate; mod friend_of_me; mod have_pronoun; mod have_take_a_look; mod hedging; +mod hello_greeting; mod hereby; mod hop_hope; mod hope_youre; @@ -86,6 +92,7 @@ mod missing_to; mod misspell; mod mixed_bag; mod modal_of; +mod modal_seem; mod months; mod most_number; mod multiple_sequential_pronouns; @@ -104,6 +111,7 @@ mod once_or_twice; mod one_and_the_same; mod open_compounds; mod open_the_light; +mod orthographic_consistency; mod ought_to_be; mod out_of_date; mod oxford_comma; @@ -115,6 +123,7 @@ mod pique_interest; mod possessive_noun; mod possessive_your; mod progressive_needs_be; +mod pronoun_are; mod pronoun_contraction; mod pronoun_inflection_be; mod pronoun_knew; @@ -126,6 +135,7 @@ mod redundant_additive_adverbs; mod regionalisms; mod repeated_words; mod roller_skated; +mod safe_to_save; mod save_to_safe; mod semicolon_apostrophe; mod sentence_capitalization; @@ -147,6 +157,7 @@ mod that_which; mod the_how_why; mod the_my; mod then_than; +mod theres; mod thing_think; mod though_thought; mod throw_away; @@ -181,8 +192,10 @@ pub use another_think_coming::AnotherThinkComing; pub use ask_no_preposition::AskNoPreposition; pub use avoid_curses::AvoidCurses; pub use back_in_the_day::BackInTheDay; +pub use be_allowed::BeAllowed; pub use best_of_all_time::BestOfAllTime; pub use boring_words::BoringWords; +pub use bought::Bought; pub use cant::Cant; pub use capitalize_personal_pronouns::CapitalizePersonalPronouns; pub use cautionary_tale::CautionaryTale; @@ -190,6 +203,7 @@ pub use change_tack::ChangeTack; pub use chock_full::ChockFull; pub use comma_fixes::CommaFixes; pub use compound_nouns::CompoundNouns; +pub use compound_subject_i::CompoundSubjectI; pub use confident::Confident; pub use correct_number_suffix::CorrectNumberSuffix; pub use criteria_phenomena::CriteriaPhenomena; @@ -199,6 +213,7 @@ pub use despite_of::DespiteOf; pub use didnt::Didnt; pub use discourse_markers::DiscourseMarkers; pub use dot_initialisms::DotInitialisms; +pub use double_click::DoubleClick; pub use double_modal::DoubleModal; pub use ellipsis_length::EllipsisLength; pub use everyday::Everyday; @@ -209,10 +224,12 @@ pub use feel_fell::FeelFell; pub use few_units_of_time_ago::FewUnitsOfTimeAgo; pub use filler_words::FillerWords; pub use for_noun::ForNoun; +pub use free_predicate::FreePredicate; pub use friend_of_me::FriendOfMe; pub use have_pronoun::HavePronoun; pub use have_take_a_look::HaveTakeALook; pub use hedging::Hedging; +pub use hello_greeting::HelloGreeting; pub use hereby::Hereby; pub use hop_hope::HopHope; pub use how_to::HowTo; @@ -243,6 +260,7 @@ pub use missing_to::MissingTo; pub use misspell::Misspell; pub use mixed_bag::MixedBag; pub use modal_of::ModalOf; +pub use modal_seem::ModalSeem; pub use months::Months; pub use most_number::MostNumber; pub use multiple_sequential_pronouns::MultipleSequentialPronouns; @@ -259,6 +277,7 @@ pub use on_floor::OnFloor; pub use once_or_twice::OnceOrTwice; pub use one_and_the_same::OneAndTheSame; pub use open_the_light::OpenTheLight; +pub use orthographic_consistency::OrthographicConsistency; pub use ought_to_be::OughtToBe; pub use out_of_date::OutOfDate; pub use oxford_comma::OxfordComma; @@ -268,6 +287,7 @@ pub use pique_interest::PiqueInterest; pub use possessive_noun::PossessiveNoun; pub use possessive_your::PossessiveYour; pub use progressive_needs_be::ProgressiveNeedsBe; +pub use pronoun_are::PronounAre; pub use pronoun_contraction::PronounContraction; pub use pronoun_inflection_be::PronounInflectionBe; pub use quantifier_needs_of::QuantifierNeedsOf; @@ -277,6 +297,7 @@ pub use redundant_additive_adverbs::RedundantAdditiveAdverbs; pub use regionalisms::Regionalisms; pub use repeated_words::RepeatedWords; pub use roller_skated::RollerSkated; +pub use safe_to_save::SafeToSave; pub use save_to_safe::SaveToSafe; pub use semicolon_apostrophe::SemicolonApostrophe; pub use sentence_capitalization::SentenceCapitalization; @@ -298,6 +319,7 @@ pub use that_which::ThatWhich; pub use the_how_why::TheHowWhy; pub use the_my::TheMy; pub use then_than::ThenThan; +pub use theres::Theres; pub use thing_think::ThingThink; pub use though_thought::ThoughThought; pub use throw_away::ThrowAway; diff --git a/harper-core/src/linting/modal_seem.rs b/harper-core/src/linting/modal_seem.rs new file mode 100644 index 00000000..acf061e3 --- /dev/null +++ b/harper-core/src/linting/modal_seem.rs @@ -0,0 +1,196 @@ +use std::sync::Arc; + +use crate::{ + CharStringExt, Token, + expr::{Expr, ExprMap, SequenceExpr}, + linting::{ExprLinter, Lint, LintKind, Suggestion}, + patterns::ModalVerb, +}; + +#[derive(Clone, Copy, Default)] +struct MatchContext { + modal_index: usize, +} + +pub struct ModalSeem { + expr: Box, + map: Arc>, +} + +impl ModalSeem { + fn base_sequence() -> SequenceExpr { + SequenceExpr::default() + .then(ModalVerb::default()) + .t_ws() + .t_aco("seen") + } + + fn adjective_step() -> SequenceExpr { + SequenceExpr::default() + .t_ws() + .then(|tok: &Token, _source: &[char]| tok.kind.is_adjective()) + } + + fn adverb_then_adjective_step() -> SequenceExpr { + SequenceExpr::default() + .t_ws() + .then(|tok: &Token, _source: &[char]| tok.kind.is_adverb()) + .t_ws() + .then(|tok: &Token, _source: &[char]| tok.kind.is_adjective()) + } +} + +impl Default for ModalSeem { + fn default() -> Self { + let mut map = ExprMap::default(); + + map.insert( + SequenceExpr::default() + .then_seq(Self::base_sequence()) + .then(Self::adjective_step()), + MatchContext::default(), + ); + + map.insert( + SequenceExpr::default() + .then_seq(Self::base_sequence()) + .then(Self::adverb_then_adjective_step()), + MatchContext::default(), + ); + + let map = Arc::new(map); + + Self { + expr: Box::new(map.clone()), + map, + } + } +} + +impl ExprLinter for ModalSeem { + fn expr(&self) -> &dyn Expr { + self.expr.as_ref() + } + + fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option { + let context = self.map.lookup(0, matched_tokens, source)?; + + let seen_token = matched_tokens + .iter() + .skip(context.modal_index) + .find(|tok| { + tok.span + .get_content(source) + .eq_ignore_ascii_case_str("seen") + })?; + + let span = seen_token.span; + let original = span.get_content(source); + + Some(Lint { + span, + lint_kind: LintKind::Grammar, + suggestions: vec![ + Suggestion::replace_with_match_case("seem".chars().collect(), original), + Suggestion::replace_with_match_case("be".chars().collect(), original), + ], + message: "Swap `seen` for a linking verb when it follows a modal before an adjective." + .to_owned(), + priority: 32, + }) + } + + fn description(&self) -> &str { + "Detects modal verbs followed by `seen` before adjectives and suggests `seem` or `be`." + } +} + +#[cfg(test)] +mod tests { + use super::ModalSeem; + use crate::linting::tests::{ + assert_lint_count, assert_no_lints, assert_nth_suggestion_result, assert_suggestion_result, + }; + + #[test] + fn corrects_basic_case() { + assert_suggestion_result( + "It may seen impossible to finish.", + ModalSeem::default(), + "It may seem impossible to finish.", + ); + } + + #[test] + fn corrects_with_adverb() { + assert_suggestion_result( + "That might seen utterly ridiculous.", + ModalSeem::default(), + "That might seem utterly ridiculous.", + ); + } + + #[test] + fn offers_be_option() { + assert_nth_suggestion_result( + "It may seen impossible to finish.", + ModalSeem::default(), + "It may be impossible to finish.", + 1, + ); + } + + #[test] + fn respects_uppercase() { + assert_suggestion_result( + "THIS COULD SEEN TERRIBLE.", + ModalSeem::default(), + "THIS COULD SEEM TERRIBLE.", + ); + } + + #[test] + fn corrects_before_punctuation() { + assert_suggestion_result( + "Still, it may seen absurd, but we will continue.", + ModalSeem::default(), + "Still, it may seem absurd, but we will continue.", + ); + } + + #[test] + fn corrects_across_newline() { + assert_suggestion_result( + "It may seen\n impossible to pull off.", + ModalSeem::default(), + "It may seem\n impossible to pull off.", + ); + } + + #[test] + fn ignores_correct_seem() { + assert_no_lints("It may seem impossible to finish.", ModalSeem::default()); + } + + #[test] + fn ignores_modal_with_be_seen() { + assert_no_lints("It may be seen as unfair.", ModalSeem::default()); + } + + #[test] + fn ignores_modal_seen_noun() { + assert_no_lints( + "It may seen results sooner than expected.", + ModalSeem::default(), + ); + } + + #[test] + fn ignores_modal_seen_clause() { + assert_lint_count( + "It may seen that we are improving.", + ModalSeem::default(), + 0, + ); + } +} diff --git a/harper-core/src/linting/orthographic_consistency.rs b/harper-core/src/linting/orthographic_consistency.rs new file mode 100644 index 00000000..aa7fa3bd --- /dev/null +++ b/harper-core/src/linting/orthographic_consistency.rs @@ -0,0 +1,355 @@ +use crate::linting::{LintKind, Suggestion}; +use std::sync::Arc; + +use crate::expr::Expr; +use crate::spell::{Dictionary, FstDictionary}; +use crate::{OrthFlags, Token}; + +use super::{ExprLinter, Lint}; +pub struct OrthographicConsistency { + dict: Arc, + expr: Box, +} + +impl OrthographicConsistency { + pub fn new() -> Self { + Self { + dict: FstDictionary::curated(), + expr: Box::new(|tok: &Token, _: &[char]| tok.kind.is_word()), + } + } +} + +impl Default for OrthographicConsistency { + fn default() -> Self { + Self::new() + } +} + +impl ExprLinter for OrthographicConsistency { + fn description(&self) -> &str { + "Ensures word casing matches the dictionary's canonical orthography." + } + + fn expr(&self) -> &dyn Expr { + self.expr.as_ref() + } + + fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option { + let word = &matched_tokens[0]; + + let Some(Some(metadata)) = word.kind.as_word() else { + return None; + }; + + let chars = word.span.get_content(source); + + let cur_flags = OrthFlags::from_letters(chars); + + if metadata.is_allcaps() + && !metadata.is_lowercase() + && !cur_flags.contains(OrthFlags::ALLCAPS) + { + return Some(Lint { + span: word.span, + lint_kind: LintKind::Capitalization, + suggestions: vec![Suggestion::ReplaceWith( + chars.iter().map(|c| c.to_ascii_uppercase()).collect(), + )], + message: "This word's canonical spelling is all-caps.".to_owned(), + priority: 127, + }); + } + + let canonical_flags = metadata.orth_info; + let flags_to_check = [ + OrthFlags::LOWER_CAMEL, + OrthFlags::UPPER_CAMEL, + OrthFlags::APOSTROPHE, + OrthFlags::HYPHENATED, + ]; + + if flags_to_check + .iter() + .any(|flag| canonical_flags.contains(*flag) != cur_flags.contains(*flag)) + && let Some(canonical) = self.dict.get_correct_capitalization_of(chars) + { + return Some(Lint { + span: word.span, + lint_kind: LintKind::Capitalization, + suggestions: vec![Suggestion::ReplaceWith(canonical.to_vec())], + message: format!( + "The canonical dictionary spelling is `{}`.", + canonical.iter().collect::() + ), + priority: 127, + }); + } + + if metadata.is_titlecase() + && cur_flags.contains(OrthFlags::LOWERCASE) + && let Some(canonical) = self.dict.get_correct_capitalization_of(chars) + && canonical != chars + { + return Some(Lint { + span: word.span, + lint_kind: LintKind::Capitalization, + suggestions: vec![Suggestion::ReplaceWith(canonical.to_vec())], + message: format!( + "The canonical dictionary spelling is `{}`.", + canonical.iter().collect::() + ), + priority: 127, + }); + } + + None + } +} + +#[cfg(test)] +mod tests { + use crate::linting::tests::{assert_no_lints, assert_suggestion_result}; + + use super::OrthographicConsistency; + + #[test] + fn nasa_should_be_all_caps() { + assert_suggestion_result( + "Nasa is a governmental institution.", + OrthographicConsistency::default(), + "NASA is a governmental institution.", + ); + } + + #[test] + fn ikea_should_be_all_caps() { + assert_suggestion_result( + "Ikea operates a vast retail network.", + OrthographicConsistency::default(), + "IKEA operates a vast retail network.", + ); + } + + #[test] + fn lego_should_be_all_caps() { + assert_suggestion_result( + "Lego bricks encourage creativity.", + OrthographicConsistency::default(), + "LEGO bricks encourage creativity.", + ); + } + + #[test] + fn nato_should_be_all_caps() { + assert_suggestion_result( + "Nato is a military alliance.", + OrthographicConsistency::default(), + "NATO is a military alliance.", + ); + } + + #[test] + fn fbi_should_be_all_caps() { + assert_suggestion_result( + "Fbi investigates federal crimes.", + OrthographicConsistency::default(), + "FBI investigates federal crimes.", + ); + } + + #[test] + fn cia_should_be_all_caps() { + assert_suggestion_result( + "Cia gathers intelligence.", + OrthographicConsistency::default(), + "CIA gathers intelligence.", + ); + } + + #[test] + fn hiv_should_be_all_caps() { + assert_suggestion_result( + "Hiv is a virus.", + OrthographicConsistency::default(), + "HIV is a virus.", + ); + } + + #[test] + fn dna_should_be_all_caps() { + assert_suggestion_result( + "Dna carries genetic information.", + OrthographicConsistency::default(), + "DNA carries genetic information.", + ); + } + + #[test] + fn rna_should_be_all_caps() { + assert_suggestion_result( + "Rna participates in protein synthesis.", + OrthographicConsistency::default(), + "RNA participates in protein synthesis.", + ); + } + + #[test] + fn cpu_should_be_all_caps() { + assert_suggestion_result( + "Cpu executes instructions.", + OrthographicConsistency::default(), + "CPU executes instructions.", + ); + } + + #[test] + fn gpu_should_be_all_caps() { + assert_suggestion_result( + "Gpu accelerates graphics.", + OrthographicConsistency::default(), + "GPU accelerates graphics.", + ); + } + + #[test] + fn html_should_be_all_caps() { + assert_suggestion_result( + "Html structures web documents.", + OrthographicConsistency::default(), + "HTML structures web documents.", + ); + } + + #[test] + fn url_should_be_all_caps() { + assert_suggestion_result( + "Url identifies a resource.", + OrthographicConsistency::default(), + "URL identifies a resource.", + ); + } + + #[test] + fn faq_should_be_all_caps() { + assert_suggestion_result( + "Faq answers common questions.", + OrthographicConsistency::default(), + "FAQ answers common questions.", + ); + } + + #[test] + fn linkedin_should_use_canonical_case() { + assert_suggestion_result( + "I updated my linkedin profile yesterday.", + OrthographicConsistency::default(), + "I updated my LinkedIn profile yesterday.", + ); + } + + #[test] + fn wordpress_should_use_canonical_case() { + assert_suggestion_result( + "She writes daily on her wordpress blog.", + OrthographicConsistency::default(), + "She writes daily on her WordPress blog.", + ); + } + + #[test] + fn pdf_should_be_all_caps() { + assert_suggestion_result( + "Pdf preserves formatting.", + OrthographicConsistency::default(), + "PDF preserves formatting.", + ); + } + + #[test] + fn ceo_should_be_all_caps() { + assert_suggestion_result( + "Our Ceo approved the budget.", + OrthographicConsistency::default(), + "Our CEO approved the budget.", + ); + } + + #[test] + fn cfo_should_be_all_caps() { + assert_suggestion_result( + "The Cfo presented the report.", + OrthographicConsistency::default(), + "The CFO presented the report.", + ); + } + + #[test] + fn hr_should_be_all_caps() { + assert_suggestion_result( + "The Hr team scheduled interviews.", + OrthographicConsistency::default(), + "The HR team scheduled interviews.", + ); + } + + #[test] + fn ai_should_be_all_caps() { + assert_suggestion_result( + "Ai enables new capabilities.", + OrthographicConsistency::default(), + "AI enables new capabilities.", + ); + } + + #[test] + fn ufo_should_be_all_caps() { + assert_suggestion_result( + "Ufo sightings provoke debate.", + OrthographicConsistency::default(), + "UFO sightings provoke debate.", + ); + } + + #[test] + fn markdown_should_be_caps() { + assert_suggestion_result( + "I adore markdown.", + OrthographicConsistency::default(), + "I adore Markdown.", + ); + } + + #[test] + fn canonical_forms_should_not_be_flagged() { + let sentences = [ + "NASA is a governmental institution.", + "IKEA operates a vast retail network.", + "LEGO bricks encourage creativity.", + "NATO is a military alliance.", + "FBI investigates federal crimes.", + "CIA gathers intelligence.", + "HIV is a virus.", + "DNA carries genetic information.", + "RNA participates in protein synthesis.", + "CPU executes instructions.", + "GPU accelerates graphics.", + "HTML structures web documents.", + "URL identifies a resource.", + "FAQ answers common questions.", + "I updated my LinkedIn profile yesterday.", + "She writes daily on her WordPress blog.", + "PDF preserves formatting.", + "Our CEO approved the budget.", + "The CFO presented the report.", + "The HR team scheduled interviews.", + "AI enables new capabilities.", + "UFO sightings provoke debate.", + "I adore Markdown.", + ]; + + for sentence in sentences { + assert_no_lints(sentence, OrthographicConsistency::default()); + } + } +} diff --git a/harper-core/src/linting/phrase_corrections/mod.rs b/harper-core/src/linting/phrase_corrections/mod.rs index cf279129..61066677 100644 --- a/harper-core/src/linting/phrase_corrections/mod.rs +++ b/harper-core/src/linting/phrase_corrections/mod.rs @@ -58,10 +58,10 @@ pub fn lint_group() -> LintGroup { "Corrects `an` to `and` after `ahead`." ), "AllOfASudden" => ( - ["all of the sudden", "all of sudden"], + ["all of the sudden", "all of sudden", "all the sudden"], ["all of a sudden"], - "The phrase is `all of a sudden`, meaning `unexpectedly`.", - "Corrects `all of the sudden` to `all of a sudden`.", + "Prefer the standard phrasing `all of a sudden`.", + "Guides this expression toward the standard `all of a sudden`.", LintKind::Nonstandard ), "ALongTime" => ( @@ -71,6 +71,13 @@ pub fn lint_group() -> LintGroup { "Corrects `along time` to `a long time`.", LintKind::Grammar ), + "Alongside" => ( + ["along side"], + ["alongside"], + "Use the single word `alongside`.", + "Replaces the spaced form `along side` with `alongside`.", + LintKind::WordChoice + ), "AlzheimersDisease" => ( ["old-timers' disease"], ["Alzheimer’s disease"], @@ -335,6 +342,13 @@ pub fn lint_group() -> LintGroup { "In English, negation still requires the complete verb form (“want”), so avoid truncating it to “wan.”", LintKind::Typo ), + "EggYolk" => ( + ["egg yoke"], + ["egg yolk"], + "Use `egg yolk` when you mean the yellow portion of an egg.", + "Corrects the eggcorn `egg yoke`, replacing it with the standard culinary term `egg yolk`.", + LintKind::Eggcorn + ), "DontCan" => ( ["don't can"], ["can't", "cannot"], diff --git a/harper-core/src/linting/phrase_corrections/tests.rs b/harper-core/src/linting/phrase_corrections/tests.rs index c1c57884..89fff0a7 100644 --- a/harper-core/src/linting/phrase_corrections/tests.rs +++ b/harper-core/src/linting/phrase_corrections/tests.rs @@ -45,6 +45,95 @@ fn corrects_all_of_a_sudden() { ) } +#[test] +fn corrects_all_the_sudden_basic() { + assert_suggestion_result( + "It happened all the sudden when the lights went out.", + lint_group(), + "It happened all of a sudden when the lights went out.", + ); +} + +#[test] +fn corrects_all_the_sudden_sentence_start() { + assert_suggestion_result( + "All the sudden the room fell quiet.", + lint_group(), + "All of a sudden the room fell quiet.", + ); +} + +#[test] +fn corrects_all_the_sudden_with_comma() { + assert_suggestion_result( + "The music stopped, all the sudden, during the chorus.", + lint_group(), + "The music stopped, all of a sudden, during the chorus.", + ); +} + +#[test] +fn corrects_all_the_sudden_question() { + assert_suggestion_result( + "Did the power cut all the sudden?", + lint_group(), + "Did the power cut all of a sudden?", + ); +} + +#[test] +fn corrects_all_the_sudden_in_quotes() { + assert_suggestion_result( + "He whispered, \"all the sudden we were alone.\"", + lint_group(), + "He whispered, \"all of a sudden we were alone.\"", + ); +} + +#[test] +fn corrects_all_the_sudden_all_caps() { + assert_suggestion_result( + "ALL THE SUDDEN THE ROOM WENT DARK.", + lint_group(), + "ALL OF A SUDDEN THE ROOM WENT DARK.", + ); +} + +#[test] +fn corrects_all_the_sudden_end_period() { + assert_suggestion_result( + "They were laughing all the sudden.", + lint_group(), + "They were laughing all of a sudden.", + ); +} + +#[test] +fn counts_all_the_sudden_once() { + assert_lint_count( + "This all the sudden change surprised everyone.", + lint_group(), + 1, + ); +} + +#[test] +fn corrects_all_of_sudden_variant() { + assert_suggestion_result( + "It stormed all of sudden after a warm morning.", + lint_group(), + "It stormed all of a sudden after a warm morning.", + ); +} + +#[test] +fn ignores_all_the_suddenness() { + assert_no_lints( + "Their excitement and suddenness were all the suddenness she remembered.", + lint_group(), + ); +} + // ALongTime #[test] fn detect_a_long_time() { @@ -60,6 +149,85 @@ fn detect_a_long_time_real_world() { ); } +// Alongside +#[test] +fn corrects_along_side_basic() { + assert_suggestion_result( + "They walked along side the river.", + lint_group(), + "They walked alongside the river.", + ); +} + +#[test] +fn corrects_along_side_sentence_start() { + assert_suggestion_result( + "Along side the road, we saw a parade.", + lint_group(), + "Alongside the road, we saw a parade.", + ); +} + +#[test] +fn corrects_along_side_all_caps() { + assert_suggestion_result( + "The banner read ALONG SIDE THE TEAM!", + lint_group(), + "The banner read ALONGSIDE THE TEAM!", + ); +} + +#[test] +fn corrects_along_side_with_period() { + assert_suggestion_result( + "The skiff pulled along side.", + lint_group(), + "The skiff pulled alongside.", + ); +} + +#[test] +fn corrects_along_side_in_quotes() { + assert_suggestion_result( + "\"We drifted along side,\" she said.", + lint_group(), + "\"We drifted alongside,\" she said.", + ); +} + +#[test] +fn corrects_along_side_before_comma() { + assert_suggestion_result( + "They stood along side, waiting patiently.", + lint_group(), + "They stood alongside, waiting patiently.", + ); +} + +#[test] +fn corrects_along_side_plural_subject() { + assert_suggestion_result( + "Cars lined up along side the curb.", + lint_group(), + "Cars lined up alongside the curb.", + ); +} + +#[test] +fn allows_correct_alongside() { + assert_lint_count("They walked alongside the river.", lint_group(), 0); +} + +#[test] +fn allows_along_the_side_phrase() { + assert_lint_count("They walked along the side of the river.", lint_group(), 0); +} + +#[test] +fn allows_lakeside_usage() { + assert_lint_count("We camped along the lakeside all weekend.", lint_group(), 0); +} + // AlzheimersDisease // -none- @@ -375,6 +543,85 @@ fn does_not_flag_already_correct() { assert_lint_count("I don't want to leave.", lint_group(), 0); } +// EggYolk +#[test] +fn corrects_simple_egg_yoke() { + assert_suggestion_result( + "She whisked the egg yoke briskly.", + lint_group(), + "She whisked the egg yolk briskly.", + ); +} + +#[test] +fn corrects_sentence_start_egg_yoke() { + assert_suggestion_result( + "Egg yoke is rich in nutrients.", + lint_group(), + "Egg yolk is rich in nutrients.", + ); +} + +#[test] +fn corrects_all_caps_egg_yoke() { + assert_suggestion_result( + "Add the EGG YOKE to the batter.", + lint_group(), + "Add the EGG YOLK to the batter.", + ); +} + +#[test] +fn corrects_punctuated_egg_yoke() { + assert_suggestion_result( + "Separate the egg yoke, then fold it in.", + lint_group(), + "Separate the egg yolk, then fold it in.", + ); +} + +#[test] +fn corrects_adjective_egg_yoke() { + assert_suggestion_result( + "The runny egg yoke spilled over the toast.", + lint_group(), + "The runny egg yolk spilled over the toast.", + ); +} + +#[test] +fn corrects_plural_context_egg_yoke() { + assert_suggestion_result( + "Blend the cream with each egg yoke before baking.", + lint_group(), + "Blend the cream with each egg yolk before baking.", + ); +} + +#[test] +fn allows_correct_egg_yolk() { + assert_lint_count("The custard calls for one egg yolk.", lint_group(), 0); +} + +#[test] +fn allows_plural_egg_yolks() { + assert_lint_count("Reserve the egg yolks for later.", lint_group(), 0); +} + +#[test] +fn allows_yoke_without_egg() { + assert_lint_count( + "The artisan carved a wooden yoke for the oxen.", + lint_group(), + 0, + ); +} + +#[test] +fn does_not_flag_partial_phrase() { + assert_lint_count("Crack the eggs so no yoke spills.", lint_group(), 0); +} + // DontCan #[test] fn corrects_dont_can() { diff --git a/harper-core/src/linting/pronoun_are.rs b/harper-core/src/linting/pronoun_are.rs new file mode 100644 index 00000000..63a3fc41 --- /dev/null +++ b/harper-core/src/linting/pronoun_are.rs @@ -0,0 +1,184 @@ +use crate::{ + Token, TokenStringExt, + expr::{Expr, SequenceExpr}, + linting::{ExprLinter, Lint, LintKind, Suggestion}, +}; + +/// Corrects the shorthand `r` after plural first- and second-person pronouns. +pub struct PronounAre { + expr: Box, +} + +impl Default for PronounAre { + fn default() -> Self { + let expr = SequenceExpr::default() + .then(|tok: &Token, _src: &[char]| { + tok.kind.is_pronoun() + && tok.kind.is_subject_pronoun() + && (tok.kind.is_second_person_pronoun() + || tok.kind.is_first_person_plural_pronoun() + || tok.kind.is_third_person_plural_pronoun()) + }) + .t_ws() + .t_aco("r"); + + Self { + expr: Box::new(expr), + } + } +} + +impl ExprLinter for PronounAre { + fn expr(&self) -> &dyn Expr { + self.expr.as_ref() + } + + fn match_to_lint(&self, tokens: &[Token], source: &[char]) -> Option { + let span = tokens.span()?; + let pronoun = tokens.first()?; + let gap = tokens.get(1)?; + let letter = tokens.get(2)?; + + let pronoun_chars = pronoun.span.get_content(source); + let gap_chars = gap.span.get_content(source); + let letter_chars = letter.span.get_content(source); + + let all_pronoun_letters_uppercase = pronoun_chars + .iter() + .filter(|c| c.is_alphabetic()) + .all(|c| c.is_uppercase()); + let letter_has_uppercase = letter_chars.iter().any(|c| c.is_uppercase()); + let uppercase_suffix = letter_has_uppercase || all_pronoun_letters_uppercase; + + let are_suffix: Vec = if uppercase_suffix { + vec!['A', 'R', 'E'] + } else { + vec!['a', 'r', 'e'] + }; + + let re_suffix: Vec = if uppercase_suffix { + vec!['R', 'E'] + } else { + vec!['r', 'e'] + }; + + let mut with_are = pronoun_chars.to_vec(); + with_are.extend_from_slice(gap_chars); + with_are.extend(are_suffix); + + let mut with_contraction = pronoun_chars.to_vec(); + with_contraction.push('\''); + with_contraction.extend(re_suffix); + + Some(Lint { + span, + lint_kind: LintKind::WordChoice, + suggestions: vec![ + Suggestion::ReplaceWith(with_are), + Suggestion::ReplaceWith(with_contraction), + ], + message: "Use the full verb or the contraction after this pronoun.".to_owned(), + priority: 40, + }) + } + + fn description(&self) -> &str { + "Spots the letter `r` used in place of `are` or `you're` after plural first- or second-person pronouns." + } +} + +#[cfg(test)] +mod tests { + use super::PronounAre; + use crate::linting::tests::{ + assert_lint_count, assert_nth_suggestion_result, assert_suggestion_result, + }; + + #[test] + fn fixes_you_r() { + assert_suggestion_result( + "You r absolutely right.", + PronounAre::default(), + "You are absolutely right.", + ); + } + + #[test] + fn offers_contraction_option() { + assert_nth_suggestion_result( + "You r absolutely right.", + PronounAre::default(), + "You're absolutely right.", + 1, + ); + } + + #[test] + fn keeps_uppercase_pronoun() { + assert_suggestion_result( + "YOU r welcome here.", + PronounAre::default(), + "YOU ARE welcome here.", + ); + } + + #[test] + fn fixes_they_r_with_comma() { + assert_suggestion_result( + "They r, of course, arriving tomorrow.", + PronounAre::default(), + "They are, of course, arriving tomorrow.", + ); + } + + #[test] + fn fixes_we_r_lowercase() { + assert_suggestion_result( + "we r ready now.", + PronounAre::default(), + "we are ready now.", + ); + } + + #[test] + fn fixes_they_r_sentence_start() { + assert_suggestion_result( + "They r planning ahead.", + PronounAre::default(), + "They are planning ahead.", + ); + } + + #[test] + fn fixes_lowercase_sentence() { + assert_suggestion_result( + "they r late again.", + PronounAre::default(), + "they are late again.", + ); + } + + #[test] + fn handles_line_break() { + assert_suggestion_result( + "We r\nready to go.", + PronounAre::default(), + "We are\nready to go.", + ); + } + + #[test] + fn does_not_flag_contraction() { + assert_lint_count("You're looking great.", PronounAre::default(), 0); + } + + #[test] + fn does_not_flag_full_form() { + assert_lint_count("They are excited about it.", PronounAre::default(), 0); + } + + #[test] + fn ignores_similar_word() { + assert_lint_count("Your results impressed everyone.", PronounAre::default(), 0); + } +} diff --git a/harper-core/src/linting/safe_to_save.rs b/harper-core/src/linting/safe_to_save.rs new file mode 100644 index 00000000..f813c598 --- /dev/null +++ b/harper-core/src/linting/safe_to_save.rs @@ -0,0 +1,186 @@ +use harper_brill::UPOS; + +use crate::expr::Expr; +use crate::expr::OwnedExprExt; +use crate::expr::SequenceExpr; +use crate::patterns::{ModalVerb, UPOSSet, WordSet}; +use crate::{ + Token, + linting::{ExprLinter, Lint, LintKind, Suggestion}, +}; + +pub struct SafeToSave { + expr: Box, +} + +impl Default for SafeToSave { + fn default() -> Self { + let with_adv = SequenceExpr::default() + .then(ModalVerb::default()) + .then_whitespace() + .then(UPOSSet::new(&[UPOS::ADV])) + .then_whitespace() + .t_aco("safe") + .then_whitespace() + .then_unless(WordSet::new(&["to"])); + + let without_adv = SequenceExpr::default() + .then(ModalVerb::default()) + .then_whitespace() + .t_aco("safe") + .then_whitespace() + .then_unless(WordSet::new(&["to"])); + + let pattern = with_adv.or_longest(without_adv); + + Self { + expr: Box::new(pattern), + } + } +} + +impl ExprLinter for SafeToSave { + fn expr(&self) -> &dyn Expr { + self.expr.as_ref() + } + + fn match_to_lint(&self, toks: &[Token], src: &[char]) -> Option { + let safe_idx = toks + .iter() + .position(|t| t.span.get_content_string(src).to_lowercase() == "safe")?; + + let safe_tok = &toks[safe_idx]; + + Some(Lint { + span: safe_tok.span, + lint_kind: LintKind::WordChoice, + suggestions: vec![Suggestion::ReplaceWith("save".chars().collect())], + message: "The word `safe` is an adjective. Did you mean the verb `save`?".to_string(), + priority: 57, + }) + } + + fn description(&self) -> &str { + "Detects `safe` (adjective) when `save` (verb) is intended after modal verbs like `could` or `should`." + } +} + +#[cfg(test)] +mod tests { + use super::SafeToSave; + use crate::linting::tests::{assert_no_lints, assert_suggestion_result}; + + #[test] + fn corrects_could_safe() { + assert_suggestion_result( + "He could safe my life.", + SafeToSave::default(), + "He could save my life.", + ); + } + + #[test] + fn corrects_should_safe() { + assert_suggestion_result( + "You should safe your work frequently.", + SafeToSave::default(), + "You should save your work frequently.", + ); + } + + #[test] + fn corrects_will_safe() { + assert_suggestion_result( + "This will safe you time.", + SafeToSave::default(), + "This will save you time.", + ); + } + + #[test] + fn corrects_would_safe() { + assert_suggestion_result( + "It would safe us money.", + SafeToSave::default(), + "It would save us money.", + ); + } + + #[test] + fn corrects_can_safe() { + assert_suggestion_result( + "You can safe the document now.", + SafeToSave::default(), + "You can save the document now.", + ); + } + + #[test] + fn corrects_might_safe() { + assert_suggestion_result( + "This might safe the company.", + SafeToSave::default(), + "This might save the company.", + ); + } + + #[test] + fn corrects_must_safe() { + assert_suggestion_result( + "We must safe our resources.", + SafeToSave::default(), + "We must save our resources.", + ); + } + + #[test] + fn corrects_may_safe() { + assert_suggestion_result( + "You may safe your progress here.", + SafeToSave::default(), + "You may save your progress here.", + ); + } + + #[test] + fn corrects_with_adverb() { + assert_suggestion_result( + "You should definitely safe your changes.", + SafeToSave::default(), + "You should definitely save your changes.", + ); + } + + #[test] + fn corrects_shall_safe() { + assert_suggestion_result( + "We shall safe the nation.", + SafeToSave::default(), + "We shall save the nation.", + ); + } + + #[test] + fn corrects_couldnt_safe() { + assert_suggestion_result( + "I couldn't safe the file.", + SafeToSave::default(), + "I couldn't save the file.", + ); + } + + #[test] + fn allows_safe_to_verb() { + assert_no_lints("It is safe to assume.", SafeToSave::default()); + } + + #[test] + fn allows_safe_noun() { + assert_no_lints("Put the money in the safe today.", SafeToSave::default()); + } + + #[test] + fn allows_correct_save() { + assert_no_lints("You should save your work.", SafeToSave::default()); + } +} diff --git a/harper-core/src/linting/theres.rs b/harper-core/src/linting/theres.rs new file mode 100644 index 00000000..01a9ec19 --- /dev/null +++ b/harper-core/src/linting/theres.rs @@ -0,0 +1,134 @@ +use crate::{ + CharStringExt, Token, + expr::SequenceExpr, + linting::{ExprLinter, Lint, LintKind, Suggestion}, +}; + +pub struct Theres { + expr: Box, +} + +impl Default for Theres { + fn default() -> Self { + let expr = SequenceExpr::aco("their's") + .t_ws() + .then(|tok: &Token, src: &[char]| { + tok.kind.is_determiner() + || tok.kind.is_quantifier() + || tok.span.get_content(src).eq_ignore_ascii_case_str("no") + || tok.span.get_content(src).eq_ignore_ascii_case_str("enough") + }); + + Self { + expr: Box::new(expr), + } + } +} + +impl ExprLinter for Theres { + fn expr(&self) -> &dyn crate::expr::Expr { + self.expr.as_ref() + } + + fn match_to_lint(&self, tokens: &[Token], source: &[char]) -> Option { + let offender = tokens.first()?; + let span = offender.span; + let template = span.get_content(source); + + Some(Lint { + span, + lint_kind: LintKind::WordChoice, + suggestions: vec![Suggestion::replace_with_match_case_str("there's", template)], + message: "Use `there's`—the contraction of “there is”—for this construction.".into(), + priority: 31, + }) + } + + fn description(&self) -> &str { + "Replaces the mistaken possessive `their's` before a determiner with the contraction `there's`." + } +} + +#[cfg(test)] +mod tests { + use super::Theres; + use crate::linting::tests::{assert_lint_count, assert_suggestion_result}; + + #[test] + fn corrects_lowercase_before_the() { + assert_suggestion_result( + "We realized their's the clue we missed.", + Theres::default(), + "We realized there's the clue we missed.", + ); + } + + #[test] + fn corrects_sentence_start() { + assert_suggestion_result( + "Their's the solution on the table.", + Theres::default(), + "There's the solution on the table.", + ); + } + + #[test] + fn corrects_before_no() { + assert_suggestion_result( + "I promise their's no extra charge.", + Theres::default(), + "I promise there's no extra charge.", + ); + } + + #[test] + fn corrects_before_an() { + assert_suggestion_result( + "I suspect their's an error in the log.", + Theres::default(), + "I suspect there's an error in the log.", + ); + } + + #[test] + fn corrects_before_a() { + assert_suggestion_result( + "Maybe their's a better route available.", + Theres::default(), + "Maybe there's a better route available.", + ); + } + + #[test] + fn corrects_before_another() { + assert_suggestion_result( + "Their's another round after this.", + Theres::default(), + "There's another round after this.", + ); + } + + #[test] + fn corrects_before_enough() { + assert_suggestion_result( + "Their's enough context in the report.", + Theres::default(), + "There's enough context in the report.", + ); + } + + #[test] + fn allows_possessive_pronoun_form() { + assert_lint_count("Theirs is the final draft.", Theres::default(), 0); + } + + #[test] + fn ignores_without_determiner_afterward() { + assert_lint_count("I think their's better already.", Theres::default(), 0); + } + + #[test] + fn ignores_correct_contraction() { + assert_lint_count("There's a bright sign ahead.", Theres::default(), 0); + } +} diff --git a/harper-core/src/linting/to_two_too/mod.rs b/harper-core/src/linting/to_two_too/mod.rs index 56cb7374..f89399ce 100644 --- a/harper-core/src/linting/to_two_too/mod.rs +++ b/harper-core/src/linting/to_two_too/mod.rs @@ -95,6 +95,11 @@ mod tests { assert_no_lints("Talk to you later.", ToTwoToo::default()); } + #[test] + fn no_lint_distance_from_center() { + assert_no_lints("Distance from the center to any face", ToTwoToo::default()); + } + #[test] fn fixes_too_go() { assert_suggestion_result( diff --git a/harper-core/src/linting/to_two_too/to_too_adverb.rs b/harper-core/src/linting/to_two_too/to_too_adverb.rs index 124e7d25..989c42f2 100644 --- a/harper-core/src/linting/to_two_too/to_too_adverb.rs +++ b/harper-core/src/linting/to_two_too/to_too_adverb.rs @@ -16,7 +16,7 @@ impl Default for ToTooAdverb { let expr = SequenceExpr::default() .t_aco("to") .t_ws() - .then_kind_is_but_is_not_except(TokenKind::is_adverb, |_| false, &["as"]) + .then_kind_is_but_is_not_except(TokenKind::is_adverb, TokenKind::is_determiner, &["as"]) .then_optional(WhitespacePattern) .then_any_of(vec![ Box::new(SequenceExpr::default().then_kind_is_but_is_not_except( diff --git a/harper-core/src/spell/fst_dictionary.rs b/harper-core/src/spell/fst_dictionary.rs index e64d0f3e..d00ad25e 100644 --- a/harper-core/src/spell/fst_dictionary.rs +++ b/harper-core/src/spell/fst_dictionary.rs @@ -264,10 +264,7 @@ mod tests { dbg!(&misspelled_lower); assert!(!misspelled_word.is_empty()); - assert!( - dict.word_map.contains_key(misspelled_word) - || dict.word_map.contains_key(misspelled_lower) - ); + assert!(dict.word_map.contains_key(misspelled_word)); } } diff --git a/harper-core/src/spell/rune/attribute_list.rs b/harper-core/src/spell/rune/attribute_list.rs index 1197d17b..176025b9 100644 --- a/harper-core/src/spell/rune/attribute_list.rs +++ b/harper-core/src/spell/rune/attribute_list.rs @@ -14,7 +14,7 @@ use super::expansion::{ use super::word_list::AnnotatedWord; use crate::dict_word_metadata_orthography::OrthFlags; use crate::spell::WordId; -use crate::{CharString, CharStringExt, DictWordMetadata, Span}; +use crate::{CharString, DictWordMetadata, Span}; #[derive(Debug, Clone)] pub struct AttributeList { @@ -61,7 +61,7 @@ impl AttributeList { let mut base_metadata = DictWordMetadata::default(); // Store metadata that should only be applied if certain conditions are met - let orth_flags = check_orthography(&annotated_word); + let orth_flags = OrthFlags::from_letters(&annotated_word.letters); base_metadata.orth_info = orth_flags; let mut conditional_expansion_metadata = Vec::new(); @@ -272,340 +272,6 @@ impl AttributeList { } } -/// Gather metadata about the orthography of a word. -fn check_orthography(word: &AnnotatedWord) -> OrthFlags { - use crate::char_ext::CharExt; - use crate::dict_word_metadata_orthography::OrthFlags; - - let mut ortho_flags = OrthFlags::default(); - let mut all_lower = true; - let mut all_upper = true; - let mut first_is_upper = false; - let mut first_is_lower = false; - let mut saw_upper_after_first = false; - let mut saw_lower_after_first = false; - let mut is_first_char = true; - let mut upper_to_lower = false; - let mut lower_to_upper = false; - let letter_count = word - .letters - .iter() - .filter(|c| c.is_english_lingual()) - .count(); - - for &c in &word.letters { - // Multiword: contains at least one space - if c == ' ' { - ortho_flags |= OrthFlags::MULTIWORD; - continue; - } - // Hyphenated: contains at least one hyphen - if c == '-' { - ortho_flags |= OrthFlags::HYPHENATED; - continue; - } - // Apostrophe: contains at least one apostrophe (straight or curly) - if c == '\'' || c == '’' { - ortho_flags |= OrthFlags::APOSTROPHE; - continue; - } - // Only consider English letters for case flags - if !c.is_english_lingual() { - continue; - } - if c.is_lowercase() { - all_upper = false; - if is_first_char { - first_is_lower = true; - } else { - saw_lower_after_first = true; - if upper_to_lower { - lower_to_upper = true; - } - upper_to_lower = true; - } - } else if c.is_uppercase() { - all_lower = false; - if is_first_char { - first_is_upper = true; - } else { - saw_upper_after_first = true; - if lower_to_upper { - upper_to_lower = true; - } - lower_to_upper = true; - } - } else { - // Non-cased char (e.g., numbers, symbols) - ignore for case flags - // Reset case tracking after non-letter character - first_is_upper = false; - first_is_lower = false; - upper_to_lower = false; - lower_to_upper = false; - } - is_first_char = false; - } - - // Set case-related orthography flags - if letter_count > 0 { - if all_lower { - ortho_flags |= OrthFlags::LOWERCASE; - } - if all_upper { - ortho_flags |= OrthFlags::ALLCAPS; - } - // Only mark as TITLECASE if more than one letter - if letter_count > 1 && first_is_upper && !saw_upper_after_first { - ortho_flags |= OrthFlags::TITLECASE; - } - // LowerCamel: first is lowercase and there's at least one uppercase character after it - // Note: This must come after Titlecase check to avoid marking Titlecase words as LowerCamel - // Example: "pH" is LowerCamel, but "Providence" is Titlecase - if first_is_lower && saw_upper_after_first { - ortho_flags |= OrthFlags::LOWER_CAMEL; - } - // UpperCamel: first is uppercase and there are both lowercase and uppercase characters after it - // Note: This must come after Titlecase check to avoid marking Titlecase words as UpperCamel - // Example: "CamelCase" is UpperCamel, but "Providence" is Titlecase - if first_is_upper && saw_lower_after_first && saw_upper_after_first { - ortho_flags |= OrthFlags::UPPER_CAMEL; - } - } - - if looks_like_roman_numerals(&word.letters) - && is_really_roman_numerals(&word.letters.to_lower()) - { - ortho_flags |= OrthFlags::ROMAN_NUMERALS; - } - - ortho_flags -} - -fn looks_like_roman_numerals(word: &CharString) -> bool { - let mut is_roman = false; - let first_char_upper; - - if let Some((&first, rest)) = word.split_first() - && "mdclxvi".contains(first.to_ascii_lowercase()) - { - first_char_upper = first.is_uppercase(); - - for &c in rest { - if !"mdclxvi".contains(c.to_ascii_lowercase()) || c.is_uppercase() != first_char_upper { - return false; - } - } - is_roman = true; - } - is_roman -} - -fn is_really_roman_numerals(word: &[char]) -> bool { - let s: String = word.iter().collect(); - let mut chars = s.chars().peekable(); - - let mut m_count = 0; - while m_count < 4 && chars.peek() == Some(&'m') { - chars.next(); - m_count += 1; - } - - if !check_roman_group(&mut chars, 'c', 'd', 'm') { - return false; - } - - if !check_roman_group(&mut chars, 'x', 'l', 'c') { - return false; - } - - if !check_roman_group(&mut chars, 'i', 'v', 'x') { - return false; - } - - if chars.next().is_some() { - return false; - } - - true -} - -fn check_roman_group>( - chars: &mut std::iter::Peekable, - one: char, - five: char, - ten: char, -) -> bool { - match chars.peek() { - Some(&c) if c == one => { - chars.next(); - match chars.peek() { - Some(&next) if next == ten || next == five => { - chars.next(); - true - } - _ => { - let mut count = 0; - while count < 2 && chars.peek() == Some(&one) { - chars.next(); - count += 1; - } - true - } - } - } - Some(&c) if c == five => { - chars.next(); - let mut count = 0; - while count < 3 && chars.peek() == Some(&one) { - chars.next(); - count += 1; - } - true - } - _ => true, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::dict_word_metadata_orthography::OrthFlags; - - fn check_orthography_str(s: &str) -> OrthFlags { - let word = AnnotatedWord { - letters: s.chars().collect(), - annotations: Vec::new(), - }; - check_orthography(&word) - } - - #[test] - fn test_lowercase() { - let flags = check_orthography_str("hello"); - assert!(flags.contains(OrthFlags::LOWERCASE)); - assert!(!flags.contains(OrthFlags::TITLECASE)); - assert!(!flags.contains(OrthFlags::ALLCAPS)); - assert!(!flags.contains(OrthFlags::LOWER_CAMEL)); - assert!(!flags.contains(OrthFlags::UPPER_CAMEL)); - - // With non-letters - let flags = check_orthography_str("hello123"); - assert!(flags.contains(OrthFlags::LOWERCASE)); - } - - #[test] - fn test_titlecase() { - let flags = check_orthography_str("Hello"); - assert!(!flags.contains(OrthFlags::LOWERCASE)); - assert!(flags.contains(OrthFlags::TITLECASE)); - assert!(!flags.contains(OrthFlags::ALLCAPS)); - assert!(!flags.contains(OrthFlags::LOWER_CAMEL)); - assert!(!flags.contains(OrthFlags::UPPER_CAMEL)); - - // Examples that should be titlecase - assert!(check_orthography_str("World").contains(OrthFlags::TITLECASE)); - assert!(check_orthography_str("Something").contains(OrthFlags::TITLECASE)); - - // These examples should NOT be titlecase (they're UPPER_CAMEL) - assert!(!check_orthography_str("McDonald").contains(OrthFlags::TITLECASE)); - assert!(!check_orthography_str("O'Reilly").contains(OrthFlags::TITLECASE)); - - // Single character should not be titlecase - assert!(!check_orthography_str("A").contains(OrthFlags::TITLECASE)); - } - - #[test] - fn test_allcaps() { - let flags = check_orthography_str("HELLO"); - assert!(!flags.contains(OrthFlags::LOWERCASE)); - assert!(!flags.contains(OrthFlags::TITLECASE)); - assert!(flags.contains(OrthFlags::ALLCAPS)); - assert!(!flags.contains(OrthFlags::LOWER_CAMEL)); - assert!(!flags.contains(OrthFlags::UPPER_CAMEL)); - - // Examples from docs - assert!(check_orthography_str("NASA").contains(OrthFlags::ALLCAPS)); - assert!(check_orthography_str("I").contains(OrthFlags::ALLCAPS)); - } - - #[test] - fn test_lower_camel() { - let flags = check_orthography_str("helloWorld"); - assert!(!flags.contains(OrthFlags::LOWERCASE)); - assert!(!flags.contains(OrthFlags::TITLECASE)); - assert!(!flags.contains(OrthFlags::ALLCAPS)); - assert!(flags.contains(OrthFlags::LOWER_CAMEL)); - assert!(!flags.contains(OrthFlags::UPPER_CAMEL)); - - // Examples from docs - assert!(check_orthography_str("getHTTPResponse").contains(OrthFlags::LOWER_CAMEL)); - assert!(check_orthography_str("eBay").contains(OrthFlags::LOWER_CAMEL)); - - // All lowercase should not be lower camel - assert!(!check_orthography_str("hello").contains(OrthFlags::LOWER_CAMEL)); - - // Starts with uppercase should not be lower camel - assert!(!check_orthography_str("HelloWorld").contains(OrthFlags::LOWER_CAMEL)); - } - - #[test] - fn test_upper_camel() { - let flags = check_orthography_str("HelloWorld"); - assert!(!flags.contains(OrthFlags::LOWERCASE)); - assert!(!flags.contains(OrthFlags::TITLECASE)); - assert!(!flags.contains(OrthFlags::ALLCAPS)); - assert!(!flags.contains(OrthFlags::LOWER_CAMEL)); - assert!(flags.contains(OrthFlags::UPPER_CAMEL)); - - // Examples from docs - assert!(check_orthography_str("HttpRequest").contains(OrthFlags::UPPER_CAMEL)); - assert!(check_orthography_str("McDonald").contains(OrthFlags::UPPER_CAMEL)); - assert!(check_orthography_str("O'Reilly").contains(OrthFlags::UPPER_CAMEL)); - assert!(check_orthography_str("XMLHttpRequest").contains(OrthFlags::UPPER_CAMEL)); - - // Titlecase should not be upper camel - assert!(!check_orthography_str("Hello").contains(OrthFlags::UPPER_CAMEL)); - - // All caps should not be upper camel - assert!(!check_orthography_str("NASA").contains(OrthFlags::UPPER_CAMEL)); - - // Needs at least 3 chars - assert!(!check_orthography_str("Hi").contains(OrthFlags::UPPER_CAMEL)); - } - - #[test] - fn test_roman_numerals() { - assert!(check_orthography_str("MCMXCIV").contains(OrthFlags::ROMAN_NUMERALS)); - assert!(check_orthography_str("mdccclxxi").contains(OrthFlags::ROMAN_NUMERALS)); - assert!(check_orthography_str("MMXXI").contains(OrthFlags::ROMAN_NUMERALS)); - assert!(check_orthography_str("mcmxciv").contains(OrthFlags::ROMAN_NUMERALS)); - assert!(check_orthography_str("MCMXCIV").contains(OrthFlags::ROMAN_NUMERALS)); - assert!(check_orthography_str("MMI").contains(OrthFlags::ROMAN_NUMERALS)); - assert!(check_orthography_str("MMXXV").contains(OrthFlags::ROMAN_NUMERALS)); - } - - #[test] - fn test_single_roman_numeral() { - assert!(check_orthography_str("i").contains(OrthFlags::ROMAN_NUMERALS)); - } - - #[test] - fn empty_string_is_not_roman_numeral() { - assert!(!check_orthography_str("").contains(OrthFlags::ROMAN_NUMERALS)); - } - - #[test] - fn dont_allow_mixed_case_roman_numerals() { - assert!(!check_orthography_str("MCMlxxxVIII").contains(OrthFlags::ROMAN_NUMERALS)); - } - - #[test] - fn dont_allow_looks_like_but_isnt_roman_numeral() { - assert!(!check_orthography_str("mdxlivx").contains(OrthFlags::ROMAN_NUMERALS)); - assert!(!check_orthography_str("XIXIVV").contains(OrthFlags::ROMAN_NUMERALS)); - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HumanReadableAttributeList { affixes: HashMap, diff --git a/harper-core/tests/run_tests.rs b/harper-core/tests/run_tests.rs index bb67e7a3..1857ddfe 100644 --- a/harper-core/tests/run_tests.rs +++ b/harper-core/tests/run_tests.rs @@ -72,15 +72,15 @@ create_test!(preexisting.md, 0, Dialect::American); create_test!(issue_109.md, 0, Dialect::American); create_test!(issue_109_ext.md, 0, Dialect::American); create_test!(chinese_lorem_ipsum.md, 2, Dialect::American); -create_test!(obsidian_links.md, 2, Dialect::American); +create_test!(obsidian_links.md, 3, Dialect::American); create_test!(issue_267.md, 0, Dialect::American); -create_test!(proper_noun_capitalization.md, 2, Dialect::American); +create_test!(proper_noun_capitalization.md, 3, Dialect::American); create_test!(amazon_hostname.md, 0, Dialect::American); create_test!(issue_159.md, 1, Dialect::American); create_test!(issue_358.md, 0, Dialect::American); create_test!(issue_195.md, 0, Dialect::American); create_test!(issue_118.md, 0, Dialect::American); -create_test!(lots_of_latin.md, 0, Dialect::American); +create_test!(lots_of_latin.md, 1, Dialect::American); create_test!(pr_504.md, 1, Dialect::American); create_test!(pr_452.md, 2, Dialect::American); create_test!(hex_basic_clean.md, 0, Dialect::American); @@ -93,4 +93,4 @@ create_test!(issue_1581.md, 0, Dialect::British); create_test!(lukas_homework.md, 3, Dialect::American); // Org mode tests -create_org_test!(index.org, 38, Dialect::American); +create_org_test!(index.org, 40, Dialect::American); diff --git a/harper-core/tests/text/linters/Alice's Adventures in Wonderland.snap.yml b/harper-core/tests/text/linters/Alice's Adventures in Wonderland.snap.yml index 9ab0d22b..cf686388 100644 --- a/harper-core/tests/text/linters/Alice's Adventures in Wonderland.snap.yml +++ b/harper-core/tests/text/linters/Alice's Adventures in Wonderland.snap.yml @@ -674,6 +674,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 320 | she began again: “Où est ma chatte?” which was the first sentence in her French + | ^~~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “EST” + + + Lint: Spelling (63 priority) Message: | 320 | she began again: “Où est ma chatte?” which was the first sentence in her French @@ -1214,6 +1223,16 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 692 | below!” (a loud crash)—“Now, who did that?—It was Bill, I fancy—Who’s to go down + | ^~~~~ The canonical dictionary spelling is `who's`. + 693 | the chimney?—Nay, I shan’t! You do it!—That I won’t, then!—Bill’s to go +Suggest: + - Replace with: “who's” + + + Lint: Capitalization (31 priority) Message: | 694 | down—Here, Bill! the master says you’re to go down the chimney!” @@ -1484,6 +1503,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 964 | “Come, my head’s free at last!” said Alice in a tone of delight, which changed + | ^~~~~~ The canonical dictionary spelling is `head's`. +Suggest: + - Replace with: “head's” + + + Lint: Readability (127 priority) Message: | 964 | “Come, my head’s free at last!” said Alice in a tone of delight, which changed @@ -1640,6 +1668,16 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 1127 | “Oh, there’s no use in talking to him,” said Alice desperately: “he’s perfectly + | ^~~~ The canonical dictionary spelling is `he's`. + 1128 | idiotic!” And she opened the door and went in. +Suggest: + - Replace with: “he's” + + + Lint: Readability (127 priority) Message: | 1130 | The door led right into a large kitchen, which was full of smoke from one end to @@ -1945,6 +1983,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 1582 | “Who’s making personal remarks now?” the Hatter asked triumphantly. + | ^~~~~ The canonical dictionary spelling is `who's`. +Suggest: + - Replace with: “who's” + + + Lint: Readability (127 priority) Message: | 1637 | The Dormouse had closed its eyes by this time, and was going off into a doze; @@ -2869,6 +2916,16 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 2306 | > “Will you walk a little faster?” said a whiting to a snail. “There’s a + 2307 | > porpoise close behind us, and he’s treading on my tail. See how eagerly the + | ^~~~ The canonical dictionary spelling is `he's`. +Suggest: + - Replace with: “he's” + + + Lint: Spelling (63 priority) Message: | 2333 | “Yes,” said Alice, “I’ve often seen them at dinn—” she checked herself hastily. @@ -3136,14 +3193,23 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2477 | eagerly that the Gryphon said, in a rather offended tone, “Hm! No accounting for + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “HM” + + + Lint: Spelling (63 priority) Message: | 2477 | eagerly that the Gryphon said, in a rather offended tone, “Hm! No accounting for | ^~ Did you mean to spell `Hm` this way? Suggest: + - Replace with: “H” - Replace with: “H'm” - - Replace with: “Ha” - - Replace with: “Ham” + - Replace with: “Hem” @@ -3176,6 +3242,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop + | ^~~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “OOP” + + + Lint: Spelling (63 priority) Message: | 2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop @@ -3209,6 +3284,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop + | ^~~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “OOP” + + + Lint: Spelling (63 priority) Message: | 2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop @@ -3232,6 +3316,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop + | ^~~ This word's canonical spelling is all-caps. + 2486 | > of the e—e—evening, Beautiful, beautiful Soup! +Suggest: + - Replace with: “OOP” + + + Lint: Spelling (63 priority) Message: | 2485 | > evening, beautiful Soup! Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop @@ -3286,6 +3380,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the + | ^~~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “OOP” + + + Lint: Spelling (63 priority) Message: | 2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the @@ -3319,6 +3422,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the + | ^~~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “OOP” + + + Lint: Spelling (63 priority) Message: | 2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the @@ -3342,6 +3454,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the + | ^~~ This word's canonical spelling is all-caps. + 2491 | > e—e—evening, Beautiful, beauti—FUL SOUP!” +Suggest: + - Replace with: “OOP” + + + Lint: Spelling (63 priority) Message: | 2490 | > beautiful Soup? Beau—ootiful Soo—oop! Beau—ootiful Soo—oop! Soo—oop of the @@ -3427,6 +3549,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2503 | > “Soo—oop of the e—e—evening, Beautiful, beautiful Soup!” + | ^~~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “OOP” + + + Lint: Spelling (63 priority) Message: | 2503 | > “Soo—oop of the e—e—evening, Beautiful, beautiful Soup!” diff --git a/harper-core/tests/text/linters/Computer science.snap.yml b/harper-core/tests/text/linters/Computer science.snap.yml index be29f3c8..b0a2b1e1 100644 --- a/harper-core/tests/text/linters/Computer science.snap.yml +++ b/harper-core/tests/text/linters/Computer science.snap.yml @@ -40,6 +40,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 49 | including the fact that he documented the binary number system. In 1820, Thomas + 50 | de Colmar launched the mechanical calculator industry[note 1] when he invented + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “DE” + + + Lint: Spelling (63 priority) Message: | 49 | including the fact that he documented the binary number system. In 1820, Thomas @@ -626,7 +636,7 @@ Message: | Suggest: - Replace with: “Ax” - Replace with: “A” - - Replace with: “Ah” + - Replace with: “Ab” @@ -755,7 +765,7 @@ Message: | 216 | that they are theory, abstraction (modeling), and design. Amnon H. Eden | ^~ Did you mean to spell `H.` this way? Suggest: - - Replace with: “Hr” + - Replace with: “Ht” - Replace with: “He” - Replace with: “Hf” @@ -978,9 +988,9 @@ Message: | 393 | term "architecture" in computer literature can be traced to the work of Lyle R. | ^~ Did you mean to spell `R.` this way? Suggest: - - Replace with: “Rd” - Replace with: “R” - - Replace with: “RC” + - Replace with: “Rd” + - Replace with: “Re” @@ -1048,6 +1058,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 444 | > easily distinguishable states, such as "on/off", "magnetized/de-magnetized", + | ^~ This word's canonical spelling is all-caps. + 445 | > "high-voltage/low-voltage", etc.). +Suggest: + - Replace with: “DE” + + + Lint: Spelling (63 priority) Message: | 444 | > easily distinguishable states, such as "on/off", "magnetized/de-magnetized", diff --git a/harper-core/tests/text/linters/Difficult sentences.snap.yml b/harper-core/tests/text/linters/Difficult sentences.snap.yml index 280456d7..cfba7975 100644 --- a/harper-core/tests/text/linters/Difficult sentences.snap.yml +++ b/harper-core/tests/text/linters/Difficult sentences.snap.yml @@ -56,6 +56,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 126 | Who's for ice-cream? + | ^~~~~ The canonical dictionary spelling is `who's`. +Suggest: + - Replace with: “who's” + + + Lint: Capitalization (31 priority) Message: | 160 | to account for one's whereabouts. @@ -65,6 +74,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 166 | You can't get all your news from the Internet. + | ^~~~ The canonical dictionary spelling is `news`. +Suggest: + - Replace with: “news” + + + Lint: Spelling (63 priority) Message: | 180 | I’ve been doing this from pickney. @@ -323,9 +341,9 @@ Message: | 443 | With their reputation on the line, they decided to fire their PR team. | ^~ Did you mean to spell `PR` this way? Suggest: - - Replace with: “Pt” + - Replace with: “Pry” - Replace with: “P” - - Replace with: “Par” + - Replace with: “Pa” diff --git a/harper-core/tests/text/linters/Part-of-speech tagging.snap.yml b/harper-core/tests/text/linters/Part-of-speech tagging.snap.yml index f86547e6..169d4bb3 100644 --- a/harper-core/tests/text/linters/Part-of-speech tagging.snap.yml +++ b/harper-core/tests/text/linters/Part-of-speech tagging.snap.yml @@ -11,6 +11,16 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 8 | In corpus linguistics, part-of-speech tagging (POS tagging or PoS tagging or + | ^~~ This word's canonical spelling is all-caps. + 9 | POST), also called grammatical tagging is the process of marking up a word in a +Suggest: + - Replace with: “POS” + + + Lint: Spelling (63 priority) Message: | 8 | In corpus linguistics, part-of-speech tagging (POS tagging or PoS tagging or @@ -68,7 +78,7 @@ Message: | Suggest: - Replace with: “Nun” - Replace with: “Non” - - Replace with: “NT” + - Replace with: “N1” diff --git a/harper-core/tests/text/linters/Spell.snap.yml b/harper-core/tests/text/linters/Spell.snap.yml index 5f6d3a26..70053227 100644 --- a/harper-core/tests/text/linters/Spell.snap.yml +++ b/harper-core/tests/text/linters/Spell.snap.yml @@ -8,6 +8,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 7 | My favourite color is blu. + | ^~~ The canonical dictionary spelling is `Blu`. +Suggest: + - Replace with: “Blu” + + + Lint: Spelling (63 priority) Message: | 7 | My favourite color is blu. diff --git a/harper-core/tests/text/linters/The Constitution of the United States.snap.yml b/harper-core/tests/text/linters/The Constitution of the United States.snap.yml index 3b8aca5c..967692e2 100644 --- a/harper-core/tests/text/linters/The Constitution of the United States.snap.yml +++ b/harper-core/tests/text/linters/The Constitution of the United States.snap.yml @@ -882,6 +882,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 338 | #### SubSection. 1. + | ^~~~~~~~~~ The canonical dictionary spelling is `subsection`. +Suggest: + - Replace with: “subsection” + + + Lint: Readability (127 priority) Message: | 340 | The Electors shall meet in their respective states, and vote @@ -994,6 +1003,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 388 | #### SubSection. 2 + | ^~~~~~~~~~ The canonical dictionary spelling is `subsection`. +Suggest: + - Replace with: “subsection” + + + Lint: Readability (127 priority) Message: | 390 | No Person except a natural born Citizen, or a Citizen of the @@ -1039,6 +1057,15 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 406 | #### SubSection 3. + | ^~~~~~~~~~ The canonical dictionary spelling is `subsection`. +Suggest: + - Replace with: “subsection” + + + Lint: Readability (127 priority) Message: | 415 | Whenever the President transmits to the President pro tempore of the Senate and @@ -1199,6 +1226,15 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 446 | #### SubSection 4. + | ^~~~~~~~~~ The canonical dictionary spelling is `subsection`. +Suggest: + - Replace with: “subsection” + + + Lint: Readability (127 priority) Message: | 448 | The President shall, at stated Times, receive for his @@ -1261,6 +1297,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 460 | #### SubSection 5. + | ^~~~~~~~~~ The canonical dictionary spelling is `subsection`. +Suggest: + - Replace with: “subsection” + + + Lint: Readability (127 priority) Message: | 465 | A number of electors of President and Vice President equal to the whole number diff --git a/harper-core/tests/text/linters/The Great Gatsby.snap.yml b/harper-core/tests/text/linters/The Great Gatsby.snap.yml index c2eb0e3a..69785b4a 100644 --- a/harper-core/tests/text/linters/The Great Gatsby.snap.yml +++ b/harper-core/tests/text/linters/The Great Gatsby.snap.yml @@ -5,7 +5,7 @@ Message: | Suggest: - Replace with: “Ft” - Replace with: “F” - - Replace with: “Ff” + - Replace with: “Fa” @@ -174,6 +174,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 57 | reference to the rather hard-boiled painting that hangs in father’s office. I + | ^~~~~~~~ The canonical dictionary spelling is `father's`. +Suggest: + - Replace with: “father's” + + + Lint: Spelling (63 priority) Message: | 61 | restless. Instead of being the warm centre of the world, the Middle West now @@ -270,6 +279,16 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 98 | of very solemn and obvious editorials for the Yale News—and now I was going to + | ^~~~ The canonical dictionary spelling is `news`. + 99 | bring back all such things into my life and become again that most limited of +Suggest: + - Replace with: “news” + + + Lint: Readability (127 priority) Message: | 106 | natural curiosities, two unusual formations of land. Twenty miles from the city @@ -308,6 +327,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 119 | thousand a season. The one on my right was a colossal affair by any standard—it + 120 | was a factual imitation of some Hôtel de Ville in Normandy, with a tower on one + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “DE” + + + Lint: Spelling (63 priority) Message: | 119 | thousand a season. The one on my right was a colossal affair by any standard—it @@ -531,6 +560,25 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 402 | “I’ll tell you a family secret,” she whispered enthusiastically. “It’s about the + 403 | butler’s nose. Do you want to hear about the butler’s nose?” + | ^~~~~~~~ The canonical dictionary spelling is `butler's`. +Suggest: + - Replace with: “butler's” + + + +Lint: Capitalization (127 priority) +Message: | + 403 | butler’s nose. Do you want to hear about the butler’s nose?” + | ^~~~~~~~ The canonical dictionary spelling is `butler's`. +Suggest: + - Replace with: “butler's” + + + Lint: Readability (127 priority) Message: | 416 | For a moment the last sunshine fell with romantic affection upon her glowing @@ -881,8 +929,8 @@ Message: | | ^~ Did you mean to spell `B.` this way? Suggest: - Replace with: “Bu” + - Replace with: “B” - Replace with: “Be” - - Replace with: “Bf” @@ -916,6 +964,16 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 728 | stout, but she carried her flesh sensuously as some women can. Her face, above a + 729 | spotted dress of dark blue crêpe-de-chine, contained no facet or gleam of + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “DE” + + + Lint: Spelling (63 priority) Message: | 728 | stout, but she carried her flesh sensuously as some women can. Her face, above a @@ -940,6 +998,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 747 | “I’ll meet you by the news-stand on the lower level.” + | ^~~~ The canonical dictionary spelling is `news`. +Suggest: + - Replace with: “news” + + + Lint: Spelling (63 priority) Message: | 756 | “Terrible place, isn’t it,” said Tom, exchanging a frown with Doctor Eckleburg. @@ -951,6 +1018,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 764 | “Wilson? He thinks she goes to see her sister in New York. He’s so dumb he + 765 | doesn’t know he’s alive.” + | ^~~~ The canonical dictionary spelling is `he's`. +Suggest: + - Replace with: “he's” + + + Lint: Spelling (63 priority) Message: | 768 | together, for Mrs. Wilson sat discreetly in another car. Tom deferred that much @@ -963,14 +1040,24 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 772 | her rather wide hips as Tom helped her to the platform in New York. At the + 773 | news-stand she bought a copy of Town Tattle and a moving-picture magazine, and + | ^~~~ The canonical dictionary spelling is `news`. +Suggest: + - Replace with: “news” + + + Lint: Spelling (63 priority) Message: | 784 | We backed up to a gray old man who bore an absurd resemblance to John D. | ^~ Did you mean to spell `D.` this way? Suggest: - - Replace with: “Do” - Replace with: “Db” - Replace with: “Dc” + - Replace with: “Do” @@ -1143,6 +1230,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 964 | “Well, they say he’s a nephew or a cousin of Kaiser Wilhelm’s. That’s where all + | ^~~~ The canonical dictionary spelling is `he's`. +Suggest: + - Replace with: “he's” + + + Lint: Spelling (63 priority) Message: | 989 | studies of him.” His lips moved silently for a moment as he invented. “‘George @@ -1150,8 +1246,8 @@ Message: | | ^~ Did you mean to spell `B.` this way? Suggest: - Replace with: “Bu” + - Replace with: “B” - Replace with: “Be” - - Replace with: “Bf” @@ -1179,6 +1275,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 1039 | kept saying to me: ‘Lucille, that man’s ’way below you!’ But if I hadn’t met + | ^~~~~ The canonical dictionary spelling is `man's`. +Suggest: + - Replace with: “man's” + + + Lint: Miscellaneous (31 priority) Message: | 1062 | She pointed suddenly at me, and every one looked at me accusingly. I tried to @@ -1353,6 +1458,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 1174 | juice of two hundred oranges in half an hour if a little button was pressed two + 1175 | hundred times by a butler’s thumb. + | ^~~~~~~~ The canonical dictionary spelling is `butler's`. +Suggest: + - Replace with: “butler's” + + + Lint: Spelling (63 priority) Message: | 1179 | enormous garden. On buffet tables, garnished with glistening hors-d’œuvre, @@ -1468,6 +1583,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 1210 | rhythm obligingly for her, and there is a burst of chatter as the erroneous news + | ^~~~ The canonical dictionary spelling is `news`. + 1211 | goes around that she is Gilda Gray’s understudy from the Follies. The party has +Suggest: + - Replace with: “news” + + + Lint: Readability (127 priority) Message: | 1223 | I had been actually invited. A chauffeur in a uniform of robin’s-egg blue @@ -1481,6 +1606,16 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 1223 | I had been actually invited. A chauffeur in a uniform of robin’s-egg blue + | ^~~~~~~ The canonical dictionary spelling is `robin's`. + 1224 | crossed my lawn early that Saturday morning with a surprisingly formal note from +Suggest: + - Replace with: “robin's” + + + Lint: Readability (127 priority) Message: | 1230 | Dressed up in white flannels I went over to his lawn a little after seven, and @@ -1835,6 +1970,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 1569 | “She had a fight with a man who says he’s her husband,” explained a girl at my + | ^~~~ The canonical dictionary spelling is `he's`. + 1570 | elbow. +Suggest: + - Replace with: “he's” + + + Lint: Readability (127 priority) Message: | 1574 | rent asunder by dissension. One of the men was talking with curious intensity to @@ -2073,6 +2218,16 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 1797 | passed so close to some workmen that our fender flicked a button on one man’s + | ^~~~~ The canonical dictionary spelling is `man's`. + 1798 | coat. +Suggest: + - Replace with: “man's” + + + Lint: Punctuation (31 priority) Message: | 1824 | perspiration appeared on her upper lip. Nevertheless there was a vague @@ -2231,9 +2386,9 @@ Message: | 1858 | farther out on the Island came the Cheadles and the O. R. P. Schraeders, and the | ^~ Did you mean to spell `R.` this way? Suggest: - - Replace with: “Rd” - Replace with: “R” - - Replace with: “RC” + - Replace with: “Rd” + - Replace with: “Re” @@ -2320,8 +2475,8 @@ Message: | | ^~ Did you mean to spell `B.` this way? Suggest: - Replace with: “Bu” + - Replace with: “B” - Replace with: “Be” - - Replace with: “Bf” @@ -2333,7 +2488,7 @@ Message: | Suggest: - Replace with: “Ax” - Replace with: “A” - - Replace with: “Ah” + - Replace with: “Ab” @@ -2456,8 +2611,8 @@ Message: | | ^~ Did you mean to spell `G.` this way? Suggest: - Replace with: “Gt” - - Replace with: “G” - - Replace with: “Gr” + - Replace with: “Gm” + - Replace with: “Go” @@ -2485,6 +2640,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 1871 | afterward strangled his wife. Da Fontano the promoter came there, and Ed Legros + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “DA” + + + Lint: Spelling (63 priority) Message: | 1871 | afterward strangled his wife. Da Fontano the promoter came there, and Ed Legros @@ -2492,7 +2656,7 @@ Message: | Suggest: - Replace with: “Dad” - Replace with: “Dam” - - Replace with: “Dc” + - Replace with: “Day” @@ -2537,8 +2701,18 @@ Message: | | ^~ Did you mean to spell `B.` this way? Suggest: - Replace with: “Bu” + - Replace with: “B” - Replace with: “Be” - - Replace with: “Bf” + + + +Lint: Capitalization (127 priority) +Message: | + 1872 | and James B. (“Rot-Gut”) Ferret and the De Jongs and Ernest Lilly—they came to + | ^~ This word's canonical spelling is all-caps. + 1873 | gamble, and when Ferret wandered into the garden it meant he was cleaned out and +Suggest: + - Replace with: “DE” @@ -2550,7 +2724,7 @@ Message: | Suggest: - Replace with: “Db” - Replace with: “Dc” - - Replace with: “Def” + - Replace with: “Dd” @@ -2946,6 +3120,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 1981 | “character” leaking sawdust at every pore as he pursued a tiger through the Bois + 1982 | de Boulogne. + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “DE” + + + Lint: Spelling (63 priority) Message: | 1981 | “character” leaking sawdust at every pore as he pursued a tiger through the Bois @@ -2990,6 +3174,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2006 | To my astonishment, the thing had an authentic look. “Orderi di Danilo,” ran the + | ^~ This word's canonical spelling is all-caps. + 2007 | circular legend, “Montenegro, Nicolas Rex.” +Suggest: + - Replace with: “DI” + + + Lint: Spelling (63 priority) Message: | 2006 | To my astonishment, the thing had an authentic look. “Orderi di Danilo,” ran the @@ -3059,6 +3253,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2056 | “All right, old sport,” called Gatsby. We slowed down. Taking a white card from + 2057 | his wallet, he waved it before the man’s eyes. + | ^~~~~ The canonical dictionary spelling is `man's`. +Suggest: + - Replace with: “man's” + + + Lint: Readability (127 priority) Message: | 2067 | Over the great bridge, with the sunlight through the girders making a constant @@ -3092,6 +3296,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2078 | driven by a white chauffeur, in which sat three modish negroes, two bucks and a + | ^~~~~~~ The canonical dictionary spelling is `Negroes`. + 2079 | girl. I laughed aloud as the yolks of their eyeballs rolled toward us in haughty +Suggest: + - Replace with: “Negroes” + + + Lint: Spelling (63 priority) Message: | 2078 | driven by a white chauffeur, in which sat three modish negroes, two bucks and a @@ -3140,6 +3354,15 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 2105 | “I handed the money to Katspaugh and I sid: ‘All right, Katspaugh, don’t pay him + | ^~~ The canonical dictionary spelling is `Sid`. +Suggest: + - Replace with: “Sid” + + + Lint: Spelling (63 priority) Message: | 2105 | “I handed the money to Katspaugh and I sid: ‘All right, Katspaugh, don’t pay him @@ -3376,6 +3599,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2215 | about women. He would never so much as look at a friend’s wife.” + | ^~~~~~~~ The canonical dictionary spelling is `friend's`. +Suggest: + - Replace with: “friend's” + + + Lint: Spelling (63 priority) Message: | 2218 | Mr. Wolfshiem drank his coffee with a jerk and got to his feet. @@ -3416,6 +3648,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2243 | “Meyer Wolfshiem? No, he’s a gambler.” Gatsby hesitated, then added coolly: + | ^~~~ The canonical dictionary spelling is `he's`. +Suggest: + - Replace with: “he's” + + + Lint: Miscellaneous (31 priority) Message: | 2265 | “Come along with me for a minute,” I said; “I’ve got to say hello to some one.” @@ -3632,6 +3873,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2395 | When Jordan Baker had finished telling all this we had left the Plaza for half + 2396 | an hour and were driving in a victoria through Central Park. The sun had gone + | ^~~~~~~~ The canonical dictionary spelling is `Victoria`. +Suggest: + - Replace with: “Victoria” + + + Lint: Spelling (63 priority) Message: | 2395 | When Jordan Baker had finished telling all this we had left the Plaza for half @@ -3655,6 +3906,25 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2425 | “He’s afraid, he’s waited so long. He thought you might be offended. You see, + | ^~~~ The canonical dictionary spelling is `he's`. +Suggest: + - Replace with: “he's” + + + +Lint: Capitalization (127 priority) +Message: | + 2425 | “He’s afraid, he’s waited so long. He thought you might be offended. You see, + 2426 | he’s regular tough underneath it all.” + | ^~~~ The canonical dictionary spelling is `he's`. +Suggest: + - Replace with: “he's” + + + Lint: Formatting (255 priority) Message: | 2437 | “I think he half expected her to wander into one of his parties, some night,” @@ -3670,6 +3940,16 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 2448 | whole idea. He doesn’t know very much about Tom, though he says he’s read a + | ^~~~ The canonical dictionary spelling is `he's`. + 2449 | Chicago paper for years just on the chance of catching a glimpse of Daisy’s +Suggest: + - Replace with: “he's” + + + Lint: Spelling (63 priority) Message: | 2455 | hard, limited person, who dealt in universal scepticism, and who leaned back @@ -3905,6 +4185,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 2776 | ecstatic patron of recurrent light, and repeated the news to Daisy. “What do you + | ^~~~ The canonical dictionary spelling is `news`. +Suggest: + - Replace with: “news” + + + Lint: Formatting (255 priority) Message: | 2806 | was in he answered: ‘‘That’s my affair,” before he realized that it wasn’t an @@ -4078,6 +4367,25 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 2905 | “Who’s this?” + | ^~~~~ The canonical dictionary spelling is `who's`. +Suggest: + - Replace with: “who's” + + + +Lint: Capitalization (127 priority) +Message: | + 2926 | said a small town. . . . He must know what a small town is. . . . Well, he’s no + | ^~~~ The canonical dictionary spelling is `he's`. + 2927 | use to us if Detroit is his idea of a small town. . . .” He rang off. +Suggest: + - Replace with: “he's” + + + Lint: Spelling (63 priority) Message: | 2940 | “I know what we'll do,” said Gatsby, “we'll have Klipspringer play the piano.” @@ -4159,6 +4467,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 3019 | become authorities upon his past, had increased all summer until he fell just + 3020 | short of being news. Contemporary legends such as the “underground pipe-line to + | ^~~~ The canonical dictionary spelling is `news`. +Suggest: + - Replace with: “news” + + + Lint: Readability (127 priority) Message: | 3020 | short of being news. Contemporary legends such as the “underground pipe-line to @@ -4251,6 +4569,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 3074 | Ella Kaye, the newspaper woman, played Madame de Maintenon to his weakness and + | ^~ This word's canonical spelling is all-caps. + 3075 | sent him to sea in a yacht, were common property of the turgid journalism +Suggest: + - Replace with: “DE” + + + Lint: Spelling (63 priority) Message: | 3074 | Ella Kaye, the newspaper woman, played Madame de Maintenon to his weakness and @@ -4430,6 +4758,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 3213 | “My God, I believe the man’s coming,” said Tom. “Doesn’t he know she doesn’t + | ^~~~~ The canonical dictionary spelling is `man's`. +Suggest: + - Replace with: “man's” + + + Lint: Readability (127 priority) Message: | 3235 | in my memory from Gatsby’s other parties that summer. There were the same @@ -4576,6 +4913,26 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 3396 | hadn’t been invited. They simply force their way in and he’s too polite to + | ^~~~ The canonical dictionary spelling is `he's`. + 3397 | object.” +Suggest: + - Replace with: “he's” + + + +Lint: Capitalization (127 priority) +Message: | + 3409 | Her glance left me and sought the lighted top of the steps, where “Three o’Clock + | ^~~~~~~ The canonical dictionary spelling is `o'clock`. + 3410 | in the Morning,” a neat, sad little waltz of that year, was drifting out the +Suggest: + - Replace with: “o'clock” + + + Lint: Readability (127 priority) Message: | 3414 | dim, incalculable hours? Perhaps some unbelievable guest would arrive, a person @@ -4634,6 +4991,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 3488 | somewhere a long time ago. For a moment a phrase tried to take shape in my mouth + 3489 | and my lips parted like a dumb man’s, as though there was more struggling upon + | ^~~~~ The canonical dictionary spelling is `man's`. +Suggest: + - Replace with: “man's” + + + Lint: Spelling (63 priority) Message: | 3490 | them than a wisp of startled air. But they made no sound, and what I had almost @@ -4828,6 +5195,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 3612 | “No, he’s not,” I assured her. “It’s a bona-fide deal. I happen to know about + | ^~~~ The canonical dictionary spelling is `he's`. +Suggest: + - Replace with: “he's” + + + Lint: Spelling (63 priority) Message: | 3612 | “No, he’s not,” I assured her. “It’s a bona-fide deal. I happen to know about @@ -4894,6 +5270,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 3646 | “The bles-sed pre-cious! Did mother get powder on your old yellowy hair? Stand + 3647 | up now, and say—How-de-do.” + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “DE” + + + Lint: Spelling (63 priority) Message: | 3646 | “The bles-sed pre-cious! Did mother get powder on your old yellowy hair? Stand @@ -4929,6 +5315,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 3686 | “I read somewhere that the sun’s getting hotter every year,” said Tom genially. + | ^~~~~ The canonical dictionary spelling is `sun's`. +Suggest: + - Replace with: “sun's” + + + Lint: Formatting (255 priority) Message: | 3687 | “It seems that pretty soon the earth’s going to fall into the sun—or wait a @@ -4936,6 +5331,16 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 3687 | “It seems that pretty soon the earth’s going to fall into the sun—or wait a + 3688 | minute—it’s just the opposite—the sun’s getting colder every year. + | ^~~~~ The canonical dictionary spelling is `sun's`. +Suggest: + - Replace with: “sun's” + + + Lint: Regionalism (63 priority) Message: | 3690 | “Come outside,” he suggested to Gatsby, “I’d like you to have a look at the @@ -4976,6 +5381,15 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 3795 | song of it. . . . High in a white palace the king’s daughter, the golden girl. . + | ^~~~~~ The canonical dictionary spelling is `king's`. +Suggest: + - Replace with: “king's” + + + Lint: Spelling (63 priority) Message: | 3809 | “Well, you take my coupé and let me drive your car to town.” @@ -5017,6 +5431,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 3864 | “Nevertheless he’s an Oxford man.” + | ^~~~ The canonical dictionary spelling is `he's`. +Suggest: + - Replace with: “he's” + + + Lint: Spelling (63 priority) Message: | 3874 | while in silence. Then as Doctor T. J. Eckleburg’s faded eyes came into sight @@ -5552,6 +5975,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 4373 | “No . . . I just remembered that to-day’s my birthday.” + | ^~~~~ The canonical dictionary spelling is `day's`. +Suggest: + - Replace with: “day's” + + + Lint: Spelling (63 priority) Message: | 4377 | It was seven o’clock when we got into the coupé with him and started for Long @@ -5743,6 +6175,24 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “GA” + + + +Lint: Capitalization (127 priority) +Message: | + 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “OD” + + + Lint: Spelling (63 priority) Message: | 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” @@ -5754,6 +6204,24 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “GA” + + + +Lint: Capitalization (127 priority) +Message: | + 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “OD” + + + Lint: Spelling (63 priority) Message: | 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” @@ -5765,6 +6233,24 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “GA” + + + +Lint: Capitalization (127 priority) +Message: | + 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “OD” + + + Lint: Spelling (63 priority) Message: | 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” @@ -5776,6 +6262,24 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “GA” + + + +Lint: Capitalization (127 priority) +Message: | + 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “OD” + + + Lint: Spelling (63 priority) Message: | 4479 | “Oh, my Ga-od! Oh, my Ga-od! Oh, Ga-od! Oh, my Ga-od!” @@ -5787,6 +6291,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 4484 | “M-a-v—” the policeman was saying, “—o———” + | ^ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “O” + + + Lint: Spelling (63 priority) Message: | 4484 | “M-a-v—” the policeman was saying, “—o———” @@ -5798,6 +6311,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 4486 | “No, r—” corrected the man, “M-a-v-r-o———” + | ^ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “O” + + + Lint: Spelling (63 priority) Message: | 4486 | “No, r—” corrected the man, “M-a-v-r-o———” @@ -5809,6 +6331,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 4490 | “r—” said the policeman, “o———” + | ^ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “O” + + + Lint: Spelling (63 priority) Message: | 4490 | “r—” said the policeman, “o———” @@ -5829,6 +6360,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 4503 | “She ran out ina road. Son-of-a-bitch didn’t even stopus car.” + | ^~~ The canonical dictionary spelling is `Ina`. +Suggest: + - Replace with: “Ina” + + + Lint: Spelling (63 priority) Message: | 4503 | “She ran out ina road. Son-of-a-bitch didn’t even stopus car.” @@ -6083,6 +6623,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 4622 | house. I sat down for a few minutes with my head in my hands, until I heard the + 4623 | phone taken up inside and the butler’s voice calling a taxi. Then I walked + | ^~~~~~~~ The canonical dictionary spelling is `butler's`. +Suggest: + - Replace with: “butler's” + + + Lint: Spelling (63 priority) Message: | 4635 | the house in a moment; I wouldn’t have been surprised to see sinister faces, the @@ -6420,15 +6970,6 @@ Suggest: -Lint: WordChoice (127 priority) -Message: | - 5069 | “Don’t belong to any.” - | ^~ Use `too` here to mean ‘also’ or an excessive degree. -Suggest: - - Replace with: “too” - - - Lint: Spelling (63 priority) Message: | 5087 | Michaelis opened the drawer nearest his hand. There was nothing in it but a @@ -6447,15 +6988,25 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 5121 | Wilson shook his head. His eyes narrowed and his mouth widened slightly with the + 5122 | ghost of a superior “Hm!” + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “HM” + + + Lint: Spelling (63 priority) Message: | 5121 | Wilson shook his head. His eyes narrowed and his mouth widened slightly with the 5122 | ghost of a superior “Hm!” | ^~ Did you mean to spell `Hm` this way? Suggest: + - Replace with: “H” - Replace with: “H'm” - - Replace with: “Ha” - - Replace with: “Ham” + - Replace with: “Hem” @@ -6723,6 +7274,16 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 5251 | Gatsby’s side, and alone. From the moment I telephoned news of the catastrophe + | ^~~~ The canonical dictionary spelling is `news`. + 5252 | to West Egg village, every surmise about him, and every practical question, was +Suggest: + - Replace with: “news” + + + Lint: Readability (127 priority) Message: | 5253 | referred to me. At first I was surprised and confused; then, as he lay in his @@ -6842,6 +7403,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 5330 | thought this would be Daisy at last. But the connection came through as a man’s + | ^~~~~ The canonical dictionary spelling is `man's`. + 5331 | voice, very thin and far away. +Suggest: + - Replace with: “man's” + + + Lint: Spelling (63 priority) Message: | 5333 | “This is Slagle speaking . . .” @@ -6907,7 +7478,7 @@ Message: | 5352 | I think it was on the third day that a telegram signed Henry C. Gatz arrived | ^~ Did you mean to spell `C.` this way? Suggest: - - Replace with: “Cu” + - Replace with: “Cw” - Replace with: “C” - Replace with: “Cc” @@ -7043,8 +7614,8 @@ Message: | | ^~ Did you mean to spell `B.` this way? Suggest: - Replace with: “Bu” + - Replace with: “B” - Replace with: “Be” - - Replace with: “Bf” @@ -7055,7 +7626,7 @@ Message: | Suggest: - Replace with: “Ft” - Replace with: “F” - - Replace with: “Ff” + - Replace with: “Fa” @@ -7110,6 +7681,35 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 5487 | “But I know he’s there.” + | ^~~~ The canonical dictionary spelling is `he's`. +Suggest: + - Replace with: “he's” + + + +Lint: Capitalization (127 priority) +Message: | + 5493 | “We’re getting sick in tired of it. When I say he’s in Chicago, he’s in + | ^~~~ The canonical dictionary spelling is `he's`. + 5494 | Chicago.” +Suggest: + - Replace with: “he's” + + + +Lint: Capitalization (127 priority) +Message: | + 5493 | “We’re getting sick in tired of it. When I say he’s in Chicago, he’s in + | ^~~~ The canonical dictionary spelling is `he's`. + 5494 | Chicago.” +Suggest: + - Replace with: “he's” + + + Lint: Formatting (255 priority) Message: | 5498 | “Oh-h!” She looked at me over again. ‘‘Will you just— What was your name?” @@ -7139,6 +7739,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 5508 | Street and asked for a job. He hadn’t eat anything for a couple of days. ‘Come + 5509 | on have some lunch with me,’ I sid. He ate more than four dollars’ worth of food + | ^~~ The canonical dictionary spelling is `Sid`. +Suggest: + - Replace with: “Sid” + + + Lint: Spelling (63 priority) Message: | 5508 | Street and asked for a job. He hadn’t eat anything for a couple of days. ‘Come @@ -7178,6 +7788,15 @@ Message: | +Lint: Capitalization (127 priority) +Message: | + 5528 | “Now he’s dead,” I said after a moment. “You were his closest friend, so I know + | ^~~~ The canonical dictionary spelling is `he's`. +Suggest: + - Replace with: “he's” + + + Lint: Spelling (63 priority) Message: | 5552 | For a moment I thought he was going to suggest a “gonnegtion,” but he only @@ -7200,6 +7819,16 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 5560 | up and down excitedly in the hall. His pride in his son and in his son’s + | ^~~~~ The canonical dictionary spelling is `son's`. + 5561 | possessions was continually increasing and now he had something to show me. +Suggest: + - Replace with: “son's” + + + Lint: Agreement (30 priority) Message: | 5575 | “He come out to see me two years ago and bought me the house I live in now. Of @@ -7255,6 +7884,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 5612 | something. Do you notice what he’s got about improving his mind? He was always + | ^~~~ The canonical dictionary spelling is `he's`. +Suggest: + - Replace with: “he's” + + + Lint: Repetition (63 priority) Message: | 5613 | great for that. He told me I et like a hog once, and I beat him for it.” @@ -7265,6 +7903,15 @@ Suggest: +Lint: Capitalization (127 priority) +Message: | + 5613 | great for that. He told me I et like a hog once, and I beat him for it.” + | ^~ This word's canonical spelling is all-caps. +Suggest: + - Replace with: “ET” + + + Lint: Spelling (63 priority) Message: | 5613 | great for that. He told me I et like a hog once, and I beat him for it.”