1use 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#[derive(Debug, thiserror::Error, Diagnostic)]
32pub enum ValidationError {
33 #[error("module has no name")]
35 #[diagnostic(help("every module must have a non-empty 'name' field"))]
36 NoModuleName,
37 #[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 #[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 #[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: String,
55 function: String,
57 },
58 #[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: String,
64 function: String,
66 param: String,
68 },
69 #[error("reserved keyword used: {0}")]
71 #[diagnostic(help("choose a different name that is not a language reserved word"))]
72 ReservedKeyword(String),
73 #[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 #[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 #[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: String,
87 name: String,
89 },
90 #[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: String,
98 code: i32,
100 },
101 #[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: String,
110 name: String,
112 },
113 #[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: String,
121 name: String,
123 },
124 #[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: String,
139 symbol: String,
141 },
142 #[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: String,
151 function: String,
153 },
154 #[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 name: String,
168 first: String,
170 second: String,
172 },
173 #[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 name: String,
187 first: String,
189 second: String,
191 },
192 #[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: String,
198 name: String,
200 },
201 #[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_name: String,
207 field: String,
209 },
210 #[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: String,
216 name: String,
218 },
219 #[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: String,
225 name: String,
227 },
228 #[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: String,
234 name: String,
236 },
237 #[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_name: String,
243 variant: String,
245 },
246 #[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_name: String,
254 variant: String,
256 field: String,
258 },
259 #[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_name: String,
267 value: i32,
269 },
270 #[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: String,
276 name: String,
278 },
279 #[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: String,
289 name: String,
291 },
292 #[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: String,
301 name: String,
303 },
304 #[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: String,
313 constructor: String,
315 },
316 #[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: String,
325 constructor: String,
327 },
328 #[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 name: String,
338 location: String,
340 },
341 #[error("unknown type reference: {name}")]
343 #[diagnostic(help("define a struct, enum, or interface with this name, or check for typos"))]
344 UnknownTypeRef {
345 name: String,
347 },
348 #[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 key_type: String,
354 },
355 #[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 ty: String,
363 location: String,
365 },
366 #[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: String,
372 name: String,
374 },
375 #[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: String,
385 listener: String,
387 callback: String,
389 },
390 #[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: String,
396 name: String,
398 },
399 #[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: String,
412 callback: String,
414 param: String,
416 ty: String,
418 },
419 #[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 location: String,
425 },
426 #[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 location: String,
437 ty: String,
439 },
440 #[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: String,
449 function: String,
451 },
452 #[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: String,
460 name: String,
462 },
463 #[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: String,
472 supported: String,
474 },
475}
476
477#[derive(Debug)]
483pub struct ValidationDiagnostics {
484 pub diagnostics: Vec<ValidationDiagnostic>,
486}
487
488impl ValidationDiagnostics {
489 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
536pub 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 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}