harper-core 2.13.0

The language checker for developers.
Documentation
use crate::{
    CharStringExt, Lint, Token,
    expr::{Expr, FirstMatchOf, SequenceExpr},
    linting::{
        ExprLinter, LintKind, Suggestion,
        expr_linter::{Chunk, find_the_only_token_matching},
    },
};

const RARE_BEFORE_ROGUE: &[&str] = &["baton", "cheek", "khmer", "lip", "moulin"];
const RARE_BEFORE_ROUGE: &[&str] = &["go", "goes", "going", "gone", "went"];
const RARE_AFTER_ROGUE: &[&str] = &["lipstick"];
const RARE_AFTER_ROUGE: &[&str] = &[
    "like", "likes", "nation", "nations", "one", "squadron", "trader", "traders", "wave", "waves",
    "who",
];
pub struct RogueRouge {
    expr: FirstMatchOf,
}

impl Default for RogueRouge {
    fn default() -> Self {
        Self {
            expr: FirstMatchOf::new([
                Box::new(
                    SequenceExpr::word_set(RARE_BEFORE_ROGUE)
                        .t_ws()
                        .then_word_seq(&["rogue"]),
                ) as Box<dyn Expr>,
                Box::new(
                    SequenceExpr::word_set(RARE_BEFORE_ROUGE)
                        .t_ws()
                        .t_aco("rouge"),
                ),
                Box::new(
                    SequenceExpr::word_seq(&["rogue"])
                        .t_ws()
                        .t_set(RARE_AFTER_ROGUE),
                ),
                Box::new(SequenceExpr::aco("rouge").t_ws().t_set(RARE_AFTER_ROUGE)),
                Box::new(
                    SequenceExpr::aco("rouge")
                        .then_hyphen()
                        .t_set(["like", "likes"]),
                ),
            ]),
        }
    }
}

impl ExprLinter for RogueRouge {
    type Unit = Chunk;

    fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
        let span = find_the_only_token_matching(matched_tokens, source, |t, c| {
            t.get_ch(c)
                .eq_any_ignore_ascii_case_str(&["rogue", "rouge"])
        })?
        .span;

        let mut the_word = span.get_content(source).to_vec();

        the_word.swap(2, 3);

        Some(Lint {
            span,
            lint_kind: LintKind::WordChoice,
            suggestions: vec![Suggestion::ReplaceWith(the_word)],
            message:
                "Are you confusing `rouge` (color/makeup) with `rogue` (untrustworthy person)?"
                    .to_owned(),
            ..Default::default()
        })
    }

    fn expr(&self) -> &dyn Expr {
        &self.expr
    }

    fn description(&self) -> &str {
        "Detects mixing up `rogue` and `rouge`."
    }
}

#[cfg(test)]
mod tests {
    use crate::linting::tests::assert_suggestion_result;

    use super::RogueRouge;

    #[test]
    fn fix_go_rouge() {
        assert_suggestion_result(
            "Claude can wake up on an idle session easily, go rouge, and blow your cloud credits.",
            RogueRouge::default(),
            "Claude can wake up on an idle session easily, go rogue, and blow your cloud credits.",
        );
    }

    #[test]
    fn fix_goes_rouge() {
        assert_suggestion_result(
            "A \"not considered mature\" incubation stalls out/stops progressing or goes rouge (no longer working with their upstream advisor)?",
            RogueRouge::default(),
            "A \"not considered mature\" incubation stalls out/stops progressing or goes rogue (no longer working with their upstream advisor)?",
        );
    }

    #[test]
    fn fix_going_rouge() {
        assert_suggestion_result(
            "port power negotiation and other devices going rouge on the network throwing me off the trail",
            RogueRouge::default(),
            "port power negotiation and other devices going rogue on the network throwing me off the trail",
        );
    }

    #[test]
    fn fix_gone_rouge() {
        assert_suggestion_result(
            "and now the its long term partner has gone rouge",
            RogueRouge::default(),
            "and now the its long term partner has gone rogue",
        );
    }

    #[test]
    fn fix_baton_rogue() {
        assert_suggestion_result(
            "The programs know you dont care about the bronx or baton rogue, you just want a spot",
            RogueRouge::default(),
            "The programs know you dont care about the bronx or baton rouge, you just want a spot",
        );
    }

    #[test]
    fn fix_khmer_rogue() {
        assert_suggestion_result(
            "After the Khmer Rogue took power they became very aggressive towards Vietnam",
            RogueRouge::default(),
            "After the Khmer Rouge took power they became very aggressive towards Vietnam",
        );
    }

    #[test]
    fn fix_rouge_who() {
        assert_suggestion_result(
            "What backstory could I use for a rouge who isn't a orphan?",
            RogueRouge::default(),
            "What backstory could I use for a rogue who isn't a orphan?",
        );
    }

    #[test]
    fn fix_rouge_squadron() {
        assert_suggestion_result(
            "Hoth from Star Wars Rouge Squadron.",
            RogueRouge::default(),
            "Hoth from Star Wars Rogue Squadron.",
        );
    }

    #[test]
    fn fix_rouge_nation() {
        assert_suggestion_result(
            "In \"Mission Impossible 5: Rouge Nation\" (2015) Benji is seen playing Halo 5.",
            RogueRouge::default(),
            "In \"Mission Impossible 5: Rogue Nation\" (2015) Benji is seen playing Halo 5.",
        );
    }

    #[test]
    fn fix_rouge_nations() {
        assert_suggestion_result(
            "what prevents rouge nations from crediting their banking system with fake USD?",
            RogueRouge::default(),
            "what prevents rogue nations from crediting their banking system with fake USD?",
        );
    }

    #[test]
    fn fix_lip_rogue() {
        assert_suggestion_result(
            "The lip rogue and nail henna as well complement the look.",
            RogueRouge::default(),
            "The lip rouge and nail henna as well complement the look.",
        );
    }

    #[test]
    fn fix_rouge_trader() {
        assert_suggestion_result(
            "I'm one upgrade away from unlocking the rouge trader is it worth doing and is it beneficial at a low level.",
            RogueRouge::default(),
            "I'm one upgrade away from unlocking the rogue trader is it worth doing and is it beneficial at a low level.",
        );
    }

    #[test]
    fn fix_rogue_lipstick() {
        assert_suggestion_result(
            "Love Dior's Rogue Lipstick-- Hate the Case!",
            RogueRouge::default(),
            "Love Dior's Rouge Lipstick-- Hate the Case!",
        );
    }

    #[test]
    fn fix_rouge_like() {
        assert_suggestion_result(
            "Do you guys think any Rouge-like mechanics would be cool???",
            RogueRouge::default(),
            "Do you guys think any Rogue-like mechanics would be cool???",
        );
    }

    #[test]
    fn fix_rouge_likes() {
        assert_suggestion_result(
            "As someone who dissent really play rouge likes, do you think I'll like risk of rain 2?",
            RogueRouge::default(),
            "As someone who dissent really play rogue likes, do you think I'll like risk of rain 2?",
        );
    }

    #[test]
    fn fix_rouge_traders() {
        assert_suggestion_result(
            "How do Pirates or Rouge Traders Power Their Gellar Fields?",
            RogueRouge::default(),
            "How do Pirates or Rogue Traders Power Their Gellar Fields?",
        );
    }
}