weaveffi-model 0.24.0

IR, parsers, Rust extractor, validation, and the resolved binding model for WeaveFFI API definitions
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
//! IDL validation. This module owns the [`ValidationError`] catalog and the
//! [`validate_api`] entry point; the work is split across submodules:
//! `rules` (per-module checks), `diagnostic` (miette span attachment), and
//! `warnings` (advisory lints).
//!
//! Validation collects *every* rule violation before failing, so a document
//! with several problems reports them all in one run rather than one per
//! invocation.

use crate::ir::{Api, SUPPORTED_VERSIONS};
#[cfg(feature = "idl")]
use miette::Diagnostic;
use std::collections::BTreeSet;

use crate::resolved::ResolvedApi;

mod diagnostic;
mod rules;
#[cfg(all(test, feature = "idl"))]
mod tests;
mod warnings;

pub use diagnostic::ValidationDiagnostic;
pub use warnings::{collect_warnings, ValidationWarning};

/// Every way an [`Api`] can fail validation.
///
/// `validate_api` collects every variant it encounters. Each variant carries
/// the names needed to render an actionable diagnostic, and the
/// `#[error]`/`#[diagnostic]` attributes supply the message and help text
/// shown to the user.
#[derive(Debug, thiserror::Error)]
#[cfg_attr(feature = "idl", derive(Diagnostic))]
pub enum ValidationError {
    /// A module is missing its required `name` field.
    #[error("module has no name")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help("every module must have a non-empty 'name' field"))
    )]
    NoModuleName,
    /// Two modules share the same name.
    #[error("duplicate module name: {0}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "module names must be unique within an API definition; rename or merge the duplicate"
        ))
    )]
    DuplicateModuleName(String),
    /// A module name is not a valid identifier; the second field explains why.
    #[error("invalid module name '{0}': {1}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word"
        ))
    )]
    InvalidModuleName(String, &'static str),
    /// Two functions in the same module share a name.
    #[error("duplicate function name in module '{module}': {function}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help("function names must be unique within a module; rename the duplicate"))
    )]
    DuplicateFunctionName {
        /// Module that contains the colliding functions.
        module: String,
        /// Duplicated function name.
        function: String,
    },
    /// Two parameters of one function share a name.
    #[error("duplicate param name in function '{function}' of module '{module}': {param}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "parameter names must be unique within a function; rename the duplicate"
        ))
    )]
    DuplicateParamName {
        /// Module that contains the function.
        module: String,
        /// Function that contains the colliding parameters.
        function: String,
        /// Duplicated parameter name.
        param: String,
    },
    /// A name matches a reserved keyword in one of the target languages.
    #[error("reserved keyword used: {0}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help("choose a different name that is not a language reserved word"))
    )]
    ReservedKeyword(String),
    /// An identifier is malformed; the second field explains why.
    #[error("invalid identifier '{0}': {1}")]
    #[cfg_attr(feature = "idl", diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters")))]
    InvalidIdentifier(String, &'static str),
    /// An error domain in the named module is missing its `name` field.
    #[error("error domain missing name in module '{0}'")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help("add a non-empty 'name' field to the error domain"))
    )]
    ErrorDomainMissingName(String),
    /// Two error codes in the same module share a name.
    #[error("duplicate error code name in module '{module}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help("error code names must be unique within a module; rename the duplicate"))
    )]
    DuplicateErrorName {
        /// Module that contains the error domain.
        module: String,
        /// Duplicated error code name.
        name: String,
    },
    /// Two error codes in the same module share a numeric value.
    #[error("duplicate error numeric code in module '{module}': {code}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "numeric error codes must be unique within a module; assign a different value"
        ))
    )]
    DuplicateErrorCode {
        /// Module that contains the error domain.
        module: String,
        /// Conflicting numeric error code.
        code: i32,
    },
    /// An error code uses a reserved value: `0` means success and the
    /// negative range belongs to the runtime's trap codes.
    #[error("invalid error code in module '{module}' for '{name}': must be a positive integer")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "0 means success and negative codes are reserved for the runtime (-1 generic, \
         -2 panic, -3 marshalling failure, -4 foreign callback error, -5 cancelled); use a positive integer"
        ))
    )]
    InvalidErrorCode {
        /// Module that contains the error domain.
        module: String,
        /// Error code name with the invalid value.
        name: String,
    },
    /// A function name collides with an error domain name in the same module.
    #[error("function name collides with error domain name in module '{module}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "function and error domain names share a namespace; rename one to avoid the collision"
        ))
    )]
    NameCollisionWithErrorDomain {
        /// Module where the collision occurs.
        module: String,
        /// Name shared by the function and the error domain.
        name: String,
    },
    /// Two declarations lower to the same C identifier.
    ///
    /// Every function, type, constant, and runtime name in the C ABI starts
    /// with the library prefix and is flattened from its module path, so
    /// `m.x_y`, `m.x.y`, and `m_x.y` all become `{prefix}_m_x_y`, and an
    /// async `foo`'s completion type `{prefix}_m_foo_callback` collides with
    /// a function named `foo_callback`.
    #[error("C symbol collision: {first} and {second} both lower to '{symbol}'")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "every C identifier is the library prefix plus the underscore-joined module path \
         plus the declaration name; rename one of the colliding declarations"
        ))
    )]
    SymbolCollision {
        /// The colliding C identifier.
        symbol: String,
        /// The declaration that claimed the identifier first.
        first: String,
        /// The declaration that collided with it.
        second: String,
    },
    /// A synchronous callable declares `cancellable: true`.
    #[error("function '{module}::{function}' is cancellable but not async")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "only async functions take a cancel token; add `async: true` or remove \
         `cancellable: true`"
        ))
    )]
    CancellableNotAsync {
        /// Module that contains the function.
        module: String,
        /// The offending function.
        function: String,
    },
    /// A function declares `throws: true` but no error domain is in scope.
    #[error("function '{module}::{function}' declares throws but no error domain is in scope")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "a throwing function reports codes from its module's error domain; declare an \
         `errors:` block on this module (or an ancestor module), or remove `throws: true`"
        ))
    )]
    ThrowsWithoutErrorDomain {
        /// Module that contains the function.
        module: String,
        /// Function marked `throws` with no domain in scope.
        function: String,
    },
    /// Two types (structs, enums, interfaces, or callback interfaces) share a
    /// bare name.
    ///
    /// Type names must be unique across the whole API: generators emit flat
    /// per-language type names, and unqualified cross-module references
    /// resolve by bare name, so two types called `Config` would collide in
    /// generated code and make references ambiguous.
    #[error("duplicate type name '{name}' (declared in '{first}' and '{second}')")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "struct, enum, interface, callback interface, and error domain names must be unique \
         across the whole API; rename one of the declarations"
        ))
    )]
    DuplicateTypeName {
        /// The colliding bare type name.
        name: String,
        /// Module path of the first declaration.
        first: String,
        /// Module path of the second declaration.
        second: String,
    },
    /// Two error domains declare a code with the same name.
    ///
    /// Code names must be unique across every domain in the API: backends
    /// with flat namespaces (Python, Node, Go) derive one error class or
    /// constant per code, so `NotFound` in two domains would collide in
    /// generated code.
    #[error("duplicate error code name '{name}' (declared in '{first}' and '{second}')")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "error code names must be unique across the whole API; qualify one of them \
         (e.g. 'OrderNotFound')"
        ))
    )]
    DuplicateErrorCodeName {
        /// The colliding code name.
        name: String,
        /// Domain of the first declaration, as `module.Domain`.
        first: String,
        /// Domain of the second declaration, as `module.Domain`.
        second: String,
    },
    /// Two structs in the same module share a name.
    #[error("duplicate struct name in module '{module}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help("struct names must be unique within a module; rename the duplicate"))
    )]
    DuplicateStructName {
        /// Module that contains the structs.
        module: String,
        /// Duplicated struct name.
        name: String,
    },
    /// Two fields of one struct share a name.
    #[error("duplicate field name in struct '{struct_name}': {field}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help("field names must be unique within a struct; rename the duplicate"))
    )]
    DuplicateStructField {
        /// Struct that contains the colliding fields.
        struct_name: String,
        /// Duplicated field name.
        field: String,
    },
    /// A struct declares no fields.
    #[error("empty struct in module '{module}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "structs must have at least one field; add a field or remove the struct"
        ))
    )]
    EmptyStruct {
        /// Module that contains the struct.
        module: String,
        /// Name of the empty struct.
        name: String,
    },
    /// Two enums in the same module share a name.
    #[error("duplicate enum name in module '{module}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help("enum names must be unique within a module; rename the duplicate"))
    )]
    DuplicateEnumName {
        /// Module that contains the enums.
        module: String,
        /// Duplicated enum name.
        name: String,
    },
    /// An enum declares no variants.
    #[error("empty enum in module '{module}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "enums must have at least one variant; add a variant or remove the enum"
        ))
    )]
    EmptyEnum {
        /// Module that contains the enum.
        module: String,
        /// Name of the empty enum.
        name: String,
    },
    /// Two variants of one enum share a name.
    #[error("duplicate enum variant in enum '{enum_name}': {variant}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help("variant names must be unique within an enum; rename the duplicate"))
    )]
    DuplicateEnumVariant {
        /// Enum that contains the colliding variants.
        enum_name: String,
        /// Duplicated variant name.
        variant: String,
    },
    /// Two associated fields of one rich enum variant share a name.
    #[error("duplicate field '{field}' in variant '{variant}' of enum '{enum_name}'")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "associated field names must be unique within an enum variant; rename the duplicate"
        ))
    )]
    DuplicateEnumVariantField {
        /// Enum that contains the variant.
        enum_name: String,
        /// Variant that contains the colliding fields.
        variant: String,
        /// Duplicated associated field name.
        field: String,
    },
    /// Two variants of one enum share a numeric discriminant.
    #[error("duplicate enum value in enum '{enum_name}': {value}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "variant numeric values must be unique within an enum; assign a different value"
        ))
    )]
    DuplicateEnumValue {
        /// Enum that contains the variants.
        enum_name: String,
        /// Conflicting numeric discriminant.
        value: i32,
    },
    /// Two interfaces in the same module share a name.
    #[error("duplicate interface name in module '{module}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help("interface names must be unique within a module; rename the duplicate"))
    )]
    DuplicateInterfaceName {
        /// Module that contains the interfaces.
        module: String,
        /// Duplicated interface name.
        name: String,
    },
    /// Two members (constructors, methods, or statics) of one interface share
    /// a name.
    #[error("duplicate member name in interface '{interface}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "constructor, method, and static names share one namespace per interface; \
         rename the duplicate"
        ))
    )]
    DuplicateInterfaceMember {
        /// Interface that contains the colliding members.
        interface: String,
        /// Duplicated member name.
        name: String,
    },
    /// An interface declares no members at all.
    #[error("empty interface in module '{module}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "interfaces must declare at least one constructor, method, or static; \
         add a member or remove the interface"
        ))
    )]
    EmptyInterface {
        /// Module that contains the interface.
        module: String,
        /// Name of the empty interface.
        name: String,
    },
    /// An interface constructor declares an explicit return type.
    #[error("constructor '{constructor}' of interface '{interface}' declares a return type")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "a constructor implicitly returns a new instance of its interface; remove the \
         `return` field"
        ))
    )]
    ConstructorHasReturn {
        /// Interface that declares the constructor.
        interface: String,
        /// The offending constructor.
        constructor: String,
    },
    /// An interface constructor is marked `async`.
    #[error("constructor '{constructor}' of interface '{interface}' cannot be async")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "constructors are synchronous; expose an async static factory returning the \
         interface instead"
        ))
    )]
    AsyncConstructor {
        /// Interface that declares the constructor.
        interface: String,
        /// The offending constructor.
        constructor: String,
    },
    /// An interface reference appears as a map key.
    #[error("interface type '{name}' is not valid in {location}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "interface objects may appear anywhere except as map keys; key a map by a scalar, \
         string, or C-style enum instead"
        ))
    )]
    InterfaceInInvalidPosition {
        /// The referenced interface name.
        name: String,
        /// Position where the interface reference appeared.
        location: String,
    },
    /// A type reference names a struct, enum, interface, or callback interface
    /// that doesn't exist.
    #[error("unknown type reference: {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "define a struct, enum, interface, or callback interface with this name, or check \
         for typos"
        ))
    )]
    UnknownTypeRef {
        /// Unresolved type name.
        name: String,
    },
    /// A map uses a key type the C ABI can't represent.
    #[error("invalid map key type: {key_type}; only integers, bools, strings, and C-style enums are allowed as map keys")]
    #[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")))]
    InvalidMapKey {
        /// Rejected key type, rendered as it appears in the IDL.
        key_type: String,
    },
    /// An iterator type appears somewhere other than a function return.
    #[error("iterator type is only valid as a function return type, found in {location}")]
    #[cfg_attr(feature = "idl", diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields")))]
    IteratorInInvalidPosition {
        /// Position where the iterator type appeared.
        location: String,
    },
    /// Two callback interfaces in the same module share a name.
    #[error("duplicate callback interface name in module '{module}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "callback interface names must be unique within a module; rename the duplicate"
        ))
    )]
    DuplicateCallbackInterfaceName {
        /// Module that contains the callback interfaces.
        module: String,
        /// Duplicated callback interface name.
        name: String,
    },
    /// A callback interface declares no methods.
    #[error("empty callback interface in module '{module}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "callback interfaces must declare at least one method; add a method or remove the \
         callback interface"
        ))
    )]
    EmptyCallbackInterface {
        /// Module that contains the callback interface.
        module: String,
        /// Name of the empty callback interface.
        name: String,
    },
    /// Two methods of one callback interface share a name.
    #[error("duplicate method name in callback interface '{interface}': {name}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "method names must be unique within a callback interface; rename the duplicate"
        ))
    )]
    DuplicateCallbackMethod {
        /// Callback interface that contains the colliding methods.
        interface: String,
        /// Duplicated method name.
        name: String,
    },
    /// A callback-interface method uses a flag or return type the callback
    /// ABI can't carry. The `reason` names the offending feature.
    #[error("method '{method}' of callback interface '{interface}' {reason}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "callback-interface methods are synchronous, never throw, and return either nothing \
         or a direct value (a scalar, bool, or C-style enum): the consumer implements them, \
         so no producer-owned allocation or error payload can flow back"
        ))
    )]
    InvalidCallbackMethod {
        /// Callback interface that declares the method.
        interface: String,
        /// The offending method.
        method: String,
        /// Why the method was rejected.
        reason: &'static str,
    },
    /// A callback-interface type appears in a position other than a callable
    /// parameter.
    #[error("callback interface '{name}' is not valid in {location}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "a callback interface is a consumer-implemented vtable: it may be passed as a \
         parameter of a function, constructor, static, or method, but it can't be returned, \
         nested inside another type, or passed to another callback-interface method"
        ))
    )]
    CallbackInterfaceInInvalidPosition {
        /// The referenced callback interface name.
        name: String,
        /// Position where the reference appeared.
        location: String,
    },
    /// An async function tries to return an iterator, which has no async ABI.
    #[error("async function '{module}::{function}' cannot return an iterator")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "the callback-completed async ABI has no streaming protocol; return a list ([T]) \
         from the async function, or make the function synchronous and return iter<T>"
        ))
    )]
    AsyncIteratorReturn {
        /// Module that contains the function.
        module: String,
        /// Async function with the iterator return.
        function: String,
    },
    /// The document declares a schema version this build doesn't support.
    #[error("unsupported schema version '{version}'; supported versions: {supported}")]
    #[cfg_attr(
        feature = "idl",
        diagnostic(help(
            "set the version field to the current schema version and update the \
         document to match the current schema (see docs/src/reference/idl.md)"
        ))
    )]
    UnsupportedSchemaVersion {
        /// Version requested by the document.
        version: String,
        /// Comma-separated list of versions this build accepts.
        supported: String,
    },
}

