Skip to main content

isoloom_spec/syntax/
spans.rs

1//! Where each field is written: a field path → line and column index, read from the YAML's
2//! events once at load, so every problem (lowering's included) points at its line.
3
4use std::collections::BTreeMap;
5
6use saphyr_parser::{Event, Parser};
7
8use super::problem::{FieldPath, Problem, Span};
9
10/// Each written field's place in the file, by its path.
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
12pub struct Spans(BTreeMap<FieldPath, Span>);
13
14/// A mapping or a list being read, and where its next item goes.
15enum Frame {
16    /// A mapping: the key of the value being read, if a key was read.
17    Map { path: FieldPath, key: Option<String> },
18    /// A list: the index of its next item.
19    Seq { path: FieldPath, next: usize },
20}
21
22impl Spans {
23    /// The places of every key and list item in `yaml`. A document the parser refuses gives the
24    /// places read before the refusal (the YAML error itself carries its own).
25    #[must_use]
26    pub fn of(yaml: &str) -> Spans {
27        let mut spans = Spans::default();
28        let mut stack: Vec<Frame> = Vec::new();
29        for (event, span) in Parser::new_from_str(yaml).map_while(Result::ok) {
30            let at = Span {
31                line: span.start.line(),
32                column: span.start.col() + 1,
33            };
34            match event {
35                Event::Scalar(text, ..) => spans.scalar(&mut stack, &text, at),
36                Event::Alias(_) => {
37                    spans.value(&mut stack, at);
38                }
39                Event::MappingStart(..) | Event::SequenceStart(..) => {
40                    let path = spans.value(&mut stack, at).unwrap_or_default();
41                    stack.push(if matches!(event, Event::MappingStart(..)) {
42                        Frame::Map { path, key: None }
43                    } else {
44                        Frame::Seq { path, next: 0 }
45                    });
46                }
47                Event::MappingEnd | Event::SequenceEnd => {
48                    stack.pop();
49                }
50                _ => {}
51            }
52        }
53        spans
54    }
55
56    /// A scalar: a mapping's key (its place recorded), or a value.
57    fn scalar(&mut self, stack: &mut [Frame], text: &str, at: Span) {
58        if let Some(Frame::Map { path, key: key @ None }) = stack.last_mut() {
59            self.0.entry(path.key(text)).or_insert(at);
60            *key = Some(text.to_owned());
61        } else {
62            self.value(stack, at);
63        }
64    }
65
66    /// A value starts at `at`: its path, once the frame it is in has moved on to the next item.
67    fn value(&mut self, stack: &mut [Frame], at: Span) -> Option<FieldPath> {
68        match stack.last_mut()? {
69            Frame::Map { path, key } => Some(path.key(&key.take()?)),
70            Frame::Seq { path, next } => {
71                let item = path.index(*next);
72                *next += 1;
73                self.0.entry(item.clone()).or_insert(at);
74                Some(item)
75            }
76        }
77    }
78
79    /// The place of `path`: its own, else its nearest written parent's (a field a group gives a
80    /// machine is placed at the machine).
81    #[must_use]
82    pub fn find(&self, path: &FieldPath) -> Option<Span> {
83        // The path, then each enclosing field: one per separator, so the walk always ends.
84        let levels = path.as_str().matches(['.', '[']).count() + 1;
85        std::iter::successors(Some(path.clone()), FieldPath::parent)
86            .take(levels)
87            .find_map(|p| self.0.get(&p).copied())
88    }
89
90    /// `problem`, placed (unless it already is).
91    #[must_use]
92    pub fn place(&self, mut problem: Problem) -> Problem {
93        if problem.span.is_none() {
94            problem.span = self.find(&problem.path);
95        }
96        problem
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn an_alias_takes_its_place_in_a_list() {
106        let spans = Spans::of(
107            "base: &b { a: 1 }
108list:
109  - *b
110  - two
111",
112        );
113        let at = |p: &str| spans.find(&FieldPath::new(p)).map(|s| (s.line, s.column));
114        assert_eq!((at("list[0]"), at("list[1]")), (Some((3, 5)), Some((4, 5))));
115    }
116
117    #[test]
118    fn keys_and_items_are_placed_in_block_and_flow_style() {
119        let yaml = "version: 1\nmachines:\n  web:\n    networks: { lab: 10 }\n    files:\n      - { path: /a, input: FLAG }\n      - path: /b\n";
120        let spans = Spans::of(yaml);
121        let at = |p: &str| spans.find(&FieldPath::new(p)).map(|s| (s.line, s.column));
122        assert_eq!(at("version"), Some((1, 1)));
123        assert_eq!(at("machines.web"), Some((3, 3)));
124        assert_eq!(at("machines.web.networks.lab"), Some((4, 17)));
125        assert_eq!(at("machines.web.files[0]"), Some((6, 9)));
126        assert_eq!(at("machines.web.files[0].input"), Some((6, 21)));
127        assert_eq!(at("machines.web.files[1].path"), Some((7, 9)));
128        // A field that isn't written takes its nearest written parent's place.
129        assert_eq!(at("machines.web.resources.cpus"), Some((3, 3)));
130        assert_eq!(at("machines.nope.deep"), Some((2, 1)));
131        assert_eq!(at("checks"), None);
132    }
133}