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#[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 #[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 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 pub fn file_not_found(name: &str) -> Self {
85 Error::from_kind(ErrorKind::FileNotFound {
86 name: name.to_owned(),
87 })
88 }
89
90 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 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 pub fn is_parse(&self) -> bool {
131 matches!(
132 &*self.kind,
133 ErrorKind::Parse { .. }
134 | ErrorKind::FileTooLarge { .. }
135 | ErrorKind::FileInvalidUtf8 { .. }
136 )
137 }
138
139 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}