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 NameCollision {
186 emitted: String,
188 originals: Vec<String>,
190 },
191}
192
193impl fmt::Display for DiagnosticKind {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 match self {
196 DiagnosticKind::UnknownType {
197 rust_path,
198 suggestion,
199 } => {
200 write!(
201 f,
202 "unknown type `{rust_path}`; register a codec for it with \
203 `register_external(\"{rust_path}\", ...)`"
204 )?;
205 if let Some(suggestion) = suggestion {
206 write!(f, " (did you mean `{suggestion}`?)")?;
207 }
208 Ok(())
209 }
210 DiagnosticKind::UnknownWithWrapper { wrapper_path } => write!(
211 f,
212 "unknown `#[rkyv(with = ...)]` wrapper `{wrapper_path}`; register it with \
213 `register_with(\"{wrapper_path}\", ...)`"
214 ),
215 DiagnosticKind::GenericArity {
216 rust_path,
217 expected,
218 found,
219 } => write!(
220 f,
221 "`{rust_path}` expects {expected} type argument{}, found {found}",
222 if *expected == 1 { "" } else { "s" },
223 ),
224 DiagnosticKind::UnresolvedTypeRef { name } => write!(
225 f,
226 "unresolved type reference `{name}`; no type with that name was added to \
227 the generator"
228 ),
229 DiagnosticKind::UnknownRenameTarget { type_name } => write!(
230 f,
231 "`set_archived_name` targets `{type_name}`, but no type with that name was \
232 added to the generator"
233 ),
234 DiagnosticKind::DuplicateType { name } => {
235 write!(f, "type `{name}` is defined more than once")
236 }
237 DiagnosticKind::ImportConflict { export, modules } => write!(
238 f,
239 "export `{export}` is imported from multiple modules: {}",
240 modules.join(", "),
241 ),
242 DiagnosticKind::UnsupportedFieldType { rust_type } => write!(
243 f,
244 "unsupported field type `{rust_type}`; only types mappable to rkyv-js \
245 codecs are supported"
246 ),
247 DiagnosticKind::NameCollision { emitted, originals } => write!(
248 f,
249 "{} collapse to `{emitted}` under the configured casing; \
250 a duplicate object key would silently drop one of them",
251 originals
252 .iter()
253 .map(|name| format!("`{name}`"))
254 .collect::<Vec<_>>()
255 .join(" and "),
256 ),
257 }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct SourceLocation {
264 pub file: Option<PathBuf>,
266 pub line: usize,
268 pub column: usize,
270}
271
272impl fmt::Display for SourceLocation {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 match &self.file {
275 Some(file) => write!(f, "{}:{}:{}", file.display(), self.line, self.column),
276 None => write!(f, "<source>:{}:{}", self.line, self.column),
277 }
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 #[test]
286 fn diagnostic_display_includes_context() {
287 let diagnostic = Diagnostic::new(DiagnosticKind::UnknownType {
288 rust_path: "chrono::NaiveDate".to_string(),
289 suggestion: None,
290 })
291 .referenced_by("Event.date")
292 .at(Some(SourceLocation {
293 file: Some(PathBuf::from("src/lib.rs")),
294 line: 12,
295 column: 5,
296 }));
297 let text = diagnostic.to_string();
298 assert!(text.contains("unknown type `chrono::NaiveDate`"));
299 assert!(text.contains("(in `Event.date`)"));
300 assert!(text.contains("at src/lib.rs:12:5"));
301 }
302
303 #[test]
304 fn suggestion_is_rendered() {
305 let kind = DiagnosticKind::UnknownType {
306 rust_path: "collections::HashMap".to_string(),
307 suggestion: Some("std::collections::HashMap".to_string()),
308 };
309 assert!(kind.to_string().contains("did you mean `std::collections::HashMap`?"));
310 }
311
312 #[test]
313 fn codegen_error_aggregates() {
314 let error = Error::Codegen(vec![
315 Diagnostic::new(DiagnosticKind::DuplicateType {
316 name: "Point".to_string(),
317 }),
318 Diagnostic::new(DiagnosticKind::UnresolvedTypeRef {
319 name: "Missing".to_string(),
320 }),
321 ]);
322 let text = error.to_string();
323 assert!(text.contains("2 errors"));
324 assert!(text.contains("`Point`"));
325 assert!(text.contains("`Missing`"));
326 }
327}