mirror of
https://github.com/Automattic/harper.git
synced 2025-12-05 19:26:55 -06:00
feat(core): many new rules (#2292)
* feat(core): `Handful` * feat(core): add several additional rules * refactor(core): combine `ItsContraction` duplicates * fix(core): `ItsContraction` should emit `LintKind::Punctuation` In accordance to part of #2220 * chore(core): update snapshots * feat(core): many more rules * feat(core): many more * feat(core): `CureFor` * fix(core): merge fallout * feat(core): expand according to feedback * fix(core): appease clippy
This commit is contained in:
144
harper-core/src/linting/apart_from.rs
Normal file
144
harper-core/src/linting/apart_from.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use crate::Token;
|
||||
use crate::expr::{Expr, SequenceExpr};
|
||||
use crate::linting::expr_linter::Chunk;
|
||||
|
||||
use super::{ExprLinter, Lint, LintKind, Suggestion};
|
||||
|
||||
pub struct ApartFrom {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for ApartFrom {
|
||||
fn default() -> Self {
|
||||
let expr = SequenceExpr::any_capitalization_of("apart")
|
||||
.t_ws()
|
||||
.then_any_capitalization_of("form");
|
||||
|
||||
Self {
|
||||
expr: Box::new(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for ApartFrom {
|
||||
type Unit = Chunk;
|
||||
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let span = matched_tokens.last()?.span;
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
suggestions: vec![Suggestion::replace_with_match_case_str(
|
||||
"from",
|
||||
span.get_content(source),
|
||||
)],
|
||||
message: "Use `from` to spell `apart from`.".to_owned(),
|
||||
priority: 50,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Flags the misspelling `apart form` and suggests `apart from`."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ApartFrom;
|
||||
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn corrects_basic_typo() {
|
||||
assert_suggestion_result(
|
||||
"Christianity was set apart form other religions.",
|
||||
ApartFrom::default(),
|
||||
"Christianity was set apart from other religions.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_title_case() {
|
||||
assert_suggestion_result(
|
||||
"Apart Form these files, everything uploaded fine.",
|
||||
ApartFrom::default(),
|
||||
"Apart From these files, everything uploaded fine.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"APART FORM THE REST OF THE FIELD.",
|
||||
ApartFrom::default(),
|
||||
"APART FROM THE REST OF THE FIELD.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_comma() {
|
||||
assert_suggestion_result(
|
||||
"It was apart form, not apart from, the original plan.",
|
||||
ApartFrom::default(),
|
||||
"It was apart from, not apart from, the original plan.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_newline() {
|
||||
assert_suggestion_result(
|
||||
"They stood apart\nform everyone else at the rally.",
|
||||
ApartFrom::default(),
|
||||
"They stood apart\nfrom everyone else at the rally.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_extra_spacing() {
|
||||
assert_suggestion_result(
|
||||
"We keep the archive apart form public assets.",
|
||||
ApartFrom::default(),
|
||||
"We keep the archive apart from public assets.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_correct_phrase() {
|
||||
assert_lint_count(
|
||||
"Lebanon's freedoms set it apart from other Arab states.",
|
||||
ApartFrom::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_hyphenated() {
|
||||
assert_lint_count(
|
||||
"Their apart-form design wasn’t what we needed.",
|
||||
ApartFrom::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_split_by_comma() {
|
||||
assert_lint_count(
|
||||
"They stood apart, form lines when asked.",
|
||||
ApartFrom::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_unrelated_form_usage() {
|
||||
assert_lint_count(
|
||||
"The form was kept apart to dry after printing.",
|
||||
ApartFrom::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
147
harper-core/src/linting/cure_for.rs
Normal file
147
harper-core/src/linting/cure_for.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use crate::{
|
||||
Span, Token,
|
||||
expr::{Expr, SequenceExpr},
|
||||
linting::expr_linter::Chunk,
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
patterns::{DerivedFrom, Word},
|
||||
};
|
||||
|
||||
pub struct CureFor {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for CureFor {
|
||||
fn default() -> Self {
|
||||
let expr = SequenceExpr::default()
|
||||
.then(DerivedFrom::new_from_str("cure"))
|
||||
.t_ws()
|
||||
.then(Word::new("against"));
|
||||
|
||||
Self {
|
||||
expr: Box::new(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for CureFor {
|
||||
type Unit = Chunk;
|
||||
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let against = matched_tokens.last()?;
|
||||
|
||||
let template: Vec<char> = against.span.get_content(source).to_vec();
|
||||
let suggestion = Suggestion::replace_with_match_case_str("for", &template);
|
||||
|
||||
Some(Lint {
|
||||
span: Span::new(against.span.start, against.span.end),
|
||||
lint_kind: LintKind::Usage,
|
||||
suggestions: vec![suggestion],
|
||||
message: "Prefer `cure for` when describing a treatment target.".to_owned(),
|
||||
priority: 31,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Flags `cure against` and prefers the standard `cure for` pairing."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::CureFor;
|
||||
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn corrects_simple_cure_against() {
|
||||
assert_suggestion_result(
|
||||
"Researchers sought a cure against the stubborn illness.",
|
||||
CureFor::default(),
|
||||
"Researchers sought a cure for the stubborn illness.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_plural_cures_against() {
|
||||
assert_suggestion_result(
|
||||
"Doctors insist this serum cures against the new variant.",
|
||||
CureFor::default(),
|
||||
"Doctors insist this serum cures for the new variant.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_past_participle_cured_against() {
|
||||
assert_suggestion_result(
|
||||
"The remedy was cured against the infection last spring.",
|
||||
CureFor::default(),
|
||||
"The remedy was cured for the infection last spring.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_uppercase_against() {
|
||||
assert_suggestion_result(
|
||||
"We still trust the cure AGAINST the dreaded plague.",
|
||||
CureFor::default(),
|
||||
"We still trust the cure FOR the dreaded plague.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_at_sentence_start() {
|
||||
assert_suggestion_result(
|
||||
"Cure against that condition became the rallying cry.",
|
||||
CureFor::default(),
|
||||
"Cure for that condition became the rallying cry.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_cure_for() {
|
||||
assert_lint_count(
|
||||
"They finally found a cure for the fever.",
|
||||
CureFor::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_cure_from() {
|
||||
assert_lint_count(
|
||||
"A cure from this rare herb is on the horizon.",
|
||||
CureFor::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_with_comma() {
|
||||
assert_lint_count(
|
||||
"A cure, against all odds, appeared in the files.",
|
||||
CureFor::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_unrelated_against() {
|
||||
assert_lint_count(
|
||||
"Travelers stand against the roaring wind on the cliffs.",
|
||||
CureFor::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_secure_against() {
|
||||
assert_lint_count(
|
||||
"The fortress stayed secure against the invaders.",
|
||||
CureFor::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
160
harper-core/src/linting/handful.rs
Normal file
160
harper-core/src/linting/handful.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use crate::expr::{Expr, SequenceExpr, SpaceOrHyphen};
|
||||
use crate::linting::expr_linter::Chunk;
|
||||
use crate::{Token, TokenStringExt};
|
||||
|
||||
use super::{ExprLinter, Lint, LintKind, Suggestion};
|
||||
|
||||
pub struct Handful {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for Handful {
|
||||
fn default() -> Self {
|
||||
let expr = SequenceExpr::default()
|
||||
.then_any_capitalization_of("hand")
|
||||
.then_one_or_more(SpaceOrHyphen)
|
||||
.then_any_capitalization_of("full")
|
||||
.then_one_or_more(SpaceOrHyphen)
|
||||
.then_any_capitalization_of("of");
|
||||
|
||||
Self {
|
||||
expr: Box::new(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for Handful {
|
||||
type Unit = Chunk;
|
||||
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
if matched_tokens.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut highlight_end = matched_tokens.len() - 1;
|
||||
while highlight_end > 0 {
|
||||
let prev = &matched_tokens[highlight_end - 1];
|
||||
if prev.kind.is_whitespace() || prev.kind.is_hyphen() {
|
||||
highlight_end -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if highlight_end == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let replacement = &matched_tokens[..highlight_end];
|
||||
let span = replacement.span()?;
|
||||
let template = matched_tokens.first()?.span.get_content(source);
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::BoundaryError,
|
||||
suggestions: vec![Suggestion::replace_with_match_case(
|
||||
"handful".chars().collect(),
|
||||
template,
|
||||
)],
|
||||
message: "Write this quantity as the single word `handful`.".to_owned(),
|
||||
priority: 31,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Keeps the palm-sized quantity expressed by `handful` as one word."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Handful;
|
||||
use crate::linting::tests::{assert_lint_count, assert_no_lints, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn suggests_plain_spacing() {
|
||||
assert_suggestion_result(
|
||||
"Her basket held a hand full of berries.",
|
||||
Handful::default(),
|
||||
"Her basket held a handful of berries.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggests_capitalized_form() {
|
||||
assert_suggestion_result(
|
||||
"Hand full of tales lined the shelf.",
|
||||
Handful::default(),
|
||||
"Handful of tales lined the shelf.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggests_hyphenated_form() {
|
||||
assert_suggestion_result(
|
||||
"A hand-full of marbles scattered across the floor.",
|
||||
Handful::default(),
|
||||
"A handful of marbles scattered across the floor.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggests_space_hyphen_combo() {
|
||||
assert_suggestion_result(
|
||||
"A hand - full of seeds spilled on the workbench.",
|
||||
Handful::default(),
|
||||
"A handful of seeds spilled on the workbench.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggests_initial_hyphen_variants() {
|
||||
assert_suggestion_result(
|
||||
"Hand-Full of furniture, the cart creaked slowly.",
|
||||
Handful::default(),
|
||||
"Handful of furniture, the cart creaked slowly.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flags_multiple_instances() {
|
||||
assert_lint_count(
|
||||
"She carried a hand full of carrots and a hand full of radishes.",
|
||||
Handful::default(),
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_correct_handful() {
|
||||
assert_no_lints(
|
||||
"A handful of volunteers arrived in time.",
|
||||
Handful::default(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_parenthetical_hand() {
|
||||
assert_no_lints(
|
||||
"His hand, full of ink, kept writing without pause.",
|
||||
Handful::default(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_hand_is_full() {
|
||||
assert_no_lints("The hand is full of water.", Handful::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_handfull_typo() {
|
||||
assert_no_lints(
|
||||
"The word handfull is an incorrect spelling.",
|
||||
Handful::default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
88
harper-core/src/linting/its_contraction/general.rs
Normal file
88
harper-core/src/linting/its_contraction/general.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use harper_brill::UPOS;
|
||||
|
||||
use crate::{
|
||||
Document, Token, TokenStringExt,
|
||||
expr::{All, Expr, ExprExt, OwnedExprExt, SequenceExpr},
|
||||
linting::{Lint, LintKind, Linter, Suggestion},
|
||||
patterns::{NominalPhrase, Pattern, UPOSSet, WordSet},
|
||||
};
|
||||
|
||||
pub struct General {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for General {
|
||||
fn default() -> Self {
|
||||
let positive = SequenceExpr::default().t_aco("its").then_whitespace().then(
|
||||
UPOSSet::new(&[UPOS::VERB, UPOS::AUX, UPOS::DET, UPOS::PRON])
|
||||
.or(WordSet::new(&["because"])),
|
||||
);
|
||||
|
||||
let exceptions = SequenceExpr::default()
|
||||
.then_anything()
|
||||
.then_anything()
|
||||
.then(WordSet::new(&["own", "intended"]));
|
||||
|
||||
let inverted = SequenceExpr::default().then_unless(exceptions);
|
||||
|
||||
let expr = All::new(vec![Box::new(positive), Box::new(inverted)]).or_longest(
|
||||
SequenceExpr::aco("its")
|
||||
.t_ws()
|
||||
.then(UPOSSet::new(&[UPOS::ADJ]))
|
||||
.t_ws()
|
||||
.then(UPOSSet::new(&[UPOS::SCONJ, UPOS::PART])),
|
||||
);
|
||||
|
||||
Self {
|
||||
expr: Box::new(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Linter for General {
|
||||
fn lint(&mut self, document: &Document) -> Vec<Lint> {
|
||||
let mut lints = Vec::new();
|
||||
let source = document.get_source();
|
||||
|
||||
for chunk in document.iter_chunks() {
|
||||
lints.extend(
|
||||
self.expr
|
||||
.iter_matches(chunk, source)
|
||||
.filter_map(|match_span| {
|
||||
self.match_to_lint(&chunk[match_span.start..], source)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
lints
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Detects the possessive `its` before `had`, `been`, or `got` and offers `it's` or `it has`."
|
||||
}
|
||||
}
|
||||
|
||||
impl General {
|
||||
fn match_to_lint(&self, toks: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let offender = toks.first()?;
|
||||
let offender_chars = offender.span.get_content(source);
|
||||
|
||||
if toks.get(2)?.kind.is_upos(UPOS::VERB)
|
||||
&& NominalPhrase.matches(&toks[2..], source).is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Lint {
|
||||
span: offender.span,
|
||||
lint_kind: LintKind::Punctuation,
|
||||
suggestions: vec![
|
||||
Suggestion::replace_with_match_case_str("it's", offender_chars),
|
||||
Suggestion::replace_with_match_case_str("it has", offender_chars),
|
||||
],
|
||||
message: "Use `it's` (short for `it has` or `it is`) here, not the possessive `its`."
|
||||
.to_owned(),
|
||||
priority: 54,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,102 +1,15 @@
|
||||
use harper_brill::UPOS;
|
||||
use super::merge_linters::merge_linters;
|
||||
|
||||
use crate::Document;
|
||||
use crate::TokenStringExt;
|
||||
use crate::expr::All;
|
||||
use crate::expr::Expr;
|
||||
use crate::expr::ExprExt;
|
||||
use crate::expr::OwnedExprExt;
|
||||
use crate::expr::SequenceExpr;
|
||||
use crate::patterns::NominalPhrase;
|
||||
use crate::patterns::Pattern;
|
||||
use crate::patterns::UPOSSet;
|
||||
use crate::patterns::WordSet;
|
||||
use crate::{
|
||||
Token,
|
||||
linting::{Lint, LintKind, Suggestion},
|
||||
};
|
||||
mod general;
|
||||
mod proper_noun;
|
||||
|
||||
use super::Linter;
|
||||
use general::General;
|
||||
use proper_noun::ProperNoun;
|
||||
|
||||
pub struct ItsContraction {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for ItsContraction {
|
||||
fn default() -> Self {
|
||||
let positive = SequenceExpr::default().t_aco("its").then_whitespace().then(
|
||||
UPOSSet::new(&[UPOS::VERB, UPOS::AUX, UPOS::DET, UPOS::PRON])
|
||||
.or(WordSet::new(&["because"])),
|
||||
);
|
||||
|
||||
let exceptions = SequenceExpr::default()
|
||||
.then_anything()
|
||||
.then_anything()
|
||||
.then(WordSet::new(&["own", "intended"]));
|
||||
|
||||
let inverted = SequenceExpr::default().then_unless(exceptions);
|
||||
|
||||
let expr = All::new(vec![Box::new(positive), Box::new(inverted)]).or_longest(
|
||||
SequenceExpr::aco("its")
|
||||
.t_ws()
|
||||
.then(UPOSSet::new(&[UPOS::ADJ]))
|
||||
.t_ws()
|
||||
.then(UPOSSet::new(&[UPOS::SCONJ, UPOS::PART])),
|
||||
);
|
||||
|
||||
Self {
|
||||
expr: Box::new(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Linter for ItsContraction {
|
||||
fn lint(&mut self, document: &Document) -> Vec<Lint> {
|
||||
let mut lints = Vec::new();
|
||||
let source = document.get_source();
|
||||
|
||||
for chunk in document.iter_chunks() {
|
||||
lints.extend(
|
||||
self.expr
|
||||
.iter_matches(chunk, source)
|
||||
.filter_map(|match_span| {
|
||||
self.match_to_lint(&chunk[match_span.start..], source)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
lints
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Detects the possessive `its` before `had`, `been`, or `got` and offers `it's` or `it has`."
|
||||
}
|
||||
}
|
||||
|
||||
impl ItsContraction {
|
||||
fn match_to_lint(&self, toks: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let offender = toks.first()?;
|
||||
let offender_chars = offender.span.get_content(source);
|
||||
|
||||
if toks.get(2)?.kind.is_upos(UPOS::VERB)
|
||||
&& NominalPhrase.matches(&toks[2..], source).is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Lint {
|
||||
span: offender.span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
suggestions: vec![
|
||||
Suggestion::replace_with_match_case_str("it's", offender_chars),
|
||||
Suggestion::replace_with_match_case_str("it has", offender_chars),
|
||||
],
|
||||
message: "Use `it's` (short for `it has` or `it is`) here, not the possessive `its`."
|
||||
.to_owned(),
|
||||
priority: 54,
|
||||
})
|
||||
}
|
||||
}
|
||||
merge_linters!(
|
||||
ItsContraction => General, ProperNoun =>
|
||||
"Detects places where the possessive `its` should be the contraction `it's`, including before verbs/clauses and before proper nouns after opinion verbs."
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -284,4 +197,90 @@ mod tests {
|
||||
ItsContraction::default(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_think_google() {
|
||||
assert_suggestion_result(
|
||||
"I think its Google, not Microsoft.",
|
||||
ItsContraction::default(),
|
||||
"I think it's Google, not Microsoft.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_hope_katie() {
|
||||
assert_suggestion_result(
|
||||
"I hope its Katie.",
|
||||
ItsContraction::default(),
|
||||
"I hope it's Katie.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_guess_date() {
|
||||
assert_suggestion_result(
|
||||
"I guess its March 6.",
|
||||
ItsContraction::default(),
|
||||
"I guess it's March 6.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_assume_john() {
|
||||
assert_suggestion_result(
|
||||
"We assume its John.",
|
||||
ItsContraction::default(),
|
||||
"We assume it's John.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_doubt_tesla() {
|
||||
assert_suggestion_result(
|
||||
"They doubt its Tesla this year.",
|
||||
ItsContraction::default(),
|
||||
"They doubt it's Tesla this year.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_two_word_name() {
|
||||
assert_suggestion_result(
|
||||
"She thinks its New York.",
|
||||
ItsContraction::default(),
|
||||
"She thinks it's New York.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_existing_contraction() {
|
||||
assert_lint_count("I think it's Google.", ItsContraction::default(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_possessive_noun_after_name() {
|
||||
assert_lint_count(
|
||||
"I think its Google product launch.",
|
||||
ItsContraction::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_without_opinion_verb() {
|
||||
assert_lint_count(
|
||||
"Its Google Pixel lineup is impressive.",
|
||||
ItsContraction::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_common_noun_target() {
|
||||
assert_lint_count(
|
||||
"We hope its accuracy improves.",
|
||||
ItsContraction::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
121
harper-core/src/linting/its_contraction/proper_noun.rs
Normal file
121
harper-core/src/linting/its_contraction/proper_noun.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
use harper_brill::UPOS;
|
||||
|
||||
use crate::{
|
||||
Document, Token, TokenStringExt,
|
||||
expr::{Expr, ExprExt, ExprMap, OwnedExprExt, SequenceExpr},
|
||||
linting::{Lint, LintKind, Linter, Suggestion},
|
||||
patterns::{DerivedFrom, UPOSSet},
|
||||
};
|
||||
|
||||
pub struct ProperNoun {
|
||||
expr: Box<dyn Expr>,
|
||||
map: Arc<ExprMap<Range<usize>>>,
|
||||
}
|
||||
|
||||
impl Default for ProperNoun {
|
||||
fn default() -> Self {
|
||||
let mut map = ExprMap::default();
|
||||
|
||||
let opinion_verbs = DerivedFrom::new_from_str("think")
|
||||
.or(DerivedFrom::new_from_str("hope"))
|
||||
.or(DerivedFrom::new_from_str("assume"))
|
||||
.or(DerivedFrom::new_from_str("doubt"))
|
||||
.or(DerivedFrom::new_from_str("guess"));
|
||||
|
||||
let capitalized_word = |tok: &Token, src: &[char]| {
|
||||
tok.kind.is_word()
|
||||
&& tok
|
||||
.span
|
||||
.get_content(src)
|
||||
.first()
|
||||
.map(|c| c.is_uppercase())
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
let name_head = UPOSSet::new(&[UPOS::PROPN]).or(capitalized_word);
|
||||
|
||||
let lookahead_word = SequenceExpr::default().t_ws().then_any_word();
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then(opinion_verbs)
|
||||
.t_ws()
|
||||
.t_aco("its")
|
||||
.t_ws()
|
||||
.then(name_head)
|
||||
.then_optional(lookahead_word),
|
||||
2..3,
|
||||
);
|
||||
|
||||
let map = Arc::new(map);
|
||||
|
||||
Self {
|
||||
expr: Box::new(map.clone()),
|
||||
map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Linter for ProperNoun {
|
||||
fn lint(&mut self, document: &Document) -> Vec<Lint> {
|
||||
let mut lints = Vec::new();
|
||||
let source = document.get_source();
|
||||
|
||||
for chunk in document.iter_chunks() {
|
||||
lints.extend(
|
||||
self.expr
|
||||
.iter_matches(chunk, source)
|
||||
.filter_map(|match_span| {
|
||||
let matched = &chunk[match_span.start..match_span.end];
|
||||
self.match_to_lint(matched, source)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
lints
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Suggests the contraction `it's` after opinion verbs when it introduces a proper noun."
|
||||
}
|
||||
}
|
||||
|
||||
impl ProperNoun {
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
if matched_tokens.len() >= 7
|
||||
&& let Some(next_word) = matched_tokens.get(6)
|
||||
{
|
||||
let is_lowercase = next_word
|
||||
.span
|
||||
.get_content(source)
|
||||
.first()
|
||||
.map(|c| c.is_lowercase())
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_lowercase
|
||||
&& (next_word.kind.is_upos(UPOS::NOUN) || next_word.kind.is_upos(UPOS::ADJ))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let range = self.map.lookup(0, matched_tokens, source)?.clone();
|
||||
let offending = matched_tokens.get(range.start)?;
|
||||
let offender_text = offending.span.get_content(source);
|
||||
|
||||
Some(Lint {
|
||||
span: offending.span,
|
||||
lint_kind: LintKind::Punctuation,
|
||||
suggestions: vec![Suggestion::replace_with_match_case_str(
|
||||
"it's",
|
||||
offender_text,
|
||||
)],
|
||||
message: "Use `it's` (short for \"it is\") before a proper noun in this construction."
|
||||
.to_owned(),
|
||||
priority: 31,
|
||||
})
|
||||
}
|
||||
}
|
||||
164
harper-core/src/linting/jealous_of.rs
Normal file
164
harper-core/src/linting/jealous_of.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use crate::{
|
||||
Token,
|
||||
expr::{Expr, SequenceExpr},
|
||||
linting::expr_linter::Chunk,
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
};
|
||||
|
||||
pub struct JealousOf {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for JealousOf {
|
||||
fn default() -> Self {
|
||||
let valid_object = |tok: &Token, _source: &[char]| {
|
||||
(tok.kind.is_nominal() || !tok.kind.is_verb())
|
||||
&& (tok.kind.is_oov() || tok.kind.is_nominal())
|
||||
&& !tok.kind.is_preposition()
|
||||
};
|
||||
|
||||
let pattern = SequenceExpr::default()
|
||||
.t_aco("jealous")
|
||||
.t_ws()
|
||||
.t_aco("from")
|
||||
.t_ws()
|
||||
.then_optional(SequenceExpr::default().then_determiner().t_ws())
|
||||
.then(valid_object);
|
||||
|
||||
Self {
|
||||
expr: Box::new(pattern),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for JealousOf {
|
||||
type Unit = Chunk;
|
||||
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let from_token = &tokens[2];
|
||||
|
||||
Some(Lint {
|
||||
span: from_token.span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
suggestions: vec![Suggestion::replace_with_match_case_str(
|
||||
"of",
|
||||
from_token.span.get_content(source),
|
||||
)],
|
||||
message: "Use `of` after `jealous`.".to_owned(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Encourages the standard preposition after `jealous`."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::JealousOf;
|
||||
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn replaces_basic_from() {
|
||||
assert_suggestion_result(
|
||||
"She was jealous from her sister's success.",
|
||||
JealousOf::default(),
|
||||
"She was jealous of her sister's success.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_optional_determiner() {
|
||||
assert_suggestion_result(
|
||||
"He grew jealous from the attention.",
|
||||
JealousOf::default(),
|
||||
"He grew jealous of the attention.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_pronoun_object() {
|
||||
assert_suggestion_result(
|
||||
"They became jealous from him.",
|
||||
JealousOf::default(),
|
||||
"They became jealous of him.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_oov_target() {
|
||||
assert_suggestion_result(
|
||||
"I'm jealous from Zybrix.",
|
||||
JealousOf::default(),
|
||||
"I'm jealous of Zybrix.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_uppercase_preposition() {
|
||||
assert_suggestion_result(
|
||||
"Jealous FROM his fame.",
|
||||
JealousOf::default(),
|
||||
"Jealous OF his fame.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_longer_phrase() {
|
||||
assert_suggestion_result(
|
||||
"They felt jealous from the sudden praise she received.",
|
||||
JealousOf::default(),
|
||||
"They felt jealous of the sudden praise she received.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_minimal_phrase() {
|
||||
assert_suggestion_result(
|
||||
"jealous from success",
|
||||
JealousOf::default(),
|
||||
"jealous of success",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_correct_usage() {
|
||||
assert_lint_count(
|
||||
"She was jealous of her sister's success.",
|
||||
JealousOf::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_other_preposition_sequence() {
|
||||
assert_lint_count(
|
||||
"They stayed jealous from within the fortress.",
|
||||
JealousOf::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_following_gerund() {
|
||||
assert_suggestion_result(
|
||||
"He was jealous from being ignored.",
|
||||
JealousOf::default(),
|
||||
"He was jealous of being ignored.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_numbers_after_from() {
|
||||
assert_lint_count(
|
||||
"She remained jealous from 2010 through 2015.",
|
||||
JealousOf::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
149
harper-core/src/linting/johns_hopkins.rs
Normal file
149
harper-core/src/linting/johns_hopkins.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use crate::{
|
||||
CharStringExt, Token,
|
||||
expr::{Expr, SequenceExpr},
|
||||
linting::expr_linter::Chunk,
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
};
|
||||
|
||||
pub struct JohnsHopkins {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for JohnsHopkins {
|
||||
fn default() -> Self {
|
||||
let expr = SequenceExpr::default()
|
||||
.then(|tok: &Token, src: &[char]| {
|
||||
tok.kind.is_proper_noun()
|
||||
&& tok.span.get_content(src).eq_ignore_ascii_case_str("john")
|
||||
})
|
||||
.t_ws()
|
||||
.then(|tok: &Token, src: &[char]| {
|
||||
tok.kind.is_proper_noun()
|
||||
&& tok
|
||||
.span
|
||||
.get_content(src)
|
||||
.eq_ignore_ascii_case_str("hopkins")
|
||||
});
|
||||
|
||||
Self {
|
||||
expr: Box::new(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for JohnsHopkins {
|
||||
type Unit = Chunk;
|
||||
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let span = matched_tokens.first()?.span;
|
||||
let template = span.get_content(source);
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
suggestions: vec![Suggestion::replace_with_match_case_str("Johns", template)],
|
||||
message: "Use `Johns Hopkins` for this name.".to_owned(),
|
||||
priority: 31,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Recommends the proper spelling `Johns Hopkins`."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::JohnsHopkins;
|
||||
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn corrects_university_reference() {
|
||||
assert_suggestion_result(
|
||||
"I applied to John Hopkins University last fall.",
|
||||
JohnsHopkins::default(),
|
||||
"I applied to Johns Hopkins University last fall.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_hospital_reference() {
|
||||
assert_suggestion_result(
|
||||
"She works at the John Hopkins hospital.",
|
||||
JohnsHopkins::default(),
|
||||
"She works at the Johns Hopkins hospital.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_standalone_name() {
|
||||
assert_suggestion_result(
|
||||
"We toured John Hopkins yesterday.",
|
||||
JohnsHopkins::default(),
|
||||
"We toured Johns Hopkins yesterday.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_lowercase_usage() {
|
||||
assert_suggestion_result(
|
||||
"I studied at john hopkins online.",
|
||||
JohnsHopkins::default(),
|
||||
"I studied at johns hopkins online.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_across_newline_whitespace() {
|
||||
assert_suggestion_result(
|
||||
"We met at John\nHopkins for lunch.",
|
||||
JohnsHopkins::default(),
|
||||
"We met at Johns\nHopkins for lunch.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_trailing_punctuation() {
|
||||
assert_suggestion_result(
|
||||
"I toured John Hopkins, and it was great.",
|
||||
JohnsHopkins::default(),
|
||||
"I toured Johns Hopkins, and it was great.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_before_hyphenated_unit() {
|
||||
assert_suggestion_result(
|
||||
"She joined the John Hopkins-affiliated lab.",
|
||||
JohnsHopkins::default(),
|
||||
"She joined the Johns Hopkins-affiliated lab.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_correct_spelling() {
|
||||
assert_lint_count(
|
||||
"Johns Hopkins University has a great program.",
|
||||
JohnsHopkins::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_apostrophized_form() {
|
||||
assert_lint_count(
|
||||
"John Hopkins's novel won awards.",
|
||||
JohnsHopkins::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_reversed_name_order() {
|
||||
assert_lint_count("Hopkins, John is a contact.", JohnsHopkins::default(), 0);
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ use super::and_in::AndIn;
|
||||
use super::and_the_like::AndTheLike;
|
||||
use super::another_thing_coming::AnotherThingComing;
|
||||
use super::another_think_coming::AnotherThinkComing;
|
||||
use super::apart_from::ApartFrom;
|
||||
use super::ask_no_preposition::AskNoPreposition;
|
||||
use super::avoid_curses::AvoidCurses;
|
||||
use super::back_in_the_day::BackInTheDay;
|
||||
@@ -44,6 +45,7 @@ use super::compound_subject_i::CompoundSubjectI;
|
||||
use super::confident::Confident;
|
||||
use super::correct_number_suffix::CorrectNumberSuffix;
|
||||
use super::criteria_phenomena::CriteriaPhenomena;
|
||||
use super::cure_for::CureFor;
|
||||
use super::currency_placement::CurrencyPlacement;
|
||||
use super::despite_of::DespiteOf;
|
||||
use super::didnt::Didnt;
|
||||
@@ -83,6 +85,8 @@ use super::interested_in::InterestedIn;
|
||||
use super::it_looks_like_that::ItLooksLikeThat;
|
||||
use super::its_contraction::ItsContraction;
|
||||
use super::its_possessive::ItsPossessive;
|
||||
use super::jealous_of::JealousOf;
|
||||
use super::johns_hopkins::JohnsHopkins;
|
||||
use super::left_right_hand::LeftRightHand;
|
||||
use super::less_worse::LessWorse;
|
||||
use super::let_to_do::LetToDo;
|
||||
@@ -140,6 +144,8 @@ use super::quote_spacing::QuoteSpacing;
|
||||
use super::redundant_additive_adverbs::RedundantAdditiveAdverbs;
|
||||
use super::regionalisms::Regionalisms;
|
||||
use super::repeated_words::RepeatedWords;
|
||||
use super::respond::Respond;
|
||||
use super::right_click::RightClick;
|
||||
use super::roller_skated::RollerSkated;
|
||||
use super::safe_to_save::SafeToSave;
|
||||
use super::save_to_safe::SaveToSafe;
|
||||
@@ -152,12 +158,14 @@ use super::single_be::SingleBe;
|
||||
use super::some_without_article::SomeWithoutArticle;
|
||||
use super::something_is::SomethingIs;
|
||||
use super::somewhat_something::SomewhatSomething;
|
||||
use super::soon_to_be::SoonToBe;
|
||||
use super::sought_after::SoughtAfter;
|
||||
use super::spaces::Spaces;
|
||||
use super::spell_check::SpellCheck;
|
||||
use super::spelled_numbers::SpelledNumbers;
|
||||
use super::split_words::SplitWords;
|
||||
use super::subject_pronoun::SubjectPronoun;
|
||||
use super::take_medicine::TakeMedicine;
|
||||
use super::that_than::ThatThan;
|
||||
use super::that_which::ThatWhich;
|
||||
use super::the_how_why::TheHowWhy;
|
||||
@@ -474,6 +482,7 @@ impl LintGroup {
|
||||
// Please maintain alphabetical order.
|
||||
// On *nix you can maintain sort order with `sort -t'(' -k2`
|
||||
insert_expr_rule!(APart, true);
|
||||
insert_expr_rule!(ApartFrom, true);
|
||||
insert_expr_rule!(AWhile, true);
|
||||
insert_expr_rule!(Addicting, true);
|
||||
insert_expr_rule!(AdjectiveDoubleDegree, true);
|
||||
@@ -506,6 +515,7 @@ impl LintGroup {
|
||||
insert_expr_rule!(Confident, true);
|
||||
insert_struct_rule!(CorrectNumberSuffix, true);
|
||||
insert_expr_rule!(CriteriaPhenomena, true);
|
||||
insert_expr_rule!(CureFor, true);
|
||||
insert_struct_rule!(CurrencyPlacement, true);
|
||||
insert_expr_rule!(Dashes, true);
|
||||
insert_expr_rule!(DespiteOf, true);
|
||||
@@ -539,6 +549,8 @@ impl LintGroup {
|
||||
insert_expr_rule!(IAmAgreement, true);
|
||||
insert_expr_rule!(IfWouldve, true);
|
||||
insert_expr_rule!(InterestedIn, true);
|
||||
insert_expr_rule!(JealousOf, true);
|
||||
insert_expr_rule!(JohnsHopkins, true);
|
||||
insert_expr_rule!(ItLooksLikeThat, true);
|
||||
insert_struct_rule!(ItsContraction, true);
|
||||
insert_expr_rule!(ItsPossessive, true);
|
||||
@@ -596,6 +608,8 @@ impl LintGroup {
|
||||
insert_struct_rule!(QuoteSpacing, true);
|
||||
insert_expr_rule!(RedundantAdditiveAdverbs, true);
|
||||
insert_struct_rule!(RepeatedWords, true);
|
||||
insert_expr_rule!(Respond, true);
|
||||
insert_expr_rule!(RightClick, true);
|
||||
insert_expr_rule!(RollerSkated, true);
|
||||
insert_expr_rule!(SafeToSave, true);
|
||||
insert_expr_rule!(SaveToSafe, true);
|
||||
@@ -603,6 +617,7 @@ impl LintGroup {
|
||||
insert_expr_rule!(ShootOneselfInTheFoot, true);
|
||||
insert_expr_rule!(SimplePastToPastParticiple, true);
|
||||
insert_expr_rule!(SinceDuration, true);
|
||||
insert_expr_rule!(SoonToBe, true);
|
||||
insert_expr_rule!(SingleBe, true);
|
||||
insert_expr_rule!(SomeWithoutArticle, true);
|
||||
insert_expr_rule!(SomethingIs, true);
|
||||
@@ -611,6 +626,7 @@ impl LintGroup {
|
||||
insert_struct_rule!(Spaces, true);
|
||||
insert_struct_rule!(SpelledNumbers, false);
|
||||
insert_expr_rule!(SplitWords, true);
|
||||
insert_expr_rule!(TakeMedicine, true);
|
||||
insert_struct_rule!(SubjectPronoun, true);
|
||||
insert_expr_rule!(ThatThan, true);
|
||||
insert_expr_rule!(ThatWhich, true);
|
||||
|
||||
@@ -17,6 +17,7 @@ mod and_in;
|
||||
mod and_the_like;
|
||||
mod another_thing_coming;
|
||||
mod another_think_coming;
|
||||
mod apart_from;
|
||||
mod ask_no_preposition;
|
||||
mod avoid_curses;
|
||||
mod back_in_the_day;
|
||||
@@ -37,6 +38,7 @@ mod compound_subject_i;
|
||||
mod confident;
|
||||
mod correct_number_suffix;
|
||||
mod criteria_phenomena;
|
||||
mod cure_for;
|
||||
mod currency_placement;
|
||||
mod dashes;
|
||||
mod despite_of;
|
||||
@@ -62,6 +64,7 @@ mod for_noun;
|
||||
mod free_predicate;
|
||||
mod friend_of_me;
|
||||
mod go_so_far_as_to;
|
||||
mod handful;
|
||||
mod have_pronoun;
|
||||
mod have_take_a_look;
|
||||
mod hedging;
|
||||
@@ -83,6 +86,8 @@ mod it_looks_like_that;
|
||||
mod it_would_be;
|
||||
mod its_contraction;
|
||||
mod its_possessive;
|
||||
mod jealous_of;
|
||||
mod johns_hopkins;
|
||||
mod left_right_hand;
|
||||
mod less_worse;
|
||||
mod let_to_do;
|
||||
@@ -150,6 +155,8 @@ mod quote_spacing;
|
||||
mod redundant_additive_adverbs;
|
||||
mod regionalisms;
|
||||
mod repeated_words;
|
||||
mod respond;
|
||||
mod right_click;
|
||||
mod roller_skated;
|
||||
mod safe_to_save;
|
||||
mod save_to_safe;
|
||||
@@ -162,6 +169,7 @@ mod single_be;
|
||||
mod some_without_article;
|
||||
mod something_is;
|
||||
mod somewhat_something;
|
||||
mod soon_to_be;
|
||||
mod sought_after;
|
||||
mod spaces;
|
||||
mod spell_check;
|
||||
@@ -169,6 +177,7 @@ mod spelled_numbers;
|
||||
mod split_words;
|
||||
mod subject_pronoun;
|
||||
mod suggestion;
|
||||
mod take_medicine;
|
||||
mod take_serious;
|
||||
mod that_than;
|
||||
mod that_which;
|
||||
|
||||
@@ -725,6 +725,13 @@ pub fn lint_group() -> LintGroup {
|
||||
"Corrects wrong variations of the idiomatic adjective `last-ditch`.",
|
||||
LintKind::Usage
|
||||
),
|
||||
"LastNight" => (
|
||||
["yesterday night"],
|
||||
["last night"],
|
||||
"The idiomatic phrase is `last night`.",
|
||||
"Flags `yesterday night` and suggests the standard phrasing `last night`.",
|
||||
LintKind::WordChoice
|
||||
),
|
||||
"LetAlone" => (
|
||||
["let along"],
|
||||
["let alone"],
|
||||
|
||||
@@ -1102,6 +1102,48 @@ fn correct_last_ditch_space() {
|
||||
);
|
||||
}
|
||||
|
||||
// LastNight
|
||||
#[test]
|
||||
fn corrects_yesterday_night_basic() {
|
||||
assert_suggestion_result(
|
||||
"I was there yesterday night.",
|
||||
lint_group(),
|
||||
"I was there last night.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_yesterday_night_capitalized() {
|
||||
assert_suggestion_result(
|
||||
"Yesterday night was fun.",
|
||||
lint_group(),
|
||||
"Last night was fun.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_yesterday_night_with_comma() {
|
||||
assert_suggestion_result(
|
||||
"Yesterday night, we watched a movie.",
|
||||
lint_group(),
|
||||
"Last night, we watched a movie.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_yesterday_night_across_newline() {
|
||||
assert_suggestion_result(
|
||||
"They left yesterday\nnight after the show.",
|
||||
lint_group(),
|
||||
"They left last night after the show.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_lint_for_last_night_phrase() {
|
||||
assert_lint_count("I remember last night clearly.", lint_group(), 0);
|
||||
}
|
||||
|
||||
// LetAlone
|
||||
#[test]
|
||||
fn let_along() {
|
||||
|
||||
180
harper-core/src/linting/respond.rs
Normal file
180
harper-core/src/linting/respond.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::Token;
|
||||
use crate::expr::{Expr, ExprMap, SequenceExpr};
|
||||
use crate::linting::expr_linter::Chunk;
|
||||
use crate::linting::{ExprLinter, Lint, LintKind, Suggestion};
|
||||
use crate::patterns::Word;
|
||||
|
||||
pub struct Respond {
|
||||
expr: Box<dyn Expr>,
|
||||
map: Arc<ExprMap<usize>>,
|
||||
}
|
||||
|
||||
impl Default for Respond {
|
||||
fn default() -> Self {
|
||||
let mut map = ExprMap::default();
|
||||
|
||||
let helper_verb = |tok: &Token, src: &[char]| {
|
||||
if tok.kind.is_auxiliary_verb() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !tok.kind.is_verb() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let lower = tok.span.get_content_string(src).to_lowercase();
|
||||
matches!(
|
||||
lower.as_str(),
|
||||
"do" | "did" | "does" | "won't" | "don't" | "didn't" | "doesn't"
|
||||
)
|
||||
};
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_nominal()
|
||||
.t_ws()
|
||||
.then(helper_verb)
|
||||
.t_ws()
|
||||
.then(Word::new("response")),
|
||||
4,
|
||||
);
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_nominal()
|
||||
.t_ws()
|
||||
.then(helper_verb)
|
||||
.t_ws()
|
||||
.then_adverb()
|
||||
.t_ws()
|
||||
.then(Word::new("response")),
|
||||
6,
|
||||
);
|
||||
|
||||
let map = Arc::new(map);
|
||||
|
||||
Self {
|
||||
expr: Box::new(map.clone()),
|
||||
map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for Respond {
|
||||
type Unit = Chunk;
|
||||
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let response_index = *self.map.lookup(0, matched_tokens, source)?;
|
||||
let response_token = matched_tokens.get(response_index)?;
|
||||
|
||||
Some(Lint {
|
||||
span: response_token.span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
suggestions: vec![Suggestion::replace_with_match_case_str(
|
||||
"respond",
|
||||
response_token.span.get_content(source),
|
||||
)],
|
||||
message: "Use the verb `respond` here.".to_owned(),
|
||||
priority: 40,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Flags uses of the noun `response` where the verb `respond` is needed after an auxiliary."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Respond;
|
||||
use crate::linting::tests::{assert_lint_count, assert_no_lints, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn fixes_will_response() {
|
||||
assert_suggestion_result(
|
||||
"He will response soon.",
|
||||
Respond::default(),
|
||||
"He will respond soon.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_can_response() {
|
||||
assert_suggestion_result(
|
||||
"They can response to the survey.",
|
||||
Respond::default(),
|
||||
"They can respond to the survey.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_did_not_response() {
|
||||
assert_suggestion_result(
|
||||
"I did not response yesterday.",
|
||||
Respond::default(),
|
||||
"I did not respond yesterday.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_might_quickly_response() {
|
||||
assert_suggestion_result(
|
||||
"She might quickly response to feedback.",
|
||||
Respond::default(),
|
||||
"She might quickly respond to feedback.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_wont_response() {
|
||||
assert_suggestion_result(
|
||||
"They won't response in time.",
|
||||
Respond::default(),
|
||||
"They won't respond in time.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_would_response() {
|
||||
assert_suggestion_result(
|
||||
"We would response if we could.",
|
||||
Respond::default(),
|
||||
"We would respond if we could.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_should_response() {
|
||||
assert_suggestion_result(
|
||||
"You should response politely.",
|
||||
Respond::default(),
|
||||
"You should respond politely.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_correct_respond() {
|
||||
assert_no_lints("Please respond when you can.", Respond::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_noun_use() {
|
||||
assert_no_lints("The response time was great.", Respond::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_question_subject() {
|
||||
assert_lint_count("Should response times be logged?", Respond::default(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_flag_response_as_object() {
|
||||
assert_no_lints("I have no response for that.", Respond::default());
|
||||
}
|
||||
}
|
||||
164
harper-core/src/linting/right_click.rs
Normal file
164
harper-core/src/linting/right_click.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
Token, TokenStringExt,
|
||||
expr::{Expr, ExprMap, SequenceExpr},
|
||||
linting::expr_linter::Chunk,
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
patterns::DerivedFrom,
|
||||
};
|
||||
|
||||
pub struct RightClick {
|
||||
expr: Box<dyn Expr>,
|
||||
map: Arc<ExprMap<usize>>,
|
||||
}
|
||||
|
||||
impl Default for RightClick {
|
||||
fn default() -> Self {
|
||||
let mut map = ExprMap::default();
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_word_set(&["right", "left", "middle"])
|
||||
.t_ws()
|
||||
.then(DerivedFrom::new_from_str("click")),
|
||||
0,
|
||||
);
|
||||
|
||||
let map = Arc::new(map);
|
||||
|
||||
Self {
|
||||
expr: Box::new(map.clone()),
|
||||
map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for RightClick {
|
||||
type Unit = Chunk;
|
||||
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let start_idx = *self.map.lookup(0, matched_tokens, source)?;
|
||||
let click_idx = matched_tokens.len().checked_sub(1)?;
|
||||
let span = matched_tokens.get(start_idx..=click_idx)?.span()?;
|
||||
let template = span.get_content(source);
|
||||
|
||||
let direction = matched_tokens.get(start_idx)?.span.get_content(source);
|
||||
let click = matched_tokens.get(click_idx)?.span.get_content(source);
|
||||
|
||||
let replacement: Vec<char> = direction
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(['-'])
|
||||
.chain(click.iter().copied())
|
||||
.collect();
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::Punctuation,
|
||||
suggestions: vec![Suggestion::replace_with_match_case(replacement, template)],
|
||||
message: "Hyphenate this mouse command.".to_owned(),
|
||||
priority: 40,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Hyphenates right-click style mouse commands."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::RightClick;
|
||||
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn hyphenates_basic_command() {
|
||||
assert_suggestion_result(
|
||||
"Right click the icon.",
|
||||
RightClick::default(),
|
||||
"Right-click the icon.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_with_preposition() {
|
||||
assert_suggestion_result(
|
||||
"Please right click on the link.",
|
||||
RightClick::default(),
|
||||
"Please right-click on the link.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_past_tense() {
|
||||
assert_suggestion_result(
|
||||
"They right clicked the submit button.",
|
||||
RightClick::default(),
|
||||
"They right-clicked the submit button.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_gerund() {
|
||||
assert_suggestion_result(
|
||||
"Right clicking the item highlights it.",
|
||||
RightClick::default(),
|
||||
"Right-clicking the item highlights it.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_plural_noun() {
|
||||
assert_suggestion_result(
|
||||
"Right clicks are tracked in the log.",
|
||||
RightClick::default(),
|
||||
"Right-clicks are tracked in the log.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"He RIGHT CLICKED the file.",
|
||||
RightClick::default(),
|
||||
"He RIGHT-CLICKED the file.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_left_click() {
|
||||
assert_suggestion_result(
|
||||
"Left click the checkbox.",
|
||||
RightClick::default(),
|
||||
"Left-click the checkbox.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_middle_click() {
|
||||
assert_suggestion_result(
|
||||
"Middle click to open in a new tab.",
|
||||
RightClick::default(),
|
||||
"Middle-click to open in a new tab.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_hyphenated_form() {
|
||||
assert_lint_count("Right-click the icon.", RightClick::default(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_unrelated_right_and_click() {
|
||||
assert_lint_count(
|
||||
"Click the right button to continue.",
|
||||
RightClick::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
240
harper-core/src/linting/soon_to_be.rs
Normal file
240
harper-core/src/linting/soon_to_be.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
Token, TokenKind, TokenStringExt,
|
||||
expr::{Expr, ExprMap, SequenceExpr},
|
||||
linting::expr_linter::Chunk,
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
patterns::NominalPhrase,
|
||||
};
|
||||
|
||||
pub struct SoonToBe {
|
||||
expr: Box<dyn Expr>,
|
||||
map: Arc<ExprMap<Range<usize>>>,
|
||||
}
|
||||
|
||||
impl Default for SoonToBe {
|
||||
fn default() -> Self {
|
||||
let mut map = ExprMap::default();
|
||||
|
||||
let soon_to_be = || {
|
||||
SequenceExpr::default()
|
||||
.t_aco("soon")
|
||||
.t_ws()
|
||||
.t_aco("to")
|
||||
.t_ws()
|
||||
.t_aco("be")
|
||||
};
|
||||
|
||||
let nominal_tail = || {
|
||||
SequenceExpr::default()
|
||||
.then_optional(SequenceExpr::default().then_one_or_more_adverbs().t_ws())
|
||||
.then(NominalPhrase)
|
||||
};
|
||||
|
||||
let hyphenated_number_modifier = || {
|
||||
SequenceExpr::default()
|
||||
.then_number()
|
||||
.then_hyphen()
|
||||
.then_nominal()
|
||||
.then_optional(SequenceExpr::default().then_hyphen().then_adjective())
|
||||
.t_ws()
|
||||
.then_nominal()
|
||||
};
|
||||
|
||||
let hyphenated_compound = || {
|
||||
SequenceExpr::default()
|
||||
.then_kind_any(&[TokenKind::is_word_like as fn(&TokenKind) -> bool])
|
||||
.then_hyphen()
|
||||
.then_nominal()
|
||||
};
|
||||
|
||||
let trailing_phrase = || {
|
||||
SequenceExpr::default().then_any_of(vec![
|
||||
Box::new(hyphenated_number_modifier()),
|
||||
Box::new(hyphenated_compound()),
|
||||
Box::new(nominal_tail()),
|
||||
])
|
||||
};
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_determiner()
|
||||
.t_ws()
|
||||
.then_seq(soon_to_be())
|
||||
.t_ws()
|
||||
.then_seq(trailing_phrase()),
|
||||
2..7,
|
||||
);
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_seq(soon_to_be())
|
||||
.t_ws()
|
||||
.then_seq(trailing_phrase()),
|
||||
0..5,
|
||||
);
|
||||
|
||||
let map = Arc::new(map);
|
||||
|
||||
Self {
|
||||
expr: Box::new(map.clone()),
|
||||
map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for SoonToBe {
|
||||
type Unit = Chunk;
|
||||
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let range = self.map.lookup(0, matched_tokens, source)?;
|
||||
let span = matched_tokens.get(range.start..range.end)?.span()?;
|
||||
let template = span.get_content(source);
|
||||
|
||||
let mut nominal_found = false;
|
||||
for tok in matched_tokens.iter().skip(range.end) {
|
||||
if tok.kind.is_whitespace() || tok.kind.is_hyphen() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if tok.kind.is_punctuation() {
|
||||
break;
|
||||
}
|
||||
|
||||
if tok.kind.is_nominal() {
|
||||
if tok.kind.is_preposition() {
|
||||
continue;
|
||||
} else {
|
||||
nominal_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !nominal_found {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::Miscellaneous,
|
||||
suggestions: vec![Suggestion::replace_with_match_case_str(
|
||||
"soon-to-be",
|
||||
template,
|
||||
)],
|
||||
message: "Use hyphens when `soon to be` modifies a noun.".to_owned(),
|
||||
priority: 31,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Hyphenates `soon-to-be` when it appears before a noun."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SoonToBe;
|
||||
use crate::linting::tests::{assert_lint_count, assert_no_lints, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn hyphenates_possessive_phrase() {
|
||||
assert_suggestion_result(
|
||||
"We met his soon to be boss at lunch.",
|
||||
SoonToBe::default(),
|
||||
"We met his soon-to-be boss at lunch.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_article_phrase() {
|
||||
assert_suggestion_result(
|
||||
"They toasted the soon to be couple.",
|
||||
SoonToBe::default(),
|
||||
"They toasted the soon-to-be couple.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_sentence_start() {
|
||||
assert_suggestion_result(
|
||||
"Soon to be parents filled the classroom.",
|
||||
SoonToBe::default(),
|
||||
"Soon-to-be parents filled the classroom.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_existing_hyphens() {
|
||||
assert_no_lints("We met his soon-to-be boss yesterday.", SoonToBe::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_non_adjectival_use() {
|
||||
assert_no_lints("The concert is soon to be over.", SoonToBe::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_with_adverb() {
|
||||
assert_suggestion_result(
|
||||
"Our soon to be newly married friends visited.",
|
||||
SoonToBe::default(),
|
||||
"Our soon-to-be newly married friends visited.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_hyphenated_number_phrase() {
|
||||
assert_suggestion_result(
|
||||
"Our soon to be 5-year-old son starts school.",
|
||||
SoonToBe::default(),
|
||||
"Our soon-to-be 5-year-old son starts school.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_in_law_phrase() {
|
||||
assert_suggestion_result(
|
||||
"She thanked her soon to be in-laws for hosting.",
|
||||
SoonToBe::default(),
|
||||
"She thanked her soon-to-be in-laws for hosting.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_future_event() {
|
||||
assert_suggestion_result(
|
||||
"We reserved space for our soon to be celebration.",
|
||||
SoonToBe::default(),
|
||||
"We reserved space for our soon-to-be celebration.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_misaligned_verb_chain() {
|
||||
assert_lint_count(
|
||||
"They will soon to be moving overseas.",
|
||||
SoonToBe::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenates_guest_example() {
|
||||
assert_suggestion_result(
|
||||
"I cooked for my soon to be guests.",
|
||||
SoonToBe::default(),
|
||||
"I cooked for my soon-to-be guests.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_rearranged_phrase() {
|
||||
assert_no_lints("We hope to soon be home.", SoonToBe::default());
|
||||
}
|
||||
}
|
||||
242
harper-core/src/linting/take_medicine.rs
Normal file
242
harper-core/src/linting/take_medicine.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
use crate::{
|
||||
Token,
|
||||
expr::{Expr, OwnedExprExt, SequenceExpr},
|
||||
linting::expr_linter::Chunk,
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
patterns::DerivedFrom,
|
||||
};
|
||||
|
||||
pub struct TakeMedicine {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for TakeMedicine {
|
||||
fn default() -> Self {
|
||||
let eat_verb = DerivedFrom::new_from_str("eat")
|
||||
.or(DerivedFrom::new_from_str("eats"))
|
||||
.or(DerivedFrom::new_from_str("ate"))
|
||||
.or(DerivedFrom::new_from_str("eating"))
|
||||
.or(DerivedFrom::new_from_str("eaten"));
|
||||
|
||||
let medication = DerivedFrom::new_from_str("antibiotic")
|
||||
.or(DerivedFrom::new_from_str("medicine"))
|
||||
.or(DerivedFrom::new_from_str("medication"))
|
||||
.or(DerivedFrom::new_from_str("pill"))
|
||||
.or(DerivedFrom::new_from_str("tablet"))
|
||||
.or(DerivedFrom::new_from_str("aspirin"))
|
||||
.or(DerivedFrom::new_from_str("paracetamol"));
|
||||
|
||||
let modifiers = SequenceExpr::default()
|
||||
.then_any_of(vec![
|
||||
Box::new(SequenceExpr::default().then_determiner()),
|
||||
Box::new(SequenceExpr::default().then_possessive_determiner()),
|
||||
Box::new(SequenceExpr::default().then_quantifier()),
|
||||
])
|
||||
.t_ws();
|
||||
|
||||
let adjectives = SequenceExpr::default().then_one_or_more_adjectives().t_ws();
|
||||
|
||||
let pattern = SequenceExpr::default()
|
||||
.then(eat_verb)
|
||||
.t_ws()
|
||||
.then_optional(modifiers)
|
||||
.then_optional(adjectives)
|
||||
.then(medication);
|
||||
|
||||
Self {
|
||||
expr: Box::new(pattern),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn replacement_for(
|
||||
verb: &Token,
|
||||
source: &[char],
|
||||
base: &str,
|
||||
third_person: &str,
|
||||
past: &str,
|
||||
past_participle: &str,
|
||||
progressive: &str,
|
||||
) -> Suggestion {
|
||||
let replacement = if verb.kind.is_verb_progressive_form() {
|
||||
progressive
|
||||
} else if verb.kind.is_verb_third_person_singular_present_form() {
|
||||
third_person
|
||||
} else if verb.kind.is_verb_past_participle_form() && !verb.kind.is_verb_simple_past_form() {
|
||||
past_participle
|
||||
} else if verb.kind.is_verb_simple_past_form() {
|
||||
past
|
||||
} else {
|
||||
base
|
||||
};
|
||||
|
||||
Suggestion::replace_with_match_case(
|
||||
replacement.chars().collect(),
|
||||
verb.span.get_content(source),
|
||||
)
|
||||
}
|
||||
|
||||
impl ExprLinter for TakeMedicine {
|
||||
type Unit = Chunk;
|
||||
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let verb = matched_tokens.first()?;
|
||||
let span = verb.span;
|
||||
|
||||
let suggestions = vec![
|
||||
replacement_for(verb, source, "take", "takes", "took", "taken", "taking"),
|
||||
replacement_for(
|
||||
verb,
|
||||
source,
|
||||
"swallow",
|
||||
"swallows",
|
||||
"swallowed",
|
||||
"swallowed",
|
||||
"swallowing",
|
||||
),
|
||||
];
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::Usage,
|
||||
suggestions,
|
||||
message: "Use a verb like `take` or `swallow` with medicine instead of `eat`."
|
||||
.to_string(),
|
||||
priority: 63,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Encourages pairing medicine-related nouns with verbs like `take` or `swallow` instead of `eat`."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::TakeMedicine;
|
||||
use crate::linting::tests::{
|
||||
assert_lint_count, assert_nth_suggestion_result, assert_suggestion_result,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn swaps_ate_antibiotics() {
|
||||
assert_suggestion_result(
|
||||
"I ate antibiotics for a week.",
|
||||
TakeMedicine::default(),
|
||||
"I took antibiotics for a week.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swaps_eat_medicine() {
|
||||
assert_suggestion_result(
|
||||
"You should eat the medicine now.",
|
||||
TakeMedicine::default(),
|
||||
"You should take the medicine now.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swaps_eats_medication() {
|
||||
assert_suggestion_result(
|
||||
"She eats medication daily.",
|
||||
TakeMedicine::default(),
|
||||
"She takes medication daily.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swaps_eating_medicines() {
|
||||
assert_suggestion_result(
|
||||
"Are you eating medicines for that illness?",
|
||||
TakeMedicine::default(),
|
||||
"Are you taking medicines for that illness?",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swaps_eaten_medication() {
|
||||
assert_suggestion_result(
|
||||
"He has eaten medication already.",
|
||||
TakeMedicine::default(),
|
||||
"He has taken medication already.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swaps_eat_pills() {
|
||||
assert_suggestion_result(
|
||||
"He ate the pills without water.",
|
||||
TakeMedicine::default(),
|
||||
"He took the pills without water.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swaps_eating_paracetamol() {
|
||||
assert_suggestion_result(
|
||||
"She is eating paracetamol for her headache.",
|
||||
TakeMedicine::default(),
|
||||
"She is taking paracetamol for her headache.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_possessive_modifier() {
|
||||
assert_suggestion_result(
|
||||
"Please eat my antibiotics.",
|
||||
TakeMedicine::default(),
|
||||
"Please take my antibiotics.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_adjectives() {
|
||||
assert_suggestion_result(
|
||||
"They ate the prescribed antibiotics.",
|
||||
TakeMedicine::default(),
|
||||
"They took the prescribed antibiotics.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_uppercase() {
|
||||
assert_suggestion_result(
|
||||
"Eat antibiotics with water.",
|
||||
TakeMedicine::default(),
|
||||
"Take antibiotics with water.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offers_swallow_alternative() {
|
||||
assert_nth_suggestion_result(
|
||||
"He ate the medication without water.",
|
||||
TakeMedicine::default(),
|
||||
"He swallowed the medication without water.",
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_correct_usage() {
|
||||
assert_lint_count(
|
||||
"She took antibiotics last winter.",
|
||||
TakeMedicine::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_unrelated_eat() {
|
||||
assert_lint_count(
|
||||
"They ate dinner after taking medicine.",
|
||||
TakeMedicine::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4910,7 +4910,7 @@ Suggest:
|
||||
|
||||
|
||||
|
||||
Lint: WordChoice (54 priority)
|
||||
Lint: Punctuation (54 priority)
|
||||
Message: |
|
||||
3495 | It was when curiosity about Gatsby was at its highest that the lights in his
|
||||
| ^~~ Use `it's` (short for `it has` or `it is`) here, not the possessive `its`.
|
||||
|
||||
Reference in New Issue
Block a user