mirror of
https://github.com/Automattic/harper.git
synced 2025-12-23 08:48:15 +00:00
feat(core): more rules (#2090)
This commit is contained in:
parent
353de58647
commit
59e4a8fec4
6 changed files with 462 additions and 0 deletions
|
|
@ -110,12 +110,14 @@ use super::quote_spacing::QuoteSpacing;
|
|||
use super::redundant_additive_adverbs::RedundantAdditiveAdverbs;
|
||||
use super::regionalisms::Regionalisms;
|
||||
use super::repeated_words::RepeatedWords;
|
||||
use super::roller_skated::RollerSkated;
|
||||
use super::save_to_safe::SaveToSafe;
|
||||
use super::semicolon_apostrophe::SemicolonApostrophe;
|
||||
use super::sentence_capitalization::SentenceCapitalization;
|
||||
use super::shoot_oneself_in_the_foot::ShootOneselfInTheFoot;
|
||||
use super::simple_past_to_past_participle::SimplePastToPastParticiple;
|
||||
use super::since_duration::SinceDuration;
|
||||
use super::some_without_article::SomeWithoutArticle;
|
||||
use super::something_is::SomethingIs;
|
||||
use super::somewhat_something::SomewhatSomething;
|
||||
use super::sought_after::SoughtAfter;
|
||||
|
|
@ -526,6 +528,7 @@ impl LintGroup {
|
|||
insert_struct_rule!(QuoteSpacing, true);
|
||||
insert_expr_rule!(RedundantAdditiveAdverbs, true);
|
||||
insert_struct_rule!(RepeatedWords, true);
|
||||
insert_expr_rule!(RollerSkated, true);
|
||||
insert_struct_rule!(SaveToSafe, true);
|
||||
insert_expr_rule!(SemicolonApostrophe, true);
|
||||
insert_expr_rule!(ShootOneselfInTheFoot, true);
|
||||
|
|
@ -534,6 +537,7 @@ impl LintGroup {
|
|||
insert_expr_rule!(SomethingIs, true);
|
||||
insert_expr_rule!(SomewhatSomething, true);
|
||||
insert_expr_rule!(SoughtAfter, true);
|
||||
insert_expr_rule!(SomeWithoutArticle, true);
|
||||
insert_struct_rule!(Spaces, true);
|
||||
insert_struct_rule!(SpelledNumbers, false);
|
||||
insert_expr_rule!(ThatThan, true);
|
||||
|
|
|
|||
|
|
@ -123,12 +123,14 @@ mod quote_spacing;
|
|||
mod redundant_additive_adverbs;
|
||||
mod regionalisms;
|
||||
mod repeated_words;
|
||||
mod roller_skated;
|
||||
mod save_to_safe;
|
||||
mod semicolon_apostrophe;
|
||||
mod sentence_capitalization;
|
||||
mod shoot_oneself_in_the_foot;
|
||||
mod simple_past_to_past_participle;
|
||||
mod since_duration;
|
||||
mod some_without_article;
|
||||
mod something_is;
|
||||
mod somewhat_something;
|
||||
mod sought_after;
|
||||
|
|
@ -269,12 +271,14 @@ pub use quote_spacing::QuoteSpacing;
|
|||
pub use redundant_additive_adverbs::RedundantAdditiveAdverbs;
|
||||
pub use regionalisms::Regionalisms;
|
||||
pub use repeated_words::RepeatedWords;
|
||||
pub use roller_skated::RollerSkated;
|
||||
pub use save_to_safe::SaveToSafe;
|
||||
pub use semicolon_apostrophe::SemicolonApostrophe;
|
||||
pub use sentence_capitalization::SentenceCapitalization;
|
||||
pub use shoot_oneself_in_the_foot::ShootOneselfInTheFoot;
|
||||
pub use simple_past_to_past_participle::SimplePastToPastParticiple;
|
||||
pub use since_duration::SinceDuration;
|
||||
pub use some_without_article::SomeWithoutArticle;
|
||||
pub use something_is::SomethingIs;
|
||||
pub use somewhat_something::SomewhatSomething;
|
||||
pub use sought_after::SoughtAfter;
|
||||
|
|
|
|||
|
|
@ -1096,6 +1096,13 @@ pub fn lint_group() -> LintGroup {
|
|||
"Corrects `to the manor born` to `to the manner born`, ensuring the intended meaning of being naturally suited to a way of life.",
|
||||
LintKind::Eggcorn
|
||||
),
|
||||
"TongueInCheek" => (
|
||||
["tongue and cheek"],
|
||||
["tongue in cheek"],
|
||||
"Use `tongue in cheek` for the idiom.",
|
||||
"Corrects the idiom when `and` replaces the needed preposition.",
|
||||
LintKind::WordChoice
|
||||
),
|
||||
"Towards" => (
|
||||
["to towards"],
|
||||
["towards"],
|
||||
|
|
|
|||
|
|
@ -1440,6 +1440,93 @@ fn correct_to_a_great_length() {
|
|||
// ToTheMannerBorn
|
||||
// -none-
|
||||
|
||||
// TongueInCheek
|
||||
#[test]
|
||||
fn tongue_and_cheek_plain() {
|
||||
assert_suggestion_result(
|
||||
"The remark was entirely tongue and cheek.",
|
||||
lint_group(),
|
||||
"The remark was entirely tongue in cheek.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tongue_and_cheek_with_article() {
|
||||
assert_suggestion_result(
|
||||
"It was a tongue and cheek response.",
|
||||
lint_group(),
|
||||
"It was a tongue in cheek response.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tongue_and_cheek_with_comma() {
|
||||
assert_suggestion_result(
|
||||
"He delivered it tongue and cheek, expecting a laugh.",
|
||||
lint_group(),
|
||||
"He delivered it tongue in cheek, expecting a laugh.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tongue_and_cheek_in_quotes() {
|
||||
assert_suggestion_result(
|
||||
"\"tongue and cheek\" jokes are tough to read.",
|
||||
lint_group(),
|
||||
"\"tongue in cheek\" jokes are tough to read.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tongue_and_cheek_all_caps() {
|
||||
assert_suggestion_result(
|
||||
"Their tone was TONGUE AND CHEEK all night.",
|
||||
lint_group(),
|
||||
"Their tone was TONGUE IN CHEEK all night.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tongue_and_cheek_capitalized() {
|
||||
assert_suggestion_result(
|
||||
"Tongue and cheek banter kept the meeting light.",
|
||||
lint_group(),
|
||||
"Tongue in cheek banter kept the meeting light.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tongue_and_cheek_in_parentheses() {
|
||||
assert_suggestion_result(
|
||||
"Her note (totally tongue and cheek) made us smile.",
|
||||
lint_group(),
|
||||
"Her note (totally tongue in cheek) made us smile.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tongue_and_cheek_question() {
|
||||
assert_suggestion_result(
|
||||
"Was that tongue and cheek or sincere?",
|
||||
lint_group(),
|
||||
"Was that tongue in cheek or sincere?",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tongue_in_cheek_is_allowed() {
|
||||
assert_lint_count(
|
||||
"Their comments were deliberately tongue in cheek.",
|
||||
lint_group(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tongue_in_cheek_hyphenated_is_allowed() {
|
||||
assert_lint_count("That was a tongue-in-cheek reply.", lint_group(), 0);
|
||||
}
|
||||
|
||||
// Towards
|
||||
// -none-
|
||||
|
||||
|
|
|
|||
207
harper-core/src/linting/roller_skated.rs
Normal file
207
harper-core/src/linting/roller_skated.rs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
Token, TokenKind, TokenStringExt,
|
||||
expr::{AnchorStart, Expr, ExprMap, SequenceExpr},
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
};
|
||||
|
||||
/// Suggests hyphenating the past tense of `roller-skate`.
|
||||
pub struct RollerSkated {
|
||||
expr: Box<dyn Expr>,
|
||||
map: Arc<ExprMap<usize>>,
|
||||
}
|
||||
|
||||
impl RollerSkated {
|
||||
fn roller_pair() -> SequenceExpr {
|
||||
SequenceExpr::default()
|
||||
.t_aco("roller")
|
||||
.t_ws()
|
||||
.t_aco("skated")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RollerSkated {
|
||||
fn default() -> Self {
|
||||
let mut map = ExprMap::default();
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_kind_is_but_is_not(
|
||||
|kind| matches!(kind, TokenKind::Word(_)),
|
||||
|kind| kind.is_determiner(),
|
||||
)
|
||||
.then_whitespace()
|
||||
.then_seq(Self::roller_pair()),
|
||||
2,
|
||||
);
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_punctuation()
|
||||
.then_whitespace()
|
||||
.then_seq(Self::roller_pair()),
|
||||
2,
|
||||
);
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then_punctuation()
|
||||
.then_seq(Self::roller_pair()),
|
||||
1,
|
||||
);
|
||||
|
||||
map.insert(
|
||||
SequenceExpr::default()
|
||||
.then(AnchorStart)
|
||||
.then_seq(Self::roller_pair()),
|
||||
0,
|
||||
);
|
||||
|
||||
let map = Arc::new(map);
|
||||
|
||||
Self {
|
||||
expr: Box::new(map.clone()),
|
||||
map,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for RollerSkated {
|
||||
fn expr(&self) -> &dyn Expr {
|
||||
self.expr.as_ref()
|
||||
}
|
||||
|
||||
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
|
||||
let roller_idx = *self.map.lookup(0, matched_tokens, source)?;
|
||||
let skated_idx = roller_idx.checked_add(2)?;
|
||||
let window = matched_tokens.get(roller_idx..=skated_idx)?;
|
||||
let span = window.span()?;
|
||||
let original = span.get_content(source);
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::Punctuation,
|
||||
suggestions: vec![Suggestion::replace_with_match_case(
|
||||
"roller-skated".chars().collect(),
|
||||
original,
|
||||
)],
|
||||
message: "Hyphenate this verb as `roller-skated`.".to_owned(),
|
||||
priority: 40,
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Encourages hyphenating the past tense of `roller-skate`."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::RollerSkated;
|
||||
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
|
||||
|
||||
#[test]
|
||||
fn corrects_basic_sentence() {
|
||||
assert_suggestion_result(
|
||||
"He roller skated down the hill.",
|
||||
RollerSkated::default(),
|
||||
"He roller-skated down the hill.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_adverb() {
|
||||
assert_suggestion_result(
|
||||
"They roller skated quickly around the rink.",
|
||||
RollerSkated::default(),
|
||||
"They roller-skated quickly around the rink.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_auxiliary() {
|
||||
assert_suggestion_result(
|
||||
"She had roller skated there before.",
|
||||
RollerSkated::default(),
|
||||
"She had roller-skated there before.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_contraction() {
|
||||
assert_suggestion_result(
|
||||
"They'd roller skated all night.",
|
||||
RollerSkated::default(),
|
||||
"They'd roller-skated all night.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_caps() {
|
||||
assert_suggestion_result(
|
||||
"They ROLLER SKATED yesterday.",
|
||||
RollerSkated::default(),
|
||||
"They ROLLER-SKATED yesterday.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_in_quotes() {
|
||||
assert_suggestion_result(
|
||||
"\"We roller skated together,\" she said.",
|
||||
RollerSkated::default(),
|
||||
"\"We roller-skated together,\" she said.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_across_line_break() {
|
||||
assert_suggestion_result(
|
||||
"We\nroller skated whenever we could.",
|
||||
RollerSkated::default(),
|
||||
"We\nroller-skated whenever we could.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_with_trailing_punctuation() {
|
||||
assert_suggestion_result(
|
||||
"He roller skated, laughed, and waved.",
|
||||
RollerSkated::default(),
|
||||
"He roller-skated, laughed, and waved.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrects_without_space_after_punctuation() {
|
||||
assert_suggestion_result(
|
||||
"He roller skated,laughed, and waved.",
|
||||
RollerSkated::default(),
|
||||
"He roller-skated,laughed, and waved.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_hyphenated_form() {
|
||||
assert_lint_count("They roller-skated yesterday.", RollerSkated::default(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_subject_named_roller() {
|
||||
assert_lint_count(
|
||||
"The roller skated across the stage.",
|
||||
RollerSkated::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allows_other_compounds() {
|
||||
assert_lint_count(
|
||||
"Their roller skating routine impressed everyone.",
|
||||
RollerSkated::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
153
harper-core/src/linting/some_without_article.rs
Normal file
153
harper-core/src/linting/some_without_article.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
use crate::{
|
||||
Token, TokenStringExt,
|
||||
expr::{Expr, SequenceExpr},
|
||||
linting::{ExprLinter, Lint, LintKind, Suggestion},
|
||||
};
|
||||
|
||||
pub struct SomeWithoutArticle {
|
||||
expr: Box<dyn Expr>,
|
||||
}
|
||||
|
||||
impl Default for SomeWithoutArticle {
|
||||
fn default() -> Self {
|
||||
let expr = SequenceExpr::default()
|
||||
.then_any_capitalization_of("the")
|
||||
.t_ws()
|
||||
.then_any_capitalization_of("some");
|
||||
|
||||
Self {
|
||||
expr: Box::new(expr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExprLinter for SomeWithoutArticle {
|
||||
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.span()?;
|
||||
let template = span.get_content(source);
|
||||
let some_chars = matched_tokens.last()?.span.get_content(source);
|
||||
|
||||
let suggestions = vec![
|
||||
Suggestion::ReplaceWith(some_chars.to_vec()),
|
||||
Suggestion::replace_with_match_case("the same".chars().collect(), template),
|
||||
];
|
||||
|
||||
Some(Lint {
|
||||
span,
|
||||
lint_kind: LintKind::WordChoice,
|
||||
message:
|
||||
"Use `some` on its own here, or switch to `the same` if that was the intention."
|
||||
.to_owned(),
|
||||
suggestions,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Detects the redundant article in front of `some` and suggests more natural phrasing."
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::linting::tests::{
|
||||
assert_lint_count, assert_nth_suggestion_result, assert_suggestion_result,
|
||||
};
|
||||
|
||||
use super::SomeWithoutArticle;
|
||||
|
||||
#[test]
|
||||
fn fixes_simple_lowercase() {
|
||||
assert_suggestion_result(
|
||||
"We interviewed the some candidates today.",
|
||||
SomeWithoutArticle::default(),
|
||||
"We interviewed some candidates today.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixes_sentence_case() {
|
||||
assert_suggestion_result(
|
||||
"The Some volunteers arrived early.",
|
||||
SomeWithoutArticle::default(),
|
||||
"Some volunteers arrived early.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_uppercase_block() {
|
||||
assert_suggestion_result(
|
||||
"THE SOME OPTIONS WERE LISTED.",
|
||||
SomeWithoutArticle::default(),
|
||||
"SOME OPTIONS WERE LISTED.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn second_suggestion_produces_the_same() {
|
||||
assert_nth_suggestion_result(
|
||||
"We kept the some approach from last year.",
|
||||
SomeWithoutArticle::default(),
|
||||
"We kept the same approach from last year.",
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_already_correct_some() {
|
||||
assert_lint_count(
|
||||
"We interviewed some candidates today.",
|
||||
SomeWithoutArticle::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_the_same() {
|
||||
assert_lint_count(
|
||||
"We kept the same approach from last year.",
|
||||
SomeWithoutArticle::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_the_something() {
|
||||
assert_lint_count(
|
||||
"We interviewed the something else entirely.",
|
||||
SomeWithoutArticle::default(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn works_before_comma() {
|
||||
assert_suggestion_result(
|
||||
"They reviewed the some, then finalized the list.",
|
||||
SomeWithoutArticle::default(),
|
||||
"They reviewed some, then finalized the list.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn works_before_possessive_noun() {
|
||||
assert_suggestion_result(
|
||||
"The report praised the some team's effort.",
|
||||
SomeWithoutArticle::default(),
|
||||
"The report praised some team's effort.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_line_break_spacing() {
|
||||
assert_suggestion_result(
|
||||
"We invited the some\nartists to perform.",
|
||||
SomeWithoutArticle::default(),
|
||||
"We invited some\nartists to perform.",
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue