1use std::fmt;
8use std::path::PathBuf;
9
10#[derive(Debug)]
12pub enum Error {
13 Io(std::io::Error),
15 Parse {
17 file: Option<PathBuf>,
20 source: syn::Error,
22 },
23 Codegen(Vec<Diagnostic>),
25}
26
27impl fmt::Display for Error {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Error::Io(err) => write!(f, "I/O error: {err}"),
31 Error::Parse { file, source } => {
32 let start = source.span().start();
33 match file {
34 Some(path) => write!(
35 f,
36 "failed to parse {}:{}:{}: {source}",
37 path.display(),
38 start.line,
39 start.column + 1,
40 ),
41 None => write!(
42 f,
43 "failed to parse source at {}:{}: {source}",
44 start.line,
45 start.column + 1,
46 ),
47 }
48 }
49 Error::Codegen(diagnostics) => {
50 writeln!(
51 f,
52 "code generation failed with {} error{}:",
53 diagnostics.len(),
54 if diagnostics.len() == 1 { "" } else { "s" },
55 )?;
56 for diagnostic in diagnostics {
57 writeln!(f, " - {diagnostic}")?;
58 }
59 Ok(())
60 }
61 }
62 }
63}
64
65impl std::error::Error for Error {
66 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
67 match self {
68 Error::Io(err) => Some(err),
69 Error::Parse { source, .. } => Some(source),
70 Error::Codegen(_) => None,
71 }
72 }
73}
74
75impl From<std::io::Error> for Error {
76 fn from(err: std::io::Error) -> Self {
77 Error::Io(err)
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct Diagnostic {
84 pub kind: DiagnosticKind,
86 pub referenced_by: Option<String>,
89 pub location: Option<SourceLocation>,
91}
92
93impl Diagnostic {
94 pub fn new(kind: DiagnosticKind) -> Self {
96 Self {
97 kind,
98 referenced_by: None,
99 location: None,
100 }
101 }
102
103 pub fn referenced_by(mut self, context: impl Into<String>) -> Self {
105 self.referenced_by = Some(context.into());
106 self
107 }
108
109 pub fn at(mut self, location: Option<SourceLocation>) -> Self {
111 self.location = location;
112 self
113 }
114}
115
116impl fmt::Display for Diagnostic {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 write!(f, "{}", self.kind)?;
119 if let Some(referenced_by) = &self.referenced_by {
120 write!(f, " (in `{referenced_by}`)")?;
121 }
122 if let Some(location) = &self.location {
123 write!(f, " at {location}")?;
124 }
125 Ok(())
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum DiagnosticKind {
132 UnknownType {
134 rust_path: String,
136 suggestion: Option<String>,
138 },
139 UnknownWithWrapper {
141 wrapper_path: String,
143 },
144 GenericArity {
147 rust_path: String,
149 expected: usize,
151 found: usize,
153 },
154 UnresolvedTypeRef {
157 name: String,
159 },
160 UnknownRenameTarget {
163 type_name: String,
165 },
166 DuplicateType {
168 name: String,
170 },
171 ImportConflict {
173 export: String,
175 modules: Vec<String>,
177 },
178 UnsupportedFieldType {
180 rust_type: String,
182 },
183}
184
185impl fmt::Display for DiagnosticKind {
186 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187 match self {
188 DiagnosticKind::UnknownType {
189 rust_path,
190 suggestion,
191 } => {
192 write!(
193 f,
194 "unknown type `{rust_path}`; register a codec for it with \
195 `register_external(\"{rust_path}\", ...)`"
196 )?;
197 if let Some(suggestion) = suggestion {
198 write!(f, " (did you mean `{suggestion}`?)")?;
199 }
200 Ok(())
201 }
202 DiagnosticKind::UnknownWithWrapper { wrapper_path } => write!(
203 f,
204 "unknown `#[rkyv(with = ...)]` wrapper `{wrapper_path}`; register it with \
205 `register_with(\"{wrapper_path}\", ...)`"
206 ),
207 DiagnosticKind::GenericArity {
208 rust_path,
209 expected,
210 found,
211 } => write!(
212 f,
213 "`{rust_path}` expects {expected} type argument{}, found {found}",
214 if *expected == 1 { "" } else { "s" },
215 ),
216 DiagnosticKind::UnresolvedTypeRef { name } => write!(
217 f,
218 "unresolved type reference `{name}`; no type with that name was added to \
219 the generator"
220 ),
221 DiagnosticKind::UnknownRenameTarget { type_name } => write!(
222 f,
223 "`set_archived_name` targets `{type_name}`, but no type with that name was \
224 added to the generator"
225 ),
226 DiagnosticKind::DuplicateType { name } => {
227 write!(f, "type `{name}` is defined more than once")
228 }
229 DiagnosticKind::ImportConflict { export, modules } => write!(
230 f,
231 "export `{export}` is imported from multiple modules: {}",
232 modules.join(", "),
233 ),
234 DiagnosticKind::UnsupportedFieldType { rust_type } => write!(
235 f,
236 "unsupported field type `{rust_type}`; only types mappable to rkyv-js \
237 codecs are supported"
238 ),
239 }
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct SourceLocation {
246 pub file: Option<PathBuf>,
248 pub line: usize,
250 pub column: usize,
252}
253
254impl fmt::Display for SourceLocation {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 match &self.file {
257 Some(file) => write!(f, "{}:{}:{}", file.display(), self.line, self.column),
258 None => write!(f, "<source>:{}:{}", self.line, self.column),
259 }
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn diagnostic_display_includes_context() {
269 let diagnostic = Diagnostic::new(DiagnosticKind::UnknownType {
270 rust_path: "chrono::NaiveDate".to_string(),
271 suggestion: None,
272 })
273 .referenced_by("Event.date")
274 .at(Some(SourceLocation {
275 file: Some(PathBuf::from("src/lib.rs")),
276 line: 12,
277 column: 5,
278 }));
279 let text = diagnostic.to_string();
280 assert!(text.contains("unknown type `chrono::NaiveDate`"));
281 assert!(text.contains("(in `Event.date`)"));
282 assert!(text.contains("at src/lib.rs:12:5"));
283 }
284
285 #[test]
286 fn suggestion_is_rendered() {
287 let kind = DiagnosticKind::UnknownType {
288 rust_path: "collections::HashMap".to_string(),
289 suggestion: Some("std::collections::HashMap".to_string()),
290 };
291 assert!(kind.to_string().contains("did you mean `std::collections::HashMap`?"));
292 }
293
294 #[test]
295 fn codegen_error_aggregates() {
296 let error = Error::Codegen(vec![
297 Diagnostic::new(DiagnosticKind::DuplicateType {
298 name: "Point".to_string(),
299 }),
300 Diagnostic::new(DiagnosticKind::UnresolvedTypeRef {
301 name: "Missing".to_string(),
302 }),
303 ]);
304 let text = error.to_string();
305 assert!(text.contains("2 errors"));
306 assert!(text.contains("`Point`"));
307 assert!(text.contains("`Missing`"));
308 }
309}