harper-core 2.0.0

The language checker for developers.
Documentation
use crate::expr::Expr;
use crate::expr::FixedPhrase;
use crate::expr::LongestMatchOf;
use crate::expr::SequenceExpr;
use crate::{Lrc, Token, TokenStringExt};

use super::{ExprLinter, Lint, LintKind, Suggestion};
use crate::linting::expr_linter::Chunk;

pub struct OneAndTheSame {
    expr: LongestMatchOf,
}

impl Default for OneAndTheSame {
    fn default() -> Self {
        let one_in_the_same = Lrc::new(FixedPhrase::from_phrase("one in the same"));

        Self {
            expr: LongestMatchOf::new(vec![
                Box::new(
                    SequenceExpr::word_set(&["are", "were"])
                        .t_ws()
                        .then(one_in_the_same.clone()),
                ),
                Box::new(
                    SequenceExpr::with(one_in_the_same.clone())
                        .t_ws()
                        .t_aco("as"),
                ),
            ]),
        }
    }
}

fn ws_word(word: &'static str) -> SequenceExpr {
    SequenceExpr::default().t_ws().t_aco(word)
}

impl ExprLinter for OneAndTheSame {
    type Unit = Chunk;

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

    fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
        let phrase = if matched_tokens.last()?.get_ch(source) == ['a', 's'] {
            matched_tokens[0..matched_tokens.len() - 2].span()?
        } else {
            matched_tokens[2..].span()?
        };

        Some(Lint {
            span: phrase,
            lint_kind: LintKind::WordChoice,
            suggestions: vec![Suggestion::replace_with_match_case(
                "one and the same".chars().collect(),
                phrase.get_content(source),
            )],
            message: "The actual idiom is with the word `and`.".to_owned(),
            priority: 127,
        })
    }

    fn description(&self) -> &'static str {
        "This linter flags instances of the nonstandard phrase `one in the same`. The correct, more accepted form is `one and the same`"
    }
}

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

    #[test]
    fn corrects_after_are_atomic() {
        assert_suggestion_result(
            "... are one in the same ...",
            OneAndTheSame::default(),
            "... are one and the same ...",
        );
    }

    #[test]
    fn corrects_after_were_atomic() {
        assert_suggestion_result(
            "... were one in the same ...",
            OneAndTheSame::default(),
            "... were one and the same ...",
        );
    }

    #[test]
    fn doesnt_flag_after_other_words_atomic() {
        assert_lint_count(
            "... and another one in the same place ...",
            OneAndTheSame::default(),
            0,
        );
    }

    #[test]
    fn corrects_github_are() {
        assert_suggestion_result(
            "Yes, I believe they are one in the same.",
            OneAndTheSame::default(),
            "Yes, I believe they are one and the same.",
        );
    }

    #[test]
    fn corrects_github_were() {
        assert_suggestion_result(
            "As prior to OpenShift 4.0, OAuth and Kubernetes REST API were one in the same, option (2) above should still work there.",
            OneAndTheSame::default(),
            "As prior to OpenShift 4.0, OAuth and Kubernetes REST API were one and the same, option (2) above should still work there.",
        );
    }

    #[test]
    fn corrects_before_as_atomic() {
        assert_suggestion_result(
            "... one in the same as ...",
            OneAndTheSame::default(),
            "... one and the same as ...",
        );
    }

    #[test]
    fn corrects_before_as_github() {
        assert_suggestion_result(
            "In our case the slicedState is one in the same as the featureState",
            OneAndTheSame::default(),
            "In our case the slicedState is one and the same as the featureState",
        );
    }

    #[test]
    #[ignore = "needs zero-width end-of-chunk pattern akin to regex `$`"]
    fn corrects_at_end() {
        assert_suggestion_result(
            "I think this is one in the same.",
            OneAndTheSame::default(),
            "I think this is one and the same.",
        );
    }

    #[test]
    fn corrects_is_as() {
        assert_suggestion_result(
            "I believe this and this issue is one in the same as Next.js uses cloudflare workers for it's edge infra.",
            OneAndTheSame::default(),
            "I believe this and this issue is one and the same as Next.js uses cloudflare workers for it's edge infra.",
        );
    }

    #[test]
    fn avoids_false_positive() {
        assert_lint_count(
            "If there is no postgresql.pg_hba either there is one in the same section of patroni.yaml or pg_hba.conf is not managed by Patroni.",
            OneAndTheSame::default(),
            0,
        );
    }

    #[test]
    #[ignore = "Cannot detect unexpected ungrammatical `same of`"]
    fn corrects_is_of() {
        assert_suggestion_result(
            "R3 that Stephan Buhre noted is one-in-the-same of what I posted.",
            OneAndTheSame::default(),
            "R3 that Stephan Buhre noted is one and the same of what I posted.",
        );
    }

    #[test]
    fn doesnt_flag_ambiguous_before_noun() {
        assert_lint_count(
            "I'm guessing this is one in the same request.",
            OneAndTheSame::default(),
            0,
        );
    }
}