use std::path::Path;
use crate::project::{ResolvedCitations, ResolvedLabels};
use crate::semantic::SemanticModel;
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode};
use super::diagnostic::{Diagnostic, Severity};
pub mod deprecated_command;
pub mod dollar_display_math;
pub mod duplicate_label;
pub mod mismatched_delimiter;
pub mod missing_nonbreaking_space;
pub mod obsolete_environment;
pub mod undefined_citation;
pub mod undefined_ref;
pub use deprecated_command::DeprecatedCommand;
pub use dollar_display_math::DollarDisplayMath;
pub use duplicate_label::DuplicateLabel;
pub use mismatched_delimiter::MismatchedDelimiter;
pub use missing_nonbreaking_space::MissingNonbreakingSpace;
pub use obsolete_environment::ObsoleteEnvironment;
pub use undefined_citation::UndefinedCitation;
pub use undefined_ref::UndefinedRef;
pub struct RuleContext<'a> {
pub path: &'a Path,
pub root: &'a SyntaxNode,
pub model: &'a SemanticModel,
pub resolution: Option<&'a ResolvedLabels>,
pub citations: Option<&'a ResolvedCitations>,
}
pub trait Rule: Send + Sync {
fn id(&self) -> &'static str;
fn default_severity(&self) -> Severity {
Severity::Warning
}
fn interests(&self) -> &'static [SyntaxKind] {
&[]
}
fn check(&self, el: &SyntaxElement, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let _ = (el, ctx, sink);
}
fn check_file(&self, ctx: &RuleContext<'_>, sink: &mut Vec<Diagnostic>) {
let _ = (ctx, sink);
}
}
pub fn all_rules() -> Vec<Box<dyn Rule>> {
vec![
Box::new(DuplicateLabel),
Box::new(DeprecatedCommand),
Box::new(MissingNonbreakingSpace),
Box::new(ObsoleteEnvironment),
Box::new(DollarDisplayMath),
Box::new(MismatchedDelimiter),
Box::new(UndefinedRef),
Box::new(UndefinedCitation),
]
}
pub const ALL_RULE_IDS: &[&str] = &[
"duplicate-label",
"deprecated-command",
"missing-nonbreaking-space",
"obsolete-environment",
"dollar-display-math",
"mismatched-delimiter",
"undefined-ref",
"undefined-citation",
];
fn all_known_rule_ids() -> impl Iterator<Item = &'static str> {
ALL_RULE_IDS
.iter()
.copied()
.chain(crate::bib::linter::ALL_BIB_RULE_IDS.iter().copied())
}
pub const PARSE_RULE_ID: &str = "parse";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuleSelection {
active: Vec<&'static str>,
}
impl RuleSelection {
pub fn resolve(select: Option<&[String]>, ignore: &[String]) -> (Self, Vec<String>) {
let mut unknown = Vec::new();
for id in select.iter().flat_map(|v| v.iter()).chain(ignore.iter()) {
if !all_known_rule_ids().any(|known| known == id) {
unknown.push(id.clone());
}
}
let base: Vec<&'static str> = match select {
Some(picks) => all_known_rule_ids()
.filter(|id| picks.iter().any(|p| p == id))
.collect(),
None => all_known_rule_ids().collect(),
};
let active = base
.into_iter()
.filter(|id| !ignore.iter().any(|i| i == id))
.collect();
(Self { active }, unknown)
}
pub fn all() -> Self {
Self {
active: all_known_rule_ids().collect(),
}
}
pub fn is_active(&self, rule: &str) -> bool {
rule == PARSE_RULE_ID || self.active.contains(&rule)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_and_id_list_agree() {
let ids: Vec<&str> = all_rules().iter().map(|r| r.id()).collect();
assert_eq!(ids, ALL_RULE_IDS);
}
#[test]
fn all_selection_keeps_every_rule_and_parse() {
let sel = RuleSelection::all();
for id in ALL_RULE_IDS {
assert!(sel.is_active(id), "{id} should be active");
}
assert!(sel.is_active(PARSE_RULE_ID));
}
#[test]
fn select_restricts_to_listed_rules_but_keeps_parse() {
let (sel, unknown) = RuleSelection::resolve(Some(&["duplicate-label".to_string()]), &[]);
assert!(unknown.is_empty());
assert!(sel.is_active("duplicate-label"));
assert!(!sel.is_active("deprecated-command"));
assert!(sel.is_active(PARSE_RULE_ID));
}
#[test]
fn ignore_subtracts_from_default_set() {
let (sel, unknown) = RuleSelection::resolve(None, &["deprecated-command".to_string()]);
assert!(unknown.is_empty());
assert!(!sel.is_active("deprecated-command"));
assert!(sel.is_active("duplicate-label"));
}
#[test]
fn ignore_overrides_select() {
let (sel, _) = RuleSelection::resolve(
Some(&["duplicate-label".to_string(), "undefined-ref".to_string()]),
&["undefined-ref".to_string()],
);
assert!(sel.is_active("duplicate-label"));
assert!(!sel.is_active("undefined-ref"));
}
#[test]
fn bib_rules_are_active_by_default() {
let sel = RuleSelection::all();
for id in crate::bib::linter::ALL_BIB_RULE_IDS {
assert!(sel.is_active(id), "{id} should be active");
}
let (sel, unknown) = RuleSelection::resolve(None, &[]);
assert!(unknown.is_empty());
assert!(sel.is_active("missing-required-field"));
}
#[test]
fn bib_rules_are_selectable_and_ignorable() {
let (sel, unknown) =
RuleSelection::resolve(Some(&["missing-required-field".to_string()]), &[]);
assert!(unknown.is_empty(), "bib id must be recognized, not unknown");
assert!(sel.is_active("missing-required-field"));
assert!(!sel.is_active("duplicate-label"));
let (sel, unknown) = RuleSelection::resolve(None, &["missing-required-field".to_string()]);
assert!(unknown.is_empty());
assert!(!sel.is_active("missing-required-field"));
assert!(sel.is_active("duplicate-label"));
}
#[test]
fn unknown_ids_are_reported() {
let (_, unknown) = RuleSelection::resolve(
Some(&["duplicate-label".to_string(), "no-such-rule".to_string()]),
&["also-bogus".to_string()],
);
assert_eq!(
unknown,
vec!["no-such-rule".to_string(), "also-bogus".to_string()]
);
}
}