Skip to main content

rkyv_js_codegen/
error.rs

1//! Error and diagnostic types for the code generator.
2//!
3//! Fallible operations return [`Error`].
4//! Code-generation problems are aggregated: [`CodeGenerator::generate`](crate::CodeGenerator::generate)
5//! validates everything it can and reports all [`Diagnostic`]s at once in [`Error::Codegen`].
6
7use std::fmt;
8use std::path::PathBuf;
9
10/// Top-level error type for the code generator.
11#[derive(Debug)]
12pub enum Error {
13    /// An I/O error while reading sources or writing output.
14    Io(std::io::Error),
15    /// A Rust source file (or string) failed to parse.
16    Parse {
17        /// The file that failed to parse; `None` for
18        /// [`add_source_str`](crate::CodeGenerator::add_source_str).
19        file: Option<PathBuf>,
20        /// The underlying parse error.
21        source: syn::Error,
22    },
23    /// One or more code-generation diagnostics, aggregated.
24    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/// A single code-generation problem with optional provenance.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct Diagnostic {
84    /// What went wrong.
85    pub kind: DiagnosticKind,
86    /// The `Type.field` (or `Enum::Variant.field`) that triggered the
87    /// diagnostic, when known.
88    pub referenced_by: Option<String>,
89    /// Where in the source the offending item lives, when known.
90    pub location: Option<SourceLocation>,
91}
92
93impl Diagnostic {
94    /// Create a diagnostic with no provenance.
95    pub fn new(kind: DiagnosticKind) -> Self {
96        Self {
97            kind,
98            referenced_by: None,
99            location: None,
100        }
101    }
102
103    /// Attach the referencing `Type.field` context.
104    pub fn referenced_by(mut self, context: impl Into<String>) -> Self {
105        self.referenced_by = Some(context.into());
106        self
107    }
108
109    /// Attach a source location.
110    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/// The kinds of code-generation diagnostics.
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum DiagnosticKind {
132    /// A fully-qualified Rust type path with no registry mapping.
133    UnknownType {
134        /// The unresolved path (e.g. `chrono::NaiveDate`).
135        rust_path: String,
136        /// A registered path sharing the last segment, if any.
137        suggestion: Option<String>,
138    },
139    /// A `#[rkyv(with = ...)]` wrapper with no registered handler.
140    UnknownWithWrapper {
141        /// The unresolved wrapper path.
142        wrapper_path: String,
143    },
144    /// A registered generic type instantiated with the wrong number of type
145    /// arguments.
146    GenericArity {
147        /// The registered path.
148        rust_path: String,
149        /// The arity the registration expects.
150        expected: usize,
151        /// The number of type arguments found at the use site.
152        found: usize,
153    },
154    /// A [`CodecExpr::TypeRef`](crate::CodecExpr::TypeRef) that does not
155    /// resolve to any added type.
156    UnresolvedTypeRef {
157        /// The Rust name that was referenced.
158        name: String,
159    },
160    /// A [`set_archived_name`](crate::CodeGenerator::set_archived_name)
161    /// target that never materialized.
162    UnknownRenameTarget {
163        /// The type name the rename targeted.
164        type_name: String,
165    },
166    /// The same type name added more than once.
167    DuplicateType {
168        /// The duplicated name.
169        name: String,
170    },
171    /// The same export name imported from two different modules.
172    ImportConflict {
173        /// The conflicting export name.
174        export: String,
175        /// The modules it is imported from.
176        modules: Vec<String>,
177    },
178    /// A Rust field type the generator cannot map to a codec.
179    UnsupportedFieldType {
180        /// The field type, printed as Rust source.
181        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/// A position in a parsed source file.
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct SourceLocation {
246    /// The source file; `None` for sources added from strings.
247    pub file: Option<PathBuf>,
248    /// 1-based line number.
249    pub line: usize,
250    /// 1-based column number.
251    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}