use std::collections::HashMap;
use crate::finding::{Finding, FindingLevel, SourcePos};
pub(crate) const STRUCTURE_LEVELS: &[(&str, FindingLevel)] = &[
("structure/section-missing", FindingLevel::Error),
("structure/section-order", FindingLevel::Error),
("structure/duplicate-section", FindingLevel::Error),
("structure/key-collision", FindingLevel::Error),
("structure/anchor-missing", FindingLevel::Error),
("structure/block-missing", FindingLevel::Error),
("structure/block-kind", FindingLevel::Error),
("structure/gap-count", FindingLevel::Error),
("structure/repeat-count", FindingLevel::Error),
("structure/heading-depth-jump", FindingLevel::Warn),
];
pub(crate) const CONTENT_LEVELS: &[(&str, FindingLevel)] = &[
("content/table/column-missing", FindingLevel::Error),
("content/table/column-extra", FindingLevel::Error),
("content/table/min-rows", FindingLevel::Error),
("content/table/cell", FindingLevel::Error),
("content/list/item-kind", FindingLevel::Error),
("content/list/min-items", FindingLevel::Error),
("content/code/lang", FindingLevel::Error),
("content/max-words", FindingLevel::Error),
("frontmatter/yaml", FindingLevel::Error),
("frontmatter/enum", FindingLevel::Error),
("frontmatter/unknown-key", FindingLevel::Error),
("frontmatter/type", FindingLevel::Error),
("frontmatter/required", FindingLevel::Error),
("frontmatter/refine", FindingLevel::Error),
];
pub(crate) const TEXT_LEVELS: &[(&str, FindingLevel)] = &[
("text/requires", FindingLevel::Error),
("text/forbids", FindingLevel::Error),
("text/count", FindingLevel::Error),
];
pub(crate) const RULE_LEVELS: &[(&str, FindingLevel)] = &[
("task/post-mortem-when-worked", FindingLevel::Error),
("task/completion-note-when-closed", FindingLevel::Error),
("summary/mentions-outcome", FindingLevel::Error),
("summary/names-contract", FindingLevel::Warn),
];
const UNHINTED: &[&str] = &["frontmatter/yaml"];
pub(crate) fn takes_hint(id: &str) -> bool {
!UNHINTED.contains(&id)
}
#[derive(Debug, Clone)]
pub struct Registry {
levels: HashMap<&'static str, FindingLevel>,
}
impl Default for Registry {
fn default() -> Self {
let levels = STRUCTURE_LEVELS
.iter()
.chain(CONTENT_LEVELS)
.chain(TEXT_LEVELS)
.chain(RULE_LEVELS)
.copied()
.collect();
Self { levels }
}
}
impl Registry {
pub fn level_for(&self, id: &str) -> FindingLevel {
self.levels.get(id).copied().unwrap_or(FindingLevel::Error)
}
}
#[derive(Debug, Clone)]
pub struct FindingSpec {
pub id: String,
pub message: String,
pub level: Option<FindingLevel>,
pub pos: Option<SourcePos>,
pub hint: Option<String>,
}
impl FindingSpec {
pub fn new(id: impl Into<String>, message: impl Into<String>) -> Self {
Self {
id: id.into(),
message: message.into(),
level: None,
pos: None,
hint: None,
}
}
pub fn level(mut self, level: FindingLevel) -> Self {
self.level = Some(level);
self
}
pub fn pos(mut self, pos: SourcePos) -> Self {
self.pos = Some(pos);
self
}
pub fn hint_opt(mut self, hint: Option<&str>) -> Self {
self.hint = hint.map(str::to_string);
self
}
}
pub struct Ctx<'a> {
pub path: &'a str,
registry: &'a Registry,
}
impl<'a> Ctx<'a> {
pub fn new(path: &'a str, registry: &'a Registry) -> Self {
Self { path, registry }
}
pub fn finding(&self, spec: FindingSpec) -> Finding {
let level = spec
.level
.unwrap_or_else(|| self.registry.level_for(&spec.id));
Finding {
id: spec.id,
level,
path: self.path.to_string(),
pos: spec.pos,
message: spec.message,
hint: spec.hint,
fix: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_defaults_match_the_ts_tables() {
let r = Registry::default();
assert_eq!(
r.level_for("structure/section-missing"),
FindingLevel::Error
);
assert_eq!(
r.level_for("structure/heading-depth-jump"),
FindingLevel::Warn
);
assert_eq!(r.level_for("summary/names-contract"), FindingLevel::Warn);
assert_eq!(r.level_for("text/requires"), FindingLevel::Error);
assert_eq!(r.level_for("totally/unregistered"), FindingLevel::Error);
}
#[test]
fn ctx_finding_stamps_path_and_default_level() {
let r = Registry::default();
let ctx = Ctx::new("docs/note.md", &r);
let f = ctx.finding(
FindingSpec::new("structure/heading-depth-jump", "skip").pos(SourcePos::at(5, 1)),
);
assert_eq!(f.level, FindingLevel::Warn);
assert_eq!(f.path, "docs/note.md");
assert_eq!(f.pos, Some(SourcePos::at(5, 1)));
}
#[test]
fn explicit_level_overrides_the_registry() {
let r = Registry::default();
let ctx = Ctx::new("a.md", &r);
let f = ctx.finding(
FindingSpec::new("structure/section-missing", "m").level(FindingLevel::Report),
);
assert_eq!(f.level, FindingLevel::Report);
}
#[test]
fn pos_is_absent_unless_supplied() {
let r = Registry::default();
let ctx = Ctx::new("a.md", &r);
assert_eq!(ctx.finding(FindingSpec::new("x/y", "m")).pos, None);
}
}