use crate::checker::Checker;
use crate::diagnostic::{Diagnostic, Severity};
use crate::po::entry::Entry;
use crate::po::message::Message;
use crate::rules::rule::RuleChecker;
const BRACKET_PAIRS: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
const BRACKET_NAMES: &[&str] = &["round", "square", "curly", "angle"];
pub struct BracketsRule;
impl RuleChecker for BracketsRule {
fn name(&self) -> &'static str {
"brackets"
}
fn is_default(&self) -> bool {
true
}
fn is_check(&self) -> bool {
true
}
fn severity(&self) -> Severity {
Severity::Info
}
fn check_msg(
&self,
checker: &Checker,
_entry: &Entry,
msgid: &Message,
msgstr: &Message,
) -> Vec<Diagnostic> {
let mut diags = vec![];
for (idx, bracket) in BRACKET_PAIRS.iter().enumerate() {
let mut id_open = get_opening_bracket_pos(&msgid.value, bracket.0);
let id_count_open = id_open.len();
let mut str_open = get_opening_bracket_pos(&msgstr.value, bracket.0);
let str_count_open = str_open.len();
let id_close = get_closing_bracket_pos(&msgid.value, bracket.1);
let id_count_close = id_close.len();
let str_close = get_closing_bracket_pos(&msgstr.value, bracket.1);
let str_count_close = str_close.len();
if BRACKET_PAIRS[idx].0 == '('
&& id_count_open < str_count_open
&& id_count_close < str_count_close
{
continue;
}
if (id_count_open > str_count_open && id_count_close > str_count_close)
|| (id_count_open < str_count_open && id_count_close < str_count_close)
{
id_open.extend(&id_close);
id_open.sort_unstable();
str_open.extend(&str_close);
str_open.sort_unstable();
let msg = format!(
"{} opening and closing {} brackets '{}' ({id_count_open} / {str_count_open}) \
and '{}' ({id_count_close} / {str_count_close})",
if id_count_open > str_count_open {
"missing"
} else {
"extra"
},
BRACKET_NAMES[idx],
bracket.0,
bracket.1,
);
diags.push(
self.new_diag(checker, msg)
.with_msgs_hl(msgid, &id_open, msgstr, &str_open),
);
continue;
}
if id_count_open > str_count_open {
let msg = format!(
"missing opening {} brackets '{}' ({id_count_open} / {str_count_open})",
BRACKET_NAMES[idx], bracket.0,
);
diags.push(
self.new_diag(checker, msg)
.with_msgs_hl(msgid, &id_open, msgstr, &str_open),
);
}
if id_count_open < str_count_open {
let msg = format!(
"extra opening {} brackets '{}' ({id_count_open} / {str_count_open})",
BRACKET_NAMES[idx], bracket.0,
);
diags.push(
self.new_diag(checker, msg)
.with_msgs_hl(msgid, &id_open, msgstr, &str_open),
);
}
if id_count_close > str_count_close {
let msg = format!(
"missing closing {} brackets '{}' ({id_count_close} / {str_count_close})",
BRACKET_NAMES[idx], bracket.1,
);
diags.push(
self.new_diag(checker, msg)
.with_msgs_hl(msgid, &id_close, msgstr, &str_close),
);
}
if id_count_close < str_count_close {
let msg = format!(
"extra closing {} brackets '{}' ({id_count_close} / {str_count_close})",
BRACKET_NAMES[idx], bracket.1,
);
diags.push(
self.new_diag(checker, msg)
.with_msgs_hl(msgid, &id_close, msgstr, &str_close),
);
}
}
diags
}
}
fn get_opening_bracket_pos(s: &str, bracket_char: char) -> Vec<(usize, usize)> {
s.match_indices(bracket_char)
.map(|(idx, value)| (idx, idx + value.len()))
.filter(|(idx, _)| !is_excluded_start(s, *idx, bracket_char))
.collect()
}
fn get_closing_bracket_pos(s: &str, bracket_char: char) -> Vec<(usize, usize)> {
s.match_indices(bracket_char)
.map(|(idx, value)| (idx, idx + value.len()))
.filter(|(idx, _)| !is_excluded_end(s, *idx, bracket_char))
.collect()
}
fn is_excluded_start(s: &str, index: usize, bracket_char: char) -> bool {
if bracket_char == '(' {
if s[index..].starts_with("(s)") || s[index..].starts_with("(S)") {
return true;
}
}
false
}
fn is_excluded_end(s: &str, index: usize, bracket_char: char) -> bool {
if bracket_char == ')' {
if s[..=index].ends_with("(s)") || s[..=index].ends_with("(S)") {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{diagnostic::Diagnostic, rules::rule::Rules};
fn check_brackets(content: &str) -> Vec<Diagnostic> {
let mut checker = Checker::new(content.as_bytes());
let rules = Rules::new(vec![Box::new(BracketsRule {})]);
checker.do_all_checks(&rules);
checker.diagnostics
}
#[test]
fn test_no_brackets() {
let diags = check_brackets(
r#"
msgid "tested"
msgstr "testé"
"#,
);
assert!(diags.is_empty());
}
#[test]
fn test_brackets_ok() {
let diags = check_brackets(
r#"
msgid "[({<tested>})]"
msgstr "[({<testé>})]"
"#,
);
assert!(diags.is_empty());
let diags = check_brackets(
r#"
msgid "[({<tested>})]"
msgstr "[({<testé>})]"
"#,
);
assert!(diags.is_empty());
let diags = check_brackets(
r#"
msgid "position: top or bottom"
msgstr "position : top (haut) ou bottom (bas)"
"#,
);
assert!(diags.is_empty());
let diags = check_brackets(
r#"
msgid "tests"
msgstr "test(s)"
"#,
);
assert!(diags.is_empty());
}
#[test]
fn test_brackets_error_noqa() {
let diags = check_brackets(
r#"
#, noqa:brackets
msgid "[(tested"
msgstr "testé>}"
"#,
);
assert!(diags.is_empty());
}
#[test]
fn test_brackets_error() {
let diags = check_brackets(
r#"
msgid "[(tested"
msgstr "testé>}"
"#,
);
assert_eq!(diags.len(), 4);
let diag = &diags[0];
assert_eq!(diag.severity, Severity::Info);
assert_eq!(diag.message, "missing opening round brackets '(' (1 / 0)");
let diag = &diags[1];
assert_eq!(diag.severity, Severity::Info);
assert_eq!(diag.message, "missing opening square brackets '[' (1 / 0)");
let diag = &diags[2];
assert_eq!(diag.severity, Severity::Info);
assert_eq!(diag.message, "extra closing curly brackets '}' (0 / 1)");
let diag = &diags[3];
assert_eq!(diag.severity, Severity::Info);
assert_eq!(diag.message, "extra closing angle brackets '>' (0 / 1)");
let diags = check_brackets(
r#"
msgid "[tested]] {tested}"
msgstr "tested] {{ested}}}"
"#,
);
assert_eq!(diags.len(), 2);
let diag = &diags[0];
assert_eq!(diag.severity, Severity::Info);
assert_eq!(
diag.message,
"missing opening and closing square brackets '[' (1 / 0) and ']' (2 / 1)"
);
let diag = &diags[1];
assert_eq!(diag.severity, Severity::Info);
assert_eq!(
diag.message,
"extra opening and closing curly brackets '{' (1 / 2) and '}' (1 / 3)"
);
let diags = check_brackets(
r#"
msgid "example (test)"
msgstr "exemple ((test)"
"#,
);
assert_eq!(diags.len(), 1);
let diag = &diags[0];
assert_eq!(diag.severity, Severity::Info);
assert_eq!(diag.message, "extra opening round brackets '(' (1 / 2)");
let diags = check_brackets(
r#"
msgid "example (test)"
msgstr "exemple (test"
"#,
);
assert_eq!(diags.len(), 1);
let diag = &diags[0];
assert_eq!(diag.severity, Severity::Info);
assert_eq!(diag.message, "missing closing round brackets ')' (1 / 0)");
}
}