topiary_core/
error.rs

1//! This module defines all errors that might be propagated out of the library,
2//! including all of the trait implementations one might expect for Errors.
3
4use std::{error::Error, fmt, io, ops::Deref, str, string};
5
6use miette::{Diagnostic, NamedSource, SourceSpan};
7use topiary_tree_sitter_facade::Range;
8
9use crate::tree_sitter::NodeSpan;
10
11/// The various errors the formatter may return.
12#[derive(Debug)]
13pub enum FormatterError {
14    /// The input produced output that isn't idempotent, i.e. formatting the
15    /// output again made further changes. If this happened using our provided
16    /// query files, it is a bug. Please log an issue.
17    Idempotence,
18
19    /// The input produced invalid output, i.e. formatting the output again led
20    /// to a parsing error. If this happened using our provided query files, it
21    /// is a bug. Please log an issue.
22    IdempotenceParsing(Box<FormatterError>),
23
24    /// An internal error occurred. This is a bug. Please log an issue.
25    Internal(String, Option<Box<dyn Error>>),
26
27    // Tree-sitter could not parse the input without errors.
28    Parsing(Box<NodeSpan>),
29    /// The query contains a pattern that had no match in the input file.
30    PatternDoesNotMatch,
31
32    /// There was an error in the query file. If this happened using our
33    /// provided query files, it is a bug. Please log an issue.
34    Query(String, Option<topiary_tree_sitter_facade::QueryError>),
35
36    /// I/O-related errors
37    Io(IoError),
38}
39
40/// A subtype of `FormatterError::Io`
41#[derive(Debug)]
42pub enum IoError {
43    // NOTE: Filesystem-based IO errors _ought_ to become a thing of the past,
44    // once the library and binary code have been completely separated (see
45    // Issue #303).
46    /// A filesystem based IO error, with an additional owned string to provide
47    /// Topiary specific information
48    Filesystem(String, io::Error),
49
50    /// Any other Error with an additional owned string to provide Topiary
51    /// specific information
52    Generic(String, Option<Box<dyn Error>>),
53}
54
55impl FormatterError {
56    fn get_span(&mut self) -> Option<&mut NodeSpan> {
57        match self {
58            Self::Parsing(span) => Some(span),
59            Self::IdempotenceParsing(err) => err.get_span(),
60            _ => None,
61        }
62    }
63    pub fn with_content(mut self, content: String) -> Self {
64        if let Some(span) = self.get_span() {
65            span.set_content(content);
66        }
67        self
68    }
69
70    pub fn with_location(mut self, location: String) -> Self {
71        if let Some(span) = self.get_span() {
72            span.set_location(location);
73        }
74        self
75    }
76}
77
78impl fmt::Display for FormatterError {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        let please_log_message = "If this happened with the built-in query files, it is a bug. It would be\nhelpful if you logged this error at\nhttps://github.com/tweag/topiary/issues/new?assignees=&labels=type%3A+bug&template=bug_report.md";
81        match self {
82            Self::Idempotence => {
83                write!(
84                    f,
85                    "The formatter did not produce the same\nresult when invoked twice (idempotence check).\n\n{please_log_message}"
86                )
87            }
88
89            Self::IdempotenceParsing(_) => {
90                write!(
91                    f,
92                    "The formatter produced invalid output and\nfailed when trying to format twice (idempotence check).\n\n{please_log_message}\n\nThe following is the error received when running the second time, but note\nthat any line and column numbers refer to the formatted code, not the\noriginal input. Run Topiary with the --skip-idempotence flag to see this\ninvalid formatted code."
93                )
94            }
95
96            Self::Parsing(span) => {
97                let report = miette::Report::new(ErrorSpan::from(span));
98                write!(f, "{report:?}")
99            }
100
101            Self::PatternDoesNotMatch => {
102                write!(
103                    f,
104                    "The query contains a pattern that does not match the input"
105                )
106            }
107
108            Self::Internal(message, _)
109            | Self::Query(message, _)
110            | Self::Io(IoError::Filesystem(message, _) | IoError::Generic(message, _)) => {
111                write!(f, "{message}")
112            }
113        }
114    }
115}
116
117impl Error for FormatterError {
118    fn source(&self) -> Option<&(dyn Error + 'static)> {
119        match self {
120            Self::Idempotence
121            | Self::Parsing(_)
122            | Self::PatternDoesNotMatch
123            | Self::Io(IoError::Generic(_, None)) => None,
124            Self::Internal(_, source) => source.as_ref().map(Deref::deref),
125            Self::Query(_, source) => source.as_ref().map(|e| e as &dyn Error),
126            Self::Io(IoError::Filesystem(_, source)) => Some(source),
127            Self::Io(IoError::Generic(_, Some(source))) => Some(source.as_ref()),
128            Self::IdempotenceParsing(source) => Some(source),
129        }
130    }
131}
132
133// NOTE: Filesystem-based IO errors _ought_ to become a thing of the past, once
134// the library and binary code have been completely separated (see Issue #303).
135impl From<io::Error> for FormatterError {
136    fn from(e: io::Error) -> Self {
137        IoError::from(e).into()
138    }
139}
140
141impl From<io::Error> for IoError {
142    fn from(e: io::Error) -> Self {
143        match e.kind() {
144            io::ErrorKind::NotFound => IoError::Filesystem("File not found".into(), e),
145            _ => IoError::Filesystem("Could not read or write to file".into(), e),
146        }
147    }
148}
149impl From<IoError> for FormatterError {
150    fn from(e: IoError) -> Self {
151        Self::Io(e)
152    }
153}
154
155impl From<str::Utf8Error> for FormatterError {
156    fn from(e: str::Utf8Error) -> Self {
157        Self::Io(IoError::Generic(
158            "Input is not valid UTF-8".into(),
159            Some(Box::new(e)),
160        ))
161    }
162}
163
164impl From<string::FromUtf8Error> for FormatterError {
165    fn from(e: string::FromUtf8Error) -> Self {
166        Self::Io(IoError::Generic(
167            "Input is not valid UTF-8".into(),
168            Some(Box::new(e)),
169        ))
170    }
171}
172
173impl From<fmt::Error> for FormatterError {
174    fn from(e: fmt::Error) -> Self {
175        Self::Io(IoError::Generic(
176            "Failed to format output".into(),
177            Some(Box::new(e)),
178        ))
179    }
180}
181
182// We only have to deal with io::BufWriter<Vec<u8>>, but the genericised code is
183// clearer
184impl<W> From<io::IntoInnerError<W>> for FormatterError
185where
186    W: io::Write + fmt::Debug + Send + 'static,
187{
188    fn from(e: io::IntoInnerError<W>) -> Self {
189        Self::Io(IoError::Generic(
190            "Cannot flush internal buffer".into(),
191            Some(Box::new(e)),
192        ))
193    }
194}
195
196impl From<serde_json::Error> for FormatterError {
197    fn from(e: serde_json::Error) -> Self {
198        Self::Internal("Could not serialise JSON output".into(), Some(Box::new(e)))
199    }
200}
201
202impl From<topiary_tree_sitter_facade::LanguageError> for FormatterError {
203    fn from(e: topiary_tree_sitter_facade::LanguageError) -> Self {
204        Self::Internal(
205            "Error while loading language grammar".into(),
206            Some(Box::new(e)),
207        )
208    }
209}
210
211impl From<topiary_tree_sitter_facade::ParserError> for FormatterError {
212    fn from(e: topiary_tree_sitter_facade::ParserError) -> Self {
213        Self::Internal("Error while parsing".into(), Some(Box::new(e)))
214    }
215}
216
217impl From<NodeSpan> for FormatterError {
218    fn from(span: NodeSpan) -> Self {
219        Self::Parsing(Box::new(span))
220    }
221}
222
223#[derive(Diagnostic, Debug)]
224struct ErrorSpan {
225    #[source_code]
226    src: NamedSource<String>,
227    #[label("(ERROR) node")]
228    span: SourceSpan,
229    range: Range,
230}
231
232impl std::fmt::Display for ErrorSpan {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        let start = self.range.start_point();
235        let end = self.range.end_point();
236        write!(
237            f,
238            "Parsing error between line {}, column {} and line {}, column {}",
239            start.row(),
240            start.column(),
241            end.row(),
242            end.column()
243        )
244    }
245}
246
247impl std::error::Error for ErrorSpan {}
248
249impl From<&Box<NodeSpan>> for ErrorSpan {
250    fn from(span: &Box<NodeSpan>) -> Self {
251        Self {
252            src: NamedSource::new(
253                span.location.clone().unwrap_or_default(),
254                span.content.clone().unwrap_or_default(),
255            )
256            .with_language(span.language),
257            span: span.source_span(),
258            range: span.range,
259        }
260    }
261}