use std::collections::{BTreeMap, HashSet};
use regex::Regex;
use super::{
CoreSchema, DerivationSource, Example, FormatRegistry, FormatSource, Grammar, KeyDerivation,
LifecycleDimension, Only, RegexCache, RuleCommon, TypeRegistry,
};
use crate::{Finding, Problem, RepoPath, Severity, Spanned, Yaml};
pub const CORE_SCHEMA_KEYS: &[&str] = &[
"line-limit",
"guidance",
"lifecycle",
"frontmatter",
"frontmatter-open",
"prohibited-patterns",
"formats",
"types",
"examples",
];
const DIMENSION_KEYS: &[&str] = &["key", "states", "guidance"];
pub fn extensions(node: &Spanned, known: &[&str]) -> BTreeMap<String, Spanned> {
node.as_map()
.unwrap_or(&[])
.iter()
.filter(|(k, _)| !known.contains(&k.value.as_str()))
.map(|(k, v)| (k.value.clone(), v.clone()))
.collect()
}
pub fn lower_formats(
yaml: &Spanned,
grammar: &Grammar,
registry: &FormatRegistry,
regexes: &RegexCache,
schema: &RepoPath,
schema_id: &str,
findings: &mut Vec<Finding>,
) -> FormatRegistry {
if yaml.get("formats").is_none() && yaml.get("types").is_none() {
return registry.clone();
}
let none = Spanned::new(Yaml::Map(vec![]), yaml.line);
let (own, refused) = FormatRegistry::layer(
yaml.get("formats").unwrap_or(&none),
yaml.get("types"),
FormatSource::Schema(schema_id.to_string()),
grammar,
regexes,
schema,
Some(registry),
);
findings.extend(refused);
registry.layered(&own)
}
pub fn lower_common(
node: &Spanned,
schema: &CoreSchema,
path: &RepoPath,
) -> (RuleCommon, Vec<Finding>) {
let regexes = RegexCache::new();
let mut l = Lowerer::new(®exes, path, TypeRegistry::default());
l.lifecycle = schema.lifecycle.clone();
let common = l.common(node);
(common, l.findings)
}
pub struct Lowerer<'a> {
pub regexes: &'a RegexCache,
pub schema: &'a RepoPath,
pub findings: Vec<Finding>,
pub rule_ids: HashSet<String>,
pub lifecycle: Vec<LifecycleDimension>,
pub types: TypeRegistry,
}
pub fn str_of(node: &Spanned, key: &str) -> Option<String> {
node.get(key).and_then(|v| v.as_str()).map(str::to_string)
}
pub fn derivation(node: &Spanned) -> KeyDerivation {
let source = match str_of(node, "from") {
Some(key) => DerivationSource::From(key),
None => DerivationSource::Rule(str_of(node, "rule").unwrap_or_default()),
};
KeyDerivation {
source,
separator: str_of(node, "separator").unwrap_or_default(),
}
}
pub fn bool_of(node: &Spanned, key: &str) -> bool {
node.get(key).and_then(|v| v.as_bool()).unwrap_or(false)
}
pub fn usize_of(node: &Spanned, key: &str) -> Option<usize> {
node.get(key)
.and_then(|v| v.as_int())
.map(|i| usize::try_from(i).unwrap_or(0))
}
pub fn list_of(node: &Spanned, key: &str) -> Vec<String> {
node.get(key)
.and_then(|v| v.as_str_or_list())
.unwrap_or_default()
}
impl<'a> Lowerer<'a> {
pub fn new(regexes: &'a RegexCache, schema: &'a RepoPath, types: TypeRegistry) -> Self {
Lowerer {
regexes,
schema,
findings: Vec::new(),
rule_ids: HashSet::new(),
lifecycle: Vec::new(),
types,
}
}
pub fn fail(&mut self, line: usize, criterion: &'static str, message: impl Into<String>) {
self.findings.push(Finding::error(
self.schema.clone(),
Some(line),
criterion,
message,
));
}
pub fn fail_with(&mut self, line: usize, problem: Problem) {
self.findings
.push(problem.error(self.schema.clone(), Some(line)));
}
pub fn regex(&self, node: &Spanned, key: &str) -> Option<Regex> {
let text = node.get(key)?.as_str()?;
self.regexes.get(text).ok()
}
pub fn prohibitions(&self, node: &Spanned) -> Vec<super::Prohibition> {
super::prohibitions(node, self.regexes)
}
pub fn only(&mut self, node: &Spanned) -> Only {
let mut only = Only::default();
let Some(map) = node.get("only") else {
return only;
};
if self.lifecycle.is_empty() {
self.fail(map.line, "TL_111", "`only` without a lifecycle");
return only;
}
for (dim, states) in map.as_map().unwrap_or(&[]) {
let Some(dimension) = self
.lifecycle
.iter()
.find(|d| d.key == dim.value)
.map(|d| d.states.clone())
else {
self.fail(
dim.line,
"TL_20",
format!(
"`only` names a dimension the lifecycle does not declare: `{}`",
dim.value
),
);
continue;
};
let admitted = states.as_str_or_list().unwrap_or_default();
for s in &admitted {
if !dimension.contains(s) {
self.fail(
states.line,
"TL_20",
format!("`only` names `{s}`, which is no state of `{}`", dim.value),
);
}
}
only.0.insert(dim.value.clone(), admitted);
}
only
}
pub fn common(&mut self, node: &Spanned) -> RuleCommon {
RuleCommon {
severity: match str_of(node, "severity").as_deref() {
Some("warn") => Severity::Warning,
_ => Severity::Error,
},
only: self.only(node),
line: node.line,
}
}
pub fn lower_core(&mut self, yaml: &Spanned, formats: FormatRegistry) -> CoreSchema {
self.lifecycle = yaml
.get("lifecycle")
.and_then(|l| l.as_seq())
.map(|dims| {
dims.iter()
.map(|d| LifecycleDimension {
key: str_of(d, "key").unwrap_or_default(),
states: list_of(d, "states"),
guidance: str_of(d, "guidance"),
line: d.line,
extensions: extensions(d, DIMENSION_KEYS),
})
.collect()
})
.unwrap_or_default();
let mut seen_keys = HashSet::new();
let twice: Vec<(usize, String)> = self
.lifecycle
.iter()
.filter(|d| !seen_keys.insert(d.key.as_str()))
.map(|d| (d.line, d.key.clone()))
.collect();
for (line, key) in twice {
self.fail(
line,
"TL_95",
format!("dimension `{key}` is declared twice"),
);
}
let mut frontmatter = Vec::new();
for (key, node) in yaml
.get("frontmatter")
.and_then(|f| f.as_map())
.unwrap_or(&[])
{
if self.lifecycle.iter().any(|d| d.key == key.value) {
self.fail(
key.line,
"TL_112",
format!(
"`{}` is a lifecycle dimension; it is not repeated under `frontmatter`",
key.value
),
);
}
let c = self.frontmatter_constraint(node, &key.value);
frontmatter.push((key.value.clone(), c));
}
let prohibited_patterns = self.prohibitions(yaml);
let examples = yaml
.get("examples")
.and_then(|e| e.as_seq())
.map(|items| {
items
.iter()
.map(|e| Example {
document: str_of(e, "document").unwrap_or_default(),
line: e.line,
})
.collect()
})
.unwrap_or_default();
CoreSchema {
line_limit: yaml.get("line-limit").and_then(|l| l.as_int()).unwrap_or(0),
lifecycle: self.lifecycle.clone(),
frontmatter,
frontmatter_open: bool_of(yaml, "frontmatter-open"),
prohibited_patterns,
formats,
guidance: str_of(yaml, "guidance"),
examples,
line: yaml.line,
key_lines: yaml
.as_map()
.unwrap_or(&[])
.iter()
.map(|(k, _)| (k.value.clone(), k.line))
.collect(),
}
}
}
mod checks;
mod shapes;