use std::collections::HashSet;
use crate::{
args,
checker::Checker,
config::Config,
diagnostic::{Diagnostic, Severity},
po::{entry::Entry, message::Message},
rules::{
blank, brackets, changed, compilation, double_quotes, double_spaces, double_words, emails,
encoding, escapes, formats, fuzzy, header, html_tags, long, newlines, noqa, obsolete,
paths, pipes, plurals, punc, punc_space, short, spelling, tabs, unchanged, unicode_ctrl,
untranslated, urls, whitespace,
},
table::render_table,
};
pub type Rule = Box<dyn RuleChecker + Send + Sync>;
const SPECIAL_RULES: [&str; 4] = ["all", "checks", "default", "spelling"];
#[derive(Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct Rules {
pub enabled: Vec<Rule>,
pub fuzzy_rule: bool,
pub noqa_rule: bool,
pub obsolete_rule: bool,
pub untranslated_rule: bool,
pub spelling_ctxt_rule: bool,
pub spelling_id_rule: bool,
pub spelling_str_rule: bool,
}
impl std::fmt::Display for Rule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.name(), self.description())
}
}
impl Rules {
pub fn new(rules: Vec<Rule>) -> Self {
let fuzzy_rule = rules.iter().any(|r| r.name() == "fuzzy");
let noqa_rule = rules.iter().any(|r| r.name() == "noqa");
let obsolete_rule = rules.iter().any(|r| r.name() == "obsolete");
let untranslated_rule = rules.iter().any(|r| r.name() == "untranslated");
let spelling_ctxt_rule = rules.iter().any(|r| r.name() == "spelling-ctxt");
let spelling_id_rule = rules.iter().any(|r| r.name() == "spelling-id");
let spelling_str_rule = rules.iter().any(|r| r.name() == "spelling-str");
Self {
enabled: rules,
fuzzy_rule,
noqa_rule,
obsolete_rule,
untranslated_rule,
spelling_ctxt_rule,
spelling_id_rule,
spelling_str_rule,
}
}
}
pub trait RuleChecker {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn is_default(&self) -> bool;
fn is_check(&self) -> bool;
fn check_file(&self, _checker: &Checker) -> Vec<Diagnostic> {
vec![]
}
fn check_header(
&self,
_checker: &Checker,
_entry: &Entry,
_msgstr: &Message,
) -> Vec<Diagnostic> {
vec![]
}
fn check_entry(&self, _checker: &Checker, _entry: &Entry) -> Vec<Diagnostic> {
vec![]
}
fn check_ctxt(&self, _checker: &Checker, _entry: &Entry, _ctxt: &Message) -> Vec<Diagnostic> {
vec![]
}
fn check_msg(
&self,
_checker: &Checker,
_entry: &Entry,
_msgid: &Message,
_msgstr: &Message,
) -> Vec<Diagnostic> {
vec![]
}
fn new_diag(
&self,
checker: &Checker,
severity: Severity,
message: impl Into<std::borrow::Cow<'static, str>>,
) -> Option<Diagnostic>
where
Self: Sized,
{
let allowed = &checker.config.check.severity;
if !allowed.is_empty() && !allowed.contains(&severity) {
return None;
}
Some(Diagnostic::new(
&checker.path,
self.name(),
severity,
message,
))
}
}
fn get_all_rules() -> Vec<Rule> {
vec![
Box::new(blank::BlankRule {}),
Box::new(brackets::BracketsRule {}),
Box::new(changed::ChangedRule {}),
Box::new(compilation::CompilationRule {}),
Box::new(double_quotes::DoubleQuotesRule {}),
Box::new(double_spaces::DoubleSpacesRule {}),
Box::new(double_words::DoubleWordsRule {}),
Box::new(emails::EmailsRule {}),
Box::new(encoding::EncodingRule {}),
Box::new(escapes::EscapesRule {}),
Box::new(formats::FormatsRule {}),
Box::new(fuzzy::FuzzyRule {}),
Box::new(header::HeaderRule {}),
Box::new(html_tags::HtmlTagsRule {}),
Box::new(long::LongRule {}),
Box::new(newlines::NewlinesRule {}),
Box::new(noqa::NoqaRule {}),
Box::new(obsolete::ObsoleteRule {}),
Box::new(paths::PathsRule {}),
Box::new(pipes::PipesRule {}),
Box::new(plurals::PluralsRule {}),
Box::new(punc::PuncStartRule {}),
Box::new(punc::PuncEndRule {}),
Box::new(punc_space::PuncSpaceIdRule {}),
Box::new(punc_space::PuncSpaceStrRule {}),
Box::new(short::ShortRule {}),
Box::new(spelling::SpellingCtxtRule {}),
Box::new(spelling::SpellingIdRule {}),
Box::new(spelling::SpellingStrRule {}),
Box::new(tabs::TabsRule {}),
Box::new(unchanged::UnchangedRule {}),
Box::new(unicode_ctrl::UnicodeCtrlRule {}),
Box::new(untranslated::UntranslatedRule {}),
Box::new(urls::UrlsRule {}),
Box::new(whitespace::WhitespaceEndRule {}),
Box::new(whitespace::WhitespaceStartRule {}),
]
}
fn get_unknown_rules<'a>(
names: &'a [String],
all_rules_names: &HashSet<&'static str>,
) -> Vec<&'a str> {
let selected_rules_names = names
.iter()
.map(std::convert::AsRef::as_ref)
.collect::<HashSet<_>>();
let mut unknown_rules_names: HashSet<&str> = selected_rules_names
.difference(all_rules_names)
.copied()
.collect();
for name in SPECIAL_RULES {
unknown_rules_names.remove(name);
}
if unknown_rules_names.is_empty() {
return vec![];
}
let mut unknown = unknown_rules_names.iter().copied().collect::<Vec<_>>();
unknown.sort_unstable();
unknown
}
pub fn get_selected_rules(config: &Config) -> Result<Rules, Box<dyn std::error::Error>> {
let mut all_rules: Vec<Rule> = get_all_rules();
let all_rules_names: HashSet<&'static str> = all_rules.iter().map(|r| r.name()).collect();
let mut selected_rules: Vec<Rule> = Vec::new();
let unknown_rules_names = get_unknown_rules(&config.check.select, &all_rules_names);
if !unknown_rules_names.is_empty() {
return Err(format!("unknown selected rules: {}", unknown_rules_names.join(", ")).into());
}
for name in &config.check.select {
if name == "all" {
selected_rules.extend(all_rules.extract_if(.., |_| true));
} else if name == "checks" {
selected_rules.extend(all_rules.extract_if(.., |rule| rule.is_check()));
} else if name == "default" {
selected_rules.extend(all_rules.extract_if(.., |rule| rule.is_default()));
} else if name == "spelling" {
selected_rules
.extend(all_rules.extract_if(.., |rule| rule.name().starts_with("spelling-")));
} else {
selected_rules.extend(all_rules.extract_if(.., |rule| rule.name() == name));
}
}
let unknown_rules_names = get_unknown_rules(&config.check.ignore, &all_rules_names);
if !unknown_rules_names.is_empty() {
return Err(format!(
"unknown rules to ignore: {}",
unknown_rules_names.join(", ")
)
.into());
}
selected_rules.retain(|rule| !config.check.ignore.iter().any(|r| r == rule.name()));
selected_rules.sort_by(|a, b| a.name().cmp(b.name()));
Ok(Rules::new(selected_rules))
}
fn print_rules_table(all_rules: &[Rule]) {
let (default_rules, other_rules) = all_rules
.iter()
.partition::<Vec<&Rule>, _>(|r| r.is_default());
let rows: Vec<Vec<String>> = default_rules
.iter()
.chain(other_rules.iter())
.map(|r| {
vec![
r.name().to_string(),
if r.is_default() { "yes" } else { "no" }.to_string(),
if r.is_check() { "yes" } else { "no" }.to_string(),
r.description().to_string(),
]
})
.collect();
println!(
"{} rules ({} default, {} non-default):\n\n{}",
all_rules.len(),
default_rules.len(),
other_rules.len(),
render_table(&["Name", "Default", "Check", "Description"], &rows),
);
}
fn print_special_rules_table(all_rules: &[Rule]) {
let mut non_check_rules: Vec<&'static str> = Vec::new();
let mut spelling_rules: Vec<&'static str> = Vec::new();
let mut default_count = 0;
for rule in all_rules {
if !rule.is_check() {
non_check_rules.push(rule.name());
}
if rule.name().starts_with("spelling-") {
spelling_rules.push(rule.name());
}
if rule.is_default() {
default_count += 1;
}
}
let rows: Vec<Vec<String>> = vec![
vec![
"all".to_string(),
all_rules.len().to_string(),
"All available rules.".to_string(),
],
vec![
"checks".to_string(),
(all_rules.len() - non_check_rules.len()).to_string(),
format!(
"All rules that actually check (all rules except: {}).",
non_check_rules.join(", ")
),
],
vec![
"default".to_string(),
default_count.to_string(),
"Default rules (can be used to add extra rules, e.g. \"default,spelling,fuzzy\")."
.to_string(),
],
vec![
"spelling".to_string(),
spelling_rules.len().to_string(),
format!("All spelling rules: {}.", spelling_rules.join(", ")),
],
];
println!(
"Special rules to enable multiple rules at once:\n\n{}",
render_table(&["Name", "Rules", "Description"], &rows),
);
}
pub fn run_rules(_args: &args::RulesArgs) -> i32 {
let rules = get_all_rules();
print_rules_table(&rules);
println!();
print_special_rules_table(&rules);
0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostic::Severity;
fn rule_names(rules: &Rules) -> Vec<&str> {
rules.enabled.iter().map(|r| r.name()).collect()
}
fn make_config(select: Vec<&str>, ignore: Vec<&str>, severity: Vec<Severity>) -> Config {
let mut config = Config::default();
config.check.select = select.into_iter().map(String::from).collect();
config.check.ignore = ignore.into_iter().map(String::from).collect();
config.check.severity = severity;
config
}
fn all_rules_name_set() -> HashSet<&'static str> {
get_all_rules().iter().map(|r| r.name()).collect()
}
#[test]
fn test_get_all_rules() {
let rules = get_all_rules();
assert!(!rules.is_empty());
let names: HashSet<&str> = rules.iter().map(|r| r.name()).collect();
assert_eq!(names.len(), rules.len(), "rule names must be unique");
assert!(
rules.iter().any(|r| r.is_default()),
"should have at least one default rule"
);
assert!(
rules.iter().any(|r| !r.is_default()),
"should have at least one non-default rule"
);
assert!(
rules.iter().any(|r| r.is_check()),
"should have at least one check rule"
);
assert!(
rules.iter().any(|r| !r.is_check()),
"should have at least one non-check rule"
);
}
#[test]
fn test_rules_new_empty() {
let rules = Rules::new(vec![]);
assert!(rules.enabled.is_empty());
assert!(!rules.fuzzy_rule);
assert!(!rules.obsolete_rule);
assert!(!rules.untranslated_rule);
assert!(!rules.spelling_ctxt_rule);
assert!(!rules.spelling_id_rule);
assert!(!rules.spelling_str_rule);
}
#[test]
fn test_rules_new_fuzzy_flag() {
let rules = Rules::new(vec![Box::new(fuzzy::FuzzyRule {})]);
assert!(rules.fuzzy_rule);
assert!(!rules.obsolete_rule);
assert!(!rules.untranslated_rule);
}
#[test]
fn test_rules_new_obsolete_flag() {
let rules = Rules::new(vec![Box::new(obsolete::ObsoleteRule {})]);
assert!(!rules.fuzzy_rule);
assert!(rules.obsolete_rule);
assert!(!rules.untranslated_rule);
}
#[test]
fn test_rules_new_untranslated_flag() {
let rules = Rules::new(vec![Box::new(untranslated::UntranslatedRule {})]);
assert!(!rules.fuzzy_rule);
assert!(!rules.obsolete_rule);
assert!(rules.untranslated_rule);
}
#[test]
fn test_rules_new_spelling_flags() {
let rules = Rules::new(vec![
Box::new(spelling::SpellingCtxtRule {}),
Box::new(spelling::SpellingIdRule {}),
Box::new(spelling::SpellingStrRule {}),
]);
assert!(rules.spelling_ctxt_rule);
assert!(rules.spelling_id_rule);
assert!(rules.spelling_str_rule);
assert!(!rules.fuzzy_rule);
}
#[test]
fn test_rules_new_all_flags() {
let rules = Rules::new(vec![
Box::new(fuzzy::FuzzyRule {}),
Box::new(obsolete::ObsoleteRule {}),
Box::new(untranslated::UntranslatedRule {}),
Box::new(spelling::SpellingCtxtRule {}),
Box::new(spelling::SpellingIdRule {}),
Box::new(spelling::SpellingStrRule {}),
]);
assert!(rules.fuzzy_rule);
assert!(rules.obsolete_rule);
assert!(rules.untranslated_rule);
assert!(rules.spelling_ctxt_rule);
assert!(rules.spelling_id_rule);
assert!(rules.spelling_str_rule);
assert_eq!(rules.enabled.len(), 6);
}
#[test]
fn test_rules_new_non_special_rule() {
let rules = Rules::new(vec![Box::new(blank::BlankRule {})]);
assert_eq!(rules.enabled.len(), 1);
assert!(!rules.fuzzy_rule);
assert!(!rules.obsolete_rule);
assert!(!rules.untranslated_rule);
assert!(!rules.spelling_ctxt_rule);
assert!(!rules.spelling_id_rule);
assert!(!rules.spelling_str_rule);
}
#[test]
fn test_rule_display() {
let rule: Rule = Box::new(blank::BlankRule {});
let display = format!("{rule}");
assert!(display.starts_with("blank: "));
assert!(display.contains(rule.description()));
}
#[test]
fn test_get_unknown_rules_empty_names() {
let names: Vec<String> = vec![];
let all_names = all_rules_name_set();
let unknown = get_unknown_rules(&names, &all_names);
assert!(unknown.is_empty());
}
#[test]
fn test_get_unknown_rules_all_known() {
let names = vec![String::from("blank"), String::from("fuzzy")];
let all_names = all_rules_name_set();
let unknown = get_unknown_rules(&names, &all_names);
assert!(unknown.is_empty());
}
#[test]
fn test_get_unknown_rules_one_unknown() {
let names = vec![String::from("blank"), String::from("nonexistent")];
let all_names = all_rules_name_set();
let unknown = get_unknown_rules(&names, &all_names);
assert_eq!(unknown, vec!["nonexistent"]);
}
#[test]
fn test_get_unknown_rules_multiple_unknown() {
let names = vec![
String::from("blank"),
String::from("zzz-unknown"),
String::from("aaa-unknown"),
];
let all_names = all_rules_name_set();
let unknown = get_unknown_rules(&names, &all_names);
assert_eq!(unknown, vec!["aaa-unknown", "zzz-unknown"]);
}
#[test]
fn test_get_unknown_rules_special_rules_ignored() {
let names = vec![
String::from("all"),
String::from("checks"),
String::from("default"),
String::from("spelling"),
];
let all_names = all_rules_name_set();
let unknown = get_unknown_rules(&names, &all_names);
assert!(unknown.is_empty());
}
#[test]
fn test_get_unknown_rules_special_mixed_with_unknown() {
let names = vec![String::from("all"), String::from("does-not-exist")];
let all_names = all_rules_name_set();
let unknown = get_unknown_rules(&names, &all_names);
assert_eq!(unknown, vec!["does-not-exist"]);
}
#[test]
fn test_get_selected_rules_default() {
let config = make_config(vec!["default"], vec![], vec![]);
let rules = get_selected_rules(&config).unwrap();
let names = rule_names(&rules);
let all = get_all_rules();
let expected_defaults: Vec<&str> = all
.iter()
.filter(|r| r.is_default())
.map(|r| r.name())
.collect();
for name in &expected_defaults {
assert!(names.contains(name), "missing default rule: {name}");
}
let non_defaults: Vec<&str> = all
.iter()
.filter(|r| !r.is_default())
.map(|r| r.name())
.collect();
for name in &non_defaults {
assert!(!names.contains(name), "unexpected non-default rule: {name}");
}
}
#[test]
fn test_get_selected_rules_all() {
let config = make_config(vec!["all"], vec![], vec![]);
let rules = get_selected_rules(&config).unwrap();
let all = get_all_rules();
assert_eq!(rules.enabled.len(), all.len());
}
#[test]
fn test_get_selected_rules_checks() {
let config = make_config(vec!["checks"], vec![], vec![]);
let rules = get_selected_rules(&config).unwrap();
let names = rule_names(&rules);
let all = get_all_rules();
let expected_checks: Vec<&str> = all
.iter()
.filter(|r| r.is_check())
.map(|r| r.name())
.collect();
for name in &expected_checks {
assert!(names.contains(name), "missing check rule: {name}");
}
let non_checks: Vec<&str> = all
.iter()
.filter(|r| !r.is_check())
.map(|r| r.name())
.collect();
for name in &non_checks {
assert!(!names.contains(name), "unexpected non-check rule: {name}");
}
}
#[test]
fn test_get_selected_rules_spelling() {
let config = make_config(vec!["spelling"], vec![], vec![]);
let rules = get_selected_rules(&config).unwrap();
let names = rule_names(&rules);
assert!(names.contains(&"spelling-ctxt"));
assert!(names.contains(&"spelling-id"));
assert!(names.contains(&"spelling-str"));
assert_eq!(rules.enabled.len(), 3);
}
#[test]
fn test_get_selected_rules_single_rule() {
let config = make_config(vec!["blank"], vec![], vec![]);
let rules = get_selected_rules(&config).unwrap();
let names = rule_names(&rules);
assert_eq!(names, vec!["blank"]);
}
#[test]
fn test_get_selected_rules_multiple_explicit() {
let config = make_config(vec!["blank", "fuzzy", "tabs"], vec![], vec![]);
let rules = get_selected_rules(&config).unwrap();
let names = rule_names(&rules);
assert_eq!(names, vec!["blank", "fuzzy", "tabs"]);
}
#[test]
fn test_get_selected_rules_default_plus_spelling() {
let config = make_config(vec!["default", "spelling"], vec![], vec![]);
let rules = get_selected_rules(&config).unwrap();
let names = rule_names(&rules);
assert!(names.contains(&"spelling-ctxt"));
assert!(names.contains(&"spelling-id"));
assert!(names.contains(&"spelling-str"));
assert!(names.contains(&"blank"));
}
#[test]
fn test_get_selected_rules_sorted_by_name() {
let config = make_config(vec!["all"], vec![], vec![]);
let rules = get_selected_rules(&config).unwrap();
let names = rule_names(&rules);
let mut sorted = names.clone();
sorted.sort_unstable();
assert_eq!(names, sorted, "rules should be sorted by name");
}
#[test]
fn test_get_selected_rules_ignore() {
let config = make_config(vec!["default"], vec!["blank", "tabs"], vec![]);
let rules = get_selected_rules(&config).unwrap();
let names = rule_names(&rules);
assert!(!names.contains(&"blank"));
assert!(!names.contains(&"tabs"));
}
#[test]
fn test_get_selected_rules_ignore_all_selected() {
let config = make_config(vec!["blank"], vec!["blank"], vec![]);
let rules = get_selected_rules(&config).unwrap();
assert!(rules.enabled.is_empty());
}
#[test]
fn test_get_selected_rules_severity_does_not_filter_rules() {
let config = make_config(vec!["all"], vec![], vec![Severity::Error]);
let rules = get_selected_rules(&config).unwrap();
let all = get_all_rules();
assert_eq!(rules.enabled.len(), all.len());
}
#[test]
fn test_new_diag_respects_severity_filter() {
let mut checker = Checker::new(b"");
checker.config.check.severity = vec![Severity::Error];
let rule = blank::BlankRule {};
assert!(rule.new_diag(&checker, Severity::Error, "boom").is_some());
assert!(rule.new_diag(&checker, Severity::Warning, "boom").is_none());
assert!(rule.new_diag(&checker, Severity::Info, "boom").is_none());
}
#[test]
fn test_new_diag_empty_filter_allows_all() {
let checker = Checker::new(b"");
let rule = blank::BlankRule {};
assert!(rule.new_diag(&checker, Severity::Error, "boom").is_some());
assert!(rule.new_diag(&checker, Severity::Warning, "boom").is_some());
assert!(rule.new_diag(&checker, Severity::Info, "boom").is_some());
}
#[test]
fn test_get_selected_rules_unknown_select_error() {
let config = make_config(vec!["nonexistent-rule"], vec![], vec![]);
let result = get_selected_rules(&config);
match result {
Err(err) => {
let err = err.to_string();
assert!(
err.contains("unknown selected rules"),
"error should mention unknown selected rules, got: {err}"
);
assert!(err.contains("nonexistent-rule"));
}
Ok(_) => panic!("expected error for unknown selected rule"),
}
}
#[test]
fn test_get_selected_rules_unknown_ignore_error() {
let config = make_config(vec!["default"], vec!["nonexistent-rule"], vec![]);
let result = get_selected_rules(&config);
match result {
Err(err) => {
let err = err.to_string();
assert!(
err.contains("unknown rules to ignore"),
"error should mention unknown rules to ignore, got: {err}"
);
assert!(err.contains("nonexistent-rule"));
}
Ok(_) => panic!("expected error for unknown ignored rule"),
}
}
#[test]
fn test_get_selected_rules_flags_set_correctly() {
let config = make_config(vec!["all"], vec![], vec![]);
let rules = get_selected_rules(&config).unwrap();
assert!(rules.fuzzy_rule);
assert!(rules.obsolete_rule);
assert!(rules.untranslated_rule);
assert!(rules.spelling_ctxt_rule);
assert!(rules.spelling_id_rule);
assert!(rules.spelling_str_rule);
}
#[test]
fn test_get_selected_rules_default_flags() {
let config = make_config(vec!["default"], vec![], vec![]);
let rules = get_selected_rules(&config).unwrap();
assert!(!rules.fuzzy_rule);
assert!(!rules.obsolete_rule);
assert!(!rules.untranslated_rule);
assert!(!rules.spelling_ctxt_rule);
assert!(!rules.spelling_id_rule);
assert!(!rules.spelling_str_rule);
}
#[test]
fn test_run_rules_returns_zero() {
let args = args::RulesArgs;
let exit_code = run_rules(&args);
assert_eq!(exit_code, 0);
}
}