From b4f24f967a9377fd996f55943b4c7ee759cc1e69 Mon Sep 17 00:00:00 2001 From: hippietrail Date: Sun, 11 Jan 2026 22:20:49 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20wip:=20do=20a=20mistake=E2=86=92make=20?= =?UTF-8?q?a=20mistake?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- harper-core/src/linting/do_mistake.rs | 267 ++++++++++++++++++++++++++ harper-core/src/linting/lint_group.rs | 2 + harper-core/src/linting/mod.rs | 44 +++++ 3 files changed, 313 insertions(+) create mode 100644 harper-core/src/linting/do_mistake.rs diff --git a/harper-core/src/linting/do_mistake.rs b/harper-core/src/linting/do_mistake.rs new file mode 100644 index 000000000..717dcbc98 --- /dev/null +++ b/harper-core/src/linting/do_mistake.rs @@ -0,0 +1,267 @@ +use crate::{ + Lint, Token, + expr::{Expr, FixedPhrase, SequenceExpr}, + linting::{ExprLinter, expr_linter::Chunk}, + patterns::WordSet, +}; + +pub struct DoMistake { + expr: Box, +} + +impl Default for DoMistake { + fn default() -> Self { + Self { + expr: Box::new( + SequenceExpr::word_set(&["do", "did", "does", "doing", "done"]) + .t_ws() + .then_any_of(vec![ + Box::new(WordSet::new(&[ + "a", "an", "the", "that", "these", "this", "those", "another", "many", + "my", "our", "your", "his", "her", "its", "their", + ])), + Box::new(FixedPhrase::from_phrase("a lot of")), + Box::new(FixedPhrase::from_phrase("lots of")), + Box::new(FixedPhrase::from_phrase("that kind of")), + Box::new(FixedPhrase::from_phrase("these kinds of")), + Box::new(FixedPhrase::from_phrase("this kind of")), + Box::new(FixedPhrase::from_phrase("those kinds of")), + Box::new(FixedPhrase::from_phrase("so many")), + Box::new(FixedPhrase::from_phrase("too many")), + Box::new(FixedPhrase::from_phrase("tons of")), + Box::new(FixedPhrase::from_phrase("tonnes of")), + ]) + .t_ws() + .then_word_set(&["mistake", "mistakes"]), + ), + } + } +} + +impl ExprLinter for DoMistake { + type Unit = Chunk; + + fn match_to_lint_with_context( + &self, + toks: &[Token], + src: &[char], + ctx: Option<(&[Token], &[Token])>, + ) -> Option { + eprintln!( + "💥 {}", + crate::linting::debug::format_lint_match(toks, ctx, src) + ); + None + } + + fn description(&self) -> &str { + "Corrects `do a mistake` to `make a mistake`" + } + + fn expr(&self) -> &dyn Expr { + &*self.expr + } +} + +#[cfg(test)] +mod tests { + use super::DoMistake; + use crate::linting::tests::{assert_lint_count, assert_no_lints}; + + #[test] + fn did_a_mistake() { + assert_lint_count( + "Hi, I did a mistake in my NGINX config file and so once the container is launched, it logs the error...", + DoMistake::default(), + 1, + ); + } + + #[test] + fn did_my_mistakes() { + assert_lint_count("Where i did my mistakes?", DoMistake::default(), 1); + } + + #[test] + fn did_several_mistakes() { + assert_lint_count( + "Maybe I did several mistakes, but I can only find a message about one?", + DoMistake::default(), + 1, + ); + } + + #[test] + fn did_some_mistakes() { + assert_lint_count( + "I made this program to learn goto use. but did some mistakes somewhere", + DoMistake::default(), + 1, + ); + } + + #[test] + fn did_that_mistake() { + assert_lint_count( + "and believe me, I did that mistake too", + DoMistake::default(), + 1, + ); + } + + #[test] + fn did_the_mistake() { + assert_lint_count( + "The issue describe is the person who did the mistake in the past & that same person is NOW correcting other people", + DoMistake::default(), + 1, + ); + } + + #[test] + fn did_this_mistake() { + assert_lint_count( + "Are there famous mathematicians who did this mistake?", + DoMistake::default(), + 1, + ); + } + + #[test] + fn do_many_mistakes() { + assert_lint_count( + "I observed that my coworkers do many mistakes using the field calculator", + DoMistake::default(), + 1, + ); + } + + #[test] + fn do_mistake() { + assert_lint_count( + "If you do a mistake that causes alot of problems, please use the command to redo", + DoMistake::default(), + 1, + ); + } + + #[test] + fn do_some_mistakes() { + assert_lint_count( + "so probably if my colleagues do some mistakes I tend to learn them as well", + DoMistake::default(), + 1, + ); + } + + #[test] + fn do_the_mistake() { + assert_lint_count( + "do I need to explicitly mention that I did not do the mistake to do not lose the point?", + DoMistake::default(), + 1, + ); + } + + #[test] + fn do_this_mistake() { + assert_lint_count( + "I barely remember any frontend developer that wouldn't do this mistake at least once.", + DoMistake::default(), + 1, + ); + } + + #[test] + fn do_this_mistakes() { + assert_lint_count( + "I do this mistakes to check the command detekt with type resolution", + DoMistake::default(), + 1, + ); + } + + #[test] + fn do_those_mistakes() { + assert_lint_count( + "An experienced developer could do those mistakes as well", + DoMistake::default(), + 1, + ); + } + + #[test] + fn doing_a_mistake() { + assert_lint_count( + "Here at work, a colleague asked if we were doing a mistake by using the ReactDOM.renderToStaticMarkup on the client side.", + DoMistake::default(), + 1, + ); + } + + #[test] + fn doing_several_mistakes() { + assert_lint_count( + "I realized I was doing several mistakes", + DoMistake::default(), + 1, + ); + } + + #[test] + fn doing_the_mistake() { + assert_lint_count("where am i doing the mistake?", DoMistake::default(), 1); + } + + #[test] + fn done_some_mistake() { + assert_lint_count( + "Might be I have done some mistake, that I do not know.", + DoMistake::default(), + 1, + ); + } + + #[test] + fn done_this_mistake() { + assert_lint_count( + "how many more users have done this mistake?", + DoMistake::default(), + 1, + ); + } + + // False positives + + #[test] + fn dont_flag_when_does_a_mistake() { + assert_no_lints( + "When does a mistake become standard usage? ", + DoMistake::default(), + ); + } + + #[test] + fn dont_flag_did_that_mistake_verb() { + assert_no_lints( + "Did that mistake occurred before or after the day 2 backup?", + DoMistake::default(), + ); + } + + #[test] + fn dont_flag_does_this_mistake_verb() { + assert_no_lints( + "Does this mistake invalidate your thesis?", + DoMistake::default(), + ); + } + + #[test] + fn dont_flag_does_the_mistake_verb() { + assert_no_lints( + "Does the mistake change the meaning of the quotation?", + DoMistake::default(), + ); + } +} diff --git a/harper-core/src/linting/lint_group.rs b/harper-core/src/linting/lint_group.rs index c85b9b512..4bf923df1 100644 --- a/harper-core/src/linting/lint_group.rs +++ b/harper-core/src/linting/lint_group.rs @@ -53,6 +53,7 @@ use super::despite_of::DespiteOf; use super::didnt::Didnt; use super::discourse_markers::DiscourseMarkers; use super::disjoint_prefixes::DisjointPrefixes; +use super::do_mistake::DoMistake; use super::dot_initialisms::DotInitialisms; use super::double_click::DoubleClick; use super::double_modal::DoubleModal; @@ -537,6 +538,7 @@ impl LintGroup { insert_expr_rule!(DespiteOf, true); insert_expr_rule!(Didnt, true); insert_struct_rule!(DiscourseMarkers, true); + insert_expr_rule!(DoMistake, true); insert_expr_rule!(DotInitialisms, true); insert_expr_rule!(DoubleClick, true); insert_expr_rule!(DoubleModal, true); diff --git a/harper-core/src/linting/mod.rs b/harper-core/src/linting/mod.rs index e633bde02..23a39634e 100644 --- a/harper-core/src/linting/mod.rs +++ b/harper-core/src/linting/mod.rs @@ -48,6 +48,7 @@ mod determiner_without_noun; mod didnt; mod discourse_markers; mod disjoint_prefixes; +mod do_mistake; mod dot_initialisms; mod double_click; mod double_modal; @@ -265,6 +266,49 @@ where } } +pub mod debug { + use crate::Token; + + /// Formats a lint match with surrounding context for debug output. + /// + /// The function takes the same `matched_tokens` and `source`, and `context` parameters + /// passed to `[match_to_lint_with_context]`. + /// + /// # Arguments + /// * `log` - `matched_tokens` + /// * `ctx` - `context`, or `None` if calling from `[match_to_lint]` + /// * `src` - `source` from `[match_to_lint]` / `[match_to_lint_with_context]` + /// + /// # Returns + /// A string with ANSI escape codes where: + /// - Context tokens are dimmed before and after the matched tokens in normal weight. + /// - Markup and formatting text hidden in whitespace tokens is filtered out. + pub fn format_lint_match( + log: &[Token], + ctx: Option<(&[Token], &[Token])>, + src: &[char], + ) -> String { + let fmt = |tokens: &[Token]| { + tokens + .iter() + .filter(|t| !t.kind.is_unlintable()) + .map(|t| t.span.get_content_string(src)) + .collect::() + }; + + if let Some((pro, epi)) = ctx { + format!( + "\x1b[2m{}\x1b[0m{}\x1b[2m{}\x1b[0m", + fmt(pro), + fmt(log), + fmt(epi) + ) + } else { + fmt(log) + } + } +} + #[cfg(test)] pub mod tests { use crate::parsers::Markdown;