Skip to main content

weaveffi_core/
validate.rs

1use miette::{Diagnostic, NamedSource, SourceSpan};
2use std::collections::{BTreeMap, BTreeSet};
3use weaveffi_ir::ir::{Api, ErrorDomain, Function, Module, Param, TypeRef, SUPPORTED_VERSIONS};
4
5mod warnings;
6pub use warnings::{collect_warnings, ValidationWarning};
7
8#[derive(Debug, thiserror::Error, Diagnostic)]
9pub enum ValidationError {
10    #[error("module has no name")]
11    #[diagnostic(help("every module must have a non-empty 'name' field"))]
12    NoModuleName,
13    #[error("duplicate module name: {0}")]
14    #[diagnostic(help(
15        "module names must be unique within an API definition; rename or merge the duplicate"
16    ))]
17    DuplicateModuleName(String),
18    #[error("invalid module name '{0}': {1}")]
19    #[diagnostic(help(
20        "choose a valid identifier (a-z, A-Z, 0-9, _) that is not a reserved word"
21    ))]
22    InvalidModuleName(String, &'static str),
23    #[error("duplicate function name in module '{module}': {function}")]
24    #[diagnostic(help("function names must be unique within a module; rename the duplicate"))]
25    DuplicateFunctionName { module: String, function: String },
26    #[error("duplicate param name in function '{function}' of module '{module}': {param}")]
27    #[diagnostic(help("parameter names must be unique within a function; rename the duplicate"))]
28    DuplicateParamName {
29        module: String,
30        function: String,
31        param: String,
32    },
33    #[error("reserved keyword used: {0}")]
34    #[diagnostic(help("choose a different name that is not a language reserved word"))]
35    ReservedKeyword(String),
36    #[error("invalid identifier '{0}': {1}")]
37    #[diagnostic(help("identifiers must start with a letter or underscore and contain only alphanumeric or underscore characters"))]
38    InvalidIdentifier(String, &'static str),
39    #[error("error domain missing name in module '{0}'")]
40    #[diagnostic(help("add a non-empty 'name' field to the error domain"))]
41    ErrorDomainMissingName(String),
42    #[error("duplicate error code name in module '{module}': {name}")]
43    #[diagnostic(help("error code names must be unique within a module; rename the duplicate"))]
44    DuplicateErrorName { module: String, name: String },
45    #[error("duplicate error numeric code in module '{module}': {code}")]
46    #[diagnostic(help(
47        "numeric error codes must be unique within a module; assign a different value"
48    ))]
49    DuplicateErrorCode { module: String, code: i32 },
50    #[error("invalid error code in module '{module}' for '{name}': must be non-zero")]
51    #[diagnostic(help("error codes must be non-zero; use a positive or negative integer"))]
52    InvalidErrorCode { module: String, name: String },
53    #[error("function name collides with error domain name in module '{module}': {name}")]
54    #[diagnostic(help(
55        "function and error domain names share a namespace; rename one to avoid the collision"
56    ))]
57    NameCollisionWithErrorDomain { module: String, name: String },
58    #[error("duplicate struct name in module '{module}': {name}")]
59    #[diagnostic(help("struct names must be unique within a module; rename the duplicate"))]
60    DuplicateStructName { module: String, name: String },
61    #[error("duplicate field name in struct '{struct_name}': {field}")]
62    #[diagnostic(help("field names must be unique within a struct; rename the duplicate"))]
63    DuplicateStructField { struct_name: String, field: String },
64    #[error("empty struct in module '{module}': {name}")]
65    #[diagnostic(help("structs must have at least one field; add a field or remove the struct"))]
66    EmptyStruct { module: String, name: String },
67    #[error("duplicate enum name in module '{module}': {name}")]
68    #[diagnostic(help("enum names must be unique within a module; rename the duplicate"))]
69    DuplicateEnumName { module: String, name: String },
70    #[error("empty enum in module '{module}': {name}")]
71    #[diagnostic(help("enums must have at least one variant; add a variant or remove the enum"))]
72    EmptyEnum { module: String, name: String },
73    #[error("duplicate enum variant in enum '{enum_name}': {variant}")]
74    #[diagnostic(help("variant names must be unique within an enum; rename the duplicate"))]
75    DuplicateEnumVariant { enum_name: String, variant: String },
76    #[error("duplicate enum value in enum '{enum_name}': {value}")]
77    #[diagnostic(help(
78        "variant numeric values must be unique within an enum; assign a different value"
79    ))]
80    DuplicateEnumValue { enum_name: String, value: i32 },
81    #[error("unknown type reference: {name}")]
82    #[diagnostic(help(
83        "define a struct or enum with this name in the same module, or check for typos"
84    ))]
85    UnknownTypeRef { name: String },
86    #[error("invalid map key type: {key_type}; only primitive types and strings are allowed as map keys")]
87    #[diagnostic(help("map keys must be primitive types (i32, u32, i64, f64, bool, string); structs, lists, and maps cannot be keys"))]
88    InvalidMapKey { key_type: String },
89    #[error(
90        "borrowed type '{ty}' is not valid in {location}; only function parameters are allowed"
91    )]
92    #[diagnostic(help("borrowed types (&str, &[u8]) can only be used as function parameters, not return types or struct fields"))]
93    BorrowedTypeInInvalidPosition { ty: String, location: String },
94    #[error("duplicate callback name in module '{module}': {name}")]
95    #[diagnostic(help("callback names must be unique within a module; rename the duplicate"))]
96    DuplicateCallbackName { module: String, name: String },
97    #[error(
98        "listener '{listener}' in module '{module}' references undefined callback '{callback}'"
99    )]
100    #[diagnostic(help(
101        "listener event_callback must reference a callback defined in the same module"
102    ))]
103    ListenerCallbackNotFound {
104        module: String,
105        listener: String,
106        callback: String,
107    },
108    #[error("duplicate listener name in module '{module}': {name}")]
109    #[diagnostic(help("listener names must be unique within a module; rename the duplicate"))]
110    DuplicateListenerName { module: String, name: String },
111    #[error("iterator type is only valid as a function return type, found in {location}")]
112    #[diagnostic(help("iterator types can only be used as function return types, not as parameters or struct fields"))]
113    IteratorInInvalidPosition { location: String },
114    #[error("builder struct '{name}' in module '{module}' must have at least one field")]
115    #[diagnostic(help(
116        "builder structs must have at least one field; add a field or set builder: false"
117    ))]
118    BuilderStructEmpty { module: String, name: String },
119    #[error("unsupported schema version '{version}'; supported versions: {supported}")]
120    #[diagnostic(help("run 'weaveffi upgrade <file>' to migrate to the current schema version"))]
121    UnsupportedSchemaVersion { version: String, supported: String },
122}
123
124/// Diagnostic wrapper that attaches an optional source code snippet and a
125/// best-effort byte range to a [`ValidationError`] for fancy rendering via
126/// [`miette`]. The wrapper delegates `help()` and `code()` to the inner error
127/// while exposing its own `source_code` and `labels` so the renderer can
128/// underline the offending identifier in the input.
129#[derive(Debug)]
130pub struct ValidationDiagnostic {
131    pub error: ValidationError,
132    pub src: Option<NamedSource<String>>,
133    pub span: Option<SourceSpan>,
134}
135
136impl std::fmt::Display for ValidationDiagnostic {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        std::fmt::Display::fmt(&self.error, f)
139    }
140}
141
142impl std::error::Error for ValidationDiagnostic {
143    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
144        self.error.source()
145    }
146}
147
148impl Diagnostic for ValidationDiagnostic {
149    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
150        self.error.code()
151    }
152
153    fn severity(&self) -> Option<miette::Severity> {
154        self.error.severity()
155    }
156
157    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
158        self.error.help()
159    }
160
161    fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
162        self.error.url()
163    }
164
165    fn source_code(&self) -> Option<&dyn miette::SourceCode> {
166        self.src
167            .as_ref()
168            .map(|s| s as &dyn miette::SourceCode)
169            .or_else(|| self.error.source_code())
170    }
171
172    fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
173        if let Some(span) = self.span {
174            Some(Box::new(std::iter::once(
175                miette::LabeledSpan::new_with_span(Some("here".to_string()), span),
176            )))
177        } else {
178            self.error.labels()
179        }
180    }
181}
182
183impl ValidationDiagnostic {
184    /// Build a [`ValidationDiagnostic`] from a [`ValidationError`] and an
185    /// optional `(filename, contents)` source. When a source is provided the
186    /// constructor performs a best-effort search for the offending identifier
187    /// (e.g. a duplicate module name or unknown type reference) and attaches
188    /// a [`SourceSpan`] for fancy rendering. If no span can be computed the
189    /// label is omitted and miette still produces a nicer message + help
190    /// section than plain `Display`.
191    pub fn new(error: ValidationError, source: Option<(&str, &str)>) -> Self {
192        let (src, span) = match source {
193            Some((filename, contents)) => {
194                let span = find_offending_span(&error, contents);
195                (Some(NamedSource::new(filename, contents.to_string())), span)
196            }
197            None => (None, None),
198        };
199        Self { error, src, span }
200    }
201}
202
203fn find_offending_span(err: &ValidationError, src: &str) -> Option<SourceSpan> {
204    let needle: &str = match err {
205        ValidationError::DuplicateModuleName(n) => Some(n.as_str()),
206        ValidationError::InvalidModuleName(n, _) => Some(n.as_str()),
207        ValidationError::DuplicateFunctionName { function, .. } => Some(function.as_str()),
208        ValidationError::DuplicateParamName { param, .. } => Some(param.as_str()),
209        ValidationError::ReservedKeyword(n) => Some(n.as_str()),
210        ValidationError::InvalidIdentifier(n, _) => Some(n.as_str()),
211        ValidationError::DuplicateErrorName { name, .. } => Some(name.as_str()),
212        ValidationError::InvalidErrorCode { name, .. } => Some(name.as_str()),
213        ValidationError::NameCollisionWithErrorDomain { name, .. } => Some(name.as_str()),
214        ValidationError::DuplicateStructName { name, .. } => Some(name.as_str()),
215        ValidationError::DuplicateStructField { field, .. } => Some(field.as_str()),
216        ValidationError::EmptyStruct { name, .. } => Some(name.as_str()),
217        ValidationError::DuplicateEnumName { name, .. } => Some(name.as_str()),
218        ValidationError::EmptyEnum { name, .. } => Some(name.as_str()),
219        ValidationError::DuplicateEnumVariant { variant, .. } => Some(variant.as_str()),
220        ValidationError::UnknownTypeRef { name } => Some(name.as_str()),
221        ValidationError::DuplicateCallbackName { name, .. } => Some(name.as_str()),
222        ValidationError::ListenerCallbackNotFound { callback, .. } => Some(callback.as_str()),
223        ValidationError::DuplicateListenerName { name, .. } => Some(name.as_str()),
224        ValidationError::BuilderStructEmpty { name, .. } => Some(name.as_str()),
225        ValidationError::UnsupportedSchemaVersion { version, .. } => Some(version.as_str()),
226        _ => None,
227    }?;
228    let quoted = format!("\"{needle}\"");
229    if let Some(pos) = src.find(&quoted) {
230        return Some(SourceSpan::new(pos.into(), quoted.len()));
231    }
232    src.find(needle)
233        .map(|pos| SourceSpan::new(pos.into(), needle.len()))
234}
235
236const RESERVED: &[&str] = &[
237    "if", "else", "for", "while", "loop", "match", "type", "return", "async", "await", "break",
238    "continue", "fn", "struct", "enum", "mod", "use",
239];
240
241fn is_valid_identifier(s: &str) -> bool {
242    let mut chars = s.chars();
243    match chars.next() {
244        None => false,
245        Some(c) if !(c.is_ascii_alphabetic() || c == '_') => false,
246        _ => chars.all(|c| c.is_ascii_alphanumeric() || c == '_'),
247    }
248}
249
250fn check_identifier(name: &str) -> Result<(), ValidationError> {
251    if !is_valid_identifier(name) {
252        return Err(ValidationError::InvalidIdentifier(
253            name.to_string(),
254            "must start with a letter or underscore and contain only alphanumeric characters or underscores",
255        ));
256    }
257    if RESERVED.contains(&name) {
258        return Err(ValidationError::ReservedKeyword(name.to_string()));
259    }
260    Ok(())
261}
262
263/// Validate an [`Api`]. The optional `source` is `(filename, contents)` of the
264/// IDL file and is used at the call site to attach a span to a returned error
265/// via [`ValidationDiagnostic::new`]. Pass `None` when the API is constructed
266/// in memory (tests, programmatic builds) and there is no on-disk source.
267#[allow(clippy::result_large_err)]
268pub fn validate_api(
269    api: &mut Api,
270    source: Option<(&str, &str)>,
271) -> Result<(), ValidationDiagnostic> {
272    validate_api_inner(api).map_err(|e| ValidationDiagnostic::new(e, source))
273}
274
275fn validate_api_inner(api: &mut Api) -> Result<(), ValidationError> {
276    if !SUPPORTED_VERSIONS.contains(&api.version.as_str()) {
277        return Err(ValidationError::UnsupportedSchemaVersion {
278            version: api.version.clone(),
279            supported: SUPPORTED_VERSIONS.join(", "),
280        });
281    }
282    let mut module_names = BTreeSet::new();
283    for m in &api.modules {
284        if !module_names.insert(m.name.clone()) {
285            return Err(ValidationError::DuplicateModuleName(m.name.clone()));
286        }
287        validate_module(m, &api.modules)?;
288    }
289    resolve_type_refs(api);
290    Ok(())
291}
292
293pub fn resolve_type_refs(api: &mut Api) {
294    let mut global_types: BTreeMap<String, (String, bool)> = BTreeMap::new();
295    for module in &api.modules {
296        index_module_types(module, "", &mut global_types);
297    }
298
299    for module in &mut api.modules {
300        resolve_module_type_refs(module, "", &global_types);
301    }
302}
303
304/// Recursively index every struct/enum in the module tree by its bare name,
305/// recording the owner's *dot-joined* module path (e.g. `graphics.shapes`) so
306/// cross-module and nested references can be auto-qualified. First definition
307/// wins on a name clash (pre-existing behavior).
308fn index_module_types(
309    module: &Module,
310    parent_path: &str,
311    out: &mut BTreeMap<String, (String, bool)>,
312) {
313    let path = join_module_path(parent_path, &module.name);
314    for s in &module.structs {
315        out.entry(s.name.clone()).or_insert((path.clone(), false));
316    }
317    for e in &module.enums {
318        out.entry(e.name.clone()).or_insert((path.clone(), true));
319    }
320    for child in &module.modules {
321        index_module_types(child, &path, out);
322    }
323}
324
325/// Resolve every type reference inside `module` (and recursively its
326/// submodules), qualifying cross-module references against `global_types`.
327/// `current_module` is tracked as a dot-joined path so multi-level nesting
328/// resolves correctly.
329fn resolve_module_type_refs(
330    module: &mut Module,
331    parent_path: &str,
332    global_types: &BTreeMap<String, (String, bool)>,
333) {
334    let module_path = join_module_path(parent_path, &module.name);
335    let local_enum_names: BTreeSet<String> = module.enums.iter().map(|e| e.name.clone()).collect();
336    let local_struct_names: BTreeSet<String> =
337        module.structs.iter().map(|s| s.name.clone()).collect();
338    for f in &mut module.functions {
339        for p in &mut f.params {
340            resolve_single_type_ref(
341                &mut p.ty,
342                &local_enum_names,
343                &local_struct_names,
344                &module_path,
345                global_types,
346            );
347        }
348        if let Some(ret) = &mut f.returns {
349            resolve_single_type_ref(
350                ret,
351                &local_enum_names,
352                &local_struct_names,
353                &module_path,
354                global_types,
355            );
356        }
357    }
358    for s in &mut module.structs {
359        for field in &mut s.fields {
360            resolve_single_type_ref(
361                &mut field.ty,
362                &local_enum_names,
363                &local_struct_names,
364                &module_path,
365                global_types,
366            );
367        }
368    }
369    for child in &mut module.modules {
370        resolve_module_type_refs(child, &module_path, global_types);
371    }
372}
373
374/// Join a parent module path with a child segment using `.`. A top-level
375/// module (empty parent) is just its own name, preserving the single-segment
376/// behavior that existed before nested resolution.
377fn join_module_path(parent_path: &str, name: &str) -> String {
378    if parent_path.is_empty() {
379        name.to_string()
380    } else {
381        format!("{parent_path}.{name}")
382    }
383}
384
385fn resolve_single_type_ref(
386    ty: &mut TypeRef,
387    local_enum_names: &BTreeSet<String>,
388    local_struct_names: &BTreeSet<String>,
389    current_module: &str,
390    global_types: &BTreeMap<String, (String, bool)>,
391) {
392    match ty {
393        TypeRef::Struct(name) if local_enum_names.contains(name.as_str()) => {
394            let name = std::mem::take(name);
395            *ty = TypeRef::Enum(name);
396        }
397        TypeRef::Struct(name) if !local_struct_names.contains(name.as_str()) => {
398            if let Some((mod_name, is_enum)) = global_types.get(name.as_str()) {
399                if mod_name != current_module {
400                    let qualified = format!("{mod_name}.{name}");
401                    if *is_enum {
402                        *ty = TypeRef::Enum(qualified);
403                    } else {
404                        *name = qualified;
405                    }
406                }
407            }
408        }
409        TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
410            resolve_single_type_ref(
411                inner,
412                local_enum_names,
413                local_struct_names,
414                current_module,
415                global_types,
416            );
417        }
418        TypeRef::Map(k, v) => {
419            resolve_single_type_ref(
420                k,
421                local_enum_names,
422                local_struct_names,
423                current_module,
424                global_types,
425            );
426            resolve_single_type_ref(
427                v,
428                local_enum_names,
429                local_struct_names,
430                current_module,
431                global_types,
432            );
433        }
434        _ => {}
435    }
436}
437
438pub fn find_type_in_api(api: &Api, name: &str) -> Option<(String, bool)> {
439    fn search(module: &Module, parent_path: &str, name: &str) -> Option<(String, bool)> {
440        let path = join_module_path(parent_path, &module.name);
441        if module.structs.iter().any(|s| s.name == name) {
442            return Some((path, false));
443        }
444        if module.enums.iter().any(|e| e.name == name) {
445            return Some((path, true));
446        }
447        module
448            .modules
449            .iter()
450            .find_map(|child| search(child, &path, name))
451    }
452    api.modules
453        .iter()
454        .find_map(|module| search(module, "", name))
455}
456
457fn validate_module(module: &Module, all_modules: &[Module]) -> Result<(), ValidationError> {
458    if module.name.trim().is_empty() {
459        return Err(ValidationError::NoModuleName);
460    }
461    check_identifier(&module.name).map_err(|e| match e {
462        ValidationError::ReservedKeyword(_) => {
463            ValidationError::InvalidModuleName(module.name.clone(), "reserved word")
464        }
465        ValidationError::InvalidIdentifier(_, reason) => {
466            ValidationError::InvalidModuleName(module.name.clone(), reason)
467        }
468        other => other,
469    })?;
470
471    let mut function_names = BTreeSet::new();
472    for f in &module.functions {
473        if !function_names.insert(f.name.clone()) {
474            return Err(ValidationError::DuplicateFunctionName {
475                module: module.name.clone(),
476                function: f.name.clone(),
477            });
478        }
479        validate_function(module, f)?;
480    }
481
482    let mut struct_names = BTreeSet::new();
483    for s in &module.structs {
484        check_identifier(&s.name)?;
485        if !struct_names.insert(s.name.clone()) {
486            return Err(ValidationError::DuplicateStructName {
487                module: module.name.clone(),
488                name: s.name.clone(),
489            });
490        }
491        if s.fields.is_empty() {
492            if s.builder {
493                return Err(ValidationError::BuilderStructEmpty {
494                    module: module.name.clone(),
495                    name: s.name.clone(),
496                });
497            }
498            return Err(ValidationError::EmptyStruct {
499                module: module.name.clone(),
500                name: s.name.clone(),
501            });
502        }
503        let mut field_names = BTreeSet::new();
504        for f in &s.fields {
505            check_identifier(&f.name)?;
506            if !field_names.insert(f.name.clone()) {
507                return Err(ValidationError::DuplicateStructField {
508                    struct_name: s.name.clone(),
509                    field: f.name.clone(),
510                });
511            }
512        }
513    }
514
515    let mut enum_names = BTreeSet::new();
516    for e in &module.enums {
517        check_identifier(&e.name)?;
518        if !enum_names.insert(e.name.clone()) {
519            return Err(ValidationError::DuplicateEnumName {
520                module: module.name.clone(),
521                name: e.name.clone(),
522            });
523        }
524        if e.variants.is_empty() {
525            return Err(ValidationError::EmptyEnum {
526                module: module.name.clone(),
527                name: e.name.clone(),
528            });
529        }
530        let mut variant_names = BTreeSet::new();
531        let mut variant_values = BTreeMap::new();
532        for v in &e.variants {
533            check_identifier(&v.name)?;
534            if !variant_names.insert(v.name.clone()) {
535                return Err(ValidationError::DuplicateEnumVariant {
536                    enum_name: e.name.clone(),
537                    variant: v.name.clone(),
538                });
539            }
540            if variant_values.insert(v.value, v.name.clone()).is_some() {
541                return Err(ValidationError::DuplicateEnumValue {
542                    enum_name: e.name.clone(),
543                    value: v.value,
544                });
545            }
546        }
547    }
548
549    let known_types: BTreeSet<&str> = struct_names
550        .iter()
551        .map(|s| s.as_str())
552        .chain(enum_names.iter().map(|s| s.as_str()))
553        .collect();
554    for s in &module.structs {
555        for f in &s.fields {
556            if let Some(ty) = contains_borrowed(&f.ty) {
557                return Err(ValidationError::BorrowedTypeInInvalidPosition {
558                    ty: ty.to_string(),
559                    location: format!("field '{}' of struct '{}'", f.name, s.name),
560                });
561            }
562            if contains_iterator(&f.ty) {
563                return Err(ValidationError::IteratorInInvalidPosition {
564                    location: format!("field '{}' of struct '{}'", f.name, s.name),
565                });
566            }
567            validate_type_ref(&f.ty, &known_types, all_modules)?;
568        }
569    }
570    for f in &module.functions {
571        for p in &f.params {
572            if contains_iterator(&p.ty) {
573                return Err(ValidationError::IteratorInInvalidPosition {
574                    location: format!(
575                        "param '{}' of function '{}::{}'",
576                        p.name, module.name, f.name
577                    ),
578                });
579            }
580            validate_type_ref(&p.ty, &known_types, all_modules)?;
581        }
582        if let Some(ret) = &f.returns {
583            if let Some(ty) = contains_borrowed(ret) {
584                return Err(ValidationError::BorrowedTypeInInvalidPosition {
585                    ty: ty.to_string(),
586                    location: format!("return type of {}::{}", module.name, f.name),
587                });
588            }
589            validate_type_ref(ret, &known_types, all_modules)?;
590        }
591    }
592
593    let mut callback_names = BTreeSet::new();
594    for cb in &module.callbacks {
595        check_identifier(&cb.name)?;
596        if !callback_names.insert(cb.name.clone()) {
597            return Err(ValidationError::DuplicateCallbackName {
598                module: module.name.clone(),
599                name: cb.name.clone(),
600            });
601        }
602        for p in &cb.params {
603            validate_param(p)?;
604        }
605    }
606
607    let mut listener_names = BTreeSet::new();
608    for l in &module.listeners {
609        check_identifier(&l.name)?;
610        if !listener_names.insert(l.name.clone()) {
611            return Err(ValidationError::DuplicateListenerName {
612                module: module.name.clone(),
613                name: l.name.clone(),
614            });
615        }
616        if !callback_names.contains(&l.event_callback) {
617            return Err(ValidationError::ListenerCallbackNotFound {
618                module: module.name.clone(),
619                listener: l.name.clone(),
620                callback: l.event_callback.clone(),
621            });
622        }
623    }
624
625    if let Some(errors) = &module.errors {
626        validate_error_domain(module, errors, &function_names)?;
627    }
628
629    let mut sub_module_names = BTreeSet::new();
630    for sub in &module.modules {
631        if !sub_module_names.insert(sub.name.clone()) {
632            return Err(ValidationError::DuplicateModuleName(sub.name.clone()));
633        }
634        validate_module(sub, all_modules)?;
635    }
636
637    Ok(())
638}
639
640fn validate_function(module: &Module, f: &Function) -> Result<(), ValidationError> {
641    check_identifier(&f.name)?;
642
643    let mut param_names = BTreeSet::new();
644    for p in &f.params {
645        validate_param(p)?;
646        if !param_names.insert(p.name.clone()) {
647            return Err(ValidationError::DuplicateParamName {
648                module: module.name.clone(),
649                function: f.name.clone(),
650                param: p.name.clone(),
651            });
652        }
653    }
654
655    Ok(())
656}
657
658fn validate_param(p: &Param) -> Result<(), ValidationError> {
659    check_identifier(&p.name)?;
660    Ok(())
661}
662
663fn contains_borrowed(ty: &TypeRef) -> Option<&'static str> {
664    match ty {
665        TypeRef::BorrowedStr => Some("&str"),
666        TypeRef::BorrowedBytes => Some("&[u8]"),
667        TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
668            contains_borrowed(inner)
669        }
670        TypeRef::Map(k, v) => contains_borrowed(k).or_else(|| contains_borrowed(v)),
671        _ => None,
672    }
673}
674
675fn contains_iterator(ty: &TypeRef) -> bool {
676    match ty {
677        TypeRef::Iterator(_) => true,
678        TypeRef::Optional(inner) | TypeRef::List(inner) => contains_iterator(inner),
679        TypeRef::Map(k, v) => contains_iterator(k) || contains_iterator(v),
680        _ => false,
681    }
682}
683
684/// Does a struct/enum named `name` exist anywhere in the module tree,
685/// including nested submodules? Validation runs before reference qualification,
686/// so an unqualified reference is valid if its bare name is defined anywhere;
687/// the resolver later rewrites it to the owning module's full path.
688fn type_exists(modules: &[Module], name: &str) -> bool {
689    modules.iter().any(|m| {
690        m.structs.iter().any(|s| s.name == name)
691            || m.enums.iter().any(|e| e.name == name)
692            || type_exists(&m.modules, name)
693    })
694}
695
696fn validate_type_ref(
697    ty: &TypeRef,
698    known: &BTreeSet<&str>,
699    all_modules: &[Module],
700) -> Result<(), ValidationError> {
701    match ty {
702        TypeRef::Struct(name) | TypeRef::Enum(name) | TypeRef::TypedHandle(name) => {
703            if !known.contains(name.as_str()) && !type_exists(all_modules, name) {
704                return Err(ValidationError::UnknownTypeRef { name: name.clone() });
705            }
706            Ok(())
707        }
708        TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
709            validate_type_ref(inner, known, all_modules)
710        }
711        TypeRef::Map(k, v) => {
712            let bad_key = match k.as_ref() {
713                TypeRef::Struct(name) => Some(format!("struct {name}")),
714                TypeRef::List(_) => Some("list".to_string()),
715                TypeRef::Map(_, _) => Some("map".to_string()),
716                _ => None,
717            };
718            if let Some(key_type) = bad_key {
719                return Err(ValidationError::InvalidMapKey { key_type });
720            }
721            validate_type_ref(k, known, all_modules)?;
722            validate_type_ref(v, known, all_modules)
723        }
724        _ => Ok(()),
725    }
726}
727
728fn validate_error_domain(
729    module: &Module,
730    errors: &ErrorDomain,
731    function_names: &BTreeSet<String>,
732) -> Result<(), ValidationError> {
733    if errors.name.trim().is_empty() {
734        return Err(ValidationError::ErrorDomainMissingName(module.name.clone()));
735    }
736    if function_names.contains(&errors.name) {
737        return Err(ValidationError::NameCollisionWithErrorDomain {
738            module: module.name.clone(),
739            name: errors.name.clone(),
740        });
741    }
742
743    let mut by_name: BTreeSet<String> = BTreeSet::new();
744    let mut by_code: BTreeMap<i32, String> = BTreeMap::new();
745    for c in &errors.codes {
746        if c.code == 0 {
747            return Err(ValidationError::InvalidErrorCode {
748                module: module.name.clone(),
749                name: c.name.clone(),
750            });
751        }
752        if !by_name.insert(c.name.clone()) {
753            return Err(ValidationError::DuplicateErrorName {
754                module: module.name.clone(),
755                name: c.name.clone(),
756            });
757        }
758        if by_code.insert(c.code, c.name.clone()).is_some() {
759            return Err(ValidationError::DuplicateErrorCode {
760                module: module.name.clone(),
761                code: c.code,
762            });
763        }
764    }
765    Ok(())
766}
767
768#[cfg(test)]
769mod tests {
770    use super::*;
771    use weaveffi_ir::ir::{
772        Api, CallbackDef, EnumDef, EnumVariant, ErrorCode, ErrorDomain, Function, ListenerDef,
773        Module, Param, StructDef, StructField, TypeRef,
774    };
775
776    fn simple_function(name: &str) -> Function {
777        Function {
778            name: name.to_string(),
779            params: vec![Param {
780                name: "x".to_string(),
781                ty: TypeRef::I32,
782                mutable: false,
783                doc: None,
784            }],
785            returns: Some(TypeRef::I32),
786            doc: None,
787            r#async: false,
788            cancellable: false,
789            deprecated: None,
790            since: None,
791        }
792    }
793
794    fn simple_module(name: &str) -> Module {
795        Module {
796            name: name.to_string(),
797            functions: vec![simple_function("do_stuff")],
798            structs: vec![],
799            enums: vec![],
800            callbacks: vec![],
801            listeners: vec![],
802            errors: None,
803            modules: vec![],
804        }
805    }
806
807    fn simple_api() -> Api {
808        Api {
809            version: "0.1.0".to_string(),
810            modules: vec![simple_module("mymod")],
811            generators: None,
812        }
813    }
814
815    #[test]
816    fn valid_api_passes() {
817        let mut api = simple_api();
818        assert!(validate_api(&mut api, None).is_ok());
819    }
820
821    #[test]
822    fn duplicate_module_names_rejected() {
823        let mut api = Api {
824            version: "0.1.0".to_string(),
825            modules: vec![simple_module("dup"), simple_module("dup")],
826            generators: None,
827        };
828        assert!(matches!(
829            validate_api(&mut api, None).unwrap_err().error,
830            ValidationError::DuplicateModuleName(n) if n == "dup"
831        ));
832    }
833
834    #[test]
835    fn duplicate_function_names_rejected() {
836        let mut api = Api {
837            version: "0.1.0".to_string(),
838            modules: vec![Module {
839                name: "mymod".to_string(),
840                functions: vec![simple_function("same"), simple_function("same")],
841                structs: vec![],
842                enums: vec![],
843                callbacks: vec![],
844                listeners: vec![],
845                errors: None,
846                modules: vec![],
847            }],
848            generators: None,
849        };
850        assert!(matches!(
851            validate_api(&mut api, None).unwrap_err().error,
852            ValidationError::DuplicateFunctionName { .. }
853        ));
854    }
855
856    #[test]
857    fn reserved_keywords_rejected() {
858        for kw in ["type", "async"] {
859            let mut api = Api {
860                version: "0.1.0".to_string(),
861                modules: vec![Module {
862                    name: kw.to_string(),
863                    functions: vec![simple_function("ok_fn")],
864                    structs: vec![],
865                    enums: vec![],
866                    callbacks: vec![],
867                    listeners: vec![],
868                    errors: None,
869                    modules: vec![],
870                }],
871                generators: None,
872            };
873            assert!(
874                validate_api(&mut api, None).is_err(),
875                "Expected reserved keyword '{kw}' to be rejected"
876            );
877        }
878    }
879
880    #[test]
881    fn invalid_identifiers_rejected() {
882        for bad in ["123", "has spaces", ""] {
883            let mut api = Api {
884                version: "0.1.0".to_string(),
885                modules: vec![Module {
886                    name: bad.to_string(),
887                    functions: vec![simple_function("ok_fn")],
888                    structs: vec![],
889                    enums: vec![],
890                    callbacks: vec![],
891                    listeners: vec![],
892                    errors: None,
893                    modules: vec![],
894                }],
895                generators: None,
896            };
897            assert!(
898                validate_api(&mut api, None).is_err(),
899                "Expected invalid identifier '{bad}' to be rejected"
900            );
901        }
902    }
903
904    #[test]
905    fn async_function_passes_validation() {
906        let mut api = Api {
907            version: "0.1.0".to_string(),
908            modules: vec![Module {
909                name: "mymod".to_string(),
910                functions: vec![Function {
911                    name: "do_async".to_string(),
912                    params: vec![],
913                    returns: None,
914                    doc: None,
915                    r#async: true,
916                    cancellable: false,
917                    deprecated: None,
918                    since: None,
919                }],
920                structs: vec![],
921                enums: vec![],
922                callbacks: vec![],
923                listeners: vec![],
924                errors: None,
925                modules: vec![],
926            }],
927            generators: None,
928        };
929        assert!(validate_api(&mut api, None).is_ok());
930    }
931
932    #[test]
933    fn async_function_with_return_passes() {
934        let mut api = Api {
935            version: "0.1.0".to_string(),
936            modules: vec![Module {
937                name: "mymod".to_string(),
938                functions: vec![Function {
939                    name: "fetch_data".to_string(),
940                    params: vec![Param {
941                        name: "url".to_string(),
942                        ty: TypeRef::StringUtf8,
943                        mutable: false,
944                        doc: None,
945                    }],
946                    returns: Some(TypeRef::StringUtf8),
947                    doc: None,
948                    r#async: true,
949                    cancellable: false,
950                    deprecated: None,
951                    since: None,
952                }],
953                structs: vec![],
954                enums: vec![],
955                callbacks: vec![],
956                listeners: vec![],
957                errors: None,
958                modules: vec![],
959            }],
960            generators: None,
961        };
962        assert!(validate_api(&mut api, None).is_ok());
963    }
964
965    #[test]
966    fn async_void_function_emits_warning() {
967        let api = Api {
968            version: "0.1.0".to_string(),
969            modules: vec![Module {
970                name: "mymod".to_string(),
971                functions: vec![Function {
972                    name: "fire_and_forget".to_string(),
973                    params: vec![],
974                    returns: None,
975                    doc: Some("documented".to_string()),
976                    r#async: true,
977                    cancellable: false,
978                    deprecated: None,
979                    since: None,
980                }],
981                structs: vec![],
982                enums: vec![],
983                callbacks: vec![],
984                listeners: vec![],
985                errors: None,
986                modules: vec![],
987            }],
988            generators: None,
989        };
990        let warnings = collect_warnings(&api);
991        assert!(warnings.iter().any(|w| matches!(
992            w,
993            ValidationWarning::AsyncVoidFunction { module, function }
994                if module == "mymod" && function == "fire_and_forget"
995        )));
996    }
997
998    #[test]
999    fn async_function_with_return_no_void_warning() {
1000        let api = Api {
1001            version: "0.1.0".to_string(),
1002            modules: vec![Module {
1003                name: "mymod".to_string(),
1004                functions: vec![Function {
1005                    name: "fetch".to_string(),
1006                    params: vec![],
1007                    returns: Some(TypeRef::StringUtf8),
1008                    doc: Some("documented".to_string()),
1009                    r#async: true,
1010                    cancellable: false,
1011                    deprecated: None,
1012                    since: None,
1013                }],
1014                structs: vec![],
1015                enums: vec![],
1016                callbacks: vec![],
1017                listeners: vec![],
1018                errors: None,
1019                modules: vec![],
1020            }],
1021            generators: None,
1022        };
1023        let warnings = collect_warnings(&api);
1024        assert!(!warnings
1025            .iter()
1026            .any(|w| matches!(w, ValidationWarning::AsyncVoidFunction { .. })));
1027    }
1028
1029    #[test]
1030    fn empty_module_name_rejected() {
1031        let mut api = Api {
1032            version: "0.1.0".to_string(),
1033            modules: vec![Module {
1034                name: "".to_string(),
1035                functions: vec![simple_function("ok_fn")],
1036                structs: vec![],
1037                enums: vec![],
1038                callbacks: vec![],
1039                listeners: vec![],
1040                errors: None,
1041                modules: vec![],
1042            }],
1043            generators: None,
1044        };
1045        assert!(matches!(
1046            validate_api(&mut api, None).unwrap_err().error,
1047            ValidationError::NoModuleName
1048        ));
1049    }
1050
1051    #[test]
1052    fn doc_example_error_domain_validates() {
1053        let mut api = Api {
1054            version: "0.1.0".to_string(),
1055            modules: vec![Module {
1056                name: "contacts".to_string(),
1057                functions: vec![
1058                    Function {
1059                        name: "create_contact".to_string(),
1060                        params: vec![
1061                            Param {
1062                                name: "name".to_string(),
1063                                ty: TypeRef::StringUtf8,
1064                                mutable: false,
1065                                doc: None,
1066                            },
1067                            Param {
1068                                name: "email".to_string(),
1069                                ty: TypeRef::StringUtf8,
1070                                mutable: false,
1071                                doc: None,
1072                            },
1073                        ],
1074                        returns: Some(TypeRef::Handle),
1075                        doc: None,
1076                        r#async: false,
1077                        cancellable: false,
1078                        deprecated: None,
1079                        since: None,
1080                    },
1081                    Function {
1082                        name: "get_contact".to_string(),
1083                        params: vec![Param {
1084                            name: "id".to_string(),
1085                            ty: TypeRef::Handle,
1086                            mutable: false,
1087                            doc: None,
1088                        }],
1089                        returns: Some(TypeRef::StringUtf8),
1090                        doc: None,
1091                        r#async: false,
1092                        cancellable: false,
1093                        deprecated: None,
1094                        since: None,
1095                    },
1096                ],
1097                structs: vec![],
1098                enums: vec![],
1099                callbacks: vec![],
1100                listeners: vec![],
1101                errors: Some(ErrorDomain {
1102                    name: "ContactErrors".to_string(),
1103                    codes: vec![
1104                        ErrorCode {
1105                            name: "not_found".to_string(),
1106                            code: 1,
1107                            message: "Contact not found".to_string(),
1108                            doc: None,
1109                        },
1110                        ErrorCode {
1111                            name: "duplicate".to_string(),
1112                            code: 2,
1113                            message: "Contact already exists".to_string(),
1114                            doc: None,
1115                        },
1116                        ErrorCode {
1117                            name: "invalid_email".to_string(),
1118                            code: 3,
1119                            message: "Email address is invalid".to_string(),
1120                            doc: None,
1121                        },
1122                    ],
1123                }),
1124                modules: vec![],
1125            }],
1126            generators: None,
1127        };
1128        assert!(validate_api(&mut api, None).is_ok());
1129    }
1130
1131    #[test]
1132    fn error_code_zero_rejected() {
1133        let mut api = Api {
1134            version: "0.1.0".to_string(),
1135            modules: vec![Module {
1136                name: "mymod".to_string(),
1137                functions: vec![simple_function("ok_fn")],
1138                structs: vec![],
1139                enums: vec![],
1140                callbacks: vec![],
1141                listeners: vec![],
1142                errors: Some(ErrorDomain {
1143                    name: "MyErrors".to_string(),
1144                    codes: vec![ErrorCode {
1145                        name: "success".to_string(),
1146                        code: 0,
1147                        message: "should fail".to_string(),
1148                        doc: None,
1149                    }],
1150                }),
1151                modules: vec![],
1152            }],
1153            generators: None,
1154        };
1155        assert!(matches!(
1156            validate_api(&mut api, None).unwrap_err().error,
1157            ValidationError::InvalidErrorCode { module, name }
1158                if module == "mymod" && name == "success"
1159        ));
1160    }
1161
1162    #[test]
1163    fn error_domain_name_collision_rejected() {
1164        let mut api = Api {
1165            version: "0.1.0".to_string(),
1166            modules: vec![Module {
1167                name: "mymod".to_string(),
1168                functions: vec![simple_function("do_stuff")],
1169                structs: vec![],
1170                enums: vec![],
1171                callbacks: vec![],
1172                listeners: vec![],
1173                errors: Some(ErrorDomain {
1174                    name: "do_stuff".to_string(),
1175                    codes: vec![ErrorCode {
1176                        name: "fail".to_string(),
1177                        code: 1,
1178                        message: "failed".to_string(),
1179                        doc: None,
1180                    }],
1181                }),
1182                modules: vec![],
1183            }],
1184            generators: None,
1185        };
1186        assert!(matches!(
1187            validate_api(&mut api, None).unwrap_err().error,
1188            ValidationError::NameCollisionWithErrorDomain { module, name }
1189                if module == "mymod" && name == "do_stuff"
1190        ));
1191    }
1192
1193    #[test]
1194    fn duplicate_error_names_rejected() {
1195        let mut api = Api {
1196            version: "0.1.0".to_string(),
1197            modules: vec![Module {
1198                name: "mymod".to_string(),
1199                functions: vec![simple_function("ok_fn")],
1200                structs: vec![],
1201                enums: vec![],
1202                callbacks: vec![],
1203                listeners: vec![],
1204                errors: Some(ErrorDomain {
1205                    name: "MyErrors".to_string(),
1206                    codes: vec![
1207                        ErrorCode {
1208                            name: "fail".to_string(),
1209                            code: 1,
1210                            message: "failed".to_string(),
1211                            doc: None,
1212                        },
1213                        ErrorCode {
1214                            name: "fail".to_string(),
1215                            code: 2,
1216                            message: "also failed".to_string(),
1217                            doc: None,
1218                        },
1219                    ],
1220                }),
1221                modules: vec![],
1222            }],
1223            generators: None,
1224        };
1225        assert!(matches!(
1226            validate_api(&mut api, None).unwrap_err().error,
1227            ValidationError::DuplicateErrorName { module, name }
1228                if module == "mymod" && name == "fail"
1229        ));
1230    }
1231
1232    #[test]
1233    fn duplicate_error_codes_rejected() {
1234        let mut api = Api {
1235            version: "0.1.0".to_string(),
1236            modules: vec![Module {
1237                name: "mymod".to_string(),
1238                functions: vec![simple_function("ok_fn")],
1239                structs: vec![],
1240                enums: vec![],
1241                callbacks: vec![],
1242                listeners: vec![],
1243                errors: Some(ErrorDomain {
1244                    name: "MyErrors".to_string(),
1245                    codes: vec![
1246                        ErrorCode {
1247                            name: "not_found".to_string(),
1248                            code: 1,
1249                            message: "not found".to_string(),
1250                            doc: None,
1251                        },
1252                        ErrorCode {
1253                            name: "timeout".to_string(),
1254                            code: 1,
1255                            message: "timed out".to_string(),
1256                            doc: None,
1257                        },
1258                    ],
1259                }),
1260                modules: vec![],
1261            }],
1262            generators: None,
1263        };
1264        assert!(matches!(
1265            validate_api(&mut api, None).unwrap_err().error,
1266            ValidationError::DuplicateErrorCode { .. }
1267        ));
1268    }
1269
1270    fn simple_struct(name: &str) -> StructDef {
1271        StructDef {
1272            name: name.to_string(),
1273            doc: None,
1274            fields: vec![StructField {
1275                name: "x".to_string(),
1276                ty: TypeRef::I32,
1277                doc: None,
1278                default: None,
1279            }],
1280            builder: false,
1281        }
1282    }
1283
1284    #[test]
1285    fn duplicate_struct_names_rejected() {
1286        let mut api = Api {
1287            version: "0.1.0".to_string(),
1288            modules: vec![Module {
1289                name: "mymod".to_string(),
1290                functions: vec![simple_function("ok_fn")],
1291                structs: vec![simple_struct("Point"), simple_struct("Point")],
1292                enums: vec![],
1293                callbacks: vec![],
1294                listeners: vec![],
1295                errors: None,
1296                modules: vec![],
1297            }],
1298            generators: None,
1299        };
1300        assert!(matches!(
1301            validate_api(&mut api, None).unwrap_err().error,
1302            ValidationError::DuplicateStructName { module, name }
1303                if module == "mymod" && name == "Point"
1304        ));
1305    }
1306
1307    #[test]
1308    fn empty_struct_rejected() {
1309        let mut api = Api {
1310            version: "0.1.0".to_string(),
1311            modules: vec![Module {
1312                name: "mymod".to_string(),
1313                functions: vec![simple_function("ok_fn")],
1314                structs: vec![StructDef {
1315                    name: "Empty".to_string(),
1316                    doc: None,
1317                    fields: vec![],
1318                    builder: false,
1319                }],
1320                enums: vec![],
1321                callbacks: vec![],
1322                listeners: vec![],
1323                errors: None,
1324                modules: vec![],
1325            }],
1326            generators: None,
1327        };
1328        assert!(matches!(
1329            validate_api(&mut api, None).unwrap_err().error,
1330            ValidationError::EmptyStruct { module, name }
1331                if module == "mymod" && name == "Empty"
1332        ));
1333    }
1334
1335    #[test]
1336    fn duplicate_struct_field_names_rejected() {
1337        let mut api = Api {
1338            version: "0.1.0".to_string(),
1339            modules: vec![Module {
1340                name: "mymod".to_string(),
1341                functions: vec![simple_function("ok_fn")],
1342                structs: vec![StructDef {
1343                    name: "Point".to_string(),
1344                    doc: None,
1345                    fields: vec![
1346                        StructField {
1347                            name: "x".to_string(),
1348                            ty: TypeRef::I32,
1349                            doc: None,
1350                            default: None,
1351                        },
1352                        StructField {
1353                            name: "x".to_string(),
1354                            ty: TypeRef::F64,
1355                            doc: None,
1356                            default: None,
1357                        },
1358                    ],
1359                    builder: false,
1360                }],
1361                enums: vec![],
1362                callbacks: vec![],
1363                listeners: vec![],
1364                errors: None,
1365                modules: vec![],
1366            }],
1367            generators: None,
1368        };
1369        assert!(matches!(
1370            validate_api(&mut api, None).unwrap_err().error,
1371            ValidationError::DuplicateStructField { struct_name, field }
1372                if struct_name == "Point" && field == "x"
1373        ));
1374    }
1375
1376    fn simple_enum(name: &str) -> EnumDef {
1377        EnumDef {
1378            name: name.to_string(),
1379            doc: None,
1380            variants: vec![
1381                EnumVariant {
1382                    name: "A".to_string(),
1383                    value: 0,
1384                    doc: None,
1385                },
1386                EnumVariant {
1387                    name: "B".to_string(),
1388                    value: 1,
1389                    doc: None,
1390                },
1391            ],
1392        }
1393    }
1394
1395    #[test]
1396    fn duplicate_enum_names_rejected() {
1397        let mut api = Api {
1398            version: "0.1.0".to_string(),
1399            modules: vec![Module {
1400                name: "mymod".to_string(),
1401                functions: vec![simple_function("ok_fn")],
1402                structs: vec![],
1403                enums: vec![simple_enum("Color"), simple_enum("Color")],
1404                callbacks: vec![],
1405                listeners: vec![],
1406                errors: None,
1407                modules: vec![],
1408            }],
1409            generators: None,
1410        };
1411        assert!(matches!(
1412            validate_api(&mut api, None).unwrap_err().error,
1413            ValidationError::DuplicateEnumName { module, name }
1414                if module == "mymod" && name == "Color"
1415        ));
1416    }
1417
1418    #[test]
1419    fn empty_enum_rejected() {
1420        let mut api = Api {
1421            version: "0.1.0".to_string(),
1422            modules: vec![Module {
1423                name: "mymod".to_string(),
1424                functions: vec![simple_function("ok_fn")],
1425                structs: vec![],
1426                enums: vec![EnumDef {
1427                    name: "Empty".to_string(),
1428                    doc: None,
1429                    variants: vec![],
1430                }],
1431                callbacks: vec![],
1432                listeners: vec![],
1433                errors: None,
1434                modules: vec![],
1435            }],
1436            generators: None,
1437        };
1438        assert!(matches!(
1439            validate_api(&mut api, None).unwrap_err().error,
1440            ValidationError::EmptyEnum { module, name }
1441                if module == "mymod" && name == "Empty"
1442        ));
1443    }
1444
1445    #[test]
1446    fn duplicate_enum_variant_rejected() {
1447        let mut api = Api {
1448            version: "0.1.0".to_string(),
1449            modules: vec![Module {
1450                name: "mymod".to_string(),
1451                functions: vec![simple_function("ok_fn")],
1452                structs: vec![],
1453                enums: vec![EnumDef {
1454                    name: "Color".to_string(),
1455                    doc: None,
1456                    variants: vec![
1457                        EnumVariant {
1458                            name: "Red".to_string(),
1459                            value: 0,
1460                            doc: None,
1461                        },
1462                        EnumVariant {
1463                            name: "Red".to_string(),
1464                            value: 1,
1465                            doc: None,
1466                        },
1467                    ],
1468                }],
1469                callbacks: vec![],
1470                listeners: vec![],
1471                errors: None,
1472                modules: vec![],
1473            }],
1474            generators: None,
1475        };
1476        assert!(matches!(
1477            validate_api(&mut api, None).unwrap_err().error,
1478            ValidationError::DuplicateEnumVariant { enum_name, variant }
1479                if enum_name == "Color" && variant == "Red"
1480        ));
1481    }
1482
1483    #[test]
1484    fn duplicate_enum_value_rejected() {
1485        let mut api = Api {
1486            version: "0.1.0".to_string(),
1487            modules: vec![Module {
1488                name: "mymod".to_string(),
1489                functions: vec![simple_function("ok_fn")],
1490                structs: vec![],
1491                enums: vec![EnumDef {
1492                    name: "Color".to_string(),
1493                    doc: None,
1494                    variants: vec![
1495                        EnumVariant {
1496                            name: "Red".to_string(),
1497                            value: 0,
1498                            doc: None,
1499                        },
1500                        EnumVariant {
1501                            name: "Green".to_string(),
1502                            value: 0,
1503                            doc: None,
1504                        },
1505                    ],
1506                }],
1507                callbacks: vec![],
1508                listeners: vec![],
1509                errors: None,
1510                modules: vec![],
1511            }],
1512            generators: None,
1513        };
1514        assert!(matches!(
1515            validate_api(&mut api, None).unwrap_err().error,
1516            ValidationError::DuplicateEnumValue { enum_name, value }
1517                if enum_name == "Color" && value == 0
1518        ));
1519    }
1520
1521    #[test]
1522    fn unknown_type_ref_rejected() {
1523        let mut api = Api {
1524            version: "0.1.0".to_string(),
1525            modules: vec![Module {
1526                name: "mymod".to_string(),
1527                functions: vec![Function {
1528                    name: "do_stuff".to_string(),
1529                    params: vec![Param {
1530                        name: "x".to_string(),
1531                        ty: TypeRef::Struct("Foo".to_string()),
1532                        mutable: false,
1533                        doc: None,
1534                    }],
1535                    returns: None,
1536                    doc: None,
1537                    r#async: false,
1538                    cancellable: false,
1539                    deprecated: None,
1540                    since: None,
1541                }],
1542                structs: vec![],
1543                enums: vec![],
1544                callbacks: vec![],
1545                listeners: vec![],
1546                errors: None,
1547                modules: vec![],
1548            }],
1549            generators: None,
1550        };
1551        assert!(matches!(
1552            validate_api(&mut api, None).unwrap_err().error,
1553            ValidationError::UnknownTypeRef { name } if name == "Foo"
1554        ));
1555    }
1556
1557    #[test]
1558    fn valid_struct_ref_passes() {
1559        let mut api = Api {
1560            version: "0.1.0".to_string(),
1561            modules: vec![Module {
1562                name: "mymod".to_string(),
1563                functions: vec![Function {
1564                    name: "do_stuff".to_string(),
1565                    params: vec![Param {
1566                        name: "p".to_string(),
1567                        ty: TypeRef::Struct("Point".to_string()),
1568                        mutable: false,
1569                        doc: None,
1570                    }],
1571                    returns: None,
1572                    doc: None,
1573                    r#async: false,
1574                    cancellable: false,
1575                    deprecated: None,
1576                    since: None,
1577                }],
1578                structs: vec![simple_struct("Point")],
1579                enums: vec![],
1580                callbacks: vec![],
1581                listeners: vec![],
1582                errors: None,
1583                modules: vec![],
1584            }],
1585            generators: None,
1586        };
1587        assert!(validate_api(&mut api, None).is_ok());
1588    }
1589
1590    #[test]
1591    fn unknown_type_ref_in_optional_rejected() {
1592        let mut api = Api {
1593            version: "0.1.0".to_string(),
1594            modules: vec![Module {
1595                name: "mymod".to_string(),
1596                functions: vec![Function {
1597                    name: "do_stuff".to_string(),
1598                    params: vec![Param {
1599                        name: "x".to_string(),
1600                        ty: TypeRef::Optional(Box::new(TypeRef::Struct("Bar".to_string()))),
1601                        mutable: false,
1602                        doc: None,
1603                    }],
1604                    returns: None,
1605                    doc: None,
1606                    r#async: false,
1607                    cancellable: false,
1608                    deprecated: None,
1609                    since: None,
1610                }],
1611                structs: vec![],
1612                enums: vec![],
1613                callbacks: vec![],
1614                listeners: vec![],
1615                errors: None,
1616                modules: vec![],
1617            }],
1618            generators: None,
1619        };
1620        assert!(matches!(
1621            validate_api(&mut api, None).unwrap_err().error,
1622            ValidationError::UnknownTypeRef { name } if name == "Bar"
1623        ));
1624    }
1625
1626    #[test]
1627    fn unknown_type_ref_in_list_rejected() {
1628        let mut api = Api {
1629            version: "0.1.0".to_string(),
1630            modules: vec![Module {
1631                name: "mymod".to_string(),
1632                functions: vec![Function {
1633                    name: "do_stuff".to_string(),
1634                    params: vec![],
1635                    returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Baz".to_string())))),
1636                    doc: None,
1637                    r#async: false,
1638                    cancellable: false,
1639                    deprecated: None,
1640                    since: None,
1641                }],
1642                structs: vec![],
1643                enums: vec![],
1644                callbacks: vec![],
1645                listeners: vec![],
1646                errors: None,
1647                modules: vec![],
1648            }],
1649            generators: None,
1650        };
1651        assert!(matches!(
1652            validate_api(&mut api, None).unwrap_err().error,
1653            ValidationError::UnknownTypeRef { name } if name == "Baz"
1654        ));
1655    }
1656
1657    #[test]
1658    fn struct_field_referencing_unknown_type() {
1659        let mut api = Api {
1660            version: "0.1.0".to_string(),
1661            modules: vec![Module {
1662                name: "mymod".to_string(),
1663                functions: vec![simple_function("ok_fn")],
1664                structs: vec![StructDef {
1665                    name: "Wrapper".to_string(),
1666                    doc: None,
1667                    fields: vec![StructField {
1668                        name: "inner".to_string(),
1669                        ty: TypeRef::Struct("Nonexistent".to_string()),
1670                        doc: None,
1671                        default: None,
1672                    }],
1673                    builder: false,
1674                }],
1675                enums: vec![],
1676                callbacks: vec![],
1677                listeners: vec![],
1678                errors: None,
1679                modules: vec![],
1680            }],
1681            generators: None,
1682        };
1683        assert!(matches!(
1684            validate_api(&mut api, None).unwrap_err().error,
1685            ValidationError::UnknownTypeRef { name } if name == "Nonexistent"
1686        ));
1687    }
1688
1689    #[test]
1690    fn function_param_with_optional_struct() {
1691        let mut api = Api {
1692            version: "0.1.0".to_string(),
1693            modules: vec![Module {
1694                name: "mymod".to_string(),
1695                functions: vec![Function {
1696                    name: "save".to_string(),
1697                    params: vec![Param {
1698                        name: "c".to_string(),
1699                        ty: TypeRef::Optional(Box::new(TypeRef::Struct("Contact".to_string()))),
1700                        mutable: false,
1701                        doc: None,
1702                    }],
1703                    returns: None,
1704                    doc: None,
1705                    r#async: false,
1706                    cancellable: false,
1707                    deprecated: None,
1708                    since: None,
1709                }],
1710                structs: vec![StructDef {
1711                    name: "Contact".to_string(),
1712                    doc: None,
1713                    fields: vec![StructField {
1714                        name: "name".to_string(),
1715                        ty: TypeRef::StringUtf8,
1716                        doc: None,
1717                        default: None,
1718                    }],
1719                    builder: false,
1720                }],
1721                enums: vec![],
1722                callbacks: vec![],
1723                listeners: vec![],
1724                errors: None,
1725                modules: vec![],
1726            }],
1727            generators: None,
1728        };
1729        assert!(validate_api(&mut api, None).is_ok());
1730    }
1731
1732    #[test]
1733    fn function_param_with_list_of_enums() {
1734        let mut api = Api {
1735            version: "0.1.0".to_string(),
1736            modules: vec![Module {
1737                name: "mymod".to_string(),
1738                functions: vec![Function {
1739                    name: "paint".to_string(),
1740                    params: vec![Param {
1741                        name: "colors".to_string(),
1742                        ty: TypeRef::List(Box::new(TypeRef::Enum("Color".to_string()))),
1743                        mutable: false,
1744                        doc: None,
1745                    }],
1746                    returns: None,
1747                    doc: None,
1748                    r#async: false,
1749                    cancellable: false,
1750                    deprecated: None,
1751                    since: None,
1752                }],
1753                structs: vec![],
1754                enums: vec![simple_enum("Color")],
1755                callbacks: vec![],
1756                listeners: vec![],
1757                errors: None,
1758                modules: vec![],
1759            }],
1760            generators: None,
1761        };
1762        assert!(validate_api(&mut api, None).is_ok());
1763    }
1764
1765    #[test]
1766    fn nested_optional_list_validates() {
1767        let mut api = Api {
1768            version: "0.1.0".to_string(),
1769            modules: vec![Module {
1770                name: "mymod".to_string(),
1771                functions: vec![Function {
1772                    name: "list_contacts".to_string(),
1773                    params: vec![],
1774                    returns: Some(TypeRef::List(Box::new(TypeRef::Optional(Box::new(
1775                        TypeRef::Struct("Contact".to_string()),
1776                    ))))),
1777                    doc: None,
1778                    r#async: false,
1779                    cancellable: false,
1780                    deprecated: None,
1781                    since: None,
1782                }],
1783                structs: vec![StructDef {
1784                    name: "Contact".to_string(),
1785                    doc: None,
1786                    fields: vec![StructField {
1787                        name: "name".to_string(),
1788                        ty: TypeRef::StringUtf8,
1789                        doc: None,
1790                        default: None,
1791                    }],
1792                    builder: false,
1793                }],
1794                enums: vec![],
1795                callbacks: vec![],
1796                listeners: vec![],
1797                errors: None,
1798                modules: vec![],
1799            }],
1800            generators: None,
1801        };
1802        assert!(validate_api(&mut api, None).is_ok());
1803    }
1804
1805    #[test]
1806    fn enum_variant_value_zero_allowed() {
1807        let mut api = Api {
1808            version: "0.1.0".to_string(),
1809            modules: vec![Module {
1810                name: "mymod".to_string(),
1811                functions: vec![simple_function("ok_fn")],
1812                structs: vec![],
1813                enums: vec![EnumDef {
1814                    name: "Status".to_string(),
1815                    doc: None,
1816                    variants: vec![
1817                        EnumVariant {
1818                            name: "Unknown".to_string(),
1819                            value: 0,
1820                            doc: None,
1821                        },
1822                        EnumVariant {
1823                            name: "Active".to_string(),
1824                            value: 1,
1825                            doc: None,
1826                        },
1827                    ],
1828                }],
1829                callbacks: vec![],
1830                listeners: vec![],
1831                errors: None,
1832                modules: vec![],
1833            }],
1834            generators: None,
1835        };
1836        assert!(validate_api(&mut api, None).is_ok());
1837    }
1838
1839    #[test]
1840    fn valid_enum_ref_passes() {
1841        let mut api = Api {
1842            version: "0.1.0".to_string(),
1843            modules: vec![Module {
1844                name: "mymod".to_string(),
1845                functions: vec![Function {
1846                    name: "get_color".to_string(),
1847                    params: vec![],
1848                    returns: Some(TypeRef::Enum("Color".to_string())),
1849                    doc: None,
1850                    r#async: false,
1851                    cancellable: false,
1852                    deprecated: None,
1853                    since: None,
1854                }],
1855                structs: vec![],
1856                enums: vec![simple_enum("Color")],
1857                callbacks: vec![],
1858                listeners: vec![],
1859                errors: None,
1860                modules: vec![],
1861            }],
1862            generators: None,
1863        };
1864        assert!(validate_api(&mut api, None).is_ok());
1865    }
1866
1867    #[test]
1868    fn resolve_enum_ref_in_function_param() {
1869        let mut api = Api {
1870            version: "0.1.0".to_string(),
1871            modules: vec![Module {
1872                name: "mymod".to_string(),
1873                functions: vec![Function {
1874                    name: "paint".to_string(),
1875                    params: vec![Param {
1876                        name: "color".to_string(),
1877                        ty: TypeRef::Struct("Color".to_string()),
1878                        mutable: false,
1879                        doc: None,
1880                    }],
1881                    returns: None,
1882                    doc: None,
1883                    r#async: false,
1884                    cancellable: false,
1885                    deprecated: None,
1886                    since: None,
1887                }],
1888                structs: vec![],
1889                enums: vec![simple_enum("Color")],
1890                callbacks: vec![],
1891                listeners: vec![],
1892                errors: None,
1893                modules: vec![],
1894            }],
1895            generators: None,
1896        };
1897        validate_api(&mut api, None).unwrap();
1898        assert_eq!(
1899            api.modules[0].functions[0].params[0].ty,
1900            TypeRef::Enum("Color".to_string())
1901        );
1902    }
1903
1904    #[test]
1905    fn resolve_enum_ref_in_optional() {
1906        let mut api = Api {
1907            version: "0.1.0".to_string(),
1908            modules: vec![Module {
1909                name: "mymod".to_string(),
1910                functions: vec![Function {
1911                    name: "paint".to_string(),
1912                    params: vec![Param {
1913                        name: "color".to_string(),
1914                        ty: TypeRef::Optional(Box::new(TypeRef::Struct("Color".to_string()))),
1915                        mutable: false,
1916                        doc: None,
1917                    }],
1918                    returns: None,
1919                    doc: None,
1920                    r#async: false,
1921                    cancellable: false,
1922                    deprecated: None,
1923                    since: None,
1924                }],
1925                structs: vec![],
1926                enums: vec![simple_enum("Color")],
1927                callbacks: vec![],
1928                listeners: vec![],
1929                errors: None,
1930                modules: vec![],
1931            }],
1932            generators: None,
1933        };
1934        validate_api(&mut api, None).unwrap();
1935        assert_eq!(
1936            api.modules[0].functions[0].params[0].ty,
1937            TypeRef::Optional(Box::new(TypeRef::Enum("Color".to_string())))
1938        );
1939    }
1940
1941    #[test]
1942    fn struct_ref_not_changed() {
1943        let mut api = Api {
1944            version: "0.1.0".to_string(),
1945            modules: vec![Module {
1946                name: "mymod".to_string(),
1947                functions: vec![Function {
1948                    name: "save".to_string(),
1949                    params: vec![Param {
1950                        name: "c".to_string(),
1951                        ty: TypeRef::Struct("Contact".to_string()),
1952                        mutable: false,
1953                        doc: None,
1954                    }],
1955                    returns: None,
1956                    doc: None,
1957                    r#async: false,
1958                    cancellable: false,
1959                    deprecated: None,
1960                    since: None,
1961                }],
1962                structs: vec![simple_struct("Contact")],
1963                enums: vec![],
1964                callbacks: vec![],
1965                listeners: vec![],
1966                errors: None,
1967                modules: vec![],
1968            }],
1969            generators: None,
1970        };
1971        validate_api(&mut api, None).unwrap();
1972        assert_eq!(
1973            api.modules[0].functions[0].params[0].ty,
1974            TypeRef::Struct("Contact".to_string())
1975        );
1976    }
1977
1978    #[test]
1979    fn map_with_string_key_passes() {
1980        let mut api = Api {
1981            version: "0.1.0".to_string(),
1982            modules: vec![Module {
1983                name: "mymod".to_string(),
1984                functions: vec![Function {
1985                    name: "get_map".to_string(),
1986                    params: vec![],
1987                    returns: Some(TypeRef::Map(
1988                        Box::new(TypeRef::StringUtf8),
1989                        Box::new(TypeRef::I32),
1990                    )),
1991                    doc: None,
1992                    r#async: false,
1993                    cancellable: false,
1994                    deprecated: None,
1995                    since: None,
1996                }],
1997                structs: vec![],
1998                enums: vec![],
1999                callbacks: vec![],
2000                listeners: vec![],
2001                errors: None,
2002                modules: vec![],
2003            }],
2004            generators: None,
2005        };
2006        assert!(validate_api(&mut api, None).is_ok());
2007    }
2008
2009    #[test]
2010    fn map_with_struct_key_rejected() {
2011        let mut api = Api {
2012            version: "0.1.0".to_string(),
2013            modules: vec![Module {
2014                name: "mymod".to_string(),
2015                functions: vec![Function {
2016                    name: "get_map".to_string(),
2017                    params: vec![],
2018                    returns: Some(TypeRef::Map(
2019                        Box::new(TypeRef::Struct("Point".to_string())),
2020                        Box::new(TypeRef::I32),
2021                    )),
2022                    doc: None,
2023                    r#async: false,
2024                    cancellable: false,
2025                    deprecated: None,
2026                    since: None,
2027                }],
2028                structs: vec![simple_struct("Point")],
2029                enums: vec![],
2030                callbacks: vec![],
2031                listeners: vec![],
2032                errors: None,
2033                modules: vec![],
2034            }],
2035            generators: None,
2036        };
2037        assert!(matches!(
2038            validate_api(&mut api, None).unwrap_err().error,
2039            ValidationError::InvalidMapKey { key_type } if key_type == "struct Point"
2040        ));
2041    }
2042
2043    #[test]
2044    fn map_with_enum_key_passes() {
2045        let mut api = Api {
2046            version: "0.1.0".to_string(),
2047            modules: vec![Module {
2048                name: "mymod".to_string(),
2049                functions: vec![Function {
2050                    name: "get_map".to_string(),
2051                    params: vec![],
2052                    returns: Some(TypeRef::Map(
2053                        Box::new(TypeRef::Enum("Color".to_string())),
2054                        Box::new(TypeRef::StringUtf8),
2055                    )),
2056                    doc: None,
2057                    r#async: false,
2058                    cancellable: false,
2059                    deprecated: None,
2060                    since: None,
2061                }],
2062                structs: vec![],
2063                enums: vec![simple_enum("Color")],
2064                callbacks: vec![],
2065                listeners: vec![],
2066                errors: None,
2067                modules: vec![],
2068            }],
2069            generators: None,
2070        };
2071        assert!(validate_api(&mut api, None).is_ok());
2072    }
2073
2074    #[test]
2075    fn warning_large_enum_variant_count() {
2076        let variants: Vec<EnumVariant> = (0..101)
2077            .map(|i| EnumVariant {
2078                name: format!("V{i}"),
2079                value: i,
2080                doc: None,
2081            })
2082            .collect();
2083        let api = Api {
2084            version: "0.1.0".to_string(),
2085            modules: vec![Module {
2086                name: "mymod".to_string(),
2087                functions: vec![simple_function("ok_fn")],
2088                structs: vec![],
2089                enums: vec![EnumDef {
2090                    name: "BigEnum".to_string(),
2091                    doc: None,
2092                    variants,
2093                }],
2094                callbacks: vec![],
2095                listeners: vec![],
2096                errors: None,
2097                modules: vec![],
2098            }],
2099            generators: None,
2100        };
2101        let warnings = collect_warnings(&api);
2102        assert!(warnings.iter().any(|w| matches!(
2103            w,
2104            ValidationWarning::LargeEnumVariantCount { enum_name, count }
2105                if enum_name == "BigEnum" && *count == 101
2106        )));
2107    }
2108
2109    #[test]
2110    fn warning_enum_at_100_no_warning() {
2111        let variants: Vec<EnumVariant> = (0..100)
2112            .map(|i| EnumVariant {
2113                name: format!("V{i}"),
2114                value: i,
2115                doc: None,
2116            })
2117            .collect();
2118        let api = Api {
2119            version: "0.1.0".to_string(),
2120            modules: vec![Module {
2121                name: "mymod".to_string(),
2122                functions: vec![simple_function("ok_fn")],
2123                structs: vec![],
2124                enums: vec![EnumDef {
2125                    name: "BigEnum".to_string(),
2126                    doc: None,
2127                    variants,
2128                }],
2129                callbacks: vec![],
2130                listeners: vec![],
2131                errors: None,
2132                modules: vec![],
2133            }],
2134            generators: None,
2135        };
2136        let warnings = collect_warnings(&api);
2137        assert!(!warnings
2138            .iter()
2139            .any(|w| matches!(w, ValidationWarning::LargeEnumVariantCount { .. })));
2140    }
2141
2142    #[test]
2143    fn warning_deep_nesting_in_param() {
2144        let deep = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
2145            Box::new(TypeRef::List(Box::new(TypeRef::I32))),
2146        )))));
2147        let api = Api {
2148            version: "0.1.0".to_string(),
2149            modules: vec![Module {
2150                name: "mymod".to_string(),
2151                functions: vec![Function {
2152                    name: "nested_fn".to_string(),
2153                    params: vec![Param {
2154                        name: "data".to_string(),
2155                        ty: deep,
2156                        mutable: false,
2157                        doc: None,
2158                    }],
2159                    returns: None,
2160                    doc: Some("documented".to_string()),
2161                    r#async: false,
2162                    cancellable: false,
2163                    deprecated: None,
2164                    since: None,
2165                }],
2166                structs: vec![],
2167                enums: vec![],
2168                callbacks: vec![],
2169                listeners: vec![],
2170                errors: None,
2171                modules: vec![],
2172            }],
2173            generators: None,
2174        };
2175        let warnings = collect_warnings(&api);
2176        assert!(warnings.iter().any(|w| matches!(
2177            w,
2178            ValidationWarning::DeepNesting { location, depth }
2179                if location == "mymod::nested_fn::data" && *depth == 4
2180        )));
2181    }
2182
2183    #[test]
2184    fn warning_nesting_at_3_no_warning() {
2185        let nested = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
2186            Box::new(TypeRef::I32),
2187        )))));
2188        let api = Api {
2189            version: "0.1.0".to_string(),
2190            modules: vec![Module {
2191                name: "mymod".to_string(),
2192                functions: vec![Function {
2193                    name: "ok_fn".to_string(),
2194                    params: vec![Param {
2195                        name: "data".to_string(),
2196                        ty: nested,
2197                        mutable: false,
2198                        doc: None,
2199                    }],
2200                    returns: None,
2201                    doc: Some("documented".to_string()),
2202                    r#async: false,
2203                    cancellable: false,
2204                    deprecated: None,
2205                    since: None,
2206                }],
2207                structs: vec![],
2208                enums: vec![],
2209                callbacks: vec![],
2210                listeners: vec![],
2211                errors: None,
2212                modules: vec![],
2213            }],
2214            generators: None,
2215        };
2216        let warnings = collect_warnings(&api);
2217        assert!(!warnings
2218            .iter()
2219            .any(|w| matches!(w, ValidationWarning::DeepNesting { .. })));
2220    }
2221
2222    #[test]
2223    fn warning_deep_nesting_in_struct_field() {
2224        let deep = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::Optional(
2225            Box::new(TypeRef::List(Box::new(TypeRef::I32))),
2226        )))));
2227        let api = Api {
2228            version: "0.1.0".to_string(),
2229            modules: vec![Module {
2230                name: "mymod".to_string(),
2231                functions: vec![simple_function("ok_fn")],
2232                structs: vec![StructDef {
2233                    name: "Widget".to_string(),
2234                    doc: None,
2235                    fields: vec![StructField {
2236                        name: "data".to_string(),
2237                        ty: deep,
2238                        doc: None,
2239                        default: None,
2240                    }],
2241                    builder: false,
2242                }],
2243                enums: vec![],
2244                callbacks: vec![],
2245                listeners: vec![],
2246                errors: None,
2247                modules: vec![],
2248            }],
2249            generators: None,
2250        };
2251        let warnings = collect_warnings(&api);
2252        assert!(warnings.iter().any(|w| matches!(
2253            w,
2254            ValidationWarning::DeepNesting { location, .. }
2255                if location == "mymod::Widget::data"
2256        )));
2257    }
2258
2259    #[test]
2260    fn warning_empty_module_doc() {
2261        let api = Api {
2262            version: "0.1.0".to_string(),
2263            modules: vec![Module {
2264                name: "undocumented".to_string(),
2265                functions: vec![
2266                    Function {
2267                        name: "a".to_string(),
2268                        params: vec![],
2269                        returns: None,
2270                        doc: None,
2271                        r#async: false,
2272                        cancellable: false,
2273                        deprecated: None,
2274                        since: None,
2275                    },
2276                    Function {
2277                        name: "b".to_string(),
2278                        params: vec![],
2279                        returns: None,
2280                        doc: None,
2281                        r#async: false,
2282                        cancellable: false,
2283                        deprecated: None,
2284                        since: None,
2285                    },
2286                ],
2287                structs: vec![],
2288                enums: vec![],
2289                callbacks: vec![],
2290                listeners: vec![],
2291                errors: None,
2292                modules: vec![],
2293            }],
2294            generators: None,
2295        };
2296        let warnings = collect_warnings(&api);
2297        assert!(warnings.iter().any(|w| matches!(
2298            w,
2299            ValidationWarning::EmptyModuleDoc { module } if module == "undocumented"
2300        )));
2301    }
2302
2303    #[test]
2304    fn warning_partial_docs_no_warning() {
2305        let api = Api {
2306            version: "0.1.0".to_string(),
2307            modules: vec![Module {
2308                name: "partial".to_string(),
2309                functions: vec![
2310                    Function {
2311                        name: "a".to_string(),
2312                        params: vec![],
2313                        returns: None,
2314                        doc: Some("has doc".to_string()),
2315                        r#async: false,
2316                        cancellable: false,
2317                        deprecated: None,
2318                        since: None,
2319                    },
2320                    Function {
2321                        name: "b".to_string(),
2322                        params: vec![],
2323                        returns: None,
2324                        doc: None,
2325                        r#async: false,
2326                        cancellable: false,
2327                        deprecated: None,
2328                        since: None,
2329                    },
2330                ],
2331                structs: vec![],
2332                enums: vec![],
2333                callbacks: vec![],
2334                listeners: vec![],
2335                errors: None,
2336                modules: vec![],
2337            }],
2338            generators: None,
2339        };
2340        let warnings = collect_warnings(&api);
2341        assert!(!warnings
2342            .iter()
2343            .any(|w| matches!(w, ValidationWarning::EmptyModuleDoc { .. })));
2344    }
2345
2346    #[test]
2347    fn warning_no_functions_no_empty_doc_warning() {
2348        let api = Api {
2349            version: "0.1.0".to_string(),
2350            modules: vec![Module {
2351                name: "empty".to_string(),
2352                functions: vec![],
2353                structs: vec![],
2354                enums: vec![],
2355                callbacks: vec![],
2356                listeners: vec![],
2357                errors: None,
2358                modules: vec![],
2359            }],
2360            generators: None,
2361        };
2362        let warnings = collect_warnings(&api);
2363        assert!(!warnings
2364            .iter()
2365            .any(|w| matches!(w, ValidationWarning::EmptyModuleDoc { .. })));
2366    }
2367
2368    #[test]
2369    fn warning_clean_api_no_warnings() {
2370        let api = Api {
2371            version: "0.1.0".to_string(),
2372            modules: vec![Module {
2373                name: "clean".to_string(),
2374                functions: vec![Function {
2375                    name: "add".to_string(),
2376                    params: vec![Param {
2377                        name: "x".to_string(),
2378                        ty: TypeRef::I32,
2379                        mutable: false,
2380                        doc: None,
2381                    }],
2382                    returns: Some(TypeRef::I32),
2383                    doc: Some("Adds numbers".to_string()),
2384                    r#async: false,
2385                    cancellable: false,
2386                    deprecated: None,
2387                    since: None,
2388                }],
2389                structs: vec![],
2390                enums: vec![simple_enum("Color")],
2391                callbacks: vec![],
2392                listeners: vec![],
2393                errors: None,
2394                modules: vec![],
2395            }],
2396            generators: None,
2397        };
2398        let warnings = collect_warnings(&api);
2399        assert!(warnings.is_empty());
2400    }
2401
2402    #[test]
2403    fn resolve_enum_ref_in_struct_field() {
2404        let mut api = Api {
2405            version: "0.1.0".to_string(),
2406            modules: vec![Module {
2407                name: "mymod".to_string(),
2408                functions: vec![simple_function("ok_fn")],
2409                structs: vec![StructDef {
2410                    name: "Widget".to_string(),
2411                    doc: None,
2412                    fields: vec![StructField {
2413                        name: "color".to_string(),
2414                        ty: TypeRef::Struct("Color".to_string()),
2415                        doc: None,
2416                        default: None,
2417                    }],
2418                    builder: false,
2419                }],
2420                enums: vec![simple_enum("Color")],
2421                callbacks: vec![],
2422                listeners: vec![],
2423                errors: None,
2424                modules: vec![],
2425            }],
2426            generators: None,
2427        };
2428        validate_api(&mut api, None).unwrap();
2429        assert_eq!(
2430            api.modules[0].structs[0].fields[0].ty,
2431            TypeRef::Enum("Color".to_string())
2432        );
2433    }
2434
2435    #[test]
2436    fn typed_handle_valid_struct_passes() {
2437        let mut api = Api {
2438            version: "0.1.0".to_string(),
2439            modules: vec![Module {
2440                name: "mymod".to_string(),
2441                functions: vec![Function {
2442                    name: "get_session".to_string(),
2443                    params: vec![Param {
2444                        name: "h".to_string(),
2445                        ty: TypeRef::TypedHandle("Session".to_string()),
2446                        mutable: false,
2447                        doc: None,
2448                    }],
2449                    returns: None,
2450                    doc: None,
2451                    r#async: false,
2452                    cancellable: false,
2453                    deprecated: None,
2454                    since: None,
2455                }],
2456                structs: vec![simple_struct("Session")],
2457                enums: vec![],
2458                callbacks: vec![],
2459                listeners: vec![],
2460                errors: None,
2461                modules: vec![],
2462            }],
2463            generators: None,
2464        };
2465        assert!(validate_api(&mut api, None).is_ok());
2466    }
2467
2468    #[test]
2469    fn typed_handle_unknown_struct_rejected() {
2470        let mut api = Api {
2471            version: "0.1.0".to_string(),
2472            modules: vec![Module {
2473                name: "mymod".to_string(),
2474                functions: vec![Function {
2475                    name: "get_session".to_string(),
2476                    params: vec![Param {
2477                        name: "h".to_string(),
2478                        ty: TypeRef::TypedHandle("Nonexistent".to_string()),
2479                        mutable: false,
2480                        doc: None,
2481                    }],
2482                    returns: None,
2483                    doc: None,
2484                    r#async: false,
2485                    cancellable: false,
2486                    deprecated: None,
2487                    since: None,
2488                }],
2489                structs: vec![],
2490                enums: vec![],
2491                callbacks: vec![],
2492                listeners: vec![],
2493                errors: None,
2494                modules: vec![],
2495            }],
2496            generators: None,
2497        };
2498        assert!(matches!(
2499            validate_api(&mut api, None).unwrap_err().error,
2500            ValidationError::UnknownTypeRef { name } if name == "Nonexistent"
2501        ));
2502    }
2503
2504    #[test]
2505    fn borrowed_str_param_accepted() {
2506        let mut api = Api {
2507            version: "0.1.0".to_string(),
2508            modules: vec![Module {
2509                name: "io".to_string(),
2510                functions: vec![Function {
2511                    name: "write".to_string(),
2512                    params: vec![Param {
2513                        name: "data".to_string(),
2514                        ty: TypeRef::BorrowedStr,
2515                        mutable: false,
2516                        doc: None,
2517                    }],
2518                    returns: None,
2519                    doc: None,
2520                    r#async: false,
2521                    cancellable: false,
2522                    deprecated: None,
2523                    since: None,
2524                }],
2525                structs: vec![],
2526                enums: vec![],
2527                callbacks: vec![],
2528                listeners: vec![],
2529                errors: None,
2530                modules: vec![],
2531            }],
2532            generators: None,
2533        };
2534        assert!(validate_api(&mut api, None).is_ok());
2535    }
2536
2537    #[test]
2538    fn borrowed_bytes_param_accepted() {
2539        let mut api = Api {
2540            version: "0.1.0".to_string(),
2541            modules: vec![Module {
2542                name: "io".to_string(),
2543                functions: vec![Function {
2544                    name: "upload".to_string(),
2545                    params: vec![Param {
2546                        name: "raw".to_string(),
2547                        ty: TypeRef::BorrowedBytes,
2548                        mutable: false,
2549                        doc: None,
2550                    }],
2551                    returns: None,
2552                    doc: None,
2553                    r#async: false,
2554                    cancellable: false,
2555                    deprecated: None,
2556                    since: None,
2557                }],
2558                structs: vec![],
2559                enums: vec![],
2560                callbacks: vec![],
2561                listeners: vec![],
2562                errors: None,
2563                modules: vec![],
2564            }],
2565            generators: None,
2566        };
2567        assert!(validate_api(&mut api, None).is_ok());
2568    }
2569
2570    #[test]
2571    fn borrowed_str_in_return_rejected() {
2572        let mut api = Api {
2573            version: "0.1.0".to_string(),
2574            modules: vec![Module {
2575                name: "io".to_string(),
2576                functions: vec![Function {
2577                    name: "read".to_string(),
2578                    params: vec![],
2579                    returns: Some(TypeRef::BorrowedStr),
2580                    doc: None,
2581                    r#async: false,
2582                    cancellable: false,
2583                    deprecated: None,
2584                    since: None,
2585                }],
2586                structs: vec![],
2587                enums: vec![],
2588                callbacks: vec![],
2589                listeners: vec![],
2590                errors: None,
2591                modules: vec![],
2592            }],
2593            generators: None,
2594        };
2595        assert!(matches!(
2596            validate_api(&mut api, None).unwrap_err().error,
2597            ValidationError::BorrowedTypeInInvalidPosition { ty, location }
2598                if ty == "&str" && location.contains("return type")
2599        ));
2600    }
2601
2602    #[test]
2603    fn borrowed_bytes_in_return_rejected() {
2604        let mut api = Api {
2605            version: "0.1.0".to_string(),
2606            modules: vec![Module {
2607                name: "io".to_string(),
2608                functions: vec![Function {
2609                    name: "read_raw".to_string(),
2610                    params: vec![],
2611                    returns: Some(TypeRef::BorrowedBytes),
2612                    doc: None,
2613                    r#async: false,
2614                    cancellable: false,
2615                    deprecated: None,
2616                    since: None,
2617                }],
2618                structs: vec![],
2619                enums: vec![],
2620                callbacks: vec![],
2621                listeners: vec![],
2622                errors: None,
2623                modules: vec![],
2624            }],
2625            generators: None,
2626        };
2627        assert!(matches!(
2628            validate_api(&mut api, None).unwrap_err().error,
2629            ValidationError::BorrowedTypeInInvalidPosition { ty, location }
2630                if ty == "&[u8]" && location.contains("return type")
2631        ));
2632    }
2633
2634    #[test]
2635    fn borrowed_str_in_struct_field_rejected() {
2636        let mut api = Api {
2637            version: "0.1.0".to_string(),
2638            modules: vec![Module {
2639                name: "data".to_string(),
2640                functions: vec![],
2641                structs: vec![StructDef {
2642                    name: "Msg".to_string(),
2643                    fields: vec![StructField {
2644                        name: "text".to_string(),
2645                        ty: TypeRef::BorrowedStr,
2646                        doc: None,
2647                        default: None,
2648                    }],
2649                    builder: false,
2650                    doc: None,
2651                }],
2652                enums: vec![],
2653                callbacks: vec![],
2654                listeners: vec![],
2655                errors: None,
2656                modules: vec![],
2657            }],
2658            generators: None,
2659        };
2660        assert!(matches!(
2661            validate_api(&mut api, None).unwrap_err().error,
2662            ValidationError::BorrowedTypeInInvalidPosition { ty, location }
2663                if ty == "&str" && location.contains("struct")
2664        ));
2665    }
2666
2667    #[test]
2668    fn borrowed_bytes_in_struct_field_rejected() {
2669        let mut api = Api {
2670            version: "0.1.0".to_string(),
2671            modules: vec![Module {
2672                name: "data".to_string(),
2673                functions: vec![],
2674                structs: vec![StructDef {
2675                    name: "Blob".to_string(),
2676                    fields: vec![StructField {
2677                        name: "content".to_string(),
2678                        ty: TypeRef::BorrowedBytes,
2679                        doc: None,
2680                        default: None,
2681                    }],
2682                    builder: false,
2683                    doc: None,
2684                }],
2685                enums: vec![],
2686                callbacks: vec![],
2687                listeners: vec![],
2688                errors: None,
2689                modules: vec![],
2690            }],
2691            generators: None,
2692        };
2693        assert!(matches!(
2694            validate_api(&mut api, None).unwrap_err().error,
2695            ValidationError::BorrowedTypeInInvalidPosition { ty, location }
2696                if ty == "&[u8]" && location.contains("struct")
2697        ));
2698    }
2699
2700    #[test]
2701    fn borrowed_str_nested_in_optional_return_rejected() {
2702        let mut api = Api {
2703            version: "0.1.0".to_string(),
2704            modules: vec![Module {
2705                name: "io".to_string(),
2706                functions: vec![Function {
2707                    name: "maybe_read".to_string(),
2708                    params: vec![],
2709                    returns: Some(TypeRef::Optional(Box::new(TypeRef::BorrowedStr))),
2710                    doc: None,
2711                    r#async: false,
2712                    cancellable: false,
2713                    deprecated: None,
2714                    since: None,
2715                }],
2716                structs: vec![],
2717                enums: vec![],
2718                callbacks: vec![],
2719                listeners: vec![],
2720                errors: None,
2721                modules: vec![],
2722            }],
2723            generators: None,
2724        };
2725        assert!(matches!(
2726            validate_api(&mut api, None).unwrap_err().error,
2727            ValidationError::BorrowedTypeInInvalidPosition { ty, .. }
2728                if ty == "&str"
2729        ));
2730    }
2731
2732    #[test]
2733    fn cross_module_struct_ref_passes() {
2734        let mut api = Api {
2735            version: "0.1.0".to_string(),
2736            modules: vec![
2737                Module {
2738                    name: "orders".to_string(),
2739                    functions: vec![Function {
2740                        name: "place_order".to_string(),
2741                        params: vec![Param {
2742                            name: "item".to_string(),
2743                            ty: TypeRef::Struct("Product".to_string()),
2744                            mutable: false,
2745                            doc: None,
2746                        }],
2747                        returns: None,
2748                        doc: None,
2749                        r#async: false,
2750                        cancellable: false,
2751                        deprecated: None,
2752                        since: None,
2753                    }],
2754                    structs: vec![],
2755                    enums: vec![],
2756                    callbacks: vec![],
2757                    listeners: vec![],
2758                    errors: None,
2759                    modules: vec![],
2760                },
2761                Module {
2762                    name: "catalog".to_string(),
2763                    functions: vec![simple_function("list_products")],
2764                    structs: vec![simple_struct("Product")],
2765                    enums: vec![],
2766                    callbacks: vec![],
2767                    listeners: vec![],
2768                    errors: None,
2769                    modules: vec![],
2770                },
2771            ],
2772            generators: None,
2773        };
2774        validate_api(&mut api, None).unwrap();
2775        assert_eq!(
2776            api.modules[0].functions[0].params[0].ty,
2777            TypeRef::Struct("catalog.Product".to_string())
2778        );
2779    }
2780
2781    #[test]
2782    fn cross_module_enum_ref_passes() {
2783        let mut api = Api {
2784            version: "0.1.0".to_string(),
2785            modules: vec![
2786                Module {
2787                    name: "orders".to_string(),
2788                    functions: vec![Function {
2789                        name: "get_status".to_string(),
2790                        params: vec![],
2791                        returns: Some(TypeRef::Struct("Status".to_string())),
2792                        doc: None,
2793                        r#async: false,
2794                        cancellable: false,
2795                        deprecated: None,
2796                        since: None,
2797                    }],
2798                    structs: vec![],
2799                    enums: vec![],
2800                    callbacks: vec![],
2801                    listeners: vec![],
2802                    errors: None,
2803                    modules: vec![],
2804                },
2805                Module {
2806                    name: "shared".to_string(),
2807                    functions: vec![simple_function("noop")],
2808                    structs: vec![],
2809                    enums: vec![simple_enum("Status")],
2810                    callbacks: vec![],
2811                    listeners: vec![],
2812                    errors: None,
2813                    modules: vec![],
2814                },
2815            ],
2816            generators: None,
2817        };
2818        validate_api(&mut api, None).unwrap();
2819        assert_eq!(
2820            api.modules[0].functions[0].returns,
2821            Some(TypeRef::Enum("shared.Status".to_string()))
2822        );
2823    }
2824
2825    #[test]
2826    fn cross_module_unknown_still_rejected() {
2827        let mut api = Api {
2828            version: "0.1.0".to_string(),
2829            modules: vec![
2830                Module {
2831                    name: "orders".to_string(),
2832                    functions: vec![Function {
2833                        name: "do_stuff".to_string(),
2834                        params: vec![Param {
2835                            name: "x".to_string(),
2836                            ty: TypeRef::Struct("Nonexistent".to_string()),
2837                            mutable: false,
2838                            doc: None,
2839                        }],
2840                        returns: None,
2841                        doc: None,
2842                        r#async: false,
2843                        cancellable: false,
2844                        deprecated: None,
2845                        since: None,
2846                    }],
2847                    structs: vec![],
2848                    enums: vec![],
2849                    callbacks: vec![],
2850                    listeners: vec![],
2851                    errors: None,
2852                    modules: vec![],
2853                },
2854                Module {
2855                    name: "catalog".to_string(),
2856                    functions: vec![simple_function("list_products")],
2857                    structs: vec![simple_struct("Product")],
2858                    enums: vec![],
2859                    callbacks: vec![],
2860                    listeners: vec![],
2861                    errors: None,
2862                    modules: vec![],
2863                },
2864            ],
2865            generators: None,
2866        };
2867        assert!(matches!(
2868            validate_api(&mut api, None).unwrap_err().error,
2869            ValidationError::UnknownTypeRef { name } if name == "Nonexistent"
2870        ));
2871    }
2872
2873    #[test]
2874    fn find_type_in_api_finds_struct() {
2875        let api = Api {
2876            version: "0.1.0".to_string(),
2877            modules: vec![Module {
2878                name: "catalog".to_string(),
2879                functions: vec![],
2880                structs: vec![simple_struct("Product")],
2881                enums: vec![],
2882                callbacks: vec![],
2883                listeners: vec![],
2884                errors: None,
2885                modules: vec![],
2886            }],
2887            generators: None,
2888        };
2889        let result = find_type_in_api(&api, "Product");
2890        assert_eq!(result, Some(("catalog".to_string(), false)));
2891    }
2892
2893    #[test]
2894    fn find_type_in_api_finds_enum() {
2895        let api = Api {
2896            version: "0.1.0".to_string(),
2897            modules: vec![Module {
2898                name: "shared".to_string(),
2899                functions: vec![],
2900                structs: vec![],
2901                enums: vec![simple_enum("Status")],
2902                callbacks: vec![],
2903                listeners: vec![],
2904                errors: None,
2905                modules: vec![],
2906            }],
2907            generators: None,
2908        };
2909        let result = find_type_in_api(&api, "Status");
2910        assert_eq!(result, Some(("shared".to_string(), true)));
2911    }
2912
2913    #[test]
2914    fn find_type_in_api_returns_none_for_unknown() {
2915        let api = Api {
2916            version: "0.1.0".to_string(),
2917            modules: vec![simple_module("mymod")],
2918            generators: None,
2919        };
2920        assert_eq!(find_type_in_api(&api, "Nonexistent"), None);
2921    }
2922
2923    #[test]
2924    fn validate_nested_module_passes() {
2925        let mut api = Api {
2926            version: "0.1.0".to_string(),
2927            modules: vec![Module {
2928                name: "parent".to_string(),
2929                functions: vec![simple_function("top_fn")],
2930                structs: vec![],
2931                enums: vec![],
2932                callbacks: vec![],
2933                listeners: vec![],
2934                errors: None,
2935                modules: vec![Module {
2936                    name: "child".to_string(),
2937                    functions: vec![simple_function("inner_fn")],
2938                    structs: vec![],
2939                    enums: vec![],
2940                    callbacks: vec![],
2941                    listeners: vec![],
2942                    errors: None,
2943                    modules: vec![],
2944                }],
2945            }],
2946            generators: None,
2947        };
2948        assert!(validate_api(&mut api, None).is_ok());
2949    }
2950
2951    fn function_returning(name: &str, ret: TypeRef) -> Function {
2952        Function {
2953            name: name.to_string(),
2954            params: vec![],
2955            returns: Some(ret),
2956            doc: None,
2957            r#async: false,
2958            cancellable: false,
2959            deprecated: None,
2960            since: None,
2961        }
2962    }
2963
2964    #[test]
2965    fn find_type_in_api_finds_nested_type() {
2966        let api = Api {
2967            version: "0.1.0".to_string(),
2968            modules: vec![Module {
2969                name: "graphics".to_string(),
2970                functions: vec![],
2971                structs: vec![],
2972                enums: vec![],
2973                callbacks: vec![],
2974                listeners: vec![],
2975                errors: None,
2976                modules: vec![Module {
2977                    name: "shapes".to_string(),
2978                    functions: vec![],
2979                    structs: vec![simple_struct("Circle")],
2980                    enums: vec![],
2981                    callbacks: vec![],
2982                    listeners: vec![],
2983                    errors: None,
2984                    modules: vec![],
2985                }],
2986            }],
2987            generators: None,
2988        };
2989        // Nested types report their full dot-joined owner path.
2990        assert_eq!(
2991            find_type_in_api(&api, "Circle"),
2992            Some(("graphics.shapes".to_string(), false))
2993        );
2994    }
2995
2996    #[test]
2997    fn resolve_qualifies_reference_to_nested_module_type() {
2998        // A top-level module references a struct defined in one of its own
2999        // nested submodules by bare name; resolution must qualify it to the
3000        // full dotted path so codegen can mangle the right C symbol.
3001        let mut api = Api {
3002            version: "0.1.0".to_string(),
3003            modules: vec![Module {
3004                name: "app".to_string(),
3005                functions: vec![function_returning(
3006                    "make",
3007                    TypeRef::Struct("Widget".to_string()),
3008                )],
3009                structs: vec![],
3010                enums: vec![],
3011                callbacks: vec![],
3012                listeners: vec![],
3013                errors: None,
3014                modules: vec![Module {
3015                    name: "ui".to_string(),
3016                    functions: vec![],
3017                    structs: vec![simple_struct("Widget")],
3018                    enums: vec![],
3019                    callbacks: vec![],
3020                    listeners: vec![],
3021                    errors: None,
3022                    modules: vec![],
3023                }],
3024            }],
3025            generators: None,
3026        };
3027        validate_api(&mut api, None).unwrap();
3028        assert_eq!(
3029            api.modules[0].functions[0].returns,
3030            Some(TypeRef::Struct("app.ui.Widget".to_string()))
3031        );
3032    }
3033
3034    #[test]
3035    fn resolve_qualifies_nested_module_reference_to_parent_type() {
3036        // A nested module references a type owned by an ancestor module. Before
3037        // the nested-aware resolver, refs inside submodules were never
3038        // qualified at all; now they resolve to the owner's dotted path.
3039        let mut api = Api {
3040            version: "0.1.0".to_string(),
3041            modules: vec![Module {
3042                name: "lib".to_string(),
3043                functions: vec![],
3044                structs: vec![simple_struct("Token")],
3045                enums: vec![],
3046                callbacks: vec![],
3047                listeners: vec![],
3048                errors: None,
3049                modules: vec![Module {
3050                    name: "inner".to_string(),
3051                    functions: vec![function_returning(
3052                        "fetch",
3053                        TypeRef::Struct("Token".to_string()),
3054                    )],
3055                    structs: vec![],
3056                    enums: vec![],
3057                    callbacks: vec![],
3058                    listeners: vec![],
3059                    errors: None,
3060                    modules: vec![],
3061                }],
3062            }],
3063            generators: None,
3064        };
3065        validate_api(&mut api, None).unwrap();
3066        assert_eq!(
3067            api.modules[0].modules[0].functions[0].returns,
3068            Some(TypeRef::Struct("lib.Token".to_string()))
3069        );
3070    }
3071
3072    #[test]
3073    fn resolve_converts_nested_enum_reference_to_enum_variant() {
3074        // An unqualified reference to an enum that lives in another module must
3075        // be rewritten as `TypeRef::Enum` (not `Struct`) with the dotted path,
3076        // using the global index's `is_enum` flag.
3077        let mut api = Api {
3078            version: "0.1.0".to_string(),
3079            modules: vec![
3080                Module {
3081                    name: "consumer".to_string(),
3082                    functions: vec![function_returning(
3083                        "status",
3084                        TypeRef::Struct("Phase".to_string()),
3085                    )],
3086                    structs: vec![],
3087                    enums: vec![],
3088                    callbacks: vec![],
3089                    listeners: vec![],
3090                    errors: None,
3091                    modules: vec![],
3092                },
3093                Module {
3094                    name: "shared".to_string(),
3095                    functions: vec![],
3096                    structs: vec![],
3097                    enums: vec![simple_enum("Phase")],
3098                    callbacks: vec![],
3099                    listeners: vec![],
3100                    errors: None,
3101                    modules: vec![],
3102                },
3103            ],
3104            generators: None,
3105        };
3106        validate_api(&mut api, None).unwrap();
3107        assert_eq!(
3108            api.modules[0].functions[0].returns,
3109            Some(TypeRef::Enum("shared.Phase".to_string()))
3110        );
3111    }
3112
3113    #[test]
3114    fn duplicate_callback_names_rejected() {
3115        let mut api = Api {
3116            version: "0.1.0".to_string(),
3117            modules: vec![Module {
3118                name: "events".to_string(),
3119                functions: vec![],
3120                structs: vec![],
3121                enums: vec![],
3122                callbacks: vec![
3123                    CallbackDef {
3124                        name: "on_data".to_string(),
3125                        params: vec![],
3126                        doc: None,
3127                    },
3128                    CallbackDef {
3129                        name: "on_data".to_string(),
3130                        params: vec![],
3131                        doc: None,
3132                    },
3133                ],
3134                listeners: vec![],
3135                errors: None,
3136                modules: vec![],
3137            }],
3138            generators: None,
3139        };
3140        assert!(matches!(
3141            validate_api(&mut api, None).unwrap_err().error,
3142            ValidationError::DuplicateCallbackName { module, name }
3143                if module == "events" && name == "on_data"
3144        ));
3145    }
3146
3147    #[test]
3148    fn listener_referencing_undefined_callback_rejected() {
3149        let mut api = Api {
3150            version: "0.1.0".to_string(),
3151            modules: vec![Module {
3152                name: "events".to_string(),
3153                functions: vec![],
3154                structs: vec![],
3155                enums: vec![],
3156                callbacks: vec![],
3157                listeners: vec![ListenerDef {
3158                    name: "watcher".to_string(),
3159                    event_callback: "nonexistent".to_string(),
3160                    doc: None,
3161                }],
3162                errors: None,
3163                modules: vec![],
3164            }],
3165            generators: None,
3166        };
3167        assert!(matches!(
3168            validate_api(&mut api, None).unwrap_err().error,
3169            ValidationError::ListenerCallbackNotFound { module, listener, callback }
3170                if module == "events" && listener == "watcher" && callback == "nonexistent"
3171        ));
3172    }
3173
3174    #[test]
3175    fn listener_referencing_defined_callback_passes() {
3176        let mut api = Api {
3177            version: "0.1.0".to_string(),
3178            modules: vec![Module {
3179                name: "events".to_string(),
3180                functions: vec![],
3181                structs: vec![],
3182                enums: vec![],
3183                callbacks: vec![CallbackDef {
3184                    name: "on_data".to_string(),
3185                    params: vec![Param {
3186                        name: "payload".to_string(),
3187                        ty: TypeRef::StringUtf8,
3188                        mutable: false,
3189                        doc: None,
3190                    }],
3191                    doc: None,
3192                }],
3193                listeners: vec![ListenerDef {
3194                    name: "data_stream".to_string(),
3195                    event_callback: "on_data".to_string(),
3196                    doc: Some("Subscribe to data".to_string()),
3197                }],
3198                errors: None,
3199                modules: vec![],
3200            }],
3201            generators: None,
3202        };
3203        assert!(validate_api(&mut api, None).is_ok());
3204    }
3205
3206    #[test]
3207    fn duplicate_listener_names_rejected() {
3208        let mut api = Api {
3209            version: "0.1.0".to_string(),
3210            modules: vec![Module {
3211                name: "events".to_string(),
3212                functions: vec![],
3213                structs: vec![],
3214                enums: vec![],
3215                callbacks: vec![CallbackDef {
3216                    name: "on_data".to_string(),
3217                    params: vec![],
3218                    doc: None,
3219                }],
3220                listeners: vec![
3221                    ListenerDef {
3222                        name: "watcher".to_string(),
3223                        event_callback: "on_data".to_string(),
3224                        doc: None,
3225                    },
3226                    ListenerDef {
3227                        name: "watcher".to_string(),
3228                        event_callback: "on_data".to_string(),
3229                        doc: None,
3230                    },
3231                ],
3232                errors: None,
3233                modules: vec![],
3234            }],
3235            generators: None,
3236        };
3237        assert!(matches!(
3238            validate_api(&mut api, None).unwrap_err().error,
3239            ValidationError::DuplicateListenerName { module, name }
3240                if module == "events" && name == "watcher"
3241        ));
3242    }
3243
3244    #[test]
3245    fn iterator_valid_as_return_type() {
3246        let mut api = Api {
3247            version: "0.2.0".to_string(),
3248            modules: vec![Module {
3249                name: "data".to_string(),
3250                functions: vec![Function {
3251                    name: "list_items".to_string(),
3252                    params: vec![],
3253                    returns: Some(TypeRef::Iterator(Box::new(TypeRef::I32))),
3254                    doc: None,
3255                    r#async: false,
3256                    cancellable: false,
3257                    deprecated: None,
3258                    since: None,
3259                }],
3260                structs: vec![],
3261                enums: vec![],
3262                callbacks: vec![],
3263                listeners: vec![],
3264                errors: None,
3265                modules: vec![],
3266            }],
3267            generators: None,
3268        };
3269        assert!(validate_api(&mut api, None).is_ok());
3270    }
3271
3272    #[test]
3273    fn iterator_rejected_as_param() {
3274        let mut api = Api {
3275            version: "0.2.0".to_string(),
3276            modules: vec![Module {
3277                name: "data".to_string(),
3278                functions: vec![Function {
3279                    name: "consume".to_string(),
3280                    params: vec![Param {
3281                        name: "items".to_string(),
3282                        ty: TypeRef::Iterator(Box::new(TypeRef::I32)),
3283                        mutable: false,
3284                        doc: None,
3285                    }],
3286                    returns: None,
3287                    doc: None,
3288                    r#async: false,
3289                    cancellable: false,
3290                    deprecated: None,
3291                    since: None,
3292                }],
3293                structs: vec![],
3294                enums: vec![],
3295                callbacks: vec![],
3296                listeners: vec![],
3297                errors: None,
3298                modules: vec![],
3299            }],
3300            generators: None,
3301        };
3302        assert!(matches!(
3303            validate_api(&mut api, None).unwrap_err().error,
3304            ValidationError::IteratorInInvalidPosition { .. }
3305        ));
3306    }
3307
3308    #[test]
3309    fn iterator_rejected_in_struct_field() {
3310        let mut api = Api {
3311            version: "0.2.0".to_string(),
3312            modules: vec![Module {
3313                name: "data".to_string(),
3314                functions: vec![],
3315                structs: vec![StructDef {
3316                    name: "Container".to_string(),
3317                    doc: None,
3318                    fields: vec![StructField {
3319                        name: "items".to_string(),
3320                        ty: TypeRef::Iterator(Box::new(TypeRef::I32)),
3321                        doc: None,
3322                        default: None,
3323                    }],
3324                    builder: false,
3325                }],
3326                enums: vec![],
3327                callbacks: vec![],
3328                listeners: vec![],
3329                errors: None,
3330                modules: vec![],
3331            }],
3332            generators: None,
3333        };
3334        assert!(matches!(
3335            validate_api(&mut api, None).unwrap_err().error,
3336            ValidationError::IteratorInInvalidPosition { .. }
3337        ));
3338    }
3339
3340    #[test]
3341    fn builder_struct_empty_is_error() {
3342        let mut api = Api {
3343            version: "0.2.0".to_string(),
3344            modules: vec![Module {
3345                name: "m".into(),
3346                functions: vec![],
3347                structs: vec![StructDef {
3348                    name: "Empty".into(),
3349                    doc: None,
3350                    fields: vec![],
3351                    builder: true,
3352                }],
3353                enums: vec![],
3354                callbacks: vec![],
3355                listeners: vec![],
3356                errors: None,
3357                modules: vec![],
3358            }],
3359            generators: None,
3360        };
3361        let err = validate_api(&mut api, None).unwrap_err();
3362        assert!(
3363            matches!(err.error, ValidationError::BuilderStructEmpty { .. }),
3364            "expected BuilderStructEmpty, got: {err}"
3365        );
3366    }
3367
3368    #[test]
3369    fn warning_mutable_on_value_type() {
3370        let api = Api {
3371            version: "0.1.0".to_string(),
3372            modules: vec![Module {
3373                name: "math".to_string(),
3374                functions: vec![Function {
3375                    name: "add".to_string(),
3376                    params: vec![Param {
3377                        name: "x".to_string(),
3378                        ty: TypeRef::I32,
3379                        mutable: true,
3380                        doc: None,
3381                    }],
3382                    returns: Some(TypeRef::I32),
3383                    doc: Some("add".to_string()),
3384                    r#async: false,
3385                    cancellable: false,
3386                    deprecated: None,
3387                    since: None,
3388                }],
3389                structs: vec![],
3390                enums: vec![],
3391                callbacks: vec![],
3392                listeners: vec![],
3393                errors: None,
3394                modules: vec![],
3395            }],
3396            generators: None,
3397        };
3398        let warnings = collect_warnings(&api);
3399        assert!(warnings.iter().any(|w| matches!(
3400            w,
3401            ValidationWarning::MutableOnValueType {
3402                param,
3403                ..
3404            } if param == "x"
3405        )));
3406    }
3407
3408    #[test]
3409    fn no_warning_mutable_on_pointer_type() {
3410        let api = Api {
3411            version: "0.1.0".to_string(),
3412            modules: vec![Module {
3413                name: "io".to_string(),
3414                functions: vec![Function {
3415                    name: "fill".to_string(),
3416                    params: vec![
3417                        Param {
3418                            name: "buf".to_string(),
3419                            ty: TypeRef::Bytes,
3420                            mutable: true,
3421                            doc: None,
3422                        },
3423                        Param {
3424                            name: "msg".to_string(),
3425                            ty: TypeRef::StringUtf8,
3426                            mutable: true,
3427                            doc: None,
3428                        },
3429                        Param {
3430                            name: "obj".to_string(),
3431                            ty: TypeRef::Struct("Thing".into()),
3432                            mutable: true,
3433                            doc: None,
3434                        },
3435                    ],
3436                    returns: None,
3437                    doc: Some("fill".to_string()),
3438                    r#async: false,
3439                    cancellable: false,
3440                    deprecated: None,
3441                    since: None,
3442                }],
3443                structs: vec![],
3444                enums: vec![],
3445                callbacks: vec![],
3446                listeners: vec![],
3447                errors: None,
3448                modules: vec![],
3449            }],
3450            generators: None,
3451        };
3452        let warnings = collect_warnings(&api);
3453        assert!(
3454            !warnings
3455                .iter()
3456                .any(|w| matches!(w, ValidationWarning::MutableOnValueType { .. })),
3457            "pointer types should not trigger mutable warning"
3458        );
3459    }
3460
3461    #[test]
3462    fn no_warning_mutable_false_on_value_type() {
3463        let api = Api {
3464            version: "0.1.0".to_string(),
3465            modules: vec![Module {
3466                name: "math".to_string(),
3467                functions: vec![Function {
3468                    name: "add".to_string(),
3469                    params: vec![Param {
3470                        name: "x".to_string(),
3471                        ty: TypeRef::I32,
3472                        mutable: false,
3473                        doc: None,
3474                    }],
3475                    returns: Some(TypeRef::I32),
3476                    doc: Some("add".to_string()),
3477                    r#async: false,
3478                    cancellable: false,
3479                    deprecated: None,
3480                    since: None,
3481                }],
3482                structs: vec![],
3483                enums: vec![],
3484                callbacks: vec![],
3485                listeners: vec![],
3486                errors: None,
3487                modules: vec![],
3488            }],
3489            generators: None,
3490        };
3491        let warnings = collect_warnings(&api);
3492        assert!(
3493            !warnings
3494                .iter()
3495                .any(|w| matches!(w, ValidationWarning::MutableOnValueType { .. })),
3496            "mutable=false should not trigger warning"
3497        );
3498    }
3499
3500    #[test]
3501    fn warning_mutable_on_enum_type() {
3502        let api = Api {
3503            version: "0.1.0".to_string(),
3504            modules: vec![Module {
3505                name: "paint".to_string(),
3506                functions: vec![Function {
3507                    name: "set_color".to_string(),
3508                    params: vec![Param {
3509                        name: "color".to_string(),
3510                        ty: TypeRef::Enum("Color".into()),
3511                        mutable: true,
3512                        doc: None,
3513                    }],
3514                    returns: None,
3515                    doc: Some("set".to_string()),
3516                    r#async: false,
3517                    cancellable: false,
3518                    deprecated: None,
3519                    since: None,
3520                }],
3521                structs: vec![],
3522                enums: vec![],
3523                callbacks: vec![],
3524                listeners: vec![],
3525                errors: None,
3526                modules: vec![],
3527            }],
3528            generators: None,
3529        };
3530        let warnings = collect_warnings(&api);
3531        assert!(warnings.iter().any(|w| matches!(
3532            w,
3533            ValidationWarning::MutableOnValueType { param, .. } if param == "color"
3534        )));
3535    }
3536
3537    #[test]
3538    fn warning_deprecated_function() {
3539        let api = Api {
3540            version: "0.2.0".to_string(),
3541            modules: vec![Module {
3542                name: "math".to_string(),
3543                functions: vec![Function {
3544                    name: "add_old".to_string(),
3545                    params: vec![],
3546                    returns: Some(TypeRef::I32),
3547                    doc: Some("old add".to_string()),
3548                    r#async: false,
3549                    cancellable: false,
3550                    deprecated: Some("Use add_v2 instead".to_string()),
3551                    since: Some("0.1.0".to_string()),
3552                }],
3553                structs: vec![],
3554                enums: vec![],
3555                callbacks: vec![],
3556                listeners: vec![],
3557                errors: None,
3558                modules: vec![],
3559            }],
3560            generators: None,
3561        };
3562        let warnings = collect_warnings(&api);
3563        assert!(warnings.iter().any(|w| matches!(
3564            w,
3565            ValidationWarning::DeprecatedFunction { function, message, .. }
3566                if function == "add_old" && message == "Use add_v2 instead"
3567        )));
3568    }
3569
3570    #[test]
3571    fn no_warning_for_non_deprecated_function() {
3572        let api = Api {
3573            version: "0.2.0".to_string(),
3574            modules: vec![Module {
3575                name: "math".to_string(),
3576                functions: vec![Function {
3577                    name: "add".to_string(),
3578                    params: vec![],
3579                    returns: Some(TypeRef::I32),
3580                    doc: Some("add things".to_string()),
3581                    r#async: false,
3582                    cancellable: false,
3583                    deprecated: None,
3584                    since: None,
3585                }],
3586                structs: vec![],
3587                enums: vec![],
3588                callbacks: vec![],
3589                listeners: vec![],
3590                errors: None,
3591                modules: vec![],
3592            }],
3593            generators: None,
3594        };
3595        let warnings = collect_warnings(&api);
3596        assert!(!warnings
3597            .iter()
3598            .any(|w| matches!(w, ValidationWarning::DeprecatedFunction { .. })));
3599    }
3600}