use crate::check::Severity;
use crate::manifest::{Line, PolicyLine};
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Policy {
pub severities: Vec<(String, Severity)>,
pub skips: Vec<String>,
pub settings: std::collections::BTreeMap<String, String>,
}
impl Policy {
pub fn is_empty(&self) -> bool {
self.severities.is_empty() && self.skips.is_empty() && self.settings.is_empty()
}
pub fn from_lines(lines: &[Line]) -> (Policy, Vec<String>) {
let mut policy = Policy::default();
let mut notes = Vec::new();
let names_something = |target: &str| {
crate::registry::CHECKS
.iter()
.any(|c| crate::names_check(c.name, target).is_some())
|| lines
.iter()
.filter(|l| l.is_check())
.any(|l| crate::names_check(&l.id(), target).is_some())
};
for line in lines {
let Line::Policy { what, lineno } = line else {
continue;
};
let target = match what {
PolicyLine::Severity { target, .. } | PolicyLine::Skip { target } => target,
PolicyLine::Set { key, value } => {
policy.settings.insert(key.clone(), value.clone());
continue;
}
};
if !names_something(target) {
let kind = match what {
PolicyLine::Severity { .. } => "severity",
PolicyLine::Skip { .. } => "skip",
PolicyLine::Set { .. } => unreachable!("set lines have no target"),
};
notes.push(format!(
"{}:{lineno}: {kind} {target:?} names no check here",
crate::manifest::MANIFEST
));
continue;
}
match what {
PolicyLine::Severity { target, severity } => {
policy.severities.push((target.clone(), *severity));
}
PolicyLine::Skip { target } => policy.skips.push(target.clone()),
PolicyLine::Set { .. } => unreachable!("set lines have no target"),
}
}
(policy, notes)
}
}
static POLICY: std::sync::OnceLock<Policy> = std::sync::OnceLock::new();
pub fn install(policy: Policy) {
let _ = POLICY.set(policy);
}
pub fn current() -> &'static Policy {
static EMPTY: Policy = Policy {
severities: Vec::new(),
skips: Vec::new(),
settings: std::collections::BTreeMap::new(),
};
POLICY.get().unwrap_or(&EMPTY)
}
pub fn union_skips(config_skips: Vec<String>, policy: &Policy) -> Vec<String> {
let mut all = config_skips;
for s in &policy.skips {
if !all.contains(s) {
all.push(s.clone());
}
}
all
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::parse_lines;
#[test]
fn collects_severities_and_skips_in_file_order() {
let lines = parse_lines(
"severity clippy warn\nskip yamllint\nseverity pre-push-cargo-test block\n",
);
let (p, notes) = Policy::from_lines(&lines);
assert!(notes.is_empty(), "{notes:?}");
assert_eq!(
p.severities,
vec![
("clippy".to_string(), Severity::Warn),
("pre-push-cargo-test".to_string(), Severity::Block),
]
);
assert_eq!(p.skips, vec!["yamllint".to_string()]);
}
#[test]
fn a_declared_check_below_the_policy_line_still_validates() {
let lines =
parse_lines("severity smoke warn\npre-commit smoke * block ./smoke.sh\n");
let (p, notes) = Policy::from_lines(&lines);
assert!(notes.is_empty(), "{notes:?}");
assert_eq!(p.severities.len(), 1);
}
#[test]
fn a_tool_pin_does_not_validate_a_policy_target() {
let lines = parse_lines("tool ruffian 0.4\nseverity ruffian warn\n");
let (p, notes) = Policy::from_lines(&lines);
assert!(p.severities.is_empty());
assert_eq!(notes.len(), 1, "{notes:?}");
assert!(notes[0].contains("names no check here"), "{notes:?}");
assert!(notes[0].contains("amont.conf:2"), "{notes:?}");
}
#[test]
fn an_unmatched_target_is_a_note_not_a_check() {
let lines = parse_lines("severity clipy warn\n");
let (p, notes) = Policy::from_lines(&lines);
assert!(p.is_empty());
assert_eq!(
notes,
vec!["amont.conf:1: severity \"clipy\" names no check here"]
);
}
#[test]
fn triggers_and_short_names_are_valid_targets() {
let lines = parse_lines("skip pre-commit\nseverity ban-terms warn\n");
let (p, notes) = Policy::from_lines(&lines);
assert!(notes.is_empty(), "{notes:?}");
assert_eq!(p.skips, vec!["pre-commit".to_string()]);
assert_eq!(p.severities.len(), 1);
}
#[test]
fn union_adds_policy_skips_without_duplicating() {
let p = Policy {
severities: Vec::new(),
skips: vec!["yamllint".into(), "clippy".into()],
settings: std::collections::BTreeMap::new(),
};
let got = union_skips(vec!["clippy".into()], &p);
assert_eq!(got, vec!["clippy".to_string(), "yamllint".to_string()]);
}
}