use crate::expr::{Expr, SequenceExpr};
use crate::linting::expr_linter::Chunk;
use crate::linting::{ExprLinter, Lint, LintKind, Suggestion};
use crate::{Token, TokenStringExt};
fn matches_hyphen(token: &Token, _source: &[char]) -> bool {
token.kind.is_hyphen()
}
fn replacement_for(template: &[char]) -> Vec<char> {
let mut replacement = "vice versa".chars().collect::<Vec<_>>();
let mut has_upper = false;
let mut has_lower = false;
let mut first_alpha_upper = None;
for ch in template.iter().copied() {
if !ch.is_alphabetic() {
continue;
}
has_upper |= ch.is_uppercase();
has_lower |= ch.is_lowercase();
if first_alpha_upper.is_none() {
first_alpha_upper = Some(ch.is_uppercase());
}
}
if has_upper && !has_lower {
for ch in replacement.iter_mut() {
if ch.is_alphabetic() {
*ch = ch.to_ascii_uppercase();
}
}
return replacement;
}
if !has_upper {
return replacement;
}
if first_alpha_upper.unwrap_or(false) {
let mut capitalized_first = false;
for ch in replacement.iter_mut() {
if !ch.is_alphabetic() {
continue;
}
if !capitalized_first {
*ch = ch.to_ascii_uppercase();
capitalized_first = true;
} else {
*ch = ch.to_ascii_lowercase();
}
}
return replacement;
}
for ch in replacement.iter_mut() {
if ch.is_alphabetic() {
*ch = ch.to_ascii_lowercase();
}
}
replacement
}
pub struct ViceVersa {
expr: SequenceExpr,
}
impl Default for ViceVersa {
fn default() -> Self {
let expr = SequenceExpr::word_set(&["vice", "vise"])
.then(matches_hyphen)
.then_optional(SequenceExpr::aco("a").then(matches_hyphen))
.t_aco("versa");
Self { expr }
}
}
impl ExprLinter for ViceVersa {
type Unit = Chunk;
fn expr(&self) -> &dyn Expr {
&self.expr
}
fn match_to_lint(&self, matched_tokens: &[Token], source: &[char]) -> Option<Lint> {
let span = matched_tokens.span()?;
let template = span.get_content(source);
Some(Lint {
span,
lint_kind: LintKind::Punctuation,
suggestions: vec![Suggestion::ReplaceWith(replacement_for(template))],
message: "The expression \"vice versa\" is spelled without hyphens.".to_owned(),
priority: 60,
})
}
fn description(&self) -> &str {
"Recommends writing ‘vice versa’ without hyphens."
}
}
#[cfg(test)]
mod tests {
use super::ViceVersa;
use crate::linting::tests::{assert_lint_count, assert_suggestion_result};
#[test]
fn corrects_basic_hyphenated() {
assert_suggestion_result(
"We swapped the arguments vice-versa this time.",
ViceVersa::default(),
"We swapped the arguments vice versa this time.",
);
}
#[test]
fn corrects_leading_capitalization() {
assert_suggestion_result(
"Vice-Versa, the movie, was interesting.",
ViceVersa::default(),
"Vice versa, the movie, was interesting.",
);
}
#[test]
fn corrects_all_caps() {
assert_suggestion_result(
"They agreed VICE-VERSA on the clause.",
ViceVersa::default(),
"They agreed VICE VERSA on the clause.",
);
}
#[test]
fn corrects_with_extra_a() {
assert_suggestion_result(
"The logic works vice-a-versa as well.",
ViceVersa::default(),
"The logic works vice versa as well.",
);
}
#[test]
fn corrects_vise_variant() {
assert_suggestion_result(
"The rule applies vise-versa too.",
ViceVersa::default(),
"The rule applies vice versa too.",
);
}
#[test]
fn corrects_vise_extra_a_variant() {
assert_suggestion_result(
"The rule applies Vise-A-Versa too.",
ViceVersa::default(),
"The rule applies Vice versa too.",
);
}
#[test]
fn corrects_with_trailing_suffix() {
assert_suggestion_result(
"That was a vice-versa-like transformation.",
ViceVersa::default(),
"That was a vice versa-like transformation.",
);
}
#[test]
fn allows_correct_spelling() {
assert_lint_count(
"We swapped the arguments vice versa this time.",
ViceVersa::default(),
0,
);
}
#[test]
fn allows_sentence_case() {
assert_lint_count(
"Vice versa, the movie, was interesting.",
ViceVersa::default(),
0,
);
}
#[test]
fn does_not_flag_unrelated_words() {
assert_lint_count(
"Their service-versa mapping was custom.",
ViceVersa::default(),
0,
);
}
}