use std::fmt;
use std::path::PathBuf;
#[derive(Debug)]
pub enum Error {
Io(std::io::Error),
Parse {
file: Option<PathBuf>,
source: syn::Error,
},
Codegen(Vec<Diagnostic>),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Io(err) => write!(f, "I/O error: {err}"),
Error::Parse { file, source } => {
let start = source.span().start();
match file {
Some(path) => write!(
f,
"failed to parse {}:{}:{}: {source}",
path.display(),
start.line,
start.column + 1,
),
None => write!(
f,
"failed to parse source at {}:{}: {source}",
start.line,
start.column + 1,
),
}
}
Error::Codegen(diagnostics) => {
writeln!(
f,
"code generation failed with {} error{}:",
diagnostics.len(),
if diagnostics.len() == 1 { "" } else { "s" },
)?;
for diagnostic in diagnostics {
writeln!(f, " - {diagnostic}")?;
}
Ok(())
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io(err) => Some(err),
Error::Parse { source, .. } => Some(source),
Error::Codegen(_) => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::Io(err)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub kind: DiagnosticKind,
pub referenced_by: Option<String>,
pub location: Option<SourceLocation>,
}
impl Diagnostic {
pub fn new(kind: DiagnosticKind) -> Self {
Self {
kind,
referenced_by: None,
location: None,
}
}
pub fn referenced_by(mut self, context: impl Into<String>) -> Self {
self.referenced_by = Some(context.into());
self
}
pub fn at(mut self, location: Option<SourceLocation>) -> Self {
self.location = location;
self
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)?;
if let Some(referenced_by) = &self.referenced_by {
write!(f, " (in `{referenced_by}`)")?;
}
if let Some(location) = &self.location {
write!(f, " at {location}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticKind {
UnknownType {
rust_path: String,
suggestion: Option<String>,
},
UnknownWithWrapper {
wrapper_path: String,
},
GenericArity {
rust_path: String,
expected: usize,
found: usize,
},
UnresolvedTypeRef {
name: String,
},
UnknownRenameTarget {
type_name: String,
},
DuplicateType {
name: String,
},
ImportConflict {
export: String,
modules: Vec<String>,
},
UnsupportedFieldType {
rust_type: String,
},
NameCollision {
emitted: String,
originals: Vec<String>,
},
}
impl fmt::Display for DiagnosticKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DiagnosticKind::UnknownType {
rust_path,
suggestion,
} => {
write!(
f,
"unknown type `{rust_path}`; register a codec for it with \
`register_external(\"{rust_path}\", ...)`"
)?;
if let Some(suggestion) = suggestion {
write!(f, " (did you mean `{suggestion}`?)")?;
}
Ok(())
}
DiagnosticKind::UnknownWithWrapper { wrapper_path } => write!(
f,
"unknown `#[rkyv(with = ...)]` wrapper `{wrapper_path}`; register it with \
`register_with(\"{wrapper_path}\", ...)`"
),
DiagnosticKind::GenericArity {
rust_path,
expected,
found,
} => write!(
f,
"`{rust_path}` expects {expected} type argument{}, found {found}",
if *expected == 1 { "" } else { "s" },
),
DiagnosticKind::UnresolvedTypeRef { name } => write!(
f,
"unresolved type reference `{name}`; no type with that name was added to \
the generator"
),
DiagnosticKind::UnknownRenameTarget { type_name } => write!(
f,
"`set_archived_name` targets `{type_name}`, but no type with that name was \
added to the generator"
),
DiagnosticKind::DuplicateType { name } => {
write!(f, "type `{name}` is defined more than once")
}
DiagnosticKind::ImportConflict { export, modules } => write!(
f,
"export `{export}` is imported from multiple modules: {}",
modules.join(", "),
),
DiagnosticKind::UnsupportedFieldType { rust_type } => write!(
f,
"unsupported field type `{rust_type}`; only types mappable to rkyv-js \
codecs are supported"
),
DiagnosticKind::NameCollision { emitted, originals } => write!(
f,
"{} collapse to `{emitted}` under the configured casing; \
a duplicate object key would silently drop one of them",
originals
.iter()
.map(|name| format!("`{name}`"))
.collect::<Vec<_>>()
.join(" and "),
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceLocation {
pub file: Option<PathBuf>,
pub line: usize,
pub column: usize,
}
impl fmt::Display for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.file {
Some(file) => write!(f, "{}:{}:{}", file.display(), self.line, self.column),
None => write!(f, "<source>:{}:{}", self.line, self.column),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diagnostic_display_includes_context() {
let diagnostic = Diagnostic::new(DiagnosticKind::UnknownType {
rust_path: "chrono::NaiveDate".to_string(),
suggestion: None,
})
.referenced_by("Event.date")
.at(Some(SourceLocation {
file: Some(PathBuf::from("src/lib.rs")),
line: 12,
column: 5,
}));
let text = diagnostic.to_string();
assert!(text.contains("unknown type `chrono::NaiveDate`"));
assert!(text.contains("(in `Event.date`)"));
assert!(text.contains("at src/lib.rs:12:5"));
}
#[test]
fn suggestion_is_rendered() {
let kind = DiagnosticKind::UnknownType {
rust_path: "collections::HashMap".to_string(),
suggestion: Some("std::collections::HashMap".to_string()),
};
assert!(kind.to_string().contains("did you mean `std::collections::HashMap`?"));
}
#[test]
fn codegen_error_aggregates() {
let error = Error::Codegen(vec![
Diagnostic::new(DiagnosticKind::DuplicateType {
name: "Point".to_string(),
}),
Diagnostic::new(DiagnosticKind::UnresolvedTypeRef {
name: "Missing".to_string(),
}),
]);
let text = error.to_string();
assert!(text.contains("2 errors"));
assert!(text.contains("`Point`"));
assert!(text.contains("`Missing`"));
}
}