1use crate::ir::{Api, SUPPORTED_VERSIONS};
11#[cfg(feature = "idl")]
12use miette::Diagnostic;
13use std::collections::BTreeSet;
14
15use crate::resolved::ResolvedApi;
16
17mod diagnostic;
18mod rules;
19#[cfg(all(test, feature = "idl"))]
20mod tests;
21mod warnings;
22
23pub use diagnostic::ValidationDiagnostic;
24pub use warnings::{collect_warnings, ValidationWarning};
25
26#[derive(Debug, thiserror::Error)]
33#[cfg_attr(feature = "idl", derive(Diagnostic))]
34pub enum ValidationError {
35 #[error("module has no name")]
37 #[cfg_attr(
38 feature = "idl",
39 diagnostic(help("every module must have a non-empty 'name' field"))
40 )]
41 NoModuleName,
42 #[error("duplicate module name: {0}")]
44 #[cfg_attr(
45 feature = "idl",
46 diagnostic(help(
47 "module names must be unique within an API definition; rename or merge the duplicate"
48 ))
49 )]
50 DuplicateModuleName(String),
51 #[error("invalid module name '{0}': {1}")]
53 #[cfg_attr(
54 feature = "idl",
55 diagnostic(help(
56 "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word"
57 ))
58 )]
59 InvalidModuleName(String, &'static str),
60 #[error("duplicate function name in module '{module}': {function}")]
62 #[cfg_attr(
63 feature = "idl",
64 diagnostic(help("function names must be unique within a module; rename the duplicate"))
65 )]
66 DuplicateFunctionName {
67 module: String,
69 function: String,
71 },
72 #[error("duplicate param name in function '{function}' of module '{module}': {param}")]
74 #[cfg_attr(
75 feature = "idl",
76 diagnostic(help(
77 "parameter names must be unique within a function; rename the duplicate"
78 ))
79 )]
80 DuplicateParamName {
81 module: String,
83 function: String,
85 param: String,
87 },
88 #[error("reserved keyword used: {0}")]
90 #[cfg_attr(
91 feature = "idl",
92 diagnostic(help("choose a different name that is not a language reserved word"))
93 )]
94 ReservedKeyword(String),
95 #[error("invalid identifier '{0}': {1}")]
97 #[cfg_attr(feature = "idl", diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters")))]
98 InvalidIdentifier(String, &'static str),
99 #[error("error domain missing name in module '{0}'")]
101 #[cfg_attr(
102 feature = "idl",
103 diagnostic(help("add a non-empty 'name' field to the error domain"))
104 )]
105 ErrorDomainMissingName(String),
106 #[error("duplicate error code name in module '{module}': {name}")]
108 #[cfg_attr(
109 feature = "idl",
110 diagnostic(help("error code names must be unique within a module; rename the duplicate"))
111 )]
112 DuplicateErrorName {
113 module: String,
115 name: String,
117 },
118 #[error("duplicate error numeric code in module '{module}': {code}")]
120 #[cfg_attr(
121 feature = "idl",
122 diagnostic(help(
123 "numeric error codes must be unique within a module; assign a different value"
124 ))
125 )]
126 DuplicateErrorCode {
127 module: String,
129 code: i32,
131 },
132 #[error("invalid error code in module '{module}' for '{name}': must be a positive integer")]
135 #[cfg_attr(
136 feature = "idl",
137 diagnostic(help(
138 "0 means success and negative codes are reserved for the runtime (-1 generic, \
139 -2 panic, -3 marshalling failure, -4 foreign callback error, -5 cancelled); use a positive integer"
140 ))
141 )]
142 InvalidErrorCode {
143 module: String,
145 name: String,
147 },
148 #[error("function name collides with error domain name in module '{module}': {name}")]
150 #[cfg_attr(
151 feature = "idl",
152 diagnostic(help(
153 "function and error domain names share a namespace; rename one to avoid the collision"
154 ))
155 )]
156 NameCollisionWithErrorDomain {
157 module: String,
159 name: String,
161 },
162 #[error("C symbol collision: {first} and {second} both lower to '{symbol}'")]
170 #[cfg_attr(
171 feature = "idl",
172 diagnostic(help(
173 "every C identifier is the library prefix plus the underscore-joined module path \
174 plus the declaration name; rename one of the colliding declarations"
175 ))
176 )]
177 SymbolCollision {
178 symbol: String,
180 first: String,
182 second: String,
184 },
185 #[error("function '{module}::{function}' is cancellable but not async")]
187 #[cfg_attr(
188 feature = "idl",
189 diagnostic(help(
190 "only async functions take a cancel token; add `async: true` or remove \
191 `cancellable: true`"
192 ))
193 )]
194 CancellableNotAsync {
195 module: String,
197 function: String,
199 },
200 #[error("function '{module}::{function}' declares throws but no error domain is in scope")]
202 #[cfg_attr(
203 feature = "idl",
204 diagnostic(help(
205 "a throwing function reports codes from its module's error domain; declare an \
206 `errors:` block on this module (or an ancestor module), or remove `throws: true`"
207 ))
208 )]
209 ThrowsWithoutErrorDomain {
210 module: String,
212 function: String,
214 },
215 #[error("duplicate type name '{name}' (declared in '{first}' and '{second}')")]
223 #[cfg_attr(
224 feature = "idl",
225 diagnostic(help(
226 "struct, enum, interface, callback interface, and error domain names must be unique \
227 across the whole API; rename one of the declarations"
228 ))
229 )]
230 DuplicateTypeName {
231 name: String,
233 first: String,
235 second: String,
237 },
238 #[error("duplicate error code name '{name}' (declared in '{first}' and '{second}')")]
245 #[cfg_attr(
246 feature = "idl",
247 diagnostic(help(
248 "error code names must be unique across the whole API; qualify one of them \
249 (e.g. 'OrderNotFound')"
250 ))
251 )]
252 DuplicateErrorCodeName {
253 name: String,
255 first: String,
257 second: String,
259 },
260 #[error("duplicate struct name in module '{module}': {name}")]
262 #[cfg_attr(
263 feature = "idl",
264 diagnostic(help("struct names must be unique within a module; rename the duplicate"))
265 )]
266 DuplicateStructName {
267 module: String,
269 name: String,
271 },
272 #[error("duplicate field name in struct '{struct_name}': {field}")]
274 #[cfg_attr(
275 feature = "idl",
276 diagnostic(help("field names must be unique within a struct; rename the duplicate"))
277 )]
278 DuplicateStructField {
279 struct_name: String,
281 field: String,
283 },
284 #[error("empty struct in module '{module}': {name}")]
286 #[cfg_attr(
287 feature = "idl",
288 diagnostic(help(
289 "structs must have at least one field; add a field or remove the struct"
290 ))
291 )]
292 EmptyStruct {
293 module: String,
295 name: String,
297 },
298 #[error("duplicate enum name in module '{module}': {name}")]
300 #[cfg_attr(
301 feature = "idl",
302 diagnostic(help("enum names must be unique within a module; rename the duplicate"))
303 )]
304 DuplicateEnumName {
305 module: String,
307 name: String,
309 },
310 #[error("empty enum in module '{module}': {name}")]
312 #[cfg_attr(
313 feature = "idl",
314 diagnostic(help(
315 "enums must have at least one variant; add a variant or remove the enum"
316 ))
317 )]
318 EmptyEnum {
319 module: String,
321 name: String,
323 },
324 #[error("duplicate enum variant in enum '{enum_name}': {variant}")]
326 #[cfg_attr(
327 feature = "idl",
328 diagnostic(help("variant names must be unique within an enum; rename the duplicate"))
329 )]
330 DuplicateEnumVariant {
331 enum_name: String,
333 variant: String,
335 },
336 #[error("duplicate field '{field}' in variant '{variant}' of enum '{enum_name}'")]
338 #[cfg_attr(
339 feature = "idl",
340 diagnostic(help(
341 "associated field names must be unique within an enum variant; rename the duplicate"
342 ))
343 )]
344 DuplicateEnumVariantField {
345 enum_name: String,
347 variant: String,
349 field: String,
351 },
352 #[error("duplicate enum value in enum '{enum_name}': {value}")]
354 #[cfg_attr(
355 feature = "idl",
356 diagnostic(help(
357 "variant numeric values must be unique within an enum; assign a different value"
358 ))
359 )]
360 DuplicateEnumValue {
361 enum_name: String,
363 value: i32,
365 },
366 #[error("duplicate interface name in module '{module}': {name}")]
368 #[cfg_attr(
369 feature = "idl",
370 diagnostic(help("interface names must be unique within a module; rename the duplicate"))
371 )]
372 DuplicateInterfaceName {
373 module: String,
375 name: String,
377 },
378 #[error("duplicate member name in interface '{interface}': {name}")]
381 #[cfg_attr(
382 feature = "idl",
383 diagnostic(help(
384 "constructor, method, and static names share one namespace per interface; \
385 rename the duplicate"
386 ))
387 )]
388 DuplicateInterfaceMember {
389 interface: String,
391 name: String,
393 },
394 #[error("empty interface in module '{module}': {name}")]
396 #[cfg_attr(
397 feature = "idl",
398 diagnostic(help(
399 "interfaces must declare at least one constructor, method, or static; \
400 add a member or remove the interface"
401 ))
402 )]
403 EmptyInterface {
404 module: String,
406 name: String,
408 },
409 #[error("constructor '{constructor}' of interface '{interface}' declares a return type")]
411 #[cfg_attr(
412 feature = "idl",
413 diagnostic(help(
414 "a constructor implicitly returns a new instance of its interface; remove the \
415 `return` field"
416 ))
417 )]
418 ConstructorHasReturn {
419 interface: String,
421 constructor: String,
423 },
424 #[error("constructor '{constructor}' of interface '{interface}' cannot be async")]
426 #[cfg_attr(
427 feature = "idl",
428 diagnostic(help(
429 "constructors are synchronous; expose an async static factory returning the \
430 interface instead"
431 ))
432 )]
433 AsyncConstructor {
434 interface: String,
436 constructor: String,
438 },
439 #[error("interface type '{name}' is not valid in {location}")]
441 #[cfg_attr(
442 feature = "idl",
443 diagnostic(help(
444 "interface objects may appear anywhere except as map keys; key a map by a scalar, \
445 string, or C-style enum instead"
446 ))
447 )]
448 InterfaceInInvalidPosition {
449 name: String,
451 location: String,
453 },
454 #[error("unknown type reference: {name}")]
457 #[cfg_attr(
458 feature = "idl",
459 diagnostic(help(
460 "define a struct, enum, interface, or callback interface with this name, or check \
461 for typos"
462 ))
463 )]
464 UnknownTypeRef {
465 name: String,
467 },
468 #[error("invalid map key type: {key_type}; only integers, bools, strings, and C-style enums are allowed as map keys")]
470 #[cfg_attr(feature = "idl", diagnostic(help("map keys must be integers (i8 through u64), bool, string, or a C-style enum; structs, rich enums, interfaces, optionals, lists, and maps cannot be keys")))]
471 InvalidMapKey {
472 key_type: String,
474 },
475 #[error("iterator type is only valid as a function return type, found in {location}")]
477 #[cfg_attr(feature = "idl", diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields")))]
478 IteratorInInvalidPosition {
479 location: String,
481 },
482 #[error("duplicate callback interface name in module '{module}': {name}")]
484 #[cfg_attr(
485 feature = "idl",
486 diagnostic(help(
487 "callback interface names must be unique within a module; rename the duplicate"
488 ))
489 )]
490 DuplicateCallbackInterfaceName {
491 module: String,
493 name: String,
495 },
496 #[error("empty callback interface in module '{module}': {name}")]
498 #[cfg_attr(
499 feature = "idl",
500 diagnostic(help(
501 "callback interfaces must declare at least one method; add a method or remove the \
502 callback interface"
503 ))
504 )]
505 EmptyCallbackInterface {
506 module: String,
508 name: String,
510 },
511 #[error("duplicate method name in callback interface '{interface}': {name}")]
513 #[cfg_attr(
514 feature = "idl",
515 diagnostic(help(
516 "method names must be unique within a callback interface; rename the duplicate"
517 ))
518 )]
519 DuplicateCallbackMethod {
520 interface: String,
522 name: String,
524 },
525 #[error("method '{method}' of callback interface '{interface}' {reason}")]
528 #[cfg_attr(
529 feature = "idl",
530 diagnostic(help(
531 "callback-interface methods are synchronous, never throw, and return either nothing \
532 or a direct value (a scalar, bool, or C-style enum): the consumer implements them, \
533 so no producer-owned allocation or error payload can flow back"
534 ))
535 )]
536 InvalidCallbackMethod {
537 interface: String,
539 method: String,
541 reason: &'static str,
543 },
544 #[error("callback interface '{name}' is not valid in {location}")]
547 #[cfg_attr(
548 feature = "idl",
549 diagnostic(help(
550 "a callback interface is a consumer-implemented vtable: it may be passed as a \
551 parameter of a function, constructor, static, or method, but it can't be returned, \
552 nested inside another type, or passed to another callback-interface method"
553 ))
554 )]
555 CallbackInterfaceInInvalidPosition {
556 name: String,
558 location: String,
560 },
561 #[error("async function '{module}::{function}' cannot return an iterator")]
563 #[cfg_attr(
564 feature = "idl",
565 diagnostic(help(
566 "the callback-completed async ABI has no streaming protocol; return a list ([T]) \
567 from the async function, or make the function synchronous and return iter<T>"
568 ))
569 )]
570 AsyncIteratorReturn {
571 module: String,
573 function: String,
575 },
576 #[error("unsupported schema version '{version}'; supported versions: {supported}")]
578 #[cfg_attr(
579 feature = "idl",
580 diagnostic(help(
581 "set the version field to the current schema version and update the \
582 document to match the current schema (see docs/src/reference/idl.md)"
583 ))
584 )]
585 UnsupportedSchemaVersion {
586 version: String,
588 supported: String,
590 },
591}
592
593#[derive(Debug)]
599pub struct ValidationDiagnostics {
600 pub diagnostics: Vec<ValidationDiagnostic>,
602}
603
604impl ValidationDiagnostics {
605 pub fn first(&self) -> &ValidationDiagnostic {
607 &self.diagnostics[0]
608 }
609}
610
611impl std::fmt::Display for ValidationDiagnostics {
612 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
613 for (i, d) in self.diagnostics.iter().enumerate() {
614 if i > 0 {
615 writeln!(f)?;
616 }
617 write!(f, "{d}")?;
618 }
619 Ok(())
620 }
621}
622
623impl std::error::Error for ValidationDiagnostics {}
624
625#[cfg(feature = "idl")]
626impl Diagnostic for ValidationDiagnostics {
627 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
628 self.first().code()
629 }
630
631 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
632 self.first().help()
633 }
634
635 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
636 self.first().source_code()
637 }
638
639 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
640 self.first().labels()
641 }
642
643 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
644 if self.diagnostics.len() <= 1 {
645 return None;
646 }
647 Some(Box::new(
648 self.diagnostics[1..].iter().map(|d| d as &dyn Diagnostic),
649 ))
650 }
651}
652
653pub fn validate_api(
672 api: Api,
673 source: Option<(&str, &str)>,
674) -> Result<ResolvedApi, ValidationDiagnostics> {
675 let errors = validate_api_inner(&api);
676 if errors.is_empty() {
677 return Ok(ResolvedApi::assume_valid(api));
678 }
679 Err(ValidationDiagnostics {
680 diagnostics: errors
681 .into_iter()
682 .map(|e| ValidationDiagnostic::new(e, source))
683 .collect(),
684 })
685}
686
687fn validate_api_inner(api: &Api) -> Vec<ValidationError> {
688 let mut errors = Vec::new();
689 if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) {
690 return vec![ValidationError::UnsupportedSchemaVersion {
693 version: api.version.clone(),
694 supported: SUPPORTED_VERSIONS.join(", "),
695 }];
696 }
697 let types = rules::TypeIndex::build(&api.modules);
698 let mut module_names = BTreeSet::new();
699 for m in &api.modules {
700 if !module_names.insert(m.name.clone()) {
701 errors.push(ValidationError::DuplicateModuleName(m.name.clone()));
702 }
703 rules::validate_module(m, &types, false, &mut errors);
704 }
705 rules::check_global_type_names(&api.modules, &mut errors);
706 rules::check_global_error_code_names(&api.modules, &mut errors);
707 if errors.is_empty() {
708 check_symbol_collisions(api, &mut errors);
709 }
710 errors
711}
712
713fn check_symbol_collisions(api: &Api, errors: &mut Vec<ValidationError>) {
716 let resolved = ResolvedApi::assume_valid(api.clone());
717 let model = crate::model::BindingModel::build(&resolved);
718 let mut seen: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
719 let runtime = crate::model::RUNTIME_SYMBOLS.len();
720 for (i, (symbol, origin)) in model.c_symbols().into_iter().enumerate() {
721 let family = symbol
722 .strip_prefix(&format!("{}_", model.prefix))
723 .and_then(|rest| {
724 crate::model::RESERVED_SYMBOL_FAMILIES
725 .iter()
726 .find(|f| rest.starts_with(*f))
727 });
728 if let (Some(family), true) = (family, i >= runtime) {
729 errors.push(ValidationError::SymbolCollision {
730 symbol: symbol.clone(),
731 first: format!("the C value-buffer helpers ('{}_{family}*')", model.prefix),
732 second: origin.clone(),
733 });
734 }
735 match seen.get(&symbol) {
736 Some(first) => errors.push(ValidationError::SymbolCollision {
737 symbol,
738 first: first.clone(),
739 second: origin,
740 }),
741 None => {
742 seen.insert(symbol, origin);
743 }
744 }
745 }
746}