Skip to main content

weaveffi_core/
validate.rs

1//! IDL validation. This module owns the [`ValidationError`] catalog and the
2//! [`validate_api`] entry point; the work is split across submodules:
3//! `rules` (per-module checks), `resolve` (type-reference qualification),
4//! `diagnostic` (miette span attachment), and `warnings` (advisory lints).
5
6use miette::Diagnostic;
7use std::collections::BTreeSet;
8use weaveffi_ir::ir::{Api, SUPPORTED_VERSIONS};
9
10mod diagnostic;
11mod resolve;
12mod rules;
13#[cfg(test)]
14mod tests;
15mod warnings;
16
17pub use diagnostic::ValidationDiagnostic;
18pub use resolve::{find_type_in_api, resolve_type_refs};
19pub use warnings::{collect_warnings, ValidationWarning};
20
21/// Every way an [`Api`] can fail validation.
22///
23/// `validate_api` returns the first variant it encounters. Each variant
24/// carries the names needed to render an actionable diagnostic, and the
25/// `#[error]`/`#[diagnostic]` attributes supply the message and help text
26/// shown to the user.
27#[derive(Debug, thiserror::Error, Diagnostic)]
28pub enum ValidationError {
29    /// A module is missing its required `name` field.
30    #[error("module has no name")]
31    #[diagnostic(help("every module must have a non-empty 'name' field"))]
32    NoModuleName,
33    /// Two modules share the same name.
34    #[error("duplicate module name: {0}")]
35    #[diagnostic(help(
36        "module names must be unique within an API definition; rename or merge the duplicate"
37    ))]
38    DuplicateModuleName(String),
39    /// A module name is not a valid identifier; the second field explains why.
40    #[error("invalid module name '{0}': {1}")]
41    #[diagnostic(help(
42        "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word"
43    ))]
44    InvalidModuleName(String, &'static str),
45    /// Two functions in the same module share a name.
46    #[error("duplicate function name in module '{module}': {function}")]
47    #[diagnostic(help("function names must be unique within a module; rename the duplicate"))]
48    DuplicateFunctionName {
49        /// Module that contains the colliding functions.
50        module: String,
51        /// Duplicated function name.
52        function: String,
53    },
54    /// Two parameters of one function share a name.
55    #[error("duplicate param name in function '{function}' of module '{module}': {param}")]
56    #[diagnostic(help("parameter names must be unique within a function; rename the duplicate"))]
57    DuplicateParamName {
58        /// Module that contains the function.
59        module: String,
60        /// Function that contains the colliding parameters.
61        function: String,
62        /// Duplicated parameter name.
63        param: String,
64    },
65    /// A name matches a reserved keyword in one of the target languages.
66    #[error("reserved keyword used: {0}")]
67    #[diagnostic(help("choose a different name that is not a language reserved word"))]
68    ReservedKeyword(String),
69    /// An identifier is malformed; the second field explains why.
70    #[error("invalid identifier '{0}': {1}")]
71    #[diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters"))]
72    InvalidIdentifier(String, &'static str),
73    /// An error domain in the named module is missing its `name` field.
74    #[error("error domain missing name in module '{0}'")]
75    #[diagnostic(help("add a non-empty 'name' field to the error domain"))]
76    ErrorDomainMissingName(String),
77    /// Two error codes in the same module share a name.
78    #[error("duplicate error code name in module '{module}': {name}")]
79    #[diagnostic(help("error code names must be unique within a module; rename the duplicate"))]
80    DuplicateErrorName {
81        /// Module that contains the error domain.
82        module: String,
83        /// Duplicated error code name.
84        name: String,
85    },
86    /// Two error codes in the same module share a numeric value.
87    #[error("duplicate error numeric code in module '{module}': {code}")]
88    #[diagnostic(help(
89        "numeric error codes must be unique within a module; assign a different value"
90    ))]
91    DuplicateErrorCode {
92        /// Module that contains the error domain.
93        module: String,
94        /// Conflicting numeric error code.
95        code: i32,
96    },
97    /// An error code uses the reserved value `0` (which means success).
98    #[error("invalid error code in module '{module}' for '{name}': must be non-zero")]
99    #[diagnostic(help("error codes must be non-zero; use a positive or negative integer"))]
100    InvalidErrorCode {
101        /// Module that contains the error domain.
102        module: String,
103        /// Error code name with the invalid value.
104        name: String,
105    },
106    /// A function name collides with an error domain name in the same module.
107    #[error("function name collides with error domain name in module '{module}': {name}")]
108    #[diagnostic(help(
109        "function and error domain names share a namespace; rename one to avoid the collision"
110    ))]
111    NameCollisionWithErrorDomain {
112        /// Module where the collision occurs.
113        module: String,
114        /// Name shared by the function and the error domain.
115        name: String,
116    },
117    /// Two structs in the same module share a name.
118    #[error("duplicate struct name in module '{module}': {name}")]
119    #[diagnostic(help("struct names must be unique within a module; rename the duplicate"))]
120    DuplicateStructName {
121        /// Module that contains the structs.
122        module: String,
123        /// Duplicated struct name.
124        name: String,
125    },
126    /// Two fields of one struct share a name.
127    #[error("duplicate field name in struct '{struct_name}': {field}")]
128    #[diagnostic(help("field names must be unique within a struct; rename the duplicate"))]
129    DuplicateStructField {
130        /// Struct that contains the colliding fields.
131        struct_name: String,
132        /// Duplicated field name.
133        field: String,
134    },
135    /// A struct declares no fields.
136    #[error("empty struct in module '{module}': {name}")]
137    #[diagnostic(help("structs must have at least one field; add a field or remove the struct"))]
138    EmptyStruct {
139        /// Module that contains the struct.
140        module: String,
141        /// Name of the empty struct.
142        name: String,
143    },
144    /// Two enums in the same module share a name.
145    #[error("duplicate enum name in module '{module}': {name}")]
146    #[diagnostic(help("enum names must be unique within a module; rename the duplicate"))]
147    DuplicateEnumName {
148        /// Module that contains the enums.
149        module: String,
150        /// Duplicated enum name.
151        name: String,
152    },
153    /// An enum declares no variants.
154    #[error("empty enum in module '{module}': {name}")]
155    #[diagnostic(help("enums must have at least one variant; add a variant or remove the enum"))]
156    EmptyEnum {
157        /// Module that contains the enum.
158        module: String,
159        /// Name of the empty enum.
160        name: String,
161    },
162    /// Two variants of one enum share a name.
163    #[error("duplicate enum variant in enum '{enum_name}': {variant}")]
164    #[diagnostic(help("variant names must be unique within an enum; rename the duplicate"))]
165    DuplicateEnumVariant {
166        /// Enum that contains the colliding variants.
167        enum_name: String,
168        /// Duplicated variant name.
169        variant: String,
170    },
171    /// Two associated fields of one rich enum variant share a name.
172    #[error("duplicate field '{field}' in variant '{variant}' of enum '{enum_name}'")]
173    #[diagnostic(help(
174        "associated field names must be unique within an enum variant; rename the duplicate"
175    ))]
176    DuplicateEnumVariantField {
177        /// Enum that contains the variant.
178        enum_name: String,
179        /// Variant that contains the colliding fields.
180        variant: String,
181        /// Duplicated associated field name.
182        field: String,
183    },
184    /// Two variants of one enum share a numeric discriminant.
185    #[error("duplicate enum value in enum '{enum_name}': {value}")]
186    #[diagnostic(help(
187        "variant numeric values must be unique within an enum; assign a different value"
188    ))]
189    DuplicateEnumValue {
190        /// Enum that contains the variants.
191        enum_name: String,
192        /// Conflicting numeric discriminant.
193        value: i32,
194    },
195    /// A type reference names a struct or enum that doesn't exist.
196    #[error("unknown type reference: {name}")]
197    #[diagnostic(help(
198        "define a struct or enum with this name in the same module, or check for typos"
199    ))]
200    UnknownTypeRef {
201        /// Unresolved type name.
202        name: String,
203    },
204    /// A map uses a key type the C ABI can't represent.
205    #[error("invalid map key type: {key_type}; only primitive types and strings are allowed as map keys")]
206    #[diagnostic(help("map keys must be primitive types (i32, u32, i64, f64, bool, string); structs, lists, and maps cannot be keys"))]
207    InvalidMapKey {
208        /// Rejected key type, rendered as it appears in the IDL.
209        key_type: String,
210    },
211    /// A borrowed type appears somewhere other than a function parameter.
212    #[error(
213        "borrowed type '{ty}' is not valid in {location}; only function parameters are allowed"
214    )]
215    #[diagnostic(help("borrowed types (&str, &[u8]) can only be used as function parameters, not return types or struct fields"))]
216    BorrowedTypeInInvalidPosition {
217        /// Borrowed type that was rejected.
218        ty: String,
219        /// Position where the borrowed type appeared.
220        location: String,
221    },
222    /// Two callbacks in the same module share a name.
223    #[error("duplicate callback name in module '{module}': {name}")]
224    #[diagnostic(help("callback names must be unique within a module; rename the duplicate"))]
225    DuplicateCallbackName {
226        /// Module that contains the callbacks.
227        module: String,
228        /// Duplicated callback name.
229        name: String,
230    },
231    /// A listener references a callback that isn't defined in its module.
232    #[error(
233        "listener '{listener}' in module '{module}' references undefined callback '{callback}'"
234    )]
235    #[diagnostic(help(
236        "listener event_callback must reference a callback defined in the same module"
237    ))]
238    ListenerCallbackNotFound {
239        /// Module that contains the listener.
240        module: String,
241        /// Listener with the dangling reference.
242        listener: String,
243        /// Callback name that could not be resolved.
244        callback: String,
245    },
246    /// Two listeners in the same module share a name.
247    #[error("duplicate listener name in module '{module}': {name}")]
248    #[diagnostic(help("listener names must be unique within a module; rename the duplicate"))]
249    DuplicateListenerName {
250        /// Module that contains the listeners.
251        module: String,
252        /// Duplicated listener name.
253        name: String,
254    },
255    /// A callback parameter uses a type that can't cross the callback ABI.
256    #[error(
257        "callback '{callback}' in module '{module}' has parameter '{param}' with unsupported \
258         type '{ty}'"
259    )]
260    #[diagnostic(help(
261        "callback parameters are limited to scalars, bool, enums, string, bytes, handles, \
262         structs, optionals of those, lists of scalars/strings, and maps of scalars/strings; \
263         every target must be able to marshal a callback argument without an FFI round-trip"
264    ))]
265    UnsupportedCallbackParamType {
266        /// Module that contains the callback.
267        module: String,
268        /// Callback that declares the parameter.
269        callback: String,
270        /// Parameter with the unsupported type.
271        param: String,
272        /// Offending type, rendered as it appears in the IDL.
273        ty: String,
274    },
275    /// An iterator type appears somewhere other than a function return.
276    #[error("iterator type is only valid as a function return type, found in {location}")]
277    #[diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields"))]
278    IteratorInInvalidPosition {
279        /// Position where the iterator type appeared.
280        location: String,
281    },
282    /// A list, map, or iterator has an element type the C ABI can't flatten.
283    #[error("unsupported element type '{ty}' in {location}")]
284    #[diagnostic(help(
285        "the C ABI lowers lists, maps, and iterators to flat parallel arrays, so element \
286         types must be flat: list/iterator elements may be scalars, bool, enums, strings, \
287         handles, or structs (plus optional structs/handles in lists); map keys and values \
288         may be scalars, bool, enums, or strings"
289    ))]
290    UnsupportedElementType {
291        /// Position where the unsupported element type appeared.
292        location: String,
293        /// Offending element type, rendered as it appears in the IDL.
294        ty: String,
295    },
296    /// An async function tries to return an iterator, which has no async ABI.
297    #[error("async function '{module}::{function}' cannot return an iterator")]
298    #[diagnostic(help(
299        "the callback-completed async ABI has no streaming protocol; return a list ([T]) \
300         from the async function, or make the function synchronous and return iter<T>"
301    ))]
302    AsyncIteratorReturn {
303        /// Module that contains the function.
304        module: String,
305        /// Async function with the iterator return.
306        function: String,
307    },
308    /// A struct marked `builder: true` declares no fields.
309    #[error("builder struct '{name}' in module '{module}' must have at least one field")]
310    #[diagnostic(help(
311        "builder structs must have at least one field; add a field or set builder: false"
312    ))]
313    BuilderStructEmpty {
314        /// Module that contains the struct.
315        module: String,
316        /// Name of the empty builder struct.
317        name: String,
318    },
319    /// The document declares a schema version this build doesn't support.
320    #[error("unsupported schema version '{version}'; supported versions: {supported}")]
321    #[diagnostic(help(
322        "set the version field to the current schema version and update the \
323         document to match the current schema (see docs/src/idl.md)"
324    ))]
325    UnsupportedSchemaVersion {
326        /// Version requested by the document.
327        version: String,
328        /// Comma-separated list of versions this build accepts.
329        supported: String,
330    },
331}
332
333/// Validate an [`Api`]. The optional `source` is `(filename, contents)` of the
334/// IDL file and is used at the call site to attach a span to a returned error
335/// via [`ValidationDiagnostic::new`]. Pass `None` when the API is constructed
336/// in memory (tests, programmatic builds) and there is no on-disk source.
337///
338/// On success, type references in `api` are resolved in place (see
339/// [`resolve_type_refs`]).
340///
341/// # Errors
342///
343/// Returns a [`ValidationDiagnostic`] wrapping the first [`ValidationError`]
344/// found: an unsupported schema version, a duplicate or invalid name, an
345/// unknown or misplaced type, an empty struct or enum, or any other rule
346/// violation in the catalog above. The diagnostic carries a source span when
347/// `source` is provided.
348#[allow(clippy::result_large_err)]
349pub fn validate_api(
350    api: &mut Api,
351    source: Option<(&str, &str)>,
352) -> Result<(), ValidationDiagnostic> {
353    validate_api_inner(api).map_err(|e| ValidationDiagnostic::new(e, source))
354}
355
356fn validate_api_inner(api: &mut Api) -> Result<(), ValidationError> {
357    if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) {
358        return Err(ValidationError::UnsupportedSchemaVersion {
359            version: api.version.clone(),
360            supported: SUPPORTED_VERSIONS.join(", "),
361        });
362    }
363    let mut module_names = BTreeSet::new();
364    for m in &api.modules {
365        if !module_names.insert(m.name.clone()) {
366            return Err(ValidationError::DuplicateModuleName(m.name.clone()));
367        }
368        rules::validate_module(m, &api.modules)?;
369    }
370    resolve_type_refs(api);
371    Ok(())
372}