Skip to main content

weaveffi_model/
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), `diagnostic` (miette span attachment), and
4//! `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 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/// Every way an [`Api`] can fail validation.
27///
28/// `validate_api` collects every variant it encounters. Each variant carries
29/// the names needed to render an actionable diagnostic, and the
30/// `#[error]`/`#[diagnostic]` attributes supply the message and help text
31/// shown to the user.
32#[derive(Debug, thiserror::Error)]
33#[cfg_attr(feature = "idl", derive(Diagnostic))]
34pub enum ValidationError {
35    /// A module is missing its required `name` field.
36    #[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    /// Two modules share the same name.
43    #[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    /// A module name is not a valid identifier; the second field explains why.
52    #[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    /// Two functions in the same module share a name.
61    #[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 that contains the colliding functions.
68        module: String,
69        /// Duplicated function name.
70        function: String,
71    },
72    /// Two parameters of one function share a name.
73    #[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 that contains the function.
82        module: String,
83        /// Function that contains the colliding parameters.
84        function: String,
85        /// Duplicated parameter name.
86        param: String,
87    },
88    /// A name matches a reserved keyword in one of the target languages.
89    #[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    /// An identifier is malformed; the second field explains why.
96    #[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    /// An error domain in the named module is missing its `name` field.
100    #[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    /// Two error codes in the same module share a name.
107    #[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 that contains the error domain.
114        module: String,
115        /// Duplicated error code name.
116        name: String,
117    },
118    /// Two error codes in the same module share a numeric value.
119    #[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 that contains the error domain.
128        module: String,
129        /// Conflicting numeric error code.
130        code: i32,
131    },
132    /// An error code uses a reserved value: `0` means success and the
133    /// negative range belongs to the runtime's trap codes.
134    #[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 that contains the error domain.
144        module: String,
145        /// Error code name with the invalid value.
146        name: String,
147    },
148    /// A function name collides with an error domain name in the same module.
149    #[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 where the collision occurs.
158        module: String,
159        /// Name shared by the function and the error domain.
160        name: String,
161    },
162    /// Two declarations lower to the same C identifier.
163    ///
164    /// Every function, type, constant, and runtime name in the C ABI starts
165    /// with the library prefix and is flattened from its module path, so
166    /// `m.x_y`, `m.x.y`, and `m_x.y` all become `{prefix}_m_x_y`, and an
167    /// async `foo`'s completion type `{prefix}_m_foo_callback` collides with
168    /// a function named `foo_callback`.
169    #[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        /// The colliding C identifier.
179        symbol: String,
180        /// The declaration that claimed the identifier first.
181        first: String,
182        /// The declaration that collided with it.
183        second: String,
184    },
185    /// A synchronous callable declares `cancellable: true`.
186    #[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 that contains the function.
196        module: String,
197        /// The offending function.
198        function: String,
199    },
200    /// A function declares `throws: true` but no error domain is in scope.
201    #[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 that contains the function.
211        module: String,
212        /// Function marked `throws` with no domain in scope.
213        function: String,
214    },
215    /// Two types (structs, enums, interfaces, or callback interfaces) share a
216    /// bare name.
217    ///
218    /// Type names must be unique across the whole API: generators emit flat
219    /// per-language type names, and unqualified cross-module references
220    /// resolve by bare name, so two types called `Config` would collide in
221    /// generated code and make references ambiguous.
222    #[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        /// The colliding bare type name.
232        name: String,
233        /// Module path of the first declaration.
234        first: String,
235        /// Module path of the second declaration.
236        second: String,
237    },
238    /// Two error domains declare a code with the same name.
239    ///
240    /// Code names must be unique across every domain in the API: backends
241    /// with flat namespaces (Python, Node, Go) derive one error class or
242    /// constant per code, so `NotFound` in two domains would collide in
243    /// generated code.
244    #[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        /// The colliding code name.
254        name: String,
255        /// Domain of the first declaration, as `module.Domain`.
256        first: String,
257        /// Domain of the second declaration, as `module.Domain`.
258        second: String,
259    },
260    /// Two structs in the same module share a name.
261    #[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 that contains the structs.
268        module: String,
269        /// Duplicated struct name.
270        name: String,
271    },
272    /// Two fields of one struct share a name.
273    #[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 that contains the colliding fields.
280        struct_name: String,
281        /// Duplicated field name.
282        field: String,
283    },
284    /// A struct declares no fields.
285    #[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 that contains the struct.
294        module: String,
295        /// Name of the empty struct.
296        name: String,
297    },
298    /// Two enums in the same module share a name.
299    #[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 that contains the enums.
306        module: String,
307        /// Duplicated enum name.
308        name: String,
309    },
310    /// An enum declares no variants.
311    #[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 that contains the enum.
320        module: String,
321        /// Name of the empty enum.
322        name: String,
323    },
324    /// Two variants of one enum share a name.
325    #[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 that contains the colliding variants.
332        enum_name: String,
333        /// Duplicated variant name.
334        variant: String,
335    },
336    /// Two associated fields of one rich enum variant share a name.
337    #[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 that contains the variant.
346        enum_name: String,
347        /// Variant that contains the colliding fields.
348        variant: String,
349        /// Duplicated associated field name.
350        field: String,
351    },
352    /// Two variants of one enum share a numeric discriminant.
353    #[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 that contains the variants.
362        enum_name: String,
363        /// Conflicting numeric discriminant.
364        value: i32,
365    },
366    /// Two interfaces in the same module share a name.
367    #[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 that contains the interfaces.
374        module: String,
375        /// Duplicated interface name.
376        name: String,
377    },
378    /// Two members (constructors, methods, or statics) of one interface share
379    /// a name.
380    #[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 that contains the colliding members.
390        interface: String,
391        /// Duplicated member name.
392        name: String,
393    },
394    /// An interface declares no members at all.
395    #[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 that contains the interface.
405        module: String,
406        /// Name of the empty interface.
407        name: String,
408    },
409    /// An interface constructor declares an explicit return type.
410    #[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 that declares the constructor.
420        interface: String,
421        /// The offending constructor.
422        constructor: String,
423    },
424    /// An interface constructor is marked `async`.
425    #[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 that declares the constructor.
435        interface: String,
436        /// The offending constructor.
437        constructor: String,
438    },
439    /// An interface reference appears as a map key.
440    #[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        /// The referenced interface name.
450        name: String,
451        /// Position where the interface reference appeared.
452        location: String,
453    },
454    /// A type reference names a struct, enum, interface, or callback interface
455    /// that doesn't exist.
456    #[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        /// Unresolved type name.
466        name: String,
467    },
468    /// A map uses a key type the C ABI can't represent.
469    #[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        /// Rejected key type, rendered as it appears in the IDL.
473        key_type: String,
474    },
475    /// An iterator type appears somewhere other than a function return.
476    #[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        /// Position where the iterator type appeared.
480        location: String,
481    },
482    /// Two callback interfaces in the same module share a name.
483    #[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 that contains the callback interfaces.
492        module: String,
493        /// Duplicated callback interface name.
494        name: String,
495    },
496    /// A callback interface declares no methods.
497    #[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 that contains the callback interface.
507        module: String,
508        /// Name of the empty callback interface.
509        name: String,
510    },
511    /// Two methods of one callback interface share a name.
512    #[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        /// Callback interface that contains the colliding methods.
521        interface: String,
522        /// Duplicated method name.
523        name: String,
524    },
525    /// A callback-interface method uses a flag or return type the callback
526    /// ABI can't carry. The `reason` names the offending feature.
527    #[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        /// Callback interface that declares the method.
538        interface: String,
539        /// The offending method.
540        method: String,
541        /// Why the method was rejected.
542        reason: &'static str,
543    },
544    /// A callback-interface type appears in a position other than a callable
545    /// parameter.
546    #[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        /// The referenced callback interface name.
557        name: String,
558        /// Position where the reference appeared.
559        location: String,
560    },
561    /// An async function tries to return an iterator, which has no async ABI.
562    #[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 that contains the function.
572        module: String,
573        /// Async function with the iterator return.
574        function: String,
575    },
576    /// The document declares a schema version this build doesn't support.
577    #[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 requested by the document.
587        version: String,
588        /// Comma-separated list of versions this build accepts.
589        supported: String,
590    },
591}
592
593/// Every validation failure found in one pass, each wrapped as a
594/// [`ValidationDiagnostic`] carrying an optional source span.
595///
596/// `Display` renders every message on its own line; miette renderers reach the
597/// individual diagnostics through [`Diagnostic::related`].
598#[derive(Debug)]
599pub struct ValidationDiagnostics {
600    /// The individual failures, in the order they were found. Never empty.
601    pub diagnostics: Vec<ValidationDiagnostic>,
602}
603
604impl ValidationDiagnostics {
605    /// The first failure, which every report is guaranteed to contain.
606    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
653/// Validate an [`Api`] document, reporting **every** rule violation found.
654/// The optional `source` is `(filename, contents)` of the IDL file and is
655/// used to attach spans to the returned diagnostics. Pass `None` when the
656/// API is constructed in memory (tests, programmatic builds) and there is no
657/// on-disk source.
658///
659/// This is the one checked way to turn a parsed document into the
660/// [`ResolvedApi`] every generator consumes. The document is never rewritten;
661/// the resolved view indexes its declarations so that
662/// [`ResolvedApi::resolve`] can turn each written reference into a
663/// [`Ty`](crate::model::Ty) whose kind and owning module are known.
664///
665/// # Errors
666///
667/// Returns [`ValidationDiagnostics`] carrying one [`ValidationDiagnostic`]
668/// per violation: an unsupported schema version, a duplicate or invalid name,
669/// an unknown or misplaced type, an empty struct or enum, a `throws` without
670/// an error domain, or any other rule violation in the catalog above.
671pub 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        // A wrong-schema document is checked no further: the rules below
691        // assume the current schema's shape.
692        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
713/// Build the binding model (any prefix works: collisions are invariant under
714/// a shared prefix) and reject any C identifier claimed twice.
715fn 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}