Skip to main content

weaveffi_model/
validate.rs

1//! IDL validation. This module owns the [`ValidationError`] catalog and the
2//! [`validate`] entry point, which checks an [`Api`] against the library's
3//! [`Identity`] and builds the one [`Model`] every generator consumes. The
4//! work is split across submodules: `rules` (name, type, and shape checks),
5//! `diagnostic` (miette span attachment), and `warnings` (advisory lints).
6//!
7//! Validation collects *every* rule violation before failing, so a document
8//! with several problems reports them all in one run rather than one per
9//! invocation.
10
11use 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/// Every way an [`Api`] can fail validation.
28///
29/// [`validate`] collects every variant it encounters. Each variant carries
30/// the names needed to render an actionable diagnostic, and the
31/// `#[error]`/`#[diagnostic]` attributes supply the message and help text
32/// shown to the user. Module fields hold the module's dotted path
33/// (`outer.inner`). The variant serializes as an object whose `code` is the
34/// variant name, with the variant's fields alongside (the shape of
35/// `weaveffi validate --format json` failures).
36#[derive(Debug, thiserror::Error, serde::Serialize)]
37#[cfg_attr(feature = "idl", derive(Diagnostic))]
38#[serde(tag = "code")]
39pub enum ValidationError {
40    /// The document declares a schema version this build doesn't support.
41    #[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 requested by the document.
51        version: String,
52        /// Comma-separated list of versions this build accepts.
53        supported: String,
54    },
55    /// A module is missing its required `name` field.
56    #[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    /// Two sibling modules share a name.
63    #[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        /// Dotted path of the duplicated module.
72        module: String,
73    },
74    /// A module name is not a valid identifier.
75    #[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        /// The rejected module name.
84        name: String,
85        /// Why the name was rejected.
86        reason: &'static str,
87    },
88    /// A name matches a reserved keyword in one of the target languages.
89    #[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        /// The reserved name.
96        name: String,
97    },
98    /// An identifier is malformed.
99    #[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        /// The rejected identifier.
103        name: String,
104        /// Why the identifier was rejected.
105        reason: &'static str,
106    },
107    /// Two types (records, enums, interfaces, callback interfaces, or error
108    /// domains) share a name.
109    ///
110    /// Type names are global: generators emit flat per-language type names
111    /// and a reference names a type by its bare name, so two types called
112    /// `Config` would collide in generated code and make references
113    /// ambiguous.
114    #[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        /// The colliding type name.
124        name: String,
125        /// Module path of the first declaration.
126        first: String,
127        /// Module path of the second declaration.
128        second: String,
129    },
130    /// Two free functions share a name.
131    ///
132    /// Function names are global: several targets flatten every module's
133    /// functions into one namespace, so `a.open` and `b.open` would collide
134    /// there.
135    #[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        /// The colliding function name.
146        name: String,
147        /// Module path of the first declaration.
148        first: String,
149        /// Module path of the second declaration.
150        second: String,
151    },
152    /// A free function has the same name as an error domain.
153    #[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        /// The shared name.
163        name: String,
164        /// Module path of the function.
165        function: String,
166        /// Module path of the error domain.
167        domain: String,
168    },
169    /// Two error codes share a name.
170    ///
171    /// Code names are global: backends with flat namespaces (Python, Node,
172    /// Go) derive one error class or constant per code, so `NotFound` in two
173    /// domains would collide in generated code.
174    #[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        /// The colliding code name.
184        name: String,
185        /// Domain of the first declaration, as `module.Domain`.
186        first: String,
187        /// Domain of the second declaration, as `module.Domain`.
188        second: String,
189    },
190    /// Two declarations lower to the same C identifier.
191    ///
192    /// Every function, type, constant, and runtime name in the C ABI starts
193    /// with the library prefix and is flattened from its module path, so
194    /// `m.x_y`, `m.x.y`, and `m_x.y` all become `{prefix}_m_x_y`, and an
195    /// async `foo`'s completion type `{prefix}_m_foo_callback` collides with
196    /// a function named `foo_callback`.
197    #[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        /// The colliding C identifier.
207        symbol: String,
208        /// The declaration that claimed the identifier first.
209        first: String,
210        /// The declaration that collided with it.
211        second: String,
212    },
213    /// Two parameters of one function share a name.
214    #[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 that contains the function.
223        module: String,
224        /// Function that contains the colliding parameters.
225        function: String,
226        /// Duplicated parameter name.
227        param: String,
228    },
229    /// A synchronous callable declares `cancellable: true`.
230    #[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 that contains the function.
240        module: String,
241        /// The offending function.
242        function: String,
243    },
244    /// A function or callback-interface method declares `throws: true` but
245    /// no error domain is in scope.
246    #[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 that contains the function.
257        module: String,
258        /// Function marked `throws` with no domain in scope.
259        function: String,
260    },
261    /// An async function tries to return an iterator, which has no async ABI.
262    #[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 that contains the function.
272        module: String,
273        /// Async function with the iterator return.
274        function: String,
275    },
276    /// An error domain in the named module is missing its `name` field.
277    #[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 that declares the error domain.
284        module: String,
285    },
286    /// Two error codes in one domain share a numeric value.
287    #[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 that declares the error domain.
296        module: String,
297        /// Conflicting numeric error code.
298        value: i32,
299    },
300    /// An error code uses a reserved value: `0` means success and the
301    /// negative range belongs to the runtime's trap codes.
302    #[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 that declares the error domain.
312        module: String,
313        /// Error code name with the invalid value.
314        name: String,
315    },
316    /// A struct declares no fields.
317    #[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 that contains the struct.
326        module: String,
327        /// Name of the empty struct.
328        name: String,
329    },
330    /// Two fields of one struct share a name.
331    #[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 that contains the colliding fields.
338        struct_name: String,
339        /// Duplicated field name.
340        field: String,
341    },
342    /// An enum declares no variants.
343    #[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 that contains the enum.
352        module: String,
353        /// Name of the empty enum.
354        name: String,
355    },
356    /// Two variants of one enum share a name.
357    #[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 that contains the colliding variants.
364        enum_name: String,
365        /// Duplicated variant name.
366        variant: String,
367    },
368    /// Two associated fields of one rich enum variant share a name.
369    #[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 that contains the variant.
378        enum_name: String,
379        /// Variant that contains the colliding fields.
380        variant: String,
381        /// Duplicated associated field name.
382        field: String,
383    },
384    /// Two variants of one enum share a numeric discriminant.
385    #[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 that contains the variants.
394        enum_name: String,
395        /// Conflicting numeric discriminant.
396        value: i32,
397    },
398    /// An interface declares no members at all.
399    #[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 that contains the interface.
409        module: String,
410        /// Name of the empty interface.
411        name: String,
412    },
413    /// Two members (constructors, methods, or statics) of one interface share
414    /// a name.
415    #[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 that contains the colliding members.
425        interface: String,
426        /// Duplicated member name.
427        name: String,
428    },
429    /// An interface constructor declares an explicit return type.
430    #[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 that declares the constructor.
440        interface: String,
441        /// The offending constructor.
442        constructor: String,
443    },
444    /// An interface constructor is marked `async`.
445    #[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 that declares the constructor.
455        interface: String,
456        /// The offending constructor.
457        constructor: String,
458    },
459    /// A callback interface declares no methods.
460    #[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 that contains the callback interface.
470        module: String,
471        /// Name of the empty callback interface.
472        name: String,
473    },
474    /// Two methods of one callback interface share a name.
475    #[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        /// Callback interface that contains the colliding methods.
484        interface: String,
485        /// Duplicated method name.
486        name: String,
487    },
488    /// A callback-interface method uses a flag or return type the callback
489    /// ABI can't carry. The `reason` names the offending feature.
490    #[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        /// Callback interface that declares the method.
501        interface: String,
502        /// The offending method.
503        method: String,
504        /// Why the method was rejected.
505        reason: &'static str,
506    },
507    /// A type reference names a struct, enum, interface, or callback interface
508    /// that doesn't exist.
509    #[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        /// Unresolved type name.
519        name: String,
520    },
521    /// A type reference is module-qualified (`a.b.T`).
522    #[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        /// The dotted name as written.
532        name: String,
533    },
534    /// A type reference names a primitive WeaveFFI doesn't support.
535    #[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        /// The primitive's name as written.
545        name: String,
546    },
547    /// A map uses a key type the C ABI can't represent.
548    #[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        /// Rejected key type, rendered as it appears in the IDL.
552        key_type: String,
553    },
554    /// An interface reference appears as a map key.
555    #[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        /// The referenced interface name.
565        name: String,
566        /// Position where the interface reference appeared.
567        location: String,
568    },
569    /// A callback-interface type appears in a position other than a callable
570    /// parameter.
571    #[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        /// The referenced callback interface name.
583        name: String,
584        /// Position where the reference appeared.
585        location: String,
586    },
587    /// An iterator type appears somewhere other than a function return.
588    #[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        /// Position where the iterator type appeared.
592        location: String,
593    },
594}
595
596/// Every validation failure found in one pass, each wrapped as a
597/// [`ValidationDiagnostic`] carrying an optional source span.
598///
599/// `Display` renders every message on its own line; miette renderers reach the
600/// individual diagnostics through [`Diagnostic::related`].
601#[derive(Debug)]
602pub struct ValidationDiagnostics {
603    /// The individual failures, in the order they were found. Never empty.
604    pub diagnostics: Vec<ValidationDiagnostic>,
605}
606
607impl ValidationDiagnostics {
608    /// The first failure, which every report is guaranteed to contain.
609    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
656/// Validate an [`Api`] document against the library's `identity`, reporting
657/// **every** rule violation found, and build its [`Model`].
658///
659/// This is the one checked way to obtain the [`Model`] every generator
660/// consumes, and it builds the model exactly once. The C symbol table is
661/// checked with the real `identity`, in the same pass. The optional `source`
662/// is `(filename, contents)` of the IDL file and is used to attach spans to
663/// the returned diagnostics, each located within its enclosing declaration.
664/// Pass `None` when the API is constructed in memory and there is no on-disk
665/// source.
666///
667/// # Errors
668///
669/// Returns [`ValidationDiagnostics`] carrying one [`ValidationDiagnostic`]
670/// per violation: an unsupported schema version, a duplicate or invalid name,
671/// an unknown, qualified, or misplaced type, an empty struct or enum, a
672/// `throws` without an error domain, a C symbol collision, or any other rule
673/// violation in the catalog above.
674pub 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/// Options for [`validate_scoped`].
688#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
689pub struct Options {
690    /// Accept a type name that no declaration in the document provides,
691    /// assuming it's a record or rich enum declared elsewhere, and resolve
692    /// it to [`Ty::Record`](crate::ty::Ty::Record). The `#[weaveffi::module]`
693    /// macro sets this: it validates one module tree at a time, and asserts
694    /// at compile time that each such name really crosses as a value buffer.
695    pub foreign_names: bool,
696}
697
698/// A rule violation paired with the declaration path that encloses it
699/// (module segments, then the declaration, member, and parameter or field
700/// names). [`validate`] uses the path to locate the offending IDL text; the
701/// `#[weaveffi::module]` macro uses it to find the offending Rust item.
702pub type Found = (ValidationError, Vec<String>);
703
704/// Validate `api` against `identity` exactly like [`validate`], with
705/// `options`, returning each violation with the declaration path that
706/// encloses it instead of a located diagnostic.
707///
708/// # Errors
709///
710/// Returns every rule violation found, each paired with its declaration
711/// path.
712pub 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        // A wrong-schema document is checked no further: the rules below
719        // assume the current schema's shape.
720        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        // The model's lowering assumes every rule holds, so the symbol
733        // table is only built for an otherwise valid document.
734        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
745/// Reject any C identifier the model claims twice, or that falls in a
746/// family the generated C value-buffer helpers reserve.
747fn 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}