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