protox/
error.rs

1use std::{fmt, io, path::PathBuf};
2
3use miette::{Diagnostic, NamedSource, SourceCode, SourceOffset, SourceSpan};
4use prost_reflect::DescriptorError;
5use protox_parse::ParseError;
6use thiserror::Error;
7
8use crate::file::File;
9
10/// An error that can occur when compiling protobuf files.
11#[derive(Diagnostic, Error)]
12#[error(transparent)]
13#[diagnostic(transparent)]
14pub struct Error {
15    kind: Box<ErrorKind>,
16}
17
18#[derive(Debug, Diagnostic, Error)]
19pub(crate) enum ErrorKind {
20    #[error("{}", err)]
21    #[diagnostic(forward(err))]
22    Parse { err: ParseError },
23    #[error("{}", err)]
24    #[diagnostic(forward(err))]
25    Check { err: DescriptorError },
26    #[error("error opening file '{path}'")]
27    OpenFile {
28        name: String,
29        path: PathBuf,
30        #[source]
31        err: io::Error,
32    },
33    #[error("file '{name}' is too large")]
34    #[diagnostic(help("the maximum file length is 2,147,483,647 bytes"))]
35    FileTooLarge { name: String },
36    #[error("file '{name}' is not valid utf-8")]
37    FileInvalidUtf8 { name: String },
38    #[error("file '{name}' not found")]
39    FileNotFound { name: String },
40    #[error("import '{name}' not found")]
41    ImportNotFound {
42        #[label("imported here")]
43        span: Option<SourceSpan>,
44        #[source_code]
45        source_code: NamedSource<String>,
46        name: String,
47    },
48    #[error("import cycle detected: {cycle}")]
49    CircularImport { name: String, cycle: String },
50    #[error("file '{path}' is not in any include path")]
51    FileNotIncluded { path: PathBuf },
52    #[error("path '{path}' is shadowed by '{shadow}' in the include paths")]
53    #[diagnostic(help("either pass '{}' as the input file, or re-order the include paths so that '{}' comes first", shadow.display(), path.display()))]
54    FileShadowed {
55        name: String,
56        path: PathBuf,
57        shadow: PathBuf,
58    },
59    /// This variant is intermediate and should not be present in the final error.
60    #[error("import '{name}' was listed twice")]
61    DuplicateImport {
62        #[label("imported here")]
63        span: Option<SourceSpan>,
64        #[source_code]
65        source_code: NamedSource<String>,
66        name: String,
67    },
68    #[error(transparent)]
69    Custom(Box<dyn std::error::Error + Send + Sync>),
70}
71
72impl Error {
73    /// Creates an instance of [`struct@Error`] with an arbitrary payload.
74    pub fn new<E>(error: E) -> Self
75    where
76        E: Into<Box<dyn std::error::Error + Send + Sync>>,
77    {
78        Error::from_kind(ErrorKind::Custom(error.into()))
79    }
80
81    /// Creates an instance of [`struct@Error`] indicating that an imported file could not be found.
82    ///
83    /// This error should be returned by [`FileResolver`](crate::file::FileResolver) instances if a file is not found.
84    pub fn file_not_found(name: &str) -> Self {
85        Error::from_kind(ErrorKind::FileNotFound {
86            name: name.to_owned(),
87        })
88    }
89
90    /// The file in which this error occurred, if available.
91    pub fn file(&self) -> Option<&str> {
92        match &*self.kind {
93            ErrorKind::Parse { err } => Some(err.file()),
94            ErrorKind::Check { err } => err.file(),
95            ErrorKind::OpenFile { name, .. }
96            | ErrorKind::FileTooLarge { name }
97            | ErrorKind::FileInvalidUtf8 { name }
98            | ErrorKind::FileNotFound { name }
99            | ErrorKind::CircularImport { name, .. }
100            | ErrorKind::FileShadowed { name, .. } => Some(name),
101            ErrorKind::FileNotIncluded { .. } => None,
102            ErrorKind::Custom(_) => None,
103            ErrorKind::ImportNotFound { source_code, .. }
104            | ErrorKind::DuplicateImport { source_code, .. } => Some(source_code.name()),
105        }
106    }
107
108    pub(crate) fn from_kind(kind: ErrorKind) -> Self {
109        Error {
110            kind: Box::new(kind),
111        }
112    }
113
114    #[cfg(test)]
115    pub(crate) fn kind(&self) -> &ErrorKind {
116        &self.kind
117    }
118
119    /// Returns true if this is an instance of [`Error::file_not_found()`]
120    pub fn is_file_not_found(&self) -> bool {
121        matches!(
122            &*self.kind,
123            ErrorKind::FileNotFound { .. }
124                | ErrorKind::ImportNotFound { .. }
125                | ErrorKind::FileNotIncluded { .. }
126        )
127    }
128
129    /// Returns true if this error is caused by an invalid protobuf source file.
130    pub fn is_parse(&self) -> bool {
131        matches!(
132            &*self.kind,
133            ErrorKind::Parse { .. }
134                | ErrorKind::FileTooLarge { .. }
135                | ErrorKind::FileInvalidUtf8 { .. }
136        )
137    }
138
139    /// Returns true if this error is caused by an IO error while opening a file.
140    pub fn is_io(&self) -> bool {
141        match &*self.kind {
142            ErrorKind::OpenFile { .. } => true,
143            ErrorKind::Custom(err) if err.downcast_ref::<io::Error>().is_some() => true,
144            _ => false,
145        }
146    }
147
148    pub(crate) fn into_import_error(self, file: &File, import_idx: usize) -> Self {
149        match *self.kind {
150            ErrorKind::FileNotFound { name } => {
151                let source_code: NamedSource<String> =
152                    NamedSource::new(file.name(), file.source().unwrap_or_default().to_owned());
153                let span = find_import_span(file, import_idx);
154                Error::from_kind(ErrorKind::ImportNotFound {
155                    span,
156                    source_code,
157                    name,
158                })
159            }
160            _ => self,
161        }
162    }
163
164    pub(crate) fn duplicated_import(name: String, file: &File, import_idx: usize) -> Error {
165        let source_code: NamedSource<String> =
166            NamedSource::new(file.name(), file.source().unwrap_or_default().to_owned());
167        let span = find_import_span(file, import_idx);
168        Error::from_kind(ErrorKind::DuplicateImport {
169            span,
170            source_code,
171            name,
172        })
173    }
174}
175
176fn find_import_span(file: &File, import_idx: usize) -> Option<SourceSpan> {
177    if let Some(sci) = &file.descriptor.source_code_info {
178        if let Some(source) = file.source() {
179            for location in &sci.location {
180                if location.path == [3, import_idx as i32] {
181                    if location.span.len() != 3 {
182                        continue;
183                    }
184                    let start_line = location.span[0] as usize + 1;
185                    let start_col = location.span[1] as usize + 1;
186                    let end_col = location.span[2] as usize + 1;
187                    return Some(SourceSpan::new(
188                        SourceOffset::from_location(source, start_line, start_col),
189                        end_col - start_col,
190                    ));
191                }
192            }
193        }
194    }
195    None
196}
197
198impl From<DescriptorError> for Error {
199    fn from(err: DescriptorError) -> Self {
200        Error::from_kind(ErrorKind::Check { err })
201    }
202}
203
204impl From<ParseError> for Error {
205    fn from(err: ParseError) -> Self {
206        Error::from_kind(ErrorKind::Parse { err })
207    }
208}
209
210impl From<io::Error> for Error {
211    fn from(err: io::Error) -> Self {
212        Error::new(err)
213    }
214}
215
216impl fmt::Debug for Error {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        match &*self.kind {
219            ErrorKind::Parse { err } => err.fmt(f),
220            ErrorKind::Check { err } => err.fmt(f),
221            ErrorKind::OpenFile { err, .. } => write!(f, "{}: {}", self, err),
222            ErrorKind::FileTooLarge { .. }
223            | ErrorKind::FileInvalidUtf8 { .. }
224            | ErrorKind::FileNotFound { .. }
225            | ErrorKind::CircularImport { .. }
226            | ErrorKind::FileNotIncluded { .. }
227            | ErrorKind::FileShadowed { .. } => write!(f, "{}", self),
228            ErrorKind::Custom(err) => err.fmt(f),
229            ErrorKind::DuplicateImport {
230                span, source_code, ..
231            }
232            | ErrorKind::ImportNotFound {
233                span, source_code, ..
234            } => {
235                write!(f, "{}:", source_code.name())?;
236                if let Some(span) = span {
237                    if let Ok(span_contents) = source_code.read_span(span, 0, 0) {
238                        write!(
239                            f,
240                            "{}:{}: ",
241                            span_contents.line() + 1,
242                            span_contents.column() + 1
243                        )?;
244                    }
245                }
246                write!(f, "{}", self)
247            }
248        }
249    }
250}
251
252#[test]
253fn fmt_debug_io() {
254    let err = Error::from_kind(ErrorKind::OpenFile {
255        name: "file.proto".into(),
256        path: "path/to/file.proto".into(),
257        err: io::Error::new(io::ErrorKind::Other, "io error"),
258    });
259
260    assert!(err.is_io());
261    assert_eq!(err.file(), Some("file.proto"));
262    assert_eq!(
263        format!("{:?}", err),
264        "error opening file 'path/to/file.proto': io error"
265    );
266}
267
268#[test]
269fn fmt_debug_parse() {
270    let err = Error::from(protox_parse::parse("file.proto", "invalid").unwrap_err());
271
272    assert!(err.is_parse());
273    assert_eq!(err.file(), Some("file.proto"));
274    assert_eq!(
275        format!("{:?}", err),
276        "file.proto:1:1: expected 'enum', 'extend', 'import', 'message', 'option', 'service', 'package' or ';', but found 'invalid'"
277    );
278}