use std::{fmt, io, path::PathBuf};
use miette::{Diagnostic, NamedSource, SourceCode, SourceOffset, SourceSpan};
use prost_reflect::DescriptorError;
use protox_parse::ParseError;
use thiserror::Error;
use crate::file::File;
#[derive(Diagnostic, Error)]
#[error(transparent)]
#[diagnostic(transparent)]
pub struct Error {
kind: Box<ErrorKind>,
}
#[derive(Debug, Diagnostic, Error)]
pub(crate) enum ErrorKind {
#[error("{}", err)]
#[diagnostic(forward(err))]
Parse { err: ParseError },
#[error("{}", err)]
#[diagnostic(forward(err))]
Check { err: DescriptorError },
#[error("error opening file '{path}'")]
OpenFile {
name: String,
path: PathBuf,
#[source]
err: io::Error,
},
#[error("file '{name}' is too large")]
#[diagnostic(help("the maximum file length is 2,147,483,647 bytes"))]
FileTooLarge { name: String },
#[error("file '{name}' is not valid utf-8")]
FileInvalidUtf8 { name: String },
#[error("file '{name}' not found")]
FileNotFound { name: String },
#[error("import '{name}' not found")]
ImportNotFound {
#[label("imported here")]
span: Option<SourceSpan>,
#[source_code]
source_code: NamedSource<String>,
name: String,
},
#[error("import cycle detected: {cycle}")]
CircularImport { name: String, cycle: String },
#[error("file '{path}' is not in any include path")]
FileNotIncluded { path: PathBuf },
#[error("path '{path}' is shadowed by '{shadow}' in the include paths")]
#[diagnostic(help("either pass '{}' as the input file, or re-order the include paths so that '{}' comes first", shadow.display(), path.display()))]
FileShadowed {
name: String,
path: PathBuf,
shadow: PathBuf,
},
#[error("import '{name}' was listed twice")]
DuplicateImport {
#[label("imported here")]
span: Option<SourceSpan>,
#[source_code]
source_code: NamedSource<String>,
name: String,
},
#[error(transparent)]
Custom(Box<dyn std::error::Error + Send + Sync>),
}
impl Error {
pub fn new<E>(error: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Error::from_kind(ErrorKind::Custom(error.into()))
}
pub fn file_not_found(name: &str) -> Self {
Error::from_kind(ErrorKind::FileNotFound {
name: name.to_owned(),
})
}
pub fn file(&self) -> Option<&str> {
match &*self.kind {
ErrorKind::Parse { err } => Some(err.file()),
ErrorKind::Check { err } => err.file(),
ErrorKind::OpenFile { name, .. }
| ErrorKind::FileTooLarge { name }
| ErrorKind::FileInvalidUtf8 { name }
| ErrorKind::FileNotFound { name }
| ErrorKind::CircularImport { name, .. }
| ErrorKind::FileShadowed { name, .. } => Some(name),
ErrorKind::FileNotIncluded { .. } => None,
ErrorKind::Custom(_) => None,
ErrorKind::ImportNotFound { source_code, .. }
| ErrorKind::DuplicateImport { source_code, .. } => Some(source_code.name()),
}
}
pub(crate) fn from_kind(kind: ErrorKind) -> Self {
Error {
kind: Box::new(kind),
}
}
#[cfg(test)]
pub(crate) fn kind(&self) -> &ErrorKind {
&self.kind
}
pub fn is_file_not_found(&self) -> bool {
matches!(
&*self.kind,
ErrorKind::FileNotFound { .. }
| ErrorKind::ImportNotFound { .. }
| ErrorKind::FileNotIncluded { .. }
)
}
pub fn is_parse(&self) -> bool {
matches!(
&*self.kind,
ErrorKind::Parse { .. }
| ErrorKind::FileTooLarge { .. }
| ErrorKind::FileInvalidUtf8 { .. }
)
}
pub fn is_io(&self) -> bool {
match &*self.kind {
ErrorKind::OpenFile { .. } => true,
ErrorKind::Custom(err) if err.downcast_ref::<io::Error>().is_some() => true,
_ => false,
}
}
pub(crate) fn into_import_error(self, file: &File, import_idx: usize) -> Self {
match *self.kind {
ErrorKind::FileNotFound { name } => {
let source_code: NamedSource<String> =
NamedSource::new(file.name(), file.source().unwrap_or_default().to_owned());
let span = find_import_span(file, import_idx);
Error::from_kind(ErrorKind::ImportNotFound {
span,
source_code,
name,
})
}
_ => self,
}
}
pub(crate) fn duplicated_import(name: String, file: &File, import_idx: usize) -> Error {
let source_code: NamedSource<String> =
NamedSource::new(file.name(), file.source().unwrap_or_default().to_owned());
let span = find_import_span(file, import_idx);
Error::from_kind(ErrorKind::DuplicateImport {
span,
source_code,
name,
})
}
}
fn find_import_span(file: &File, import_idx: usize) -> Option<SourceSpan> {
if let Some(sci) = &file.descriptor.source_code_info {
if let Some(source) = file.source() {
for location in &sci.location {
if location.path == [3, import_idx as i32] {
if location.span.len() != 3 {
continue;
}
let start_line = location.span[0] as usize + 1;
let start_col = location.span[1] as usize + 1;
let end_col = location.span[2] as usize + 1;
return Some(SourceSpan::new(
SourceOffset::from_location(source, start_line, start_col),
end_col - start_col,
));
}
}
}
}
None
}
impl From<DescriptorError> for Error {
fn from(err: DescriptorError) -> Self {
Error::from_kind(ErrorKind::Check { err })
}
}
impl From<ParseError> for Error {
fn from(err: ParseError) -> Self {
Error::from_kind(ErrorKind::Parse { err })
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Error::new(err)
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &*self.kind {
ErrorKind::Parse { err } => err.fmt(f),
ErrorKind::Check { err } => err.fmt(f),
ErrorKind::OpenFile { err, .. } => write!(f, "{}: {}", self, err),
ErrorKind::FileTooLarge { .. }
| ErrorKind::FileInvalidUtf8 { .. }
| ErrorKind::FileNotFound { .. }
| ErrorKind::CircularImport { .. }
| ErrorKind::FileNotIncluded { .. }
| ErrorKind::FileShadowed { .. } => write!(f, "{}", self),
ErrorKind::Custom(err) => err.fmt(f),
ErrorKind::DuplicateImport {
span, source_code, ..
}
| ErrorKind::ImportNotFound {
span, source_code, ..
} => {
write!(f, "{}:", source_code.name())?;
if let Some(span) = span {
if let Ok(span_contents) = source_code.read_span(span, 0, 0) {
write!(
f,
"{}:{}: ",
span_contents.line() + 1,
span_contents.column() + 1
)?;
}
}
write!(f, "{}", self)
}
}
}
}
#[test]
fn fmt_debug_io() {
let err = Error::from_kind(ErrorKind::OpenFile {
name: "file.proto".into(),
path: "path/to/file.proto".into(),
err: io::Error::new(io::ErrorKind::Other, "io error"),
});
assert!(err.is_io());
assert_eq!(err.file(), Some("file.proto"));
assert_eq!(
format!("{:?}", err),
"error opening file 'path/to/file.proto': io error"
);
}
#[test]
fn fmt_debug_parse() {
let err = Error::from(protox_parse::parse("file.proto", "invalid").unwrap_err());
assert!(err.is_parse());
assert_eq!(err.file(), Some("file.proto"));
assert_eq!(
format!("{:?}", err),
"file.proto:1:1: expected 'enum', 'extend', 'import', 'message', 'option', 'service', 'package' or ';', but found 'invalid'"
);
}