harper-core 2.0.0

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

pub struct Bought {
    expr: SequenceExpr,
}

impl Default for Bought {
    fn default() -> Self {
        let subject = SequenceExpr::with(Self::is_subject_pronoun_like)
            .t_ws()
            .then_optional(SequenceExpr::default().then_adverb().t_ws())
            .then_optional(SequenceExpr::default().then_auxiliary_verb().t_ws())
            .then_optional(SequenceExpr::default().then_adverb().t_ws())
            .then_any_capitalization_of("bough");

        Self { expr: subject }
    }
}

impl ExprLinter for Bought {
    type Unit = Chunk;

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

    fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
        let typo = matched_tokens.last()?;

        Some(Lint {
            span: typo.span,
            lint_kind: LintKind::WordChoice,
            suggestions: vec![Suggestion::replace_with_match_case(
                "bought".chars().collect(),
                typo.get_ch(source),
            )],
            message: "Prefer the past-tense form `bought` here.".to_owned(),
            priority: 31,
        })
    }

    fn description(&self) -> &'static str {
        "Replaces the incorrect past-tense spelling `bough` with `bought` after subject pronouns."
    }
}

impl Bought {
    fn is_subject_pronoun_like(token: &Token, source: &[char]) -> bool {
        if token.kind.is_subject_pronoun() {
            return true;
        }

        if !token.kind.is_word() || !token.kind.is_apostrophized() {
            return false;
        }

        let text = token.get_str(source);
        let lower = text.to_ascii_lowercase();

        let Some((stem, suffix)) = lower.split_once('\'') else {
            return false;
        };

        let is_subject_stem = matches!(stem, "i" | "you" | "we" | "they" | "he" | "she" | "it");
        let is_supported_suffix = matches!(suffix, "d" | "ve");

        is_subject_stem && is_supported_suffix
    }
}

#[cfg(test)]
mod tests {
    use super::Bought;
    use crate::linting::tests::{assert_no_lints, assert_suggestion_result};

    #[test]
    fn corrects_he_bough() {
        assert_suggestion_result(
            "He bough a laptop yesterday.",
            Bought::default(),
            "He bought a laptop yesterday.",
        );
    }

    #[test]
    fn corrects_she_never_bough() {
        assert_suggestion_result(
            "She never bough fresh herbs there.",
            Bought::default(),
            "She never bought fresh herbs there.",
        );
    }

    #[test]
    fn corrects_they_already_bough() {
        assert_suggestion_result(
            "They already bough the train tickets.",
            Bought::default(),
            "They already bought the train tickets.",
        );
    }

    #[test]
    fn corrects_we_have_bough() {
        assert_suggestion_result(
            "We have bough extra paint.",
            Bought::default(),
            "We have bought extra paint.",
        );
    }

    #[test]
    fn corrects_they_have_never_bough() {
        assert_suggestion_result(
            "They have never bough theatre seats online.",
            Bought::default(),
            "They have never bought theatre seats online.",
        );
    }

    #[test]
    fn corrects_ive_bough() {
        assert_suggestion_result(
            "I've bough the ingredients already.",
            Bought::default(),
            "I've bought the ingredients already.",
        );
    }

    #[test]
    fn corrects_wed_bough() {
        assert_suggestion_result(
            "We'd bough snacks before the film.",
            Bought::default(),
            "We'd bought snacks before the film.",
        );
    }

    #[test]
    fn no_lint_for_tree_bough() {
        assert_no_lints("The heavy bough cracked under the snow.", Bought::default());
    }

    #[test]
    fn no_lint_for_he_bought() {
        assert_no_lints("He bought a laptop yesterday.", Bought::default());
    }

    #[test]
    fn no_lint_for_plural_boughs() {
        assert_no_lints("Boughs swayed in the evening breeze.", Bought::default());
    }
}