Skip to main content

rd_ast/
shape.rs

1//! Shared diagnostics for malformed or unexpected Rd tree shapes.
2
3use std::fmt;
4
5use crate::{RdNode, RdTag};
6
7/// A human-oriented explanation of a shape mismatch, including its path.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct RdShapeError {
10    path: crate::RdPath,
11    tag: Option<RdTag>,
12    kind: RdShapeErrorKind,
13}
14
15impl RdShapeError {
16    pub(crate) fn new(path: crate::RdPath, tag: Option<RdTag>, kind: RdShapeErrorKind) -> Self {
17        Self { path, tag, kind }
18    }
19    pub fn path(&self) -> &crate::RdPath {
20        &self.path
21    }
22    pub fn tag(&self) -> Option<&RdTag> {
23        self.tag.as_ref()
24    }
25    pub fn kind(&self) -> &RdShapeErrorKind {
26        &self.kind
27    }
28}
29
30impl fmt::Display for RdShapeError {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        write!(f, "{}", self.kind)?;
33        if let Some(tag) = &self.tag {
34            write!(f, " for {}", tag.as_rd_tag())?;
35        }
36        write!(f, " at {}", self.path)
37    }
38}
39
40impl std::error::Error for RdShapeError {}
41
42/// The shared vocabulary of shape errors returned by strict views.
43#[derive(Debug, Clone, PartialEq, Eq)]
44#[non_exhaustive]
45pub enum RdShapeErrorKind {
46    UnexpectedNode {
47        expected: RdExpectedNode,
48        actual: RdNodeKind,
49    },
50    UnexpectedOption,
51    MissingOption,
52    WrongArity {
53        expected: RdArity,
54        actual: usize,
55    },
56    Duplicate {
57        construct: RdConstruct,
58        first_path: crate::RdPath,
59    },
60    UnexpectedContent {
61        actual: RdNodeKind,
62    },
63    InvalidValue {
64        construct: RdConstruct,
65        value: String,
66    },
67    CountMismatch {
68        construct: RdConstruct,
69        expected: usize,
70        actual: usize,
71    },
72    MissingFollowing {
73        construct: RdConstruct,
74    },
75    DefinitionMismatch {
76        construct: RdConstruct,
77    },
78    UnsupportedRepresentation {
79        construct: RdConstruct,
80    },
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[non_exhaustive]
85pub enum RdNodeKind {
86    Text,
87    RCode,
88    Verb,
89    Comment,
90    Tagged,
91    Group,
92    Raw,
93}
94
95impl RdNodeKind {
96    pub fn of(node: &RdNode) -> Self {
97        match node {
98            RdNode::Text(_) => Self::Text,
99            RdNode::RCode(_) => Self::RCode,
100            RdNode::Verb(_) => Self::Verb,
101            RdNode::Comment(_) => Self::Comment,
102            RdNode::Tagged(_) => Self::Tagged,
103            RdNode::Group(_) => Self::Group,
104            RdNode::Raw(_) => Self::Raw,
105        }
106    }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110#[non_exhaustive]
111pub enum RdArity {
112    Exactly(usize),
113    Range { min: usize, max: usize },
114    AtLeast(usize),
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118#[non_exhaustive]
119pub enum RdExpectedNode {
120    Tagged,
121    Link,
122    Href,
123    List,
124    Tabular,
125    Equation,
126    Sexpr,
127    RdOpts,
128    Group,
129    Item,
130    TextLike,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134#[non_exhaustive]
135pub enum RdConstruct {
136    Tag(RdTag),
137    OptionKey(String),
138    ColumnSpec,
139    TableRow,
140    SystemMacro(String),
141    SystemMacroExpansion(String),
142}
143
144/// Returns whether a node is ignorable trivia between structural items.
145pub fn is_inter_item_trivia(node: &RdNode) -> bool {
146    matches!(node, RdNode::Text(text) if text.trim().is_empty())
147        || matches!(node, RdNode::Comment(_))
148}
149
150impl fmt::Display for RdShapeErrorKind {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        match self {
153            Self::UnexpectedNode { expected, actual } => {
154                write!(f, "expected {expected}, found {actual}")
155            }
156            Self::UnexpectedOption => f.write_str("unexpected option"),
157            Self::MissingOption => f.write_str("missing option"),
158            Self::WrongArity { expected, actual } => {
159                write!(f, "expected {expected} children, found {actual}")
160            }
161            Self::Duplicate { construct, .. } => write!(f, "duplicate {construct}"),
162            Self::UnexpectedContent { actual } => write!(f, "unexpected {actual} content"),
163            Self::InvalidValue { construct, value } => {
164                write!(f, "invalid {construct} value '{value}'")
165            }
166            Self::CountMismatch {
167                construct,
168                expected,
169                actual,
170            } => write!(f, "expected {expected} {construct} entries, found {actual}"),
171            Self::MissingFollowing { construct } => write!(f, "missing following {construct}"),
172            Self::DefinitionMismatch { construct } => {
173                write!(f, "definition mismatch for {construct}")
174            }
175            Self::UnsupportedRepresentation { construct } => {
176                write!(f, "unsupported representation for {construct}")
177            }
178        }
179    }
180}
181
182impl fmt::Display for RdNodeKind {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        write!(
185            f,
186            "{} node",
187            match self {
188                Self::Text => "text",
189                Self::RCode => "R code",
190                Self::Verb => "verbatim",
191                Self::Comment => "comment",
192                Self::Tagged => "tagged",
193                Self::Group => "group",
194                Self::Raw => "raw",
195            }
196        )
197    }
198}
199impl fmt::Display for RdExpectedNode {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        f.write_str(match self {
202            Self::Tagged => "tagged node",
203            Self::Link => r"\link node",
204            Self::Href => r"\href node",
205            Self::List => "list node",
206            Self::Tabular => r"\tabular node",
207            Self::Equation => "equation node",
208            Self::Sexpr => r"\Sexpr node",
209            Self::RdOpts => r"\RdOpts node",
210            Self::Group => "group",
211            Self::Item => "item",
212            Self::TextLike => "text-like node",
213        })
214    }
215}
216impl fmt::Display for RdArity {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        match self {
219            Self::Exactly(n) => write!(f, "exactly {n}"),
220            Self::Range { min, max } => write!(f, "{min}..={max}"),
221            Self::AtLeast(n) => write!(f, "at least {n}"),
222        }
223    }
224}
225impl fmt::Display for RdConstruct {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        match self {
228            Self::Tag(tag) => write!(f, "{}", tag.as_rd_tag()),
229            Self::OptionKey(key) => write!(f, "option {key}"),
230            Self::ColumnSpec => f.write_str("column specification"),
231            Self::TableRow => f.write_str("table row"),
232            Self::SystemMacro(name) => write!(f, "system macro {name}"),
233            Self::SystemMacroExpansion(name) => write!(f, "system macro {name} expansion"),
234        }
235    }
236}
237
238impl UnexpectedRawNode {
239    /// Converts corpus classification details into the shared shape vocabulary.
240    pub fn shape_error(&self, path: crate::RdPath) -> RdShapeError {
241        RdShapeError::new(
242            path,
243            self.tag().map(RdTag::from_rd_tag),
244            RdShapeErrorKind::UnexpectedNode {
245                expected: RdExpectedNode::Tagged,
246                actual: RdNodeKind::Raw,
247            },
248        )
249    }
250}
251
252use crate::UnexpectedRawNode;
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    #[test]
258    fn displays_shape_error() {
259        let error = RdShapeError::new(
260            crate::RdPath::new(vec![crate::RdPathSegment::TopLevel(2)]),
261            Some(RdTag::Title),
262            RdShapeErrorKind::UnexpectedOption,
263        );
264        assert_eq!(
265            error.to_string(),
266            r"unexpected option for \title at top-level[2]"
267        );
268    }
269
270    #[test]
271    fn displays_value_and_count_shape_errors() {
272        let path = crate::RdPath::new(vec![crate::RdPathSegment::TopLevel(0)]);
273        let invalid = RdShapeError::new(
274            path.clone(),
275            None,
276            RdShapeErrorKind::InvalidValue {
277                construct: RdConstruct::ColumnSpec,
278                value: "bad".into(),
279            },
280        );
281        assert_eq!(
282            invalid.to_string(),
283            "invalid column specification value 'bad' at top-level[0]"
284        );
285        let count = RdShapeError::new(
286            path,
287            None,
288            RdShapeErrorKind::CountMismatch {
289                construct: RdConstruct::TableRow,
290                expected: 2,
291                actual: 3,
292            },
293        );
294        assert_eq!(
295            count.to_string(),
296            "expected 2 table row entries, found 3 at top-level[0]"
297        );
298    }
299
300    #[cfg(feature = "rds")]
301    #[test]
302    fn unexpected_raw_classifier_bridges_to_shared_shape_error() {
303        let raw = crate::producer::raw_node(Some("OTHER".into()), None, vec![], None, vec![]);
304        let crate::RawNodeClassification::Unexpected(unexpected) = crate::classify_raw_node(&raw)
305        else {
306            panic!("expected an unexpected raw classification");
307        };
308        let path = crate::RdPath::new(vec![crate::RdPathSegment::TopLevel(4)]);
309        let error = unexpected.shape_error(path.clone());
310        assert_eq!(error.path(), &path);
311        assert_eq!(error.tag(), Some(&crate::RdTag::Unknown("OTHER".into())));
312        assert!(matches!(
313            error.kind(),
314            RdShapeErrorKind::UnexpectedNode {
315                expected: RdExpectedNode::Tagged,
316                actual: RdNodeKind::Raw
317            }
318        ));
319    }
320}