/// Every validation failure found in one pass, each wrapped as a
/// [`ValidationDiagnostic`] carrying an optional source span.
///
/// `Display` renders every message on its own line; miette renderers reach the
/// individual diagnostics through [`Diagnostic::related`].
#[derive(Debug)]
pub struct ValidationDiagnostics {
    /// The individual failures, in the order they were found. Never empty.
    pub diagnostics: Vec<ValidationDiagnostic>,
}

impl ValidationDiagnostics {
    /// The first failure, which every report is guaranteed to contain.
    pub fn first(&self) -> &ValidationDiagnostic {
        &self.diagnostics[0]
    }
}

impl std::fmt::Display for ValidationDiagnostics {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for (i, d) in self.diagnostics.iter().enumerate() {
            if i > 0 {
                writeln!(f)?;
            }
            write!(f, "{d}")?;
        }
        Ok(())
    }
}

impl std::error::Error for ValidationDiagnostics {}

#[cfg(feature = "idl")]
impl Diagnostic for ValidationDiagnostics {
    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
        self.first().code()
    }

    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
        self.first().help()
    }

    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
        self.first().source_code()
    }

    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
        self.first().labels()
    }

    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
        if self.diagnostics.len() <= 1 {
            return None;
        }
        Some(Box::new(
            self.diagnostics[1..].iter().map(|d| d as &dyn Diagnostic),
        ))
    }
}

