use crate::{
CharStringExt, Lint, Token, TokenKind,
expr::{Expr, FirstMatchOf, SequenceExpr},
linting::{ExprLinter, LintKind, Suggestion, expr_linter::Chunk},
spell::Dictionary,
};
static NON_MODAL_AUX: &[&str] = &[
"do", "don't", "does", "doesn't", "have", "has", "haven't", "hasn't", "dont", "doesnt",
"havent", "hasnt",
];
static IRREGULAR: &[(&str, &str)] = &[("don't", "doesn't"), ("have", "has"), ("haven't", "hasn't")];
static SUBJUNCTIVE: &[&str] = &[
"that",
"demanded",
"demanding",
"insisted",
"insisting",
"recommended",
"recommending",
"requested",
"requesting",
"suggested",
"suggesting",
];
static DITRANSITIVE: &[&str] = &[
"give", "gave", "given", "gives", "giving", "lose", "lost", "loses", "losing",
];
pub struct PronounVerbAgreement<D> {
expr: FirstMatchOf,
dict: D,
}
impl<D: Dictionary> PronounVerbAgreement<D> {
pub fn new(dict: D) -> Self {
let non_3p_sing_pres_pron_with_3p_sing_pres_verb = SequenceExpr::default()
.then_kind_both_but_not(
(
TokenKind::is_personal_pronoun,
TokenKind::is_subject_pronoun,
),
TokenKind::is_third_person_singular_pronoun,
)
.t_ws()
.then_verb_third_person_singular_present_form();
let third_person_sing_pres_pron = |t: &Token, _: &[char]| {
t.kind.is_subject_pronoun()
&& !t.kind.is_object_pronoun()
&& t.kind.is_personal_pronoun()
&& t.kind.is_third_person_singular_pronoun()
&& !t.kind.is_plural_pronoun()
};
let verb_lemma = |t: &Token, src: &[char]| {
t.kind.is_verb_lemma()
&& !t.kind.is_verb_third_person_singular_present_form()
&& !t.kind.is_verb_simple_past_form() && !t.kind.is_adverb() && !t.kind.is_conjunction() && (!t.kind.is_auxiliary_verb() || t.get_ch(src).eq_any_ignore_ascii_case_str(NON_MODAL_AUX))
};
Self {
expr: FirstMatchOf::new(vec![
Box::new(non_3p_sing_pres_pron_with_3p_sing_pres_verb),
Box::new(
SequenceExpr::with(third_person_sing_pres_pron)
.t_ws()
.then(verb_lemma),
),
Box::new(SequenceExpr::word_seq(&["it", "don't"])),
]),
dict,
}
}
fn third_person_singular_present_to_lemma(&self, form: &[char]) -> Vec<Vec<char>> {
let mut words: Vec<Vec<char>> = Vec::new();
if form.ends_with_ignore_ascii_case_chars(&['s']) {
words.push(form[0..form.len() - 1].to_vec());
if form.ends_with_ignore_ascii_case_chars(&['e', 's']) {
words.push(form[0..form.len() - 2].to_vec());
if form.ends_with_ignore_ascii_case_chars(&['i', 'e', 's']) {
words.push(
format!("{}y", form[0..form.len() - 3].iter().collect::<String>())
.chars()
.collect(),
);
}
}
}
if let Some((lemma, _)) = IRREGULAR.iter().find(|(_, f)| form.eq_str(f)) {
words.push(lemma.chars().collect::<Vec<char>>());
}
words
.iter()
.filter(|&w| {
self.dict
.get_word_metadata(w)
.is_some_and(|md| md.is_verb_lemma())
})
.map(|w| w.to_vec())
.collect()
}
fn lemma_to_third_person_singular_present(&self, input: &str) -> Vec<Vec<char>> {
let mut words: Vec<Vec<char>> = Vec::new();
words.push(format!("{input}s").chars().collect());
words.push(format!("{input}es").chars().collect());
if input.ends_with("y") {
words.push(
format!("{}ies", &input[0..input.len() - 1])
.chars()
.collect(),
);
}
if let Some((_, form)) = IRREGULAR
.iter()
.find(|(lemma, _)| input.eq_ignore_ascii_case(lemma))
{
words.push(form.chars().collect());
}
words
.iter()
.filter(|&w| {
self.dict
.get_word_metadata(w)
.is_some_and(|md| md.is_verb_third_person_singular_present_form())
})
.map(|w| w.to_vec())
.collect()
}
}
impl<D: Dictionary> ExprLinter for PronounVerbAgreement<D> {
type Unit = Chunk;
fn expr(&self) -> &dyn Expr {
&self.expr
}
fn match_to_lint_with_context(
&self,
toks: &[Token],
src: &[char],
ctx: Option<(&[Token], &[Token])>,
) -> Option<Lint> {
let pron_tok = &toks[0];
let is_3psg = pron_tok.kind.is_third_person_singular_pronoun();
if pron_tok.span.end < src.len() && src[pron_tok.span.end] == '-' {
return None;
}
let verb_tok = toks.last()?;
if let Some((before, _)) = ctx
&& let [.., prev_word_tok, ws_tok] = before
&& ws_tok.kind.is_whitespace()
{
let prev_word = prev_word_tok.get_ch(src);
let is_exempt = if is_3psg {
prev_word_tok.kind.is_auxiliary_verb()
|| prev_word.eq_any_ignore_ascii_case_str(SUBJUNCTIVE)
} else if pron_tok.kind.is_subject_pronoun() {
prev_word_tok.kind.is_preposition() && !prev_word.eq_str("up")
|| (prev_word.eq_any_ignore_ascii_case_str(DITRANSITIVE) && verb_tok.kind.is_noun())
} else {
false
};
if is_exempt {
return None;
}
}
let verb_span = verb_tok.span;
let verb_chars = verb_tok.get_ch(src);
let verb_str = verb_tok.get_str(src);
let suggs = if is_3psg {
self.lemma_to_third_person_singular_present(&verb_str)
} else {
self.third_person_singular_present_to_lemma(verb_chars)
};
let suggestions = suggs
.into_iter()
.map(|s| Suggestion::replace_with_match_case(s, verb_chars))
.collect();
Some(Lint {
span: verb_span,
lint_kind: LintKind::Agreement,
suggestions,
message: "The form of the verb must agree in grammatical number with the pronoun."
.to_owned(),
..Default::default()
})
}
fn description(&self) -> &str {
"Ensures pronouns agree with their verbs."
}
}
#[cfg(test)]
mod lints {
use super::PronounVerbAgreement;
use crate::linting::create_test_pool;
use crate::linting::tests::{assert_no_lints, assert_suggestion_result};
use crate::spell::FstDictionary;
use std::sync::Arc;
create_test_pool!(
PronounVerbAgreement,
PronounVerbAgreement<Arc<FstDictionary>>,
PronounVerbAgreement::new(FstDictionary::curated())
);
#[test]
fn issue_233_1() {
assert_suggestion_result("I likes this place.", test_linter(), "I like this place.");
}
#[test]
fn issue_233_2() {
assert_suggestion_result("I sits under the AC.", test_linter(), "I sit under the AC.");
}
#[test]
#[ignore = "because 'like' is an adjective as well as a verb."]
fn issue_233_1_reverse() {
assert_suggestion_result("He like this place.", test_linter(), "He likes this place.");
}
#[test]
fn why_we_cant_flag_like_yet() {
assert_no_lints("What is he like?", test_linter());
}
#[test]
fn issue_233_2_reverse() {
assert_suggestion_result(
"She sit under the AC.",
test_linter(),
"She sits under the AC.",
);
}
#[test]
fn dont_flag_correct_agreement() {
assert_no_lints("He likes this place. I sit under the AC.", test_linter());
}
#[test]
fn issue_3925_dont_flag_simple_past_forms() {
assert_no_lints(
"He fought tenaciously. He wrought tenaciously.",
test_linter(),
);
}
#[test]
fn fixes_i() {
assert_suggestion_result("I wakes up.", test_linter(), "I wake up.");
}
#[test]
fn fixes_we() {
assert_suggestion_result("We gets dressed.", test_linter(), "We get dressed.");
}
#[test]
fn fixes_you() {
assert_suggestion_result(
"You drops off the kids.",
test_linter(),
"You drop off the kids.",
);
}
#[test]
fn fixes_he() {
assert_suggestion_result("He work hard.", test_linter(), "He works hard.");
}
#[test]
fn fixes_she() {
assert_suggestion_result("She study hard.", test_linter(), "She studies hard.");
}
#[test]
#[ignore = "Becasue 'it' is also object case. Eg. 'watch it break down'"]
fn we_cant_fix_it_yet() {
assert_suggestion_result("It break down.", test_linter(), "It breaks down.");
}
#[test]
fn why_we_cant_fix_it_yet() {
assert_no_lints("I heard it break down.", test_linter());
}
#[test]
fn fixes_they() {
assert_suggestion_result("They repairs it.", test_linter(), "They repair it.")
}
#[test]
fn dont_flag_i() {
assert_no_lints("I eat", test_linter());
}
#[test]
fn dont_flag_we() {
assert_no_lints("We drink", test_linter());
}
#[test]
fn dont_flag_you() {
assert_no_lints("You walk", test_linter());
}
#[test]
fn dont_flag_he() {
assert_no_lints("He runs", test_linter());
}
#[test]
fn dont_flag_she() {
assert_no_lints("She swims", test_linter());
}
#[test]
fn dont_flag_it() {
assert_no_lints("It works!", test_linter());
}
#[test]
fn dont_flag_they() {
assert_no_lints("They finish", test_linter());
}
#[test]
fn fix_flies() {
assert_suggestion_result("I flies", test_linter(), "I fly");
}
#[test]
fn fix_cry() {
assert_suggestion_result("He cry", test_linter(), "He cries");
}
#[test]
fn fix_go() {
assert_suggestion_result("She go", test_linter(), "She goes");
}
#[test]
fn fix_goes() {
assert_suggestion_result("They goes", test_linter(), "They go");
}
#[test]
fn fix_has() {
assert_suggestion_result("You has", test_linter(), "You have");
}
#[test]
fn fix_have() {
assert_suggestion_result("She have", test_linter(), "She has");
}
#[test]
fn fix_hasnt() {
assert_suggestion_result("You hasn't", test_linter(), "You haven't");
}
#[test]
fn fix_havent() {
assert_suggestion_result("He haven't", test_linter(), "He hasn't");
}
#[test]
fn fix_box() {
assert_suggestion_result("He box", test_linter(), "He boxes");
}
#[test]
fn fix_boxes() {
assert_suggestion_result("You boxes", test_linter(), "You box");
}
#[test]
fn fix_doesnt() {
assert_suggestion_result("We doesn't", test_linter(), "We don't");
}
#[test]
fn fix_dont() {
assert_suggestion_result("It don't", test_linter(), "It doesn't");
}
#[test]
fn fix_do() {
assert_suggestion_result("He do", test_linter(), "He does");
}
#[test]
fn fix_does() {
assert_suggestion_result("You does", test_linter(), "You do");
}
#[test]
fn false_positive_she_consider() {
assert_no_lints(
"On April 10th, I suggested she consider a smaller, more intimate gathering.",
test_linter(),
);
}
#[test]
fn false_positive_she_sell() {
assert_no_lints(
"I suggested she sell it and use the proceeds to help with her relocation expenses, or perhaps rent a similar camera while in Barcelona.",
test_linter(),
);
}
#[test]
fn false_positive_she_rent() {
assert_no_lints(
"I suggested she sell it and use the proceeds to help with her relocation expenses, or perhaps rent a similar camera while in Barcelona.",
test_linter(),
);
}
#[test]
fn false_positive_he_donned() {
assert_no_lints(
"He donned his heavy oilskins and descended the winding staircase, his boots echoing in the hollow tower.",
test_linter(),
);
}
#[test]
fn false_positive_he_cannot() {
assert_no_lints(
"Surely, he cannot offer the same sum as the developers.",
test_linter(),
);
}
#[test]
fn false_positive_insisting_she_return() {
assert_no_lints(
"Am I the asshole for insisting she return the dress?",
test_linter(),
);
}
#[test]
fn false_positive_pride_in_you_is() {
assert_no_lints(
"It’s also important to recognize that your family's pride in you is a genuine reflection of your value.",
test_linter(),
);
}
#[test]
fn false_positive_she_sought() {
assert_no_lints(
"She sought out Mrs. Hawthorne, the village’s oldest resident, a woman known for her vast knowledge of local history and her unsettlingly accurate intuition.",
test_linter(),
);
}
#[test]
fn false_positive_lose_you_points() {
assert_no_lints(
"I admire your dedication to consistently drafting players who are actively trying to lose you points.",
test_linter(),
);
}
#[test]
fn false_positive_she_hung_up() {
assert_no_lints(
"When I reiterated the conditions I'd previously set, she hung up on me.",
test_linter(),
);
}
}