kdl/
error.rs

1use std::{error::Error, fmt::Display, iter, sync::Arc};
2
3use miette::{Diagnostic, LabeledSpan, Severity, SourceSpan};
4
5#[cfg(doc)]
6use {
7    crate::KdlNode,
8    std::convert::{TryFrom, TryInto},
9};
10
11/// The toplevel `Error` type for KDL: this is returned when a KDL document
12/// failed to parse entirely.
13///
14/// This diagnostic implements [`miette::Diagnostic`] and can be used to
15/// display detailed, pretty-printed diagnostic messages when using
16/// [`miette::Result`] and the `"fancy"` feature flag for `miette`:
17///
18/// ```no_run
19/// fn main() -> miette::Result<()> {
20///     "foo 1.".parse::<kdl::KdlDocument>()?;
21///     Ok(())
22/// }
23/// ```
24///
25/// This will display a message like:
26/// ```text
27/// Error:
28///   × Expected valid value.
29///    ╭────
30///  1 │ foo 1.
31///    ·     ─┬
32///    ·      ╰── invalid float
33///    ╰────
34///   help: Floating point numbers must be base 10, and have numbers after the decimal point.
35/// ```
36#[derive(Debug, Clone, Eq, PartialEq)]
37pub struct KdlError {
38    /// Original input that this failure came from.
39    pub input: Arc<String>,
40
41    /// Sub-diagnostics for this failure.
42    pub diagnostics: Vec<KdlDiagnostic>,
43}
44
45impl Display for KdlError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        write!(f, "Failed to parse KDL document")
48    }
49}
50impl Error for KdlError {}
51
52impl Diagnostic for KdlError {
53    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
54        Some(&self.input)
55    }
56
57    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
58        Some(Box::new(
59            self.diagnostics.iter().map(|d| d as &dyn Diagnostic),
60        ))
61    }
62}
63
64/// An individual diagnostic message for a KDL parsing issue.
65///
66/// While generally signifying errors, they can also be treated as warnings.
67#[derive(Debug, Clone, Eq, PartialEq)]
68pub struct KdlDiagnostic {
69    /// Shared source for the diagnostic.
70    pub input: Arc<String>,
71
72    /// Offset in chars of the error.
73    pub span: SourceSpan,
74
75    /// Message for the error itself.
76    pub message: Option<String>,
77
78    /// Label text for this span. Defaults to `"here"`.
79    pub label: Option<String>,
80
81    /// Suggestion for fixing the parser error.
82    pub help: Option<String>,
83
84    /// Severity level for the Diagnostic.
85    pub severity: Severity,
86}
87
88impl Display for KdlDiagnostic {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        let message = self
91            .message
92            .clone()
93            .unwrap_or_else(|| "Unexpected error".into());
94        write!(f, "{message}")
95    }
96}
97impl Error for KdlDiagnostic {}
98
99impl Diagnostic for KdlDiagnostic {
100    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
101        Some(&self.input)
102    }
103
104    fn severity(&self) -> Option<Severity> {
105        Some(self.severity)
106    }
107
108    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
109        self.help.as_ref().map(|s| Box::new(s) as Box<dyn Display>)
110    }
111
112    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
113        let label = self.label.clone().unwrap_or_else(|| "here".to_owned());
114        let labeled_span = LabeledSpan::new_with_span(Some(label), self.span);
115
116        Some(Box::new(iter::once(labeled_span)))
117    }
118}
119
120#[cfg(feature = "v1")]
121impl From<kdlv1::KdlError> for KdlError {
122    fn from(value: kdlv1::KdlError) -> Self {
123        let input = Arc::new(value.input);
124        Self {
125            input: input.clone(),
126            diagnostics: vec![KdlDiagnostic {
127                input,
128                span: SourceSpan::new(value.span.offset().into(), value.span.len()),
129                message: Some(format!("{}", value.kind)),
130                label: value.label.map(|x| x.into()),
131                help: value.help.map(|x| x.into()),
132                severity: Severity::Error,
133            }],
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn kdl_error() {
144        let kdl_diagnostic = KdlDiagnostic {
145            input: Default::default(),
146            span: SourceSpan::new(0.into(), 0),
147            message: Default::default(),
148            label: Default::default(),
149            help: Default::default(),
150            severity: Default::default(),
151        };
152
153        let kdl_error = KdlError {
154            input: Arc::new("bark? i guess?".to_owned()),
155            diagnostics: vec![kdl_diagnostic.clone(), kdl_diagnostic],
156        };
157
158        // Test `Error` impl
159        assert_eq!(kdl_error.to_string(), "Failed to parse KDL document");
160        assert!(kdl_error.source().is_none());
161
162        // Test `Diagnostic` impl
163        let related: Vec<_> = kdl_error.related().unwrap().collect();
164        assert_eq!(related.len(), 2);
165        assert_eq!(
166            kdl_error
167                .source_code()
168                .unwrap()
169                .read_span(&SourceSpan::new(0.into(), 5), 0, 0)
170                .unwrap()
171                .data(),
172            b"bark?"
173        );
174    }
175
176    #[test]
177    fn kdl_diagnostic() {
178        let mut kdl_diagnostic = KdlDiagnostic {
179            input: Arc::new("Catastrophic failure!!!".to_owned()),
180            span: SourceSpan::new(0.into(), 3),
181            message: None,
182            label: Some("cute".to_owned()),
183            help: Some("try harder?".to_owned()),
184            severity: Severity::Error,
185        };
186
187        // Test `Error` impl
188        assert_eq!(kdl_diagnostic.to_string(), "Unexpected error");
189        assert!(kdl_diagnostic.source().is_none());
190
191        kdl_diagnostic.message = Some("mega bad news, kiddo".to_owned());
192
193        assert_eq!(kdl_diagnostic.to_string(), "mega bad news, kiddo");
194        assert!(kdl_diagnostic.source().is_none());
195
196        // Test `Diagnostic` impl
197        let labels: Vec<_> = kdl_diagnostic.labels().unwrap().collect();
198        assert_eq!(labels.len(), 1);
199        assert_eq!(labels[0].label().unwrap(), "cute");
200        assert_eq!(
201            kdl_diagnostic
202                .source_code()
203                .unwrap()
204                .read_span(labels[0].inner(), 0, 0)
205                .unwrap()
206                .data(),
207            b"Cat"
208        );
209        assert_eq!(kdl_diagnostic.help().unwrap().to_string(), "try harder?");
210        assert_eq!(kdl_diagnostic.severity().unwrap(), Severity::Error);
211    }
212}