use crate::condition::{rule_enabled, ConditionTag};
use crate::config::Profile;
use crate::parser::Document;
use crate::types::{Diagnostic, Language};
pub mod enumeration;
pub mod lexicon;
pub mod readability;
pub mod rhythm;
pub mod structure;
pub mod syntax;
pub use lexicon::all_caps_shouting::AllCapsShouting;
pub use lexicon::consonant_cluster::ConsonantCluster;
pub use lexicon::excessive_nominalization::ExcessiveNominalization;
pub use lexicon::jargon_undefined::JargonUndefined;
pub use lexicon::low_lexical_diversity::LowLexicalDiversity;
pub use lexicon::redundant_intensifier::RedundantIntensifier;
pub use lexicon::unexplained_abbreviation::UnexplainedAbbreviation;
pub use lexicon::weasel_words::WeaselWords;
pub use readability::score::ReadabilityScore;
pub use rhythm::consecutive_long_sentences::ConsecutiveLongSentences;
pub use rhythm::repetitive_connectors::RepetitiveConnectors;
pub use structure::deep_subordination::DeepSubordination;
pub use structure::deeply_nested_lists::DeeplyNestedLists;
pub use structure::excessive_commas::ExcessiveCommas;
pub use structure::heading_jump::HeadingJump;
pub use structure::line_length_wide::LineLengthWide;
pub use structure::long_enumeration::LongEnumeration;
pub use structure::mixed_numeric_format::MixedNumericFormat;
pub use structure::paragraph_too_long::ParagraphTooLong;
pub use structure::sentence_too_long::SentenceTooLong;
pub use syntax::conditional_stacking::ConditionalStacking;
pub use syntax::dense_punctuation_burst::DensePunctuationBurst;
pub use syntax::nested_negation::NestedNegation;
pub use syntax::passive_voice::PassiveVoice;
pub use syntax::unclear_antecedent::UnclearAntecedent;
pub trait Rule {
fn id(&self) -> &'static str;
fn check(&self, document: &Document, language: Language) -> Vec<Diagnostic>;
fn condition_tags(&self) -> &'static [ConditionTag] {
&[ConditionTag::General]
}
}
#[must_use]
pub fn filter_by_conditions(
rules: Vec<Box<dyn Rule>>,
active: &[ConditionTag],
) -> Vec<Box<dyn Rule>> {
rules
.into_iter()
.filter(|r| rule_enabled(r.condition_tags(), active))
.collect()
}
#[must_use]
pub fn default_rules(profile: Profile) -> Vec<Box<dyn Rule>> {
vec![
Box::new(SentenceTooLong::for_profile(profile)),
Box::new(ParagraphTooLong::for_profile(profile)),
Box::new(HeadingJump::for_profile(profile)),
Box::new(DeeplyNestedLists::for_profile(profile)),
Box::new(ExcessiveCommas::for_profile(profile)),
Box::new(ConsecutiveLongSentences::for_profile(profile)),
Box::new(WeaselWords::for_profile(profile)),
Box::new(UnexplainedAbbreviation::for_profile(profile)),
Box::new(JargonUndefined::for_profile(profile)),
Box::new(ExcessiveNominalization::for_profile(profile)),
Box::new(RepetitiveConnectors::for_profile(profile)),
Box::new(ReadabilityScore::for_profile(profile)),
Box::new(LongEnumeration::for_profile(profile)),
Box::new(DeepSubordination::for_profile(profile)),
Box::new(PassiveVoice::for_profile(profile)),
Box::new(UnclearAntecedent::for_profile(profile)),
Box::new(LowLexicalDiversity::for_profile(profile)),
Box::new(NestedNegation::for_profile(profile)),
Box::new(ConditionalStacking::for_profile(profile)),
Box::new(AllCapsShouting::for_profile(profile)),
Box::new(LineLengthWide::for_profile(profile)),
Box::new(MixedNumericFormat::for_profile(profile)),
Box::new(RedundantIntensifier::for_profile(profile)),
Box::new(DensePunctuationBurst::for_profile(profile)),
Box::new(ConsonantCluster::for_profile(profile)),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_rules_is_non_empty() {
let rules = default_rules(Profile::Public);
assert!(!rules.is_empty());
}
#[test]
fn every_default_rule_is_tagged_general() {
for rule in default_rules(Profile::Public) {
assert!(
rule.condition_tags().contains(&ConditionTag::General),
"rule `{}` is missing the `general` condition tag (v0.2 baseline)",
rule.id()
);
}
}
#[test]
fn filter_by_conditions_keeps_general_rules() {
let kept = filter_by_conditions(default_rules(Profile::Public), &[]);
assert_eq!(kept.len(), 25);
}
#[test]
fn each_rule_has_a_well_formed_id() {
for rule in default_rules(Profile::Public) {
let id = rule.id();
assert!(!id.is_empty(), "empty rule id");
assert!(
id.chars()
.all(|c| c.is_ascii_lowercase() || c == '-' || c == '.'),
"rule id `{id}` contains unexpected characters (only lowercase, `-`, `.` allowed)"
);
let parts: Vec<&str> = id.split('.').collect();
assert_eq!(
parts.len(),
2,
"rule id `{id}` must be `category.rule-name`"
);
assert!(
!parts[0].is_empty() && !parts[1].is_empty(),
"rule id `{id}` has an empty category or name half"
);
}
}
}