/// Validate an [`Api`] document, reporting **every** rule violation found.
/// The optional `source` is `(filename, contents)` of the IDL file and is
/// used to attach spans to the returned diagnostics. Pass `None` when the
/// API is constructed in memory (tests, programmatic builds) and there is no
/// on-disk source.
///
/// This is the one checked way to turn a parsed document into the
/// [`ResolvedApi`] every generator consumes. The document is never rewritten;
/// the resolved view indexes its declarations so that
/// [`ResolvedApi::resolve`] can turn each written reference into a
/// [`Ty`](crate::model::Ty) whose kind and owning module are known.
///
/// # Errors
///
/// Returns [`ValidationDiagnostics`] carrying one [`ValidationDiagnostic`]
/// per violation: an unsupported schema version, a duplicate or invalid name,
/// an unknown or misplaced type, an empty struct or enum, a `throws` without
/// an error domain, or any other rule violation in the catalog above.
pub fn validate_api(
    api: Api,
    source: Option<(&str, &str)>,
) -> Result<ResolvedApi, ValidationDiagnostics> {
    let errors = validate_api_inner(&api);
    if errors.is_empty() {
        return Ok(ResolvedApi::assume_valid(api));
    }
    Err(ValidationDiagnostics {
        diagnostics: errors
            .into_iter()
            .map(|e| ValidationDiagnostic::new(e, source))
            .collect(),
    })
}

