Skip to main content

rkyv_js_codegen/
error.rs

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