diff --git a/harper-core/src/linting/have_the_nerve.rs b/harper-core/src/linting/have_the_nerve.rs new file mode 100644 index 000000000..41847987f --- /dev/null +++ b/harper-core/src/linting/have_the_nerve.rs @@ -0,0 +1,192 @@ +use crate::{ + CharStringExt, Lint, Token, TokenStringExt, + expr::{Expr, SequenceExpr}, + linting::{ExprLinter, Suggestion, debug::format_lint_match, expr_linter::Chunk}, +}; + +pub struct HaveTheNerve { + expr: Box, +} + +impl Default for HaveTheNerve { + fn default() -> Self { + Self { + expr: Box::new( + SequenceExpr::word_set(&["had", "have", "having", "has"]) + .t_ws() + .t_aco("the") + .t_ws() + .then_word_set(&["nerve", "nerves"]) + .t_ws() + .t_aco("to"), + ), + } + } +} + +impl ExprLinter for HaveTheNerve { + type Unit = Chunk; + + fn description(&self) -> &str { + "Flags `have the nerves` used for audacity or `have the nerve` used for patience." + } + + fn expr(&self) -> &dyn Expr { + self.expr.as_ref() + } + + fn match_to_lint_with_context( + &self, + toks: &[Token], + src: &[char], + ctx: Option<(&[Token], &[Token])>, + ) -> Option { + let apprehensive_emoji = "😰"; + let rude_emoji = "😠"; + + // I can't believe they had the nerve to do that! -> rude emoji + // Oh I don't have the nerves for that kind of thing! -> apprehensive emoji + let emoji = if toks + .get_rel(-3)? + .span + .get_content(src) + .ends_with_ignore_ascii_case_chars(&['s']) + { + apprehensive_emoji + } else { + rude_emoji + }; + + eprintln!("{emoji} {}", format_lint_match(toks, ctx, src)); + None + } +} + +#[cfg(test)] +mod tests { + use super::HaveTheNerve; + use crate::linting::tests::{assert_lint_count, assert_no_lints}; + + // Flag audacity with plural "nerves" + + #[test] + fn flag_the_nerves_to_ask() { + assert_lint_count( + "How the hell he has the nerves to ask more than triple?!", + HaveTheNerve::default(), + 1, + ); + } + + #[test] + fn flag_the_nerves_to_tell_us() { + assert_lint_count( + "And yet you have the nerves to tell us that \"there is no EVE community\".", + HaveTheNerve::default(), + 1, + ); + } + + #[test] + fn flag_she_had_the_nerves() { + assert_lint_count( + "She had the nerves to t-pose on me.", + HaveTheNerve::default(), + 1, + ); + } + + #[test] + fn flag_has_the_nerves_to_say() { + assert_lint_count( + "Nagumo has the Nerves to say this when he cracks the most unfunniest joke", + HaveTheNerve::default(), + 1, + ); + } + + #[test] + fn flag_had_the_nerves_to_mock() { + assert_lint_count( + "That Frank even had the nerves to mock us towards the end", + HaveTheNerve::default(), + 1, + ); + } + + #[test] + fn flag_they_still_have_the_nerve() { + assert_lint_count( + "and they still have the nerves to say that \"Carti carried\"", + HaveTheNerve::default(), + 1, + ); + } + + #[test] + fn flag_the_nerves_to_thumbs_down() { + assert_lint_count( + "Bro had the nerves to thumbs down afterwards.", + HaveTheNerve::default(), + 1, + ); + } + + // Dont' flag lacking courage or patience + + #[test] + fn dont_flag_wont_have_the_nerves() { + assert_no_lints( + "But it's likely that you won't have the nerves to change the whole DAO layer afterwards", + HaveTheNerve::default(), + ); + } + + #[test] + fn dont_flag_might_have_the_nerves() { + assert_lint_count( + "Nevermind, someone might have the nerves to go through it", + HaveTheNerve::default(), + 1, + ); + } + + #[test] + fn dont_flag_have_need_to_have_the_nerves() { + assert_no_lints( + "The only thing you need is to have the nerves to embrace OpenGL.", + HaveTheNerve::default(), + ); + } + + #[test] + fn dont_flag_didnt_had_the_nerves() { + assert_no_lints("i didnt had the nerves to try ZF2", HaveTheNerve::default()); + } + + #[test] + fn dont_flag_has_the_nerves_to_help() { + assert_no_lints( + "It surely still needs work, and possibly someone has the nerves to help editing it.", + HaveTheNerve::default(), + ); + } + + #[test] + fn dont_flag_the_nerves_to_see_it_through() { + assert_no_lints( + "This one is relatively simple just to see if I had the nerves to see it through to the end.", + HaveTheNerve::default(), + ); + } + + // Don't flag literal uses + + #[test] + fn dont_flag_the_nerves_to_their_ears() { + assert_no_lints( + "Some dressage horses have the nerves to their ears blocked/cut", + HaveTheNerve::default(), + ); + } +} diff --git a/harper-core/src/linting/lint_group.rs b/harper-core/src/linting/lint_group.rs index c85b9b512..278c3fb47 100644 --- a/harper-core/src/linting/lint_group.rs +++ b/harper-core/src/linting/lint_group.rs @@ -79,6 +79,7 @@ use super::good_at::GoodAt; use super::handful::Handful; use super::have_pronoun::HavePronoun; use super::have_take_a_look::HaveTakeALook; +use super::have_the_nerve::HaveTheNerve; use super::hedging::Hedging; use super::hello_greeting::HelloGreeting; use super::hereby::Hereby; @@ -561,6 +562,7 @@ impl LintGroup { insert_expr_rule!(GoodAt, true); insert_expr_rule!(Handful, true); insert_expr_rule!(HavePronoun, true); + insert_expr_rule!(HaveTheNerve, true); insert_expr_rule!(Hedging, true); insert_expr_rule!(HelloGreeting, true); insert_expr_rule!(Hereby, true); diff --git a/harper-core/src/linting/mod.rs b/harper-core/src/linting/mod.rs index e633bde02..39669cb8d 100644 --- a/harper-core/src/linting/mod.rs +++ b/harper-core/src/linting/mod.rs @@ -74,6 +74,7 @@ mod good_at; mod handful; mod have_pronoun; mod have_take_a_look; +mod have_the_nerve; mod hedging; mod hello_greeting; mod hereby; @@ -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; diff --git a/harper-core/src/token_string_ext.rs b/harper-core/src/token_string_ext.rs index 5a510e5f0..e3e69e522 100644 --- a/harper-core/src/token_string_ext.rs +++ b/harper-core/src/token_string_ext.rs @@ -90,6 +90,43 @@ pub trait TokenStringExt: private::Sealed { create_decl_for!(word_like); create_decl_for!(heading_start); + /// Get a reference to a token by index, with negative numbers counting from the end. + /// + /// # Examples + /// ``` + /// # use harper_core::{Token, TokenStringExt}; + /// # fn main() { + /// let tokens = []; // In a real test, this would be actual tokens + /// let _last = tokens.get_rel(-1); // Gets the last token + /// let _third_last = tokens.get_rel(-3); // Gets the third last token + /// let _first = tokens.get_rel(0); // Gets the first token + /// # } + /// ``` + /// + /// # Returns + /// + /// * `Some(&Token)` - If the index is in bounds + /// * `None` - If the index is out of bounds + fn get_rel(&self, index: isize) -> Option<&Token> + where + Self: AsRef<[Token]>, + { + let slice = self.as_ref(); + let len = slice.len() as isize; + + if index >= len || -index > len { + return None; + } + + let idx = if index >= 0 { + index as usize + } else { + (len + index) as usize + }; + + slice.get(idx) + } + fn iter_linking_verb_indices(&self) -> impl Iterator + '_; fn iter_linking_verbs(&self) -> impl Iterator + '_;