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    /// Two or more names within one type collapse to the same identifier
184    /// under the configured [`Casing`](crate::Casing).
185    NameCollision {
186        /// The identifier every one of `originals` converts to.
187        emitted: String,
188        /// The colliding Rust names, in declaration order.
189        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/// A position in a parsed source file.
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct SourceLocation {
264    /// The source file; `None` for sources added from strings.
265    pub file: Option<PathBuf>,
266    /// 1-based line number.
267    pub line: usize,
268    /// 1-based column number.
269    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}