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//!
6//! Validation collects *every* rule violation before failing, so a document
7//! with several problems reports them all in one run rather than one per
8//! invocation.
9
10use miette::Diagnostic;
11use std::collections::BTreeSet;
12use weaveffi_ir::ir::{Api, SUPPORTED_VERSIONS};
13
14mod diagnostic;
15mod resolve;
16mod rules;
17#[cfg(test)]
18mod tests;
19mod warnings;
20
21pub use diagnostic::ValidationDiagnostic;
22pub use resolve::{find_type_in_api, resolve_type_refs};
23pub use warnings::{collect_warnings, ValidationWarning};
24
25/// Every way an [`Api`] can fail validation.
26///
27/// `validate_api` collects every variant it encounters. Each variant carries
28/// the names needed to render an actionable diagnostic, and the
29/// `#[error]`/`#[diagnostic]` attributes supply the message and help text
30/// shown to the user.
31#[derive(Debug, thiserror::Error, Diagnostic)]
32pub enum ValidationError {
33    /// A module is missing its required `name` field.
34    #[error("module has no name")]
35    #[diagnostic(help("every module must have a non-empty 'name' field"))]
36    NoModuleName,
37    /// Two modules share the same name.
38    #[error("duplicate module name: {0}")]
39    #[diagnostic(help(
40        "module names must be unique within an API definition; rename or merge the duplicate"
41    ))]
42    DuplicateModuleName(String),
43    /// A module name is not a valid identifier; the second field explains why.
44    #[error("invalid module name '{0}': {1}")]
45    #[diagnostic(help(
46        "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word"
47    ))]
48    InvalidModuleName(String, &'static str),
49    /// Two functions in the same module share a name.
50    #[error("duplicate function name in module '{module}': {function}")]
51    #[diagnostic(help("function names must be unique within a module; rename the duplicate"))]
52    DuplicateFunctionName {
53        /// Module that contains the colliding functions.
54        module: String,
55        /// Duplicated function name.
56        function: String,
57    },
58    /// Two parameters of one function share a name.
59    #[error("duplicate param name in function '{function}' of module '{module}': {param}")]
60    #[diagnostic(help("parameter names must be unique within a function; rename the duplicate"))]
61    DuplicateParamName {
62        /// Module that contains the function.
63        module: String,
64        /// Function that contains the colliding parameters.
65        function: String,
66        /// Duplicated parameter name.
67        param: String,
68    },
69    /// A name matches a reserved keyword in one of the target languages.
70    #[error("reserved keyword used: {0}")]
71    #[diagnostic(help("choose a different name that is not a language reserved word"))]
72    ReservedKeyword(String),
73    /// An identifier is malformed; the second field explains why.
74    #[error("invalid identifier '{0}': {1}")]
75    #[diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters"))]
76    InvalidIdentifier(String, &'static str),
77    /// An error domain in the named module is missing its `name` field.
78    #[error("error domain missing name in module '{0}'")]
79    #[diagnostic(help("add a non-empty 'name' field to the error domain"))]
80    ErrorDomainMissingName(String),
81    /// Two error codes in the same module share a name.
82    #[error("duplicate error code name in module '{module}': {name}")]
83    #[diagnostic(help("error code names must be unique within a module; rename the duplicate"))]
84    DuplicateErrorName {
85        /// Module that contains the error domain.
86        module: String,
87        /// Duplicated error code name.
88        name: String,
89    },
90    /// Two error codes in the same module share a numeric value.
91    #[error("duplicate error numeric code in module '{module}': {code}")]
92    #[diagnostic(help(
93        "numeric error codes must be unique within a module; assign a different value"
94    ))]
95    DuplicateErrorCode {
96        /// Module that contains the error domain.
97        module: String,
98        /// Conflicting numeric error code.
99        code: i32,
100    },
101    /// An error code uses a reserved value: `0` means success and `-2` is the
102    /// panic code the runtime reports when a producer panics.
103    #[error("invalid error code in module '{module}' for '{name}': must not be 0 or -2")]
104    #[diagnostic(help(
105        "0 means success and -2 is reserved for producer panics; use another integer"
106    ))]
107    InvalidErrorCode {
108        /// Module that contains the error domain.
109        module: String,
110        /// Error code name with the invalid value.
111        name: String,
112    },
113    /// A function name collides with an error domain name in the same module.
114    #[error("function name collides with error domain name in module '{module}': {name}")]
115    #[diagnostic(help(
116        "function and error domain names share a namespace; rename one to avoid the collision"
117    ))]
118    NameCollisionWithErrorDomain {
119        /// Module where the collision occurs.
120        module: String,
121        /// Name shared by the function and the error domain.
122        name: String,
123    },
124    /// Two callables in one module lower to the same C symbol.
125    ///
126    /// Free functions claim `{prefix}_{module}_{name}`; interface members
127    /// claim `{prefix}_{module}_{Interface}_{member}` plus an implicit
128    /// `_destroy`. A free function named `Store_get` and a `get` method on an
129    /// interface `Store` would collide.
130    #[error("C symbol collision in module '{module}': two declarations lower to '..._{symbol}'")]
131    #[diagnostic(help(
132        "free functions and interface members share the module's C symbol namespace \
133         (interface members are prefixed with the interface name); rename one of the \
134         colliding declarations"
135    ))]
136    AbiSymbolCollision {
137        /// Module whose symbol namespace has the collision.
138        module: String,
139        /// The colliding symbol suffix (without the `{prefix}_{module}_` part).
140        symbol: String,
141    },
142    /// A function declares `throws: true` but no error domain is in scope.
143    #[error("function '{module}::{function}' declares throws but no error domain is in scope")]
144    #[diagnostic(help(
145        "a throwing function reports codes from its module's error domain; declare an \
146         `errors:` block on this module (or an ancestor module), or remove `throws: true`"
147    ))]
148    ThrowsWithoutErrorDomain {
149        /// Module that contains the function.
150        module: String,
151        /// Function marked `throws` with no domain in scope.
152        function: String,
153    },
154    /// Two types (structs, enums, or interfaces) share a bare name.
155    ///
156    /// Type names must be unique across the whole API: generators emit flat
157    /// per-language type names, and unqualified cross-module references
158    /// resolve by bare name, so two types called `Config` would collide in
159    /// generated code and make references ambiguous.
160    #[error("duplicate type name '{name}' (declared in '{first}' and '{second}')")]
161    #[diagnostic(help(
162        "struct, enum, interface, and error domain names must be unique across the whole \
163         API; rename one of the declarations"
164    ))]
165    DuplicateTypeName {
166        /// The colliding bare type name.
167        name: String,
168        /// Module path of the first declaration.
169        first: String,
170        /// Module path of the second declaration.
171        second: String,
172    },
173    /// Two error domains declare a code with the same name.
174    ///
175    /// Code names must be unique across every domain in the API: backends
176    /// with flat namespaces (Python, Node, Go) derive one error class or
177    /// constant per code, so `NotFound` in two domains would collide in
178    /// generated code.
179    #[error("duplicate error code name '{name}' (declared in '{first}' and '{second}')")]
180    #[diagnostic(help(
181        "error code names must be unique across the whole API; qualify one of them \
182         (e.g. 'OrderNotFound')"
183    ))]
184    DuplicateErrorCodeName {
185        /// The colliding code name.
186        name: String,
187        /// Domain of the first declaration, as `module.Domain`.
188        first: String,
189        /// Domain of the second declaration, as `module.Domain`.
190        second: String,
191    },
192    /// Two structs in the same module share a name.
193    #[error("duplicate struct name in module '{module}': {name}")]
194    #[diagnostic(help("struct names must be unique within a module; rename the duplicate"))]
195    DuplicateStructName {
196        /// Module that contains the structs.
197        module: String,
198        /// Duplicated struct name.
199        name: String,
200    },
201    /// Two fields of one struct share a name.
202    #[error("duplicate field name in struct '{struct_name}': {field}")]
203    #[diagnostic(help("field names must be unique within a struct; rename the duplicate"))]
204    DuplicateStructField {
205        /// Struct that contains the colliding fields.
206        struct_name: String,
207        /// Duplicated field name.
208        field: String,
209    },
210    /// A struct declares no fields.
211    #[error("empty struct in module '{module}': {name}")]
212    #[diagnostic(help("structs must have at least one field; add a field or remove the struct"))]
213    EmptyStruct {
214        /// Module that contains the struct.
215        module: String,
216        /// Name of the empty struct.
217        name: String,
218    },
219    /// Two enums in the same module share a name.
220    #[error("duplicate enum name in module '{module}': {name}")]
221    #[diagnostic(help("enum names must be unique within a module; rename the duplicate"))]
222    DuplicateEnumName {
223        /// Module that contains the enums.
224        module: String,
225        /// Duplicated enum name.
226        name: String,
227    },
228    /// An enum declares no variants.
229    #[error("empty enum in module '{module}': {name}")]
230    #[diagnostic(help("enums must have at least one variant; add a variant or remove the enum"))]
231    EmptyEnum {
232        /// Module that contains the enum.
233        module: String,
234        /// Name of the empty enum.
235        name: String,
236    },
237    /// Two variants of one enum share a name.
238    #[error("duplicate enum variant in enum '{enum_name}': {variant}")]
239    #[diagnostic(help("variant names must be unique within an enum; rename the duplicate"))]
240    DuplicateEnumVariant {
241        /// Enum that contains the colliding variants.
242        enum_name: String,
243        /// Duplicated variant name.
244        variant: String,
245    },
246    /// Two associated fields of one rich enum variant share a name.
247    #[error("duplicate field '{field}' in variant '{variant}' of enum '{enum_name}'")]
248    #[diagnostic(help(
249        "associated field names must be unique within an enum variant; rename the duplicate"
250    ))]
251    DuplicateEnumVariantField {
252        /// Enum that contains the variant.
253        enum_name: String,
254        /// Variant that contains the colliding fields.
255        variant: String,
256        /// Duplicated associated field name.
257        field: String,
258    },
259    /// Two variants of one enum share a numeric discriminant.
260    #[error("duplicate enum value in enum '{enum_name}': {value}")]
261    #[diagnostic(help(
262        "variant numeric values must be unique within an enum; assign a different value"
263    ))]
264    DuplicateEnumValue {
265        /// Enum that contains the variants.
266        enum_name: String,
267        /// Conflicting numeric discriminant.
268        value: i32,
269    },
270    /// Two interfaces in the same module share a name.
271    #[error("duplicate interface name in module '{module}': {name}")]
272    #[diagnostic(help("interface names must be unique within a module; rename the duplicate"))]
273    DuplicateInterfaceName {
274        /// Module that contains the interfaces.
275        module: String,
276        /// Duplicated interface name.
277        name: String,
278    },
279    /// Two members (constructors, methods, or statics) of one interface share
280    /// a name.
281    #[error("duplicate member name in interface '{interface}': {name}")]
282    #[diagnostic(help(
283        "constructor, method, and static names share one namespace per interface; \
284         rename the duplicate"
285    ))]
286    DuplicateInterfaceMember {
287        /// Interface that contains the colliding members.
288        interface: String,
289        /// Duplicated member name.
290        name: String,
291    },
292    /// An interface declares no members at all.
293    #[error("empty interface in module '{module}': {name}")]
294    #[diagnostic(help(
295        "interfaces must declare at least one constructor, method, or static; \
296         add a member or remove the interface"
297    ))]
298    EmptyInterface {
299        /// Module that contains the interface.
300        module: String,
301        /// Name of the empty interface.
302        name: String,
303    },
304    /// An interface constructor declares an explicit return type.
305    #[error("constructor '{constructor}' of interface '{interface}' declares a return type")]
306    #[diagnostic(help(
307        "a constructor implicitly returns a new instance of its interface; remove the \
308         `return` field"
309    ))]
310    ConstructorHasReturn {
311        /// Interface that declares the constructor.
312        interface: String,
313        /// The offending constructor.
314        constructor: String,
315    },
316    /// An interface constructor is marked `async`.
317    #[error("constructor '{constructor}' of interface '{interface}' cannot be async")]
318    #[diagnostic(help(
319        "constructors are synchronous; expose an async static factory returning the \
320         interface instead"
321    ))]
322    AsyncConstructor {
323        /// Interface that declares the constructor.
324        interface: String,
325        /// The offending constructor.
326        constructor: String,
327    },
328    /// An interface reference appears in a position the ABI cannot support.
329    #[error("interface type '{name}' is not valid in {location}")]
330    #[diagnostic(help(
331        "interface objects may appear as function parameters, return types, and \
332         optionals of those; they cannot be struct fields, collection elements, \
333         map keys/values, or callback parameters"
334    ))]
335    InterfaceInInvalidPosition {
336        /// The referenced interface name.
337        name: String,
338        /// Position where the interface reference appeared.
339        location: String,
340    },
341    /// A type reference names a struct, enum, or interface that doesn't exist.
342    #[error("unknown type reference: {name}")]
343    #[diagnostic(help("define a struct, enum, or interface with this name, or check for typos"))]
344    UnknownTypeRef {
345        /// Unresolved type name.
346        name: String,
347    },
348    /// A map uses a key type the C ABI can't represent.
349    #[error("invalid map key type: {key_type}; only primitive types and strings are allowed as map keys")]
350    #[diagnostic(help("map keys must be primitive types (i32, u32, i64, f64, bool, string); structs, lists, and maps cannot be keys"))]
351    InvalidMapKey {
352        /// Rejected key type, rendered as it appears in the IDL.
353        key_type: String,
354    },
355    /// A borrowed type appears somewhere other than a function parameter.
356    #[error(
357        "borrowed type '{ty}' is not valid in {location}; only function parameters are allowed"
358    )]
359    #[diagnostic(help("borrowed types (&str, &[u8]) can only be used as function parameters, not return types or struct fields"))]
360    BorrowedTypeInInvalidPosition {
361        /// Borrowed type that was rejected.
362        ty: String,
363        /// Position where the borrowed type appeared.
364        location: String,
365    },
366    /// Two callbacks in the same module share a name.
367    #[error("duplicate callback name in module '{module}': {name}")]
368    #[diagnostic(help("callback names must be unique within a module; rename the duplicate"))]
369    DuplicateCallbackName {
370        /// Module that contains the callbacks.
371        module: String,
372        /// Duplicated callback name.
373        name: String,
374    },
375    /// A listener references a callback that isn't defined in its module.
376    #[error(
377        "listener '{listener}' in module '{module}' references undefined callback '{callback}'"
378    )]
379    #[diagnostic(help(
380        "listener event_callback must reference a callback defined in the same module"
381    ))]
382    ListenerCallbackNotFound {
383        /// Module that contains the listener.
384        module: String,
385        /// Listener with the dangling reference.
386        listener: String,
387        /// Callback name that could not be resolved.
388        callback: String,
389    },
390    /// Two listeners in the same module share a name.
391    #[error("duplicate listener name in module '{module}': {name}")]
392    #[diagnostic(help("listener names must be unique within a module; rename the duplicate"))]
393    DuplicateListenerName {
394        /// Module that contains the listeners.
395        module: String,
396        /// Duplicated listener name.
397        name: String,
398    },
399    /// A callback parameter uses a type that can't cross the callback ABI.
400    #[error(
401        "callback '{callback}' in module '{module}' has parameter '{param}' with unsupported \
402         type '{ty}'"
403    )]
404    #[diagnostic(help(
405        "callback parameters are limited to scalars, bool, enums, string, bytes, handles, \
406         structs, optionals of those, lists of scalars/strings, and maps of scalars/strings; \
407         every target must be able to marshal a callback argument without an FFI round-trip"
408    ))]
409    UnsupportedCallbackParamType {
410        /// Module that contains the callback.
411        module: String,
412        /// Callback that declares the parameter.
413        callback: String,
414        /// Parameter with the unsupported type.
415        param: String,
416        /// Offending type, rendered as it appears in the IDL.
417        ty: String,
418    },
419    /// An iterator type appears somewhere other than a function return.
420    #[error("iterator type is only valid as a function return type, found in {location}")]
421    #[diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields"))]
422    IteratorInInvalidPosition {
423        /// Position where the iterator type appeared.
424        location: String,
425    },
426    /// A list, map, or iterator has an element type the C ABI can't flatten.
427    #[error("unsupported element type '{ty}' in {location}")]
428    #[diagnostic(help(
429        "the C ABI lowers lists, maps, and iterators to flat parallel arrays, so element \
430         types must be flat: list/iterator elements may be scalars, bool, enums, strings, \
431         handles, or structs (plus optional structs/handles in lists); map keys and values \
432         may be scalars, bool, enums, or strings"
433    ))]
434    UnsupportedElementType {
435        /// Position where the unsupported element type appeared.
436        location: String,
437        /// Offending element type, rendered as it appears in the IDL.
438        ty: String,
439    },
440    /// An async function tries to return an iterator, which has no async ABI.
441    #[error("async function '{module}::{function}' cannot return an iterator")]
442    #[diagnostic(help(
443        "the callback-completed async ABI has no streaming protocol; return a list ([T]) \
444         from the async function, or make the function synchronous and return iter<T>"
445    ))]
446    AsyncIteratorReturn {
447        /// Module that contains the function.
448        module: String,
449        /// Async function with the iterator return.
450        function: String,
451    },
452    /// A struct marked `builder: true` declares no fields.
453    #[error("builder struct '{name}' in module '{module}' must have at least one field")]
454    #[diagnostic(help(
455        "builder structs must have at least one field; add a field or set builder: false"
456    ))]
457    BuilderStructEmpty {
458        /// Module that contains the struct.
459        module: String,
460        /// Name of the empty builder struct.
461        name: String,
462    },
463    /// The document declares a schema version this build doesn't support.
464    #[error("unsupported schema version '{version}'; supported versions: {supported}")]
465    #[diagnostic(help(
466        "set the version field to the current schema version and update the \
467         document to match the current schema (see docs/src/reference/idl.md)"
468    ))]
469    UnsupportedSchemaVersion {
470        /// Version requested by the document.
471        version: String,
472        /// Comma-separated list of versions this build accepts.
473        supported: String,
474    },
475}
476
477/// Every validation failure found in one pass, each wrapped as a
478/// [`ValidationDiagnostic`] carrying an optional source span.
479///
480/// `Display` renders every message on its own line; miette renderers reach the
481/// individual diagnostics through [`Diagnostic::related`].
482#[derive(Debug)]
483pub struct ValidationDiagnostics {
484    /// The individual failures, in the order they were found. Never empty.
485    pub diagnostics: Vec<ValidationDiagnostic>,
486}
487
488impl ValidationDiagnostics {
489    /// The first failure, which every report is guaranteed to contain.
490    pub fn first(&self) -> &ValidationDiagnostic {
491        &self.diagnostics[0]
492    }
493}
494
495impl std::fmt::Display for ValidationDiagnostics {
496    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497        for (i, d) in self.diagnostics.iter().enumerate() {
498            if i > 0 {
499                writeln!(f)?;
500            }
501            write!(f, "{d}")?;
502        }
503        Ok(())
504    }
505}
506
507impl std::error::Error for ValidationDiagnostics {}
508
509impl Diagnostic for ValidationDiagnostics {
510    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
511        self.first().code()
512    }
513
514    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
515        self.first().help()
516    }
517
518    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
519        self.first().source_code()
520    }
521
522    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
523        self.first().labels()
524    }
525
526    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
527        if self.diagnostics.len() <= 1 {
528            return None;
529        }
530        Some(Box::new(
531            self.diagnostics[1..].iter().map(|d| d as &dyn Diagnostic),
532        ))
533    }
534}
535
536/// Validate an [`Api`], reporting **every** rule violation found. The optional
537/// `source` is `(filename, contents)` of the IDL file and is used to attach
538/// spans to the returned diagnostics. Pass `None` when the API is constructed
539/// in memory (tests, programmatic builds) and there is no on-disk source.
540///
541/// On success, type references in `api` are resolved in place (see
542/// [`resolve_type_refs`]): enum and interface references are distinguished
543/// from struct references, and cross-module references are qualified.
544///
545/// # Errors
546///
547/// Returns [`ValidationDiagnostics`] carrying one [`ValidationDiagnostic`]
548/// per violation: an unsupported schema version, a duplicate or invalid name,
549/// an unknown or misplaced type, an empty struct or enum, a `throws` without
550/// an error domain, or any other rule violation in the catalog above.
551pub fn validate_api(
552    api: &mut Api,
553    source: Option<(&str, &str)>,
554) -> Result<(), ValidationDiagnostics> {
555    let errors = validate_api_inner(api);
556    if errors.is_empty() {
557        return Ok(());
558    }
559    Err(ValidationDiagnostics {
560        diagnostics: errors
561            .into_iter()
562            .map(|e| ValidationDiagnostic::new(e, source))
563            .collect(),
564    })
565}
566
567fn validate_api_inner(api: &mut Api) -> Vec<ValidationError> {
568    let mut errors = Vec::new();
569    if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) {
570        // A wrong-schema document is checked no further: the rules below
571        // assume the current schema's shape.
572        return vec![ValidationError::UnsupportedSchemaVersion {
573            version: api.version.clone(),
574            supported: SUPPORTED_VERSIONS.join(", "),
575        }];
576    }
577    let mut module_names = BTreeSet::new();
578    for m in &api.modules {
579        if !module_names.insert(m.name.clone()) {
580            errors.push(ValidationError::DuplicateModuleName(m.name.clone()));
581        }
582        rules::validate_module(m, &api.modules, false, &mut errors);
583    }
584    rules::check_global_type_names(&api.modules, &mut errors);
585    rules::check_global_error_code_names(&api.modules, &mut errors);
586    if errors.is_empty() {
587        resolve_type_refs(api);
588    }
589    errors
590}