1use crate::ir::{Api, SUPPORTED_VERSIONS};
12use crate::model::{Model, RESERVED_SYMBOL_FAMILIES, RUNTIME_SYMBOLS};
13use crate::pkg::Identity;
14#[cfg(feature = "idl")]
15use miette::Diagnostic;
16use std::collections::BTreeMap;
17
18mod diagnostic;
19mod rules;
20#[cfg(all(test, feature = "idl"))]
21mod tests;
22mod warnings;
23
24pub use diagnostic::ValidationDiagnostic;
25pub use warnings::{collect_warnings, ValidationWarning};
26
27#[derive(Debug, thiserror::Error, serde::Serialize)]
37#[cfg_attr(feature = "idl", derive(Diagnostic))]
38#[serde(tag = "code")]
39pub enum ValidationError {
40 #[error("unsupported schema version '{version}'; supported versions: {supported}")]
42 #[cfg_attr(
43 feature = "idl",
44 diagnostic(help(
45 "set the version field to the current schema version and update the \
46 document to match the current schema (see docs/src/reference/idl.md)"
47 ))
48 )]
49 UnsupportedSchemaVersion {
50 version: String,
52 supported: String,
54 },
55 #[error("module has no name")]
57 #[cfg_attr(
58 feature = "idl",
59 diagnostic(help("every module must have a non-empty 'name' field"))
60 )]
61 NoModuleName,
62 #[error("duplicate module name: {module}")]
64 #[cfg_attr(
65 feature = "idl",
66 diagnostic(help(
67 "module names must be unique among siblings; rename or merge the duplicate"
68 ))
69 )]
70 DuplicateModuleName {
71 module: String,
73 },
74 #[error("invalid module name '{name}': {reason}")]
76 #[cfg_attr(
77 feature = "idl",
78 diagnostic(help(
79 "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word"
80 ))
81 )]
82 InvalidModuleName {
83 name: String,
85 reason: &'static str,
87 },
88 #[error("reserved keyword used: {name}")]
90 #[cfg_attr(
91 feature = "idl",
92 diagnostic(help("choose a different name that is not a language reserved word"))
93 )]
94 ReservedKeyword {
95 name: String,
97 },
98 #[error("invalid identifier '{name}': {reason}")]
100 #[cfg_attr(feature = "idl", diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters")))]
101 InvalidIdentifier {
102 name: String,
104 reason: &'static str,
106 },
107 #[error("duplicate type name '{name}' (declared in '{first}' and '{second}')")]
115 #[cfg_attr(
116 feature = "idl",
117 diagnostic(help(
118 "struct, enum, interface, callback interface, and error domain names must be unique \
119 across the whole API; rename one of the declarations"
120 ))
121 )]
122 DuplicateTypeName {
123 name: String,
125 first: String,
127 second: String,
129 },
130 #[error("duplicate function name '{name}' (declared in '{first}' and '{second}')")]
136 #[cfg_attr(
137 feature = "idl",
138 diagnostic(help(
139 "free function names must be unique across the whole API, because targets \
140 flatten module functions into one namespace; rename one of them (for example \
141 'open_store')"
142 ))
143 )]
144 DuplicateFunctionName {
145 name: String,
147 first: String,
149 second: String,
151 },
152 #[error("function '{function}.{name}' has the same name as error domain '{domain}.{name}'")]
154 #[cfg_attr(
155 feature = "idl",
156 diagnostic(help(
157 "function and error domain names share one flat namespace in several targets; \
158 rename one of them"
159 ))
160 )]
161 NameCollisionWithErrorDomain {
162 name: String,
164 function: String,
166 domain: String,
168 },
169 #[error("duplicate error code name '{name}' (declared in '{first}' and '{second}')")]
175 #[cfg_attr(
176 feature = "idl",
177 diagnostic(help(
178 "error code names must be unique across the whole API; qualify one of them \
179 (for example 'OrderNotFound')"
180 ))
181 )]
182 DuplicateErrorCodeName {
183 name: String,
185 first: String,
187 second: String,
189 },
190 #[error("C symbol collision: {first} and {second} both lower to '{symbol}'")]
198 #[cfg_attr(
199 feature = "idl",
200 diagnostic(help(
201 "every C identifier is the library prefix plus the underscore-joined module path \
202 plus the declaration name; rename one of the colliding declarations"
203 ))
204 )]
205 SymbolCollision {
206 symbol: String,
208 first: String,
210 second: String,
212 },
213 #[error("duplicate param name in function '{function}' of module '{module}': {param}")]
215 #[cfg_attr(
216 feature = "idl",
217 diagnostic(help(
218 "parameter names must be unique within a function; rename the duplicate"
219 ))
220 )]
221 DuplicateParamName {
222 module: String,
224 function: String,
226 param: String,
228 },
229 #[error("function '{module}::{function}' is cancellable but not async")]
231 #[cfg_attr(
232 feature = "idl",
233 diagnostic(help(
234 "only async functions take a cancel token; add `async: true` or remove \
235 `cancellable: true`"
236 ))
237 )]
238 CancellableNotAsync {
239 module: String,
241 function: String,
243 },
244 #[error("function '{module}::{function}' declares throws but no error domain is in scope")]
247 #[cfg_attr(
248 feature = "idl",
249 diagnostic(help(
250 "a throwing function (or callback-interface method) reports codes from its \
251 module's error domain; declare an `errors:` block on this module (or an ancestor \
252 module), or remove `throws: true`"
253 ))
254 )]
255 ThrowsWithoutErrorDomain {
256 module: String,
258 function: String,
260 },
261 #[error("async function '{module}::{function}' cannot return an iterator")]
263 #[cfg_attr(
264 feature = "idl",
265 diagnostic(help(
266 "the callback-completed async ABI has no streaming protocol; return a list ([T]) \
267 from the async function, or make the function synchronous and return iter<T>"
268 ))
269 )]
270 AsyncIteratorReturn {
271 module: String,
273 function: String,
275 },
276 #[error("error domain missing name in module '{module}'")]
278 #[cfg_attr(
279 feature = "idl",
280 diagnostic(help("add a non-empty 'name' field to the error domain"))
281 )]
282 ErrorDomainMissingName {
283 module: String,
285 },
286 #[error("duplicate error numeric code in module '{module}': {value}")]
288 #[cfg_attr(
289 feature = "idl",
290 diagnostic(help(
291 "numeric error codes must be unique within a domain; assign a different value"
292 ))
293 )]
294 DuplicateErrorCode {
295 module: String,
297 value: i32,
299 },
300 #[error("invalid error code in module '{module}' for '{name}': must be a positive integer")]
303 #[cfg_attr(
304 feature = "idl",
305 diagnostic(help(
306 "0 means success and negative codes are reserved for the runtime (-1 generic, \
307 -2 panic, -3 marshalling failure, -4 foreign callback error, -5 cancelled); use a positive integer"
308 ))
309 )]
310 InvalidErrorCode {
311 module: String,
313 name: String,
315 },
316 #[error("empty struct in module '{module}': {name}")]
318 #[cfg_attr(
319 feature = "idl",
320 diagnostic(help(
321 "structs must have at least one field; add a field or remove the struct"
322 ))
323 )]
324 EmptyStruct {
325 module: String,
327 name: String,
329 },
330 #[error("duplicate field name in struct '{struct_name}': {field}")]
332 #[cfg_attr(
333 feature = "idl",
334 diagnostic(help("field names must be unique within a struct; rename the duplicate"))
335 )]
336 DuplicateStructField {
337 struct_name: String,
339 field: String,
341 },
342 #[error("empty enum in module '{module}': {name}")]
344 #[cfg_attr(
345 feature = "idl",
346 diagnostic(help(
347 "enums must have at least one variant; add a variant or remove the enum"
348 ))
349 )]
350 EmptyEnum {
351 module: String,
353 name: String,
355 },
356 #[error("duplicate enum variant in enum '{enum_name}': {variant}")]
358 #[cfg_attr(
359 feature = "idl",
360 diagnostic(help("variant names must be unique within an enum; rename the duplicate"))
361 )]
362 DuplicateEnumVariant {
363 enum_name: String,
365 variant: String,
367 },
368 #[error("duplicate field '{field}' in variant '{variant}' of enum '{enum_name}'")]
370 #[cfg_attr(
371 feature = "idl",
372 diagnostic(help(
373 "associated field names must be unique within an enum variant; rename the duplicate"
374 ))
375 )]
376 DuplicateEnumVariantField {
377 enum_name: String,
379 variant: String,
381 field: String,
383 },
384 #[error("duplicate enum value in enum '{enum_name}': {value}")]
386 #[cfg_attr(
387 feature = "idl",
388 diagnostic(help(
389 "variant numeric values must be unique within an enum; assign a different value"
390 ))
391 )]
392 DuplicateEnumValue {
393 enum_name: String,
395 value: i32,
397 },
398 #[error("empty interface in module '{module}': {name}")]
400 #[cfg_attr(
401 feature = "idl",
402 diagnostic(help(
403 "interfaces must declare at least one constructor, method, or static; \
404 add a member or remove the interface"
405 ))
406 )]
407 EmptyInterface {
408 module: String,
410 name: String,
412 },
413 #[error("duplicate member name in interface '{interface}': {name}")]
416 #[cfg_attr(
417 feature = "idl",
418 diagnostic(help(
419 "constructor, method, and static names share one namespace per interface; \
420 rename the duplicate"
421 ))
422 )]
423 DuplicateInterfaceMember {
424 interface: String,
426 name: String,
428 },
429 #[error("constructor '{constructor}' of interface '{interface}' declares a return type")]
431 #[cfg_attr(
432 feature = "idl",
433 diagnostic(help(
434 "a constructor implicitly returns a new instance of its interface; remove the \
435 `return` field"
436 ))
437 )]
438 ConstructorHasReturn {
439 interface: String,
441 constructor: String,
443 },
444 #[error("constructor '{constructor}' of interface '{interface}' cannot be async")]
446 #[cfg_attr(
447 feature = "idl",
448 diagnostic(help(
449 "constructors are synchronous; expose an async static factory returning the \
450 interface instead"
451 ))
452 )]
453 AsyncConstructor {
454 interface: String,
456 constructor: String,
458 },
459 #[error("empty callback interface in module '{module}': {name}")]
461 #[cfg_attr(
462 feature = "idl",
463 diagnostic(help(
464 "callback interfaces must declare at least one method; add a method or remove the \
465 callback interface"
466 ))
467 )]
468 EmptyCallbackInterface {
469 module: String,
471 name: String,
473 },
474 #[error("duplicate method name in callback interface '{interface}': {name}")]
476 #[cfg_attr(
477 feature = "idl",
478 diagnostic(help(
479 "method names must be unique within a callback interface; rename the duplicate"
480 ))
481 )]
482 DuplicateCallbackMethod {
483 interface: String,
485 name: String,
487 },
488 #[error("method '{method}' of callback interface '{interface}' {reason}")]
491 #[cfg_attr(
492 feature = "idl",
493 diagnostic(help(
494 "callback-interface methods are synchronous calls the consumer implements: they \
495 can't be async or cancellable, and they return nothing or any value except an \
496 iterator or a callback interface"
497 ))
498 )]
499 InvalidCallbackMethod {
500 interface: String,
502 method: String,
504 reason: &'static str,
506 },
507 #[error("unknown type reference: {name}")]
510 #[cfg_attr(
511 feature = "idl",
512 diagnostic(help(
513 "define a struct, enum, interface, or callback interface with this name, or check \
514 for typos"
515 ))
516 )]
517 UnknownTypeRef {
518 name: String,
520 },
521 #[error("qualified type reference '{name}': type names are global, so refer to a type by its bare name")]
523 #[cfg_attr(
524 feature = "idl",
525 diagnostic(help(
526 "every type name is unique across the API, so a reference is just the name: write \
527 'Contact', not 'contacts.Contact'"
528 ))
529 )]
530 QualifiedTypeRef {
531 name: String,
533 },
534 #[error("unsupported primitive type '{name}'")]
536 #[cfg_attr(
537 feature = "idl",
538 diagnostic(help(
539 "`usize` and `isize` vary with the platform and `u128`/`i128` have no portable C \
540 type: use `u64` or `i64`; for `char`, use `string`"
541 ))
542 )]
543 UnsupportedPrimitive {
544 name: String,
546 },
547 #[error("invalid map key type: {key_type}; only integers, bools, strings, and C-style enums are allowed as map keys")]
549 #[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")))]
550 InvalidMapKey {
551 key_type: String,
553 },
554 #[error("interface type '{name}' is not valid in {location}")]
556 #[cfg_attr(
557 feature = "idl",
558 diagnostic(help(
559 "interface objects may appear anywhere except as map keys; key a map by a scalar, \
560 string, or C-style enum instead"
561 ))
562 )]
563 InterfaceInInvalidPosition {
564 name: String,
566 location: String,
568 },
569 #[error("callback interface '{name}' is not valid in {location}")]
572 #[cfg_attr(
573 feature = "idl",
574 diagnostic(help(
575 "a callback interface is a consumer-implemented vtable: it may be passed (bare, or \
576 optional as `Cb?`) as a parameter of a function, constructor, static, or method, but \
577 it can't be returned, nested inside another type, or passed to another \
578 callback-interface method"
579 ))
580 )]
581 CallbackInterfaceInInvalidPosition {
582 name: String,
584 location: String,
586 },
587 #[error("iterator type is only valid as a function return type, found in {location}")]
589 #[cfg_attr(feature = "idl", diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields")))]
590 IteratorInInvalidPosition {
591 location: String,
593 },
594}
595
596#[derive(Debug)]
602pub struct ValidationDiagnostics {
603 pub diagnostics: Vec<ValidationDiagnostic>,
605}
606
607impl ValidationDiagnostics {
608 pub fn first(&self) -> &ValidationDiagnostic {
610 &self.diagnostics[0]
611 }
612}
613
614impl std::fmt::Display for ValidationDiagnostics {
615 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
616 for (i, d) in self.diagnostics.iter().enumerate() {
617 if i > 0 {
618 writeln!(f)?;
619 }
620 write!(f, "{d}")?;
621 }
622 Ok(())
623 }
624}
625
626impl std::error::Error for ValidationDiagnostics {}
627
628#[cfg(feature = "idl")]
629impl Diagnostic for ValidationDiagnostics {
630 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
631 self.first().code()
632 }
633
634 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
635 self.first().help()
636 }
637
638 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
639 self.first().source_code()
640 }
641
642 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
643 self.first().labels()
644 }
645
646 fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
647 if self.diagnostics.len() <= 1 {
648 return None;
649 }
650 Some(Box::new(
651 self.diagnostics[1..].iter().map(|d| d as &dyn Diagnostic),
652 ))
653 }
654}
655
656pub fn validate(
675 api: &Api,
676 identity: &Identity,
677 source: Option<(&str, &str)>,
678) -> Result<Model, ValidationDiagnostics> {
679 validate_scoped(api, identity, Options::default()).map_err(|errors| ValidationDiagnostics {
680 diagnostics: errors
681 .into_iter()
682 .map(|(error, scope)| ValidationDiagnostic::new(error, &scope, source))
683 .collect(),
684 })
685}
686
687#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
689pub struct Options {
690 pub foreign_names: bool,
696}
697
698pub type Found = (ValidationError, Vec<String>);
703
704pub fn validate_scoped(
713 api: &Api,
714 identity: &Identity,
715 options: Options,
716) -> Result<Model, Vec<Found>> {
717 if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) {
718 return Err(vec![(
721 ValidationError::UnsupportedSchemaVersion {
722 version: api.version.clone(),
723 supported: SUPPORTED_VERSIONS.join(", "),
724 },
725 vec![],
726 )]);
727 }
728 let types = crate::model::index(api);
729 let mut found = Vec::new();
730 rules::check(api, &types, options, &mut found);
731 if !found.is_empty() {
732 return Err(found);
735 }
736 let model = crate::model::build_indexed(api, identity.clone(), types);
737 check_symbol_collisions(&model, &mut found);
738 if found.is_empty() {
739 Ok(model)
740 } else {
741 Err(found)
742 }
743}
744
745fn check_symbol_collisions(model: &Model, found: &mut Vec<Found>) {
748 let prefix = model.prefix();
749 let mut seen: BTreeMap<String, String> = BTreeMap::new();
750 for (i, (symbol, origin)) in model.c_symbols().into_iter().enumerate() {
751 let family = symbol.strip_prefix(&format!("{prefix}_")).and_then(|rest| {
752 RESERVED_SYMBOL_FAMILIES
753 .iter()
754 .find(|f| rest.starts_with(*f))
755 });
756 if let (Some(family), true) = (family, i >= RUNTIME_SYMBOLS.len()) {
757 found.push((
758 ValidationError::SymbolCollision {
759 symbol: symbol.clone(),
760 first: format!("the C value-buffer helpers ('{prefix}_{family}*')"),
761 second: origin.clone(),
762 },
763 vec![],
764 ));
765 }
766 match seen.get(&symbol) {
767 Some(first) => found.push((
768 ValidationError::SymbolCollision {
769 symbol,
770 first: first.clone(),
771 second: origin,
772 },
773 vec![],
774 )),
775 None => {
776 seen.insert(symbol, origin);
777 }
778 }
779 }
780}