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#[derive(Debug, thiserror::Error, Diagnostic)]
22pub enum ValidationError {
23    #[error("module has no name")]
24    #[diagnostic(help("every module must have a non-empty 'name' field"))]
25    NoModuleName,
26    #[error("duplicate module name: {0}")]
27    #[diagnostic(help(
28        "module names must be unique within an API definition; rename or merge the duplicate"
29    ))]
30    DuplicateModuleName(String),
31    #[error("invalid module name '{0}': {1}")]
32    #[diagnostic(help(
33        "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word"
34    ))]
35    InvalidModuleName(String, &'static str),
36    #[error("duplicate function name in module '{module}': {function}")]
37    #[diagnostic(help("function names must be unique within a module; rename the duplicate"))]
38    DuplicateFunctionName { module: String, function: String },
39    #[error("duplicate param name in function '{function}' of module '{module}': {param}")]
40    #[diagnostic(help("parameter names must be unique within a function; rename the duplicate"))]
41    DuplicateParamName {
42        module: String,
43        function: String,
44        param: String,
45    },
46    #[error("reserved keyword used: {0}")]
47    #[diagnostic(help("choose a different name that is not a language reserved word"))]
48    ReservedKeyword(String),
49    #[error("invalid identifier '{0}': {1}")]
50    #[diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters"))]
51    InvalidIdentifier(String, &'static str),
52    #[error("error domain missing name in module '{0}'")]
53    #[diagnostic(help("add a non-empty 'name' field to the error domain"))]
54    ErrorDomainMissingName(String),
55    #[error("duplicate error code name in module '{module}': {name}")]
56    #[diagnostic(help("error code names must be unique within a module; rename the duplicate"))]
57    DuplicateErrorName { module: String, name: String },
58    #[error("duplicate error numeric code in module '{module}': {code}")]
59    #[diagnostic(help(
60        "numeric error codes must be unique within a module; assign a different value"
61    ))]
62    DuplicateErrorCode { module: String, code: i32 },
63    #[error("invalid error code in module '{module}' for '{name}': must be non-zero")]
64    #[diagnostic(help("error codes must be non-zero; use a positive or negative integer"))]
65    InvalidErrorCode { module: String, name: String },
66    #[error("function name collides with error domain name in module '{module}': {name}")]
67    #[diagnostic(help(
68        "function and error domain names share a namespace; rename one to avoid the collision"
69    ))]
70    NameCollisionWithErrorDomain { module: String, name: String },
71    #[error("duplicate struct name in module '{module}': {name}")]
72    #[diagnostic(help("struct names must be unique within a module; rename the duplicate"))]
73    DuplicateStructName { module: String, name: String },
74    #[error("duplicate field name in struct '{struct_name}': {field}")]
75    #[diagnostic(help("field names must be unique within a struct; rename the duplicate"))]
76    DuplicateStructField { struct_name: String, field: String },
77    #[error("empty struct in module '{module}': {name}")]
78    #[diagnostic(help("structs must have at least one field; add a field or remove the struct"))]
79    EmptyStruct { module: String, name: String },
80    #[error("duplicate enum name in module '{module}': {name}")]
81    #[diagnostic(help("enum names must be unique within a module; rename the duplicate"))]
82    DuplicateEnumName { module: String, name: String },
83    #[error("empty enum in module '{module}': {name}")]
84    #[diagnostic(help("enums must have at least one variant; add a variant or remove the enum"))]
85    EmptyEnum { module: String, name: String },
86    #[error("duplicate enum variant in enum '{enum_name}': {variant}")]
87    #[diagnostic(help("variant names must be unique within an enum; rename the duplicate"))]
88    DuplicateEnumVariant { enum_name: String, variant: String },
89    #[error("duplicate enum value in enum '{enum_name}': {value}")]
90    #[diagnostic(help(
91        "variant numeric values must be unique within an enum; assign a different value"
92    ))]
93    DuplicateEnumValue { enum_name: String, value: i32 },
94    #[error("unknown type reference: {name}")]
95    #[diagnostic(help(
96        "define a struct or enum with this name in the same module, or check for typos"
97    ))]
98    UnknownTypeRef { name: String },
99    #[error("invalid map key type: {key_type}; only primitive types and strings are allowed as map keys")]
100    #[diagnostic(help("map keys must be primitive types (i32, u32, i64, f64, bool, string); structs, lists, and maps cannot be keys"))]
101    InvalidMapKey { key_type: String },
102    #[error(
103        "borrowed type '{ty}' is not valid in {location}; only function parameters are allowed"
104    )]
105    #[diagnostic(help("borrowed types (&str, &[u8]) can only be used as function parameters, not return types or struct fields"))]
106    BorrowedTypeInInvalidPosition { ty: String, location: String },
107    #[error("duplicate callback name in module '{module}': {name}")]
108    #[diagnostic(help("callback names must be unique within a module; rename the duplicate"))]
109    DuplicateCallbackName { module: String, name: String },
110    #[error(
111        "listener '{listener}' in module '{module}' references undefined callback '{callback}'"
112    )]
113    #[diagnostic(help(
114        "listener event_callback must reference a callback defined in the same module"
115    ))]
116    ListenerCallbackNotFound {
117        module: String,
118        listener: String,
119        callback: String,
120    },
121    #[error("duplicate listener name in module '{module}': {name}")]
122    #[diagnostic(help("listener names must be unique within a module; rename the duplicate"))]
123    DuplicateListenerName { module: String, name: String },
124    #[error(
125        "callback '{callback}' in module '{module}' has parameter '{param}' with unsupported \
126         type '{ty}'"
127    )]
128    #[diagnostic(help(
129        "callback parameters are limited to scalars, bool, enums, string, bytes, handles, \
130         structs, optionals of those, lists of scalars/strings, and maps of scalars/strings — \
131         every target must be able to marshal a callback argument without an FFI round-trip"
132    ))]
133    UnsupportedCallbackParamType {
134        module: String,
135        callback: String,
136        param: String,
137        ty: String,
138    },
139    #[error("iterator type is only valid as a function return type, found in {location}")]
140    #[diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields"))]
141    IteratorInInvalidPosition { location: String },
142    #[error("unsupported element type '{ty}' in {location}")]
143    #[diagnostic(help(
144        "the C ABI lowers lists, maps, and iterators to flat parallel arrays, so element \
145         types must be flat: list/iterator elements may be scalars, bool, enums, strings, \
146         handles, or structs (plus optional structs/handles in lists); map keys and values \
147         may be scalars, bool, enums, or strings"
148    ))]
149    UnsupportedElementType { location: String, ty: String },
150    #[error("async function '{module}::{function}' cannot return an iterator")]
151    #[diagnostic(help(
152        "the callback-completed async ABI has no streaming protocol; return a list ([T]) \
153         from the async function, or make the function synchronous and return iter<T>"
154    ))]
155    AsyncIteratorReturn { module: String, function: String },
156    #[error("builder struct '{name}' in module '{module}' must have at least one field")]
157    #[diagnostic(help(
158        "builder structs must have at least one field; add a field or set builder: false"
159    ))]
160    BuilderStructEmpty { module: String, name: String },
161    #[error("unsupported schema version '{version}'; supported versions: {supported}")]
162    #[diagnostic(help(
163        "set the version field to the current schema version and update the \
164         document to match the current schema (see docs/src/idl.md)"
165    ))]
166    UnsupportedSchemaVersion { version: String, supported: String },
167}
168
169/// Validate an [`Api`]. The optional `source` is `(filename, contents)` of the
170/// IDL file and is used at the call site to attach a span to a returned error
171/// via [`ValidationDiagnostic::new`]. Pass `None` when the API is constructed
172/// in memory (tests, programmatic builds) and there is no on-disk source.
173#[allow(clippy::result_large_err)]
174pub fn validate_api(
175    api: &mut Api,
176    source: Option<(&str, &str)>,
177) -> Result<(), ValidationDiagnostic> {
178    validate_api_inner(api).map_err(|e| ValidationDiagnostic::new(e, source))
179}
180
181fn validate_api_inner(api: &mut Api) -> Result<(), ValidationError> {
182    if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) {
183        return Err(ValidationError::UnsupportedSchemaVersion {
184            version: api.version.clone(),
185            supported: SUPPORTED_VERSIONS.join(", "),
186        });
187    }
188    let mut module_names = BTreeSet::new();
189    for m in &api.modules {
190        if !module_names.insert(m.name.clone()) {
191            return Err(ValidationError::DuplicateModuleName(m.name.clone()));
192        }
193        rules::validate_module(m, &api.modules)?;
194    }
195    resolve_type_refs(api);
196    Ok(())
197}