feat(core): more rules (#2107)

This commit is contained in:
Elijah Potter 2025-11-05 08:27:25 -07:00 committed by GitHub
parent c6055ab267
commit c0426a7349
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 3637 additions and 401 deletions

View file

@ -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
//

View file

@ -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.");

View file

@ -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

View file

@ -527,6 +527,7 @@
"Las Vegas",
"Los Angeles",
"New York",
"New York City",
"Niagara Falls",
"Novi Sad",
"Panama Canal",

View file

@ -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;

View file

@ -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;

View file

@ -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<I: Iterator<Item = char>>(
chars: &mut std::iter::Peekable<I>,
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));

View file

@ -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};

View file

@ -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<dyn Expr>,
map: Arc<ExprMap<usize>>,
}
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<Lint> {
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,
);
}
}

View file

@ -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<dyn Expr>,
}
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<Lint> {
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());
}
}

View file

@ -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<dyn Expr>,
}
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<Lint> {
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,
);
}
}

View file

@ -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<dyn Expr>,
map: Arc<ExprMap<usize>>,
}
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<Lint> {
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<char> = 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,
);
}
}

View file

@ -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<dyn Expr>,
map: Arc<ExprMap<usize>>,
}
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<Lint> {
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,
);
}
}

View file

@ -0,0 +1,127 @@
use crate::{
Token,
expr::{AnchorStart, Expr, SequenceExpr},
linting::{ExprLinter, Lint, LintKind, Suggestion},
};
pub struct HelloGreeting {
expr: Box<dyn Expr>,
}
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<Lint> {
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!\"",
);
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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<dyn Expr>,
map: Arc<ExprMap<MatchContext>>,
}
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<Lint> {
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,
);
}
}

View file

@ -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<FstDictionary>,
expr: Box<dyn Expr>,
}
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<Lint> {
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::<String>()
),
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::<String>()
),
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());
}
}
}

View file

@ -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"],
["Alzheimers 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"],

View file

@ -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() {

View file

@ -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<dyn Expr>,
}
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<Lint> {
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<char> = if uppercase_suffix {
vec!['A', 'R', 'E']
} else {
vec!['a', 'r', 'e']
};
let re_suffix: Vec<char> = 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);
}
}

View file

@ -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<dyn Expr>,
}
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<Lint> {
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());
}
}

View file

@ -0,0 +1,134 @@
use crate::{
CharStringExt, Token,
expr::SequenceExpr,
linting::{ExprLinter, Lint, LintKind, Suggestion},
};
pub struct Theres {
expr: Box<dyn crate::expr::Expr>,
}
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<Lint> {
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);
}
}

View file

@ -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(

View file

@ -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(

View file

@ -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));
}
}

View file

@ -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<I: Iterator<Item = char>>(
chars: &mut std::iter::Peekable<I>,
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<char, HumanReadableExpansion>,

View file

@ -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);

View file

@ -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—Whos to go down
| ^~~~~ The canonical dictionary spelling is `who's`.
693 | the chimney?—Nay, I shant! You do it!—That I wont, then!—Bills to go
Suggest:
- Replace with: “who's”
Lint: Capitalization (31 priority)
Message: |
694 | down—Here, Bill! the master says youre to go down the chimney!”
@ -1484,6 +1503,15 @@ Suggest:
Lint: Capitalization (127 priority)
Message: |
964 | “Come, my heads 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 heads free at last!” said Alice in a tone of delight, which changed
@ -1640,6 +1668,16 @@ Message: |
Lint: Capitalization (127 priority)
Message: |
1127 | “Oh, theres no use in talking to him,” said Alice desperately: “hes 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 | “Whos 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. “Theres a
2307 | > porpoise close behind us, and hes 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, “Ive 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!”

View file

@ -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",

View file

@ -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 | Ive 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

View file

@ -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

View file

@ -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.

View file

@ -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

File diff suppressed because it is too large Load diff