fn validate_api_inner(api: &Api) -> Vec<ValidationError> {
    let mut errors = Vec::new();
    if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) {
        // A wrong-schema document is checked no further: the rules below
        // assume the current schema's shape.
        return vec![ValidationError::UnsupportedSchemaVersion {
            version: api.version.clone(),
            supported: SUPPORTED_VERSIONS.join(", "),
        }];
    }
    let types = rules::TypeIndex::build(&api.modules);
    let mut module_names = BTreeSet::new();
    for m in &api.modules {
        if !module_names.insert(m.name.clone()) {
            errors.push(ValidationError::DuplicateModuleName(m.name.clone()));
        }
        rules::validate_module(m, &types, false, &mut errors);
    }
    rules::check_global_type_names(&api.modules, &mut errors);
    rules::check_global_error_code_names(&api.modules, &mut errors);
    if errors.is_empty() {
        check_symbol_collisions(api, &mut errors);
    }
    errors
}

/// Build the binding model (any prefix works: collisions are invariant under
/// a shared prefix) and reject any C identifier claimed twice.
fn check_symbol_collisions(api: &Api, errors: &mut Vec<ValidationError>) {
    let resolved = ResolvedApi::assume_valid(api.clone());
    let model = crate::model::BindingModel::build(&resolved);
    let mut seen: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
    let runtime = crate::model::RUNTIME_SYMBOLS.len();
    for (i, (symbol, origin)) in model.c_symbols().into_iter().enumerate() {
        let family = symbol
            .strip_prefix(&format!("{}_", model.prefix))
            .and_then(|rest| {
                crate::model::RESERVED_SYMBOL_FAMILIES
                    .iter()
                    .find(|f| rest.starts_with(*f))
            });
        if let (Some(family), true) = (family, i >= runtime) {
            errors.push(ValidationError::SymbolCollision {
                symbol: symbol.clone(),
                first: format!("the C value-buffer helpers ('{}_{family}*')", model.prefix),
                second: origin.clone(),
            });
        }
        match seen.get(&symbol) {
            Some(first) => errors.push(ValidationError::SymbolCollision {
                symbol,
                first: first.clone(),
                second: origin,
            }),
            None => {
                seen.insert(symbol, origin);
            }
        }
    }
}