1use std::collections::{BTreeMap, HashSet};
6
7use regex::Regex;
8
9use super::{
10 CoreSchema, DerivationSource, Example, FormatRegistry, FormatSource, Grammar, KeyDerivation,
11 LifecycleDimension, Only, RegexCache, RuleCommon, TypeRegistry,
12};
13use crate::{Finding, Problem, RepoPath, Severity, Spanned, Yaml};
14
15pub const CORE_SCHEMA_KEYS: &[&str] = &[
19 "line-limit",
20 "guidance",
21 "lifecycle",
22 "frontmatter",
23 "frontmatter-open",
24 "prohibited-patterns",
25 "formats",
26 "types",
27 "examples",
28];
29const DIMENSION_KEYS: &[&str] = &["key", "states", "guidance"];
33
34pub fn extensions(node: &Spanned, known: &[&str]) -> BTreeMap<String, Spanned> {
37 node.as_map()
38 .unwrap_or(&[])
39 .iter()
40 .filter(|(k, _)| !known.contains(&k.value.as_str()))
41 .map(|(k, v)| (k.value.clone(), v.clone()))
42 .collect()
43}
44
45pub fn lower_formats(
53 yaml: &Spanned,
54 grammar: &Grammar,
55 registry: &FormatRegistry,
56 regexes: &RegexCache,
57 schema: &RepoPath,
58 schema_id: &str,
59 findings: &mut Vec<Finding>,
60) -> FormatRegistry {
61 if yaml.get("formats").is_none() && yaml.get("types").is_none() {
63 return registry.clone();
64 }
65 let none = Spanned::new(Yaml::Map(vec![]), yaml.line);
66 let (own, refused) = FormatRegistry::layer(
69 yaml.get("formats").unwrap_or(&none),
70 yaml.get("types"),
71 FormatSource::Schema(schema_id.to_string()),
72 grammar,
73 regexes,
74 schema,
75 Some(registry),
76 );
77 findings.extend(refused);
79 registry.layered(&own)
80}
81
82pub fn lower_common(
88 node: &Spanned,
89 schema: &CoreSchema,
90 path: &RepoPath,
91) -> (RuleCommon, Vec<Finding>) {
92 let regexes = RegexCache::new();
94 let mut l = Lowerer::new(®exes, path, TypeRegistry::default());
95 l.lifecycle = schema.lifecycle.clone();
96 let common = l.common(node);
97 (common, l.findings)
98}
99
100pub struct Lowerer<'a> {
104 pub regexes: &'a RegexCache,
106 pub schema: &'a RepoPath,
108 pub findings: Vec<Finding>,
110 pub rule_ids: HashSet<String>,
112 pub lifecycle: Vec<LifecycleDimension>,
114 pub types: TypeRegistry,
116}
117pub fn str_of(node: &Spanned, key: &str) -> Option<String> {
121 node.get(key).and_then(|v| v.as_str()).map(str::to_string)
122}
123
124pub fn derivation(node: &Spanned) -> KeyDerivation {
126 let source = match str_of(node, "from") {
127 Some(key) => DerivationSource::From(key),
128 None => DerivationSource::Rule(str_of(node, "rule").unwrap_or_default()),
129 };
130 KeyDerivation {
131 source,
132 separator: str_of(node, "separator").unwrap_or_default(),
133 }
134}
135
136pub fn bool_of(node: &Spanned, key: &str) -> bool {
138 node.get(key).and_then(|v| v.as_bool()).unwrap_or(false)
139}
140
141pub fn usize_of(node: &Spanned, key: &str) -> Option<usize> {
143 node.get(key)
144 .and_then(|v| v.as_int())
145 .map(|i| usize::try_from(i).unwrap_or(0))
146}
147
148pub fn list_of(node: &Spanned, key: &str) -> Vec<String> {
150 node.get(key)
151 .and_then(|v| v.as_str_or_list())
152 .unwrap_or_default()
153}
154
155impl<'a> Lowerer<'a> {
156 pub fn new(regexes: &'a RegexCache, schema: &'a RepoPath, types: TypeRegistry) -> Self {
159 Lowerer {
161 regexes,
162 schema,
163 findings: Vec::new(),
164 rule_ids: HashSet::new(),
165 lifecycle: Vec::new(),
166 types,
167 }
168 }
169
170 pub fn fail(&mut self, line: usize, criterion: &'static str, message: impl Into<String>) {
172 self.findings.push(Finding::error(
173 self.schema.clone(),
174 Some(line),
175 criterion,
176 message,
177 ));
178 }
179
180 pub fn fail_with(&mut self, line: usize, problem: Problem) {
182 self.findings
183 .push(problem.error(self.schema.clone(), Some(line)));
184 }
185
186 pub fn regex(&self, node: &Spanned, key: &str) -> Option<Regex> {
188 let text = node.get(key)?.as_str()?;
189 self.regexes.get(text).ok()
190 }
191
192 pub fn prohibitions(&self, node: &Spanned) -> Vec<super::Prohibition> {
194 super::prohibitions(node, self.regexes)
195 }
196
197 pub fn only(&mut self, node: &Spanned) -> Only {
201 let mut only = Only::default();
202 let Some(map) = node.get("only") else {
203 return only;
204 };
205 if self.lifecycle.is_empty() {
206 self.fail(map.line, "TL_111", "`only` without a lifecycle");
208 return only;
209 }
210 for (dim, states) in map.as_map().unwrap_or(&[]) {
211 let Some(dimension) = self
212 .lifecycle
213 .iter()
214 .find(|d| d.key == dim.value)
215 .map(|d| d.states.clone())
216 else {
217 self.fail(
218 dim.line,
219 "TL_20",
220 format!(
221 "`only` names a dimension the lifecycle does not declare: `{}`",
222 dim.value
223 ),
224 );
225 continue;
226 };
227 let admitted = states.as_str_or_list().unwrap_or_default();
228 for s in &admitted {
229 if !dimension.contains(s) {
230 self.fail(
231 states.line,
232 "TL_20",
233 format!("`only` names `{s}`, which is no state of `{}`", dim.value),
234 );
235 }
236 }
237 only.0.insert(dim.value.clone(), admitted);
238 }
239 only
240 }
241
242 pub fn common(&mut self, node: &Spanned) -> RuleCommon {
245 RuleCommon {
246 severity: match str_of(node, "severity").as_deref() {
247 Some("warn") => Severity::Warning,
248 _ => Severity::Error,
249 },
250 only: self.only(node),
251 line: node.line,
252 }
253 }
254
255 pub fn lower_core(&mut self, yaml: &Spanned, formats: FormatRegistry) -> CoreSchema {
261 self.lifecycle = yaml
263 .get("lifecycle")
264 .and_then(|l| l.as_seq())
265 .map(|dims| {
266 dims.iter()
267 .map(|d| LifecycleDimension {
268 key: str_of(d, "key").unwrap_or_default(),
269 states: list_of(d, "states"),
270 guidance: str_of(d, "guidance"),
271 line: d.line,
272 extensions: extensions(d, DIMENSION_KEYS),
274 })
275 .collect()
276 })
277 .unwrap_or_default();
278 let mut seen_keys = HashSet::new();
279 let twice: Vec<(usize, String)> = self
280 .lifecycle
281 .iter()
282 .filter(|d| !seen_keys.insert(d.key.as_str()))
283 .map(|d| (d.line, d.key.clone()))
284 .collect();
285 for (line, key) in twice {
286 self.fail(
289 line,
290 "TL_95",
291 format!("dimension `{key}` is declared twice"),
292 );
293 }
294
295 let mut frontmatter = Vec::new();
296 for (key, node) in yaml
297 .get("frontmatter")
298 .and_then(|f| f.as_map())
299 .unwrap_or(&[])
300 {
301 if self.lifecycle.iter().any(|d| d.key == key.value) {
302 self.fail(
305 key.line,
306 "TL_112",
307 format!(
308 "`{}` is a lifecycle dimension; it is not repeated under `frontmatter`",
309 key.value
310 ),
311 );
312 }
313 let c = self.frontmatter_constraint(node, &key.value);
314 frontmatter.push((key.value.clone(), c));
315 }
316 let prohibited_patterns = self.prohibitions(yaml);
319 let examples = yaml
320 .get("examples")
321 .and_then(|e| e.as_seq())
322 .map(|items| {
323 items
324 .iter()
325 .map(|e| Example {
326 document: str_of(e, "document").unwrap_or_default(),
327 line: e.line,
328 })
329 .collect()
330 })
331 .unwrap_or_default();
332 CoreSchema {
333 line_limit: yaml.get("line-limit").and_then(|l| l.as_int()).unwrap_or(0),
334 lifecycle: self.lifecycle.clone(),
335 frontmatter,
336 frontmatter_open: bool_of(yaml, "frontmatter-open"),
337 prohibited_patterns,
338 formats,
339 guidance: str_of(yaml, "guidance"),
340 examples,
341 line: yaml.line,
342 key_lines: yaml
343 .as_map()
344 .unwrap_or(&[])
345 .iter()
346 .map(|(k, _)| (k.value.clone(), k.line))
347 .collect(),
348 }
349 }
350}
351
352mod checks;
353mod shapes;