feat(core): more rules (#2090)

This commit is contained in:
Elijah Potter 2025-10-22 11:50:13 -06:00 committed by GitHub
parent 353de58647
commit 59e4a8fec4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 462 additions and 0 deletions

View file

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

View file

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

View file

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

View file

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

View 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,
);
}
}

View 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.",
);
}
}