Skip to main content

xsd_schema/schema/
derivation.rs

1//! Type derivation validation
2//!
3//! This module validates type derivation rules according to the XSD specification.
4//! It is run after reference resolution (Task 3.1) and dependency graph construction
5//! (Task 3.2), using the topological order to process types in correct order.
6//!
7//! # Validation Rules
8//!
9//! ## Simple Type Derivation
10//!
11//! - **Restriction**: Derived facets must be more restrictive than base facets
12//! - **List**: Item type must be atomic (not list or union of lists)
13//! - **Union**: Member types must be simple types
14//!
15//! ## Complex Type Derivation
16//!
17//! - **Extension**: Base type content + new content must be valid
18//! - **Restriction**: Content model must be valid restriction of base content model
19//!
20//! # XSD Constraint IDs
21//!
22//! - `cos-st-restricts` - Derivation Valid (Restriction, Simple)
23//! - `cos-list-of-atomic` - List item type must be atomic
24//! - `cos-union-memberTypes` - Union member types must be simple
25//! - `cos-ct-extends` - Complex Type Derivation OK (Extension)
26//! - `derivation-ok-restriction` - Complex Type Derivation OK (Restriction)
27
28use crate::error::{SchemaError, SchemaResult};
29use crate::ids::{
30    AttributeGroupKey, AttributeKey, ComplexTypeKey, ElementKey, NameId, SimpleTypeKey, TypeKey,
31};
32use crate::parser::frames::{
33    AttributeUseKind, ComplexContentResult, Compositor, DerivationMethod, ElementFrameResult,
34    ModelGroupDefResult, ParticleResult, ParticleTerm, ProcessContents, SimpleTypeVariety,
35    WildcardNamespace, WildcardResult,
36};
37#[cfg(feature = "xsd11")]
38use crate::parser::frames::{OpenContentMode, OpenContentResult};
39use crate::parser::location::{SourceLocation, SourceRef};
40use crate::schema::dependencies::DependencyGraph;
41use crate::schema::model::DerivationSet;
42use crate::schema::SchemaSet;
43use crate::types::facets::{FacetKind, FacetSet};
44
45/// Statistics from derivation validation
46#[derive(Debug, Default)]
47pub struct DerivationStats {
48    /// Number of simple types validated
49    pub simple_types_validated: usize,
50    /// Number of complex types validated
51    pub complex_types_validated: usize,
52    /// Number of list types validated
53    pub list_types_validated: usize,
54    /// Number of union types validated
55    pub union_types_validated: usize,
56    /// Number of restriction derivations validated
57    pub restrictions_validated: usize,
58    /// Number of extension derivations validated
59    pub extensions_validated: usize,
60    /// Number of errors encountered
61    pub errors: usize,
62}
63
64/// Validate all type derivations in a schema set
65///
66/// Uses the dependency graph to process types in topological order,
67/// ensuring base types are validated before derived types.
68///
69/// # Arguments
70///
71/// * `schema_set` - The schema set with resolved references
72/// * `dep_graph` - The dependency graph with sorted types
73///
74/// # Errors
75///
76/// Returns the first error encountered. All errors have source locations.
77pub fn validate_all_derivations(
78    schema_set: &SchemaSet,
79    dep_graph: &DependencyGraph,
80) -> SchemaResult<DerivationStats> {
81    let mut stats = DerivationStats::default();
82    let mut errors: Vec<SchemaError> = Vec::new();
83
84    // Process types in compilation order (dependencies first)
85    for &type_key in dep_graph.compilation_order() {
86        match type_key {
87            TypeKey::Simple(key) => {
88                if let Err(e) = validate_simple_type(schema_set, key, &mut stats) {
89                    errors.push(e);
90                    stats.errors += 1;
91                }
92            }
93            TypeKey::Complex(key) => {
94                if let Err(e) = validate_complex_type(schema_set, key, &mut stats) {
95                    errors.push(e);
96                    stats.errors += 1;
97                }
98            }
99        }
100    }
101
102    // §src-redefine 6.2.2 / 7.2.2 deferred restriction checks — must run
103    // after reference resolution and type-derivation passes, because
104    // `resolved_particle_types` / `resolved_attributes` on the flagged
105    // groups are only populated post-resolve.
106    validate_all_redefine_group_restrictions(schema_set, &mut errors, &mut stats);
107    validate_all_redefine_attribute_group_restrictions(schema_set, &mut errors, &mut stats);
108
109    // src-attribute_group circularity: an attribute group cannot transitively
110    // reference itself in XSD 1.0. XSD 1.1 explicitly relaxed this — circular
111    // attribute groups are permitted (W3C Bugzilla 15795). Walks `resolved_ref`
112    // and `resolved_attribute_groups` for each group via DFS and flags any
113    // back-edge in XSD 1.0 mode only.
114    if schema_set.is_xsd10() {
115        validate_attribute_group_no_circular(schema_set, &mut errors);
116    } else {
117        // XSD 1.1: circular attribute groups are allowed in general, but a
118        // schema-level `defaultAttributes` group cannot itself participate in
119        // a cycle — `resolve_all_references` injects the resolved group into
120        // every applicable complex type, so a cycle here would imply the
121        // schema-for-schemas validity rule §3.6.3 (no cycles via the
122        // defaulting closure). Targeted DFS only on each document's selected
123        // defaultAttributes group avoids re-enabling the global ban.
124        validate_default_attribute_groups_no_circular(schema_set, &mut errors);
125    }
126
127    // Return first error if any
128    if let Some(first_error) = errors.into_iter().next() {
129        return Err(first_error);
130    }
131
132    Ok(stats)
133}
134
135/// Walk the attribute-group reference DAG and report cycles.
136///
137/// Each `xs:attributeGroup` definition references zero or more nested
138/// attribute groups (including self via `ref` or `<attributeGroup ref=...>`
139/// children). The schema-for-schemas does not allow circular references —
140/// `src-attribute_group` constraint 3 in XSD 1.0 / §3.6.3 in XSD 1.1 forbid
141/// any group from transitively referencing itself.
142fn validate_attribute_group_no_circular(schema_set: &SchemaSet, errors: &mut Vec<SchemaError>) {
143    use std::collections::HashSet;
144
145    // Iterate every attribute group key once.
146    let keys: Vec<_> = schema_set.arenas.attribute_groups.keys().collect();
147
148    for start in keys {
149        let mut path: Vec<AttributeGroupKey> = Vec::new();
150        let mut visited: HashSet<AttributeGroupKey> = HashSet::new();
151        if let Some(cycle_key) =
152            find_attribute_group_cycle(schema_set, start, &mut path, &mut visited)
153        {
154            let location = schema_set
155                .arenas
156                .attribute_groups
157                .get(cycle_key)
158                .and_then(|ag| ag.source.as_ref())
159                .and_then(|s| schema_set.source_maps.locate(s));
160            errors.push(SchemaError::structural(
161                "src-attribute_group",
162                "Circular attribute group reference detected",
163                location,
164            ));
165        }
166    }
167}
168
169/// DFS helper for `validate_attribute_group_no_circular`. Returns the key
170/// involved in the cycle (the first repeated node on the stack) when one is
171/// found.
172fn find_attribute_group_cycle(
173    schema_set: &SchemaSet,
174    key: AttributeGroupKey,
175    path: &mut Vec<AttributeGroupKey>,
176    visited: &mut std::collections::HashSet<AttributeGroupKey>,
177) -> Option<AttributeGroupKey> {
178    if path.contains(&key) {
179        return Some(key);
180    }
181    if !visited.insert(key) {
182        return None;
183    }
184    path.push(key);
185
186    let result = if let Some(ag) = schema_set.arenas.attribute_groups.get(key) {
187        let mut found: Option<AttributeGroupKey> = None;
188        if let Some(ref_key) = ag.resolved_ref {
189            if let Some(c) = find_attribute_group_cycle(schema_set, ref_key, path, visited) {
190                found = Some(c);
191            }
192        }
193        if found.is_none() {
194            for &nested_key in &ag.resolved_attribute_groups {
195                if let Some(c) = find_attribute_group_cycle(schema_set, nested_key, path, visited) {
196                    found = Some(c);
197                    break;
198                }
199            }
200        }
201        found
202    } else {
203        None
204    };
205
206    path.pop();
207    result
208}
209
210/// XSD 1.1: Validate that the schema-level `defaultAttributes` selected groups
211/// are not part of a circular reference chain. Targeted to the defaultAttributes
212/// closure only — XSD 1.1 (W3C Bugzilla 15795) permits circular AGs in general,
213/// but the defaulting injection in `resolver::resolve_all_references` would
214/// loop forever if its starting group were itself cyclic.
215fn validate_default_attribute_groups_no_circular(
216    schema_set: &SchemaSet,
217    errors: &mut Vec<SchemaError>,
218) {
219    use std::collections::HashSet;
220    let mut seen_starts: HashSet<AttributeGroupKey> = HashSet::new();
221    for doc in &schema_set.documents {
222        let Some(ref qname) = doc.default_attributes else {
223            continue;
224        };
225        let Some(start) = schema_set.lookup_attribute_group(qname.namespace_uri, qname.local_name)
226        else {
227            continue; // unresolvable defaultAttributes already reported elsewhere
228        };
229        if !seen_starts.insert(start) {
230            continue;
231        }
232        let mut path: Vec<AttributeGroupKey> = Vec::new();
233        let mut visited: HashSet<AttributeGroupKey> = HashSet::new();
234        if let Some(cycle_key) =
235            find_attribute_group_cycle(schema_set, start, &mut path, &mut visited)
236        {
237            let location = schema_set
238                .arenas
239                .attribute_groups
240                .get(cycle_key)
241                .and_then(|ag| ag.source.as_ref())
242                .and_then(|s| schema_set.source_maps.locate(s));
243            errors.push(SchemaError::structural(
244                "src-attribute_group",
245                "Circular attribute group reference detected via defaultAttributes",
246                location,
247            ));
248        }
249    }
250}
251
252/// Validate a simple type definition
253fn validate_simple_type(
254    schema_set: &SchemaSet,
255    key: SimpleTypeKey,
256    stats: &mut DerivationStats,
257) -> SchemaResult<()> {
258    let type_def = schema_set
259        .arenas
260        .simple_types
261        .get(key)
262        .ok_or_else(|| SchemaError::internal("Simple type not found in arena"))?;
263
264    stats.simple_types_validated += 1;
265
266    // cos-applicable-facets: Check that facets are applicable to the type variety
267    validate_applicable_facets(schema_set, type_def)?;
268
269    match type_def.variety {
270        SimpleTypeVariety::Atomic => {
271            // Atomic types are derived by restriction
272            validate_simple_restriction(schema_set, type_def, stats)?;
273        }
274        SimpleTypeVariety::List => {
275            stats.list_types_validated += 1;
276            validate_simple_list(schema_set, type_def)?;
277            validate_facets_against_resolved_base(schema_set, type_def)?;
278        }
279        SimpleTypeVariety::Union => {
280            stats.union_types_validated += 1;
281            validate_simple_union(schema_set, type_def)?;
282            validate_facets_against_resolved_base(schema_set, type_def)?;
283        }
284    }
285
286    Ok(())
287}
288
289/// Run `FacetSet::merge_with_base` against the resolved base of a list or
290/// union simple type, then validate that local facet values fall in the
291/// base type's value space. Atomic types perform both checks inline in
292/// `validate_simple_restriction`; list (length/minLength/maxLength/whiteSpace)
293/// and union (pattern/enumeration/assertions) varieties share the same
294/// {facets} derivation semantics for the facets they are allowed to carry,
295/// so the merge must run for them too.
296fn validate_facets_against_resolved_base(
297    schema_set: &SchemaSet,
298    type_def: &crate::arenas::SimpleTypeDefData,
299) -> SchemaResult<()> {
300    let Some(base_key) = type_def.resolved_base_type else {
301        return Ok(());
302    };
303    if let Some(base_facets) = get_type_facets(schema_set, base_key)? {
304        type_def.facets.merge_with_base(&base_facets).map_err(|e| {
305            let (location, type_name) = type_error_context(schema_set, type_def);
306            SchemaError::structural(
307                "cos-st-restricts",
308                format!("Simple type '{}' has invalid restriction: {}", type_name, e),
309                location,
310            )
311        })?;
312    }
313    validate_facet_values_against_base_type(schema_set, type_def, base_key)?;
314    Ok(())
315}
316
317/// Validate cos-applicable-facets: only certain facets are applicable to certain type varieties
318///
319/// - List types: length, minLength, maxLength, pattern, enumeration, whiteSpace
320/// - Union types (XSD 1.0): pattern, enumeration
321/// - Union types (XSD 1.1): pattern, enumeration, assertions
322fn validate_applicable_facets(
323    schema_set: &SchemaSet,
324    type_def: &crate::arenas::SimpleTypeDefData,
325) -> SchemaResult<()> {
326    let facets = &type_def.facets;
327
328    match type_def.variety {
329        SimpleTypeVariety::List => {
330            // List types: only length, minLength, maxLength, pattern, enumeration, whiteSpace
331            let has_inapplicable = facets.min_inclusive.is_some()
332                || facets.max_inclusive.is_some()
333                || facets.min_exclusive.is_some()
334                || facets.max_exclusive.is_some()
335                || facets.total_digits.is_some()
336                || facets.fraction_digits.is_some()
337                || facets.explicit_timezone.is_some();
338
339            if has_inapplicable {
340                let (location, type_name) = type_error_context(schema_set, type_def);
341                let inapplicable = list_inapplicable_facets_for_list(facets);
342                return Err(SchemaError::structural(
343                    "cos-applicable-facets",
344                    format!(
345                        "List type '{}' has inapplicable facet(s): {}",
346                        type_name, inapplicable
347                    ),
348                    location,
349                ));
350            }
351        }
352        SimpleTypeVariety::Union => {
353            // Union types: only pattern, enumeration (and assertions in XSD 1.1)
354            let has_inapplicable = facets.length.is_some()
355                || facets.min_length.is_some()
356                || facets.max_length.is_some()
357                || facets.whitespace.is_some()
358                || facets.min_inclusive.is_some()
359                || facets.max_inclusive.is_some()
360                || facets.min_exclusive.is_some()
361                || facets.max_exclusive.is_some()
362                || facets.total_digits.is_some()
363                || facets.fraction_digits.is_some()
364                || facets.explicit_timezone.is_some();
365
366            if has_inapplicable {
367                let (location, type_name) = type_error_context(schema_set, type_def);
368                let inapplicable = list_inapplicable_facets_for_union(facets);
369                return Err(SchemaError::structural(
370                    "cos-applicable-facets",
371                    format!(
372                        "Union type '{}' has inapplicable facet(s): {}",
373                        type_name, inapplicable
374                    ),
375                    location,
376                ));
377            }
378        }
379        SimpleTypeVariety::Atomic => {
380            // Atomic types: applicability depends on the primitive ancestor
381            // (§4.1.5 / Table F.1 of Datatypes). Walk up the base chain to the
382            // closest built-in primitive and check each facet kind against it.
383            if let Some(primitive_code) = primitive_type_code(schema_set, type_def) {
384                use crate::types::facets::{
385                    facet_applicable_for_type, ExplicitTimezone, FacetApplicability, FacetKind,
386                    WhitespaceMode,
387                };
388                let facets = &type_def.facets;
389                let mut bad: Vec<&'static str> = Vec::new();
390                let mut check = |present: bool, kind: FacetKind| {
391                    if present
392                        && matches!(
393                            facet_applicable_for_type(kind, primitive_code),
394                            FacetApplicability::NotApplicable
395                        )
396                    {
397                        bad.push(kind.name());
398                    }
399                };
400                check(facets.length.is_some(), FacetKind::Length);
401                check(facets.min_length.is_some(), FacetKind::MinLength);
402                check(facets.max_length.is_some(), FacetKind::MaxLength);
403                check(facets.whitespace.is_some(), FacetKind::Whitespace);
404                check(facets.min_inclusive.is_some(), FacetKind::MinInclusive);
405                check(facets.max_inclusive.is_some(), FacetKind::MaxInclusive);
406                check(facets.min_exclusive.is_some(), FacetKind::MinExclusive);
407                check(facets.max_exclusive.is_some(), FacetKind::MaxExclusive);
408                check(facets.total_digits.is_some(), FacetKind::TotalDigits);
409                check(facets.fraction_digits.is_some(), FacetKind::FractionDigits);
410                check(
411                    facets.explicit_timezone.is_some(),
412                    FacetKind::ExplicitTimezone,
413                );
414                if let Some(ws) = &facets.whitespace {
415                    if !matches!(
416                        primitive_code,
417                        crate::types::XmlTypeCode::String
418                            | crate::types::XmlTypeCode::NormalizedString
419                            | crate::types::XmlTypeCode::Token
420                            | crate::types::XmlTypeCode::Language
421                            | crate::types::XmlTypeCode::NmToken
422                            | crate::types::XmlTypeCode::Name
423                            | crate::types::XmlTypeCode::NCName
424                            | crate::types::XmlTypeCode::Id
425                            | crate::types::XmlTypeCode::IdRef
426                            | crate::types::XmlTypeCode::Entity
427                    ) && ws.value != WhitespaceMode::Collapse
428                    {
429                        bad.push(FacetKind::Whitespace.name());
430                    }
431                }
432                if let Some(tz) = &facets.explicit_timezone {
433                    if primitive_code == crate::types::XmlTypeCode::DateTimeStamp
434                        && tz.value != ExplicitTimezone::Required
435                    {
436                        bad.push(FacetKind::ExplicitTimezone.name());
437                    }
438                }
439                if !bad.is_empty() {
440                    let (location, type_name) = type_error_context(schema_set, type_def);
441                    return Err(SchemaError::structural(
442                        "cos-applicable-facets",
443                        format!(
444                            "Atomic type '{}' has inapplicable facet(s) for primitive '{}': {}",
445                            type_name,
446                            primitive_code.local_name().unwrap_or("<unnamed>"),
447                            bad.join(", ")
448                        ),
449                        location,
450                    ));
451                }
452            }
453        }
454    }
455
456    Ok(())
457}
458
459/// Walk a simple type's base chain to the closest built-in with an `XmlTypeCode`.
460///
461/// Depth is capped at 64 — the XSD primitive hierarchy is shallow and the
462/// dependency graph already rejects cycles before this runs, so the bound is
463/// purely a defence against a malformed arena state.
464fn primitive_type_code(
465    schema_set: &SchemaSet,
466    type_def: &crate::arenas::SimpleTypeDefData,
467) -> Option<crate::types::XmlTypeCode> {
468    let builtin = schema_set.builtin_types();
469    let mut current_base = type_def.resolved_base_type;
470    for _ in 0..64 {
471        let Some(TypeKey::Simple(k)) = current_base else {
472            return None;
473        };
474        if let Some(code) = builtin.get_type_code(k) {
475            return Some(code);
476        }
477        current_base = schema_set
478            .arenas
479            .simple_types
480            .get(k)
481            .and_then(|t| t.resolved_base_type);
482    }
483    None
484}
485
486/// Emit a `cos-st-restricts`-family error when `simple_key` names
487/// `xs:anyAtomicType`, which XSD 1.1 bug 11103 declared abstract — it must
488/// not appear as a restriction base, list item type, or union member.
489fn reject_any_atomic_type(
490    schema_set: &SchemaSet,
491    type_def: &crate::arenas::SimpleTypeDefData,
492    simple_key: SimpleTypeKey,
493    constraint: &'static str,
494    role: &'static str,
495) -> SchemaResult<()> {
496    if !schema_set.builtin_types().is_any_atomic_type(simple_key) {
497        return Ok(());
498    }
499    let (location, type_name) = type_error_context(schema_set, type_def);
500    Err(SchemaError::structural(
501        constraint,
502        format!(
503            "Simple type '{}' cannot {} xs:anyAtomicType (abstract per XSD 1.1 bug 11103)",
504            type_name, role
505        ),
506        location,
507    ))
508}
509
510/// List inapplicable facet names for list types
511fn list_inapplicable_facets_for_list(facets: &FacetSet) -> String {
512    let mut names = Vec::new();
513    if facets.min_inclusive.is_some() {
514        names.push("minInclusive");
515    }
516    if facets.max_inclusive.is_some() {
517        names.push("maxInclusive");
518    }
519    if facets.min_exclusive.is_some() {
520        names.push("minExclusive");
521    }
522    if facets.max_exclusive.is_some() {
523        names.push("maxExclusive");
524    }
525    if facets.total_digits.is_some() {
526        names.push("totalDigits");
527    }
528    if facets.fraction_digits.is_some() {
529        names.push("fractionDigits");
530    }
531    if facets.explicit_timezone.is_some() {
532        names.push("explicitTimezone");
533    }
534    names.join(", ")
535}
536
537/// List inapplicable facet names for union types
538fn list_inapplicable_facets_for_union(facets: &FacetSet) -> String {
539    let mut names = Vec::new();
540    if facets.length.is_some() {
541        names.push("length");
542    }
543    if facets.min_length.is_some() {
544        names.push("minLength");
545    }
546    if facets.max_length.is_some() {
547        names.push("maxLength");
548    }
549    if facets.whitespace.is_some() {
550        names.push("whiteSpace");
551    }
552    if facets.min_inclusive.is_some() {
553        names.push("minInclusive");
554    }
555    if facets.max_inclusive.is_some() {
556        names.push("maxInclusive");
557    }
558    if facets.min_exclusive.is_some() {
559        names.push("minExclusive");
560    }
561    if facets.max_exclusive.is_some() {
562        names.push("maxExclusive");
563    }
564    if facets.total_digits.is_some() {
565        names.push("totalDigits");
566    }
567    if facets.fraction_digits.is_some() {
568        names.push("fractionDigits");
569    }
570    if facets.explicit_timezone.is_some() {
571        names.push("explicitTimezone");
572    }
573    names.join(", ")
574}
575
576/// Validate simple type restriction derivation
577///
578/// Constraint: cos-st-restricts (Derivation Valid - Restriction, Simple)
579fn validate_simple_restriction(
580    schema_set: &SchemaSet,
581    type_def: &crate::arenas::SimpleTypeDefData,
582    stats: &mut DerivationStats,
583) -> SchemaResult<()> {
584    // If no base type, this is a primitive type or xs:anySimpleType derivation
585    let base_key = match type_def.resolved_base_type {
586        Some(key) => key,
587        None => return Ok(()), // No base type to validate against
588    };
589
590    // cos-st-restricts.1.1: base type must be a simple type definition
591    if let TypeKey::Complex(_) = base_key {
592        let (location, type_name) = type_error_context(schema_set, type_def);
593        return Err(SchemaError::structural(
594            "cos-st-restricts",
595            format!("Simple type '{}': base type must be a simple type definition (cos-st-restricts.1.1)", type_name),
596            location,
597        ));
598    }
599
600    if let TypeKey::Simple(base_simple_key) = base_key {
601        reject_any_atomic_type(
602            schema_set,
603            type_def,
604            base_simple_key,
605            "cos-st-restricts",
606            "restrict",
607        )?;
608    }
609
610    stats.restrictions_validated += 1;
611
612    // Check that base type is not final for restriction
613    if let TypeKey::Simple(base_simple_key) = base_key {
614        if let Some(base_type) = schema_set.arenas.simple_types.get(base_simple_key) {
615            if base_type.final_derivation.contains_restriction() {
616                let (location, type_name) = type_error_context(schema_set, type_def);
617                let base_name =
618                    format_type_name(schema_set, base_type.name, base_type.target_namespace);
619                return Err(SchemaError::structural(
620                    "cos-st-restricts",
621                    format!(
622                        "Simple type '{}' cannot restrict '{}' because base type is final for restriction",
623                        type_name, base_name
624                    ),
625                    location,
626                ));
627            }
628        }
629    }
630
631    // Get base type facets
632    let base_facets = get_type_facets(schema_set, base_key)?;
633
634    // Validate that derived facets are more restrictive
635    if let Some(ref base_facets) = base_facets {
636        // FacetSet.merge_with_base validates derivation rules
637        type_def.facets.merge_with_base(base_facets).map_err(|e| {
638            let (location, type_name) = type_error_context(schema_set, type_def);
639            SchemaError::structural(
640                "cos-st-restricts",
641                format!("Simple type '{}' has invalid restriction: {}", type_name, e),
642                location,
643            )
644        })?;
645    }
646
647    // Validate that facet values are in the base type's value space
648    // (e.g., enumeration values must be valid for xs:float when base is xs:float)
649    validate_facet_values_against_base_type(schema_set, type_def, base_key)?;
650
651    Ok(())
652}
653
654/// Validate simple type list derivation
655///
656/// Constraint: cos-list-of-atomic (List item type must be atomic)
657fn validate_simple_list(
658    schema_set: &SchemaSet,
659    type_def: &crate::arenas::SimpleTypeDefData,
660) -> SchemaResult<()> {
661    // Check that the item type is not final for list derivation
662    if let Some(TypeKey::Simple(item_simple_key)) = type_def.resolved_item_type {
663        if let Some(item_type) = schema_set.arenas.simple_types.get(item_simple_key) {
664            if item_type.final_derivation.contains_list() {
665                let (location, type_name) = type_error_context(schema_set, type_def);
666                let item_name =
667                    format_type_name(schema_set, item_type.name, item_type.target_namespace);
668                return Err(SchemaError::structural(
669                    "cos-st-restricts",
670                    format!(
671                        "List type '{}' cannot use '{}' as item type because it is final for list",
672                        type_name, item_name
673                    ),
674                    location,
675                ));
676            }
677        }
678    }
679
680    // Also check the base type's final for restriction (list types restrict xs:anySimpleType)
681    if let Some(TypeKey::Simple(base_simple_key)) = type_def.resolved_base_type {
682        if let Some(base_type) = schema_set.arenas.simple_types.get(base_simple_key) {
683            if base_type.final_derivation.contains_list() {
684                let (location, type_name) = type_error_context(schema_set, type_def);
685                let base_name =
686                    format_type_name(schema_set, base_type.name, base_type.target_namespace);
687                return Err(SchemaError::structural(
688                    "cos-st-restricts",
689                    format!(
690                        "List type '{}' cannot derive from '{}' because it is final for list",
691                        type_name, base_name
692                    ),
693                    location,
694                ));
695            }
696        }
697    }
698
699    // Get the item type
700    let item_key = match type_def.resolved_item_type {
701        Some(key) => key,
702        None => {
703            // No resolved item type - might be inline or error
704            return Ok(());
705        }
706    };
707
708    // Item type must be atomic (not a list, not a union containing lists)
709    match item_key {
710        TypeKey::Simple(simple_key) => {
711            reject_any_atomic_type(
712                schema_set,
713                type_def,
714                simple_key,
715                "cos-list-of-atomic",
716                "use as list item type",
717            )?;
718            if let Some(item_type) = schema_set.arenas.simple_types.get(simple_key) {
719                match item_type.variety {
720                    SimpleTypeVariety::Atomic => {
721                        // Valid - atomic types are OK
722                    }
723                    SimpleTypeVariety::List => {
724                        // Invalid - list of list is not allowed
725                        let (location, type_name) = type_error_context(schema_set, type_def);
726                        return Err(SchemaError::structural(
727                            "cos-list-of-atomic",
728                            format!(
729                                "List type '{}' has list item type, which is not allowed",
730                                type_name
731                            ),
732                            location,
733                        ));
734                    }
735                    SimpleTypeVariety::Union => {
736                        // Must check that union doesn't contain list members
737                        if union_contains_list(schema_set, item_type) {
738                            let (location, type_name) = type_error_context(schema_set, type_def);
739                            return Err(SchemaError::structural(
740                                "cos-list-of-atomic",
741                                format!(
742                                    "List type '{}' has union item type containing list member",
743                                    type_name
744                                ),
745                                location,
746                            ));
747                        }
748                    }
749                }
750            }
751        }
752        TypeKey::Complex(_) => {
753            // Complex types cannot be list item types
754            let (location, type_name) = type_error_context(schema_set, type_def);
755            return Err(SchemaError::structural(
756                "cos-list-of-atomic",
757                format!(
758                    "List type '{}' has complex item type, which is not allowed",
759                    type_name
760                ),
761                location,
762            ));
763        }
764    }
765
766    Ok(())
767}
768
769/// Check if a union type (or nested unions) contains any list members
770fn union_contains_list(
771    schema_set: &SchemaSet,
772    union_type: &crate::arenas::SimpleTypeDefData,
773) -> bool {
774    for member_key in &union_type.resolved_member_types {
775        if let TypeKey::Simple(simple_key) = member_key {
776            if let Some(member) = schema_set.arenas.simple_types.get(*simple_key) {
777                match member.variety {
778                    SimpleTypeVariety::List => return true,
779                    SimpleTypeVariety::Union => {
780                        if union_contains_list(schema_set, member) {
781                            return true;
782                        }
783                    }
784                    SimpleTypeVariety::Atomic => {}
785                }
786            }
787        }
788    }
789    false
790}
791
792/// Validate simple type union derivation
793///
794/// Constraint: cos-union-memberTypes (Union member types must be simple types)
795fn validate_simple_union(
796    schema_set: &SchemaSet,
797    type_def: &crate::arenas::SimpleTypeDefData,
798) -> SchemaResult<()> {
799    // Check that member types are not final for union derivation
800    for member_key in &type_def.resolved_member_types {
801        if let TypeKey::Simple(simple_key) = member_key {
802            if let Some(member_type) = schema_set.arenas.simple_types.get(*simple_key) {
803                if member_type.final_derivation.contains_union() {
804                    let (location, type_name) = type_error_context(schema_set, type_def);
805                    let member_name = format_type_name(
806                        schema_set,
807                        member_type.name,
808                        member_type.target_namespace,
809                    );
810                    return Err(SchemaError::structural(
811                        "cos-st-restricts",
812                        format!(
813                            "Union type '{}' cannot use '{}' as member type because it is final for union",
814                            type_name, member_name
815                        ),
816                        location,
817                    ));
818                }
819            }
820        }
821    }
822
823    // All member types must be simple types
824    for member_key in &type_def.resolved_member_types {
825        match member_key {
826            TypeKey::Simple(simple_key) => {
827                reject_any_atomic_type(
828                    schema_set,
829                    type_def,
830                    *simple_key,
831                    "cos-union-memberTypes",
832                    "use as union member type",
833                )?;
834            }
835            TypeKey::Complex(_) => {
836                // Invalid - complex types cannot be union members
837                let (location, type_name) = type_error_context(schema_set, type_def);
838                return Err(SchemaError::structural(
839                    "cos-union-memberTypes",
840                    format!(
841                        "Union type '{}' has complex member type, which is not allowed",
842                        type_name
843                    ),
844                    location,
845                ));
846            }
847        }
848    }
849
850    Ok(())
851}
852
853/// Validate a complex type definition
854fn validate_complex_type(
855    schema_set: &SchemaSet,
856    key: ComplexTypeKey,
857    stats: &mut DerivationStats,
858) -> SchemaResult<()> {
859    let type_def = schema_set
860        .arenas
861        .complex_types
862        .get(key)
863        .ok_or_else(|| SchemaError::internal("Complex type not found in arena"))?;
864
865    stats.complex_types_validated += 1;
866
867    // Check derivation method
868    match type_def.derivation_method {
869        Some(DerivationMethod::Extension) => {
870            stats.extensions_validated += 1;
871            validate_complex_extension(schema_set, key, type_def)?;
872        }
873        Some(DerivationMethod::Restriction) => {
874            stats.restrictions_validated += 1;
875            validate_complex_restriction(schema_set, type_def)?;
876        }
877        None => {
878            // No explicit derivation - this is a new complex type definition
879            // Implicitly derived from xs:anyType by restriction
880        }
881    }
882
883    Ok(())
884}
885
886/// Validate complex type extension
887///
888/// Constraint: cos-ct-extends (Complex Type Derivation OK - Extension)
889fn validate_complex_extension(
890    schema_set: &SchemaSet,
891    #[cfg_attr(not(feature = "xsd11"), allow(unused_variables))] derived_key: ComplexTypeKey,
892    type_def: &crate::arenas::ComplexTypeDefData,
893) -> SchemaResult<()> {
894    // Get base type
895    let base_key = match type_def.resolved_base_type {
896        Some(key) => key,
897        None => return Ok(()), // No base type
898    };
899
900    // Check that base type exists and is accessible
901    match base_key {
902        TypeKey::Simple(base_simple_key) => {
903            // Extension from simple type is valid only with simpleContent
904            // complexContent extension from simple type is invalid
905            if matches!(type_def.content, ComplexContentResult::Complex(_)) {
906                let (location, type_name) = type_error_context(schema_set, type_def);
907                return Err(SchemaError::structural(
908                    "cos-ct-extends",
909                    format!(
910                        "Complex type '{}' cannot use complexContent extension from a simple type",
911                        type_name,
912                    ),
913                    location,
914                ));
915            }
916
917            // Check that simple base type is not final for extension
918            if let Some(base_type) = schema_set.arenas.simple_types.get(base_simple_key) {
919                if base_type.final_derivation.contains_extension() {
920                    let (location, type_name) = type_error_context(schema_set, type_def);
921                    let base_name =
922                        format_type_name(schema_set, base_type.name, base_type.target_namespace);
923                    return Err(SchemaError::structural(
924                        "cos-ct-extends",
925                        format!(
926                            "Complex type '{}' cannot extend simple type '{}' because it is final for extension",
927                            type_name, base_name,
928                        ),
929                        location,
930                    ));
931                }
932            }
933        }
934        TypeKey::Complex(base_complex_key) => {
935            if let Some(base_type) = schema_set.arenas.complex_types.get(base_complex_key) {
936                // Check that base type is not final for extension
937                if base_type.final_derivation.contains_extension() {
938                    let (location, type_name) = type_error_context(schema_set, type_def);
939                    let base_name =
940                        format_type_name(schema_set, base_type.name, base_type.target_namespace);
941                    return Err(SchemaError::structural(
942                        "cos-ct-extends",
943                        format!(
944                            "Complex type '{}' cannot extend '{}' because base type is final for extension",
945                            type_name, base_name
946                        ),
947                        location,
948                    ));
949                }
950
951                // src-ct.2 (§3.4.6.2): when the derived type uses <xs:simpleContent>
952                // and the <xs:extension> alternative, the base's {content type}
953                // must be either a simple type (clause 2.1.3 requires a simple-type
954                // base, handled above via TypeKey::Simple) or a complex type whose
955                // {content type} is a simple type definition (clause 2.1.1).
956                // A base with element-only or mixed complex content is rejected.
957                if matches!(type_def.content, ComplexContentResult::Simple(_))
958                    && !matches!(base_type.content, ComplexContentResult::Simple(_))
959                {
960                    let (location, type_name) = type_error_context(schema_set, type_def);
961                    let base_name =
962                        format_type_name(schema_set, base_type.name, base_type.target_namespace);
963                    return Err(SchemaError::structural(
964                        "src-ct",
965                        format!(
966                            "Complex type '{}' uses xs:simpleContent extension but base '{}' \
967                             does not have a simple {{content type}} (src-ct.2.1.1)",
968                            type_name, base_name,
969                        ),
970                        location,
971                    ));
972                }
973
974                // cos-ct-extends: Cannot use complexContent extension to add particles
975                // to a base type with simpleContent.
976                // XSD 1.0: only rejected when a particle is actually added.
977                // XSD 1.1 cos-ct-extends clause 1.4: content variety must match;
978                //   simpleContent base + complexContent derived is always invalid.
979                if matches!(base_type.content, ComplexContentResult::Simple(_)) {
980                    if let ComplexContentResult::Complex(ref complex) = type_def.content {
981                        if complex.particle.is_some() || schema_set.is_xsd11() {
982                            let (location, type_name) = type_error_context(schema_set, type_def);
983                            let base_name = format_type_name(
984                                schema_set,
985                                base_type.name,
986                                base_type.target_namespace,
987                            );
988                            return Err(SchemaError::structural(
989                                "cos-ct-extends",
990                                format!(
991                                    "Complex type '{}' cannot use complexContent to extend '{}' which has simpleContent{}",
992                                    type_name, base_name,
993                                    if complex.particle.is_some() { " with element content" }
994                                    else { " (XSD 1.1 cos-ct-extends clause 1.4)" },
995                                ),
996                                location,
997                            ));
998                        }
999                    }
1000                }
1001
1002                validate_extension_mixed_parity(schema_set, type_def, base_type)?;
1003
1004                // cos-ct-extends / cos-particle-extend: Cannot extend non-empty
1005                // non-all content with an all compositor.  The effective content
1006                // type of an extension is sequence(base, extension) per §3.4.2.3.3.
1007                // cos-particle-extend §3.9.6.2 only allows: (1) same particle,
1008                // (2) E is a sequence wrapping B, or (3) both are all groups.
1009                // An all group nested inside a sequence also violates
1010                // cos-all-limited.1 (placement constraint).
1011                //
1012                // Exception: XSD 1.1 allows all-over-all extensions (clause 3 of
1013                // cos-particle-extend). If both base and extension are all groups,
1014                // skip this check.
1015                if let ComplexContentResult::Complex(ref base_complex) = base_type.content {
1016                    if let Some(ref base_particle) = base_complex.particle {
1017                        if let ComplexContentResult::Complex(ref derived_complex) = type_def.content
1018                        {
1019                            if let Some(ref ext_particle) = derived_complex.particle {
1020                                let ext_compositor = match &ext_particle.term {
1021                                    ParticleTerm::Group(mg) => mg.compositor,
1022                                    _ => None,
1023                                };
1024                                let base_is_all = matches!(
1025                                    base_particle.term,
1026                                    ParticleTerm::Group(ModelGroupDefResult {
1027                                        compositor: Some(Compositor::All),
1028                                        ..
1029                                    })
1030                                );
1031
1032                                // cos-particle-extend §3.9.6.2: over a non-empty base,
1033                                // the only valid extension shapes are:
1034                                // (1) No extension particle  (handled by outer if-let)
1035                                // (2) Extension particle is a sequence
1036                                // (3) XSD 1.1: all-over-all
1037                                match ext_compositor {
1038                                    Some(Compositor::Sequence) => {
1039                                        // OK: sequence extension is always valid
1040                                    }
1041                                    Some(Compositor::All)
1042                                        if base_is_all
1043                                            && schema_set.xsd_version
1044                                                == crate::schema::model::XsdVersion::V1_1 =>
1045                                    {
1046                                        // OK: XSD 1.1 all-over-all
1047                                    }
1048                                    Some(Compositor::Choice) if !base_is_all => {
1049                                        // OK: the effective content type is
1050                                        // sequence(base, extension) per §3.4.2.3.3
1051                                        // clause 4.2.3.3, so cos-particle-extend
1052                                        // clause 2 is satisfied regardless of the
1053                                        // extension particle's compositor — as long
1054                                        // as the base particle is not xs:all (which
1055                                        // would get nested inside the sequence and
1056                                        // violate cos-all-limited.1).
1057                                    }
1058                                    Some(compositor @ (Compositor::All | Compositor::Choice)) => {
1059                                        let location = type_def
1060                                            .source
1061                                            .as_ref()
1062                                            .and_then(|s| schema_set.source_maps.locate(s));
1063                                        let type_name = format_type_name(
1064                                            schema_set,
1065                                            type_def.name,
1066                                            type_def.target_namespace,
1067                                        );
1068                                        let base_name = format_type_name(
1069                                            schema_set,
1070                                            base_type.name,
1071                                            base_type.target_namespace,
1072                                        );
1073                                        let (comp_name, reason) = match compositor {
1074                                            Compositor::All => (
1075                                                "all",
1076                                                "the resulting content model would \
1077                                                 violate cos-all-limited placement \
1078                                                 constraints",
1079                                            ),
1080                                            Compositor::Choice => (
1081                                                "choice",
1082                                                "the base type's xs:all particle would \
1083                                                 be nested inside a sequence, \
1084                                                 violating cos-all-limited.1",
1085                                            ),
1086                                            Compositor::Sequence => unreachable!(),
1087                                        };
1088                                        return Err(SchemaError::structural(
1089                                            "cos-ct-extends",
1090                                            format!(
1091                                                "Complex type '{}' cannot extend '{}' with \
1092                                                 an xs:{} compositor because the base type \
1093                                                 has non-empty content; {}",
1094                                                type_name, base_name, comp_name, reason,
1095                                            ),
1096                                            location,
1097                                        ));
1098                                    }
1099                                    None => {
1100                                        // Bare element or wildcard term (no model group
1101                                        // wrapper).  The effective content type mapping
1102                                        // wraps it in a sequence with the base, so this
1103                                        // is equivalent to a sequence extension — OK.
1104                                    }
1105                                }
1106                            }
1107                        }
1108                    }
1109                }
1110
1111                // XSD 1.1: Validate open-content compatibility
1112                #[cfg(feature = "xsd11")]
1113                validate_open_content_extension(
1114                    schema_set,
1115                    derived_key,
1116                    type_def,
1117                    base_complex_key,
1118                    base_type,
1119                )?;
1120
1121                // ct-props-correct.4 is enforced globally by
1122                // `validate_complex_type_attribute_uniqueness` (run from
1123                // `pipeline.rs` after reference resolution); no extension-
1124                // local check needed here.
1125            }
1126        }
1127    }
1128
1129    Ok(())
1130}
1131
1132/// Validate complex type restriction
1133///
1134/// Constraint: derivation-ok-restriction (Complex Type Derivation OK - Restriction)
1135fn validate_complex_restriction(
1136    schema_set: &SchemaSet,
1137    type_def: &crate::arenas::ComplexTypeDefData,
1138) -> SchemaResult<()> {
1139    // Get base type
1140    let base_key = match type_def.resolved_base_type {
1141        Some(key) => key,
1142        None => return Ok(()), // No base type (derived from anyType)
1143    };
1144
1145    match base_key {
1146        TypeKey::Simple(base_simple_key) => {
1147            // ct-props-correct.2: If the base type is a simple type definition,
1148            // the derivation method must be extension (not restriction).
1149            let (location, type_name) = type_error_context(schema_set, type_def);
1150            let base_name =
1151                if let Some(base_type) = schema_set.arenas.simple_types.get(base_simple_key) {
1152                    format_type_name(schema_set, base_type.name, base_type.target_namespace)
1153                } else {
1154                    "(unknown)".to_string()
1155                };
1156            return Err(SchemaError::structural(
1157                "ct-props-correct",
1158                format!(
1159                    "Complex type '{}' cannot restrict simple type '{}'; \
1160                     derivation from a simple type must use extension",
1161                    type_name, base_name,
1162                ),
1163                location,
1164            ));
1165        }
1166        TypeKey::Complex(base_complex_key) => {
1167            if let Some(base_type) = schema_set.arenas.complex_types.get(base_complex_key) {
1168                // Check that base type is not final for restriction
1169                if base_type.final_derivation.contains_restriction() {
1170                    let (location, type_name) = type_error_context(schema_set, type_def);
1171                    let base_name =
1172                        format_type_name(schema_set, base_type.name, base_type.target_namespace);
1173                    return Err(SchemaError::structural(
1174                        "derivation-ok-restriction",
1175                        format!(
1176                            "Complex type '{}' cannot restrict '{}' because base type is final for restriction",
1177                            type_name, base_name
1178                        ),
1179                        location,
1180                    ));
1181                }
1182
1183                // §3.4.6.4 clause 5.4.1 mixed-parity (one direction only):
1184                // restricting an element-only base to a `mixed="true"` derived
1185                // type would add character data, which is incompatible with
1186                // restriction. The reverse (mixed → element-only) is the
1187                // "pointless mixed restriction" tolerated by Saxon/Xerces and
1188                // exercised by valid-schema tests particlesL012 / mgA015 /
1189                // idK012, so only the asymmetric error is enforced here.
1190                // ctF006 (element-only choice + mixed restriction).
1191                if let (
1192                    ComplexContentResult::Complex(base_complex),
1193                    ComplexContentResult::Complex(derived_complex),
1194                ) = (&base_type.content, &type_def.content)
1195                {
1196                    let base_mixed = effective_mixed_of(base_type, base_complex);
1197                    let derived_mixed = effective_mixed_of(type_def, derived_complex);
1198                    if derived_mixed && !base_mixed {
1199                        let (location, type_name) = type_error_context(schema_set, type_def);
1200                        let base_name = format_type_name(
1201                            schema_set,
1202                            base_type.name,
1203                            base_type.target_namespace,
1204                        );
1205                        return Err(SchemaError::structural(
1206                            "derivation-ok-restriction",
1207                            format!(
1208                                "Complex type '{}' cannot restrict element-only base '{}' \
1209                                 to mixed content (§3.4.6.4 clause 5.4.1)",
1210                                type_name, base_name,
1211                            ),
1212                            location,
1213                        ));
1214                    }
1215                }
1216
1217                // src-ct.2 (§3.4.6.2): when the derived type uses <xs:simpleContent>
1218                // and the <xs:restriction> alternative, the base must be either a
1219                // complex type with a simple {content type} (clause 2.1.1) or a
1220                // complex type whose {content type} is mixed and whose particle is
1221                // emptiable (clause 2.1.2). Element-only or mixed-but-not-emptiable
1222                // bases are rejected.
1223                if matches!(type_def.content, ComplexContentResult::Simple(_))
1224                    && !is_valid_simple_content_restriction_base(schema_set, base_type)
1225                {
1226                    let (location, type_name) = type_error_context(schema_set, type_def);
1227                    let base_name =
1228                        format_type_name(schema_set, base_type.name, base_type.target_namespace);
1229                    return Err(SchemaError::structural(
1230                        "src-ct",
1231                        format!(
1232                            "Complex type '{}' uses xs:simpleContent restriction but base '{}' \
1233                             does not have a simple {{content type}} nor mixed+emptiable content \
1234                             (src-ct.2.1.1 / 2.1.2)",
1235                            type_name, base_name,
1236                        ),
1237                        location,
1238                    ));
1239                }
1240
1241                validate_content_particle_restriction(schema_set, type_def, base_type)?;
1242
1243                // XSD 1.1 §3.4.6.4 / cos-element-consistent (extended for
1244                // all-group restrictions): when a derived all-group restricts
1245                // a base all-group and removes a base local element, any
1246                // wildcard in the derived that admits the removed element's
1247                // QName must resolve to a governing type that's substitutable
1248                // for the base local's type. This is the schema-time analog of
1249                // dynamic EDC, applied where the all-group's unordered
1250                // matching makes the conflict structurally inevitable
1251                // (wild069 — the corresponding xs:sequence case wild068 is
1252                // covered by runtime dynamic EDC instead).
1253                #[cfg(feature = "xsd11")]
1254                validate_all_group_restriction_edc(schema_set, type_def, base_type)?;
1255
1256                // XSD 1.1: Validate open-content compatibility
1257                #[cfg(feature = "xsd11")]
1258                validate_open_content_restriction(schema_set, type_def, base_type)?;
1259
1260                // Validate attribute restriction (derivation-ok-restriction clause 3)
1261                validate_attribute_restriction(schema_set, type_def, base_type)?;
1262
1263                // Validate simpleContent inline type restriction
1264                // (derivation-ok-restriction clause 2.2.2.1)
1265                validate_simple_content_restriction(schema_set, type_def, base_type)?;
1266            }
1267        }
1268    }
1269
1270    Ok(())
1271}
1272
1273/// §3.4.2.3 mapping: the `mixed` flag carried by a `<complexContent>` wrapper
1274/// overrides the outer `<complexType mixed="…">` attribute.  For complex
1275/// types authored in the short form (no `<complexContent>` wrapper), the
1276/// outer attribute applies unchanged.
1277fn effective_mixed_of(
1278    type_def: &crate::arenas::ComplexTypeDefData,
1279    complex: &crate::parser::frames::ComplexContentDefResult,
1280) -> bool {
1281    // `complex.mixed` reflects the `<complexContent mixed="…">` attribute.
1282    // When the complexType was parsed from a short form, the wrapper is
1283    // synthesized with mixed=false and the outer attribute is preserved on
1284    // `type_def.mixed` — so we OR the two.  When the wrapper is present,
1285    // `type_def.mixed` carries the same bit, so the OR is a no-op.
1286    complex.mixed || type_def.mixed
1287}
1288
1289/// cos-ct-extends clause 1.4.3.2.2.4.1 (§3.4.6.2): when the derived type
1290/// supplies its own particle, the effective mixed of the derived {content
1291/// type} must match the base's — both element-only, or both mixed. Also
1292/// fires when the derived has no own particle but explicitly declares a
1293/// `mixed=` value that disagrees with the base (ctF008): per §3.4.2.3
1294/// clause 4.1 the {content type} inherits from the base, but Saxon/Xerces
1295/// (and the W3C suite) treat the contradictory `mixed="true"` declaration
1296/// as a structural error. The "no-particle and no explicit mixed flag"
1297/// case is the only scenario that must be skipped, because there is then
1298/// no inconsistency.
1299fn validate_extension_mixed_parity(
1300    schema_set: &SchemaSet,
1301    type_def: &crate::arenas::ComplexTypeDefData,
1302    base_type: &crate::arenas::ComplexTypeDefData,
1303) -> SchemaResult<()> {
1304    let ComplexContentResult::Complex(ref base_complex) = base_type.content else {
1305        return Ok(());
1306    };
1307    let ComplexContentResult::Complex(ref derived_complex) = type_def.content else {
1308        return Ok(());
1309    };
1310    let base_mixed = effective_mixed_of(base_type, base_complex);
1311    let derived_mixed = effective_mixed_of(type_def, derived_complex);
1312    if derived_mixed == base_mixed {
1313        return Ok(());
1314    }
1315    // No own particle and no explicit mixed declaration → parity is
1316    // trivially inherited from the base; nothing to check.
1317    if derived_complex.particle.is_none() && !derived_complex.mixed && !type_def.mixed {
1318        return Ok(());
1319    }
1320    let (location, type_name) = type_error_context(schema_set, type_def);
1321    let base_name = format_type_name(schema_set, base_type.name, base_type.target_namespace);
1322    Err(SchemaError::structural(
1323        "cos-ct-extends",
1324        format!(
1325            "Complex type '{}' cannot extend '{}' — derived is {} but base is {} \
1326             (cos-ct-extends clause 1.4.3.2.2.4.1)",
1327            type_name,
1328            base_name,
1329            if derived_mixed {
1330                "mixed"
1331            } else {
1332                "element-only"
1333            },
1334            if base_mixed { "mixed" } else { "element-only" },
1335        ),
1336        location,
1337    ))
1338}
1339
1340/// §3.4.6.2 src-ct.2: a complex type derived via `<xs:simpleContent>` from
1341/// another complex base is legal only when the base's `{content type}` is
1342/// a simple type (clause 2.1.1) or when — for the restriction branch — the
1343/// base is mixed with an emptiable particle (clause 2.1.2).
1344/// Returns `true` if the base is acceptable for a simpleContent restriction.
1345fn is_valid_simple_content_restriction_base(
1346    schema_set: &SchemaSet,
1347    base: &crate::arenas::ComplexTypeDefData,
1348) -> bool {
1349    match &base.content {
1350        ComplexContentResult::Simple(_) => true,
1351        ComplexContentResult::Complex(complex) => {
1352            // Clause 2.1.2: mixed content with an emptiable particle.  The
1353            // `mixed` flag on a <complexContent> wrapper overrides the outer
1354            // <complexType mixed="…"> attribute per §3.4.2.2, so consult it
1355            // here; the outer flag applies only to the short-form path.
1356            if !complex.mixed {
1357                return false;
1358            }
1359            match &complex.particle {
1360                // Absent particle ≡ empty sequence ≡ emptiable.
1361                None => true,
1362                Some(particle) => match normalize_type_particle(schema_set, base, particle) {
1363                    Ok(normalized) => particle_is_emptiable(&normalized),
1364                    Err(_) => false,
1365                },
1366            }
1367        }
1368        // Short-form complex type without <simpleContent>/<complexContent>:
1369        // the {content type} is determined by §3.4.2.2 from the outer
1370        // `mixed` attribute and any top-level particle.  When `mixed` is
1371        // true and no particle is present, the type has mixed emptiable
1372        // content and is a valid base for simpleContent restriction
1373        // (clause 2.1.2).  When `mixed` is false, the content type is
1374        // element-only-empty, which is not a valid base.
1375        ComplexContentResult::Empty => base.mixed,
1376    }
1377}
1378
1379#[derive(Debug, Clone)]
1380struct NormalizedParticle {
1381    term: NormalizedParticleTerm,
1382    min_occurs: u32,
1383    max_occurs: Option<u32>,
1384    source: Option<SourceRef>,
1385}
1386
1387#[derive(Debug, Clone)]
1388enum NormalizedParticleTerm {
1389    Element(NormalizedElement),
1390    Wildcard(Box<NormalizedWildcard>),
1391    Group(NormalizedGroup),
1392}
1393
1394#[derive(Debug, Clone)]
1395struct NormalizedElement {
1396    name: NameId,
1397    namespace: Option<NameId>,
1398    type_key: TypeKey,
1399    element_key: Option<ElementKey>,
1400    block: DerivationSet,
1401    nillable: bool,
1402    fixed_value: Option<String>,
1403}
1404
1405#[derive(Debug, Clone)]
1406struct NormalizedWildcard {
1407    wildcard: WildcardResult,
1408    target_namespace: Option<NameId>,
1409}
1410
1411#[derive(Debug, Clone)]
1412struct NormalizedGroup {
1413    compositor: Compositor,
1414    particles: Vec<NormalizedParticle>,
1415}
1416
1417struct ParticleNormalizer<'a> {
1418    schema_set: &'a SchemaSet,
1419    target_namespace: Option<NameId>,
1420    resolved_types: &'a [Option<TypeKey>],
1421    flat_index: usize,
1422    depth: usize,
1423}
1424
1425const MAX_PARTICLE_RESTRICTION_DEPTH: usize = 100;
1426
1427impl<'a> ParticleNormalizer<'a> {
1428    fn new(
1429        schema_set: &'a SchemaSet,
1430        target_namespace: Option<NameId>,
1431        resolved_types: &'a [Option<TypeKey>],
1432    ) -> Self {
1433        Self {
1434            schema_set,
1435            target_namespace,
1436            resolved_types,
1437            flat_index: 0,
1438            depth: 0,
1439        }
1440    }
1441
1442    fn normalize_particle(
1443        &mut self,
1444        particle: &ParticleResult,
1445    ) -> SchemaResult<NormalizedParticle> {
1446        if self.depth >= MAX_PARTICLE_RESTRICTION_DEPTH {
1447            return Err(SchemaError::internal(
1448                "particle restriction normalization exceeded recursion limit",
1449            ));
1450        }
1451
1452        self.depth += 1;
1453        let term = match &particle.term {
1454            ParticleTerm::Element(elem) => {
1455                let source = particle.source.as_ref().or(elem.source.as_ref());
1456                NormalizedParticleTerm::Element(self.normalize_element(elem, source)?)
1457            }
1458            ParticleTerm::Any(wildcard) => {
1459                NormalizedParticleTerm::Wildcard(Box::new(NormalizedWildcard {
1460                    wildcard: wildcard.clone(),
1461                    target_namespace: self.target_namespace,
1462                }))
1463            }
1464            ParticleTerm::Group(group) => {
1465                NormalizedParticleTerm::Group(self.normalize_group(group)?)
1466            }
1467        };
1468        self.depth -= 1;
1469
1470        Ok(collapse_single_child_groups(NormalizedParticle {
1471            term,
1472            min_occurs: particle.min_occurs,
1473            max_occurs: particle.max_occurs,
1474            source: particle.source.clone(),
1475        }))
1476    }
1477
1478    fn normalize_element(
1479        &mut self,
1480        elem: &ElementFrameResult,
1481        source: Option<&SourceRef>,
1482    ) -> SchemaResult<NormalizedElement> {
1483        if let Some(ref_name) = &elem.ref_name {
1484            let elem_key = self
1485                .schema_set
1486                .lookup_element(ref_name.namespace, ref_name.local_name);
1487            let (type_key, block, nillable, fixed_value) = elem_key
1488                .and_then(|key| self.schema_set.arenas.elements.get(key))
1489                .map(|decl| {
1490                    let (eff_block, _) =
1491                        crate::compiler::substitution::effective_element_constraints(
1492                            self.schema_set,
1493                            decl,
1494                        );
1495                    let tk = decl
1496                        .resolved_type
1497                        .unwrap_or_else(|| TypeKey::Complex(self.schema_set.any_type_key()));
1498                    (tk, eff_block, decl.nillable, decl.fixed_value.clone())
1499                })
1500                .unwrap_or_else(|| {
1501                    (
1502                        TypeKey::Complex(self.schema_set.any_type_key()),
1503                        DerivationSet::empty(),
1504                        false,
1505                        None,
1506                    )
1507                });
1508            return Ok(NormalizedElement {
1509                name: ref_name.local_name,
1510                namespace: ref_name.namespace,
1511                type_key,
1512                element_key: elem_key,
1513                block,
1514                nillable,
1515                fixed_value,
1516            });
1517        }
1518
1519        let name = elem
1520            .name
1521            .ok_or_else(|| SchemaError::internal("element particle missing name and ref"))?;
1522        let index = self.flat_index;
1523        self.flat_index += 1;
1524
1525        let namespace = self.schema_set.effective_local_element_namespace(
1526            elem.target_namespace,
1527            elem.form.as_deref(),
1528            source,
1529            self.target_namespace,
1530        );
1531        let type_key = self
1532            .resolved_types
1533            .get(index)
1534            .copied()
1535            .flatten()
1536            .or_else(|| resolve_element_type_ref(self.schema_set, elem))
1537            .unwrap_or_else(|| TypeKey::Complex(self.schema_set.any_type_key()));
1538
1539        // Compute effective block for local element
1540        // block=None means absent → inherit blockDefault; Some(b) means explicit (including "").
1541        let block = match elem.block {
1542            Some(b) => b,
1543            None => source
1544                .and_then(|s| {
1545                    let doc_id = s.schema_defaults_doc.unwrap_or(s.doc_id);
1546                    self.schema_set
1547                        .documents
1548                        .get(doc_id as usize)
1549                        .map(|d| d.block_default)
1550                })
1551                .unwrap_or_default(),
1552        };
1553
1554        Ok(NormalizedElement {
1555            name,
1556            namespace,
1557            type_key,
1558            element_key: None,
1559            block,
1560            nillable: elem.nillable,
1561            fixed_value: elem.fixed_value.clone(),
1562        })
1563    }
1564
1565    fn normalize_group(&mut self, group: &ModelGroupDefResult) -> SchemaResult<NormalizedGroup> {
1566        if let Some(ref_name) = &group.ref_name {
1567            let group_key = self
1568                .schema_set
1569                .lookup_model_group(ref_name.namespace, ref_name.local_name)
1570                .ok_or_else(|| SchemaError::internal("model group reference was not resolved"))?;
1571            let group_data = self
1572                .schema_set
1573                .arenas
1574                .get_model_group(group_key)
1575                .ok_or_else(|| SchemaError::internal("resolved model group not found"))?;
1576            let compositor = group_data
1577                .compositor
1578                .ok_or_else(|| SchemaError::internal("resolved model group missing compositor"))?;
1579            let mut nested = ParticleNormalizer::new(
1580                self.schema_set,
1581                group_data.target_namespace,
1582                &group_data.resolved_particle_types,
1583            );
1584            nested.depth = self.depth;
1585            let particles = group_data
1586                .particles
1587                .iter()
1588                .map(|particle| nested.normalize_particle(particle))
1589                .collect::<SchemaResult<Vec<_>>>()?;
1590            return Ok(NormalizedGroup {
1591                compositor,
1592                particles,
1593            });
1594        }
1595
1596        let compositor = group
1597            .compositor
1598            .ok_or_else(|| SchemaError::internal("inline model group missing compositor"))?;
1599        let particles = group
1600            .particles
1601            .iter()
1602            .map(|particle| self.normalize_particle(particle))
1603            .collect::<SchemaResult<Vec<_>>>()?;
1604        Ok(NormalizedGroup {
1605            compositor,
1606            particles,
1607        })
1608    }
1609}
1610
1611fn resolve_element_type_ref(schema_set: &SchemaSet, elem: &ElementFrameResult) -> Option<TypeKey> {
1612    match &elem.type_ref {
1613        Some(crate::parser::frames::TypeRefResult::QName(qname)) => schema_set
1614            .lookup_type(qname.namespace, qname.local_name)
1615            .or_else(|| schema_set.get_built_in_type_by_qname(qname.namespace, qname.local_name)),
1616        _ => None,
1617    }
1618}
1619
1620fn validate_content_particle_restriction(
1621    schema_set: &SchemaSet,
1622    derived: &crate::arenas::ComplexTypeDefData,
1623    base: &crate::arenas::ComplexTypeDefData,
1624) -> SchemaResult<()> {
1625    let derived_particle = complex_content_particle(&derived.content);
1626    // For the base, resolve effective content by walking up extension chain.
1627    // An empty extension inherits its base type's content model.
1628    let (effective_base, base_particle) = effective_base_content_particle(schema_set, base);
1629
1630    let location = derived
1631        .source
1632        .as_ref()
1633        .and_then(|s| schema_set.source_maps.locate(s));
1634    let type_name = format_type_name(schema_set, derived.name, derived.target_namespace);
1635    let base_name = format_type_name(schema_set, base.name, base.target_namespace);
1636
1637    match (derived_particle, base_particle) {
1638        (None, None) => Ok(()),
1639        (Some(derived_particle), None) => {
1640            let derived_particle = normalize_type_particle(schema_set, derived, derived_particle)?;
1641            // Empty derived particle can restrict an empty-particle base, but not simpleContent (different violation).
1642            if !matches!(effective_base.content, ComplexContentResult::Simple(_))
1643                && is_effectively_empty(&derived_particle)
1644            {
1645                Ok(())
1646            } else {
1647                Err(SchemaError::structural(
1648                    "derivation-ok-restriction",
1649                    format!(
1650                        "Complex type '{}' adds particle content while restricting '{}' which has empty content",
1651                        type_name, base_name
1652                    ),
1653                    location,
1654                ))
1655            }
1656        }
1657        (None, Some(base_particle)) => {
1658            let base_particle = normalize_type_particle(schema_set, effective_base, base_particle)?;
1659            if particle_is_emptiable(&base_particle) {
1660                Ok(())
1661            } else {
1662                Err(SchemaError::structural(
1663                    "derivation-ok-restriction",
1664                    format!(
1665                        "Complex type '{}' removes required particle content from base type '{}'",
1666                        type_name, base_name
1667                    ),
1668                    location,
1669                ))
1670            }
1671        }
1672        (Some(derived_particle), Some(base_particle)) => {
1673            let derived_particle = normalize_type_particle(schema_set, derived, derived_particle)?;
1674            let base_particle = normalize_type_particle(schema_set, effective_base, base_particle)?;
1675
1676            if is_effectively_empty(&derived_particle) {
1677                if particle_is_emptiable(&base_particle) {
1678                    return Ok(());
1679                } else {
1680                    return Err(SchemaError::structural(
1681                        "derivation-ok-restriction",
1682                        format!(
1683                            "Complex type '{}' removes required particle content from base type '{}'",
1684                            type_name, base_name
1685                        ),
1686                        location,
1687                    ));
1688                }
1689            }
1690
1691            // All compositor combinations are now handled by
1692            // particle_restricts: same-compositor, sequence→choice,
1693            // sequence→all, choice expansion, and the catch-all rejection
1694            // for structurally forbidden pairs like all→choice.
1695
1696            if particle_restricts(schema_set, &derived_particle, &base_particle) {
1697                Ok(())
1698            } else {
1699                Err(SchemaError::structural(
1700                    "derivation-ok-restriction",
1701                    format!(
1702                        "Content model of '{}' is not a valid restriction of base type '{}'",
1703                        type_name, base_name
1704                    ),
1705                    location,
1706                ))
1707            }
1708        }
1709    }
1710}
1711
1712/// Check if a normalized particle is an empty group (all children removed as pointless).
1713fn is_empty_group(particle: &NormalizedParticle) -> bool {
1714    matches!(&particle.term, NormalizedParticleTerm::Group(group) if group.particles.is_empty())
1715}
1716
1717/// Top-level particle with maxOccurs=0 or fully pruned group — treated as empty content per §3.8.
1718fn is_effectively_empty(particle: &NormalizedParticle) -> bool {
1719    particle.max_occurs == Some(0) || is_empty_group(particle)
1720}
1721
1722fn complex_content_particle(content: &ComplexContentResult) -> Option<&ParticleResult> {
1723    match content {
1724        ComplexContentResult::Complex(def) => def.particle.as_ref(),
1725        ComplexContentResult::Empty | ComplexContentResult::Simple(_) => None,
1726    }
1727}
1728
1729/// Walk up the extension chain to find the effective content particle.
1730/// Empty extensions inherit their base type's content model.
1731/// Returns (type_def_owning_particle, particle) so the normalizer uses the
1732/// correct target_namespace and resolved_content_particle_types.
1733fn effective_base_content_particle<'a>(
1734    schema_set: &'a SchemaSet,
1735    base: &'a crate::arenas::ComplexTypeDefData,
1736) -> (
1737    &'a crate::arenas::ComplexTypeDefData,
1738    Option<&'a ParticleResult>,
1739) {
1740    let mut current = base;
1741    let mut depth = 0;
1742    loop {
1743        if let Some(particle) = complex_content_particle(&current.content) {
1744            return (current, Some(particle));
1745        }
1746        // If this type has no content and was derived by extension, check its base
1747        if current.derivation_method != Some(DerivationMethod::Extension) {
1748            return (current, None);
1749        }
1750        let Some(TypeKey::Complex(base_key)) = current.resolved_base_type else {
1751            return (current, None);
1752        };
1753        let Some(base_type) = schema_set.arenas.complex_types.get(base_key) else {
1754            return (current, None);
1755        };
1756        depth += 1;
1757        if depth > 50 {
1758            return (current, None); // safety limit
1759        }
1760        current = base_type;
1761    }
1762}
1763
1764fn normalize_type_particle(
1765    schema_set: &SchemaSet,
1766    type_def: &crate::arenas::ComplexTypeDefData,
1767    particle: &ParticleResult,
1768) -> SchemaResult<NormalizedParticle> {
1769    let mut normalizer = ParticleNormalizer::new(
1770        schema_set,
1771        type_def.target_namespace,
1772        &type_def.resolved_content_particle_types,
1773    );
1774    let particle = normalizer.normalize_particle(particle)?;
1775    let particle = remove_pointless_particles(particle);
1776    // XSD 1.1: skip flattening to preserve structural grouping needed for
1777    // intensional restriction (e.g. a single-branch choice whose collapsed
1778    // sequence must match against a multi-branch choice in the base).
1779    if !schema_set.is_xsd11() {
1780        let particle = flatten_same_compositor_groups(particle);
1781        return Ok(particle);
1782    }
1783    Ok(particle)
1784}
1785
1786/// Normalize a top-level named model group into a `NormalizedParticle` for
1787/// §src-redefine 6.2.2 restriction comparisons.
1788///
1789/// **Chain-of-redefine caveat**: when called on an original whose own
1790/// particles include `group-ref`s, those refs are resolved via
1791/// `schema_set.lookup_model_group` inside [`ParticleNormalizer::normalize_group`]
1792/// — which returns the *currently bound* version. For a chain
1793/// `orig → v1 → v2`, v1's inner group-refs resolve to whatever the
1794/// current namespace binding is, not to what v1 saw at creation time. This
1795/// is a pre-existing limitation shared with the complex-type restriction
1796/// path; it is not fixed here.
1797fn normalize_model_group_as_particle(
1798    schema_set: &SchemaSet,
1799    group_data: &crate::arenas::ModelGroupData,
1800) -> SchemaResult<NormalizedParticle> {
1801    let compositor = group_data
1802        .compositor
1803        .ok_or_else(|| SchemaError::internal("redefined named model group missing compositor"))?;
1804
1805    let mut normalizer = ParticleNormalizer::new(
1806        schema_set,
1807        group_data.target_namespace,
1808        &group_data.resolved_particle_types,
1809    );
1810    let particles = group_data
1811        .particles
1812        .iter()
1813        .map(|particle| normalizer.normalize_particle(particle))
1814        .collect::<SchemaResult<Vec<_>>>()?;
1815
1816    let wrapper = NormalizedParticle {
1817        term: NormalizedParticleTerm::Group(NormalizedGroup {
1818            compositor,
1819            particles,
1820        }),
1821        min_occurs: group_data.min_occurs,
1822        max_occurs: group_data.max_occurs,
1823        source: group_data.source.clone(),
1824    };
1825
1826    // `normalize_particle` collapses every child; the outer wrapper we
1827    // built by hand above is *not* collapsed and must be so explicitly so
1828    // a single-element named group ends up shaped identically to the same
1829    // content inside a complex type.
1830    let particle = collapse_single_child_groups(wrapper);
1831    let particle = remove_pointless_particles(particle);
1832    // XSD 1.1: skip flattening (same rationale as `normalize_type_particle`).
1833    if !schema_set.is_xsd11() {
1834        let particle = flatten_same_compositor_groups(particle);
1835        return Ok(particle);
1836    }
1837    Ok(particle)
1838}
1839
1840/// Remove "pointless" particles per Section 3.8 normalization:
1841/// - Particles with maxOccurs=0 are effectively absent
1842/// - Groups with no remaining children after removal are also pointless
1843fn remove_pointless_particles(mut particle: NormalizedParticle) -> NormalizedParticle {
1844    if let NormalizedParticleTerm::Group(group) = &mut particle.term {
1845        group.particles = group
1846            .particles
1847            .drain(..)
1848            .map(remove_pointless_particles)
1849            .filter(|p| p.max_occurs != Some(0))
1850            .collect();
1851    }
1852    particle
1853}
1854
1855/// Flatten nested groups with unit occurs and the same compositor into
1856/// their parent (Section 3.8 particle normalization).
1857/// E.g. sequence(sequence{1,1}(a, b), c) → sequence(a, b, c).
1858fn flatten_same_compositor_groups(mut particle: NormalizedParticle) -> NormalizedParticle {
1859    if let NormalizedParticleTerm::Group(group) = &mut particle.term {
1860        // First recurse into children
1861        group.particles = group
1862            .particles
1863            .drain(..)
1864            .map(flatten_same_compositor_groups)
1865            .collect();
1866        // Then flatten children that are same-compositor groups with unit occurs
1867        let parent_compositor = group.compositor;
1868        let mut flattened = Vec::with_capacity(group.particles.len());
1869        for child in group.particles.drain(..) {
1870            if let NormalizedParticleTerm::Group(ref child_group) = child.term {
1871                if child_group.compositor == parent_compositor
1872                    && occurs_is_unit(child.min_occurs, child.max_occurs)
1873                {
1874                    flattened.extend(child_group.particles.iter().cloned());
1875                    continue;
1876                }
1877            }
1878            flattened.push(child);
1879        }
1880        group.particles = flattened;
1881    }
1882    particle
1883}
1884
1885fn collapse_single_child_groups(mut particle: NormalizedParticle) -> NormalizedParticle {
1886    if let NormalizedParticleTerm::Group(group) = &mut particle.term {
1887        group.particles = group
1888            .particles
1889            .drain(..)
1890            .map(collapse_single_child_groups)
1891            .collect();
1892    }
1893
1894    loop {
1895        let child = match &particle.term {
1896            NormalizedParticleTerm::Group(group)
1897                if group.particles.len() == 1
1898                    && can_collapse_single_child_group(
1899                        group.compositor,
1900                        particle.min_occurs,
1901                        particle.max_occurs,
1902                        &group.particles[0],
1903                    ) =>
1904            {
1905                Some(group.particles[0].clone())
1906            }
1907            _ => None,
1908        };
1909        let Some(child) = child else {
1910            return particle;
1911        };
1912        let (min_occurs, max_occurs) = multiply_occurs(
1913            particle.min_occurs,
1914            particle.max_occurs,
1915            child.min_occurs,
1916            child.max_occurs,
1917        );
1918        particle = NormalizedParticle {
1919            term: child.term,
1920            min_occurs,
1921            max_occurs,
1922            source: particle.source.clone().or(child.source),
1923        };
1924    }
1925}
1926
1927fn can_collapse_single_child_group(
1928    compositor: Compositor,
1929    group_min_occurs: u32,
1930    group_max_occurs: Option<u32>,
1931    child: &NormalizedParticle,
1932) -> bool {
1933    if compositor == Compositor::Choice {
1934        return true;
1935    }
1936
1937    occurs_is_unit(group_min_occurs, group_max_occurs) || child.max_occurs == Some(1)
1938}
1939
1940fn occurs_is_unit(min_occurs: u32, max_occurs: Option<u32>) -> bool {
1941    min_occurs == 1 && max_occurs == Some(1)
1942}
1943
1944fn multiply_occurs(
1945    left_min: u32,
1946    left_max: Option<u32>,
1947    right_min: u32,
1948    right_max: Option<u32>,
1949) -> (u32, Option<u32>) {
1950    let min_occurs = left_min.saturating_mul(right_min);
1951    let max_occurs = match (left_max, right_max) {
1952        (Some(left), Some(right)) => Some(left.saturating_mul(right)),
1953        (Some(0), None) | (None, Some(0)) => Some(0),
1954        _ => None,
1955    };
1956    (min_occurs, max_occurs)
1957}
1958
1959/// XSD 1.1: fold a single-child sequence/all group by multiplying occurs.
1960/// sequence{M,N}(e{m,n}) ≡ e{M*m, N*n}
1961fn fold_single_child_group(particle: &NormalizedParticle) -> Option<NormalizedParticle> {
1962    if let NormalizedParticleTerm::Group(group) = &particle.term {
1963        if group.particles.len() == 1
1964            && matches!(group.compositor, Compositor::Sequence | Compositor::All)
1965        {
1966            let child = &group.particles[0];
1967            let (min_occurs, max_occurs) = multiply_occurs(
1968                particle.min_occurs,
1969                particle.max_occurs,
1970                child.min_occurs,
1971                child.max_occurs,
1972            );
1973            return Some(NormalizedParticle {
1974                term: child.term.clone(),
1975                min_occurs,
1976                max_occurs,
1977                source: particle.source.clone().or(child.source.clone()),
1978            });
1979        }
1980    }
1981    None
1982}
1983
1984fn particle_restricts(
1985    schema_set: &SchemaSet,
1986    derived: &NormalizedParticle,
1987    base: &NormalizedParticle,
1988) -> bool {
1989    // XSD 1.1 intensional restriction: fold single-child sequence/all groups
1990    // symmetrically on both sides so they are compared in the same normal form.
1991    if schema_set.is_xsd11() {
1992        if let Some(folded) = fold_single_child_group(derived) {
1993            return particle_restricts(schema_set, &folded, base);
1994        }
1995        if let Some(folded_base) = fold_single_child_group(base) {
1996            return particle_restricts(schema_set, derived, &folded_base);
1997        }
1998    }
1999
2000    // XSD 1.0: A non-choice optional particle cannot restrict an optional non-repeated
2001    // multi-branch choice. The expand_choice_branches approach merges choice occurs into
2002    // branches, which gives wrong results for RecurseLax when max_occurs=1.
2003    // For repeated choices (max>1), the spec is ambiguous — provisionally accept.
2004    if schema_set.is_xsd10()
2005        && derived.min_occurs == 0
2006        && !matches!(
2007            &derived.term,
2008            NormalizedParticleTerm::Group(group) if group.compositor == Compositor::Choice
2009        )
2010        && matches!(
2011            &base.term,
2012            NormalizedParticleTerm::Group(group)
2013                if group.compositor == Compositor::Choice
2014                    && base.min_occurs == 0
2015                    && base.max_occurs == Some(1)
2016                    && group.particles.len() > 1
2017        )
2018    {
2019        return false;
2020    }
2021
2022    if let Some(base_branches) = expand_choice_branches(base) {
2023        if let Some(derived_branches) = expand_choice_branches(derived) {
2024            // XSD 1.0 RecurseLax: order-preserving mapping required.
2025            // XSD 1.1: unordered set-based matching.
2026            if schema_set.is_xsd10() {
2027                return choice_branches_restrict_ordered(
2028                    schema_set,
2029                    &derived_branches,
2030                    &base_branches,
2031                );
2032            }
2033            // XSD 1.1: each derived branch must restrict some base branch —
2034            // OR, when the derived branch is emptiable and at least one base
2035            // branch is emptiable, the derived branch's empty production is
2036            // covered by that emptiable base branch and the non-empty form
2037            // (min≥1) must restrict some base branch. This handles cases
2038            // like addB118 where the derived choice is optional (min=0) but
2039            // no single base choice branch is emptiable AND accepts the
2040            // derived's elements — the union of branches covers it.
2041            let base_has_emptiable_branch = base_branches.iter().any(particle_is_emptiable);
2042            return derived_branches.iter().all(|branch| {
2043                if base_branches
2044                    .iter()
2045                    .any(|candidate| particle_restricts(schema_set, branch, candidate))
2046                {
2047                    return true;
2048                }
2049                if branch.min_occurs == 0 && base_has_emptiable_branch {
2050                    let mut non_empty = branch.clone();
2051                    non_empty.min_occurs = non_empty.min_occurs.max(1);
2052                    if non_empty.max_occurs.is_some_and(|m| m == 0) {
2053                        // original was min=0,max=0 (empty); empty production
2054                        // alone is covered, no non-empty form to check.
2055                        return true;
2056                    }
2057                    return base_branches
2058                        .iter()
2059                        .any(|candidate| particle_restricts(schema_set, &non_empty, candidate));
2060                }
2061                false
2062            });
2063        }
2064
2065        // Sequence-vs-choice: dedicated handler instead of "any branch" check.
2066        if let NormalizedParticleTerm::Group(derived_group) = &derived.term {
2067            if derived_group.compositor == Compositor::Sequence {
2068                // XSD 1.1: try "restricts any single branch" first.
2069                // Sound because if derived restricts one branch, it restricts
2070                // a subset of the choice's language.
2071                if schema_set.is_xsd11() {
2072                    let any_branch = base_branches
2073                        .iter()
2074                        .any(|candidate| particle_restricts(schema_set, derived, candidate));
2075                    if any_branch {
2076                        return true;
2077                    }
2078                }
2079                let NormalizedParticleTerm::Group(base_group) = &base.term else {
2080                    unreachable!()
2081                };
2082                return sequence_restricts_choice(
2083                    schema_set,
2084                    derived,
2085                    derived_group,
2086                    base,
2087                    base_group,
2088                );
2089            }
2090        }
2091
2092        return base_branches
2093            .iter()
2094            .any(|candidate| particle_restricts(schema_set, derived, candidate));
2095    }
2096
2097    if let Some(derived_branches) = expand_choice_branches(derived) {
2098        return derived_branches
2099            .iter()
2100            .all(|branch| particle_restricts(schema_set, branch, base));
2101    }
2102
2103    match (&derived.term, &base.term) {
2104        (
2105            NormalizedParticleTerm::Element(derived_element),
2106            NormalizedParticleTerm::Element(base_element),
2107        ) => {
2108            let names_match = derived_element.name == base_element.name
2109                && derived_element.namespace == base_element.namespace;
2110            let subst_match = !names_match
2111                && match (derived_element.element_key, base_element.element_key) {
2112                    (Some(d_key), Some(b_key)) => {
2113                        crate::compiler::substitution::is_element_substitutable_for(
2114                            schema_set, b_key, d_key,
2115                        )
2116                    }
2117                    _ => false,
2118                };
2119            // NameAndTypeOK (§3.9.6):
2120            // 1. Names match or substitution group
2121            (names_match || subst_match)
2122            // 2. Occurrence range subset
2123            && occurs_range_is_subset(
2124                derived.min_occurs,
2125                derived.max_occurs,
2126                base.min_occurs,
2127                base.max_occurs,
2128            )
2129            // 3. nillable: derived nillable only if base nillable
2130            && (base_element.nillable || !derived_element.nillable)
2131            // 4. fixed value: if base is fixed, derived must be fixed with same value (value-space)
2132            && match (&base_element.fixed_value, &derived_element.fixed_value) {
2133                (None, _) => true,
2134                (Some(_), None) => false,
2135                (Some(base_fixed), Some(derived_fixed)) => {
2136                    crate::validation::simple::fixed_values_equal(
2137                        derived_fixed,
2138                        base_fixed,
2139                        Some(derived_element.type_key),
2140                        schema_set,
2141                    )
2142                }
2143            }
2144            // TODO: 5. identity-constraint definitions subset (not yet implemented)
2145            // 6. block superset (masked to element-relevant bits)
2146            && derived_element.block.element_block_mask()
2147                .contains(base_element.block.element_block_mask())
2148            // 7. type derivation
2149            && schema_set.is_type_derived_from(
2150                derived_element.type_key,
2151                base_element.type_key,
2152                DerivationSet::extension(),
2153            )
2154        }
2155        (
2156            NormalizedParticleTerm::Element(element),
2157            NormalizedParticleTerm::Wildcard(base_wildcard),
2158        ) => {
2159            occurs_range_is_subset(
2160                derived.min_occurs,
2161                derived.max_occurs,
2162                base.min_occurs,
2163                base.max_occurs,
2164            ) && wildcard_allows_element(element, base_wildcard)
2165        }
2166        (
2167            NormalizedParticleTerm::Wildcard(derived_wildcard),
2168            NormalizedParticleTerm::Wildcard(base_wildcard),
2169        ) => {
2170            occurs_range_is_subset(
2171                derived.min_occurs,
2172                derived.max_occurs,
2173                base.min_occurs,
2174                base.max_occurs,
2175            ) && wildcard_restricts(derived_wildcard, base_wildcard)
2176        }
2177        (
2178            NormalizedParticleTerm::Group(derived_group),
2179            NormalizedParticleTerm::Wildcard(base_wildcard),
2180        ) => group_particle_restricts_wildcard(derived, derived_group, base, base_wildcard),
2181        (
2182            NormalizedParticleTerm::Group(derived_group),
2183            NormalizedParticleTerm::Group(base_group),
2184        ) if derived_group.compositor == base_group.compositor => {
2185            if !occurs_range_is_subset(
2186                derived.min_occurs,
2187                derived.max_occurs,
2188                base.min_occurs,
2189                base.max_occurs,
2190            ) {
2191                return false;
2192            }
2193            match derived_group.compositor {
2194                Compositor::Sequence => sequence_particles_restrict(
2195                    schema_set,
2196                    &derived_group.particles,
2197                    &base_group.particles,
2198                ),
2199                Compositor::All => all_particles_restrict(
2200                    schema_set,
2201                    &derived_group.particles,
2202                    &base_group.particles,
2203                ),
2204                Compositor::Choice => unreachable!("choice particles are handled earlier"),
2205            }
2206        }
2207        // Sequence:All — RecurseUnordered (§3.9.6): unordered bipartite
2208        // matching regardless of XSD version.  The XSD 1.0 ordered fallback
2209        // in all_particles_restrict is only correct for All:All (Recurse).
2210        (
2211            NormalizedParticleTerm::Group(derived_group),
2212            NormalizedParticleTerm::Group(base_group),
2213        ) if derived_group.compositor == Compositor::Sequence
2214            && base_group.compositor == Compositor::All =>
2215        {
2216            if !occurs_range_is_subset(
2217                derived.min_occurs,
2218                derived.max_occurs,
2219                base.min_occurs,
2220                base.max_occurs,
2221            ) {
2222                return false;
2223            }
2224            recurse_unordered(schema_set, &derived_group.particles, &base_group.particles)
2225        }
2226        // recurseAsIfGroup: wrap derived element/wildcard in an implicit group{1,1}
2227        // and check outer occurs before delegating to sequence/all matching.
2228        (
2229            NormalizedParticleTerm::Element(_) | NormalizedParticleTerm::Wildcard(_),
2230            NormalizedParticleTerm::Group(base_group),
2231        ) if base_group.compositor == Compositor::Sequence => {
2232            occurs_range_is_subset(1, Some(1), base.min_occurs, base.max_occurs)
2233                && sequence_particles_restrict(
2234                    schema_set,
2235                    std::slice::from_ref(derived),
2236                    &base_group.particles,
2237                )
2238        }
2239        (
2240            NormalizedParticleTerm::Element(_) | NormalizedParticleTerm::Wildcard(_),
2241            NormalizedParticleTerm::Group(base_group),
2242        ) if base_group.compositor == Compositor::All => {
2243            occurs_range_is_subset(1, Some(1), base.min_occurs, base.max_occurs)
2244                && all_particles_restrict(
2245                    schema_set,
2246                    std::slice::from_ref(derived),
2247                    &base_group.particles,
2248                )
2249        }
2250        _ => false,
2251    }
2252}
2253
2254fn group_particle_restricts_wildcard(
2255    derived: &NormalizedParticle,
2256    group: &NormalizedGroup,
2257    base: &NormalizedParticle,
2258    wildcard: &NormalizedWildcard,
2259) -> bool {
2260    let (derived_min, derived_max) = particle_total_occurrence_range(derived);
2261    if !occurs_range_is_subset(derived_min, derived_max, base.min_occurs, base.max_occurs) {
2262        return false;
2263    }
2264
2265    group_particles_fit_wildcard(&group.particles, wildcard)
2266}
2267
2268fn occurs_range_is_subset(
2269    derived_min: u32,
2270    derived_max: Option<u32>,
2271    base_min: u32,
2272    base_max: Option<u32>,
2273) -> bool {
2274    if derived_min < base_min {
2275        return false;
2276    }
2277
2278    match (derived_max, base_max) {
2279        (_, None) => true,
2280        (Some(derived), Some(base)) => derived <= base,
2281        (None, Some(_)) => false,
2282    }
2283}
2284
2285fn expand_choice_branches(particle: &NormalizedParticle) -> Option<Vec<NormalizedParticle>> {
2286    let NormalizedParticleTerm::Group(group) = &particle.term else {
2287        return None;
2288    };
2289    if group.compositor != Compositor::Choice {
2290        return None;
2291    }
2292
2293    Some(
2294        group
2295            .particles
2296            .iter()
2297            .map(|child| {
2298                let (min_occurs, max_occurs) = multiply_occurs(
2299                    particle.min_occurs,
2300                    particle.max_occurs,
2301                    child.min_occurs,
2302                    child.max_occurs,
2303                );
2304                collapse_single_child_groups(NormalizedParticle {
2305                    term: child.term.clone(),
2306                    min_occurs,
2307                    max_occurs,
2308                    source: particle.source.clone().or(child.source.clone()),
2309                })
2310            })
2311            .collect(),
2312    )
2313}
2314
2315fn particle_total_occurrence_range(particle: &NormalizedParticle) -> (u32, Option<u32>) {
2316    let (term_min, term_max) = match &particle.term {
2317        NormalizedParticleTerm::Element(_) | NormalizedParticleTerm::Wildcard(_) => (1, Some(1)),
2318        NormalizedParticleTerm::Group(group) => match group.compositor {
2319            Compositor::Sequence | Compositor::All => {
2320                group
2321                    .particles
2322                    .iter()
2323                    .fold((0u32, Some(0u32)), |(acc_min, acc_max), child| {
2324                        let (child_min, child_max) = particle_total_occurrence_range(child);
2325                        (
2326                            acc_min.saturating_add(child_min),
2327                            add_optional_occurs(acc_max, child_max),
2328                        )
2329                    })
2330            }
2331            Compositor::Choice => {
2332                let mut min_total: Option<u32> = None;
2333                let mut max_total: Option<Option<u32>> = None;
2334                for child in &group.particles {
2335                    let (child_min, child_max) = particle_total_occurrence_range(child);
2336                    min_total = Some(match min_total {
2337                        Some(current) => current.min(child_min),
2338                        None => child_min,
2339                    });
2340                    max_total = Some(match max_total {
2341                        Some(current) => max_optional_occurs(current, child_max),
2342                        None => child_max,
2343                    });
2344                }
2345                (min_total.unwrap_or(0), max_total.unwrap_or(Some(0)))
2346            }
2347        },
2348    };
2349
2350    multiply_occurs(particle.min_occurs, particle.max_occurs, term_min, term_max)
2351}
2352
2353fn add_optional_occurs(left: Option<u32>, right: Option<u32>) -> Option<u32> {
2354    match (left, right) {
2355        (Some(left), Some(right)) => Some(left.saturating_add(right)),
2356        _ => None,
2357    }
2358}
2359
2360fn max_optional_occurs(left: Option<u32>, right: Option<u32>) -> Option<u32> {
2361    match (left, right) {
2362        (Some(left), Some(right)) => Some(left.max(right)),
2363        _ => None,
2364    }
2365}
2366
2367fn group_particles_fit_wildcard(
2368    particles: &[NormalizedParticle],
2369    wildcard: &NormalizedWildcard,
2370) -> bool {
2371    particles
2372        .iter()
2373        .all(|particle| particle_fits_wildcard(particle, wildcard))
2374}
2375
2376fn particle_fits_wildcard(particle: &NormalizedParticle, wildcard: &NormalizedWildcard) -> bool {
2377    if let Some(branches) = expand_choice_branches(particle) {
2378        return branches
2379            .iter()
2380            .all(|branch| particle_fits_wildcard(branch, wildcard));
2381    }
2382
2383    match &particle.term {
2384        NormalizedParticleTerm::Element(element) => wildcard_allows_element(element, wildcard),
2385        NormalizedParticleTerm::Wildcard(derived_wildcard) => {
2386            wildcard_restricts(derived_wildcard, wildcard)
2387        }
2388        NormalizedParticleTerm::Group(group) => {
2389            group_particles_fit_wildcard(&group.particles, wildcard)
2390        }
2391    }
2392}
2393
2394/// Check whether a derived sequence restricts a base choice.
2395///
2396/// Two conditions are verified:
2397///
2398/// 1. **Per-particle match** — every child of the derived sequence must
2399///    restrict at least one *raw* base choice branch (name, type, and
2400///    per-iteration occurs).
2401///
2402/// 2. **Iteration budget** — each non-empty derived particle consumes at
2403///    least one choice iteration.  The total iterations across all sequence
2404///    repetitions must fit within the base choice's occurs range.
2405fn sequence_restricts_choice(
2406    schema_set: &SchemaSet,
2407    derived: &NormalizedParticle,
2408    derived_group: &NormalizedGroup,
2409    base: &NormalizedParticle,
2410    base_group: &NormalizedGroup,
2411) -> bool {
2412    let base_branches = &base_group.particles;
2413
2414    let mut required_per_iter: u32 = 0;
2415    let mut total_per_iter: u32 = 0;
2416
2417    for derived_child in &derived_group.particles {
2418        // Each derived child must restrict at least one raw base branch.
2419        let found = base_branches
2420            .iter()
2421            .any(|branch| particle_restricts(schema_set, derived_child, branch));
2422        if !found {
2423            return false;
2424        }
2425
2426        // Count choice-iteration demand per sequence iteration.
2427        if derived_child.min_occurs > 0 {
2428            required_per_iter += 1;
2429        }
2430        if derived_child.max_occurs != Some(0) {
2431            total_per_iter += 1;
2432        }
2433    }
2434
2435    // The total choice iterations across all sequence repetitions must fit
2436    // within the base choice's occurs range.
2437    let min_demand = derived.min_occurs.saturating_mul(required_per_iter);
2438    let max_demand = match derived.max_occurs {
2439        Some(m) => Some(m.saturating_mul(total_per_iter)),
2440        None => {
2441            if total_per_iter == 0 {
2442                Some(0)
2443            } else {
2444                None
2445            }
2446        }
2447    };
2448
2449    occurs_range_is_subset(min_demand, max_demand, base.min_occurs, base.max_occurs)
2450}
2451
2452/// XSD 1.0 RecurseLax: order-preserving matching of choice branches.
2453/// Each derived branch must map to a base branch at or after the previous match.
2454/// Unmatched base branches are implicitly skipped (lax, not strict).
2455fn choice_branches_restrict_ordered(
2456    schema_set: &SchemaSet,
2457    derived_branches: &[NormalizedParticle],
2458    base_branches: &[NormalizedParticle],
2459) -> bool {
2460    let mut base_index = 0;
2461    for derived in derived_branches {
2462        let mut found = false;
2463        while base_index < base_branches.len() {
2464            if particle_restricts(schema_set, derived, &base_branches[base_index]) {
2465                base_index += 1;
2466                found = true;
2467                break;
2468            }
2469            base_index += 1;
2470        }
2471        if !found {
2472            return false;
2473        }
2474    }
2475    true
2476}
2477
2478fn sequence_particles_restrict(
2479    schema_set: &SchemaSet,
2480    derived_particles: &[NormalizedParticle],
2481    base_particles: &[NormalizedParticle],
2482) -> bool {
2483    let mut base_index = 0;
2484    let mut derived_index = 0;
2485
2486    while derived_index < derived_particles.len() {
2487        let mut matched = false;
2488
2489        while let Some(base) = base_particles.get(base_index) {
2490            // 1. Direct particle-vs-particle match (the normal greedy step).
2491            if particle_restricts(schema_set, &derived_particles[derived_index], base) {
2492                matched = true;
2493                base_index += 1;
2494                derived_index += 1;
2495                break;
2496            }
2497
2498            // 2. Atomic sequence-unit match: when the base particle is a
2499            //    sequence group (e.g. an expanded repetition unit), try to
2500            //    match a contiguous slice of derived particles against the
2501            //    unit's children.  This keeps the unit atomic — either the
2502            //    full slice matches or we fall through.
2503            if let NormalizedParticleTerm::Group(base_group) = &base.term {
2504                if base_group.compositor == Compositor::Sequence && !base_group.particles.is_empty()
2505                {
2506                    let unit_len = base_group.particles.len();
2507                    let remaining = derived_particles.len() - derived_index;
2508                    if remaining >= unit_len
2509                        && sequence_particles_restrict(
2510                            schema_set,
2511                            &derived_particles[derived_index..derived_index + unit_len],
2512                            &base_group.particles,
2513                        )
2514                    {
2515                        matched = true;
2516                        base_index += 1;
2517                        derived_index += unit_len;
2518                        break;
2519                    }
2520                }
2521            }
2522
2523            // 3. XSD 1.1: expand choice in derived sequence.
2524            //    Each branch must independently work from the current base position
2525            //    for the entire remaining derived + base suffix.
2526            if schema_set.is_xsd11() {
2527                if let Some(branches) = expand_choice_branches(&derived_particles[derived_index]) {
2528                    let all_ok = branches.iter().all(|branch| {
2529                        let mut remaining = vec![branch.clone()];
2530                        remaining.extend_from_slice(&derived_particles[derived_index + 1..]);
2531                        sequence_particles_restrict(
2532                            schema_set,
2533                            &remaining,
2534                            &base_particles[base_index..],
2535                        )
2536                    });
2537                    if all_ok {
2538                        return true;
2539                    }
2540                }
2541            }
2542
2543            // 3a. XSD 1.1: inline unit-occurs derived sequence group.
2544            //    Compensates for the disabled flatten_same_compositor_groups.
2545            //    sequence{1,1}(a, b, c, ...) at derived can be inlined into
2546            //    the parent sequence and matched element-by-element against base.
2547            if schema_set.is_xsd11() {
2548                if let NormalizedParticleTerm::Group(dg) = &derived_particles[derived_index].term {
2549                    if dg.compositor == Compositor::Sequence
2550                        && occurs_is_unit(
2551                            derived_particles[derived_index].min_occurs,
2552                            derived_particles[derived_index].max_occurs,
2553                        )
2554                        && !dg.particles.is_empty()
2555                    {
2556                        let mut inlined = dg.particles.clone();
2557                        inlined.extend_from_slice(&derived_particles[derived_index + 1..]);
2558                        if sequence_particles_restrict(
2559                            schema_set,
2560                            &inlined,
2561                            &base_particles[base_index..],
2562                        ) {
2563                            return true;
2564                        }
2565                    }
2566                }
2567            }
2568
2569            // 4. Skip emptiable base particles.
2570            if particle_is_emptiable(base) {
2571                base_index += 1;
2572                continue;
2573            }
2574
2575            return false;
2576        }
2577
2578        if !matched {
2579            return false;
2580        }
2581    }
2582
2583    base_particles[base_index..]
2584        .iter()
2585        .all(particle_is_emptiable)
2586}
2587
2588/// Merge element particles in an unordered-matching context that share the
2589/// same expanded name (local + target namespace). The merged particle sums
2590/// `min_occurs` and `max_occurs` (unbounded on either side stays unbounded).
2591///
2592/// Used by `recurse_unordered` to support Sequence→All derivations where the
2593/// derived sequence lists the same element more than once (saxonData
2594/// All/all216 is the canonical case). Order within the derived side
2595/// doesn't affect the base's all-group language, so treating duplicate
2596/// names as one combined occurrence range is sound.
2597///
2598/// Non-element particles (wildcards, nested groups) are passed through
2599/// unchanged: collapsing them would change their matching semantics.
2600fn merge_duplicate_elements(particles: &[NormalizedParticle]) -> Vec<NormalizedParticle> {
2601    let mut merged: Vec<NormalizedParticle> = Vec::with_capacity(particles.len());
2602    for particle in particles {
2603        let NormalizedParticleTerm::Element(elem) = &particle.term else {
2604            merged.push(particle.clone());
2605            continue;
2606        };
2607        let existing = merged.iter_mut().find(|m| {
2608            matches!(
2609                &m.term,
2610                NormalizedParticleTerm::Element(other)
2611                    if other.name == elem.name && other.namespace == elem.namespace
2612            )
2613        });
2614        if let Some(existing) = existing {
2615            existing.min_occurs = existing.min_occurs.saturating_add(particle.min_occurs);
2616            existing.max_occurs = match (existing.max_occurs, particle.max_occurs) {
2617                (None, _) | (_, None) => None,
2618                (Some(a), Some(b)) => Some(a.saturating_add(b)),
2619            };
2620        } else {
2621            merged.push(particle.clone());
2622        }
2623    }
2624    merged
2625}
2626
2627/// RecurseUnordered: order-independent matching of derived particles against
2628/// the base all-group's particles. Combines two strategies:
2629///
2630/// 1. **Count-based bucket subsumption** (preferred — handles substitution
2631///    groups, wildcard partition, and choice expansion). Each derived
2632///    particle is assigned to a base "bucket" (by name match, substitution
2633///    group head, or wildcard subset). For each bucket the summed derived
2634///    occurrence range must fit within the base particle's range; unassigned
2635///    base particles must be emptiable.
2636///
2637/// 2. **Bipartite 1-to-1 matching** (fallback for derived particles that
2638///    contain nested groups not handled by the bucket approach). Each derived
2639///    particle must restrict some base particle; unmatched base particles
2640///    must be emptiable.
2641fn recurse_unordered(
2642    schema_set: &SchemaSet,
2643    derived_particles: &[NormalizedParticle],
2644    base_particles: &[NormalizedParticle],
2645) -> bool {
2646    // Try count-based bucket subsumption first — it handles substitution
2647    // group merging, wildcard partition, and choice distribution.
2648    if let Some(result) = try_count_based_subsumption(schema_set, derived_particles, base_particles)
2649    {
2650        if result {
2651            return true;
2652        }
2653        // Bucket said "no" — but for the failure path we still try bipartite
2654        // because it can recover via different particle wirings (e.g. when
2655        // a derived particle could fit either an element or wildcard bucket
2656        // but the bucket heuristic picked the wrong one).
2657    }
2658
2659    // Fallback: bipartite 1-to-1 matching with same-name merge.
2660    fn backtrack(
2661        schema_set: &SchemaSet,
2662        derived_particles: &[NormalizedParticle],
2663        base_particles: &[NormalizedParticle],
2664        used: &mut [bool],
2665        derived_index: usize,
2666    ) -> bool {
2667        if derived_index == derived_particles.len() {
2668            return base_particles
2669                .iter()
2670                .enumerate()
2671                .all(|(index, particle)| used[index] || particle_is_emptiable(particle));
2672        }
2673
2674        for (base_index, base_particle) in base_particles.iter().enumerate() {
2675            if used[base_index]
2676                || !particle_restricts(schema_set, &derived_particles[derived_index], base_particle)
2677            {
2678                continue;
2679            }
2680            used[base_index] = true;
2681            if backtrack(
2682                schema_set,
2683                derived_particles,
2684                base_particles,
2685                used,
2686                derived_index + 1,
2687            ) {
2688                return true;
2689            }
2690            used[base_index] = false;
2691        }
2692
2693        false
2694    }
2695
2696    // Merge duplicate-name element particles in the derived side: unordered
2697    // matching counts total occurrences per element, not particle positions.
2698    let merged_owned;
2699    let derived_particles = if derived_particles
2700        .iter()
2701        .any(|p| matches!(&p.term, NormalizedParticleTerm::Element(_)))
2702        && has_duplicate_element_names(derived_particles)
2703    {
2704        merged_owned = merge_duplicate_elements(derived_particles);
2705        &merged_owned[..]
2706    } else {
2707        derived_particles
2708    };
2709
2710    let mut used = vec![false; base_particles.len()];
2711    backtrack(schema_set, derived_particles, base_particles, &mut used, 0)
2712}
2713
2714/// Count-based subsumption: bucket each derived particle by the base particle
2715/// it maps to, sum derived occurrence ranges per bucket, and check that the
2716/// summed range fits the base range. Unassigned base particles must be
2717/// emptiable.
2718///
2719/// Returns:
2720/// - `Some(true)` — the derived particles validly restrict the base under
2721///   count-based subsumption (substitution groups, wildcard partition, and
2722///   top-level derived choices are all handled).
2723/// - `Some(false)` — every derived particle was bucket-able but the
2724///   per-bucket sum exceeded the base range or an unassigned base particle
2725///   is not emptiable.
2726/// - `None` — at least one derived particle is a nested model group; the
2727///   caller should fall back to bipartite matching.
2728fn try_count_based_subsumption(
2729    schema_set: &SchemaSet,
2730    derived_particles: &[NormalizedParticle],
2731    base_particles: &[NormalizedParticle],
2732) -> Option<bool> {
2733    // Step 1: Expand top-level choices into optional alternatives.
2734    let expanded = expand_top_level_choices_for_unordered(derived_particles)?;
2735
2736    // Step 2: Bucket each expanded derived particle to a base index.
2737    let mut buckets: Vec<Vec<(u32, Option<u32>)>> = vec![Vec::new(); base_particles.len()];
2738    for derived in &expanded {
2739        // Nested groups are not bucket-able here.
2740        if matches!(&derived.term, NormalizedParticleTerm::Group(_)) {
2741            return None;
2742        }
2743
2744        match find_subsumption_bucket(schema_set, derived, base_particles)? {
2745            BucketAssignment::Single(idx) => {
2746                buckets[idx].push((derived.min_occurs, derived.max_occurs))
2747            }
2748            BucketAssignment::Partition(idxs) => {
2749                // Each base bucket the partition spans must be emptiable
2750                // (b_min = 0) since derived elements may not land in it,
2751                // and derived's max must fit each spanned base's max
2752                // (worst case: all elements land in one bucket).
2753                for &i in &idxs {
2754                    let base = &base_particles[i];
2755                    if base.min_occurs > 0 {
2756                        return Some(false);
2757                    }
2758                    if !occurs_max_fits(derived.max_occurs, base.max_occurs) {
2759                        return Some(false);
2760                    }
2761                }
2762                // Contribute (0, derived.max) to each spanned bucket — the
2763                // total never exceeds the partition's d_max in any one
2764                // bucket, but might be 0.
2765                for &i in &idxs {
2766                    buckets[i].push((0, derived.max_occurs));
2767                }
2768            }
2769            BucketAssignment::None => return Some(false),
2770        }
2771    }
2772
2773    // Step 3: Check each bucket's summed range fits the base range; unmatched
2774    // base particles must be emptiable.
2775    for (i, ranges) in buckets.iter().enumerate() {
2776        let base = &base_particles[i];
2777        if ranges.is_empty() {
2778            if !particle_is_emptiable(base) {
2779                return Some(false);
2780            }
2781        } else {
2782            let (sum_min, sum_max) =
2783                ranges
2784                    .iter()
2785                    .fold((0u32, Some(0u32)), |(amin, amax), &(min, max)| {
2786                        (amin.saturating_add(min), add_optional_occurs(amax, max))
2787                    });
2788            if !occurs_range_is_subset(sum_min, sum_max, base.min_occurs, base.max_occurs) {
2789                return Some(false);
2790            }
2791        }
2792    }
2793
2794    Some(true)
2795}
2796
2797/// Expand top-level derived choices into a flat list of element/wildcard
2798/// particles, treating each branch as an optional alternative. This enables
2799/// the count-based subsumption to handle cases like all234 where a derived
2800/// sequence contains a `<xs:choice>` that distributes across base all-group
2801/// particles.
2802///
2803/// Returns `None` if any derived particle contains a nested group whose
2804/// shape can't be flattened (the caller falls back to bipartite matching).
2805fn expand_top_level_choices_for_unordered(
2806    particles: &[NormalizedParticle],
2807) -> Option<Vec<NormalizedParticle>> {
2808    let mut result = Vec::with_capacity(particles.len());
2809    for p in particles {
2810        match &p.term {
2811            NormalizedParticleTerm::Group(group) if group.compositor == Compositor::Choice => {
2812                // Each branch becomes a particle with min=0 (might not be picked)
2813                // and max = outer_max * branch_max.
2814                let outer_max = p.max_occurs;
2815                for branch in &group.particles {
2816                    if matches!(&branch.term, NormalizedParticleTerm::Group(_)) {
2817                        // Nested group inside choice — bail to bipartite.
2818                        return None;
2819                    }
2820                    let new_max = match (outer_max, branch.max_occurs) {
2821                        (Some(om), Some(bm)) => Some(om.saturating_mul(bm)),
2822                        _ => None,
2823                    };
2824                    result.push(NormalizedParticle {
2825                        term: branch.term.clone(),
2826                        min_occurs: 0,
2827                        max_occurs: new_max,
2828                        source: branch.source.clone(),
2829                    });
2830                }
2831            }
2832            NormalizedParticleTerm::Group(_) => {
2833                // Other nested groups (Sequence, All) — bail to bipartite.
2834                return None;
2835            }
2836            _ => result.push(p.clone()),
2837        }
2838    }
2839    Some(result)
2840}
2841
2842/// How a derived particle is assigned to base particle bucket(s).
2843#[derive(Debug, Clone)]
2844enum BucketAssignment {
2845    /// derived maps to a single base particle.
2846    Single(usize),
2847    /// derived (necessarily a wildcard) partitions across multiple base
2848    /// wildcards — its admissible (ns, name) set is covered by the union of
2849    /// the listed base wildcards.
2850    Partition(Vec<usize>),
2851    /// derived has no matching base particle (restriction is invalid).
2852    None,
2853}
2854
2855/// Find which base particle bucket(s) the derived particle should be assigned
2856/// to.
2857///
2858/// Returns:
2859/// - `Some(BucketAssignment::Single(idx))` — derived maps to base particle `idx`.
2860/// - `Some(BucketAssignment::Partition(idxs))` — derived wildcard partitions
2861///   across multiple base wildcards (wild049-style restriction).
2862/// - `Some(BucketAssignment::None)` — derived has no matching base particle.
2863/// - `None` — derived is a nested group that can't be bucketed (caller
2864///   should fall back to bipartite matching).
2865///
2866/// Search order:
2867/// 1. Direct element name + namespace match (NameAndTypeOK without occurs).
2868/// 2. Substitution group head match.
2869/// 3. Element fitting a base wildcard.
2870/// 4. Wildcard subset of a base wildcard.
2871/// 5. Wildcard subset of the union of multiple base wildcards (partition).
2872fn find_subsumption_bucket(
2873    schema_set: &SchemaSet,
2874    derived: &NormalizedParticle,
2875    base_particles: &[NormalizedParticle],
2876) -> Option<BucketAssignment> {
2877    match &derived.term {
2878        NormalizedParticleTerm::Element(d_elem) => {
2879            // 1. Direct name match
2880            for (i, base) in base_particles.iter().enumerate() {
2881                if let NormalizedParticleTerm::Element(b_elem) = &base.term {
2882                    if d_elem.name == b_elem.name
2883                        && d_elem.namespace == b_elem.namespace
2884                        && name_and_type_ok_no_occurs(schema_set, d_elem, b_elem)
2885                    {
2886                        return Some(BucketAssignment::Single(i));
2887                    }
2888                }
2889            }
2890            // 2. Substitution group head match
2891            for (i, base) in base_particles.iter().enumerate() {
2892                if let NormalizedParticleTerm::Element(b_elem) = &base.term {
2893                    if d_elem.name == b_elem.name && d_elem.namespace == b_elem.namespace {
2894                        continue; // already tried
2895                    }
2896                    if derived_element_substitutes_base(schema_set, d_elem, b_elem)
2897                        && name_and_type_ok_no_occurs(schema_set, d_elem, b_elem)
2898                    {
2899                        return Some(BucketAssignment::Single(i));
2900                    }
2901                }
2902            }
2903            // 3. Element fits base wildcard
2904            for (i, base) in base_particles.iter().enumerate() {
2905                if let NormalizedParticleTerm::Wildcard(b_wc) = &base.term {
2906                    if wildcard_allows_element(d_elem, b_wc) {
2907                        return Some(BucketAssignment::Single(i));
2908                    }
2909                }
2910            }
2911            Some(BucketAssignment::None)
2912        }
2913        NormalizedParticleTerm::Wildcard(d_wc) => {
2914            // 4. Wildcard subset of base wildcard
2915            for (i, base) in base_particles.iter().enumerate() {
2916                if let NormalizedParticleTerm::Wildcard(b_wc) = &base.term {
2917                    if wildcard_restricts(d_wc, b_wc) {
2918                        return Some(BucketAssignment::Single(i));
2919                    }
2920                }
2921            }
2922            // 5. Wildcard subset of the union of multiple base wildcards.
2923            // Collect all base wildcard indices and check coverage. Only
2924            // consider buckets that share the derived processContents
2925            // strictness or stronger.
2926            let candidate_idxs: Vec<usize> = base_particles
2927                .iter()
2928                .enumerate()
2929                .filter_map(|(i, base)| {
2930                    let NormalizedParticleTerm::Wildcard(b_wc) = &base.term else {
2931                        return None;
2932                    };
2933                    if process_contents_strictness(d_wc.wildcard.process_contents)
2934                        < process_contents_strictness(b_wc.wildcard.process_contents)
2935                    {
2936                        return None;
2937                    }
2938                    Some(i)
2939                })
2940                .collect();
2941            if candidate_idxs.len() >= 2 {
2942                let bases: Vec<&NormalizedWildcard> = candidate_idxs
2943                    .iter()
2944                    .map(|&i| match &base_particles[i].term {
2945                        NormalizedParticleTerm::Wildcard(b_wc) => b_wc.as_ref(),
2946                        _ => unreachable!(),
2947                    })
2948                    .collect();
2949                if let Some(spanned) = wildcard_subset_of_union(d_wc, &bases) {
2950                    let idxs: Vec<usize> = spanned
2951                        .into_iter()
2952                        .map(|local_idx| candidate_idxs[local_idx])
2953                        .collect();
2954                    return Some(BucketAssignment::Partition(idxs));
2955                }
2956            }
2957            Some(BucketAssignment::None)
2958        }
2959        NormalizedParticleTerm::Group(_) => None,
2960    }
2961}
2962
2963/// Return the smallest occurs-max that fits both `derived_max` and
2964/// `base_max`, treating `None` as unbounded. `derived_max` must fit within
2965/// `base_max`.
2966fn occurs_max_fits(derived_max: Option<u32>, base_max: Option<u32>) -> bool {
2967    match (derived_max, base_max) {
2968        (_, None) => true,
2969        (None, Some(_)) => false,
2970        (Some(d), Some(b)) => d <= b,
2971    }
2972}
2973
2974/// NameAndTypeOK clauses 3, 4, 6, 7 (everything except clause 2 — the
2975/// occurrence range subset check). Used by count-based subsumption where
2976/// the occurrence check is performed at the bucket aggregate level.
2977fn name_and_type_ok_no_occurs(
2978    schema_set: &SchemaSet,
2979    derived: &NormalizedElement,
2980    base: &NormalizedElement,
2981) -> bool {
2982    // Clause 3: derived nillable only if base nillable.
2983    if derived.nillable && !base.nillable {
2984        return false;
2985    }
2986    // Clause 4: fixed value.
2987    match (&base.fixed_value, &derived.fixed_value) {
2988        (None, _) => {}
2989        (Some(_), None) => return false,
2990        (Some(base_fixed), Some(derived_fixed)) => {
2991            if !crate::validation::simple::fixed_values_equal(
2992                derived_fixed,
2993                base_fixed,
2994                Some(derived.type_key),
2995                schema_set,
2996            ) {
2997                return false;
2998            }
2999        }
3000    }
3001    // Clause 6: block superset (masked to element bits).
3002    if !derived
3003        .block
3004        .element_block_mask()
3005        .contains(base.block.element_block_mask())
3006    {
3007        return false;
3008    }
3009    // Clause 7: type derivation.
3010    schema_set.is_type_derived_from(derived.type_key, base.type_key, DerivationSet::extension())
3011}
3012
3013/// Check whether the derived element is a substitution group member of the
3014/// base element. Looks up the global element by name when the derived
3015/// element_key is None (local declaration), per W3C bug 5296 — a local
3016/// element can match a substitution group via its global namesake.
3017fn derived_element_substitutes_base(
3018    schema_set: &SchemaSet,
3019    derived: &NormalizedElement,
3020    base: &NormalizedElement,
3021) -> bool {
3022    let d_key = derived
3023        .element_key
3024        .or_else(|| schema_set.lookup_element(derived.namespace, derived.name));
3025    let b_key = base
3026        .element_key
3027        .or_else(|| schema_set.lookup_element(base.namespace, base.name));
3028    match (d_key, b_key) {
3029        (Some(d), Some(b)) => {
3030            crate::compiler::substitution::is_element_substitutable_for(schema_set, b, d)
3031        }
3032        _ => false,
3033    }
3034}
3035
3036/// True when two or more element particles in the slice share the same
3037/// expanded name — a cheap check to avoid the allocation in
3038/// `merge_duplicate_elements` for the overwhelming-majority case where
3039/// every element particle is unique.
3040fn has_duplicate_element_names(particles: &[NormalizedParticle]) -> bool {
3041    for (i, a) in particles.iter().enumerate() {
3042        let NormalizedParticleTerm::Element(a_elem) = &a.term else {
3043            continue;
3044        };
3045        for b in &particles[i + 1..] {
3046            if let NormalizedParticleTerm::Element(b_elem) = &b.term {
3047                if a_elem.name == b_elem.name && a_elem.namespace == b_elem.namespace {
3048                    return true;
3049                }
3050            }
3051        }
3052    }
3053    false
3054}
3055
3056fn all_particles_restrict(
3057    schema_set: &SchemaSet,
3058    derived_particles: &[NormalizedParticle],
3059    base_particles: &[NormalizedParticle],
3060) -> bool {
3061    // XSD 1.0: All:All uses order-preserving Recurse (same as Sequence:Sequence).
3062    // XSD 1.1: RecurseUnordered allows reordering via backtracking.
3063    if schema_set.is_xsd10() {
3064        return sequence_particles_restrict(schema_set, derived_particles, base_particles);
3065    }
3066    recurse_unordered(schema_set, derived_particles, base_particles)
3067}
3068
3069fn particle_is_emptiable(particle: &NormalizedParticle) -> bool {
3070    if particle.min_occurs == 0 {
3071        return true;
3072    }
3073
3074    match &particle.term {
3075        NormalizedParticleTerm::Element(_) | NormalizedParticleTerm::Wildcard(_) => false,
3076        NormalizedParticleTerm::Group(group) => match group.compositor {
3077            Compositor::Sequence | Compositor::All => {
3078                group.particles.iter().all(particle_is_emptiable)
3079            }
3080            Compositor::Choice => group.particles.iter().any(particle_is_emptiable),
3081        },
3082    }
3083}
3084
3085fn wildcard_restricts(derived: &NormalizedWildcard, base: &NormalizedWildcard) -> bool {
3086    is_wildcard_ns_subset(
3087        &derived.wildcard,
3088        derived.target_namespace,
3089        &base.wildcard,
3090        base.target_namespace,
3091    ) && process_contents_strictness(derived.wildcard.process_contents)
3092        >= process_contents_strictness(base.wildcard.process_contents)
3093}
3094
3095/// Check whether the derived wildcard's admissible (namespace, name) set is
3096/// covered by the union of the given base wildcards. Returns the indices of
3097/// the bases that participate in the partition (those that admit at least
3098/// one (ns, name) admitted by derived); returns `None` if the union doesn't
3099/// cover derived.
3100///
3101/// Implements the partition extension of NSSubset (§3.10.6.2): a single
3102/// derived wildcard restricts a base content model if every (ns, name)
3103/// admitted by derived is admitted by some base wildcard. Used for cases
3104/// like wild049 where the base type's xs:all has multiple wildcards
3105/// partitioning the namespace space, and the derived sequence has a single
3106/// wildcard whose admissions span both base wildcards.
3107fn wildcard_subset_of_union(
3108    derived: &NormalizedWildcard,
3109    bases: &[&NormalizedWildcard],
3110) -> Option<Vec<usize>> {
3111    use crate::parser::frames::NotQNameItem;
3112
3113    let derived_target = derived.target_namespace;
3114
3115    // Collect all explicit namespace witnesses from both sides.
3116    let mut explicit_namespaces: Vec<Option<NameId>> = Vec::new();
3117    let push_ns = |ns: Option<NameId>, out: &mut Vec<Option<NameId>>| {
3118        if !out.contains(&ns) {
3119            out.push(ns);
3120        }
3121    };
3122
3123    let collect_namespaces = |wc: &NormalizedWildcard, out: &mut Vec<Option<NameId>>| {
3124        let target = wc.target_namespace;
3125        match &wc.wildcard.namespace {
3126            WildcardNamespace::TargetNamespace => {
3127                if !out.contains(&target) {
3128                    out.push(target);
3129                }
3130            }
3131            WildcardNamespace::Local => {
3132                if !out.contains(&None) {
3133                    out.push(None);
3134                }
3135            }
3136            WildcardNamespace::List(tokens) => {
3137                for t in tokens {
3138                    let ns = t.resolve(target);
3139                    if !out.contains(&ns) {
3140                        out.push(ns);
3141                    }
3142                }
3143            }
3144            _ => {}
3145        }
3146        for t in &wc.wildcard.not_namespace {
3147            let ns = t.resolve(target);
3148            if !out.contains(&ns) {
3149                out.push(ns);
3150            }
3151        }
3152        for item in &wc.wildcard.not_qname {
3153            if let NotQNameItem::QName { namespace, .. } = item {
3154                if !out.contains(namespace) {
3155                    out.push(*namespace);
3156                }
3157            }
3158        }
3159    };
3160
3161    collect_namespaces(derived, &mut explicit_namespaces);
3162    for base in bases {
3163        collect_namespaces(base, &mut explicit_namespaces);
3164    }
3165    push_ns(derived_target, &mut explicit_namespaces);
3166    for base in bases {
3167        push_ns(base.target_namespace, &mut explicit_namespaces);
3168    }
3169    push_ns(None, &mut explicit_namespaces);
3170
3171    let mut spanned: Vec<usize> = Vec::new();
3172
3173    let check_namespace = |ns: Option<NameId>,
3174                           is_explicit_ns: bool,
3175                           bases: &[&NormalizedWildcard],
3176                           spanned: &mut Vec<usize>|
3177     -> bool {
3178        if !wildcard_admits_ns(derived, ns) {
3179            return true;
3180        }
3181        // Find admitting bases (their indices in `bases`).
3182        let admitting: Vec<usize> = (0..bases.len())
3183            .filter(|&i| wildcard_admits_ns(bases[i], ns))
3184            .collect();
3185        if admitting.is_empty() {
3186            return false;
3187        }
3188        // For each name witness in this namespace, check coverage.
3189        let mut name_witnesses: Vec<NameId> = Vec::new();
3190        let push_name = |n: NameId, out: &mut Vec<NameId>| {
3191            if !out.contains(&n) {
3192                out.push(n);
3193            }
3194        };
3195        for item in &derived.wildcard.not_qname {
3196            if let NotQNameItem::QName {
3197                namespace,
3198                local_name,
3199            } = item
3200            {
3201                if *namespace == ns {
3202                    push_name(*local_name, &mut name_witnesses);
3203                }
3204            }
3205        }
3206        for &i in &admitting {
3207            for item in &bases[i].wildcard.not_qname {
3208                if let NotQNameItem::QName {
3209                    namespace,
3210                    local_name,
3211                } = item
3212                {
3213                    if *namespace == ns {
3214                        push_name(*local_name, &mut name_witnesses);
3215                    }
3216                }
3217            }
3218        }
3219        for name in &name_witnesses {
3220            if !wildcard_admits_qname(derived, ns, *name) {
3221                continue;
3222            }
3223            let any_admits = admitting
3224                .iter()
3225                .any(|&i| wildcard_admits_qname(bases[i], ns, *name));
3226            if !any_admits {
3227                return false;
3228            }
3229        }
3230        // "Any other name" witness: derived admits some name not in the
3231        // explicit witness set if and only if no `##defined`/`##definedSibling`
3232        // entry catches it. If derived admits this symbolic case, at least
3233        // one base must too.
3234        let derived_admits_other = wildcard_admits_qname_symbolic_other(derived, ns);
3235        if derived_admits_other {
3236            let any_admits_other = admitting
3237                .iter()
3238                .any(|&i| wildcard_admits_qname_symbolic_other(bases[i], ns));
3239            if !any_admits_other {
3240                return false;
3241            }
3242        }
3243        // Record participating bases. For explicit-namespace witnesses, we
3244        // know derived admits at least one name in this namespace, so each
3245        // admitting base participates; for the symbolic "any-other-namespace"
3246        // sentinel we'd over-count, so the caller marks those separately.
3247        if is_explicit_ns {
3248            for &i in &admitting {
3249                if !spanned.contains(&i) {
3250                    spanned.push(i);
3251                }
3252            }
3253        }
3254        true
3255    };
3256
3257    // Check each explicit namespace witness.
3258    for ns in &explicit_namespaces {
3259        if !check_namespace(*ns, true, bases, &mut spanned) {
3260            return None;
3261        }
3262    }
3263
3264    // Symbolic "fresh namespace" witness: a namespace not in any explicit
3265    // set. Use a sentinel NameId guaranteed not to appear (NameId::MAX).
3266    let fresh_ns = Some(NameId(u32::MAX));
3267    let derived_admits_fresh =
3268        wildcard_admits_ns(derived, fresh_ns) || wildcard_admits_ns(derived, None);
3269    if derived_admits_fresh {
3270        // The "fresh ns" sentinel covers any unmentioned namespace; we need
3271        // at least one base to admit it for partition coverage to work.
3272        let mut fresh_bases: Vec<usize> = (0..bases.len())
3273            .filter(|&i| wildcard_admits_ns(bases[i], fresh_ns))
3274            .collect();
3275        if wildcard_admits_ns(derived, fresh_ns) && fresh_bases.is_empty() {
3276            return None;
3277        }
3278        for &i in &fresh_bases {
3279            if !spanned.contains(&i) {
3280                spanned.push(i);
3281            }
3282        }
3283        fresh_bases.clear();
3284    }
3285
3286    if spanned.is_empty() {
3287        // Derived admits nothing — degenerate, treat as covered by no bases.
3288        return Some(spanned);
3289    }
3290    Some(spanned)
3291}
3292
3293/// Whether the wildcard admits `ns` in its `{namespace constraint}` after
3294/// applying `notNamespace`.
3295fn wildcard_admits_ns(wc: &NormalizedWildcard, ns: Option<NameId>) -> bool {
3296    if !wildcard_namespace_matches(&wc.wildcard.namespace, ns, wc.target_namespace) {
3297        return false;
3298    }
3299    !wc.wildcard
3300        .not_namespace
3301        .iter()
3302        .any(|t| t.resolve(wc.target_namespace) == ns)
3303}
3304
3305/// Whether the wildcard admits the QName `(ns, name)` after applying
3306/// `notNamespace` and `notQName`. `##defined` and `##definedSibling` are
3307/// treated pessimistically — if either appears, the QName is rejected, since
3308/// at schema-time we cannot resolve which names they catch.
3309fn wildcard_admits_qname(wc: &NormalizedWildcard, ns: Option<NameId>, name: NameId) -> bool {
3310    use crate::parser::frames::NotQNameItem;
3311    if !wildcard_admits_ns(wc, ns) {
3312        return false;
3313    }
3314    !wc.wildcard.not_qname.iter().any(|item| match item {
3315        NotQNameItem::QName {
3316            namespace,
3317            local_name,
3318        } => *namespace == ns && *local_name == name,
3319        NotQNameItem::Defined | NotQNameItem::DefinedSibling => true,
3320    })
3321}
3322
3323/// Whether the wildcard admits a "symbolic other name" in `ns` — i.e., some
3324/// name that doesn't appear in any explicit `notQName` entry. The check
3325/// fails only if `##defined`/`##definedSibling` would catch any name, since
3326/// concrete QName entries match only specific names.
3327fn wildcard_admits_qname_symbolic_other(wc: &NormalizedWildcard, ns: Option<NameId>) -> bool {
3328    use crate::parser::frames::NotQNameItem;
3329    if !wildcard_admits_ns(wc, ns) {
3330        return false;
3331    }
3332    !wc.wildcard
3333        .not_qname
3334        .iter()
3335        .any(|item| matches!(item, NotQNameItem::Defined | NotQNameItem::DefinedSibling))
3336}
3337
3338fn wildcard_allows_element(element: &NormalizedElement, wildcard: &NormalizedWildcard) -> bool {
3339    if !wildcard_namespace_matches(
3340        &wildcard.wildcard.namespace,
3341        element.namespace,
3342        wildcard.target_namespace,
3343    ) {
3344        return false;
3345    }
3346
3347    let excluded_namespace = wildcard
3348        .wildcard
3349        .not_namespace
3350        .iter()
3351        .map(|token| token.resolve(wildcard.target_namespace))
3352        .any(|namespace| namespace == element.namespace);
3353    if excluded_namespace {
3354        return false;
3355    }
3356
3357    !wildcard_not_qname_excludes(
3358        &wildcard.wildcard.not_qname,
3359        element.namespace,
3360        element.name,
3361    )
3362}
3363
3364fn wildcard_namespace_matches(
3365    namespace: &WildcardNamespace,
3366    element_namespace: Option<NameId>,
3367    target_namespace: Option<NameId>,
3368) -> bool {
3369    match namespace {
3370        WildcardNamespace::Any => true,
3371        WildcardNamespace::Other => {
3372            !other_exclusion_set(target_namespace).contains(&element_namespace)
3373        }
3374        WildcardNamespace::TargetNamespace => element_namespace == target_namespace,
3375        WildcardNamespace::Local => element_namespace.is_none(),
3376        WildcardNamespace::List(tokens) => tokens
3377            .iter()
3378            .map(|token| token.resolve(target_namespace))
3379            .any(|resolved| resolved == element_namespace),
3380    }
3381}
3382
3383fn wildcard_not_qname_excludes(
3384    not_qname: &[crate::parser::frames::NotQNameItem],
3385    namespace: Option<NameId>,
3386    local_name: NameId,
3387) -> bool {
3388    not_qname.iter().any(|item| match item {
3389        crate::parser::frames::NotQNameItem::QName {
3390            namespace: excluded_ns,
3391            local_name: excluded_name,
3392        } => *excluded_ns == namespace && *excluded_name == local_name,
3393        crate::parser::frames::NotQNameItem::Defined => true,
3394        crate::parser::frames::NotQNameItem::DefinedSibling => false,
3395    })
3396}
3397
3398// ---------------------------------------------------------------------------
3399// XSD 1.1: Open-content derivation helpers
3400// ---------------------------------------------------------------------------
3401
3402/// Return the effective open content, treating `mode=None` as absent.
3403///
3404/// `compile.rs::open_content_from_result` collapses `mode=None` to `None`,
3405/// so derivation validation must agree: a raw `OpenContentResult` with
3406/// `mode=None` is semantically equivalent to no open content.
3407#[cfg(feature = "xsd11")]
3408fn effective_open_content(oc: Option<&OpenContentResult>) -> Option<&OpenContentResult> {
3409    oc.filter(|o| o.mode != OpenContentMode::None)
3410}
3411
3412/// Map processContents to a strictness level (Strict=2, Lax=1, Skip=0).
3413fn process_contents_strictness(pc: ProcessContents) -> u8 {
3414    match pc {
3415        ProcessContents::Strict => 2,
3416        ProcessContents::Lax => 1,
3417        ProcessContents::Skip => 0,
3418    }
3419}
3420
3421/// Compute the exclusion set for `##other` per the spec (§3.10.1):
3422/// `namespace="##other"` maps to `not({target namespace}, absent)`.
3423/// The result always contains `None` (absent) and, if the target namespace
3424/// is present, also contains `Some(target_ns)`.
3425fn other_exclusion_set(target_ns: Option<NameId>) -> Vec<Option<NameId>> {
3426    match target_ns {
3427        Some(ns) => vec![Some(ns), None],
3428        None => vec![None],
3429    }
3430}
3431
3432/// Resolve a `WildcardNamespace` to a set of effective `Option<NameId>` values
3433/// that the wildcard **allows** (positive set).
3434///
3435/// Returns `None` for unbounded / complement constraints (`Any`, `Other`)
3436/// that cannot be represented as a finite positive set — callers must handle
3437/// those structurally.
3438fn resolve_ns_set(
3439    wns: &WildcardNamespace,
3440    target_ns: Option<NameId>,
3441) -> Option<Vec<Option<NameId>>> {
3442    match wns {
3443        WildcardNamespace::Any | WildcardNamespace::Other => None,
3444        WildcardNamespace::TargetNamespace => Some(vec![target_ns]),
3445        WildcardNamespace::Local => Some(vec![None]),
3446        WildcardNamespace::List(tokens) => {
3447            Some(tokens.iter().map(|t| t.resolve(target_ns)).collect())
3448        }
3449    }
3450}
3451
3452/// Check whether `derived` namespace constraint is a subset of `base`
3453/// (cos-ns-subset, §3.10.6.2).
3454///
3455/// Both constraints are resolved against their respective target namespaces
3456/// so that `##targetNamespace` and an explicit URI equal to the target
3457/// namespace are treated as equivalent.
3458///
3459/// Key spec detail: `##other` maps to `not({target namespace}, absent)`,
3460/// i.e. it **always** excludes both the target namespace and the absent
3461/// namespace (§3.10.1).
3462///
3463/// processContents is checked separately by the open-content derivation
3464/// validators.
3465fn is_namespace_subset(
3466    derived: &WildcardNamespace,
3467    derived_target_ns: Option<NameId>,
3468    base: &WildcardNamespace,
3469    base_target_ns: Option<NameId>,
3470) -> bool {
3471    match base {
3472        WildcardNamespace::Any => true,
3473
3474        WildcardNamespace::Other => {
3475            // base = not({base_target_ns, absent}).
3476            // Derived ⊆ base iff every namespace derived allows is also
3477            // allowed by base, i.e. is not in base's exclusion set.
3478            let base_excluded = other_exclusion_set(base_target_ns);
3479
3480            match derived {
3481                WildcardNamespace::Any => false,
3482
3483                WildcardNamespace::Other => {
3484                    // derived = not({derived_target_ns, absent}).
3485                    // Derived ⊆ base iff base_excluded ⊆ derived_excluded,
3486                    // i.e. derived excludes at least everything base excludes.
3487                    let derived_excluded = other_exclusion_set(derived_target_ns);
3488                    base_excluded.iter().all(|ns| derived_excluded.contains(ns))
3489                }
3490
3491                _ => {
3492                    // Finite positive set — every allowed ns must not be in
3493                    // base's exclusion set.
3494                    match resolve_ns_set(derived, derived_target_ns) {
3495                        Some(resolved) => resolved.iter().all(|ns| !base_excluded.contains(ns)),
3496                        None => false,
3497                    }
3498                }
3499            }
3500        }
3501
3502        WildcardNamespace::TargetNamespace
3503        | WildcardNamespace::Local
3504        | WildcardNamespace::List(_) => {
3505            // Base is a finite positive set — resolve both sides and check
3506            // set inclusion.
3507            let Some(base_set) = resolve_ns_set(base, base_target_ns) else {
3508                return false;
3509            };
3510            match derived {
3511                WildcardNamespace::Any | WildcardNamespace::Other => false,
3512                _ => {
3513                    let Some(derived_set) = resolve_ns_set(derived, derived_target_ns) else {
3514                        return false;
3515                    };
3516                    derived_set.iter().all(|ns| base_set.contains(ns))
3517                }
3518            }
3519        }
3520    }
3521}
3522
3523/// Check whether `derived` wildcard's namespace constraint is a subset of
3524/// `base` wildcard's, also considering notNamespace and notQName exclusions.
3525///
3526/// Implements cos-ns-subset (§3.10.6.2) — a pure namespace-constraint
3527/// relation.  processContents is NOT checked here; callers handle it
3528/// separately for extension vs restriction semantics.
3529///
3530/// `derived_target_ns` / `base_target_ns` are the effective target namespaces
3531/// of the schema documents that contain the derived / base types.
3532fn is_wildcard_ns_subset(
3533    derived: &WildcardResult,
3534    derived_target_ns: Option<NameId>,
3535    base: &WildcardResult,
3536    base_target_ns: Option<NameId>,
3537) -> bool {
3538    // Namespace constraint must be a subset
3539    if !is_namespace_subset(
3540        &derived.namespace,
3541        derived_target_ns,
3542        &base.namespace,
3543        base_target_ns,
3544    ) {
3545        return false;
3546    }
3547
3548    // notNamespace: for every namespace that base excludes, derived must
3549    // not allow it.  The naive "derived.not_namespace ⊇ base.not_namespace"
3550    // check over-rejects when derived's positive `{namespace constraint}`
3551    // already excludes the namespace by construction (e.g. derived is a
3552    // finite List whose members don't overlap base's notNamespace set).
3553    for base_excl in &base.not_namespace {
3554        let base_ns = base_excl.resolve(base_target_ns);
3555        let derived_allows =
3556            wildcard_namespace_matches(&derived.namespace, base_ns, derived_target_ns)
3557                && !derived
3558                    .not_namespace
3559                    .iter()
3560                    .any(|d| d.resolve(derived_target_ns) == base_ns);
3561        if derived_allows {
3562            return false;
3563        }
3564    }
3565
3566    // notQName: derived must exclude at least everything base excludes that
3567    // derived's namespace constraint actually admits. If derived's
3568    // {namespace constraint} ∪ notNamespace already excludes the QName's
3569    // namespace, base's exclusion is moot for the subset check.
3570    for item in &base.not_qname {
3571        match item {
3572            crate::parser::frames::NotQNameItem::QName { namespace, .. } => {
3573                let derived_admits_ns =
3574                    wildcard_namespace_matches(&derived.namespace, *namespace, derived_target_ns)
3575                        && !derived
3576                            .not_namespace
3577                            .iter()
3578                            .any(|t| t.resolve(derived_target_ns) == *namespace);
3579                if derived_admits_ns && !derived.not_qname.contains(item) {
3580                    return false;
3581                }
3582            }
3583            crate::parser::frames::NotQNameItem::Defined
3584            | crate::parser::frames::NotQNameItem::DefinedSibling => {
3585                if !derived.not_qname.contains(item) {
3586                    return false;
3587                }
3588            }
3589        }
3590    }
3591
3592    true
3593}
3594
3595/// Validate open-content compatibility for complex type extension (cos-ct-extends).
3596///
3597/// Implements §3.4.6.2 clauses 1.4.3.2.2 by comparing the **effective**
3598/// `{open content}` property of each type (BOT, EOT) per §3.4.2.3 clauses
3599/// 4–6, rather than the raw `<xs:openContent>` child elements.
3600///
3601/// EOT inherits from the base when the derivation omits `<openContent>` or
3602/// specifies `mode="none"` (clause 6.1); otherwise EOT's wildcard is the
3603/// union (§3.10.6.3 cos-aw-union) of the derivation's wildcard with the
3604/// base's (clause 6.2). This lets schemas like saxonData/Open/open027 (base
3605/// has suffix OC, derived declares none) and open047 (derivation widens the
3606/// wildcard via notNamespace) pass validation.
3607#[cfg(feature = "xsd11")]
3608fn validate_open_content_extension(
3609    schema_set: &SchemaSet,
3610    derived_key: ComplexTypeKey,
3611    derived: &crate::arenas::ComplexTypeDefData,
3612    base_key: ComplexTypeKey,
3613    base: &crate::arenas::ComplexTypeDefData,
3614) -> SchemaResult<()> {
3615    let bot = compute_effective_open_content(schema_set, base_key);
3616    let eot = compute_effective_open_content(schema_set, derived_key);
3617
3618    // Clause 1.4.3.2.2.3.1: if BOT is absent, extension is unconstrained wrt OC.
3619    let Some(bot) = bot else {
3620        return Ok(());
3621    };
3622
3623    let (location, type_name) = type_error_context(schema_set, derived);
3624    let base_name = format_type_name(schema_set, base.name, base.target_namespace);
3625
3626    // If BOT is present then EOT must be too (by construction of EOT: if
3627    // derivation has no OC and no mode=none override, clause 6.1 inherits BOT).
3628    // Reaching `None` here means the derivation's own `<openContent mode="none"/>`
3629    // plus an empty explicit content type collapsed EOT to absent in a way the
3630    // base does not satisfy — or, without that override, that the base chain
3631    // produced an OC but the derived chain didn't (mismatch).
3632    let Some(eot) = eot else {
3633        return Err(SchemaError::structural(
3634            "cos-ct-extends",
3635            format!(
3636                "Complex type '{}' extends '{}' which has open content, \
3637                 but derived type has no open content",
3638                type_name, base_name
3639            ),
3640            location,
3641        ));
3642    };
3643
3644    // Clause 1.4.3.2.2.3: either EOT.mode = interleave, or both modes = suffix.
3645    let mode_ok = eot.mode == OpenContentMode::Interleave
3646        || (bot.mode == OpenContentMode::Suffix && eot.mode == OpenContentMode::Suffix);
3647    if !mode_ok {
3648        return Err(SchemaError::structural(
3649            "cos-ct-extends",
3650            format!(
3651                "Complex type '{}' uses suffix open content mode but base type '{}' \
3652                 uses interleave mode — suffix cannot extend interleave",
3653                type_name, base_name
3654            ),
3655            location,
3656        ));
3657    }
3658
3659    // Clause 1.4.3.2.2.4: BOT.{wildcard}.{namespace constraint} ⊆ EOT.{wildcard}.
3660    if let (Some(bot_wc), Some(eot_wc)) = (bot.wildcard.as_ref(), eot.wildcard.as_ref()) {
3661        if !is_wildcard_ns_subset(
3662            bot_wc,
3663            base.target_namespace,
3664            eot_wc,
3665            derived.target_namespace,
3666        ) {
3667            return Err(SchemaError::structural(
3668                "cos-ct-extends",
3669                format!(
3670                    "Open content wildcard of '{}' is not a valid extension \
3671                     of base type '{}' wildcard",
3672                    type_name, base_name
3673                ),
3674                location,
3675            ));
3676        }
3677    }
3678
3679    Ok(())
3680}
3681
3682/// Effective `{open content}` property per §3.4.2.3 clauses 5–6.
3683///
3684/// Represents a non-absent open content: an absent OC is encoded as `None`
3685/// (returned by `compute_effective_open_content`). `target_namespace` is the
3686/// context for resolving any unresolved `##targetNamespace` tokens inside
3687/// `wildcard` — needed because a type's own `<openContent>` child is stored
3688/// with tokens in parser form.
3689#[cfg(feature = "xsd11")]
3690#[derive(Debug, Clone)]
3691struct EffectiveOpenContent {
3692    mode: OpenContentMode,
3693    wildcard: Option<WildcardResult>,
3694    target_namespace: Option<NameId>,
3695}
3696
3697/// Compute the effective `{open content}` of a complex type per §3.4.2.3
3698/// clauses 5 and 6. Walks the base chain through extension derivations.
3699///
3700/// Returns `None` when the type's effective OC is absent.
3701#[cfg(feature = "xsd11")]
3702fn compute_effective_open_content(
3703    schema_set: &SchemaSet,
3704    key: ComplexTypeKey,
3705) -> Option<EffectiveOpenContent> {
3706    compute_effective_open_content_bounded(schema_set, key, 0)
3707}
3708
3709#[cfg(feature = "xsd11")]
3710fn compute_effective_open_content_bounded(
3711    schema_set: &SchemaSet,
3712    key: ComplexTypeKey,
3713    depth: u32,
3714) -> Option<EffectiveOpenContent> {
3715    // Guard against pathological cycles — reference resolution should have
3716    // detected them upstream, but keep a local belt-and-braces cap.
3717    if depth > 100 {
3718        return None;
3719    }
3720    let type_data = schema_set.arenas.complex_types.get(key)?;
3721    let target_ns = type_data.target_namespace;
3722
3723    // Clause 5: select the "wildcard element" (the OC source for this type).
3724    // Clause 5.1 picks the <xs:openContent> child element regardless of its
3725    // @mode — a literal `mode="none"` still "corresponds" to the element per
3726    // the spec.  The clause-6.1 mode=none branch below handles that case,
3727    // short-circuiting the defaultOpenContent fallback that 5.2 would apply.
3728    let own_oc: Option<EffectiveOpenContent> =
3729        type_data
3730            .open_content
3731            .as_ref()
3732            .map(|oc| EffectiveOpenContent {
3733                mode: oc.mode,
3734                wildcard: oc.wildcard.clone(),
3735                target_namespace: target_ns,
3736            });
3737
3738    let wildcard_element: Option<EffectiveOpenContent> = if own_oc.is_some() {
3739        // Clause 5.1
3740        own_oc
3741    } else if let Some(default) = type_data
3742        .source
3743        .as_ref()
3744        .and_then(|s| schema_set.documents.get(s.defaults_doc() as usize))
3745        .and_then(|d| d.default_open_content.as_ref())
3746    {
3747        // Clause 5.2: schema-level <xs:defaultOpenContent> applies when
3748        // appliesToEmpty=true OR the explicit content type is non-empty.
3749        if default.applies_to_empty || !explicit_content_is_empty(schema_set, type_data, 0) {
3750            default_open_content_to_effective(default, target_ns)
3751        } else {
3752            None
3753        }
3754    } else {
3755        None // Clause 5.3
3756    };
3757
3758    // Base's effective OC (clause 4.2 inheritance). Only inherited across
3759    // extension; for restriction or anyType-derivation the base OC does not
3760    // flow into the derived explicit content type.
3761    let base_oc: Option<EffectiveOpenContent> = if matches!(
3762        type_data.derivation_method,
3763        Some(DerivationMethod::Extension)
3764    ) {
3765        match type_data.resolved_base_type {
3766            Some(TypeKey::Complex(base_key)) => {
3767                compute_effective_open_content_bounded(schema_set, base_key, depth + 1)
3768            }
3769            _ => None,
3770        }
3771    } else {
3772        None
3773    };
3774
3775    // Clause 6.1: absent / mode=None wildcard element → inherit base.
3776    let Some(we) = wildcard_element else {
3777        return base_oc;
3778    };
3779    if we.mode == OpenContentMode::None {
3780        return base_oc;
3781    }
3782
3783    // Clause 6.2: build a new OC record with the unioned wildcard.
3784    let wildcard = match (base_oc.as_ref(), we.wildcard.as_ref()) {
3785        (Some(b), Some(w)) => match &b.wildcard {
3786            Some(bw) => Some(wildcard_result_union(bw, b.target_namespace, w, target_ns)),
3787            None => Some(w.clone()),
3788        },
3789        (None, Some(w)) => Some(w.clone()),
3790        (Some(b), None) => b.wildcard.clone(),
3791        (None, None) => None,
3792    };
3793
3794    Some(EffectiveOpenContent {
3795        mode: we.mode,
3796        wildcard,
3797        target_namespace: target_ns,
3798    })
3799}
3800
3801/// Determine whether a complex type's *explicit content type* is empty per
3802/// §3.4.2.3 clause 3 (needed for clause 5.2.2's `appliesToEmpty` gate).
3803///
3804/// For extension with no derivation-level particle, the explicit content is
3805/// the base's — so recurse. For restriction or non-derivation types the
3806/// explicit content is the derivation's own content.
3807#[cfg(feature = "xsd11")]
3808fn explicit_content_is_empty(
3809    schema_set: &SchemaSet,
3810    type_data: &crate::arenas::ComplexTypeDefData,
3811    depth: u32,
3812) -> bool {
3813    if depth > 100 {
3814        return true;
3815    }
3816    // Use the §3.4.2.3 5.2.2 gate (explicit content type variety = empty),
3817    // which incorporates the effective-mixed promotion of step 3.1.1.
3818    if !type_data.content.explicit_content_type_is_empty() {
3819        return false;
3820    }
3821    if matches!(
3822        type_data.derivation_method,
3823        Some(DerivationMethod::Extension)
3824    ) {
3825        if let Some(TypeKey::Complex(base_key)) = type_data.resolved_base_type {
3826            if let Some(base_data) = schema_set.arenas.complex_types.get(base_key) {
3827                return explicit_content_is_empty(schema_set, base_data, depth + 1);
3828            }
3829        }
3830    }
3831    true
3832}
3833
3834/// Convert a `DefaultOpenContent` (schema-model form built from the
3835/// `<xs:defaultOpenContent>` element) into the `EffectiveOpenContent` form
3836/// used during derivation validation.
3837#[cfg(feature = "xsd11")]
3838fn default_open_content_to_effective(
3839    default: &crate::schema::model::DefaultOpenContent,
3840    target_ns: Option<NameId>,
3841) -> Option<EffectiveOpenContent> {
3842    let mode = match default.mode {
3843        crate::schema::model::OpenContentMode::None => OpenContentMode::None,
3844        crate::schema::model::OpenContentMode::Interleave => OpenContentMode::Interleave,
3845        crate::schema::model::OpenContentMode::Suffix => OpenContentMode::Suffix,
3846    };
3847    if mode == OpenContentMode::None {
3848        return None;
3849    }
3850    let wildcard = default.wildcard.as_ref().map(element_wildcard_to_result);
3851    Some(EffectiveOpenContent {
3852        mode,
3853        wildcard,
3854        target_namespace: target_ns,
3855    })
3856}
3857
3858/// Convert a schema-model `ElementWildcard` to a parser-form `WildcardResult`
3859/// so it can share the subset / union helpers below.
3860#[cfg(feature = "xsd11")]
3861fn element_wildcard_to_result(ew: &crate::schema::wildcard::ElementWildcard) -> WildcardResult {
3862    use crate::parser::frames::NotQNameItem;
3863    use crate::schema::wildcard::{NamespaceConstraint, QNameDisallowed};
3864
3865    let (namespace, not_namespace) = match &ew.namespace_constraint {
3866        NamespaceConstraint::Any => (WildcardNamespace::Any, Vec::new()),
3867        NamespaceConstraint::Other => (WildcardNamespace::Other, Vec::new()),
3868        NamespaceConstraint::Enumeration(nss) => (
3869            WildcardNamespace::List(nss.iter().copied().map(ns_token).collect()),
3870            Vec::new(),
3871        ),
3872        NamespaceConstraint::Not(nss) => (
3873            WildcardNamespace::Any,
3874            nss.iter().copied().map(ns_token).collect(),
3875        ),
3876    };
3877
3878    let process_contents = match ew.process_contents {
3879        crate::schema::wildcard::ProcessContents::Strict => ProcessContents::Strict,
3880        crate::schema::wildcard::ProcessContents::Lax => ProcessContents::Lax,
3881        crate::schema::wildcard::ProcessContents::Skip => ProcessContents::Skip,
3882    };
3883
3884    let not_qname = ew
3885        .not_qnames
3886        .iter()
3887        .map(|q| match q {
3888            QNameDisallowed::QName {
3889                namespace,
3890                local_name,
3891            } => NotQNameItem::QName {
3892                namespace: *namespace,
3893                local_name: *local_name,
3894            },
3895            QNameDisallowed::Defined => NotQNameItem::Defined,
3896            QNameDisallowed::DefinedSibling => NotQNameItem::DefinedSibling,
3897        })
3898        .collect();
3899
3900    WildcardResult {
3901        namespace,
3902        process_contents,
3903        not_namespace,
3904        not_qname,
3905        id: ew.id.clone(),
3906        annotation: None,
3907        source: ew.source.clone(),
3908    }
3909}
3910
3911/// Canonical namespace form: finite allowed set, or finite excluded set
3912/// (complement in the "namespace universe").
3913#[cfg(feature = "xsd11")]
3914#[derive(Debug, Clone)]
3915enum NsForm {
3916    Pos(Vec<Option<NameId>>),
3917    Neg(Vec<Option<NameId>>),
3918}
3919
3920/// Normalise a wildcard's `{namespace constraint}` into canonical form,
3921/// resolving `##targetNamespace`/`##local` tokens and merging `notNamespace`
3922/// into the excluded set.
3923#[cfg(feature = "xsd11")]
3924fn wildcard_to_ns_form(
3925    ns: &WildcardNamespace,
3926    not_namespace: &[crate::parser::frames::NamespaceToken],
3927    target_ns: Option<NameId>,
3928) -> NsForm {
3929    let resolved_not: Vec<Option<NameId>> =
3930        not_namespace.iter().map(|t| t.resolve(target_ns)).collect();
3931    match ns {
3932        WildcardNamespace::Any => NsForm::Neg(resolved_not),
3933        WildcardNamespace::Other => {
3934            let mut excl = other_exclusion_set(target_ns);
3935            for r in resolved_not {
3936                if !excl.contains(&r) {
3937                    excl.push(r);
3938                }
3939            }
3940            NsForm::Neg(excl)
3941        }
3942        WildcardNamespace::TargetNamespace => {
3943            let base = target_ns;
3944            if resolved_not.contains(&base) {
3945                NsForm::Pos(Vec::new())
3946            } else {
3947                NsForm::Pos(vec![base])
3948            }
3949        }
3950        WildcardNamespace::Local => {
3951            if resolved_not.contains(&None) {
3952                NsForm::Pos(Vec::new())
3953            } else {
3954                NsForm::Pos(vec![None])
3955            }
3956        }
3957        WildcardNamespace::List(tokens) => {
3958            let allowed: Vec<Option<NameId>> = tokens
3959                .iter()
3960                .map(|t| t.resolve(target_ns))
3961                .filter(|r| !resolved_not.contains(r))
3962                .collect();
3963            NsForm::Pos(allowed)
3964        }
3965    }
3966}
3967
3968/// Convert a canonical `NsForm` back into `(WildcardNamespace, not_namespace)`
3969/// pair suitable for a `WildcardResult`. Any excluded-set result that's empty
3970/// collapses to `##any`; a non-empty excluded set becomes `##any` with
3971/// `notNamespace` tokens.
3972#[cfg(feature = "xsd11")]
3973fn ns_form_to_wildcard(
3974    form: NsForm,
3975) -> (
3976    WildcardNamespace,
3977    Vec<crate::parser::frames::NamespaceToken>,
3978) {
3979    use crate::parser::frames::NamespaceToken;
3980    match form {
3981        NsForm::Pos(list) => {
3982            let tokens: Vec<NamespaceToken> = list.into_iter().map(ns_token).collect();
3983            (WildcardNamespace::List(tokens), Vec::new())
3984        }
3985        NsForm::Neg(list) if list.is_empty() => (WildcardNamespace::Any, Vec::new()),
3986        NsForm::Neg(list) => {
3987            let tokens: Vec<NamespaceToken> = list.into_iter().map(ns_token).collect();
3988            (WildcardNamespace::Any, tokens)
3989        }
3990    }
3991}
3992
3993/// Convert a resolved namespace (`Some(id)` = URI, `None` = absent/local) into
3994/// a parser-form `NamespaceToken`. Used by the open-content derivation helpers
3995/// to reconstruct parser-form wildcards from canonicalised lists.
3996#[cfg(feature = "xsd11")]
3997fn ns_token(ns: Option<NameId>) -> crate::parser::frames::NamespaceToken {
3998    match ns {
3999        Some(id) => crate::parser::frames::NamespaceToken::Uri(id),
4000        None => crate::parser::frames::NamespaceToken::Local,
4001    }
4002}
4003
4004/// Wildcard union per §3.10.6.3 cos-aw-union, restricted to the namespace
4005/// constraint portion. `notQName` items are intersected (an excluded QName
4006/// stays excluded only if both wildcards exclude it). `processContents` is
4007/// inherited from `a` (convention: `a` is the derivation's own `<any>`).
4008///
4009/// Tokens in the produced `WildcardResult` are already resolved against the
4010/// input target namespaces, so the caller does not need to supply one.
4011#[cfg(feature = "xsd11")]
4012pub(crate) fn wildcard_result_union(
4013    a: &WildcardResult,
4014    a_tns: Option<NameId>,
4015    b: &WildcardResult,
4016    b_tns: Option<NameId>,
4017) -> WildcardResult {
4018    let form_a = wildcard_to_ns_form(&a.namespace, &a.not_namespace, a_tns);
4019    let form_b = wildcard_to_ns_form(&b.namespace, &b.not_namespace, b_tns);
4020
4021    let merged = match (form_a, form_b) {
4022        (NsForm::Pos(mut pa), NsForm::Pos(pb)) => {
4023            for item in pb {
4024                if !pa.contains(&item) {
4025                    pa.push(item);
4026                }
4027            }
4028            NsForm::Pos(pa)
4029        }
4030        (NsForm::Pos(pa), NsForm::Neg(nb)) | (NsForm::Neg(nb), NsForm::Pos(pa)) => {
4031            NsForm::Neg(nb.into_iter().filter(|ns| !pa.contains(ns)).collect())
4032        }
4033        (NsForm::Neg(na), NsForm::Neg(nb)) => {
4034            NsForm::Neg(na.into_iter().filter(|ns| nb.contains(ns)).collect())
4035        }
4036    };
4037
4038    let (namespace, not_namespace) = ns_form_to_wildcard(merged);
4039
4040    let not_qname: Vec<crate::parser::frames::NotQNameItem> = a
4041        .not_qname
4042        .iter()
4043        .filter(|item| b.not_qname.contains(item))
4044        .cloned()
4045        .collect();
4046
4047    WildcardResult {
4048        namespace,
4049        process_contents: a.process_contents,
4050        not_namespace,
4051        not_qname,
4052        id: None,
4053        annotation: None,
4054        source: a.source.clone(),
4055    }
4056}
4057
4058/// True when the derived complex type's explicit particle is absent or
4059/// normalizes to an empty group (`<xs:sequence/>`, `<xs:all/>`, or a group
4060/// whose children all prune away). Used to decide when the stricter
4061/// open-content restriction checks can be safely relaxed: if the derived
4062/// particle contributes no elements to the content language, the derived
4063/// type's language comes entirely from its open content wildcard, so the
4064/// mode-and-subset checks against the base reduce to a pure wildcard-
4065/// subset check (cos-ns-subset).
4066#[cfg(feature = "xsd11")]
4067fn derived_particle_is_empty(
4068    schema_set: &SchemaSet,
4069    derived: &crate::arenas::ComplexTypeDefData,
4070) -> bool {
4071    let Some(particle) = complex_content_particle(&derived.content) else {
4072        return true;
4073    };
4074    let Ok(normalized) = normalize_type_particle(schema_set, derived, particle) else {
4075        return false;
4076    };
4077    is_effectively_empty(&normalized)
4078}
4079
4080/// Extract a wildcard from a base type's effective content particle when it
4081/// is a single wildcard (optionally wrapped in a pointless sequence/all
4082/// group). Returns `None` when the base has element particles that would
4083/// require the derived type's empty particle to not be a valid restriction.
4084///
4085/// This supports §3.4.6.4 clause 1 (language containment) for the narrow
4086/// case where the base has no `<xs:openContent>` but its content model is
4087/// a single wildcard — which is language-equivalent to having
4088/// interleave/suffix open content over that same wildcard.
4089#[cfg(feature = "xsd11")]
4090fn base_content_single_wildcard<'a>(
4091    schema_set: &'a SchemaSet,
4092    base: &'a crate::arenas::ComplexTypeDefData,
4093) -> Option<NormalizedWildcard> {
4094    let (particle_owner, particle) = effective_base_content_particle(schema_set, base);
4095    let particle = particle?;
4096    let normalized = normalize_type_particle(schema_set, particle_owner, particle).ok()?;
4097    match &normalized.term {
4098        NormalizedParticleTerm::Wildcard(wc) => Some((**wc).clone()),
4099        NormalizedParticleTerm::Group(group) => {
4100            if group.particles.len() == 1 {
4101                if let NormalizedParticleTerm::Wildcard(wc) = &group.particles[0].term {
4102                    return Some((**wc).clone());
4103                }
4104            }
4105            None
4106        }
4107        _ => None,
4108    }
4109}
4110
4111/// XSD 1.1 §3.4.6.4 schema-time EDC for all-group restrictions
4112/// (cvc-complex-type rule 5 / cos-element-consistent extended). When a
4113/// derived all-group restricts a base all-group and removes a base local
4114/// element, the derived's wildcard can structurally admit elements with the
4115/// removed QName. The "tighter EDC rule" of XSD 1.1 (Saxon test category
4116/// `xsd1_1-Wildcards-TighterMatchingRuleForEDC`, e.g. wild069) demands the
4117/// schema be invalid if the wildcard's governing type for that QName is not
4118/// validly substitutable for the base local's declared type.
4119///
4120/// The xs:sequence variant of the same construct (wild068) is intentionally
4121/// not subject to this check: the position constraint of sequence keeps the
4122/// conflict from arising structurally, leaving runtime dynamic EDC as the
4123/// catcher.
4124#[cfg(feature = "xsd11")]
4125fn validate_all_group_restriction_edc(
4126    schema_set: &SchemaSet,
4127    derived: &crate::arenas::ComplexTypeDefData,
4128    base: &crate::arenas::ComplexTypeDefData,
4129) -> SchemaResult<()> {
4130    use crate::parser::frames::ProcessContents;
4131
4132    let derived_particle = complex_content_particle(&derived.content);
4133    let (effective_base, base_particle) = effective_base_content_particle(schema_set, base);
4134
4135    let (Some(derived_p), Some(base_p)) = (derived_particle, base_particle) else {
4136        return Ok(());
4137    };
4138
4139    let derived_norm = match normalize_type_particle(schema_set, derived, derived_p) {
4140        Ok(n) => n,
4141        Err(_) => return Ok(()),
4142    };
4143    let base_norm = match normalize_type_particle(schema_set, effective_base, base_p) {
4144        Ok(n) => n,
4145        Err(_) => return Ok(()),
4146    };
4147
4148    // Trigger only when both top-level groups are xs:all.
4149    if !is_top_all_group(&derived_norm) || !is_top_all_group(&base_norm) {
4150        return Ok(());
4151    }
4152
4153    // Collect derived's local element QNames and wildcards (top-level only —
4154    // an all-group's particles are flat).
4155    let mut derived_local_qnames: Vec<(Option<NameId>, NameId)> = Vec::new();
4156    let mut derived_wildcards: Vec<&NormalizedWildcard> = Vec::new();
4157    if let NormalizedParticleTerm::Group(group) = &derived_norm.term {
4158        for p in &group.particles {
4159            match &p.term {
4160                NormalizedParticleTerm::Element(elem) => {
4161                    derived_local_qnames.push((elem.namespace, elem.name));
4162                }
4163                NormalizedParticleTerm::Wildcard(wc) => {
4164                    derived_wildcards.push(wc.as_ref());
4165                }
4166                NormalizedParticleTerm::Group(_) => {}
4167            }
4168        }
4169    }
4170
4171    if derived_wildcards.is_empty() {
4172        return Ok(());
4173    }
4174
4175    // Walk base's local elements (top-level all-group particles).
4176    let base_locals: Vec<(Option<NameId>, NameId, TypeKey)> =
4177        if let NormalizedParticleTerm::Group(group) = &base_norm.term {
4178            group
4179                .particles
4180                .iter()
4181                .filter_map(|p| match &p.term {
4182                    NormalizedParticleTerm::Element(elem) => {
4183                        Some((elem.namespace, elem.name, elem.type_key))
4184                    }
4185                    _ => None,
4186                })
4187                .collect()
4188        } else {
4189            Vec::new()
4190        };
4191
4192    let (location, type_name) = type_error_context(schema_set, derived);
4193    let base_name = format_type_name(schema_set, base.name, base.target_namespace);
4194
4195    for (l_ns, l_name, l_type) in &base_locals {
4196        // Skip if derived also has this local element.
4197        if derived_local_qnames.contains(&(*l_ns, *l_name)) {
4198            continue;
4199        }
4200
4201        for wc in &derived_wildcards {
4202            if !wildcard_admits_qname(wc, *l_ns, *l_name) {
4203                continue;
4204            }
4205
4206            let pc = wc.wildcard.process_contents;
4207            if matches!(pc, ProcessContents::Skip) {
4208                continue;
4209            }
4210
4211            let global_key = schema_set.lookup_element(*l_ns, *l_name);
4212            let governing_type = global_key
4213                .and_then(|k| schema_set.arenas.elements.get(k))
4214                .and_then(|d| d.resolved_type);
4215
4216            match (pc, governing_type) {
4217                (ProcessContents::Strict, None) => {
4218                    // strict + no global: instance with this QName fails strict
4219                    // wildcard validation, so derived rejects. No conflict.
4220                    continue;
4221                }
4222                (ProcessContents::Lax, None) => {
4223                    // lax + no global: wildcard skip-validates, so derived
4224                    // admits arbitrary content for this QName. Base local
4225                    // would enforce its type — derived broader → reject.
4226                }
4227                (_, Some(gov_type)) => {
4228                    if schema_set.is_type_derived_from(gov_type, *l_type, DerivationSet::empty()) {
4229                        continue;
4230                    }
4231                }
4232                _ => continue,
4233            }
4234
4235            return Err(SchemaError::structural(
4236                "cos-element-consistent",
4237                format!(
4238                    "Complex type '{}' restricts '{}' (xs:all) by removing local element \
4239                     while keeping a wildcard that admits the same QName; the wildcard's \
4240                     governing type is not validly substitutable for the base local element's \
4241                     type (cvc-complex-type rule 5 / tighter EDC for xs:all restriction)",
4242                    type_name, base_name,
4243                ),
4244                location.clone(),
4245            ));
4246        }
4247    }
4248
4249    Ok(())
4250}
4251
4252/// Whether a normalized particle is a top-level xs:all group (with min/max
4253/// = 1, since xs:all allows minOccurs ∈ {0,1} and maxOccurs = 1).
4254#[cfg(feature = "xsd11")]
4255fn is_top_all_group(particle: &NormalizedParticle) -> bool {
4256    matches!(
4257        &particle.term,
4258        NormalizedParticleTerm::Group(group) if group.compositor == Compositor::All
4259    )
4260}
4261
4262/// Validate open-content compatibility for complex type restriction
4263/// (derivation-ok-restriction).
4264///
4265/// Rules:
4266/// - If base has no OC, derived must not add OC — **unless** the derived
4267///   particle is empty and the base's content model is a single wildcard
4268///   that subsumes the derived OC's wildcard (language-equivalent case).
4269/// - If base has OC but derived doesn't — OK (restriction removes it).
4270/// - Interleave cannot restrict suffix — **unless** the derived particle
4271///   is empty, in which case the mode choice is irrelevant because the
4272///   derived language is the wildcard closure.
4273/// - Derived wildcard must be a subset of base wildcard.
4274#[cfg(feature = "xsd11")]
4275fn validate_open_content_restriction(
4276    schema_set: &SchemaSet,
4277    derived: &crate::arenas::ComplexTypeDefData,
4278    base: &crate::arenas::ComplexTypeDefData,
4279) -> SchemaResult<()> {
4280    let base_oc = effective_open_content(base.open_content.as_ref());
4281    let derived_oc = effective_open_content(derived.open_content.as_ref());
4282
4283    // If base has no open content, derived must not add one — except when
4284    // the derived particle is empty and the base's single-wildcard particle
4285    // subsumes the derived OC wildcard (language containment, §3.4.6.4).
4286    if base_oc.is_none() && derived_oc.is_some() {
4287        if derived_particle_is_empty(schema_set, derived) {
4288            let derived_oc_wc = derived_oc.as_ref().and_then(|o| o.wildcard.as_ref());
4289            if let Some(d_wc) = derived_oc_wc {
4290                if let Some(base_wc) = base_content_single_wildcard(schema_set, base) {
4291                    if is_wildcard_ns_subset(
4292                        d_wc,
4293                        derived.target_namespace,
4294                        &base_wc.wildcard,
4295                        base_wc.target_namespace,
4296                    ) {
4297                        return Ok(());
4298                    }
4299                }
4300            }
4301        }
4302        let (location, type_name) = type_error_context(schema_set, derived);
4303        let base_name = format_type_name(schema_set, base.name, base.target_namespace);
4304        return Err(SchemaError::structural(
4305            "derivation-ok-restriction",
4306            format!(
4307                "Complex type '{}' restricts '{}' which has no open content, \
4308                 but adds open content — not allowed",
4309                type_name, base_name
4310            ),
4311            location,
4312        ));
4313    }
4314
4315    // If base has OC but derived doesn't — OK (restriction removes it)
4316    let (Some(base_oc), Some(derived_oc)) = (base_oc, derived_oc) else {
4317        return Ok(());
4318    };
4319
4320    let (location, type_name) = type_error_context(schema_set, derived);
4321    let base_name = format_type_name(schema_set, base.name, base.target_namespace);
4322
4323    // Mode: if base is suffix, derived cannot use interleave — unless the
4324    // derived particle is empty, in which case the derived language is just
4325    // the wildcard closure and the mode choice is irrelevant.
4326    if base_oc.mode == OpenContentMode::Suffix
4327        && derived_oc.mode == OpenContentMode::Interleave
4328        && !derived_particle_is_empty(schema_set, derived)
4329    {
4330        return Err(SchemaError::structural(
4331            "derivation-ok-restriction",
4332            format!(
4333                "Complex type '{}' uses interleave open content mode but base type '{}' \
4334                 uses suffix mode — interleave cannot restrict suffix",
4335                type_name, base_name
4336            ),
4337            location,
4338        ));
4339    }
4340
4341    // Wildcard: derived must be subset of base
4342    if let (Some(base_wc), Some(derived_wc)) =
4343        (base_oc.wildcard.as_ref(), derived_oc.wildcard.as_ref())
4344    {
4345        if !is_wildcard_ns_subset(
4346            derived_wc,
4347            derived.target_namespace,
4348            base_wc,
4349            base.target_namespace,
4350        ) {
4351            return Err(SchemaError::structural(
4352                "derivation-ok-restriction",
4353                format!(
4354                    "Open content wildcard of '{}' is not a valid restriction \
4355                     of base type '{}' wildcard",
4356                    type_name, base_name
4357                ),
4358                location,
4359            ));
4360        }
4361
4362        // processContents: restriction must be at least as strict
4363        if process_contents_strictness(derived_wc.process_contents)
4364            < process_contents_strictness(base_wc.process_contents)
4365        {
4366            return Err(SchemaError::structural(
4367                "derivation-ok-restriction",
4368                format!(
4369                    "Open content wildcard of '{}' has weaker processContents \
4370                     than base type '{}' wildcard",
4371                    type_name, base_name
4372                ),
4373                location,
4374            ));
4375        }
4376    }
4377
4378    Ok(())
4379}
4380
4381/// Check if a type derives from NOTATION or QName.
4382fn is_notation_or_qname_base(schema_set: &SchemaSet, key: TypeKey) -> bool {
4383    let TypeKey::Simple(sk) = key else {
4384        return false;
4385    };
4386    let bt = schema_set.builtin_types();
4387    schema_set.derives_from(sk, bt.notation) || schema_set.derives_from(sk, bt.qname)
4388}
4389
4390/// Walk up the simple type chain past any types that have enumeration facets.
4391///
4392/// Returns the first ancestor without enumeration facets.  This lets us
4393/// validate enumeration values against the "structural" base (bounds, digits,
4394/// lexical form) without hitting the string-equality enumeration comparison
4395/// in `validate_simple_type` (which can false-reject when canonical forms
4396/// differ — e.g. `12:00:00.990` vs `12:00:00.99`).  The enumeration-subset
4397/// rule is already enforced by `merge_with_base`.
4398fn base_without_enumeration(schema_set: &SchemaSet, key: TypeKey) -> TypeKey {
4399    let mut current = key;
4400    for _ in 0..100 {
4401        if let TypeKey::Simple(sk) = current {
4402            if let Some(st_data) = schema_set.arenas.simple_types.get(sk) {
4403                if st_data.facets.enumeration.is_none() {
4404                    return current;
4405                }
4406                if let Some(base) = st_data.resolved_base_type {
4407                    current = base;
4408                    continue;
4409                }
4410            }
4411        }
4412        break;
4413    }
4414    current
4415}
4416
4417/// Validate that facet values are in the value space of the base type.
4418///
4419/// Reuses the existing `validate_simple_type` runtime infrastructure (type code
4420/// resolution, facet collection, validator dispatch) to check each locally
4421/// declared facet value against the base type at schema-compile time.
4422///
4423/// Implements XSD Part 2 constraints:
4424/// - `enumeration-valid-restriction`: enumeration values must be in the base type's value space
4425/// - `minInclusive-valid-restriction`, `maxInclusive-valid-restriction`,
4426///   `minExclusive-valid-restriction`, `maxExclusive-valid-restriction`:
4427///   bound values must be in the base type's value space
4428fn validate_facet_values_against_base_type(
4429    schema_set: &SchemaSet,
4430    type_def: &crate::arenas::SimpleTypeDefData,
4431    base_key: TypeKey,
4432) -> SchemaResult<()> {
4433    let (location, type_name) = type_error_context(schema_set, type_def);
4434
4435    // QName/NOTATION need a runtime namespace context for full value-space
4436    // validation, so most lexical checks are deferred. We *can* still reject
4437    // the always-invalid empty literal in enumeration / bound facets — an
4438    // empty string is not a valid QName or NOTATION lexically (xsd:QName ≡
4439    // Prefix? ':'? LocalPart, where LocalPart is an NCName, never empty).
4440    if is_notation_or_qname_base(schema_set, base_key) {
4441        if let Some(ref enum_facet) = type_def.facets.enumeration {
4442            for value in &enum_facet.values {
4443                if value.trim().is_empty() {
4444                    return Err(SchemaError::structural(
4445                        "enumeration-valid-restriction",
4446                        format!(
4447                            "Enumeration value '' in type '{}' is not in the value space of the base type",
4448                            type_name
4449                        ),
4450                        location.clone(),
4451                    ));
4452                }
4453            }
4454        }
4455        return Ok(());
4456    }
4457
4458    // Validate enumeration values.
4459    // Walk past any base with its own enumeration to avoid string-equality comparison
4460    // (canonical-form mismatch). merge_with_base already checks the subset rule.
4461    if let Some(ref enum_facet) = type_def.facets.enumeration {
4462        let enum_base = base_without_enumeration(schema_set, base_key);
4463        for value in &enum_facet.values {
4464            if crate::validation::simple::validate_simple_type(value, enum_base, schema_set)
4465                .is_err()
4466            {
4467                return Err(SchemaError::structural(
4468                    "enumeration-valid-restriction",
4469                    format!(
4470                        "Enumeration value '{}' in type '{}' is not in the value space of the base type",
4471                        value, type_name
4472                    ),
4473                    location.clone(),
4474                ));
4475            }
4476        }
4477    }
4478
4479    // Validate bound facet values. XSD Part 2 §4.3.9 permits a derived bound
4480    // to equal the base's same-kind bound (boundary equality), even though
4481    // that base value is not in the base's own value space.
4482    let check_bound = |value: &str,
4483                       constraint: &'static str,
4484                       kind: FacetKind|
4485     -> SchemaResult<()> {
4486        match crate::validation::simple::validate_simple_type(value, base_key, schema_set) {
4487            Ok(_) => Ok(()),
4488            Err(err) if is_bound_self_violation(&err, kind, schema_set, base_key, value) => Ok(()),
4489            Err(_) => Err(SchemaError::structural(
4490                constraint,
4491                format!(
4492                    "{} value '{}' in type '{}' is not in the value space of the base type",
4493                    kind.name(),
4494                    value,
4495                    type_name
4496                ),
4497                location.clone(),
4498            )),
4499        }
4500    };
4501
4502    if let Some(ref f) = type_def.facets.min_inclusive {
4503        check_bound(
4504            &f.value,
4505            "minInclusive-valid-restriction",
4506            FacetKind::MinInclusive,
4507        )?;
4508    }
4509    if let Some(ref f) = type_def.facets.max_inclusive {
4510        check_bound(
4511            &f.value,
4512            "maxInclusive-valid-restriction",
4513            FacetKind::MaxInclusive,
4514        )?;
4515    }
4516    if let Some(ref f) = type_def.facets.min_exclusive {
4517        check_bound(
4518            &f.value,
4519            "minExclusive-valid-restriction",
4520            FacetKind::MinExclusive,
4521        )?;
4522    }
4523    if let Some(ref f) = type_def.facets.max_exclusive {
4524        check_bound(
4525            &f.value,
4526            "maxExclusive-valid-restriction",
4527            FacetKind::MaxExclusive,
4528        )?;
4529    }
4530
4531    validate_typed_bound_consistency(
4532        schema_set,
4533        &type_def.facets,
4534        base_key,
4535        &type_name,
4536        &location,
4537    )?;
4538
4539    Ok(())
4540}
4541
4542fn validate_typed_bound_consistency(
4543    schema_set: &SchemaSet,
4544    facets: &FacetSet,
4545    base_key: TypeKey,
4546    type_name: &str,
4547    location: &Option<SourceLocation>,
4548) -> SchemaResult<()> {
4549    let check_pair = |lower: Option<&str>,
4550                      upper: Option<&str>,
4551                      lower_name: &'static str,
4552                      upper_name: &'static str,
4553                      allow_equal: bool|
4554     -> SchemaResult<()> {
4555        let (Some(lower), Some(upper)) = (lower, upper) else {
4556            return Ok(());
4557        };
4558        let Some(cmp) = compare_bound_literals(schema_set, base_key, lower, upper) else {
4559            return Ok(());
4560        };
4561        let valid = if allow_equal {
4562            matches!(cmp, std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
4563        } else {
4564            cmp == std::cmp::Ordering::Less
4565        };
4566        if valid {
4567            return Ok(());
4568        }
4569        Err(SchemaError::structural(
4570            "cos-st-restricts",
4571            format!(
4572                "{} value '{}' is not below {} value '{}' in type '{}'",
4573                lower_name, lower, upper_name, upper, type_name
4574            ),
4575            location.clone(),
4576        ))
4577    };
4578
4579    check_pair(
4580        facets.min_inclusive.as_ref().map(|f| f.value.as_str()),
4581        facets.max_inclusive.as_ref().map(|f| f.value.as_str()),
4582        "minInclusive",
4583        "maxInclusive",
4584        true,
4585    )?;
4586    check_pair(
4587        facets.min_exclusive.as_ref().map(|f| f.value.as_str()),
4588        facets.max_exclusive.as_ref().map(|f| f.value.as_str()),
4589        "minExclusive",
4590        "maxExclusive",
4591        false,
4592    )?;
4593    check_pair(
4594        facets.min_inclusive.as_ref().map(|f| f.value.as_str()),
4595        facets.max_exclusive.as_ref().map(|f| f.value.as_str()),
4596        "minInclusive",
4597        "maxExclusive",
4598        false,
4599    )?;
4600    check_pair(
4601        facets.min_exclusive.as_ref().map(|f| f.value.as_str()),
4602        facets.max_inclusive.as_ref().map(|f| f.value.as_str()),
4603        "minExclusive",
4604        "maxInclusive",
4605        false,
4606    )
4607}
4608
4609fn compare_bound_literals(
4610    schema_set: &SchemaSet,
4611    base_key: TypeKey,
4612    lower: &str,
4613    upper: &str,
4614) -> Option<std::cmp::Ordering> {
4615    let parse_base = bound_comparison_base(schema_set, base_key);
4616    let lower =
4617        crate::validation::simple::validate_simple_type(lower, parse_base, schema_set).ok()?;
4618    let upper =
4619        crate::validation::simple::validate_simple_type(upper, parse_base, schema_set).ok()?;
4620    compare_xml_values(&lower.typed_value, &upper.typed_value)
4621}
4622
4623fn bound_comparison_base(schema_set: &SchemaSet, key: TypeKey) -> TypeKey {
4624    let mut current = key;
4625    for _ in 0..100 {
4626        let TypeKey::Simple(sk) = current else {
4627            return current;
4628        };
4629        let Some(st) = schema_set.arenas.simple_types.get(sk) else {
4630            return current;
4631        };
4632        let has_bounds_or_enum = st.facets.enumeration.is_some()
4633            || st.facets.min_inclusive.is_some()
4634            || st.facets.min_exclusive.is_some()
4635            || st.facets.max_inclusive.is_some()
4636            || st.facets.max_exclusive.is_some();
4637        if !has_bounds_or_enum {
4638            return current;
4639        }
4640        let Some(base) = st.resolved_base_type else {
4641            return current;
4642        };
4643        current = base;
4644    }
4645    current
4646}
4647
4648fn compare_xml_values(
4649    lower: &crate::types::value::XmlValue,
4650    upper: &crate::types::value::XmlValue,
4651) -> Option<std::cmp::Ordering> {
4652    use crate::types::value::XmlValueKind;
4653    match (&lower.value, &upper.value) {
4654        (XmlValueKind::Atomic(a), XmlValueKind::Atomic(b)) => compare_xml_atomic_values(a, b),
4655        (XmlValueKind::Union(a), _) => compare_xml_values(a, upper),
4656        (_, XmlValueKind::Union(b)) => compare_xml_values(lower, b),
4657        _ => None,
4658    }
4659}
4660
4661fn compare_xml_atomic_values(
4662    lower: &crate::types::value::XmlAtomicValue,
4663    upper: &crate::types::value::XmlAtomicValue,
4664) -> Option<std::cmp::Ordering> {
4665    use crate::types::value::XmlAtomicValue;
4666    match (lower, upper) {
4667        (XmlAtomicValue::DateTime(a), XmlAtomicValue::DateTime(b)) => a.partial_cmp(b),
4668        (XmlAtomicValue::Date(a), XmlAtomicValue::Date(b)) => a.partial_cmp(b),
4669        (XmlAtomicValue::Time(a), XmlAtomicValue::Time(b)) => a.partial_cmp(b),
4670        (XmlAtomicValue::Duration(a), XmlAtomicValue::Duration(b)) => a.partial_cmp(b),
4671        (XmlAtomicValue::YearMonthDuration(a), XmlAtomicValue::YearMonthDuration(b)) => {
4672            a.partial_cmp(b)
4673        }
4674        (XmlAtomicValue::DayTimeDuration(a), XmlAtomicValue::DayTimeDuration(b)) => {
4675            a.partial_cmp(b)
4676        }
4677        (XmlAtomicValue::GYearMonth(a), XmlAtomicValue::GYearMonth(b)) => a.partial_cmp(b),
4678        (XmlAtomicValue::GYear(a), XmlAtomicValue::GYear(b)) => a.partial_cmp(b),
4679        (XmlAtomicValue::GMonthDay(a), XmlAtomicValue::GMonthDay(b)) => a.partial_cmp(b),
4680        (XmlAtomicValue::GDay(a), XmlAtomicValue::GDay(b)) => a.partial_cmp(b),
4681        (XmlAtomicValue::GMonth(a), XmlAtomicValue::GMonth(b)) => a.partial_cmp(b),
4682        _ => None,
4683    }
4684}
4685
4686fn get_type_facets(schema_set: &SchemaSet, type_key: TypeKey) -> SchemaResult<Option<FacetSet>> {
4687    match type_key {
4688        TypeKey::Simple(key) => {
4689            if let Some(type_def) = schema_set.arenas.simple_types.get(key) {
4690                Ok(Some(type_def.facets.clone()))
4691            } else {
4692                Ok(None)
4693            }
4694        }
4695        TypeKey::Complex(_) => {
4696            // Complex types don't have direct facets
4697            // (simpleContent types have facets in their content definition)
4698            Ok(None)
4699        }
4700    }
4701}
4702
4703/// Format a type name for error messages
4704pub(crate) fn format_type_name(
4705    schema_set: &SchemaSet,
4706    name: Option<NameId>,
4707    namespace: Option<NameId>,
4708) -> String {
4709    match name {
4710        Some(name_id) => {
4711            let local = schema_set.name_table.resolve(name_id);
4712            match namespace {
4713                Some(ns_id) => {
4714                    let ns = schema_set.name_table.resolve(ns_id);
4715                    if ns.is_empty() {
4716                        local.to_string()
4717                    } else {
4718                        format!("{{{}}}{}", ns, local)
4719                    }
4720                }
4721                None => local.to_string(),
4722            }
4723        }
4724        None => "(anonymous)".to_string(),
4725    }
4726}
4727
4728/// Minimal "type-like component" view used by [`type_error_context`].
4729/// Implemented by the type-def structs whose errors carry both a source
4730/// location and a formatted type name.
4731pub(crate) trait TypeDefForError {
4732    fn error_name(&self) -> Option<NameId>;
4733    fn error_target_namespace(&self) -> Option<NameId>;
4734    fn error_source(&self) -> Option<&SourceRef>;
4735}
4736
4737impl TypeDefForError for crate::arenas::SimpleTypeDefData {
4738    fn error_name(&self) -> Option<NameId> {
4739        self.name
4740    }
4741    fn error_target_namespace(&self) -> Option<NameId> {
4742        self.target_namespace
4743    }
4744    fn error_source(&self) -> Option<&SourceRef> {
4745        self.source.as_ref()
4746    }
4747}
4748
4749impl TypeDefForError for crate::arenas::ComplexTypeDefData {
4750    fn error_name(&self) -> Option<NameId> {
4751        self.name
4752    }
4753    fn error_target_namespace(&self) -> Option<NameId> {
4754        self.target_namespace
4755    }
4756    fn error_source(&self) -> Option<&SourceRef> {
4757        self.source.as_ref()
4758    }
4759}
4760
4761/// Returns `(location, type_name)` for error construction on a type component.
4762/// Pairs [`SchemaSet::locate`] with [`format_type_name`] since they always
4763/// co-occur in `SchemaError::structural` calls built for a type.
4764pub(crate) fn type_error_context<T: TypeDefForError>(
4765    schema_set: &SchemaSet,
4766    type_def: &T,
4767) -> (Option<SourceLocation>, String) {
4768    (
4769        schema_set.locate(type_def.error_source()),
4770        format_type_name(
4771            schema_set,
4772            type_def.error_name(),
4773            type_def.error_target_namespace(),
4774        ),
4775    )
4776}
4777
4778/// Resolved effective attribute use for comparison during restriction validation.
4779/// Attribute identity is (target_namespace, name) per §3.2.6.
4780struct EffectiveAttributeUse {
4781    name: NameId,
4782    target_namespace: Option<NameId>,
4783    use_kind: AttributeUseKind,
4784    resolved_type: Option<TypeKey>,
4785    fixed_value: Option<String>,
4786    default_value: Option<String>,
4787    /// XSD 1.1 §3.5.1 / §3.2.2.3: the use-level `{inheritable}` is the local
4788    /// `inheritable` attribute when present; for `ref`-based uses without an
4789    /// own `inheritable` it falls back to the resolved declaration's
4790    /// `{inheritable}`. Used by §3.4.6.3 derivation-ok-restriction to enforce
4791    /// `G.{inheritable} = S.{inheritable}` (subsumes clause 5.3).
4792    inheritable: bool,
4793}
4794
4795/// Resolve a single attribute use + its parallel resolved data into an
4796/// `EffectiveAttributeUse`.  Returns `None` when the attribute name
4797/// cannot be determined (malformed data).
4798fn resolve_single_attribute_use(
4799    schema_set: &SchemaSet,
4800    attr_use: &crate::parser::frames::AttributeUseResult,
4801    resolved: Option<&crate::arenas::ResolvedAttributeUse>,
4802) -> Option<EffectiveAttributeUse> {
4803    let (name, target_namespace) = if let Some(ref_name) = &attr_use.attribute.ref_name {
4804        if let Some(resolved_attr) = resolved.and_then(|r| r.resolved_ref) {
4805            let decl = schema_set.arenas.attributes.get(resolved_attr);
4806            (
4807                decl.and_then(|d| d.name)?,
4808                decl.and_then(|d| d.target_namespace),
4809            )
4810        } else {
4811            (ref_name.local_name, ref_name.namespace)
4812        }
4813    } else {
4814        let n = attr_use.attribute.name?;
4815        // For inline (non-ref) attributes, compute effective namespace
4816        // using form + attributeFormDefault per §3.2.2.
4817        let ns = schema_set.effective_local_attribute_namespace(
4818            attr_use.attribute.target_namespace,
4819            attr_use.attribute.form.as_deref(),
4820            attr_use.attribute.source.as_ref(),
4821            None,
4822        );
4823        (n, ns)
4824    };
4825
4826    // Prefer the use's resolved_type; fall back to the global declaration's type.
4827    let resolved_type = resolved.and_then(|r| r.resolved_type).or_else(|| {
4828        resolved
4829            .and_then(|r| r.resolved_ref)
4830            .and_then(|ref_key| schema_set.arenas.attributes.get(ref_key))
4831            .and_then(|decl| decl.resolved_type)
4832    });
4833
4834    // For fixed_value: use the inline fixed, or the resolved global decl's fixed.
4835    let fixed_value = attr_use.attribute.fixed_value.clone().or_else(|| {
4836        resolved
4837            .and_then(|r| r.resolved_ref)
4838            .and_then(|ref_key| schema_set.arenas.attributes.get(ref_key))
4839            .and_then(|decl| decl.fixed_value.clone())
4840    });
4841    // For default_value: use the inline default, or the resolved global decl's default.
4842    let default_value = attr_use.attribute.default_value.clone().or_else(|| {
4843        resolved
4844            .and_then(|r| r.resolved_ref)
4845            .and_then(|ref_key| schema_set.arenas.attributes.get(ref_key))
4846            .and_then(|decl| decl.default_value.clone())
4847    });
4848
4849    // {inheritable} per §3.2.2.3: the actual value of the use's
4850    // `inheritable` attribute (default false). For ref-based uses, the
4851    // mapping rule says use the {attribute declaration}.{inheritable} —
4852    // but the parser stores the use's literal value with a `false`
4853    // default, indistinguishable from "unspecified". Fall back to the
4854    // referenced declaration's inheritable when the use itself is a
4855    // ref and is not flagged.
4856    let inheritable = if attr_use.attribute.inheritable {
4857        true
4858    } else if attr_use.attribute.ref_name.is_some() {
4859        resolved
4860            .and_then(|r| r.resolved_ref)
4861            .and_then(|ref_key| schema_set.arenas.attributes.get(ref_key))
4862            .map(|decl| decl.inheritable)
4863            .unwrap_or(false)
4864    } else {
4865        false
4866    };
4867
4868    Some(EffectiveAttributeUse {
4869        name,
4870        target_namespace,
4871        use_kind: attr_use.use_kind,
4872        resolved_type,
4873        fixed_value,
4874        default_value,
4875        inheritable,
4876    })
4877}
4878
4879/// Collect effective attribute uses from a complex type definition.
4880///
4881/// Resolves attribute refs and expands attribute groups into a flat list.
4882/// Attributes are always on `type_def.attributes` (moved from sc/cc at parse time).
4883fn collect_effective_attribute_uses(
4884    schema_set: &SchemaSet,
4885    type_def: &crate::arenas::ComplexTypeDefData,
4886) -> Vec<EffectiveAttributeUse> {
4887    let mut result = Vec::new();
4888
4889    for (i, attr_use) in type_def.attributes.iter().enumerate() {
4890        let resolved = type_def.resolved_attributes.get(i);
4891        if let Some(eau) = resolve_single_attribute_use(schema_set, attr_use, resolved) {
4892            result.push(eau);
4893        }
4894    }
4895
4896    for &ag_key in &type_def.resolved_attribute_groups {
4897        collect_attribute_group_uses(schema_set, ag_key, &mut result, 0);
4898    }
4899
4900    result
4901}
4902
4903// ---------------------------------------------------------------------------
4904// §3.6.2.2 Effective Attribute Wildcard + §3.10.6.4 Intersection
4905// ---------------------------------------------------------------------------
4906//
4907// These helpers implement the "Common Rules for Attribute Wildcards"
4908// (§3.6.2.2) used by both the complex-type restriction path
4909// (`validate_attribute_restriction`) and the redefine attribute-group
4910// restriction path (`validate_all_redefine_attribute_group_restrictions`).
4911//
4912// The output is an `EffectiveAttributeWildcard` with a canonical namespace
4913// constraint (`CanonicalNs`) in which all `WildcardNamespace` variants have
4914// been normalized to either `Any`, an explicit positive set, or a
4915// complement set, with `not_namespace` exclusions already folded in and
4916// `##other` resolved against XSD version. Intersection then reduces to
4917// pure set theory on `HashSet<Option<NameId>>`.
4918//
4919// Intentionally private to this module — the canonical form never leaks
4920// into the arena model.
4921
4922/// Canonical namespace constraint for attribute wildcards (§3.10.6.4).
4923///
4924/// All `WildcardNamespace` variants normalize to one of these three cases,
4925/// with `not_namespace` exclusions already folded in and `##other`
4926/// resolved via XSD-version-aware rules (XSD 1.0 excludes absent namespace,
4927/// XSD 1.1 does not).
4928#[derive(Debug, Clone, PartialEq, Eq)]
4929pub(crate) enum CanonicalNs {
4930    /// Every namespace is allowed.
4931    Any,
4932    /// Positive set of allowed namespaces. `None` represents the absent
4933    /// (no-namespace) case.
4934    Enum(std::collections::HashSet<Option<NameId>>),
4935    /// Complement set: every namespace except those in the set is allowed.
4936    /// `Not(empty)` is equivalent to `Any` but is preserved as-is for
4937    /// symmetry; `canonical_ns_subset` handles this case.
4938    Not(std::collections::HashSet<Option<NameId>>),
4939}
4940
4941/// Effective attribute wildcard, the result of §3.6.2.2.
4942///
4943/// Target-namespace-free: `namespace` has already been resolved against
4944/// each contributor's own target namespace during normalization.
4945#[derive(Debug, Clone)]
4946pub(crate) struct EffectiveAttributeWildcard {
4947    pub(crate) namespace: CanonicalNs,
4948    pub(crate) not_qname: Vec<crate::parser::frames::NotQNameItem>,
4949    pub(crate) process_contents: ProcessContents,
4950}
4951
4952/// Normalize a single `WildcardResult` into canonical form, resolving
4953/// `##other`, `##targetNamespace`, `##local`, list tokens, and folding in
4954/// `not_namespace` exclusions against `target_ns`.
4955fn normalize_attribute_wildcard(
4956    schema_set: &SchemaSet,
4957    wc: &WildcardResult,
4958    target_ns: Option<NameId>,
4959) -> EffectiveAttributeWildcard {
4960    use std::collections::HashSet;
4961
4962    // Step 1: resolve the primary namespace constraint.
4963    let base: CanonicalNs = match &wc.namespace {
4964        WildcardNamespace::Any => CanonicalNs::Any,
4965        WildcardNamespace::Other => {
4966            // Version-aware ##other exclusion set (§3.10.1):
4967            //   XSD 1.0: excludes {target_ns, absent}
4968            //   XSD 1.1: excludes {target_ns} only
4969            //
4970            // When the schema has no target namespace, the "target
4971            // namespace" IS the absent namespace (None), so
4972            // `target_ns` is inserted unconditionally to capture that
4973            // case. For XSD 1.0 with a non-absent target, we additionally
4974            // insert None (HashSet dedupes if target is already None).
4975            let mut excl = HashSet::new();
4976            excl.insert(target_ns);
4977            if schema_set.is_xsd10() {
4978                excl.insert(None);
4979            }
4980            CanonicalNs::Not(excl)
4981        }
4982        WildcardNamespace::TargetNamespace => {
4983            let mut s = HashSet::new();
4984            s.insert(target_ns);
4985            CanonicalNs::Enum(s)
4986        }
4987        WildcardNamespace::Local => {
4988            let mut s = HashSet::new();
4989            s.insert(None);
4990            CanonicalNs::Enum(s)
4991        }
4992        WildcardNamespace::List(tokens) => {
4993            let mut s = HashSet::new();
4994            for tok in tokens {
4995                s.insert(tok.resolve(target_ns));
4996            }
4997            CanonicalNs::Enum(s)
4998        }
4999    };
5000
5001    // Step 2: fold `not_namespace` exclusions into the canonical form.
5002    let not_ns: HashSet<Option<NameId>> = wc
5003        .not_namespace
5004        .iter()
5005        .map(|t| t.resolve(target_ns))
5006        .collect();
5007
5008    let namespace = if not_ns.is_empty() {
5009        base
5010    } else {
5011        match base {
5012            CanonicalNs::Any => CanonicalNs::Not(not_ns),
5013            CanonicalNs::Enum(set) => {
5014                let filtered: HashSet<Option<NameId>> =
5015                    set.into_iter().filter(|ns| !not_ns.contains(ns)).collect();
5016                CanonicalNs::Enum(filtered)
5017            }
5018            CanonicalNs::Not(set) => {
5019                let mut combined = set;
5020                combined.extend(not_ns);
5021                CanonicalNs::Not(combined)
5022            }
5023        }
5024    };
5025
5026    EffectiveAttributeWildcard {
5027        namespace,
5028        not_qname: wc.not_qname.clone(),
5029        process_contents: wc.process_contents,
5030    }
5031}
5032
5033/// §3.10.6.4 namespace-constraint intersection. Pure set theory on the
5034/// canonical lattice.
5035fn intersect_canonical_ns(a: &CanonicalNs, b: &CanonicalNs) -> CanonicalNs {
5036    use std::collections::HashSet;
5037    match (a, b) {
5038        // Any ∩ X = X
5039        (CanonicalNs::Any, other) | (other, CanonicalNs::Any) => other.clone(),
5040
5041        // Enum ∩ Enum = set intersection
5042        (CanonicalNs::Enum(s1), CanonicalNs::Enum(s2)) => {
5043            let inter: HashSet<Option<NameId>> = s1.intersection(s2).copied().collect();
5044            CanonicalNs::Enum(inter)
5045        }
5046
5047        // Enum ∩ Not(N) = Enum \ N
5048        (CanonicalNs::Enum(s), CanonicalNs::Not(n))
5049        | (CanonicalNs::Not(n), CanonicalNs::Enum(s)) => {
5050            let filtered: HashSet<Option<NameId>> =
5051                s.iter().filter(|ns| !n.contains(ns)).copied().collect();
5052            CanonicalNs::Enum(filtered)
5053        }
5054
5055        // Not(N1) ∩ Not(N2) = Not(N1 ∪ N2)
5056        (CanonicalNs::Not(n1), CanonicalNs::Not(n2)) => {
5057            let mut union = n1.clone();
5058            union.extend(n2.iter().copied());
5059            CanonicalNs::Not(union)
5060        }
5061    }
5062}
5063
5064/// §3.10.6.3 cos-aw-union on the canonical namespace lattice.
5065/// Mirror of `intersect_canonical_ns` for the union side.
5066fn union_canonical_ns(a: &CanonicalNs, b: &CanonicalNs) -> CanonicalNs {
5067    use std::collections::HashSet;
5068    match (a, b) {
5069        // Any ∪ X = Any
5070        (CanonicalNs::Any, _) | (_, CanonicalNs::Any) => CanonicalNs::Any,
5071
5072        // Enum(s1) ∪ Enum(s2) = set union
5073        (CanonicalNs::Enum(s1), CanonicalNs::Enum(s2)) => {
5074            let mut union = s1.clone();
5075            union.extend(s2.iter().copied());
5076            CanonicalNs::Enum(union)
5077        }
5078
5079        // Enum(s) ∪ Not(n) = Not(n \ s) — every namespace b allows
5080        // (= everything except n) plus everything in s. The result is
5081        // "not (n minus s)": elements of n already in s no longer need
5082        // to be excluded.
5083        (CanonicalNs::Enum(s), CanonicalNs::Not(n))
5084        | (CanonicalNs::Not(n), CanonicalNs::Enum(s)) => {
5085            let filtered: HashSet<Option<NameId>> =
5086                n.iter().filter(|ns| !s.contains(ns)).copied().collect();
5087            if filtered.is_empty() {
5088                CanonicalNs::Any
5089            } else {
5090                CanonicalNs::Not(filtered)
5091            }
5092        }
5093
5094        // Not(n1) ∪ Not(n2) = Not(n1 ∩ n2). A namespace is excluded by
5095        // the union only if it's excluded by both sides.
5096        (CanonicalNs::Not(n1), CanonicalNs::Not(n2)) => {
5097            let inter: HashSet<Option<NameId>> = n1.intersection(n2).copied().collect();
5098            if inter.is_empty() {
5099                CanonicalNs::Any
5100            } else {
5101                CanonicalNs::Not(inter)
5102            }
5103        }
5104    }
5105}
5106
5107/// Canonical namespace subset: `a ⊆ b`. Tests whether every namespace
5108/// allowed by `a` is also allowed by `b`.
5109fn canonical_ns_subset(a: &CanonicalNs, b: &CanonicalNs) -> bool {
5110    match (a, b) {
5111        // Anything ⊆ Any (Any accepts all namespaces)
5112        (_, CanonicalNs::Any) => true,
5113
5114        // Any ⊆ Not(empty) also holds, but only when b is literally `Any`
5115        // after the above match. Otherwise Any is not a subset of anything
5116        // finite or complemented.
5117        (CanonicalNs::Any, _) => false,
5118
5119        // Enum(s) ⊆ Enum(t) iff s ⊆ t
5120        (CanonicalNs::Enum(s), CanonicalNs::Enum(t)) => s.iter().all(|ns| t.contains(ns)),
5121
5122        // Enum(s) ⊆ Not(n) iff s ∩ n = ∅  (no element of s is excluded by n)
5123        (CanonicalNs::Enum(s), CanonicalNs::Not(n)) => s.iter().all(|ns| !n.contains(ns)),
5124
5125        // Not(n1) ⊆ Not(n2) iff n2 ⊆ n1  (a's exclusion set must be at
5126        // least as large as b's; the larger the exclusion, the smaller the
5127        // allowed set)
5128        (CanonicalNs::Not(n1), CanonicalNs::Not(n2)) => n2.iter().all(|ns| n1.contains(ns)),
5129
5130        // Not(n) ⊆ Enum(s): `Not(n)` allows infinitely many namespaces, a
5131        // finite `Enum(s)` cannot contain them all. False.
5132        (CanonicalNs::Not(_), CanonicalNs::Enum(_)) => false,
5133    }
5134}
5135
5136/// Intersect two effective attribute wildcards per §3.10.6.4.
5137///
5138/// - `namespace`: `intersect_canonical_ns`
5139/// - `not_qname`: union of both lists (deduplicated). Per §3.10.6.4
5140///   disallowed_names clause 3, `##defined` is preserved if present on
5141///   either side.
5142/// - `process_contents`: takes the LEFT operand's value. Callers must
5143///   pass the operands in the order required by §3.6.2.2 (clause 3.2.1
5144///   passes L first, clause 3.2.2 passes W[0] first).
5145fn intersect_effective_attribute_wildcards(
5146    a: &EffectiveAttributeWildcard,
5147    b: &EffectiveAttributeWildcard,
5148) -> EffectiveAttributeWildcard {
5149    let namespace = intersect_canonical_ns(&a.namespace, &b.namespace);
5150
5151    // Union not_qname lists. Items whose namespace is no longer admitted
5152    // by the intersected constraint are redundant but harmless to keep.
5153    let mut not_qname = a.not_qname.clone();
5154    for item in &b.not_qname {
5155        if !not_qname.contains(item) {
5156            not_qname.push(item.clone());
5157        }
5158    }
5159
5160    EffectiveAttributeWildcard {
5161        namespace,
5162        not_qname,
5163        process_contents: a.process_contents,
5164    }
5165}
5166
5167/// Structural error for attribute-group reference cycles exceeding the
5168/// depth guard during §3.6.2.2 walking. These should already be rejected
5169/// by the resolver — fail loudly rather than silently synthesize `Any`.
5170fn attribute_group_cycle_error() -> SchemaError {
5171    SchemaError::structural(
5172        "derivation-ok-restriction",
5173        "attribute group reference cycle exceeded max depth while computing \
5174         effective attribute wildcard (§3.6.2.2)",
5175        None,
5176    )
5177}
5178
5179/// Combine a local effective wildcard with an ordered sequence of
5180/// contributed effective wildcards per §3.6.2.2 clauses 3.1/3.2.1/3.2.2:
5181///
5182/// * W empty ⇒ `local` (or `None` if neither side is present).
5183/// * L non-absent ⇒ pc from L, intersect L with every Wi.
5184/// * L absent, W non-empty ⇒ pc from W[0], intersect every Wi.
5185fn combine_effective_wildcards(
5186    local: Option<EffectiveAttributeWildcard>,
5187    w: Vec<EffectiveAttributeWildcard>,
5188) -> Option<EffectiveAttributeWildcard> {
5189    match (local, w.is_empty()) {
5190        (None, true) => None,
5191        (Some(l), true) => Some(l),
5192        (Some(l), false) => Some(w.into_iter().fold(l, |acc, wi| {
5193            intersect_effective_attribute_wildcards(&acc, &wi)
5194        })),
5195        (None, false) => {
5196            let mut it = w.into_iter();
5197            let first = it.next().expect("w is non-empty");
5198            Some(it.fold(first, |acc, wi| {
5199                intersect_effective_attribute_wildcards(&acc, &wi)
5200            }))
5201        }
5202    }
5203}
5204
5205/// §3.6.2.2 Common Rules for Attribute Wildcards.
5206///
5207/// Given a local wildcard `local_wc` (optional) and the ordered sequence
5208/// of resolved referenced attribute groups, compute the effective
5209/// attribute wildcard. Each referenced group's own effective wildcard is
5210/// computed recursively (so wildcards inherited through chains of
5211/// `<xs:attributeGroup ref=...>` references are properly intersected).
5212///
5213/// Returns `Err` if the attribute-group reference tree exceeds the depth
5214/// guard (cycle protection, matching `collect_attribute_group_uses`).
5215pub(crate) fn effective_attribute_wildcard(
5216    schema_set: &SchemaSet,
5217    local_wc: Option<&WildcardResult>,
5218    local_target_ns: Option<NameId>,
5219    attribute_groups: &[AttributeGroupKey],
5220) -> SchemaResult<Option<EffectiveAttributeWildcard>> {
5221    let local = local_wc.map(|w| normalize_attribute_wildcard(schema_set, w, local_target_ns));
5222
5223    let mut w: Vec<EffectiveAttributeWildcard> = Vec::new();
5224    for &ag_key in attribute_groups {
5225        collect_effective_group_wildcards(schema_set, ag_key, &mut w, 0)?;
5226    }
5227
5228    Ok(combine_effective_wildcards(local, w))
5229}
5230
5231/// Runtime entry point for attribute wildcard matching.
5232///
5233/// Returns the type's full effective `{attribute wildcard}` per §3.6.2.2
5234/// (intersection of own + attribute groups) chained with §3.4.2.5's
5235/// extension union over the base chain. Restriction picks the derived's
5236/// own wildcard (§3.6.2.2 "complete wildcard") with no base inheritance
5237/// — per §3.4.2.5 clause 2.1, a restriction's {attribute wildcard} IS
5238/// the complete wildcard, which is absent when no local <anyAttribute>
5239/// or attribute-group wildcard contributes one.
5240///
5241/// The return value is target-namespace-free: all `##targetNamespace` /
5242/// `##other` / list tokens have been resolved against each contributor's
5243/// origin target namespace, so the runtime can match attributes against
5244/// `EffectiveAttributeWildcard.namespace` directly.
5245pub(crate) fn compute_runtime_attribute_wildcard(
5246    schema_set: &SchemaSet,
5247    ct_key: ComplexTypeKey,
5248) -> Option<EffectiveAttributeWildcard> {
5249    compute_runtime_attribute_wildcard_bounded(schema_set, ct_key, 0)
5250}
5251
5252fn compute_runtime_attribute_wildcard_bounded(
5253    schema_set: &SchemaSet,
5254    ct_key: ComplexTypeKey,
5255    depth: u32,
5256) -> Option<EffectiveAttributeWildcard> {
5257    if depth > 100 {
5258        return None;
5259    }
5260    let ct = schema_set.arenas.complex_types.get(ct_key)?;
5261
5262    // Own §3.6.2.2 result: own xs:anyAttribute combined with all referenced
5263    // attribute groups via the existing canonical helpers. Errors here
5264    // (cycle overflow) collapse to "no wildcard" — this is the runtime
5265    // path; cycles are rejected upstream.
5266    let own_local = own_attribute_wildcard_ref(ct);
5267    let own = effective_attribute_wildcard(
5268        schema_set,
5269        own_local,
5270        ct.target_namespace,
5271        &ct.resolved_attribute_groups,
5272    )
5273    .ok()
5274    .flatten();
5275
5276    let Some(TypeKey::Complex(base_key)) = ct.resolved_base_type else {
5277        return own;
5278    };
5279    if base_key == schema_set.any_type_key() {
5280        return own;
5281    }
5282
5283    match ct.derivation_method {
5284        Some(DerivationMethod::Extension) => {
5285            let base = compute_runtime_attribute_wildcard_bounded(schema_set, base_key, depth + 1);
5286            match (own, base) {
5287                (Some(a), Some(b)) => Some(union_effective_attribute_wildcards(&a, &b)),
5288                (Some(a), None) => Some(a),
5289                (None, Some(b)) => Some(b),
5290                (None, None) => None,
5291            }
5292        }
5293        // Restriction or no derivation: derived's own wildcard is
5294        // authoritative per XSD §3.4.2.5 clause 2.1 ("If {derivation
5295        // method} = restriction, then the complete wildcard"). The
5296        // base's wildcard is NOT inherited — a restriction with no
5297        // local <anyAttribute> and no attribute-group ref wildcard
5298        // has {attribute wildcard} = absent. This matches sunData
5299        // combined/008 test.10/11.n: an `alias` element restriction
5300        // of a base with `<anyAttribute namespace="urn:a urn:b"/>`
5301        // and no own wildcard must reject all foreign-namespace
5302        // attributes (cvc-complex-type.3.2 / cvc-assess-attr).
5303        _ => own,
5304    }
5305}
5306
5307/// Pull the type's "own" attribute wildcard out of either the top-level
5308/// field or the SimpleContent / ComplexContent derivation arm where the
5309/// `<xs:anyAttribute>` legitimately lives.
5310fn own_attribute_wildcard_ref(ct: &crate::arenas::ComplexTypeDefData) -> Option<&WildcardResult> {
5311    if let Some(wc) = ct.attribute_wildcard.as_ref() {
5312        return Some(wc);
5313    }
5314    match &ct.content {
5315        ComplexContentResult::Empty => None,
5316        ComplexContentResult::Simple(sc) => sc.attribute_wildcard.as_ref(),
5317        ComplexContentResult::Complex(cc) => cc.attribute_wildcard.as_ref(),
5318    }
5319}
5320
5321/// §3.4.2.5 extension union of two effective attribute wildcards.
5322///
5323/// - `namespace`: `union_canonical_ns` (set-theoretic union)
5324/// - `not_qname`: per §3.10.6.3 cos-aw-union, the union must not admit any
5325///   name that neither input wildcard admits. A literal QName excluded by
5326///   one wildcard stays excluded in the union iff the other wildcard
5327///   doesn't admit it either (whether by namespace or by its own
5328///   disallowed_names). `##defined` / `##definedSibling` keep the simple
5329///   intersection rule — each is in the result iff both inputs have it.
5330/// - `process_contents`: less restrictive of the two (Skip > Lax > Strict).
5331fn union_effective_attribute_wildcards(
5332    a: &EffectiveAttributeWildcard,
5333    b: &EffectiveAttributeWildcard,
5334) -> EffectiveAttributeWildcard {
5335    use crate::parser::frames::NotQNameItem;
5336
5337    let namespace = union_canonical_ns(&a.namespace, &b.namespace);
5338
5339    // Combine disallowed names. For each QName excluded by one side, keep
5340    // it excluded iff the other side also excludes it (by namespace or by
5341    // literal QName). For `##defined` / `##definedSibling`, the simple
5342    // intersection rule applies.
5343    let mut not_qname: Vec<NotQNameItem> = Vec::new();
5344
5345    let mut consider = |item: &NotQNameItem, other: &EffectiveAttributeWildcard| match item {
5346        NotQNameItem::QName {
5347            namespace,
5348            local_name,
5349        } => {
5350            let admitted_by_other_ns = match &other.namespace {
5351                CanonicalNs::Any => true,
5352                CanonicalNs::Enum(set) => set.contains(namespace),
5353                CanonicalNs::Not(set) => !set.contains(namespace),
5354            };
5355            let excluded_by_other_qname = other.not_qname.iter().any(|o| match o {
5356                NotQNameItem::QName {
5357                    namespace: ons,
5358                    local_name: oln,
5359                } => ons == namespace && oln == local_name,
5360                NotQNameItem::Defined | NotQNameItem::DefinedSibling => false,
5361            });
5362            if (!admitted_by_other_ns || excluded_by_other_qname) && !not_qname.contains(item) {
5363                not_qname.push(item.clone());
5364            }
5365        }
5366        NotQNameItem::Defined | NotQNameItem::DefinedSibling => {
5367            if other
5368                .not_qname
5369                .iter()
5370                .any(|o| std::mem::discriminant(o) == std::mem::discriminant(item))
5371                && !not_qname.contains(item)
5372            {
5373                not_qname.push(item.clone());
5374            }
5375        }
5376    };
5377
5378    for item in &a.not_qname {
5379        consider(item, b);
5380    }
5381    for item in &b.not_qname {
5382        consider(item, a);
5383    }
5384
5385    let process_contents = if process_contents_strictness(a.process_contents)
5386        <= process_contents_strictness(b.process_contents)
5387    {
5388        a.process_contents
5389    } else {
5390        b.process_contents
5391    };
5392
5393    EffectiveAttributeWildcard {
5394        namespace,
5395        not_qname,
5396        process_contents,
5397    }
5398}
5399
5400/// Recursive walker for `effective_attribute_wildcard`: follows
5401/// `resolved_ref` delegation, then iterates `resolved_attribute_groups`
5402/// in document order. Each referenced group's own effective wildcard is
5403/// computed and appended to `out` if it is non-absent (per §3.6.2.2
5404/// step 2 — "non-absent `{attribute wildcard}`s").
5405///
5406/// Depth guard matches `collect_attribute_group_uses` (> 20).
5407fn collect_effective_group_wildcards(
5408    schema_set: &SchemaSet,
5409    ag_key: AttributeGroupKey,
5410    out: &mut Vec<EffectiveAttributeWildcard>,
5411    depth: usize,
5412) -> SchemaResult<()> {
5413    if depth > 20 {
5414        return Err(attribute_group_cycle_error());
5415    }
5416
5417    let Some(ag) = schema_set.arenas.attribute_groups.get(ag_key) else {
5418        return Ok(());
5419    };
5420
5421    if let Some(ref_key) = ag.resolved_ref {
5422        return collect_effective_group_wildcards(schema_set, ref_key, out, depth + 1);
5423    }
5424
5425    if let Some(eff) = effective_attribute_wildcard_for_group(schema_set, ag, depth + 1)? {
5426        out.push(eff);
5427    }
5428
5429    Ok(())
5430}
5431
5432/// Compute the effective wildcard for a single attribute group, walking
5433/// `resolved_ref` delegation and `resolved_attribute_groups` recursively.
5434/// Separate from the top-level `effective_attribute_wildcard` so the depth
5435/// counter propagates correctly through nested calls.
5436fn effective_attribute_wildcard_for_group(
5437    schema_set: &SchemaSet,
5438    ag: &crate::arenas::AttributeGroupData,
5439    depth: usize,
5440) -> SchemaResult<Option<EffectiveAttributeWildcard>> {
5441    if depth > 20 {
5442        return Err(attribute_group_cycle_error());
5443    }
5444
5445    if let Some(ref_key) = ag.resolved_ref {
5446        let Some(target) = schema_set.arenas.attribute_groups.get(ref_key) else {
5447            return Ok(None);
5448        };
5449        return effective_attribute_wildcard_for_group(schema_set, target, depth + 1);
5450    }
5451
5452    let local = ag
5453        .attribute_wildcard
5454        .as_ref()
5455        .map(|w| normalize_attribute_wildcard(schema_set, w, ag.target_namespace));
5456
5457    let mut w: Vec<EffectiveAttributeWildcard> = Vec::new();
5458    for &nested_key in &ag.resolved_attribute_groups {
5459        collect_effective_group_wildcards(schema_set, nested_key, &mut w, depth + 1)?;
5460    }
5461
5462    Ok(combine_effective_wildcards(local, w))
5463}
5464
5465/// Check that `derived` is a valid restriction of `base` (derived ⊆ base)
5466/// per cos-ns-subset (§3.10.6.2) on attribute wildcards.
5467///
5468/// Verifies:
5469/// 1. canonical namespace subset (clauses 1-4 of §3.10.6.2 on the
5470///    namespace constraint),
5471/// 2. each QName in `base.not_qname` must not be allowed by `derived`
5472///    (§3.10.6.2 disallowed_names clause 1). "Not allowed" covers any
5473///    rejection mechanism on the derived side: namespace constraint,
5474///    literal notQName entry, or `##defined` (which rejects any
5475///    globally declared attribute). This is delegated to
5476///    `effective_wildcard_allows_attribute` so all three mechanisms
5477///    are checked uniformly.
5478/// 3. if `base.not_qname` contains `##defined`, derived must also
5479///    (§3.10.6.2 clause 2); same for `##sibling` (clause 3). These
5480///    two keywords require literal containment, unlike QName members.
5481/// 4. derived processContents strictness ≥ base strictness (mirrors
5482///    `validate_open_content_restriction` at derivation.rs:2488-2501).
5483///
5484/// Takes `schema_set` because `##defined` coverage requires a lookup
5485/// against `schema_set.lookup_attribute` via
5486/// `effective_wildcard_allows_attribute`.
5487///
5488/// On failure returns `Err(reason)` so callers can build informative
5489/// error messages.
5490fn effective_attribute_wildcard_restricts(
5491    schema_set: &SchemaSet,
5492    derived: &EffectiveAttributeWildcard,
5493    base: &EffectiveAttributeWildcard,
5494) -> Result<(), &'static str> {
5495    use crate::parser::frames::NotQNameItem;
5496
5497    if !canonical_ns_subset(&derived.namespace, &base.namespace) {
5498        return Err("namespace constraint is not a subset of the base wildcard");
5499    }
5500
5501    // §3.10.6.2 disallowed_names clause 1: each QName member of base's
5502    // not_qname must not be admitted by derived. `effective_wildcard_allows_attribute`
5503    // correctly handles namespace-constraint rejection, literal QName
5504    // exclusion, and `##defined` (with schema lookup) in one pass.
5505    for item in &base.not_qname {
5506        match item {
5507            NotQNameItem::QName {
5508                namespace,
5509                local_name,
5510            } => {
5511                if effective_wildcard_allows_attribute(schema_set, derived, *namespace, *local_name)
5512                {
5513                    return Err(
5514                        "notQName exclusions do not cover the base wildcard's disallowed names",
5515                    );
5516                }
5517            }
5518            // Clause 2: `##defined` requires literal containment.
5519            NotQNameItem::Defined => {
5520                if !derived
5521                    .not_qname
5522                    .iter()
5523                    .any(|d| matches!(d, NotQNameItem::Defined))
5524                {
5525                    return Err("base wildcard excludes ##defined but derived does not");
5526                }
5527            }
5528            // Clause 3: `##definedSibling` requires literal containment.
5529            NotQNameItem::DefinedSibling => {
5530                if !derived
5531                    .not_qname
5532                    .iter()
5533                    .any(|d| matches!(d, NotQNameItem::DefinedSibling))
5534                {
5535                    return Err("base wildcard excludes ##definedSibling but derived does not");
5536                }
5537            }
5538        }
5539    }
5540
5541    if process_contents_strictness(derived.process_contents)
5542        < process_contents_strictness(base.process_contents)
5543    {
5544        return Err("processContents is weaker than the base wildcard");
5545    }
5546
5547    Ok(())
5548}
5549
5550/// Does this effective wildcard admit a specific `(namespace, name)`
5551/// attribute?
5552///
5553/// Mirror of `wildcard_allows_attribute` (derivation.rs:3091) operating
5554/// on the canonical form. Preserves the load-bearing `NotQNameItem::Defined`
5555/// semantics documented at derivation.rs:3078-3090 — `##defined` only
5556/// excludes attributes that are actually globally declared, not an
5557/// unconditional block.
5558pub(crate) fn effective_wildcard_allows_attribute(
5559    schema_set: &SchemaSet,
5560    wc: &EffectiveAttributeWildcard,
5561    attr_namespace: Option<NameId>,
5562    attr_name: NameId,
5563) -> bool {
5564    // Namespace constraint check.
5565    let ns_ok = match &wc.namespace {
5566        CanonicalNs::Any => true,
5567        CanonicalNs::Enum(set) => set.contains(&attr_namespace),
5568        CanonicalNs::Not(set) => !set.contains(&attr_namespace),
5569    };
5570    if !ns_ok {
5571        return false;
5572    }
5573
5574    // not_qname exclusions, including ##defined schema lookup.
5575    for item in &wc.not_qname {
5576        match item {
5577            crate::parser::frames::NotQNameItem::QName {
5578                namespace: qns,
5579                local_name,
5580            } => {
5581                if *qns == attr_namespace && *local_name == attr_name {
5582                    return false;
5583                }
5584            }
5585            crate::parser::frames::NotQNameItem::Defined => {
5586                if schema_set
5587                    .lookup_attribute(attr_namespace, attr_name)
5588                    .is_some()
5589                {
5590                    return false;
5591                }
5592            }
5593            crate::parser::frames::NotQNameItem::DefinedSibling => {
5594                // Not meaningful for attribute wildcards; ignore (matches
5595                // `wildcard_allows_attribute`).
5596            }
5597        }
5598    }
5599
5600    true
5601}
5602
5603/// Recursively expand an attribute group into effective attribute uses.
5604fn collect_attribute_group_uses(
5605    schema_set: &SchemaSet,
5606    ag_key: AttributeGroupKey,
5607    result: &mut Vec<EffectiveAttributeUse>,
5608    depth: usize,
5609) {
5610    if depth > 20 {
5611        return;
5612    }
5613
5614    let Some(ag) = schema_set.arenas.attribute_groups.get(ag_key) else {
5615        return;
5616    };
5617
5618    if let Some(ref_key) = ag.resolved_ref {
5619        collect_attribute_group_uses(schema_set, ref_key, result, depth + 1);
5620        return;
5621    }
5622
5623    for (i, attr_use) in ag.attributes.iter().enumerate() {
5624        let resolved = ag.resolved_attributes.get(i);
5625        if let Some(eau) = resolve_single_attribute_use(schema_set, attr_use, resolved) {
5626            result.push(eau);
5627        }
5628    }
5629
5630    for &nested_key in &ag.resolved_attribute_groups {
5631        collect_attribute_group_uses(schema_set, nested_key, result, depth + 1);
5632    }
5633}
5634
5635/// Delegate to `SchemaSet::is_type_derived_from` with no method exclusions.
5636fn is_type_derived_from(schema_set: &SchemaSet, derived_key: TypeKey, base_key: TypeKey) -> bool {
5637    schema_set.is_type_derived_from(derived_key, base_key, DerivationSet::empty())
5638}
5639
5640/// Outcome of comparing derived and base effective attribute wildcards.
5641/// Shared by the complex-type restriction path and the redefine
5642/// attribute-group restriction path.
5643enum WildcardRestrictionOutcome {
5644    /// Derived has no effective wildcard; any base is valid.
5645    DerivedAbsent,
5646    /// Derived has a wildcard but base has none — invalid restriction.
5647    AddedInDerived,
5648    /// Both have wildcards and the subset check failed with the given
5649    /// reason string.
5650    NotSubset(&'static str),
5651    /// Both have wildcards and derived is a valid restriction of base.
5652    Valid,
5653}
5654
5655/// Compare two precomputed effective attribute wildcards and classify
5656/// the restriction relationship. The caller is responsible for deciding
5657/// how to report each outcome.
5658fn classify_attribute_wildcard_restriction(
5659    schema_set: &SchemaSet,
5660    derived_eff: Option<&EffectiveAttributeWildcard>,
5661    base_eff: Option<&EffectiveAttributeWildcard>,
5662) -> WildcardRestrictionOutcome {
5663    match (derived_eff, base_eff) {
5664        (None, _) => WildcardRestrictionOutcome::DerivedAbsent,
5665        (Some(_), None) => WildcardRestrictionOutcome::AddedInDerived,
5666        (Some(d), Some(b)) => match effective_attribute_wildcard_restricts(schema_set, d, b) {
5667            Ok(()) => WildcardRestrictionOutcome::Valid,
5668            Err(reason) => WildcardRestrictionOutcome::NotSubset(reason),
5669        },
5670    }
5671}
5672
5673/// True when the complex type has no local attribute wildcard AND no
5674/// attribute groups that could contribute one — the §3.6.2.2 walk is
5675/// guaranteed to return `None`, so callers can skip the full computation.
5676fn complex_type_has_no_attribute_wildcard_source(
5677    type_def: &crate::arenas::ComplexTypeDefData,
5678) -> bool {
5679    type_def.attribute_wildcard.is_none() && type_def.resolved_attribute_groups.is_empty()
5680}
5681
5682/// Validate attribute uses in a complex type restriction.
5683///
5684/// derivation-ok-restriction clause 3 (§3.4.6.3): If E's attributes satisfy
5685/// T's attribute constraints, they must also satisfy B's.  This means:
5686/// - Required attributes in the base must remain required in the derived type
5687///
5688/// derivation-ok-restriction clause 4: Attribute types in T must be validly
5689/// substitutable for those in B.
5690fn validate_attribute_restriction(
5691    schema_set: &SchemaSet,
5692    derived: &crate::arenas::ComplexTypeDefData,
5693    base: &crate::arenas::ComplexTypeDefData,
5694) -> SchemaResult<()> {
5695    let derived_attrs = collect_effective_attribute_uses(schema_set, derived);
5696    let base_attrs = collect_effective_attribute_uses(schema_set, base);
5697
5698    let location = derived
5699        .source
5700        .as_ref()
5701        .and_then(|s| schema_set.source_maps.locate(s));
5702    let type_name = format_type_name(schema_set, derived.name, derived.target_namespace);
5703    let base_name = format_type_name(schema_set, base.name, base.target_namespace);
5704
5705    // Check clause 3: required base attributes must remain required in the
5706    // derived type's effective {attribute uses}.
5707    //
5708    // Per §3.4.2.3 mapping rules, an attribute use in the base that is NOT
5709    // matched (by name + target namespace) by a directly-declared use in the
5710    // restriction is inherited into the derived type's {attribute uses}
5711    // unchanged.  The derived type therefore satisfies clause 3 trivially for
5712    // inherited attribute uses — we only need to reject cases where the
5713    // derived type explicitly *declares* the attribute with a weaker use
5714    // kind (optional or prohibited).
5715    for base_attr in &base_attrs {
5716        if base_attr.use_kind != AttributeUseKind::Required {
5717            continue;
5718        }
5719
5720        // Find matching derived attribute by expanded name (namespace + local)
5721        let derived_attr = derived_attrs
5722            .iter()
5723            .find(|a| a.name == base_attr.name && a.target_namespace == base_attr.target_namespace);
5724
5725        match derived_attr {
5726            // Explicit re-declaration preserves required-ness: OK.
5727            Some(da) if da.use_kind == AttributeUseKind::Required => {}
5728            // Not declared in restriction → inherited from base as required: OK.
5729            None => {}
5730            // Explicit re-declaration with weaker use (optional / prohibited):
5731            // the derived type's effective {attribute uses} no longer guarantees
5732            // presence — reject as invalid restriction.
5733            Some(_) => {
5734                let attr_name_str = schema_set.name_table.resolve(base_attr.name);
5735                return Err(SchemaError::structural(
5736                    "derivation-ok-restriction",
5737                    format!(
5738                        "Complex type '{}' restricting '{}': base type requires attribute '{}' \
5739                         but the derived type weakens it to optional or prohibited",
5740                        type_name, base_name, attr_name_str,
5741                    ),
5742                    location,
5743                ));
5744            }
5745        }
5746    }
5747
5748    // Check clause 4: attribute type derivation
5749    for derived_attr in &derived_attrs {
5750        let Some(derived_type_key) = derived_attr.resolved_type else {
5751            continue;
5752        };
5753
5754        let base_attr = base_attrs.iter().find(|a| {
5755            a.name == derived_attr.name && a.target_namespace == derived_attr.target_namespace
5756        });
5757        let Some(base_attr) = base_attr else { continue };
5758        let Some(base_type_key) = base_attr.resolved_type else {
5759            continue;
5760        };
5761
5762        if derived_type_key == base_type_key {
5763            continue;
5764        }
5765
5766        if !is_type_derived_from(schema_set, derived_type_key, base_type_key) {
5767            let attr_name_str = schema_set.name_table.resolve(derived_attr.name);
5768            return Err(SchemaError::structural(
5769                "derivation-ok-restriction",
5770                format!(
5771                    "Complex type '{}' restricting '{}': attribute '{}' has a type \
5772                     that is not validly derived from the base attribute type",
5773                    type_name, base_name, attr_name_str,
5774                ),
5775                location,
5776            ));
5777        }
5778    }
5779
5780    // §3.4.6.3 derivation-ok-restriction value-constraint check: when the
5781    // base attribute use has a `fixed` value, the derived attribute use
5782    // (when re-declared) must also have a `fixed` value equal to the base's.
5783    // A derived `default` or no value constraint over a base `fixed` is
5784    // invalid because it loosens the constraint.
5785    for base_attr in &base_attrs {
5786        let Some(base_fixed) = base_attr.fixed_value.as_deref() else {
5787            continue;
5788        };
5789        // Find re-declared derived attr matching this base attr.
5790        let Some(derived_attr) = derived_attrs
5791            .iter()
5792            .find(|a| a.name == base_attr.name && a.target_namespace == base_attr.target_namespace)
5793        else {
5794            // Inherited unchanged: OK.
5795            continue;
5796        };
5797        match derived_attr.fixed_value.as_deref() {
5798            Some(d_fixed)
5799                if crate::validation::simple::fixed_values_equal(
5800                    d_fixed,
5801                    base_fixed,
5802                    base_attr.resolved_type,
5803                    schema_set,
5804                ) => {}
5805            Some(d_fixed) => {
5806                let attr_name_str = schema_set.name_table.resolve(derived_attr.name);
5807                return Err(SchemaError::structural(
5808                    "derivation-ok-restriction",
5809                    format!(
5810                        "Complex type '{}' restricting '{}': attribute '{}' \
5811                         changes 'fixed' value from '{}' to '{}'",
5812                        type_name, base_name, attr_name_str, base_fixed, d_fixed,
5813                    ),
5814                    location,
5815                ));
5816            }
5817            None => {
5818                // Derived has no fixed value (either default or nothing) — too
5819                // loose under base's fixed constraint.
5820                let attr_name_str = schema_set.name_table.resolve(derived_attr.name);
5821                let what = if derived_attr.default_value.is_some() {
5822                    "uses 'default' (cannot weaken base 'fixed')"
5823                } else {
5824                    "drops the 'fixed' value constraint"
5825                };
5826                return Err(SchemaError::structural(
5827                    "derivation-ok-restriction",
5828                    format!(
5829                        "Complex type '{}' restricting '{}': attribute '{}' {}",
5830                        type_name, base_name, attr_name_str, what,
5831                    ),
5832                    location,
5833                ));
5834            }
5835        }
5836    }
5837
5838    // §3.4.6.3 clause 3 / "subsumes" clause 5.3 (XSD 1.1 only):
5839    // for each attribute use that exists in both the base and the derived
5840    // type, the base's {inheritable} must equal the derived's. The default
5841    // binding for an attribute information item subsumes only when the
5842    // attribute use's inheritability matches; flipping it changes the
5843    // descendant attribute-inheritance graph, which is not a valid
5844    // restriction.
5845    if schema_set.is_xsd11() {
5846        for derived_attr in &derived_attrs {
5847            let Some(base_attr) = base_attrs.iter().find(|a| {
5848                a.name == derived_attr.name && a.target_namespace == derived_attr.target_namespace
5849            }) else {
5850                continue;
5851            };
5852            if base_attr.inheritable != derived_attr.inheritable {
5853                let attr_name_str = schema_set.name_table.resolve(derived_attr.name);
5854                return Err(SchemaError::structural(
5855                    "derivation-ok-restriction",
5856                    format!(
5857                        "Complex type '{}' restricting '{}': attribute '{}' changes \
5858                         {{inheritable}} from {} to {}",
5859                        type_name,
5860                        base_name,
5861                        attr_name_str,
5862                        base_attr.inheritable,
5863                        derived_attr.inheritable,
5864                    ),
5865                    location,
5866                ));
5867            }
5868        }
5869    }
5870
5871    // §3.4.6.3 clause 3 (attribute wildcard half): compute the effective
5872    // attribute wildcard for both sides per §3.6.2.2 and verify
5873    // derived ⊆ base. When the derived type has no wildcard source at
5874    // all, the §3.6.2.2 walk is guaranteed to return `None` and the
5875    // restriction is trivially valid — skip both walks in that common
5876    // case to avoid O(types × groups) arena lookups per compile.
5877    if !complex_type_has_no_attribute_wildcard_source(derived) {
5878        let derived_eff = effective_attribute_wildcard(
5879            schema_set,
5880            derived.attribute_wildcard.as_ref(),
5881            derived.target_namespace,
5882            &derived.resolved_attribute_groups,
5883        )?;
5884        let base_eff = effective_attribute_wildcard(
5885            schema_set,
5886            base.attribute_wildcard.as_ref(),
5887            base.target_namespace,
5888            &base.resolved_attribute_groups,
5889        )?;
5890
5891        match classify_attribute_wildcard_restriction(
5892            schema_set,
5893            derived_eff.as_ref(),
5894            base_eff.as_ref(),
5895        ) {
5896            WildcardRestrictionOutcome::DerivedAbsent | WildcardRestrictionOutcome::Valid => {}
5897            WildcardRestrictionOutcome::AddedInDerived => {
5898                return Err(SchemaError::structural(
5899                    "derivation-ok-restriction",
5900                    format!(
5901                        "Complex type '{}' restricting '{}': derived type has an attribute \
5902                         wildcard but the base type does not",
5903                        type_name, base_name,
5904                    ),
5905                    location,
5906                ));
5907            }
5908            WildcardRestrictionOutcome::NotSubset(reason) => {
5909                return Err(SchemaError::structural(
5910                    "derivation-ok-restriction",
5911                    format!(
5912                        "Complex type '{}' restricting '{}': attribute wildcard is not \
5913                         a valid restriction of the base wildcard: {}",
5914                        type_name, base_name, reason,
5915                    ),
5916                    location,
5917                ));
5918            }
5919        }
5920    }
5921
5922    Ok(())
5923}
5924
5925/// Treat a facet-bound-literal validation failure as acceptable only when it
5926/// is a same-kind bound violation *and* the derived literal equals the base
5927/// type's matching bound literal. XSD Part 2 §4.3.9 permits equality at the
5928/// boundary (derived `maxExclusive` = base `maxExclusive`) even though the
5929/// base's value space excludes values equal to its own bound.
5930fn is_bound_self_violation(
5931    err: &crate::validation::errors::ValidationError,
5932    kind: FacetKind,
5933    schema_set: &SchemaSet,
5934    base_key: TypeKey,
5935    value: &str,
5936) -> bool {
5937    let code = match kind {
5938        FacetKind::MaxExclusive => "cvc-maxExclusive-valid",
5939        FacetKind::MaxInclusive => "cvc-maxInclusive-valid",
5940        FacetKind::MinExclusive => "cvc-minExclusive-valid",
5941        FacetKind::MinInclusive => "cvc-minInclusive-valid",
5942        _ => return false,
5943    };
5944    if err.constraint != code {
5945        return false;
5946    }
5947    let Some(base_bound) = find_base_bound_literal(schema_set, base_key, kind) else {
5948        return false;
5949    };
5950    let Some(v) = parse_past_own_bound(schema_set, base_key, value) else {
5951        return false;
5952    };
5953    let Some(b) = parse_past_own_bound(schema_set, base_key, &base_bound) else {
5954        return false;
5955    };
5956    v.typed_value == b.typed_value
5957}
5958
5959/// Parse `value` as an instance of `base_key`, falling back to the nearest
5960/// ancestor without bound facets when the direct parse fails on a same-kind
5961/// bound violation (the boundary-equality case this helper exists to serve).
5962fn parse_past_own_bound(
5963    schema_set: &SchemaSet,
5964    base_key: TypeKey,
5965    value: &str,
5966) -> Option<crate::validation::simple::SimpleTypeResult> {
5967    if let Ok(r) = crate::validation::simple::validate_simple_type(value, base_key, schema_set) {
5968        return Some(r);
5969    }
5970    let without_bounds = lexical_base(schema_set, base_key)?;
5971    crate::validation::simple::validate_simple_type(value, without_bounds, schema_set).ok()
5972}
5973
5974/// Walk past bound-restriction types to find a primitive base suitable for
5975/// lexical-only parsing of a bound literal.
5976fn lexical_base(schema_set: &SchemaSet, base_key: TypeKey) -> Option<TypeKey> {
5977    let mut current = base_key;
5978    for _ in 0..100 {
5979        match current {
5980            TypeKey::Simple(sk) => {
5981                let st = schema_set.arenas.simple_types.get(sk)?;
5982                let has_bounds = st.facets.min_inclusive.is_some()
5983                    || st.facets.min_exclusive.is_some()
5984                    || st.facets.max_inclusive.is_some()
5985                    || st.facets.max_exclusive.is_some();
5986                if !has_bounds {
5987                    return Some(current);
5988                }
5989                current = st.resolved_base_type?;
5990            }
5991            TypeKey::Complex(_) => return None,
5992        }
5993    }
5994    None
5995}
5996
5997/// Find the base type's same-kind bound literal by walking the simple-type
5998/// chain. Returns the first matching facet literal encountered.
5999fn find_base_bound_literal(
6000    schema_set: &SchemaSet,
6001    base_key: TypeKey,
6002    kind: FacetKind,
6003) -> Option<String> {
6004    let mut current = base_key;
6005    for _ in 0..100 {
6006        match current {
6007            TypeKey::Simple(sk) => {
6008                let st = schema_set.arenas.simple_types.get(sk)?;
6009                let literal = match kind {
6010                    FacetKind::MaxExclusive => {
6011                        st.facets.max_exclusive.as_ref().map(|f| f.value.clone())
6012                    }
6013                    FacetKind::MaxInclusive => {
6014                        st.facets.max_inclusive.as_ref().map(|f| f.value.clone())
6015                    }
6016                    FacetKind::MinExclusive => {
6017                        st.facets.min_exclusive.as_ref().map(|f| f.value.clone())
6018                    }
6019                    FacetKind::MinInclusive => {
6020                        st.facets.min_inclusive.as_ref().map(|f| f.value.clone())
6021                    }
6022                    _ => None,
6023                };
6024                if let Some(v) = literal {
6025                    return Some(v);
6026                }
6027                current = st.resolved_base_type?;
6028            }
6029            TypeKey::Complex(_) => return None,
6030        }
6031    }
6032    None
6033}
6034
6035/// Walk the complex type extension chain to find the effective simple content
6036/// type key. Returns `None` if there is no simple content type in the chain.
6037fn effective_simple_content_type_key(
6038    schema_set: &SchemaSet,
6039    type_def: &crate::arenas::ComplexTypeDefData,
6040) -> Option<TypeKey> {
6041    let mut current_base = type_def.resolved_base_type?;
6042    for _ in 0..50 {
6043        match current_base {
6044            TypeKey::Simple(sk) => return Some(TypeKey::Simple(sk)),
6045            TypeKey::Complex(ck) => {
6046                let ct = schema_set.arenas.complex_types.get(ck)?;
6047                current_base = ct.resolved_base_type?;
6048            }
6049        }
6050    }
6051    None
6052}
6053
6054/// Validate simpleContent restriction inline simpleType.
6055///
6056/// derivation-ok-restriction clause 2.2.2.1 (§3.4.6.3): let S_B = B's content
6057/// type simple type definition and S_T = T's content type simple type definition.
6058/// S_T must be validly derived from S_B.
6059fn validate_simple_content_restriction(
6060    schema_set: &SchemaSet,
6061    derived: &crate::arenas::ComplexTypeDefData,
6062    base: &crate::arenas::ComplexTypeDefData,
6063) -> SchemaResult<()> {
6064    // Only applies when derived has simpleContent with an inline simpleType
6065    let ComplexContentResult::Simple(ref sc) = derived.content else {
6066        return Ok(());
6067    };
6068
6069    let Some(ref inline_st) = sc.content_type else {
6070        return Ok(());
6071    };
6072
6073    // Find the base type's effective simple content type
6074    let Some(base_simple_key) = effective_simple_content_type_key(schema_set, base) else {
6075        return Ok(());
6076    };
6077
6078    // anySimpleType is the ur-type of all simple types — any variety
6079    // is a valid restriction.  Per §3.14.6 clause 2, inline list/union
6080    // types automatically derive from anySimpleType.  This function
6081    // only checks variety compatibility, not facets, so the early
6082    // return is safe.
6083    if let TypeKey::Simple(sk) = base_simple_key {
6084        if sk == schema_set.builtin_types().any_simple_type {
6085            return Ok(());
6086        }
6087    }
6088
6089    // Get the base simple type's variety
6090    let base_variety = match base_simple_key {
6091        TypeKey::Simple(sk) => schema_set.arenas.simple_types.get(sk).map(|st| st.variety),
6092        TypeKey::Complex(_) => None,
6093    };
6094
6095    let Some(base_variety) = base_variety else {
6096        return Ok(());
6097    };
6098
6099    // Check variety compatibility:
6100    // A list type cannot restrict an atomic type.
6101    // A union type cannot restrict an atomic type (unless it's a restriction of
6102    // the base union/atomic via resolved_base_type chain).
6103    let derived_variety = inline_st.variety;
6104
6105    if derived_variety != base_variety {
6106        // Different varieties — check if the inline type's base chain leads to
6107        // the base simple type (which would mean it's a valid restriction despite
6108        // variety difference, e.g. restriction of a union member).
6109        // For the common case (list restricting atomic, union restricting atomic),
6110        // this chain walk will NOT find the base type.
6111        if let Some(inline_resolved_base) = resolve_inline_simple_type_base(schema_set, inline_st) {
6112            if is_type_derived_from(schema_set, inline_resolved_base, base_simple_key) {
6113                return Ok(());
6114            }
6115        }
6116
6117        let location = derived
6118            .source
6119            .as_ref()
6120            .and_then(|s| schema_set.source_maps.locate(s));
6121        let type_name = format_type_name(schema_set, derived.name, derived.target_namespace);
6122        let base_name = format_type_name(schema_set, base.name, base.target_namespace);
6123        return Err(SchemaError::structural(
6124            "derivation-ok-restriction",
6125            format!(
6126                "Complex type '{}' restricting '{}': simpleContent inline type \
6127                 has variety {:?} which is not a valid restriction of the base \
6128                 type's simple content (variety {:?})",
6129                type_name, base_name, derived_variety, base_variety,
6130            ),
6131            location,
6132        ));
6133    }
6134
6135    Ok(())
6136}
6137
6138/// Try to resolve the base type key of an inline SimpleTypeResult.
6139/// The inline type may have a base_type as a QName that has been resolved,
6140/// or it may reference a known type directly.
6141fn resolve_inline_simple_type_base(
6142    schema_set: &SchemaSet,
6143    inline_st: &crate::parser::frames::SimpleTypeResult,
6144) -> Option<TypeKey> {
6145    // For inline types used in simpleContent/restriction, the base_type
6146    // is the type the restriction derives from. If it was resolved during
6147    // assembly, it would be in the arena. We can try to find it by matching
6148    // the QName if present.
6149    match &inline_st.base_type {
6150        Some(crate::parser::frames::TypeRefResult::QName(qname)) => {
6151            schema_set.lookup_type(qname.namespace, qname.local_name)
6152        }
6153        _ => None,
6154    }
6155}
6156
6157/// XSD 1.0 §3.2.6 constraint 2 (`cos-attribute-decl`): if an attribute's type
6158/// is or derives from xs:ID, it must not have a value constraint (default or
6159/// fixed). XSD 1.1 relaxes this restriction. Called from the pipeline after
6160/// reference resolution.
6161pub fn validate_attribute_id_constraints(schema_set: &SchemaSet) -> SchemaResult<()> {
6162    use crate::types::XmlTypeCode;
6163
6164    if !schema_set.is_xsd10() {
6165        return Ok(());
6166    }
6167
6168    let id_key = match schema_set.builtin_types().get_by_type_code(XmlTypeCode::Id) {
6169        Some(k) => k,
6170        None => return Ok(()),
6171    };
6172
6173    for (_key, attr_data) in schema_set.arenas.attributes.iter() {
6174        if attr_data.default_value.is_none() && attr_data.fixed_value.is_none() {
6175            continue;
6176        }
6177        if let Some(TypeKey::Simple(st_key)) = attr_data.resolved_type {
6178            if schema_set.derives_from(st_key, id_key) {
6179                let attr_name = attr_data
6180                    .name
6181                    .map(|n| schema_set.name_table.resolve(n).to_string())
6182                    .unwrap_or_else(|| "(anonymous)".to_string());
6183                let constraint = if attr_data.default_value.is_some() {
6184                    "default"
6185                } else {
6186                    "fixed"
6187                };
6188                return Err(SchemaError::structural(
6189                    "cos-attribute-decl",
6190                    format!(
6191                        "Attribute '{}' has type xs:ID (or derived) and must not have a {} value constraint",
6192                        attr_name, constraint
6193                    ),
6194                    schema_set.locate(attr_data.source.as_ref()),
6195                ));
6196            }
6197        }
6198    }
6199
6200    for (_key, ct_data) in schema_set.arenas.complex_types.iter() {
6201        for (i, attr_use) in ct_data.attributes.iter().enumerate() {
6202            if attr_use.use_kind == AttributeUseKind::Prohibited {
6203                continue;
6204            }
6205            let resolved = ct_data.resolved_attributes.get(i);
6206            let ref_decl = resolved
6207                .and_then(|r| r.resolved_ref)
6208                .and_then(|k| schema_set.arenas.attributes.get(k));
6209
6210            let has_constraint = attr_use.attribute.default_value.is_some()
6211                || attr_use.attribute.fixed_value.is_some()
6212                || ref_decl.is_some_and(|d| d.default_value.is_some() || d.fixed_value.is_some());
6213            if !has_constraint {
6214                continue;
6215            }
6216
6217            let attr_type = resolved
6218                .and_then(|r| r.resolved_type)
6219                .or_else(|| ref_decl.and_then(|d| d.resolved_type));
6220            if let Some(TypeKey::Simple(st_key)) = attr_type {
6221                if schema_set.derives_from(st_key, id_key) {
6222                    let attr_name = attr_use
6223                        .attribute
6224                        .name
6225                        .map(|n| schema_set.name_table.resolve(n).to_string())
6226                        .or_else(|| {
6227                            ref_decl
6228                                .and_then(|d| d.name)
6229                                .map(|n| schema_set.name_table.resolve(n).to_string())
6230                        })
6231                        .unwrap_or_else(|| "(anonymous)".to_string());
6232                    let constraint = if attr_use.attribute.default_value.is_some()
6233                        || ref_decl.and_then(|d| d.default_value.as_ref()).is_some()
6234                    {
6235                        "default"
6236                    } else {
6237                        "fixed"
6238                    };
6239                    let location = attr_use
6240                        .attribute
6241                        .source
6242                        .as_ref()
6243                        .or(ct_data.source.as_ref())
6244                        .and_then(|s| schema_set.source_maps.locate(s));
6245                    return Err(SchemaError::structural(
6246                        "cos-attribute-decl",
6247                        format!(
6248                            "Attribute '{}' has type xs:ID (or derived) and must not have a {} value constraint",
6249                            attr_name, constraint
6250                        ),
6251                        location,
6252                    ));
6253                }
6254            }
6255        }
6256    }
6257
6258    Ok(())
6259}
6260
6261/// `a-props-correct.3`: validate that attribute `default`/`fixed` values
6262/// are type-valid for the declared type.
6263///
6264/// Walks every globally-declared attribute and every attribute use inside
6265/// complex types and rejects when the value constraint cannot be parsed
6266/// against the attribute's declared simple type.
6267pub fn validate_attribute_value_constraints(schema_set: &SchemaSet) -> SchemaResult<()> {
6268    // Top-level attribute declarations
6269    for (_key, attr) in schema_set.arenas.attributes.iter() {
6270        if attr.source.is_none() {
6271            // Built-in xsi:* attributes have `source: None`; skip them.
6272            continue;
6273        }
6274        // a-props-correct.1 / src-attribute: the {type definition} of every
6275        // attribute must be a simple type definition. attD002 (`type="ct"`
6276        // where `ct` is a complex type with simpleContent).
6277        if matches!(attr.resolved_type, Some(TypeKey::Complex(_))) {
6278            let attr_name = attr
6279                .name
6280                .map(|n| schema_set.name_table.resolve(n).to_string())
6281                .unwrap_or_else(|| "(anonymous)".to_string());
6282            return Err(SchemaError::structural(
6283                "a-props-correct",
6284                format!(
6285                    "Attribute '{}' references a complex type; the type definition of an \
6286                     attribute must be a simple type",
6287                    attr_name,
6288                ),
6289                schema_set.locate(attr.source.as_ref()),
6290            ));
6291        }
6292        let (value, is_fixed) = match (&attr.default_value, &attr.fixed_value) {
6293            (Some(v), _) => (v.as_str(), false),
6294            (_, Some(v)) => (v.as_str(), true),
6295            (None, None) => continue,
6296        };
6297        let Some(type_key @ TypeKey::Simple(_)) = attr.resolved_type else {
6298            continue;
6299        };
6300        if crate::validation::simple::validate_simple_type(value, type_key, schema_set).is_err() {
6301            let attr_name = attr
6302                .name
6303                .map(|n| schema_set.name_table.resolve(n).to_string())
6304                .unwrap_or_else(|| "(anonymous)".to_string());
6305            let constraint = if is_fixed { "fixed" } else { "default" };
6306            return Err(SchemaError::structural(
6307                "a-props-correct",
6308                format!(
6309                    "Attribute '{}' {} value '{}' is not valid for its declared type",
6310                    attr_name, constraint, value
6311                ),
6312                schema_set.locate(attr.source.as_ref()),
6313            ));
6314        }
6315    }
6316
6317    // Attribute uses inside complex types
6318    for (_key, ct) in schema_set.arenas.complex_types.iter() {
6319        for (i, attr_use) in ct.attributes.iter().enumerate() {
6320            if attr_use.use_kind == AttributeUseKind::Prohibited {
6321                continue;
6322            }
6323            let resolved = ct.resolved_attributes.get(i);
6324            let ref_decl = resolved
6325                .and_then(|r| r.resolved_ref)
6326                .and_then(|k| schema_set.arenas.attributes.get(k));
6327
6328            // Use the local (override) value-constraint when present;
6329            // otherwise the global declaration's. au-props-correct.2 says
6330            // a use's fixed must equal the declaration's, but here we only
6331            // need to check that *some* effective value-constraint parses.
6332            let value_constraint: Option<(&str, bool)> = attr_use
6333                .attribute
6334                .fixed_value
6335                .as_deref()
6336                .map(|v| (v, true))
6337                .or_else(|| {
6338                    attr_use
6339                        .attribute
6340                        .default_value
6341                        .as_deref()
6342                        .map(|v| (v, false))
6343                })
6344                .or_else(|| {
6345                    ref_decl.and_then(|d| {
6346                        d.fixed_value
6347                            .as_deref()
6348                            .map(|v| (v, true))
6349                            .or_else(|| d.default_value.as_deref().map(|v| (v, false)))
6350                    })
6351                });
6352
6353            // au-props-correct.2 (§3.5.6): if the referenced attribute
6354            // declaration has `fixed`, then a `fixed` on the attribute use
6355            // must denote the same value (the use's `default` is forbidden
6356            // when the declaration is fixed). Literal comparison after
6357            // whitespace collapse — sufficient for the `xs:string`-flavoured
6358            // fixed-value cases the test suite exercises (addB108, attO025).
6359            if let (Some(use_fixed), Some(decl_fixed)) = (
6360                attr_use.attribute.fixed_value.as_deref(),
6361                ref_decl.and_then(|d| d.fixed_value.as_deref()),
6362            ) {
6363                use crate::types::WhitespaceMode;
6364                let normalize = |s: &str| -> String {
6365                    crate::types::facets::normalize_whitespace(s, WhitespaceMode::Collapse)
6366                };
6367                if normalize(use_fixed) != normalize(decl_fixed) {
6368                    let attr_name = ref_decl
6369                        .and_then(|d| d.name)
6370                        .map(|n| schema_set.name_table.resolve(n).to_string())
6371                        .unwrap_or_else(|| "(anonymous)".to_string());
6372                    let location = attr_use
6373                        .attribute
6374                        .source
6375                        .as_ref()
6376                        .or(ct.source.as_ref())
6377                        .and_then(|s| schema_set.source_maps.locate(s));
6378                    return Err(SchemaError::structural(
6379                        "au-props-correct",
6380                        format!(
6381                            "Attribute use 'fixed' value '{}' on '{}' does not match the \
6382                             referenced attribute declaration's 'fixed' value '{}'",
6383                            use_fixed, attr_name, decl_fixed,
6384                        ),
6385                        location,
6386                    ));
6387                }
6388            }
6389
6390            // au-props-correct.2 also forbids a `default` on the use when
6391            // the declaration has `fixed`.
6392            if attr_use.attribute.default_value.is_some()
6393                && ref_decl.and_then(|d| d.fixed_value.as_deref()).is_some()
6394            {
6395                let attr_name = ref_decl
6396                    .and_then(|d| d.name)
6397                    .map(|n| schema_set.name_table.resolve(n).to_string())
6398                    .unwrap_or_else(|| "(anonymous)".to_string());
6399                let location = attr_use
6400                    .attribute
6401                    .source
6402                    .as_ref()
6403                    .or(ct.source.as_ref())
6404                    .and_then(|s| schema_set.source_maps.locate(s));
6405                return Err(SchemaError::structural(
6406                    "au-props-correct",
6407                    format!(
6408                        "Attribute use cannot specify 'default' for '{}' because the \
6409                         referenced attribute declaration has 'fixed'",
6410                        attr_name,
6411                    ),
6412                    location,
6413                ));
6414            }
6415
6416            let Some((value, is_fixed)) = value_constraint else {
6417                continue;
6418            };
6419
6420            let attr_type = resolved
6421                .and_then(|r| r.resolved_type)
6422                .or_else(|| ref_decl.and_then(|d| d.resolved_type));
6423            let Some(type_key @ TypeKey::Simple(_)) = attr_type else {
6424                continue;
6425            };
6426            if crate::validation::simple::validate_simple_type(value, type_key, schema_set).is_err()
6427            {
6428                let attr_name = attr_use
6429                    .attribute
6430                    .name
6431                    .map(|n| schema_set.name_table.resolve(n).to_string())
6432                    .or_else(|| {
6433                        ref_decl
6434                            .and_then(|d| d.name)
6435                            .map(|n| schema_set.name_table.resolve(n).to_string())
6436                    })
6437                    .unwrap_or_else(|| "(anonymous)".to_string());
6438                let constraint = if is_fixed { "fixed" } else { "default" };
6439                let location = attr_use
6440                    .attribute
6441                    .source
6442                    .as_ref()
6443                    .or(ct.source.as_ref())
6444                    .and_then(|s| schema_set.source_maps.locate(s));
6445                return Err(SchemaError::structural(
6446                    "a-props-correct",
6447                    format!(
6448                        "Attribute '{}' {} value '{}' is not valid for its declared type",
6449                        attr_name, constraint, value
6450                    ),
6451                    location,
6452                ));
6453            }
6454        }
6455    }
6456
6457    Ok(())
6458}
6459
6460/// `e-props-correct.2` and `e-props-correct.4`: validate that element
6461/// `default`/`fixed` values are type-valid for the declared type.
6462///
6463/// - `e-props-correct.2`: the value must be valid for the element's type.
6464/// - `e-props-correct.4`: if the type is (or derives from) xs:ID, no value
6465///   constraint is allowed.
6466pub fn validate_element_value_constraints(schema_set: &SchemaSet) -> SchemaResult<()> {
6467    use crate::parser::frames::ComplexContentResult;
6468    use crate::types::XmlTypeCode;
6469
6470    let id_key = schema_set.builtin_types().get_by_type_code(XmlTypeCode::Id);
6471    let any_type_key = TypeKey::Complex(schema_set.any_type_key());
6472
6473    for (_key, elem) in schema_set.arenas.elements.iter() {
6474        let (value, is_fixed) = match (&elem.default_value, &elem.fixed_value) {
6475            (Some(v), _) => (v.as_str(), false),
6476            (_, Some(v)) => (v.as_str(), true),
6477            (None, None) => continue,
6478        };
6479
6480        // Element refs inherit constraints from the referenced element
6481        if elem.resolved_ref.is_some() {
6482            continue;
6483        }
6484
6485        let type_key = match elem.resolved_type {
6486            Some(tk) if tk != any_type_key => tk,
6487            _ => continue,
6488        };
6489
6490        let elem_name = || {
6491            elem.name
6492                .map(|n| schema_set.name_table.resolve_ref(n))
6493                .unwrap_or("(anonymous)")
6494        };
6495        let location = || schema_set.locate(elem.source.as_ref());
6496        let constraint = if is_fixed { "fixed" } else { "default" };
6497
6498        // src-element §3.3.3 clause 3.2: when an element declaration has a
6499        // `default` or `fixed` value, the type definition must be either a
6500        // simple type, a complex type with simple content, or a complex
6501        // type with mixed=true. Anything else (complex non-simple, non-mixed
6502        // content) is invalid.
6503        if let TypeKey::Complex(ct_key) = type_key {
6504            if let Some(ct) = schema_set.arenas.complex_types.get(ct_key) {
6505                let simple_content = matches!(ct.content, ComplexContentResult::Simple(_));
6506                if !simple_content && !ct.mixed {
6507                    return Err(SchemaError::structural(
6508                        "src-element",
6509                        format!(
6510                            "Element '{}' has '{}' value but its type is a complex type \
6511                             with non-mixed, non-simple content",
6512                            elem_name(),
6513                            constraint
6514                        ),
6515                        location(),
6516                    ));
6517                }
6518            }
6519        }
6520
6521        // e-props-correct.4: xs:ID (or derived) cannot have a value constraint.
6522        // XSD 1.1 §3.3.6.1 removes this restriction (it has no analogous clause);
6523        // only apply in XSD 1.0 mode.
6524        if !schema_set.is_xsd11() {
6525            if let (Some(id_simple_key), TypeKey::Simple(st_key)) = (id_key, type_key) {
6526                if schema_set.derives_from(st_key, id_simple_key) {
6527                    return Err(SchemaError::structural(
6528                        "e-props-correct.4",
6529                        format!(
6530                            "Element '{}' has type xs:ID (or derived) and must not have a {} value constraint",
6531                            elem_name(), constraint
6532                        ),
6533                        location(),
6534                    ));
6535                }
6536            }
6537        }
6538
6539        // e-props-correct.2: value must be valid for the declared type
6540        let effective_type = match type_key {
6541            TypeKey::Simple(_) => Some(type_key),
6542            TypeKey::Complex(ck) => schema_set
6543                .arenas
6544                .complex_types
6545                .get(ck)
6546                .and_then(|ct| effective_simple_content_type_key(schema_set, ct)),
6547        };
6548
6549        if let Some(st_key) = effective_type {
6550            if crate::validation::simple::validate_simple_type(value, st_key, schema_set).is_err() {
6551                return Err(SchemaError::structural(
6552                    "e-props-correct.2",
6553                    format!(
6554                        "Element '{}' {} value '{}' is not valid for its declared type",
6555                        elem_name(),
6556                        constraint,
6557                        value
6558                    ),
6559                    location(),
6560                ));
6561            }
6562        }
6563    }
6564
6565    Ok(())
6566}
6567
6568/// XSD 1.1 §3.3.2 Schema Representation Constraint: Type Alternative
6569/// Representation OK (`src-type-alternative`).
6570///
6571/// Among an element's sequence of `<xs:alternative>` children, only the last
6572/// alternative is allowed to omit the `test` attribute (acting as a default
6573/// fallback). An alternative without `@test` in a non-final position is a
6574/// schema error.
6575#[cfg(feature = "xsd11")]
6576pub fn validate_element_type_alternatives(schema_set: &SchemaSet) -> SchemaResult<()> {
6577    if !schema_set.is_xsd11() {
6578        return Ok(());
6579    }
6580    for (_key, elem) in schema_set.arenas.elements.iter() {
6581        let alts = &elem.alternatives;
6582        if alts.len() < 2 {
6583            continue;
6584        }
6585        for alt in &alts[..alts.len() - 1] {
6586            if alt.test.is_none() {
6587                let name = elem
6588                    .name
6589                    .map(|n| schema_set.name_table.resolve_ref(n))
6590                    .unwrap_or("(anonymous)");
6591                let location = schema_set.locate(elem.source.as_ref());
6592                return Err(SchemaError::structural(
6593                    "src-type-alternative",
6594                    format!(
6595                        "Element '{}': <xs:alternative> without a 'test' attribute is only \
6596                         permitted as the last alternative",
6597                        name
6598                    ),
6599                    location,
6600                ));
6601            }
6602        }
6603    }
6604    Ok(())
6605}
6606
6607/// `ct-props-correct.4` / `ag-props-correct.2`: every complex type's
6608/// effective `{attribute uses}` must contain at most one entry per
6609/// `(target_namespace, name)`. Two distinct attribute declarations with the
6610/// same expanded name are forbidden by both XSD 1.0 and XSD 1.1.
6611///
6612/// The check is keyed by *declaration identity*, not by `(name, namespace)`,
6613/// so reaching the same declaration along multiple paths (including XSD 1.1
6614/// circular attribute groups) does not produce a false positive — only
6615/// genuinely distinct declarations that happen to share an expanded name
6616/// are flagged. The W3C `attQ011` fixture exercises the cross-attribute-
6617/// group case where attribute "foo" appears once via a global
6618/// `<attribute ref="x:foo"/>` reference and once via a redefined
6619/// `<attributeGroup ref="x:red"/>` whose members include a local
6620/// `<attribute name="foo"/>`.
6621pub fn validate_complex_type_attribute_uniqueness(schema_set: &SchemaSet) -> SchemaResult<()> {
6622    use std::collections::{HashMap, HashSet};
6623
6624    // Stable identity of an attribute declaration. Variants:
6625    // - `GlobalRef(k)`        — `<xs:attribute ref="...">` resolving to
6626    //                            global attribute key `k`.
6627    // - `InlineGroup(g, i)`   — i-th inline `<xs:attribute>` in
6628    //                            attribute group `g`.
6629    // - `InlineComplex(c, i)` — i-th inline `<xs:attribute>` in
6630    //                            complex type `c`.
6631    #[derive(Hash, PartialEq, Eq, Clone, Copy)]
6632    enum AttrDeclId {
6633        GlobalRef(AttributeKey),
6634        InlineGroup(AttributeGroupKey, usize),
6635        InlineComplex(ComplexTypeKey, usize),
6636    }
6637
6638    fn walk_attribute_group(
6639        schema_set: &SchemaSet,
6640        ag_key: AttributeGroupKey,
6641        visiting_groups: &mut HashSet<AttributeGroupKey>,
6642        seen: &mut HashSet<AttrDeclId>,
6643        out: &mut Vec<EffectiveAttributeUse>,
6644    ) {
6645        if !visiting_groups.insert(ag_key) {
6646            return;
6647        }
6648        let Some(ag) = schema_set.arenas.attribute_groups.get(ag_key) else {
6649            visiting_groups.remove(&ag_key);
6650            return;
6651        };
6652        if let Some(ref_key) = ag.resolved_ref {
6653            walk_attribute_group(schema_set, ref_key, visiting_groups, seen, out);
6654            visiting_groups.remove(&ag_key);
6655            return;
6656        }
6657
6658        for (i, attr_use) in ag.attributes.iter().enumerate() {
6659            let resolved = ag.resolved_attributes.get(i);
6660            let decl_id = if let Some(global_key) = resolved.and_then(|r| r.resolved_ref) {
6661                AttrDeclId::GlobalRef(global_key)
6662            } else {
6663                AttrDeclId::InlineGroup(ag_key, i)
6664            };
6665            if seen.insert(decl_id) {
6666                if let Some(eau) = resolve_single_attribute_use(schema_set, attr_use, resolved) {
6667                    out.push(eau);
6668                }
6669            }
6670        }
6671        for &nested in &ag.resolved_attribute_groups {
6672            walk_attribute_group(schema_set, nested, visiting_groups, seen, out);
6673        }
6674
6675        visiting_groups.remove(&ag_key);
6676    }
6677
6678    fn collect_with_dedup(
6679        schema_set: &SchemaSet,
6680        type_def: &crate::arenas::ComplexTypeDefData,
6681        ct_key: ComplexTypeKey,
6682        depth: usize,
6683        visiting_groups: &mut HashSet<AttributeGroupKey>,
6684        seen: &mut HashSet<AttrDeclId>,
6685        out: &mut Vec<EffectiveAttributeUse>,
6686    ) {
6687        if depth > 50 {
6688            return;
6689        }
6690        for (i, attr_use) in type_def.attributes.iter().enumerate() {
6691            let resolved = type_def.resolved_attributes.get(i);
6692            let decl_id = if let Some(global_key) = resolved.and_then(|r| r.resolved_ref) {
6693                AttrDeclId::GlobalRef(global_key)
6694            } else {
6695                AttrDeclId::InlineComplex(ct_key, i)
6696            };
6697            if seen.insert(decl_id) {
6698                if let Some(eau) = resolve_single_attribute_use(schema_set, attr_use, resolved) {
6699                    out.push(eau);
6700                }
6701            }
6702        }
6703        for &ag_key in &type_def.resolved_attribute_groups {
6704            visiting_groups.clear();
6705            walk_attribute_group(schema_set, ag_key, visiting_groups, seen, out);
6706        }
6707        if type_def.derivation_method == Some(DerivationMethod::Extension) {
6708            if let Some(TypeKey::Complex(base_key)) = type_def.resolved_base_type {
6709                if let Some(base) = schema_set.arenas.complex_types.get(base_key) {
6710                    collect_with_dedup(
6711                        schema_set,
6712                        base,
6713                        base_key,
6714                        depth + 1,
6715                        visiting_groups,
6716                        seen,
6717                        out,
6718                    );
6719                }
6720            }
6721        }
6722    }
6723
6724    // Reusable scratch buffers, cleared per type to avoid per-iteration
6725    // allocator traffic on schemas with many complex types.
6726    let mut seen: HashSet<AttrDeclId> = HashSet::new();
6727    let mut attrs: Vec<EffectiveAttributeUse> = Vec::new();
6728    let mut visiting_groups: HashSet<AttributeGroupKey> = HashSet::new();
6729    let mut by_name: HashMap<(Option<NameId>, NameId), ()> = HashMap::new();
6730
6731    // ct-props-correct clause 4 is XSD 1.0-only. Hoist the `xs:ID` builtin
6732    // lookup out of the per-complex-type loop; it's an arena-backed constant
6733    // for the lifetime of the schema set.
6734    let id_key_for_xsd10 = if schema_set.is_xsd10() {
6735        schema_set
6736            .builtin_types()
6737            .get_by_type_code(crate::types::XmlTypeCode::Id)
6738    } else {
6739        None
6740    };
6741
6742    for (key, type_def) in schema_set.arenas.complex_types.iter() {
6743        seen.clear();
6744        attrs.clear();
6745        by_name.clear();
6746        collect_with_dedup(
6747            schema_set,
6748            type_def,
6749            key,
6750            0,
6751            &mut visiting_groups,
6752            &mut seen,
6753            &mut attrs,
6754        );
6755
6756        // §3.4.6: a prohibited attribute use is NOT an entry in the
6757        // `{attribute uses}` set, so it cannot collide with a (re-)declared
6758        // use in a derived type.
6759        attrs.retain(|eau| eau.use_kind != AttributeUseKind::Prohibited);
6760
6761        for attr in &attrs {
6762            if by_name
6763                .insert((attr.target_namespace, attr.name), ())
6764                .is_some()
6765            {
6766                let attr_name_str = schema_set.name_table.resolve(attr.name);
6767                let type_name =
6768                    format_type_name(schema_set, type_def.name, type_def.target_namespace);
6769                let location = type_def
6770                    .source
6771                    .as_ref()
6772                    .and_then(|s| schema_set.source_maps.locate(s));
6773                return Err(SchemaError::structural(
6774                    "ct-props-correct",
6775                    format!(
6776                        "Complex type '{}': two distinct attribute declarations \
6777                         with the same expanded name '{}' (ct-props-correct \
6778                         clause 4 / ag-props-correct clause 2)",
6779                        type_name, attr_name_str,
6780                    ),
6781                    location,
6782                ));
6783            }
6784        }
6785
6786        // ct-props-correct clause 4 (XSD 1.0 only): "Two distinct members of
6787        // the {attribute uses} must not have {type definition}s which are
6788        // both `xs:ID` or are derived from `xs:ID`." XSD 1.1 dropped this
6789        // constraint — see saxon's id001 test which explicitly documents
6790        // that an XSD 1.1 type may declare multiple ID-typed attributes.
6791        if let Some(id_key) = id_key_for_xsd10 {
6792            let mut id_attrs = attrs.iter().filter(|attr| match attr.resolved_type {
6793                Some(TypeKey::Simple(st_key)) => schema_set.derives_from(st_key, id_key),
6794                _ => false,
6795            });
6796            if let (Some(first), Some(second)) = (id_attrs.next(), id_attrs.next()) {
6797                let first_name = schema_set.name_table.resolve(first.name);
6798                let second_name = schema_set.name_table.resolve(second.name);
6799                let type_name =
6800                    format_type_name(schema_set, type_def.name, type_def.target_namespace);
6801                let location = type_def
6802                    .source
6803                    .as_ref()
6804                    .and_then(|s| schema_set.source_maps.locate(s));
6805                return Err(SchemaError::structural(
6806                    "ct-props-correct",
6807                    format!(
6808                        "Complex type '{}': attributes '{}' and '{}' both have \
6809                         xs:ID-derived types (ct-props-correct clause 4; XSD 1.0 only)",
6810                        type_name, first_name, second_name,
6811                    ),
6812                    location,
6813                ));
6814            }
6815        }
6816    }
6817    Ok(())
6818}
6819
6820/// XSD 1.1 §3.10.6.1 rule 4 (Wildcard Properties Correct): for every QName
6821/// member in a wildcard's `{disallowed names}`, that QName's namespace name
6822/// must be admitted by the wildcard's `{namespace constraint}` (the
6823/// combination of `namespace` and `notNamespace`).
6824///
6825/// In other words: the schema cannot list a notQName entry whose namespace
6826/// the wildcard already excludes — such an entry would be redundant and
6827/// the spec rules it out as a structural error. Covers W3C saxonData
6828/// `wild031`..`wild035` (and is the post-parse step that backs the
6829/// per-entry checks `parse_not_qname` performs at parse time).
6830///
6831/// The check needs the resolved target namespace of the wildcard's owner
6832/// to interpret `##targetNamespace`/`##other`/`##local`, which is only
6833/// available after assembly — hence this is a separate pipeline pass
6834/// rather than something `parse_not_qname` can do on its own.
6835#[cfg(feature = "xsd11")]
6836pub fn validate_wildcard_disallowed_names(schema_set: &SchemaSet) -> SchemaResult<()> {
6837    if !schema_set.is_xsd11() {
6838        return Ok(());
6839    }
6840
6841    fn check_wildcard(
6842        schema_set: &SchemaSet,
6843        wc: &WildcardResult,
6844        target_ns: Option<NameId>,
6845    ) -> SchemaResult<()> {
6846        use crate::parser::frames::NotQNameItem;
6847
6848        for item in &wc.not_qname {
6849            let NotQNameItem::QName {
6850                namespace: q_ns,
6851                local_name,
6852            } = item
6853            else {
6854                continue;
6855            };
6856            // The QName must satisfy the wildcard's namespace constraint
6857            // (cvc-wildcard-namespace §3.10.4.3): it must be admitted by
6858            // the positive constraint AND not be excluded by notNamespace.
6859            let admitted_by_constraint =
6860                wildcard_namespace_matches(&wc.namespace, *q_ns, target_ns);
6861            let excluded_by_not_namespace = wc
6862                .not_namespace
6863                .iter()
6864                .any(|t| t.resolve(target_ns) == *q_ns);
6865            if !admitted_by_constraint || excluded_by_not_namespace {
6866                let location = schema_set.locate(wc.source.as_ref());
6867                let qname_text = match q_ns {
6868                    Some(ns) => format!(
6869                        "{{{}}}:{}",
6870                        schema_set.name_table.resolve_ref(*ns),
6871                        schema_set.name_table.resolve_ref(*local_name),
6872                    ),
6873                    None => schema_set.name_table.resolve_ref(*local_name).to_string(),
6874                };
6875                return Err(SchemaError::structural(
6876                    "w-props-correct",
6877                    format!(
6878                        "notQName entry '{}' is not admitted by the wildcard's \
6879                         namespace constraint (§3.10.6.1 rule 4)",
6880                        qname_text
6881                    ),
6882                    location,
6883                ));
6884            }
6885        }
6886        Ok(())
6887    }
6888
6889    fn check_particle(
6890        schema_set: &SchemaSet,
6891        particle: &ParticleResult,
6892        target_ns: Option<NameId>,
6893        depth: usize,
6894    ) -> SchemaResult<()> {
6895        if depth > 100 {
6896            return Ok(());
6897        }
6898        match &particle.term {
6899            ParticleTerm::Any(wc) => check_wildcard(schema_set, wc, target_ns)?,
6900            ParticleTerm::Group(group) => {
6901                for child in &group.particles {
6902                    check_particle(schema_set, child, target_ns, depth + 1)?;
6903                }
6904            }
6905            ParticleTerm::Element(_) => {}
6906        }
6907        Ok(())
6908    }
6909
6910    // Complex types: own attribute_wildcard + content particles + any
6911    // attribute_wildcard hiding inside the SimpleContent / ComplexContent
6912    // derivation defs.
6913    for (_key, ct) in schema_set.arenas.complex_types.iter() {
6914        let target_ns = ct.target_namespace;
6915        if let Some(wc) = ct.attribute_wildcard.as_ref() {
6916            check_wildcard(schema_set, wc, target_ns)?;
6917        }
6918        match &ct.content {
6919            ComplexContentResult::Empty => {}
6920            ComplexContentResult::Simple(sc) => {
6921                if let Some(wc) = sc.attribute_wildcard.as_ref() {
6922                    check_wildcard(schema_set, wc, target_ns)?;
6923                }
6924            }
6925            ComplexContentResult::Complex(cc) => {
6926                if let Some(wc) = cc.attribute_wildcard.as_ref() {
6927                    check_wildcard(schema_set, wc, target_ns)?;
6928                }
6929                if let Some(p) = cc.particle.as_ref() {
6930                    check_particle(schema_set, p, target_ns, 0)?;
6931                }
6932                if let Some(oc) = cc.open_content.as_ref() {
6933                    if let Some(wc) = oc.wildcard.as_ref() {
6934                        check_wildcard(schema_set, wc, target_ns)?;
6935                    }
6936                }
6937            }
6938        }
6939        if let Some(oc) = ct.open_content.as_ref() {
6940            if let Some(wc) = oc.wildcard.as_ref() {
6941                check_wildcard(schema_set, wc, target_ns)?;
6942            }
6943        }
6944    }
6945
6946    // Attribute groups: own attribute_wildcard.
6947    for (_key, ag) in schema_set.arenas.attribute_groups.iter() {
6948        if let Some(wc) = ag.attribute_wildcard.as_ref() {
6949            check_wildcard(schema_set, wc, ag.target_namespace)?;
6950        }
6951    }
6952
6953    // Model-group definitions: walk content particles for element wildcards.
6954    for (_key, mg) in schema_set.arenas.model_groups.iter() {
6955        for child in &mg.particles {
6956            check_particle(schema_set, child, mg.target_namespace, 0)?;
6957        }
6958    }
6959
6960    Ok(())
6961}
6962
6963/// XSD 1.1 §3.8.6.3 / cos-element-consistent (second clause): when a complex
6964/// type's content model contains both a local element declaration with
6965/// expanded name Q AND a strict/lax wildcard that admits Q, AND a top-level
6966/// element declaration G with expanded name Q exists, then the type tables
6967/// of the local element and G must be either both absent or both present
6968/// and equivalent.
6969///
6970/// Closes wild078/079 (local has no type table, global has one) and wild081
6971/// (local has a type table, global doesn't).
6972#[cfg(feature = "xsd11")]
6973pub fn validate_wildcard_element_type_table_consistency(
6974    schema_set: &SchemaSet,
6975) -> SchemaResult<()> {
6976    use crate::parser::frames::AlternativeResult;
6977
6978    if !schema_set.is_xsd11() {
6979        return Ok(());
6980    }
6981
6982    /// Walk a particle tree using the parallel `local_keys`/`flat_idx` scheme
6983    /// from `allocate_content_particle_elements`. For each local element
6984    /// particle, look up its allocated arena key (where post-resolution
6985    /// alternatives live) instead of relying on the stale parser-frame copy.
6986    #[allow(clippy::too_many_arguments)]
6987    fn walk_collect<'a>(
6988        particle: &'a ParticleResult,
6989        target_ns: Option<NameId>,
6990        schema_set: &'a SchemaSet,
6991        local_keys: &[Option<ElementKey>],
6992        flat_idx: &mut usize,
6993        local_elems: &mut Vec<(Option<NameId>, NameId, ElementKey, Option<SourceRef>)>,
6994        wildcards: &mut Vec<&'a WildcardResult>,
6995        depth: usize,
6996    ) {
6997        if depth > 100 {
6998            return;
6999        }
7000        match &particle.term {
7001            ParticleTerm::Element(elem) => {
7002                if let Some(ref_qn) = &elem.ref_name {
7003                    // ref slot: increment flat_idx but no local key.
7004                    *flat_idx += 1;
7005                    let _ = ref_qn;
7006                } else if let Some(name) = elem.name {
7007                    let ns = elem.target_namespace.or(target_ns);
7008                    let idx = *flat_idx;
7009                    *flat_idx += 1;
7010                    if let Some(key) = local_keys.get(idx).copied().flatten() {
7011                        local_elems.push((ns, name, key, elem.source.clone()));
7012                    }
7013                }
7014            }
7015            ParticleTerm::Any(wc) => {
7016                wildcards.push(wc);
7017            }
7018            ParticleTerm::Group(group) => {
7019                if let Some(ref_qn) = &group.ref_name {
7020                    if let Some(group_key) =
7021                        schema_set.lookup_model_group(ref_qn.namespace, ref_qn.local_name)
7022                    {
7023                        let mg = &schema_set.arenas.model_groups[group_key];
7024                        let mg_ns = mg.target_namespace.or(target_ns);
7025                        let mut group_flat_idx = 0usize;
7026                        for child in &mg.particles {
7027                            walk_collect(
7028                                child,
7029                                mg_ns,
7030                                schema_set,
7031                                &mg.resolved_particle_elements,
7032                                &mut group_flat_idx,
7033                                local_elems,
7034                                wildcards,
7035                                depth + 1,
7036                            );
7037                        }
7038                    }
7039                    // Group refs do not advance the outer flat_idx (mirrors
7040                    // collect_content_particle_elements_recursive).
7041                } else {
7042                    for child in &group.particles {
7043                        walk_collect(
7044                            child,
7045                            target_ns,
7046                            schema_set,
7047                            local_keys,
7048                            flat_idx,
7049                            local_elems,
7050                            wildcards,
7051                            depth + 1,
7052                        );
7053                    }
7054                }
7055            }
7056        }
7057    }
7058
7059    /// Resolve an alternative's effective type — fall back to looking up the
7060    /// QName via `schema_set.lookup_type` when the parser-frame copy hasn't
7061    /// been resolved yet (which is the case for local element alternatives
7062    /// allocated post-`resolve_all_references`).
7063    fn alt_effective_type(alt: &AlternativeResult, schema_set: &SchemaSet) -> Option<TypeKey> {
7064        use crate::parser::frames::TypeRefResult;
7065        if let Some(t) = alt.resolved_type {
7066            return Some(t);
7067        }
7068        if let Some(TypeRefResult::QName(qname)) = &alt.type_ref {
7069            return schema_set
7070                .lookup_type(qname.namespace, qname.local_name)
7071                .or_else(|| {
7072                    schema_set.get_built_in_type_by_qname(qname.namespace, qname.local_name)
7073                });
7074        }
7075        None
7076    }
7077
7078    fn alternatives_equivalent(
7079        a: &[AlternativeResult],
7080        b: &[AlternativeResult],
7081        schema_set: &SchemaSet,
7082    ) -> bool {
7083        if a.len() != b.len() {
7084            return false;
7085        }
7086        for (x, y) in a.iter().zip(b.iter()) {
7087            if x.test != y.test {
7088                return false;
7089            }
7090            if alt_effective_type(x, schema_set) != alt_effective_type(y, schema_set) {
7091                return false;
7092            }
7093        }
7094        true
7095    }
7096
7097    for (_key, ct) in schema_set.arenas.complex_types.iter() {
7098        let target_ns = ct.target_namespace;
7099        let ComplexContentResult::Complex(cc) = &ct.content else {
7100            continue;
7101        };
7102        let Some(particle) = cc.particle.as_ref() else {
7103            continue;
7104        };
7105
7106        let mut local_elems: Vec<(Option<NameId>, NameId, ElementKey, Option<SourceRef>)> =
7107            Vec::new();
7108        let mut wildcards: Vec<&WildcardResult> = Vec::new();
7109        let mut flat_idx = 0usize;
7110        walk_collect(
7111            particle,
7112            target_ns,
7113            schema_set,
7114            &ct.resolved_content_particle_elements,
7115            &mut flat_idx,
7116            &mut local_elems,
7117            &mut wildcards,
7118            0,
7119        );
7120
7121        // Open-content wildcards also count.
7122        if let Some(oc) = cc.open_content.as_ref() {
7123            if let Some(wc) = oc.wildcard.as_ref() {
7124                wildcards.push(wc);
7125            }
7126        }
7127
7128        if wildcards.is_empty() {
7129            continue;
7130        }
7131
7132        for (l_ns, l_name, l_key, l_source) in &local_elems {
7133            let global_key = schema_set.lookup_element(*l_ns, *l_name);
7134            let Some(g_key) = global_key else {
7135                continue;
7136            };
7137            // If the local declaration is the global itself (e.g., a ref'd
7138            // element resolving back to the same arena key), skip — there's
7139            // only one declaration so no inconsistency is possible.
7140            if *l_key == g_key {
7141                continue;
7142            }
7143            let l_decl = &schema_set.arenas.elements[*l_key];
7144            let g_decl = &schema_set.arenas.elements[g_key];
7145
7146            // The wildcard must be lax/strict (skip wildcards bypass the EDC
7147            // per wild080) AND admit (l_ns, l_name).
7148            let admitted = wildcards.iter().any(|wc| {
7149                if matches!(wc.process_contents, ProcessContents::Skip) {
7150                    return false;
7151                }
7152                wildcard_result_admits_qname(wc, target_ns, *l_ns, *l_name)
7153            });
7154            if !admitted {
7155                continue;
7156            }
7157
7158            if !alternatives_equivalent(&l_decl.alternatives, &g_decl.alternatives, schema_set) {
7159                let location = schema_set
7160                    .locate(l_source.as_ref())
7161                    .or_else(|| schema_set.locate(ct.source.as_ref()));
7162                let qname = match l_ns {
7163                    Some(ns) => format!(
7164                        "{{{}}}:{}",
7165                        schema_set.name_table.resolve_ref(*ns),
7166                        schema_set.name_table.resolve_ref(*l_name),
7167                    ),
7168                    None => schema_set.name_table.resolve_ref(*l_name).to_string(),
7169                };
7170                return Err(SchemaError::structural(
7171                    "cos-element-consistent",
7172                    format!(
7173                        "Local element '{}' is in the same content model as a strict/lax \
7174                         wildcard that admits its expanded name; the local element's type \
7175                         table is not equivalent to that of the top-level declaration of \
7176                         the same name (§3.8.6.3 / cos-element-consistent)",
7177                        qname
7178                    ),
7179                    location,
7180                ));
7181            }
7182        }
7183    }
7184
7185    Ok(())
7186}
7187
7188// Shared helpers for §3.8.6.3 / §3.4.6.3 cos-element-consistent.
7189
7190/// Per-local-element record produced by [`collect_local_elements`].
7191#[cfg(feature = "xsd11")]
7192type LocalElementEntry = (Option<NameId>, NameId, ElementKey, Option<SourceRef>);
7193
7194/// Recursively walk a complex type's content model and emit one
7195/// `LocalElementEntry` per inline local element declaration.
7196///
7197/// `flat_idx` tracks the walker's position in the owning CT's
7198/// `resolved_content_particle_elements`, which is the post-resolution
7199/// arena lookup that carries CTA alternatives (the parser-frame
7200/// `AlternativeResult` slice on the `ElementParticle` is stale for
7201/// inline alternatives resolved later).
7202#[cfg(feature = "xsd11")]
7203fn walk_collect_local_elements(
7204    particle: &ParticleResult,
7205    target_ns: Option<NameId>,
7206    local_keys: &[Option<ElementKey>],
7207    flat_idx: &mut usize,
7208    out: &mut Vec<LocalElementEntry>,
7209) {
7210    if let ParticleTerm::Group(group) = &particle.term {
7211        walk_group_local_elements(&group.particles, target_ns, local_keys, flat_idx, out);
7212    }
7213}
7214
7215#[cfg(feature = "xsd11")]
7216fn walk_group_local_elements(
7217    particles: &[ParticleResult],
7218    target_ns: Option<NameId>,
7219    local_keys: &[Option<ElementKey>],
7220    flat_idx: &mut usize,
7221    out: &mut Vec<LocalElementEntry>,
7222) {
7223    for p in particles {
7224        match &p.term {
7225            ParticleTerm::Element(elem) if elem.ref_name.is_none() => {
7226                if let Some(Some(elem_key)) = local_keys.get(*flat_idx) {
7227                    let ns = elem.target_namespace.or(target_ns);
7228                    if let Some(name) = elem.name {
7229                        out.push((ns, name, *elem_key, elem.source.clone()));
7230                    }
7231                }
7232                *flat_idx += 1;
7233            }
7234            ParticleTerm::Element(_) => {
7235                *flat_idx += 1;
7236            }
7237            ParticleTerm::Group(group) if group.ref_name.is_none() => {
7238                walk_group_local_elements(&group.particles, target_ns, local_keys, flat_idx, out);
7239            }
7240            _ => {}
7241        }
7242    }
7243}
7244
7245/// Collect every local element declaration in `ct`'s content model.
7246#[cfg(feature = "xsd11")]
7247fn collect_local_elements(ct: &crate::arenas::ComplexTypeDefData) -> Vec<LocalElementEntry> {
7248    let mut out = Vec::new();
7249    let ComplexContentResult::Complex(cc) = &ct.content else {
7250        return out;
7251    };
7252    let Some(particle) = cc.particle.as_ref() else {
7253        return out;
7254    };
7255    let mut flat_idx = 0usize;
7256    walk_collect_local_elements(
7257        particle,
7258        ct.target_namespace,
7259        &ct.resolved_content_particle_elements,
7260        &mut flat_idx,
7261        &mut out,
7262    );
7263    out
7264}
7265
7266/// Resolve an alternative's effective `TypeKey`, falling back to the
7267/// schema-set's name lookup or built-in registry when the parser-frame
7268/// `resolved_type` is absent.
7269#[cfg(feature = "xsd11")]
7270fn alt_effective_type(
7271    alt: &crate::parser::frames::AlternativeResult,
7272    schema_set: &SchemaSet,
7273) -> Option<TypeKey> {
7274    use crate::parser::frames::TypeRefResult;
7275    if let Some(t) = alt.resolved_type {
7276        return Some(t);
7277    }
7278    if let Some(TypeRefResult::QName(qname)) = &alt.type_ref {
7279        return schema_set
7280            .lookup_type(qname.namespace, qname.local_name)
7281            .or_else(|| schema_set.get_built_in_type_by_qname(qname.namespace, qname.local_name));
7282    }
7283    None
7284}
7285
7286/// Two alternative lists are equivalent when they have the same length,
7287/// pairwise-equal `@test` strings, and pairwise-equal effective types.
7288#[cfg(feature = "xsd11")]
7289fn alternatives_equivalent(
7290    a: &[crate::parser::frames::AlternativeResult],
7291    b: &[crate::parser::frames::AlternativeResult],
7292    schema_set: &SchemaSet,
7293) -> bool {
7294    if a.len() != b.len() {
7295        return false;
7296    }
7297    a.iter().zip(b.iter()).all(|(x, y)| {
7298        x.test == y.test && alt_effective_type(x, schema_set) == alt_effective_type(y, schema_set)
7299    })
7300}
7301
7302/// XSD 1.1 §3.8.6.3 / cos-element-consistent: two local element declarations
7303/// with the same expanded QName in the same complex-type content model must
7304/// have equivalent type tables (or both have no type table).
7305#[cfg(feature = "xsd11")]
7306pub fn validate_local_element_type_table_consistency(schema_set: &SchemaSet) -> SchemaResult<()> {
7307    use std::collections::HashMap;
7308
7309    if !schema_set.is_xsd11() {
7310        return Ok(());
7311    }
7312
7313    for (_key, ct) in schema_set.arenas.complex_types.iter() {
7314        let local_elems = collect_local_elements(ct);
7315        if local_elems.len() < 2 {
7316            continue;
7317        }
7318
7319        let mut by_name: HashMap<(Option<NameId>, NameId), Vec<usize>> = HashMap::new();
7320        for (i, (ns, name, _, _)) in local_elems.iter().enumerate() {
7321            by_name.entry((*ns, *name)).or_default().push(i);
7322        }
7323
7324        for (qname, indices) in &by_name {
7325            if indices.len() < 2 {
7326                continue;
7327            }
7328            let first_idx = indices[0];
7329            let first_decl = &schema_set.arenas.elements[local_elems[first_idx].2];
7330            for &idx in &indices[1..] {
7331                let other_decl = &schema_set.arenas.elements[local_elems[idx].2];
7332                if alternatives_equivalent(
7333                    &first_decl.alternatives,
7334                    &other_decl.alternatives,
7335                    schema_set,
7336                ) {
7337                    continue;
7338                }
7339                let qn_str = match qname.0 {
7340                    Some(ns) => format!(
7341                        "{{{}}}{}",
7342                        schema_set.name_table.resolve_ref(ns),
7343                        schema_set.name_table.resolve_ref(qname.1),
7344                    ),
7345                    None => schema_set.name_table.resolve_ref(qname.1).to_string(),
7346                };
7347                let location = schema_set
7348                    .locate(local_elems[idx].3.as_ref())
7349                    .or_else(|| schema_set.locate(ct.source.as_ref()));
7350                return Err(SchemaError::structural(
7351                    "cos-element-consistent",
7352                    format!(
7353                        "Two local element declarations of '{}' appear in the same \
7354                         content model but their type tables are not equivalent \
7355                         (§3.8.6.3 / cos-element-consistent)",
7356                        qn_str
7357                    ),
7358                    location,
7359                ));
7360            }
7361        }
7362    }
7363
7364    Ok(())
7365}
7366
7367/// XSD 1.1 §3.4.6.3 / cos-element-consistent (cross-derivation): when a
7368/// complex type T restricts a base type B and both contain local element
7369/// declarations with the same expanded QName, the type tables of those
7370/// declarations must be equivalent (or both absent).
7371///
7372/// Complements `validate_local_element_type_table_consistency`, which
7373/// catches duplicates *within* one content model.
7374#[cfg(feature = "xsd11")]
7375pub fn validate_restriction_local_element_type_table_consistency(
7376    schema_set: &SchemaSet,
7377) -> SchemaResult<()> {
7378    use std::collections::HashMap;
7379
7380    if !schema_set.is_xsd11() {
7381        return Ok(());
7382    }
7383
7384    for (_key, ct) in schema_set.arenas.complex_types.iter() {
7385        // §3.4.6.2 (extension) only adds particles and never re-issues
7386        // them, so type-table consistency is automatic there. Restrict to
7387        // §3.4.6.3 (restriction).
7388        if ct.derivation_method != Some(crate::parser::frames::DerivationMethod::Restriction) {
7389            continue;
7390        }
7391        let Some(TypeKey::Complex(base_ck)) = ct.resolved_base_type else {
7392            continue;
7393        };
7394        let Some(base_ct) = schema_set.arenas.complex_types.get(base_ck) else {
7395            continue;
7396        };
7397
7398        let derived_locals = collect_local_elements(ct);
7399        if derived_locals.is_empty() {
7400            continue;
7401        }
7402        let base_locals = collect_local_elements(base_ct);
7403        if base_locals.is_empty() {
7404            continue;
7405        }
7406
7407        // Multiple base locals with the same name is itself invalid; the
7408        // in-CT consistency pass will reject the base independently.
7409        let mut base_by_name: HashMap<(Option<NameId>, NameId), ElementKey> = HashMap::new();
7410        for (ns, name, ek, _) in &base_locals {
7411            base_by_name.entry((*ns, *name)).or_insert(*ek);
7412        }
7413
7414        for (ns, name, derived_ek, derived_src) in &derived_locals {
7415            let Some(&base_ek) = base_by_name.get(&(*ns, *name)) else {
7416                continue;
7417            };
7418            let derived_decl = &schema_set.arenas.elements[*derived_ek];
7419            let base_decl = &schema_set.arenas.elements[base_ek];
7420            if alternatives_equivalent(
7421                &derived_decl.alternatives,
7422                &base_decl.alternatives,
7423                schema_set,
7424            ) {
7425                continue;
7426            }
7427            let qn_str = match ns {
7428                Some(ns_id) => format!(
7429                    "{{{}}}{}",
7430                    schema_set.name_table.resolve_ref(*ns_id),
7431                    schema_set.name_table.resolve_ref(*name),
7432                ),
7433                None => schema_set.name_table.resolve_ref(*name).to_string(),
7434            };
7435            let location = schema_set
7436                .locate(derived_src.as_ref())
7437                .or_else(|| schema_set.locate(ct.source.as_ref()));
7438            let derived_name = format_type_name(schema_set, ct.name, ct.target_namespace);
7439            let base_name = format_type_name(schema_set, base_ct.name, base_ct.target_namespace);
7440            return Err(SchemaError::structural(
7441                "cos-element-consistent",
7442                format!(
7443                    "Complex type '{}' restricting '{}': local element '{}' has a \
7444                     type table that is not equivalent to the base type's local \
7445                     element of the same name (§3.4.6.3 / cos-element-consistent)",
7446                    derived_name, base_name, qn_str,
7447                ),
7448                location,
7449            ));
7450        }
7451    }
7452
7453    Ok(())
7454}
7455
7456/// Whether the wildcard's namespace constraint and notQName admit the QName
7457/// `(ns, name)`. Treats `##defined` and `##definedSibling` pessimistically
7458/// (rejects).
7459#[cfg(feature = "xsd11")]
7460fn wildcard_result_admits_qname(
7461    wc: &WildcardResult,
7462    target_ns: Option<NameId>,
7463    ns: Option<NameId>,
7464    name: NameId,
7465) -> bool {
7466    use crate::parser::frames::NotQNameItem;
7467    if !wildcard_namespace_matches(&wc.namespace, ns, target_ns) {
7468        return false;
7469    }
7470    if wc.not_namespace.iter().any(|t| t.resolve(target_ns) == ns) {
7471        return false;
7472    }
7473    !wc.not_qname.iter().any(|item| match item {
7474        NotQNameItem::QName {
7475            namespace,
7476            local_name,
7477        } => *namespace == ns && *local_name == name,
7478        NotQNameItem::Defined | NotQNameItem::DefinedSibling => true,
7479    })
7480}
7481
7482/// XSD 1.0 §3.2.17 lexical check for `xs:anyURI` source attributes on
7483/// `xs:appinfo` and `xs:documentation`. The W3C `anyURI_a001_1336` fixture
7484/// places `source="9999...anyURI:"` and `source="1111...http://foo/bar"`
7485/// on annotations of an element declaration; both have a colon whose
7486/// scheme prefix starts with a digit, which is invalid per RFC 2396.
7487/// XSD 1.1 explicitly relaxed the rule, so this validator is XSD 1.0-only.
7488///
7489/// We deliberately scope the check to annotation `source` attributes:
7490///   - directives' `schemaLocation` values like `"0"` and `"123"` are
7491///     valid relative URIs per RFC 2396 and survive any reasonable
7492///     strict lexer;
7493///   - the same goes for `xs:notation/@public`/`@system` and
7494///     `xs:anyAttribute/@namespace` numeric values in the same fixture.
7495///
7496/// The annotation source values are the only unambiguously-malformed
7497/// anyURIs in the fixture, and they alone are sufficient to make the
7498/// schema fail per the W3C "one or more invalid anyURIs" ruling.
7499pub fn validate_xsd10_annotation_source_anyuri(schema_set: &SchemaSet) -> SchemaResult<()> {
7500    use crate::schema::annotation::{Annotation, AnnotationItem};
7501    use crate::types::validators::is_strict_xsd10_anyuri;
7502
7503    if !schema_set.is_xsd10() {
7504        return Ok(());
7505    }
7506
7507    fn check_annotation(
7508        schema_set: &SchemaSet,
7509        annotation: Option<&Annotation>,
7510    ) -> SchemaResult<()> {
7511        let Some(annotation) = annotation else {
7512            return Ok(());
7513        };
7514        for item in &annotation.items {
7515            match item {
7516                AnnotationItem::AppInfo(ai) => {
7517                    if let Some(ref src) = ai.source {
7518                        if !is_strict_xsd10_anyuri(src) {
7519                            let location = ai
7520                                .source_ref
7521                                .as_ref()
7522                                .and_then(|s| schema_set.source_maps.locate(s));
7523                            return Err(SchemaError::structural(
7524                                "cvc-datatype-valid",
7525                                format!(
7526                                    "<xs:appinfo source=\"{}\"> is not a valid xs:anyURI \
7527                                     (XSD 1.0 strict scheme syntax)",
7528                                    src
7529                                ),
7530                                location,
7531                            ));
7532                        }
7533                    }
7534                }
7535                AnnotationItem::Documentation(d) => {
7536                    if let Some(ref src) = d.source {
7537                        if !is_strict_xsd10_anyuri(src) {
7538                            let location = d
7539                                .source_ref
7540                                .as_ref()
7541                                .and_then(|s| schema_set.source_maps.locate(s));
7542                            return Err(SchemaError::structural(
7543                                "cvc-datatype-valid",
7544                                format!(
7545                                    "<xs:documentation source=\"{}\"> is not a valid \
7546                                     xs:anyURI (XSD 1.0 strict scheme syntax)",
7547                                    src
7548                                ),
7549                                location,
7550                            ));
7551                        }
7552                    }
7553                }
7554            }
7555        }
7556        Ok(())
7557    }
7558
7559    // Walk every arena that can carry an annotation. NOTE: anyone adding a
7560    // new annotatable arena type must extend this list.
7561    for (_k, ct) in schema_set.arenas.complex_types.iter() {
7562        check_annotation(schema_set, ct.annotation.as_ref())?;
7563    }
7564    for (_k, st) in schema_set.arenas.simple_types.iter() {
7565        check_annotation(schema_set, st.annotation.as_ref())?;
7566    }
7567    for (_k, el) in schema_set.arenas.elements.iter() {
7568        check_annotation(schema_set, el.annotation.as_ref())?;
7569    }
7570    for (_k, at) in schema_set.arenas.attributes.iter() {
7571        check_annotation(schema_set, at.annotation.as_ref())?;
7572    }
7573    for (_k, ag) in schema_set.arenas.attribute_groups.iter() {
7574        check_annotation(schema_set, ag.annotation.as_ref())?;
7575    }
7576    for (_k, mg) in schema_set.arenas.model_groups.iter() {
7577        check_annotation(schema_set, mg.annotation.as_ref())?;
7578    }
7579    for (_k, n) in schema_set.arenas.notations.iter() {
7580        check_annotation(schema_set, n.annotation.as_ref())?;
7581    }
7582    for (_k, ic) in schema_set.arenas.identity_constraints.iter() {
7583        check_annotation(schema_set, ic.annotation.as_ref())?;
7584    }
7585    // Schema-level top-level `<xs:annotation>` elements (a schema can hold
7586    // several, hence `Vec<Annotation>` rather than `Option<Annotation>`).
7587    for doc in &schema_set.documents {
7588        for ann in &doc.annotations {
7589            check_annotation(schema_set, Some(ann))?;
7590        }
7591    }
7592    Ok(())
7593}
7594
7595/// Validate `xsi:` Not Allowed (§3.2.6.4 / `no-xsi`): the `{target namespace}`
7596/// of a *user-declared* attribute must not match the XML Schema instance
7597/// namespace. The four pre-defined XSI attributes (`type`, `nil`,
7598/// `schemaLocation`, `noNamespaceSchemaLocation`) are seeded into the
7599/// attributes arena with `source: None`; that absence is the marker we use
7600/// to skip them.
7601pub fn validate_no_xsi_attribute_declarations(schema_set: &SchemaSet) -> SchemaResult<()> {
7602    use crate::namespace::table::well_known;
7603
7604    for (_key, attr) in schema_set.arenas.attributes.iter() {
7605        if attr.source.is_none() {
7606            // Built-in xsi:type / xsi:nil / xsi:schemaLocation /
7607            // xsi:noNamespaceSchemaLocation are seeded in
7608            // `types::builtin::initialize_xsi_attributes` with no source.
7609            continue;
7610        }
7611        let Some(ns) = attr.target_namespace else {
7612            continue;
7613        };
7614        if ns != well_known::XSI_NAMESPACE {
7615            continue;
7616        }
7617        let attr_name = attr
7618            .name
7619            .map(|n| schema_set.name_table.resolve_ref(n).to_string())
7620            .unwrap_or_else(|| "(anonymous)".to_string());
7621        let location = schema_set.locate(attr.source.as_ref());
7622        return Err(SchemaError::structural(
7623            "no-xsi",
7624            format!(
7625                "Attribute declaration '{}' has target namespace \
7626                 'http://www.w3.org/2001/XMLSchema-instance', which is \
7627                 reserved (no-xsi, §3.2.6.4)",
7628                attr_name
7629            ),
7630            location,
7631        ));
7632    }
7633
7634    // The same constraint must also apply to inline attribute declarations
7635    // inside attribute groups and complex types — `<attribute>` children
7636    // without a `ref=` declare a fresh attribute whose effective namespace
7637    // is determined by `form` / `attributeFormDefault`. With
7638    // `attributeFormDefault="qualified"` and `targetNamespace=XSI`
7639    // (attKb018a), each unqualified attribute in an attribute group is
7640    // promoted to the XSI namespace and must be rejected.
7641    //
7642    // Inline `target_namespace=XSI` on an `<attribute>` child reaches the
7643    // arena unchanged. The form/default-driven path only routes to XSI
7644    // when the owner's `target_namespace` is the XSI namespace, which is
7645    // exceedingly rare; skip the per-use scan when no document declares
7646    // XSI as its target.
7647    let any_xsi_owner = schema_set
7648        .documents
7649        .iter()
7650        .any(|d| d.target_namespace == Some(well_known::XSI_NAMESPACE));
7651    let owners_to_scan = any_xsi_owner;
7652    for (_key, group) in schema_set.arenas.attribute_groups.iter() {
7653        check_no_xsi_in_attribute_uses(
7654            schema_set,
7655            &group.attributes,
7656            group.target_namespace,
7657            owners_to_scan,
7658        )?;
7659    }
7660    for (_key, ct) in schema_set.arenas.complex_types.iter() {
7661        check_no_xsi_in_attribute_uses(
7662            schema_set,
7663            &ct.attributes,
7664            ct.target_namespace,
7665            owners_to_scan,
7666        )?;
7667    }
7668    Ok(())
7669}
7670
7671/// Apply `no-xsi` (§3.2.6.4) to a slice of attribute uses owned by an
7672/// attribute group or complex type. Skips `ref=` uses (they delegate to
7673/// the global declaration, which is already covered by the global pass).
7674fn check_no_xsi_in_attribute_uses(
7675    schema_set: &SchemaSet,
7676    attrs: &[crate::parser::frames::AttributeUseResult],
7677    owner_target_namespace: Option<NameId>,
7678    owner_could_be_xsi: bool,
7679) -> SchemaResult<()> {
7680    use crate::namespace::table::well_known;
7681
7682    for attr_use in attrs {
7683        if attr_use.attribute.ref_name.is_some() {
7684            continue;
7685        }
7686        // Fast-path: when the attribute carries no explicit XSI target and
7687        // no owner document declares XSI as its target, the
7688        // form/default-driven effective namespace can never be XSI.
7689        let explicit_xsi = attr_use.attribute.target_namespace == Some(well_known::XSI_NAMESPACE);
7690        if !explicit_xsi && !owner_could_be_xsi {
7691            continue;
7692        }
7693        let effective_ns = schema_set.effective_local_attribute_namespace(
7694            attr_use.attribute.target_namespace,
7695            attr_use.attribute.form.as_deref(),
7696            attr_use.attribute.source.as_ref(),
7697            owner_target_namespace,
7698        );
7699        if effective_ns != Some(well_known::XSI_NAMESPACE) {
7700            continue;
7701        }
7702        let attr_name = attr_use
7703            .attribute
7704            .name
7705            .map(|n| schema_set.name_table.resolve_ref(n).to_string())
7706            .unwrap_or_else(|| "(anonymous)".to_string());
7707        let location = schema_set.locate(attr_use.attribute.source.as_ref());
7708        return Err(SchemaError::structural(
7709            "no-xsi",
7710            format!(
7711                "Attribute declaration '{}' has target namespace \
7712                 'http://www.w3.org/2001/XMLSchema-instance', which is \
7713                 reserved (no-xsi, §3.2.6.4)",
7714                attr_name
7715            ),
7716            location,
7717        ));
7718    }
7719    Ok(())
7720}
7721
7722/// Validate src-element §3.3.3 clause 4.3 / src-attribute §3.2.3 clause 6.3:
7723/// a local `<element>` or `<attribute>` declaring an explicit
7724/// `targetNamespace` attribute that differs from the schema's own
7725/// `targetNamespace` is permitted only when there is a `<restriction>`
7726/// ancestor (between the local declaration and its nearest `<complexType>`
7727/// ancestor) whose base does not match `xs:anyType`.
7728///
7729/// Implementation: per complex type, treat the declaration's "nearest
7730/// `<complexType>` ancestor" as this complex type. The clause is satisfied
7731/// iff the type's `derivation_method` is `Restriction` and its
7732/// `resolved_base_type` is not `xs:anyType`. In every other case (extension,
7733/// no derivation, or restriction of `xs:anyType`), any local element /
7734/// attribute carrying a divergent `targetNamespace` is invalid.
7735///
7736/// Closes saxon `target002` (element case) and `target004` (attribute case);
7737/// `target001`/`target003` (the matching `valid` cases) keep passing because
7738/// they use `restriction` of a non-`anyType` base.
7739pub fn validate_local_decl_target_namespace(schema_set: &SchemaSet) -> SchemaResult<()> {
7740    use crate::parser::frames::{ComplexContentResult, ParticleResult, ParticleTerm};
7741
7742    fn find_divergent_local_element<'a>(
7743        schema_set: &'a SchemaSet,
7744        particle: &'a ParticleResult,
7745        schema_tns: Option<NameId>,
7746        depth: usize,
7747    ) -> Option<(Option<SourceRef>, String)> {
7748        if depth > 100 {
7749            return None;
7750        }
7751        match &particle.term {
7752            ParticleTerm::Element(elem) => {
7753                if elem.ref_name.is_some() {
7754                    return None;
7755                }
7756                if let Some(ns) = elem.target_namespace {
7757                    if Some(ns) != schema_tns {
7758                        let name_str = elem
7759                            .name
7760                            .map(|n| schema_set.name_table.resolve_ref(n).to_string())
7761                            .unwrap_or_default();
7762                        return Some((elem.source.clone(), name_str));
7763                    }
7764                }
7765            }
7766            ParticleTerm::Group(group) => {
7767                // Only descend into inline groups (no ref_name); a group ref
7768                // points at a top-level group whose own decls are validated
7769                // independently when their containing context is examined.
7770                if group.ref_name.is_none() {
7771                    for child in &group.particles {
7772                        if let Some(found) =
7773                            find_divergent_local_element(schema_set, child, schema_tns, depth + 1)
7774                        {
7775                            return Some(found);
7776                        }
7777                    }
7778                }
7779            }
7780            ParticleTerm::Any(_) => {}
7781        }
7782        None
7783    }
7784
7785    for (_, ct) in schema_set.arenas.complex_types.iter() {
7786        let schema_tns = ct.target_namespace;
7787        let restriction_of_non_any = match (ct.derivation_method, ct.resolved_base_type) {
7788            (Some(crate::parser::frames::DerivationMethod::Restriction), Some(base_key)) => {
7789                !schema_set.is_any_type(base_key)
7790            }
7791            _ => false,
7792        };
7793        if restriction_of_non_any {
7794            continue;
7795        }
7796
7797        // Walk content particles for local elements with divergent
7798        // targetNamespace.
7799        let particle_opt = match &ct.content {
7800            ComplexContentResult::Complex(def) => def.particle.as_ref(),
7801            _ => None,
7802        };
7803        if let Some(particle) = particle_opt {
7804            if let Some((src, name)) =
7805                find_divergent_local_element(schema_set, particle, schema_tns, 0)
7806            {
7807                let location = schema_set
7808                    .locate(src.as_ref())
7809                    .or_else(|| schema_set.locate(ct.source.as_ref()));
7810                return Err(SchemaError::structural(
7811                    "src-element",
7812                    format!(
7813                        "Local element '{}' has an explicit targetNamespace differing from the \
7814                         schema's, but is not inside a <restriction> of a non-anyType base \
7815                         (src-element §3.3.3 clause 4.3)",
7816                        name
7817                    ),
7818                    location,
7819                ));
7820            }
7821        }
7822
7823        // Check direct attribute uses for divergent targetNamespace.
7824        for au in &ct.attributes {
7825            let attr = &au.attribute;
7826            if attr.ref_name.is_some() {
7827                continue;
7828            }
7829            let Some(ns) = attr.target_namespace else {
7830                continue;
7831            };
7832            if Some(ns) == schema_tns {
7833                continue;
7834            }
7835            let name = attr
7836                .name
7837                .map(|n| schema_set.name_table.resolve_ref(n).to_string())
7838                .unwrap_or_default();
7839            let location = schema_set
7840                .locate(attr.source.as_ref())
7841                .or_else(|| schema_set.locate(ct.source.as_ref()));
7842            return Err(SchemaError::structural(
7843                "src-attribute",
7844                format!(
7845                    "Local attribute '{}' has an explicit targetNamespace differing from the \
7846                     schema's, but is not inside a <restriction> of a non-anyType base \
7847                     (src-attribute §3.2.3 clause 6.3)",
7848                    name
7849                ),
7850                location,
7851            ));
7852        }
7853    }
7854    Ok(())
7855}
7856
7857/// Validate cos-element-consistent (§3.8.6.3) for the substitution-group
7858/// case: a content model that contains both a local element with QName Q
7859/// AND an element ref whose substitution-group expansion includes another
7860/// declaration with the same QName Q must agree on `{type definition}`.
7861///
7862/// The base XSD 1.1 EDC machinery (`validate_local_element_type_table_*`)
7863/// only compares type *tables*; this pass also covers the type-definition
7864/// rule that makes saxon `subsgroup901.bad.xsd` invalid (a CT containing
7865/// local `n: xs:date` plus `<xs:element ref="appendixContent">`, where the
7866/// global `n: xs:string` substitutes for `appendixContent`).
7867///
7868/// Active for both XSD 1.0 and 1.1.
7869pub fn validate_substitution_group_element_consistency(schema_set: &SchemaSet) -> SchemaResult<()> {
7870    use crate::parser::frames::{ComplexContentResult, ParticleResult, ParticleTerm};
7871    use std::collections::HashMap;
7872
7873    type Entry = (TypeKey, Option<SourceRef>);
7874
7875    // head → direct substitution members. Built once so the per-ref expansion
7876    // is O(direct members) instead of an O(elements) arena scan per ref.
7877    let mut subst_index: HashMap<ElementKey, Vec<ElementKey>> = HashMap::new();
7878    for (mk, m) in schema_set.arenas.elements.iter() {
7879        for &head in &m.resolved_substitution_groups {
7880            subst_index.entry(head).or_default().push(mk);
7881        }
7882    }
7883
7884    #[allow(clippy::too_many_arguments)]
7885    fn walk_particle(
7886        schema_set: &SchemaSet,
7887        particle: &ParticleResult,
7888        target_ns: Option<NameId>,
7889        local_keys: &[Option<ElementKey>],
7890        flat_idx: &mut usize,
7891        subst_index: &HashMap<ElementKey, Vec<ElementKey>>,
7892        out: &mut HashMap<(Option<NameId>, NameId), Vec<Entry>>,
7893        depth: usize,
7894    ) {
7895        if depth > 100 {
7896            return;
7897        }
7898        match &particle.term {
7899            ParticleTerm::Element(elem) => {
7900                if let Some(ref_qn) = &elem.ref_name {
7901                    *flat_idx += 1;
7902                    // The head itself contributes only when non-abstract;
7903                    // otherwise only its substitution members can appear.
7904                    let Some(head_key) =
7905                        schema_set.lookup_element(ref_qn.namespace, ref_qn.local_name)
7906                    else {
7907                        return;
7908                    };
7909                    let mut visited: std::collections::HashSet<ElementKey> =
7910                        std::collections::HashSet::new();
7911                    let mut stack = vec![head_key];
7912                    while let Some(current) = stack.pop() {
7913                        if !visited.insert(current) {
7914                            continue;
7915                        }
7916                        let Some(decl) = schema_set.arenas.elements.get(current) else {
7917                            continue;
7918                        };
7919                        // Per XSD 1.1 (W3C Bugzilla 4337), abstract members participate
7920                        // in the substitution group for cos-element-consistent purposes;
7921                        // XSD 1.0 excludes them.
7922                        if !decl.is_abstract || schema_set.is_xsd11() {
7923                            if let (Some(name), Some(t)) = (decl.name, decl.resolved_type) {
7924                                out.entry((decl.target_namespace, name))
7925                                    .or_default()
7926                                    .push((t, particle.source.clone()));
7927                            }
7928                        }
7929                        if let Some(members) = subst_index.get(&current) {
7930                            stack.extend(members.iter().copied());
7931                        }
7932                    }
7933                } else {
7934                    let idx = *flat_idx;
7935                    *flat_idx += 1;
7936                    if let Some(Some(elem_key)) = local_keys.get(idx) {
7937                        if let Some(decl) = schema_set.arenas.elements.get(*elem_key) {
7938                            if let (Some(name), Some(t)) = (decl.name, decl.resolved_type) {
7939                                // Arena's `target_namespace` is already the effective
7940                                // namespace (form + elementFormDefault applied during
7941                                // allocate_content_particle_elements), so we use it
7942                                // directly instead of falling back to the outer CT's
7943                                // target_ns.
7944                                let ns = decl.target_namespace;
7945                                out.entry((ns, name))
7946                                    .or_default()
7947                                    .push((t, decl.source.clone()));
7948                            }
7949                        }
7950                    }
7951                }
7952            }
7953            ParticleTerm::Group(group) => {
7954                if let Some(ref_qn) = &group.ref_name {
7955                    // Group refs don't advance the outer flat_idx; the
7956                    // model-group arena owns its own resolved_particle_elements.
7957                    if let Some(group_key) =
7958                        schema_set.lookup_model_group(ref_qn.namespace, ref_qn.local_name)
7959                    {
7960                        let mg = &schema_set.arenas.model_groups[group_key];
7961                        let inner_ns = mg.target_namespace.or(target_ns);
7962                        let mut inner_idx = 0usize;
7963                        for child in &mg.particles {
7964                            walk_particle(
7965                                schema_set,
7966                                child,
7967                                inner_ns,
7968                                &mg.resolved_particle_elements,
7969                                &mut inner_idx,
7970                                subst_index,
7971                                out,
7972                                depth + 1,
7973                            );
7974                        }
7975                    }
7976                } else {
7977                    for child in &group.particles {
7978                        walk_particle(
7979                            schema_set,
7980                            child,
7981                            target_ns,
7982                            local_keys,
7983                            flat_idx,
7984                            subst_index,
7985                            out,
7986                            depth + 1,
7987                        );
7988                    }
7989                }
7990            }
7991            ParticleTerm::Any(_) => {}
7992        }
7993    }
7994
7995    for (_key, ct) in schema_set.arenas.complex_types.iter() {
7996        let ComplexContentResult::Complex(cc) = &ct.content else {
7997            continue;
7998        };
7999        let Some(particle) = cc.particle.as_ref() else {
8000            continue;
8001        };
8002        let mut entries: HashMap<(Option<NameId>, NameId), Vec<Entry>> = HashMap::new();
8003        let mut flat_idx = 0usize;
8004        walk_particle(
8005            schema_set,
8006            particle,
8007            ct.target_namespace,
8008            &ct.resolved_content_particle_elements,
8009            &mut flat_idx,
8010            &subst_index,
8011            &mut entries,
8012            0,
8013        );
8014
8015        for ((ns, name), list) in &entries {
8016            if list.len() < 2 {
8017                continue;
8018            }
8019            let first_type = list[0].0;
8020            for (other_type, other_src) in &list[1..] {
8021                if *other_type == first_type {
8022                    continue;
8023                }
8024                let qn_str = format_type_name(schema_set, Some(*name), *ns);
8025                let location = schema_set
8026                    .locate(other_src.as_ref())
8027                    .or_else(|| schema_set.locate(list[0].1.as_ref()))
8028                    .or_else(|| schema_set.locate(ct.source.as_ref()));
8029                return Err(SchemaError::structural(
8030                    "cos-element-consistent",
8031                    format!(
8032                        "Element declarations for '{}' in the same content model \
8033                         (counting substitution-group expansion) have different \
8034                         {{type definition}}s (§3.8.6.3 / cos-element-consistent)",
8035                        qn_str
8036                    ),
8037                    location,
8038                ));
8039            }
8040        }
8041    }
8042    Ok(())
8043}
8044
8045// ---------------------------------------------------------------------------
8046// §src-redefine 6.2.2 / 7.2.2 — deferred restriction validation for redefines
8047// ---------------------------------------------------------------------------
8048//
8049// When an `<xs:redefine>` child group (or attribute group) has zero
8050// self-references, §src-redefine clauses 6.2.2 / 7.2.2 require that the
8051// redefined component be a *valid restriction* of the original. Composition
8052// (`schema/redefine.rs`) validates the self-reference shape (clauses 6.1 /
8053// 7.1) and flags zero-self-ref redefines via
8054// `redefine_requires_restriction_check`; this module does the deferred
8055// restriction check after reference resolution is complete.
8056//
8057// Spec anchors (source of truth: `structures.html` — W3C XSD 1.1 §src-redefine
8058// and §3.4.6.3 Derivation Valid (Restriction, Complex)):
8059//   - §src-redefine 6.2.2: redefined model group must be a valid restriction
8060//     of the original per §3.9.6 Particle Valid (Restriction).
8061//   - §src-redefine 7.2.2: redefined attribute group must satisfy clause 3
8062//     of §3.4.6.3 (clause-3 only, NOT clause-4 local-type-substitution).
8063//   - §3.8: pointless particles (`maxOccurs=0`) are eliminated before the
8064//     restriction check.
8065//   - §3.2.2: a prohibited `<xs:attribute>` is NOT an attribute use.
8066//
8067// Scope limitations:
8068//   - Chained redefines (`orig → v1 → v2`) resolve nested `group-ref`s via
8069//     the currently bound namespace version; see
8070//     `normalize_model_group_as_particle` doc comment.
8071
8072/// Flatten an attribute group's effective attribute uses, filtering out
8073/// prohibited uses per §3.2.2 (a prohibited `<xs:attribute>` is not an
8074/// attribute use on either side of a restriction comparison).
8075fn collect_flat_attribute_uses_for_group(
8076    schema_set: &SchemaSet,
8077    ag_key: AttributeGroupKey,
8078) -> Vec<EffectiveAttributeUse> {
8079    let mut result = Vec::new();
8080    collect_attribute_group_uses(schema_set, ag_key, &mut result, 0);
8081    result.retain(|eau| eau.use_kind != AttributeUseKind::Prohibited);
8082    result
8083}
8084
8085/// Construct a §src-redefine 6.2.2 structural error for a model group whose
8086/// restriction of its original cannot be validated.
8087fn make_redefine_group_restriction_error(
8088    schema_set: &SchemaSet,
8089    derived: &crate::arenas::ModelGroupData,
8090    detail: &str,
8091) -> SchemaError {
8092    let name = format_type_name(schema_set, derived.name, derived.target_namespace);
8093    let location = derived
8094        .source
8095        .as_ref()
8096        .and_then(|s| schema_set.source_maps.locate(s));
8097    SchemaError::structural(
8098        "src-redefine.6.2.2",
8099        format!(
8100            "Redefined group '{}' must be a valid restriction of the original \
8101             (§src-redefine 6.2.2): {}",
8102            name, detail,
8103        ),
8104        location,
8105    )
8106}
8107
8108/// Construct a §src-redefine 7.2.2 structural error for an attribute group
8109/// whose restriction of its original cannot be validated.
8110fn make_redefine_attr_group_restriction_error(
8111    schema_set: &SchemaSet,
8112    derived: &crate::arenas::AttributeGroupData,
8113    detail: &str,
8114) -> SchemaError {
8115    let name = format_type_name(schema_set, derived.name, derived.target_namespace);
8116    let location = derived
8117        .source
8118        .as_ref()
8119        .and_then(|s| schema_set.source_maps.locate(s));
8120    SchemaError::structural(
8121        "src-redefine.7.2.2",
8122        format!(
8123            "Redefined attribute group '{}' must be a valid restriction of the original \
8124             (§src-redefine 7.2.2): {}",
8125            name, detail,
8126        ),
8127        location,
8128    )
8129}
8130
8131/// Driver for §src-redefine 6.2.2: for each model group flagged as a
8132/// zero-self-reference redefine, verify its normalized particle is a valid
8133/// restriction of the original's normalized particle per §3.9.6 Particle
8134/// Valid (Restriction).
8135fn validate_all_redefine_group_restrictions(
8136    schema_set: &SchemaSet,
8137    errors: &mut Vec<SchemaError>,
8138    stats: &mut DerivationStats,
8139) {
8140    for (_key, derived) in schema_set.arenas.model_groups.iter() {
8141        if !derived.redefine_requires_restriction_check {
8142            continue;
8143        }
8144        let Some(original_key) = derived.redefine_original else {
8145            continue;
8146        };
8147        let Some(original) = schema_set.arenas.model_groups.get(original_key) else {
8148            continue;
8149        };
8150
8151        let derived_particle = match normalize_model_group_as_particle(schema_set, derived) {
8152            Ok(p) => p,
8153            Err(e) => {
8154                errors.push(e);
8155                stats.errors += 1;
8156                continue;
8157            }
8158        };
8159        let base_particle = match normalize_model_group_as_particle(schema_set, original) {
8160            Ok(p) => p,
8161            Err(e) => {
8162                errors.push(e);
8163                stats.errors += 1;
8164                continue;
8165            }
8166        };
8167
8168        // Empty-group special case (§3.8 + §3.9.6): when the derived group
8169        // normalizes to empty content after pointless-particle removal
8170        // (e.g. its only child had `maxOccurs=0`), `particle_restricts` does
8171        // not model the "empty content" case correctly — it would reject
8172        // legal restrictions whenever the base does not normalize to the
8173        // exact same surviving shape. Mirror the existing short-circuit in
8174        // `validate_content_particle_restriction` (derivation.rs:1148-1161):
8175        // empty derived is a valid restriction iff the base is emptiable.
8176        if is_effectively_empty(&derived_particle) {
8177            if !particle_is_emptiable(&base_particle) {
8178                errors.push(make_redefine_group_restriction_error(
8179                    schema_set,
8180                    derived,
8181                    "removes required content model of the original group",
8182                ));
8183                stats.errors += 1;
8184            }
8185            continue;
8186        }
8187
8188        if !particle_restricts(schema_set, &derived_particle, &base_particle) {
8189            errors.push(make_redefine_group_restriction_error(
8190                schema_set,
8191                derived,
8192                "content model is not a valid restriction of the original group",
8193            ));
8194            stats.errors += 1;
8195        }
8196    }
8197}
8198
8199/// Driver for §src-redefine 7.2.2: implementation of §3.4.6.3 clause 3
8200/// (derivation-ok-restriction, attribute side) applied to redefined
8201/// attribute groups. Checks:
8202///  - every derived attribute is present in the base (direct match) or
8203///    admitted by the base's effective attribute wildcard (§3.6.2.2);
8204///  - clause 3(b) type tightening on directly-matched pairs;
8205///  - required-stays-required;
8206///  - wildcard-vs-wildcard subset: the derived group's effective
8207///    attribute wildcard must be a valid restriction of the original's.
8208fn validate_all_redefine_attribute_group_restrictions(
8209    schema_set: &SchemaSet,
8210    errors: &mut Vec<SchemaError>,
8211    stats: &mut DerivationStats,
8212) {
8213    for (_key, derived) in schema_set.arenas.attribute_groups.iter() {
8214        if !derived.redefine_requires_restriction_check {
8215            continue;
8216        }
8217        let Some(original_key) = derived.redefine_original else {
8218            continue;
8219        };
8220        let Some(original) = schema_set.arenas.attribute_groups.get(original_key) else {
8221            continue;
8222        };
8223
8224        // Flatten both sides, filtering out Prohibited uses (§3.2.2).
8225        // The derived key is the key we're iterating on — look it up to
8226        // get an `AttributeGroupKey` for `collect_flat_attribute_uses_for_group`.
8227        let derived_attrs = collect_flat_attribute_uses_for_group(schema_set, _key);
8228        let base_attrs = collect_flat_attribute_uses_for_group(schema_set, original_key);
8229
8230        // Compute the base's effective attribute wildcard per §3.6.2.2
8231        // once, outside the per-attribute loop. This is the full
8232        // intersection across the original group's local wildcard and
8233        // the wildcards of every referenced nested attribute group.
8234        let base_effective_wc = match effective_attribute_wildcard(
8235            schema_set,
8236            original.attribute_wildcard.as_ref(),
8237            original.target_namespace,
8238            &original.resolved_attribute_groups,
8239        ) {
8240            Ok(eff) => eff,
8241            Err(e) => {
8242                errors.push(e);
8243                stats.errors += 1;
8244                continue;
8245            }
8246        };
8247
8248        // Subset check (clause 3, first half): every derived attribute must
8249        // be valid in the base, either directly by (namespace, name) match
8250        // or via the base's effective {attribute wildcard}. Also applies the
8251        // clause 3(b) type-subsumption check for directly-matched pairs.
8252        let mut failed = false;
8253        for da in &derived_attrs {
8254            // (a) Direct match by (namespace, name).
8255            if let Some(ba) = base_attrs
8256                .iter()
8257                .find(|b| b.name == da.name && b.target_namespace == da.target_namespace)
8258            {
8259                // Type tightening (clause 3(b)): derived type must equal or
8260                // be derived from base type when both are resolved.
8261                if let (Some(dt), Some(bt)) = (da.resolved_type, ba.resolved_type) {
8262                    if dt != bt && !is_type_derived_from(schema_set, dt, bt) {
8263                        let attr_name_str = schema_set.name_table.resolve(da.name).to_string();
8264                        errors.push(make_redefine_attr_group_restriction_error(
8265                            schema_set,
8266                            derived,
8267                            &format!(
8268                                "attribute '{}' has a type that is not validly derived from the \
8269                                 base attribute type",
8270                                attr_name_str,
8271                            ),
8272                        ));
8273                        stats.errors += 1;
8274                        failed = true;
8275                        break;
8276                    }
8277                }
8278                // Fixed-value tightening (clause 3, derivation-ok-restriction
8279                // §3.4.6.3 attribute side): if the base attribute use has
8280                // {value constraint} = (fixed, V), the derived attribute use
8281                // must also have {value constraint} = (fixed, V). It cannot
8282                // be relaxed to (default, V), nor removed entirely. The W3C
8283                // `schM10` fixture exercises the fixed→default relaxation.
8284                if let Some(ref base_fixed) = ba.fixed_value {
8285                    let derived_matches =
8286                        da.fixed_value.as_ref().is_some_and(|dv| dv == base_fixed);
8287                    if !derived_matches {
8288                        let attr_name_str = schema_set.name_table.resolve(da.name).to_string();
8289                        errors.push(make_redefine_attr_group_restriction_error(
8290                            schema_set,
8291                            derived,
8292                            &format!(
8293                                "attribute '{}' relaxes or removes the base 'fixed=\"{}\"' \
8294                                 value constraint",
8295                                attr_name_str, base_fixed,
8296                            ),
8297                        ));
8298                        stats.errors += 1;
8299                        failed = true;
8300                        break;
8301                    }
8302                }
8303                continue;
8304            }
8305            // (b) Admitted by the base's *effective* {attribute wildcard}
8306            // (§3.6.2.2), not just the original group's local wildcard.
8307            if let Some(ref bwc) = base_effective_wc {
8308                if effective_wildcard_allows_attribute(
8309                    schema_set,
8310                    bwc,
8311                    da.target_namespace,
8312                    da.name,
8313                ) {
8314                    continue;
8315                }
8316            }
8317            // Neither (a) nor (b) holds — not a valid restriction.
8318            let attr_name_str = schema_set.name_table.resolve(da.name).to_string();
8319            errors.push(make_redefine_attr_group_restriction_error(
8320                schema_set,
8321                derived,
8322                &format!(
8323                    "attribute '{}' is not present in the original and is not admitted by \
8324                     the original's attribute wildcard",
8325                    attr_name_str,
8326                ),
8327            ));
8328            stats.errors += 1;
8329            failed = true;
8330            break;
8331        }
8332        if failed {
8333            continue;
8334        }
8335
8336        // Required-stays-required (clause 3(a)): every base Required attribute
8337        // must also be Required in the derived side.
8338        let mut req_failed = false;
8339        for ba in &base_attrs {
8340            if ba.use_kind != AttributeUseKind::Required {
8341                continue;
8342            }
8343            let matching = derived_attrs
8344                .iter()
8345                .find(|d| d.name == ba.name && d.target_namespace == ba.target_namespace);
8346            match matching {
8347                Some(da) if da.use_kind == AttributeUseKind::Required => {}
8348                _ => {
8349                    let attr_name_str = schema_set.name_table.resolve(ba.name).to_string();
8350                    errors.push(make_redefine_attr_group_restriction_error(
8351                        schema_set,
8352                        derived,
8353                        &format!(
8354                            "base attribute '{}' is required but the redefined group does not \
8355                             declare it as required",
8356                            attr_name_str,
8357                        ),
8358                    ));
8359                    stats.errors += 1;
8360                    req_failed = true;
8361                    break;
8362                }
8363            }
8364        }
8365        if req_failed {
8366            continue;
8367        }
8368
8369        // Wildcard-vs-wildcard subset (clause 3, second half of §3.6.2.2):
8370        // the derived group's effective attribute wildcard must be a
8371        // valid restriction of the original's. This catches cases where
8372        // the redefined group broadens an inherited wildcard even when
8373        // every directly-named attribute already checks out.
8374        let derived_effective_wc = match effective_attribute_wildcard(
8375            schema_set,
8376            derived.attribute_wildcard.as_ref(),
8377            derived.target_namespace,
8378            &derived.resolved_attribute_groups,
8379        ) {
8380            Ok(eff) => eff,
8381            Err(e) => {
8382                errors.push(e);
8383                stats.errors += 1;
8384                continue;
8385            }
8386        };
8387        match classify_attribute_wildcard_restriction(
8388            schema_set,
8389            derived_effective_wc.as_ref(),
8390            base_effective_wc.as_ref(),
8391        ) {
8392            WildcardRestrictionOutcome::DerivedAbsent | WildcardRestrictionOutcome::Valid => {}
8393            WildcardRestrictionOutcome::AddedInDerived => {
8394                errors.push(make_redefine_attr_group_restriction_error(
8395                    schema_set,
8396                    derived,
8397                    "redefined attribute group declares an attribute wildcard but \
8398                     the original has none",
8399                ));
8400                stats.errors += 1;
8401            }
8402            WildcardRestrictionOutcome::NotSubset(reason) => {
8403                errors.push(make_redefine_attr_group_restriction_error(
8404                    schema_set,
8405                    derived,
8406                    &format!(
8407                        "attribute wildcard is not a valid restriction of the \
8408                         original: {}",
8409                        reason,
8410                    ),
8411                ));
8412                stats.errors += 1;
8413            }
8414        }
8415    }
8416}
8417
8418#[cfg(test)]
8419mod tests {
8420    use super::*;
8421    use crate::arenas::{ComplexTypeDefData, SimpleTypeDefData};
8422    use crate::parser::frames::ComplexContentResult;
8423    use crate::schema::model::DerivationSet;
8424
8425    fn create_simple_type_data(
8426        name: Option<NameId>,
8427        variety: SimpleTypeVariety,
8428    ) -> SimpleTypeDefData {
8429        SimpleTypeDefData {
8430            name,
8431            target_namespace: None,
8432            variety,
8433            base_type: None,
8434            item_type: None,
8435            member_types: Vec::new(),
8436            facets: FacetSet::new(),
8437            final_derivation: DerivationSet::empty(),
8438            id: None,
8439            derivation_id: None,
8440            annotation: None,
8441            source: None,
8442            resolved_base_type: None,
8443            resolved_item_type: None,
8444            resolved_member_types: Vec::new(),
8445            redefine_original: None,
8446            deferred_item_type_error: None,
8447        }
8448    }
8449
8450    fn create_complex_type_data(name: Option<NameId>) -> ComplexTypeDefData {
8451        ComplexTypeDefData {
8452            name,
8453            target_namespace: None,
8454            base_type: None,
8455            derivation_method: None,
8456            content: ComplexContentResult::Empty,
8457            open_content: None,
8458            attributes: Vec::new(),
8459            attribute_groups: Vec::new(),
8460            attribute_wildcard: None,
8461            mixed: false,
8462            is_abstract: false,
8463            final_derivation: DerivationSet::empty(),
8464            block: DerivationSet::empty(),
8465            default_attributes_apply: true,
8466            id: None,
8467            #[cfg(feature = "xsd11")]
8468            assertions: Vec::new(),
8469            #[cfg(feature = "xsd11")]
8470            xpath_default_namespace: None,
8471            annotation: None,
8472            source: None,
8473            resolved_base_type: None,
8474            resolved_attribute_groups: Vec::new(),
8475            resolved_attributes: Vec::new(),
8476            resolved_content_particle_types: Vec::new(),
8477            resolved_content_particle_elements: Vec::new(),
8478            resolved_simple_content_type: None,
8479            redefine_original: None,
8480        }
8481    }
8482
8483    #[test]
8484    fn test_derivation_stats_default() {
8485        let stats = DerivationStats::default();
8486        assert_eq!(stats.simple_types_validated, 0);
8487        assert_eq!(stats.complex_types_validated, 0);
8488        assert_eq!(stats.errors, 0);
8489    }
8490
8491    #[test]
8492    fn test_validate_empty_schema() {
8493        let schema_set = SchemaSet::new();
8494        let dep_graph = DependencyGraph::new();
8495
8496        let result = validate_all_derivations(&schema_set, &dep_graph);
8497        assert!(result.is_ok());
8498
8499        let stats = result.unwrap();
8500        assert_eq!(stats.simple_types_validated, 0);
8501        assert_eq!(stats.complex_types_validated, 0);
8502    }
8503
8504    #[test]
8505    fn test_validate_atomic_type_no_base() {
8506        let mut schema_set = SchemaSet::new();
8507        let type_data = create_simple_type_data(None, SimpleTypeVariety::Atomic);
8508        let key = schema_set.arenas.alloc_simple_type(type_data);
8509
8510        let mut stats = DerivationStats::default();
8511        let result = validate_simple_type(&schema_set, key, &mut stats);
8512
8513        assert!(result.is_ok());
8514        assert_eq!(stats.simple_types_validated, 1);
8515    }
8516
8517    #[test]
8518    fn test_validate_list_type_no_item() {
8519        let mut schema_set = SchemaSet::new();
8520        let type_data = create_simple_type_data(None, SimpleTypeVariety::List);
8521        let key = schema_set.arenas.alloc_simple_type(type_data);
8522
8523        let mut stats = DerivationStats::default();
8524        let result = validate_simple_type(&schema_set, key, &mut stats);
8525
8526        assert!(result.is_ok());
8527        assert_eq!(stats.list_types_validated, 1);
8528    }
8529
8530    #[test]
8531    fn test_validate_union_type_no_members() {
8532        let mut schema_set = SchemaSet::new();
8533        let type_data = create_simple_type_data(None, SimpleTypeVariety::Union);
8534        let key = schema_set.arenas.alloc_simple_type(type_data);
8535
8536        let mut stats = DerivationStats::default();
8537        let result = validate_simple_type(&schema_set, key, &mut stats);
8538
8539        assert!(result.is_ok());
8540        assert_eq!(stats.union_types_validated, 1);
8541    }
8542
8543    #[test]
8544    fn test_validate_list_of_atomic() {
8545        let mut schema_set = SchemaSet::new();
8546
8547        // Create an atomic item type
8548        let item_type_data = create_simple_type_data(None, SimpleTypeVariety::Atomic);
8549        let item_key = schema_set.arenas.alloc_simple_type(item_type_data);
8550
8551        // Create a list type with atomic item type
8552        let mut list_type_data = create_simple_type_data(None, SimpleTypeVariety::List);
8553        list_type_data.resolved_item_type = Some(TypeKey::Simple(item_key));
8554        let list_key = schema_set.arenas.alloc_simple_type(list_type_data);
8555
8556        let mut stats = DerivationStats::default();
8557        let result = validate_simple_type(&schema_set, list_key, &mut stats);
8558
8559        assert!(result.is_ok());
8560    }
8561
8562    #[test]
8563    fn test_validate_list_of_list_error() {
8564        let mut schema_set = SchemaSet::new();
8565
8566        // Create a list item type (invalid)
8567        let inner_list_data = create_simple_type_data(None, SimpleTypeVariety::List);
8568        let inner_key = schema_set.arenas.alloc_simple_type(inner_list_data);
8569
8570        // Create a list type with list item type (should fail)
8571        let mut outer_list_data = create_simple_type_data(None, SimpleTypeVariety::List);
8572        outer_list_data.resolved_item_type = Some(TypeKey::Simple(inner_key));
8573        let outer_key = schema_set.arenas.alloc_simple_type(outer_list_data);
8574
8575        let mut stats = DerivationStats::default();
8576        let result = validate_simple_type(&schema_set, outer_key, &mut stats);
8577
8578        assert!(result.is_err());
8579        if let Err(SchemaError::StructuralError { constraint, .. }) = result {
8580            assert_eq!(constraint, "cos-list-of-atomic");
8581        } else {
8582            panic!("Expected structural error with cos-list-of-atomic constraint");
8583        }
8584    }
8585
8586    #[test]
8587    fn test_validate_union_with_complex_member_error() {
8588        let mut schema_set = SchemaSet::new();
8589
8590        // Create a complex type (invalid for union member)
8591        let complex_data = create_complex_type_data(None);
8592        let complex_key = schema_set.arenas.alloc_complex_type(complex_data);
8593
8594        // Create a union type with complex member (should fail)
8595        let mut union_data = create_simple_type_data(None, SimpleTypeVariety::Union);
8596        union_data.resolved_member_types = vec![TypeKey::Complex(complex_key)];
8597        let union_key = schema_set.arenas.alloc_simple_type(union_data);
8598
8599        let mut stats = DerivationStats::default();
8600        let result = validate_simple_type(&schema_set, union_key, &mut stats);
8601
8602        assert!(result.is_err());
8603        if let Err(SchemaError::StructuralError { constraint, .. }) = result {
8604            assert_eq!(constraint, "cos-union-memberTypes");
8605        } else {
8606            panic!("Expected structural error with cos-union-memberTypes constraint");
8607        }
8608    }
8609
8610    #[test]
8611    fn test_validate_complex_type_no_base() {
8612        let mut schema_set = SchemaSet::new();
8613        let type_data = create_complex_type_data(None);
8614        let key = schema_set.arenas.alloc_complex_type(type_data);
8615
8616        let mut stats = DerivationStats::default();
8617        let result = validate_complex_type(&schema_set, key, &mut stats);
8618
8619        assert!(result.is_ok());
8620        assert_eq!(stats.complex_types_validated, 1);
8621    }
8622
8623    #[test]
8624    fn test_validate_complex_extension() {
8625        let mut schema_set = SchemaSet::new();
8626
8627        // Create base complex type
8628        let base_data = create_complex_type_data(None);
8629        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8630
8631        // Create derived type with extension
8632        let mut derived_data = create_complex_type_data(None);
8633        derived_data.derivation_method = Some(DerivationMethod::Extension);
8634        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
8635        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
8636
8637        let mut stats = DerivationStats::default();
8638        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
8639
8640        assert!(result.is_ok());
8641        assert_eq!(stats.extensions_validated, 1);
8642    }
8643
8644    #[test]
8645    fn test_validate_complex_restriction() {
8646        let mut schema_set = SchemaSet::new();
8647
8648        // Create base complex type
8649        let base_data = create_complex_type_data(None);
8650        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8651
8652        // Create derived type with restriction
8653        let mut derived_data = create_complex_type_data(None);
8654        derived_data.derivation_method = Some(DerivationMethod::Restriction);
8655        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
8656        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
8657
8658        let mut stats = DerivationStats::default();
8659        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
8660
8661        assert!(result.is_ok());
8662        assert_eq!(stats.restrictions_validated, 1);
8663    }
8664
8665    #[test]
8666    fn test_validate_extension_of_final_type_error() {
8667        let mut schema_set = SchemaSet::new();
8668
8669        // Create base complex type with final="extension"
8670        let mut base_data = create_complex_type_data(None);
8671        base_data.final_derivation = DerivationSet::extension();
8672        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8673
8674        // Create derived type with extension (should fail)
8675        let mut derived_data = create_complex_type_data(None);
8676        derived_data.derivation_method = Some(DerivationMethod::Extension);
8677        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
8678        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
8679
8680        let mut stats = DerivationStats::default();
8681        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
8682
8683        assert!(result.is_err());
8684        if let Err(SchemaError::StructuralError { constraint, .. }) = result {
8685            assert_eq!(constraint, "cos-ct-extends");
8686        } else {
8687            panic!("Expected structural error with cos-ct-extends constraint");
8688        }
8689    }
8690
8691    #[test]
8692    fn test_validate_extension_of_final_default_type_error() {
8693        // Assembly would apply finalDefault to types without an explicit final.
8694        // This test simulates that: base.final_derivation = extension (inherited from finalDefault).
8695        let mut schema_set = SchemaSet::new();
8696
8697        let mut base_data = create_complex_type_data(None);
8698        base_data.final_derivation = DerivationSet::extension();
8699        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8700
8701        // Create derived type with extension (should fail).
8702        let mut derived_data = create_complex_type_data(None);
8703        derived_data.derivation_method = Some(DerivationMethod::Extension);
8704        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
8705        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
8706
8707        let mut stats = DerivationStats::default();
8708        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
8709
8710        assert!(result.is_err());
8711        if let Err(SchemaError::StructuralError { constraint, .. }) = result {
8712            assert_eq!(constraint, "cos-ct-extends");
8713        } else {
8714            panic!("Expected structural error with cos-ct-extends constraint");
8715        }
8716    }
8717
8718    #[test]
8719    fn test_validate_restriction_of_final_type_error() {
8720        let mut schema_set = SchemaSet::new();
8721
8722        // Create base complex type with final="restriction"
8723        let mut base_data = create_complex_type_data(None);
8724        base_data.final_derivation = DerivationSet::restriction();
8725        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8726
8727        // Create derived type with restriction (should fail)
8728        let mut derived_data = create_complex_type_data(None);
8729        derived_data.derivation_method = Some(DerivationMethod::Restriction);
8730        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
8731        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
8732
8733        let mut stats = DerivationStats::default();
8734        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
8735
8736        assert!(result.is_err());
8737        if let Err(SchemaError::StructuralError { constraint, .. }) = result {
8738            assert_eq!(constraint, "derivation-ok-restriction");
8739        } else {
8740            panic!("Expected structural error with derivation-ok-restriction constraint");
8741        }
8742    }
8743
8744    #[test]
8745    fn test_format_type_name_anonymous() {
8746        let schema_set = SchemaSet::new();
8747        let name = format_type_name(&schema_set, None, None);
8748        assert_eq!(name, "(anonymous)");
8749    }
8750
8751    #[test]
8752    fn test_format_type_name_with_namespace() {
8753        let schema_set = SchemaSet::new();
8754        let name_id = schema_set.name_table.add("myType");
8755        let ns_id = schema_set.name_table.add("http://example.com");
8756        let name = format_type_name(&schema_set, Some(name_id), Some(ns_id));
8757        assert_eq!(name, "{http://example.com}myType");
8758    }
8759
8760    #[test]
8761    fn test_format_type_name_no_namespace() {
8762        let schema_set = SchemaSet::new();
8763        let name_id = schema_set.name_table.add("myType");
8764        let name = format_type_name(&schema_set, Some(name_id), None);
8765        assert_eq!(name, "myType");
8766    }
8767
8768    // ====================================================================
8769    // XSD 1.1: Open-content derivation tests
8770    // ====================================================================
8771
8772    #[cfg(feature = "xsd11")]
8773    fn make_open_content(
8774        mode: crate::parser::frames::OpenContentMode,
8775        namespace: crate::parser::frames::WildcardNamespace,
8776        pc: crate::parser::frames::ProcessContents,
8777    ) -> crate::parser::frames::OpenContentResult {
8778        crate::parser::frames::OpenContentResult {
8779            mode,
8780            wildcard: Some(crate::parser::frames::WildcardResult {
8781                namespace,
8782                process_contents: pc,
8783                not_namespace: Vec::new(),
8784                not_qname: Vec::new(),
8785                id: None,
8786                annotation: None,
8787                source: None,
8788            }),
8789            id: None,
8790            annotation: None,
8791            source: None,
8792        }
8793    }
8794
8795    #[cfg(feature = "xsd11")]
8796    #[test]
8797    fn test_extension_suffix_cannot_extend_interleave() {
8798        use crate::parser::frames::{OpenContentMode, ProcessContents, WildcardNamespace};
8799
8800        let mut schema_set = SchemaSet::new();
8801
8802        let mut base_data = create_complex_type_data(None);
8803        base_data.open_content = Some(make_open_content(
8804            OpenContentMode::Interleave,
8805            WildcardNamespace::Any,
8806            ProcessContents::Lax,
8807        ));
8808        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8809
8810        let mut derived_data = create_complex_type_data(None);
8811        derived_data.derivation_method = Some(DerivationMethod::Extension);
8812        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
8813        derived_data.open_content = Some(make_open_content(
8814            OpenContentMode::Suffix,
8815            WildcardNamespace::Any,
8816            ProcessContents::Lax,
8817        ));
8818        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
8819
8820        let mut stats = DerivationStats::default();
8821        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
8822
8823        assert!(result.is_err());
8824        if let Err(SchemaError::StructuralError { constraint, .. }) = result {
8825            assert_eq!(constraint, "cos-ct-extends");
8826        } else {
8827            panic!("Expected cos-ct-extends error");
8828        }
8829    }
8830
8831    #[cfg(feature = "xsd11")]
8832    #[test]
8833    fn test_extension_interleave_extends_interleave_valid() {
8834        use crate::parser::frames::{OpenContentMode, ProcessContents, WildcardNamespace};
8835
8836        let mut schema_set = SchemaSet::new();
8837
8838        let mut base_data = create_complex_type_data(None);
8839        base_data.open_content = Some(make_open_content(
8840            OpenContentMode::Interleave,
8841            WildcardNamespace::Any,
8842            ProcessContents::Lax,
8843        ));
8844        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8845
8846        let mut derived_data = create_complex_type_data(None);
8847        derived_data.derivation_method = Some(DerivationMethod::Extension);
8848        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
8849        derived_data.open_content = Some(make_open_content(
8850            OpenContentMode::Interleave,
8851            WildcardNamespace::Any,
8852            ProcessContents::Lax,
8853        ));
8854        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
8855
8856        let mut stats = DerivationStats::default();
8857        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
8858        assert!(result.is_ok());
8859    }
8860
8861    #[cfg(feature = "xsd11")]
8862    #[test]
8863    fn test_extension_base_has_oc_derived_has_none() {
8864        use crate::parser::frames::{OpenContentMode, ProcessContents, WildcardNamespace};
8865
8866        // Per §3.4.2.3 clause 6.1 and §3.4.6.2 clause 1.4.3.2.2:
8867        // when the derivation declares no <xs:openContent>, the effective
8868        // {open content} of the derived type (EOT) inherits the base's
8869        // (BOT).  That trivially satisfies clauses 1.4.3.2.2.3 and
8870        // 1.4.3.2.2.4, so extension is valid.  (saxonData/Open/open027.)
8871        let mut schema_set = SchemaSet::new();
8872
8873        let mut base_data = create_complex_type_data(None);
8874        base_data.open_content = Some(make_open_content(
8875            OpenContentMode::Interleave,
8876            WildcardNamespace::Any,
8877            ProcessContents::Lax,
8878        ));
8879        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8880
8881        let mut derived_data = create_complex_type_data(None);
8882        derived_data.derivation_method = Some(DerivationMethod::Extension);
8883        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
8884        // No open_content on derived — inherits from base per clause 6.1.
8885        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
8886
8887        let mut stats = DerivationStats::default();
8888        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
8889
8890        assert!(
8891            result.is_ok(),
8892            "derived inherits BOT per clause 6.1: {:?}",
8893            result
8894        );
8895    }
8896
8897    #[cfg(feature = "xsd11")]
8898    #[test]
8899    fn test_extension_base_no_oc_derived_adds_oc_valid() {
8900        use crate::parser::frames::{OpenContentMode, ProcessContents, WildcardNamespace};
8901
8902        let mut schema_set = SchemaSet::new();
8903
8904        // Base has no open content
8905        let base_data = create_complex_type_data(None);
8906        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8907
8908        let mut derived_data = create_complex_type_data(None);
8909        derived_data.derivation_method = Some(DerivationMethod::Extension);
8910        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
8911        derived_data.open_content = Some(make_open_content(
8912            OpenContentMode::Interleave,
8913            WildcardNamespace::Any,
8914            ProcessContents::Lax,
8915        ));
8916        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
8917
8918        let mut stats = DerivationStats::default();
8919        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
8920        assert!(result.is_ok());
8921    }
8922
8923    #[cfg(feature = "xsd11")]
8924    #[test]
8925    fn test_restriction_adds_oc_when_base_has_none() {
8926        use crate::parser::frames::{OpenContentMode, ProcessContents, WildcardNamespace};
8927
8928        let mut schema_set = SchemaSet::new();
8929
8930        // Base has no open content
8931        let base_data = create_complex_type_data(None);
8932        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8933
8934        let mut derived_data = create_complex_type_data(None);
8935        derived_data.derivation_method = Some(DerivationMethod::Restriction);
8936        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
8937        derived_data.open_content = Some(make_open_content(
8938            OpenContentMode::Interleave,
8939            WildcardNamespace::Any,
8940            ProcessContents::Lax,
8941        ));
8942        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
8943
8944        let mut stats = DerivationStats::default();
8945        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
8946
8947        assert!(result.is_err());
8948        if let Err(SchemaError::StructuralError { constraint, .. }) = result {
8949            assert_eq!(constraint, "derivation-ok-restriction");
8950        } else {
8951            panic!("Expected derivation-ok-restriction error");
8952        }
8953    }
8954
8955    #[cfg(feature = "xsd11")]
8956    #[test]
8957    fn test_restriction_removes_oc_valid() {
8958        use crate::parser::frames::{OpenContentMode, ProcessContents, WildcardNamespace};
8959
8960        let mut schema_set = SchemaSet::new();
8961
8962        let mut base_data = create_complex_type_data(None);
8963        base_data.open_content = Some(make_open_content(
8964            OpenContentMode::Interleave,
8965            WildcardNamespace::Any,
8966            ProcessContents::Lax,
8967        ));
8968        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8969
8970        let mut derived_data = create_complex_type_data(None);
8971        derived_data.derivation_method = Some(DerivationMethod::Restriction);
8972        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
8973        // No open_content — restriction removes it
8974        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
8975
8976        let mut stats = DerivationStats::default();
8977        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
8978        assert!(result.is_ok());
8979    }
8980
8981    #[cfg(feature = "xsd11")]
8982    #[test]
8983    fn test_restriction_empty_derived_allows_interleave_over_suffix() {
8984        // Per §3.4.6.4 (language containment), an empty derived particle
8985        // emits only wildcard content, so the OC mode choice is irrelevant —
8986        // interleave and suffix accept the same empty-particle language.
8987        // Mirrors W3C saxonData/Open/open020/open021 which expect VALID.
8988        use crate::parser::frames::{OpenContentMode, ProcessContents, WildcardNamespace};
8989
8990        let mut schema_set = SchemaSet::new();
8991
8992        let mut base_data = create_complex_type_data(None);
8993        base_data.open_content = Some(make_open_content(
8994            OpenContentMode::Suffix,
8995            WildcardNamespace::Any,
8996            ProcessContents::Lax,
8997        ));
8998        let base_key = schema_set.arenas.alloc_complex_type(base_data);
8999
9000        let mut derived_data = create_complex_type_data(None);
9001        derived_data.derivation_method = Some(DerivationMethod::Restriction);
9002        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
9003        derived_data.open_content = Some(make_open_content(
9004            OpenContentMode::Interleave,
9005            WildcardNamespace::Any,
9006            ProcessContents::Lax,
9007        ));
9008        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
9009
9010        let mut stats = DerivationStats::default();
9011        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
9012        assert!(
9013            result.is_ok(),
9014            "empty derived content should accept interleave over suffix, got {:?}",
9015            result.err(),
9016        );
9017    }
9018
9019    #[cfg(feature = "xsd11")]
9020    #[test]
9021    fn test_restriction_suffix_restricts_interleave_valid() {
9022        use crate::parser::frames::{OpenContentMode, ProcessContents, WildcardNamespace};
9023
9024        let mut schema_set = SchemaSet::new();
9025
9026        let mut base_data = create_complex_type_data(None);
9027        base_data.open_content = Some(make_open_content(
9028            OpenContentMode::Interleave,
9029            WildcardNamespace::Any,
9030            ProcessContents::Lax,
9031        ));
9032        let base_key = schema_set.arenas.alloc_complex_type(base_data);
9033
9034        let mut derived_data = create_complex_type_data(None);
9035        derived_data.derivation_method = Some(DerivationMethod::Restriction);
9036        derived_data.resolved_base_type = Some(TypeKey::Complex(base_key));
9037        derived_data.open_content = Some(make_open_content(
9038            OpenContentMode::Suffix,
9039            WildcardNamespace::Any,
9040            ProcessContents::Lax,
9041        ));
9042        let derived_key = schema_set.arenas.alloc_complex_type(derived_data);
9043
9044        let mut stats = DerivationStats::default();
9045        let result = validate_complex_type(&schema_set, derived_key, &mut stats);
9046        assert!(result.is_ok());
9047    }
9048
9049    // ====================================================================
9050    // cos-ns-subset: ##other exclusion-set tests (§3.10.1, §3.10.6.2)
9051    //
9052    // ##other maps to not({target namespace}, absent), so it always
9053    // excludes both the target namespace AND absent.
9054    // ====================================================================
9055
9056    #[cfg(feature = "xsd11")]
9057    #[test]
9058    fn test_ns_subset_local_not_subset_of_other() {
9059        // Base ##other with target ns urn:a excludes {Some(urn:a), None}.
9060        // Derived ##local allows {None}.
9061        // None is in base's exclusion set → NOT a subset.
9062        use crate::parser::frames::WildcardNamespace;
9063
9064        let schema_set = SchemaSet::new();
9065        let urn_a = schema_set.name_table.add("urn:a");
9066
9067        let result = is_namespace_subset(
9068            &WildcardNamespace::Local,
9069            None,
9070            &WildcardNamespace::Other,
9071            Some(urn_a),
9072        );
9073        assert!(
9074            !result,
9075            "##local must NOT be a subset of ##other (absent is excluded)"
9076        );
9077    }
9078
9079    #[cfg(feature = "xsd11")]
9080    #[test]
9081    fn test_ns_subset_other_no_tns_not_subset_of_other_with_tns() {
9082        // Base ##other with tns=urn:a excludes {Some(urn:a), None}.
9083        // Derived ##other with tns=None excludes {None}.
9084        // Derived still allows urn:a, which base excludes → NOT a subset.
9085        use crate::parser::frames::WildcardNamespace;
9086
9087        let schema_set = SchemaSet::new();
9088        let urn_a = schema_set.name_table.add("urn:a");
9089
9090        let result = is_namespace_subset(
9091            &WildcardNamespace::Other,
9092            None,
9093            &WildcardNamespace::Other,
9094            Some(urn_a),
9095        );
9096        assert!(
9097            !result,
9098            "##other(tns=None) must NOT be a subset of ##other(tns=urn:a)"
9099        );
9100    }
9101
9102    #[cfg(feature = "xsd11")]
9103    #[test]
9104    fn test_ns_subset_other_with_tns_is_subset_of_other_no_tns() {
9105        // Base ##other with tns=None excludes {None}.
9106        // Derived ##other with tns=urn:a excludes {Some(urn:a), None}.
9107        // Derived excludes a superset → IS a subset.
9108        use crate::parser::frames::WildcardNamespace;
9109
9110        let schema_set = SchemaSet::new();
9111        let urn_a = schema_set.name_table.add("urn:a");
9112
9113        let result = is_namespace_subset(
9114            &WildcardNamespace::Other,
9115            Some(urn_a),
9116            &WildcardNamespace::Other,
9117            None,
9118        );
9119        assert!(
9120            result,
9121            "##other(tns=urn:a) MUST be a subset of ##other(tns=None)"
9122        );
9123    }
9124
9125    #[cfg(feature = "xsd11")]
9126    #[test]
9127    fn test_ns_subset_list_with_tns_uri_not_subset_of_other() {
9128        // Base ##other with tns=urn:a excludes {Some(urn:a), None}.
9129        // Derived list contains explicit urn:a URI.
9130        // urn:a is in base's exclusion set → NOT a subset.
9131        use crate::parser::frames::{NamespaceToken, WildcardNamespace};
9132
9133        let schema_set = SchemaSet::new();
9134        let urn_a = schema_set.name_table.add("urn:a");
9135        let urn_b = schema_set.name_table.add("urn:b");
9136
9137        let result = is_namespace_subset(
9138            &WildcardNamespace::List(vec![NamespaceToken::Uri(urn_a), NamespaceToken::Uri(urn_b)]),
9139            None,
9140            &WildcardNamespace::Other,
9141            Some(urn_a),
9142        );
9143        assert!(
9144            !result,
9145            "List containing base's target ns must NOT be a subset of ##other"
9146        );
9147    }
9148
9149    // -----------------------------------------------------------------
9150    // §src-redefine 6.2.2 / 7.2.2 — focused pin tests
9151    //
9152    // Broad end-to-end rejection coverage (schR5/attgC028/mgO013) and
9153    // positive-coverage guards (annotA019, attgC017, schH1, schU1, …)
9154    // are already exercised by the W3C conformance suite. The tests
9155    // below pin the subtle invariants that are NOT directly covered by
9156    // conformance: (a) the `wildcard_allows_attribute` helper's
9157    // `##defined` correctness — which is the whole reason the helper
9158    // exists, and (b) the `all{required_e1}` vs `all{}` particle shape
9159    // that `mgO013` ultimately relies on.
9160    // -----------------------------------------------------------------
9161
9162    fn default_wildcard(ns: WildcardNamespace) -> WildcardResult {
9163        WildcardResult {
9164            namespace: ns,
9165            process_contents: ProcessContents::Strict,
9166            not_namespace: Vec::new(),
9167            not_qname: Vec::new(),
9168            id: None,
9169            annotation: None,
9170            source: None,
9171        }
9172    }
9173
9174    /// Normalize `w` against `target_ns` and ask whether the resulting
9175    /// effective wildcard admits `(attr_ns, attr_name)`. Used by the
9176    /// spec-invariant pin tests below so they exercise the production
9177    /// canonical-form path.
9178    fn admits(
9179        schema_set: &SchemaSet,
9180        w: &WildcardResult,
9181        target_ns: Option<NameId>,
9182        attr_ns: Option<NameId>,
9183        attr_name: NameId,
9184    ) -> bool {
9185        let eff = normalize_attribute_wildcard(schema_set, w, target_ns);
9186        effective_wildcard_allows_attribute(schema_set, &eff, attr_ns, attr_name)
9187    }
9188
9189    #[test]
9190    fn test_effective_wildcard_any_admits_anything() {
9191        let schema_set = SchemaSet::new();
9192        let name = schema_set.name_table.add("foo");
9193        let w = default_wildcard(WildcardNamespace::Any);
9194        assert!(admits(&schema_set, &w, None, None, name));
9195    }
9196
9197    #[test]
9198    fn test_effective_wildcard_other_excludes_target_ns() {
9199        // ##other must exclude the target namespace itself.
9200        let schema_set = SchemaSet::new();
9201        let ns = schema_set.name_table.add("urn:foo");
9202        let name = schema_set.name_table.add("bar");
9203        let w = default_wildcard(WildcardNamespace::Other);
9204        assert!(
9205            !admits(&schema_set, &w, Some(ns), Some(ns), name),
9206            "##other must NOT admit the target namespace"
9207        );
9208    }
9209
9210    #[test]
9211    fn test_effective_wildcard_other_admits_different_ns() {
9212        let schema_set = SchemaSet::new();
9213        let tns = schema_set.name_table.add("urn:foo");
9214        let other_ns = schema_set.name_table.add("urn:bar");
9215        let name = schema_set.name_table.add("qux");
9216        let w = default_wildcard(WildcardNamespace::Other);
9217        assert!(
9218            admits(&schema_set, &w, Some(tns), Some(other_ns), name),
9219            "##other must admit a namespace different from the target"
9220        );
9221    }
9222
9223    #[test]
9224    fn test_effective_wildcard_other_absent_ns_xsd10_vs_xsd11() {
9225        // §3.10.4.2 `##other` differs by version:
9226        //   - XSD 1.0: excludes both the target namespace AND the absent namespace.
9227        //   - XSD 1.1: excludes only the target namespace; the absent namespace is admitted.
9228        let schema_10 = SchemaSet::new(); // defaults to XSD 1.0
9229        let tns = schema_10.name_table.add("urn:foo");
9230        let name = schema_10.name_table.add("local_attr");
9231        let w = default_wildcard(WildcardNamespace::Other);
9232
9233        assert!(
9234            !admits(&schema_10, &w, Some(tns), None, name),
9235            "XSD 1.0: ##other must NOT admit the absent namespace"
9236        );
9237
9238        let schema_11 = SchemaSet::xsd11();
9239        let tns11 = schema_11.name_table.add("urn:foo");
9240        let name11 = schema_11.name_table.add("local_attr");
9241        assert!(
9242            admits(&schema_11, &w, Some(tns11), None, name11),
9243            "XSD 1.1: ##other MUST admit the absent namespace"
9244        );
9245    }
9246
9247    #[test]
9248    fn test_effective_wildcard_defined_excludes_declared_only() {
9249        // §3.10.4 `##defined` excludes ONLY attributes that are globally
9250        // declared — not all attributes unconditionally.
9251        use crate::arenas::AttributeDeclData;
9252        use crate::parser::frames::NotQNameItem;
9253
9254        let mut schema_set = SchemaSet::new();
9255        let declared_name = schema_set.name_table.add("declared_attr");
9256        let undeclared_name = schema_set.name_table.add("undeclared_attr");
9257
9258        let attr_data = AttributeDeclData {
9259            name: Some(declared_name),
9260            target_namespace: None,
9261            ref_name: None,
9262            type_ref: None,
9263            inline_type: None,
9264            default_value: None,
9265            fixed_value: None,
9266            use_kind: None,
9267            form: None,
9268            inheritable: false,
9269            id: None,
9270            annotation: None,
9271            source: None,
9272            resolved_type: None,
9273            resolved_ref: None,
9274        };
9275        let attr_key = schema_set.arenas.alloc_attribute(attr_data);
9276        schema_set
9277            .get_or_create_namespace(None)
9278            .register_attribute(declared_name, attr_key);
9279
9280        let mut w = default_wildcard(WildcardNamespace::Any);
9281        w.not_qname = vec![NotQNameItem::Defined];
9282
9283        assert!(
9284            !admits(&schema_set, &w, None, None, declared_name),
9285            "##defined MUST exclude globally-declared attributes"
9286        );
9287        assert!(
9288            admits(&schema_set, &w, None, None, undeclared_name),
9289            "##defined MUST NOT exclude attributes that are not globally declared"
9290        );
9291    }
9292
9293    #[test]
9294    fn test_effective_wildcard_not_qname_literal_excludes() {
9295        use crate::parser::frames::NotQNameItem;
9296
9297        let schema_set = SchemaSet::new();
9298        let blocked = schema_set.name_table.add("blocked");
9299        let allowed = schema_set.name_table.add("allowed");
9300
9301        let mut w = default_wildcard(WildcardNamespace::Any);
9302        w.not_qname = vec![NotQNameItem::QName {
9303            namespace: None,
9304            local_name: blocked,
9305        }];
9306
9307        assert!(!admits(&schema_set, &w, None, None, blocked));
9308        assert!(admits(&schema_set, &w, None, None, allowed));
9309    }
9310
9311    #[test]
9312    fn test_particle_restricts_all_required_over_empty_all_rejects() {
9313        // Pin test for the exact shape mgO013 reaches after
9314        // `remove_pointless_particles`: base `all{}` (e1{0,0} removed)
9315        // vs derived `all{e1{1,1}}`. The driver must reject — derived
9316        // adds a required particle to empty content, which is not a
9317        // valid restriction under §3.9.6.
9318        let schema_set = SchemaSet::new();
9319        let e1_name = schema_set.name_table.add("e1");
9320        let any_type = TypeKey::Complex(schema_set.any_type_key());
9321
9322        let make_elem = |min_occurs: u32, max_occurs: Option<u32>| NormalizedParticle {
9323            term: NormalizedParticleTerm::Element(NormalizedElement {
9324                name: e1_name,
9325                namespace: None,
9326                type_key: any_type,
9327                element_key: None,
9328                block: DerivationSet::empty(),
9329                nillable: false,
9330                fixed_value: None,
9331            }),
9332            min_occurs,
9333            max_occurs,
9334            source: None,
9335        };
9336
9337        let derived = NormalizedParticle {
9338            term: NormalizedParticleTerm::Group(NormalizedGroup {
9339                compositor: Compositor::All,
9340                particles: vec![make_elem(1, Some(1))],
9341            }),
9342            min_occurs: 1,
9343            max_occurs: Some(1),
9344            source: None,
9345        };
9346        let base_empty_all = NormalizedParticle {
9347            term: NormalizedParticleTerm::Group(NormalizedGroup {
9348                compositor: Compositor::All,
9349                particles: Vec::new(),
9350            }),
9351            min_occurs: 1,
9352            max_occurs: Some(1),
9353            source: None,
9354        };
9355
9356        assert!(
9357            !particle_restricts(&schema_set, &derived, &base_empty_all),
9358            "all{{e1{{1,1}}}} must NOT restrict all{{}} — derived adds a required particle"
9359        );
9360    }
9361
9362    #[test]
9363    fn test_collect_flat_attribute_uses_filters_prohibited() {
9364        // §3.2.2: prohibited attribute uses do NOT correspond to components
9365        // and must not appear in either side of a restriction comparison.
9366        use crate::arenas::AttributeGroupData;
9367        use crate::parser::frames::{
9368            AttributeFrameResult, AttributeUseKind as AuK, AttributeUseResult,
9369        };
9370
9371        let mut schema_set = SchemaSet::new();
9372        let grp_name = schema_set.name_table.add("ag");
9373        let opt_name = schema_set.name_table.add("opt");
9374        let banned_name = schema_set.name_table.add("banned");
9375
9376        let make_attr = |name: NameId, kind: AuK| AttributeUseResult {
9377            attribute: AttributeFrameResult {
9378                name: Some(name),
9379                ref_name: None,
9380                target_namespace: None,
9381                type_ref: None,
9382                inline_type: None,
9383                default_value: None,
9384                fixed_value: None,
9385                use_kind: None,
9386                form: None,
9387                inheritable: false,
9388                id: None,
9389                annotation: None,
9390                source: None,
9391            },
9392            use_kind: kind,
9393        };
9394
9395        let ag = AttributeGroupData {
9396            name: Some(grp_name),
9397            target_namespace: None,
9398            ref_name: None,
9399            attributes: vec![
9400                make_attr(opt_name, AuK::Optional),
9401                make_attr(banned_name, AuK::Prohibited),
9402            ],
9403            attribute_groups: Vec::new(),
9404            attribute_wildcard: None,
9405            id: None,
9406            annotation: None,
9407            source: None,
9408            resolved_ref: None,
9409            resolved_attribute_groups: Vec::new(),
9410            resolved_attributes: vec![
9411                crate::arenas::ResolvedAttributeUse {
9412                    resolved_type: None,
9413                    resolved_ref: None,
9414                },
9415                crate::arenas::ResolvedAttributeUse {
9416                    resolved_type: None,
9417                    resolved_ref: None,
9418                },
9419            ],
9420            redefine_original: None,
9421            redefine_requires_restriction_check: false,
9422        };
9423        let ag_key = schema_set.arenas.alloc_attribute_group(ag);
9424
9425        let uses = collect_flat_attribute_uses_for_group(&schema_set, ag_key);
9426        // Prohibited must be dropped; only `opt` survives.
9427        assert_eq!(
9428            uses.len(),
9429            1,
9430            "prohibited attribute uses must be filtered out"
9431        );
9432        assert_eq!(uses[0].name, opt_name);
9433    }
9434
9435    // -----------------------------------------------------------------
9436    // §3.6.2.2 effective attribute wildcard + §3.10.6.4 intersection
9437    // -----------------------------------------------------------------
9438
9439    fn wildcard_with_ns(namespace: WildcardNamespace) -> WildcardResult {
9440        WildcardResult {
9441            namespace,
9442            process_contents: ProcessContents::Strict,
9443            not_namespace: Vec::new(),
9444            not_qname: Vec::new(),
9445            id: None,
9446            annotation: None,
9447            source: None,
9448        }
9449    }
9450
9451    #[test]
9452    fn test_normalize_any() {
9453        let schema_set = SchemaSet::new();
9454        let wc = wildcard_with_ns(WildcardNamespace::Any);
9455        let eff = normalize_attribute_wildcard(&schema_set, &wc, None);
9456        assert!(matches!(eff.namespace, CanonicalNs::Any));
9457    }
9458
9459    #[test]
9460    fn test_normalize_list_resolves_tokens() {
9461        use crate::parser::frames::NamespaceToken;
9462        let schema_set = SchemaSet::new();
9463        let ns_a = schema_set.name_table.add("http://a");
9464        let target = schema_set.name_table.add("http://t");
9465
9466        let wc = wildcard_with_ns(WildcardNamespace::List(vec![
9467            NamespaceToken::Uri(ns_a),
9468            NamespaceToken::TargetNamespace,
9469            NamespaceToken::Local,
9470        ]));
9471        let eff = normalize_attribute_wildcard(&schema_set, &wc, Some(target));
9472        match eff.namespace {
9473            CanonicalNs::Enum(set) => {
9474                assert!(set.contains(&Some(ns_a)));
9475                assert!(set.contains(&Some(target)));
9476                assert!(set.contains(&None));
9477                assert_eq!(set.len(), 3);
9478            }
9479            _ => panic!("expected Enum"),
9480        }
9481    }
9482
9483    #[test]
9484    fn test_normalize_other_xsd10_vs_xsd11() {
9485        // Pins the fix documented at types/complex.rs:287-303 — XSD 1.0
9486        // `##other` excludes {target, absent}; XSD 1.1 excludes {target}
9487        // only.
9488        let schema_10 = SchemaSet::new();
9489        let schema_11 = SchemaSet::xsd11();
9490        let target_10 = schema_10.name_table.add("http://t");
9491        let target_11 = schema_11.name_table.add("http://t");
9492
9493        let wc10 = wildcard_with_ns(WildcardNamespace::Other);
9494        let wc11 = wildcard_with_ns(WildcardNamespace::Other);
9495
9496        let eff10 = normalize_attribute_wildcard(&schema_10, &wc10, Some(target_10));
9497        let eff11 = normalize_attribute_wildcard(&schema_11, &wc11, Some(target_11));
9498
9499        match eff10.namespace {
9500            CanonicalNs::Not(set) => {
9501                assert!(set.contains(&Some(target_10)));
9502                assert!(set.contains(&None), "XSD 1.0 ##other excludes absent");
9503            }
9504            _ => panic!("expected Not"),
9505        }
9506        match eff11.namespace {
9507            CanonicalNs::Not(set) => {
9508                assert!(set.contains(&Some(target_11)));
9509                assert!(!set.contains(&None), "XSD 1.1 ##other admits absent");
9510            }
9511            _ => panic!("expected Not"),
9512        }
9513    }
9514
9515    #[test]
9516    fn test_normalize_other_absent_target_namespace() {
9517        // Regression: when the schema has no target namespace, the
9518        // "target namespace" IS the absent namespace (None), so
9519        // ##other must exclude None even in XSD 1.1. An earlier
9520        // implementation skipped inserting None for XSD 1.1 when
9521        // target_ns was None, producing Not({}) ≡ Any and incorrectly
9522        // accepting invalid derivations for no-targetNamespace schemas.
9523        let schema_10 = SchemaSet::new();
9524        let schema_11 = SchemaSet::xsd11();
9525
9526        let wc = wildcard_with_ns(WildcardNamespace::Other);
9527        let eff10 = normalize_attribute_wildcard(&schema_10, &wc, None);
9528        let eff11 = normalize_attribute_wildcard(&schema_11, &wc, None);
9529
9530        for (label, eff) in [("XSD 1.0", eff10), ("XSD 1.1", eff11)] {
9531            match eff.namespace {
9532                CanonicalNs::Not(set) => {
9533                    assert!(
9534                        set.contains(&None),
9535                        "{}: ##other with absent target MUST exclude the absent namespace",
9536                        label,
9537                    );
9538                }
9539                other => panic!("{}: expected Not, got {:?}", label, other),
9540            }
9541        }
9542    }
9543
9544    #[test]
9545    fn test_effective_wildcard_restricts_defined_covers_declared_qname() {
9546        // Regression: per §3.10.6.2 disallowed_names clause 1, a base
9547        // QName exclusion is satisfied whenever the derived wildcard
9548        // is "not allowed" for that QName — including via a derived
9549        // `##defined` when the base QName names a globally declared
9550        // attribute. An earlier implementation required literal
9551        // `QName{}` containment and wrongly rejected this pattern.
9552        use crate::arenas::AttributeDeclData;
9553        use crate::parser::frames::NotQNameItem;
9554
9555        let mut schema_set = SchemaSet::new();
9556        let declared_name = schema_set.name_table.add("declared_attr");
9557
9558        // Globally declare `declared_attr`.
9559        let attr_data = AttributeDeclData {
9560            name: Some(declared_name),
9561            target_namespace: None,
9562            ref_name: None,
9563            type_ref: None,
9564            inline_type: None,
9565            default_value: None,
9566            fixed_value: None,
9567            use_kind: None,
9568            form: None,
9569            inheritable: false,
9570            id: None,
9571            annotation: None,
9572            source: None,
9573            resolved_type: None,
9574            resolved_ref: None,
9575        };
9576        let attr_key = schema_set.arenas.alloc_attribute(attr_data);
9577        schema_set
9578            .get_or_create_namespace(None)
9579            .register_attribute(declared_name, attr_key);
9580
9581        // Base excludes the declared attribute by literal QName.
9582        let base = EffectiveAttributeWildcard {
9583            namespace: CanonicalNs::Any,
9584            not_qname: vec![NotQNameItem::QName {
9585                namespace: None,
9586                local_name: declared_name,
9587            }],
9588            process_contents: ProcessContents::Strict,
9589        };
9590        // Derived excludes via ##defined — should cover the base
9591        // exclusion because declared_attr is globally declared.
9592        let derived = EffectiveAttributeWildcard {
9593            namespace: CanonicalNs::Any,
9594            not_qname: vec![NotQNameItem::Defined],
9595            process_contents: ProcessContents::Strict,
9596        };
9597
9598        assert!(
9599            effective_attribute_wildcard_restricts(&schema_set, &derived, &base).is_ok(),
9600            "derived ##defined must cover a base literal QName exclusion \
9601             when the attribute is globally declared"
9602        );
9603
9604        // Undeclared attribute: ##defined does NOT cover it.
9605        let undeclared = schema_set.name_table.add("undeclared_attr");
9606        let base_undeclared = EffectiveAttributeWildcard {
9607            namespace: CanonicalNs::Any,
9608            not_qname: vec![NotQNameItem::QName {
9609                namespace: None,
9610                local_name: undeclared,
9611            }],
9612            process_contents: ProcessContents::Strict,
9613        };
9614        assert!(
9615            effective_attribute_wildcard_restricts(&schema_set, &derived, &base_undeclared)
9616                .is_err(),
9617            "derived ##defined must NOT cover a base QName exclusion \
9618             when the attribute is not globally declared"
9619        );
9620    }
9621
9622    #[test]
9623    fn test_normalize_folds_not_namespace() {
9624        use crate::parser::frames::NamespaceToken;
9625        let schema_set = SchemaSet::new();
9626        let ns_a = schema_set.name_table.add("http://a");
9627
9628        // Any wildcard with not_namespace=[ns_a] becomes Not({ns_a}).
9629        let mut wc = wildcard_with_ns(WildcardNamespace::Any);
9630        wc.not_namespace = vec![NamespaceToken::Uri(ns_a)];
9631        let eff = normalize_attribute_wildcard(&schema_set, &wc, None);
9632        match eff.namespace {
9633            CanonicalNs::Not(set) => {
9634                assert_eq!(set.len(), 1);
9635                assert!(set.contains(&Some(ns_a)));
9636            }
9637            _ => panic!("expected Not"),
9638        }
9639    }
9640
9641    #[test]
9642    fn test_intersect_any_is_identity() {
9643        let mut s = std::collections::HashSet::new();
9644        let schema_set = SchemaSet::new();
9645        let ns_a = schema_set.name_table.add("http://a");
9646        s.insert(Some(ns_a));
9647
9648        let enum_a = CanonicalNs::Enum(s.clone());
9649        let result = intersect_canonical_ns(&CanonicalNs::Any, &enum_a);
9650        assert_eq!(result, enum_a);
9651        let result2 = intersect_canonical_ns(&enum_a, &CanonicalNs::Any);
9652        assert_eq!(result2, enum_a);
9653    }
9654
9655    #[test]
9656    fn test_intersect_enum_enum_is_set_intersection() {
9657        let schema_set = SchemaSet::new();
9658        let ns_a = schema_set.name_table.add("http://a");
9659        let ns_b = schema_set.name_table.add("http://b");
9660        let ns_c = schema_set.name_table.add("http://c");
9661
9662        let mut s1 = std::collections::HashSet::new();
9663        s1.insert(Some(ns_a));
9664        s1.insert(Some(ns_b));
9665        let mut s2 = std::collections::HashSet::new();
9666        s2.insert(Some(ns_b));
9667        s2.insert(Some(ns_c));
9668
9669        let result = intersect_canonical_ns(&CanonicalNs::Enum(s1), &CanonicalNs::Enum(s2));
9670        match result {
9671            CanonicalNs::Enum(set) => {
9672                assert_eq!(set.len(), 1);
9673                assert!(set.contains(&Some(ns_b)));
9674            }
9675            _ => panic!("expected Enum"),
9676        }
9677    }
9678
9679    #[test]
9680    fn test_intersect_enum_not_is_set_difference() {
9681        let schema_set = SchemaSet::new();
9682        let ns_a = schema_set.name_table.add("http://a");
9683        let ns_b = schema_set.name_table.add("http://b");
9684
9685        let mut s = std::collections::HashSet::new();
9686        s.insert(Some(ns_a));
9687        s.insert(Some(ns_b));
9688        let mut n = std::collections::HashSet::new();
9689        n.insert(Some(ns_b));
9690
9691        let result = intersect_canonical_ns(&CanonicalNs::Enum(s), &CanonicalNs::Not(n));
9692        match result {
9693            CanonicalNs::Enum(set) => {
9694                assert_eq!(set.len(), 1);
9695                assert!(set.contains(&Some(ns_a)));
9696            }
9697            _ => panic!("expected Enum"),
9698        }
9699    }
9700
9701    #[test]
9702    fn test_intersect_not_not_is_union_of_exclusions() {
9703        let schema_set = SchemaSet::new();
9704        let ns_a = schema_set.name_table.add("http://a");
9705        let ns_b = schema_set.name_table.add("http://b");
9706
9707        let mut n1 = std::collections::HashSet::new();
9708        n1.insert(Some(ns_a));
9709        let mut n2 = std::collections::HashSet::new();
9710        n2.insert(Some(ns_b));
9711
9712        let result = intersect_canonical_ns(&CanonicalNs::Not(n1), &CanonicalNs::Not(n2));
9713        match result {
9714            CanonicalNs::Not(set) => {
9715                assert_eq!(set.len(), 2);
9716                assert!(set.contains(&Some(ns_a)));
9717                assert!(set.contains(&Some(ns_b)));
9718            }
9719            _ => panic!("expected Not"),
9720        }
9721    }
9722
9723    #[test]
9724    fn test_canonical_ns_subset_various() {
9725        let schema_set = SchemaSet::new();
9726        let ns_a = schema_set.name_table.add("http://a");
9727        let ns_b = schema_set.name_table.add("http://b");
9728
9729        let empty_set = std::collections::HashSet::new();
9730        let mut s_a = std::collections::HashSet::new();
9731        s_a.insert(Some(ns_a));
9732        let mut s_ab = std::collections::HashSet::new();
9733        s_ab.insert(Some(ns_a));
9734        s_ab.insert(Some(ns_b));
9735
9736        // Anything ⊆ Any
9737        assert!(canonical_ns_subset(&CanonicalNs::Any, &CanonicalNs::Any));
9738        assert!(canonical_ns_subset(
9739            &CanonicalNs::Enum(s_a.clone()),
9740            &CanonicalNs::Any
9741        ));
9742        assert!(canonical_ns_subset(
9743            &CanonicalNs::Not(s_a.clone()),
9744            &CanonicalNs::Any
9745        ));
9746
9747        // Any ⊄ non-Any
9748        assert!(!canonical_ns_subset(
9749            &CanonicalNs::Any,
9750            &CanonicalNs::Enum(s_a.clone())
9751        ));
9752
9753        // Enum(s) ⊆ Enum(t) iff s ⊆ t
9754        assert!(canonical_ns_subset(
9755            &CanonicalNs::Enum(s_a.clone()),
9756            &CanonicalNs::Enum(s_ab.clone()),
9757        ));
9758        assert!(!canonical_ns_subset(
9759            &CanonicalNs::Enum(s_ab.clone()),
9760            &CanonicalNs::Enum(s_a.clone()),
9761        ));
9762
9763        // Enum(s) ⊆ Not(n) iff s ∩ n = ∅
9764        assert!(canonical_ns_subset(
9765            &CanonicalNs::Enum(s_a.clone()),
9766            &CanonicalNs::Not(empty_set.clone()),
9767        ));
9768        assert!(!canonical_ns_subset(
9769            &CanonicalNs::Enum(s_a.clone()),
9770            &CanonicalNs::Not(s_a.clone()),
9771        ));
9772
9773        // Not(n1) ⊆ Not(n2) iff n2 ⊆ n1 (derived exclusion must be ≥ base)
9774        assert!(canonical_ns_subset(
9775            &CanonicalNs::Not(s_ab.clone()),
9776            &CanonicalNs::Not(s_a.clone()),
9777        ));
9778        assert!(!canonical_ns_subset(
9779            &CanonicalNs::Not(s_a.clone()),
9780            &CanonicalNs::Not(s_ab.clone()),
9781        ));
9782
9783        // Not(n) ⊄ Enum(s) — infinite cannot fit in finite
9784        assert!(!canonical_ns_subset(
9785            &CanonicalNs::Not(empty_set),
9786            &CanonicalNs::Enum(s_a),
9787        ));
9788    }
9789
9790    #[test]
9791    fn test_effective_attribute_wildcard_absent_no_groups_returns_none() {
9792        let schema_set = SchemaSet::new();
9793        let result = effective_attribute_wildcard(&schema_set, None, None, &[]);
9794        assert!(matches!(result, Ok(None)));
9795    }
9796
9797    #[test]
9798    fn test_effective_attribute_wildcard_local_only() {
9799        let schema_set = SchemaSet::new();
9800        let wc = wildcard_with_ns(WildcardNamespace::Any);
9801        let result = effective_attribute_wildcard(&schema_set, Some(&wc), None, &[]).unwrap();
9802        let eff = result.expect("expected Some");
9803        assert!(matches!(eff.namespace, CanonicalNs::Any));
9804    }
9805
9806    #[test]
9807    fn test_effective_attribute_wildcard_intersects_across_group_and_local() {
9808        use crate::arenas::AttributeGroupData;
9809        use crate::parser::frames::NamespaceToken;
9810
9811        let mut schema_set = SchemaSet::new();
9812        let ns_a = schema_set.name_table.add("http://a");
9813        let ns_b = schema_set.name_table.add("http://b");
9814
9815        // Referenced group has wildcard List[a, b]
9816        let group_wc = WildcardResult {
9817            namespace: WildcardNamespace::List(vec![
9818                NamespaceToken::Uri(ns_a),
9819                NamespaceToken::Uri(ns_b),
9820            ]),
9821            process_contents: ProcessContents::Strict,
9822            not_namespace: Vec::new(),
9823            not_qname: Vec::new(),
9824            id: None,
9825            annotation: None,
9826            source: None,
9827        };
9828        let group = AttributeGroupData {
9829            name: None,
9830            target_namespace: None,
9831            ref_name: None,
9832            attributes: Vec::new(),
9833            attribute_groups: Vec::new(),
9834            attribute_wildcard: Some(group_wc),
9835            id: None,
9836            annotation: None,
9837            source: None,
9838            resolved_ref: None,
9839            resolved_attribute_groups: Vec::new(),
9840            resolved_attributes: Vec::new(),
9841            redefine_original: None,
9842            redefine_requires_restriction_check: false,
9843        };
9844        let group_key = schema_set.arenas.alloc_attribute_group(group);
9845
9846        // Local wildcard is List[a]. Intersection should be {a}.
9847        let local = WildcardResult {
9848            namespace: WildcardNamespace::List(vec![NamespaceToken::Uri(ns_a)]),
9849            process_contents: ProcessContents::Strict,
9850            not_namespace: Vec::new(),
9851            not_qname: Vec::new(),
9852            id: None,
9853            annotation: None,
9854            source: None,
9855        };
9856        let result =
9857            effective_attribute_wildcard(&schema_set, Some(&local), None, &[group_key]).unwrap();
9858        let eff = result.expect("expected Some");
9859        match eff.namespace {
9860            CanonicalNs::Enum(set) => {
9861                assert_eq!(set.len(), 1);
9862                assert!(set.contains(&Some(ns_a)));
9863            }
9864            other => panic!("expected Enum({{ns_a}}), got {:?}", other),
9865        }
9866    }
9867
9868    #[test]
9869    fn test_effective_attribute_wildcard_no_local_uses_first_group_pc() {
9870        use crate::arenas::AttributeGroupData;
9871
9872        let mut schema_set = SchemaSet::new();
9873        let group_wc = WildcardResult {
9874            namespace: WildcardNamespace::Any,
9875            process_contents: ProcessContents::Lax,
9876            not_namespace: Vec::new(),
9877            not_qname: Vec::new(),
9878            id: None,
9879            annotation: None,
9880            source: None,
9881        };
9882        let group = AttributeGroupData {
9883            name: None,
9884            target_namespace: None,
9885            ref_name: None,
9886            attributes: Vec::new(),
9887            attribute_groups: Vec::new(),
9888            attribute_wildcard: Some(group_wc),
9889            id: None,
9890            annotation: None,
9891            source: None,
9892            resolved_ref: None,
9893            resolved_attribute_groups: Vec::new(),
9894            resolved_attributes: Vec::new(),
9895            redefine_original: None,
9896            redefine_requires_restriction_check: false,
9897        };
9898        let group_key = schema_set.arenas.alloc_attribute_group(group);
9899
9900        // No local ⇒ pc comes from W[0] (Lax).
9901        let result = effective_attribute_wildcard(&schema_set, None, None, &[group_key]).unwrap();
9902        let eff = result.expect("expected Some");
9903        assert_eq!(eff.process_contents, ProcessContents::Lax);
9904        assert!(matches!(eff.namespace, CanonicalNs::Any));
9905    }
9906
9907    #[test]
9908    fn test_effective_wildcard_allows_attribute_basic() {
9909        let schema_set = SchemaSet::new();
9910        let name = schema_set.name_table.add("foo");
9911        let ns_a = schema_set.name_table.add("http://a");
9912
9913        let any_eff = EffectiveAttributeWildcard {
9914            namespace: CanonicalNs::Any,
9915            not_qname: Vec::new(),
9916            process_contents: ProcessContents::Strict,
9917        };
9918        assert!(effective_wildcard_allows_attribute(
9919            &schema_set,
9920            &any_eff,
9921            Some(ns_a),
9922            name,
9923        ));
9924
9925        let mut s = std::collections::HashSet::new();
9926        s.insert(Some(ns_a));
9927        let enum_eff = EffectiveAttributeWildcard {
9928            namespace: CanonicalNs::Enum(s),
9929            not_qname: Vec::new(),
9930            process_contents: ProcessContents::Strict,
9931        };
9932        assert!(effective_wildcard_allows_attribute(
9933            &schema_set,
9934            &enum_eff,
9935            Some(ns_a),
9936            name,
9937        ));
9938        assert!(!effective_wildcard_allows_attribute(
9939            &schema_set,
9940            &enum_eff,
9941            None,
9942            name,
9943        ));
9944    }
9945
9946    #[test]
9947    fn test_effective_wildcard_restricts_enforces_subset() {
9948        let schema_set = SchemaSet::new();
9949        let ns_a = schema_set.name_table.add("http://a");
9950        let ns_b = schema_set.name_table.add("http://b");
9951
9952        let mut s_a = std::collections::HashSet::new();
9953        s_a.insert(Some(ns_a));
9954        let mut s_ab = std::collections::HashSet::new();
9955        s_ab.insert(Some(ns_a));
9956        s_ab.insert(Some(ns_b));
9957
9958        let derived_narrow = EffectiveAttributeWildcard {
9959            namespace: CanonicalNs::Enum(s_a.clone()),
9960            not_qname: Vec::new(),
9961            process_contents: ProcessContents::Strict,
9962        };
9963        let base_wide = EffectiveAttributeWildcard {
9964            namespace: CanonicalNs::Enum(s_ab.clone()),
9965            not_qname: Vec::new(),
9966            process_contents: ProcessContents::Strict,
9967        };
9968
9969        assert!(
9970            effective_attribute_wildcard_restricts(&schema_set, &derived_narrow, &base_wide)
9971                .is_ok()
9972        );
9973        assert!(
9974            effective_attribute_wildcard_restricts(&schema_set, &base_wide, &derived_narrow)
9975                .is_err()
9976        );
9977    }
9978
9979    #[test]
9980    fn test_effective_wildcard_restricts_enforces_process_contents() {
9981        let schema_set = SchemaSet::new();
9982        let skip_eff = EffectiveAttributeWildcard {
9983            namespace: CanonicalNs::Any,
9984            not_qname: Vec::new(),
9985            process_contents: ProcessContents::Skip,
9986        };
9987        let strict_eff = EffectiveAttributeWildcard {
9988            namespace: CanonicalNs::Any,
9989            not_qname: Vec::new(),
9990            process_contents: ProcessContents::Strict,
9991        };
9992        // Strict restricts Skip (tightening).
9993        assert!(
9994            effective_attribute_wildcard_restricts(&schema_set, &strict_eff, &skip_eff).is_ok()
9995        );
9996        // Skip cannot restrict Strict (loosening).
9997        assert!(
9998            effective_attribute_wildcard_restricts(&schema_set, &skip_eff, &strict_eff).is_err()
9999        );
10000    }
10001
10002    #[test]
10003    fn test_validate_attribute_restriction_rejects_added_wildcard() {
10004        // Base has no wildcard, derived adds Any — invalid restriction.
10005        let mut schema_set = SchemaSet::new();
10006        let base = create_complex_type_data(None);
10007        let base_key = schema_set.arenas.alloc_complex_type(base);
10008
10009        let mut derived = create_complex_type_data(None);
10010        derived.attribute_wildcard = Some(wildcard_with_ns(WildcardNamespace::Any));
10011        derived.derivation_method = Some(DerivationMethod::Restriction);
10012        derived.resolved_base_type = Some(TypeKey::Complex(base_key));
10013        let derived_key = schema_set.arenas.alloc_complex_type(derived);
10014
10015        let derived_ref = schema_set.arenas.complex_types.get(derived_key).unwrap();
10016        let base_ref = schema_set.arenas.complex_types.get(base_key).unwrap();
10017        let result = validate_attribute_restriction(&schema_set, derived_ref, base_ref);
10018        assert!(result.is_err());
10019        if let Err(SchemaError::StructuralError {
10020            constraint,
10021            message,
10022            ..
10023        }) = result
10024        {
10025            assert_eq!(constraint, "derivation-ok-restriction");
10026            assert!(
10027                message.contains("wildcard"),
10028                "message should mention wildcard, got: {}",
10029                message
10030            );
10031        } else {
10032            panic!("expected StructuralError");
10033        }
10034    }
10035
10036    #[test]
10037    fn test_validate_attribute_restriction_accepts_narrower_wildcard() {
10038        use crate::parser::frames::NamespaceToken;
10039        let mut schema_set = SchemaSet::new();
10040        let ns_a = schema_set.name_table.add("http://a");
10041
10042        let mut base = create_complex_type_data(None);
10043        base.attribute_wildcard = Some(wildcard_with_ns(WildcardNamespace::Any));
10044        let base_key = schema_set.arenas.alloc_complex_type(base);
10045
10046        let mut derived = create_complex_type_data(None);
10047        derived.attribute_wildcard = Some(wildcard_with_ns(WildcardNamespace::List(vec![
10048            NamespaceToken::Uri(ns_a),
10049        ])));
10050        derived.derivation_method = Some(DerivationMethod::Restriction);
10051        derived.resolved_base_type = Some(TypeKey::Complex(base_key));
10052        let derived_key = schema_set.arenas.alloc_complex_type(derived);
10053
10054        let derived_ref = schema_set.arenas.complex_types.get(derived_key).unwrap();
10055        let base_ref = schema_set.arenas.complex_types.get(base_key).unwrap();
10056        assert!(validate_attribute_restriction(&schema_set, derived_ref, base_ref).is_ok());
10057    }
10058
10059    #[test]
10060    fn test_validate_attribute_restriction_allows_removing_wildcard() {
10061        // Base has Any, derived removes the wildcard — always valid
10062        // (restriction may remove the wildcard).
10063        let mut schema_set = SchemaSet::new();
10064        let mut base = create_complex_type_data(None);
10065        base.attribute_wildcard = Some(wildcard_with_ns(WildcardNamespace::Any));
10066        let base_key = schema_set.arenas.alloc_complex_type(base);
10067
10068        let mut derived = create_complex_type_data(None);
10069        derived.derivation_method = Some(DerivationMethod::Restriction);
10070        derived.resolved_base_type = Some(TypeKey::Complex(base_key));
10071        let derived_key = schema_set.arenas.alloc_complex_type(derived);
10072
10073        let derived_ref = schema_set.arenas.complex_types.get(derived_key).unwrap();
10074        let base_ref = schema_set.arenas.complex_types.get(base_key).unwrap();
10075        assert!(validate_attribute_restriction(&schema_set, derived_ref, base_ref).is_ok());
10076    }
10077
10078    #[test]
10079    fn test_redefine_attribute_group_rejects_broader_wildcard() {
10080        // Original has Any; redefined "restriction" keeps Any + adds a
10081        // broader-than-original effective wildcard via added scope —
10082        // emulated here by giving the redefined side a wildcard that
10083        // excludes fewer namespaces than the original (via not_namespace).
10084        use crate::arenas::AttributeGroupData;
10085        use crate::parser::frames::NamespaceToken;
10086
10087        let mut schema_set = SchemaSet::new();
10088        let ns_a = schema_set.name_table.add("http://a");
10089
10090        // Original wildcard: Any with not_namespace=[ns_a]  ⇒  Not({ns_a})
10091        let mut original_wc = wildcard_with_ns(WildcardNamespace::Any);
10092        original_wc.not_namespace = vec![NamespaceToken::Uri(ns_a)];
10093        let original = AttributeGroupData {
10094            name: None,
10095            target_namespace: None,
10096            ref_name: None,
10097            attributes: Vec::new(),
10098            attribute_groups: Vec::new(),
10099            attribute_wildcard: Some(original_wc),
10100            id: None,
10101            annotation: None,
10102            source: None,
10103            resolved_ref: None,
10104            resolved_attribute_groups: Vec::new(),
10105            resolved_attributes: Vec::new(),
10106            redefine_original: None,
10107            redefine_requires_restriction_check: false,
10108        };
10109        let original_key = schema_set.arenas.alloc_attribute_group(original);
10110
10111        // Derived wildcard: plain Any (allows ns_a, which original excludes).
10112        let derived = AttributeGroupData {
10113            name: None,
10114            target_namespace: None,
10115            ref_name: None,
10116            attributes: Vec::new(),
10117            attribute_groups: Vec::new(),
10118            attribute_wildcard: Some(wildcard_with_ns(WildcardNamespace::Any)),
10119            id: None,
10120            annotation: None,
10121            source: None,
10122            resolved_ref: None,
10123            resolved_attribute_groups: Vec::new(),
10124            resolved_attributes: Vec::new(),
10125            redefine_original: Some(original_key),
10126            redefine_requires_restriction_check: true,
10127        };
10128        schema_set.arenas.alloc_attribute_group(derived);
10129
10130        let mut errors = Vec::new();
10131        let mut stats = DerivationStats::default();
10132        validate_all_redefine_attribute_group_restrictions(&schema_set, &mut errors, &mut stats);
10133
10134        assert!(
10135            !errors.is_empty(),
10136            "expected a src-redefine.7.2.2 error for broader derived wildcard"
10137        );
10138        let msg = match &errors[0] {
10139            SchemaError::StructuralError {
10140                constraint,
10141                message,
10142                ..
10143            } => {
10144                assert_eq!(*constraint, "src-redefine.7.2.2");
10145                message.clone()
10146            }
10147            _ => panic!("expected StructuralError"),
10148        };
10149        assert!(
10150            msg.contains("wildcard") || msg.contains("restriction"),
10151            "error should mention wildcard restriction, got: {}",
10152            msg
10153        );
10154    }
10155
10156    #[test]
10157    fn test_redefine_attribute_group_effective_wildcard_admits_inherited_attr() {
10158        // Original attribute group references a nested group whose local
10159        // wildcard is List[ns_a]. The redefined group adds an attribute in
10160        // ns_a. Without the §3.6.2.2 effective-wildcard fix, this would
10161        // fail: the original's *local* attribute_wildcard is None, so the
10162        // old code would reject ns_a even though the inherited wildcard
10163        // admits it.
10164        use crate::arenas::{AttributeGroupData, ResolvedAttributeUse};
10165        use crate::parser::frames::{
10166            AttributeFrameResult, AttributeUseKind as AuK, AttributeUseResult, NamespaceToken,
10167        };
10168
10169        let mut schema_set = SchemaSet::new();
10170        let ns_a = schema_set.name_table.add("http://a");
10171        let attr_name = schema_set.name_table.add("foo");
10172
10173        // Nested group with wildcard List[ns_a].
10174        let nested_wc = WildcardResult {
10175            namespace: WildcardNamespace::List(vec![NamespaceToken::Uri(ns_a)]),
10176            process_contents: ProcessContents::Strict,
10177            not_namespace: Vec::new(),
10178            not_qname: Vec::new(),
10179            id: None,
10180            annotation: None,
10181            source: None,
10182        };
10183        let nested = AttributeGroupData {
10184            name: None,
10185            target_namespace: None,
10186            ref_name: None,
10187            attributes: Vec::new(),
10188            attribute_groups: Vec::new(),
10189            attribute_wildcard: Some(nested_wc),
10190            id: None,
10191            annotation: None,
10192            source: None,
10193            resolved_ref: None,
10194            resolved_attribute_groups: Vec::new(),
10195            resolved_attributes: Vec::new(),
10196            redefine_original: None,
10197            redefine_requires_restriction_check: false,
10198        };
10199        let nested_key = schema_set.arenas.alloc_attribute_group(nested);
10200
10201        // Original group: no local wildcard, references nested.
10202        let original = AttributeGroupData {
10203            name: None,
10204            target_namespace: None,
10205            ref_name: None,
10206            attributes: Vec::new(),
10207            attribute_groups: Vec::new(),
10208            attribute_wildcard: None,
10209            id: None,
10210            annotation: None,
10211            source: None,
10212            resolved_ref: None,
10213            resolved_attribute_groups: vec![nested_key],
10214            resolved_attributes: Vec::new(),
10215            redefine_original: None,
10216            redefine_requires_restriction_check: false,
10217        };
10218        let original_key = schema_set.arenas.alloc_attribute_group(original);
10219
10220        // Redefined group: adds an attribute in ns_a, inherits the nested
10221        // wildcard through the same reference chain.
10222        let attr_use = AttributeUseResult {
10223            attribute: AttributeFrameResult {
10224                name: Some(attr_name),
10225                ref_name: None,
10226                target_namespace: Some(ns_a),
10227                type_ref: None,
10228                inline_type: None,
10229                default_value: None,
10230                fixed_value: None,
10231                use_kind: None,
10232                form: None,
10233                inheritable: false,
10234                id: None,
10235                annotation: None,
10236                source: None,
10237            },
10238            use_kind: AuK::Optional,
10239        };
10240        let derived = AttributeGroupData {
10241            name: None,
10242            target_namespace: None,
10243            ref_name: None,
10244            attributes: vec![attr_use],
10245            attribute_groups: Vec::new(),
10246            attribute_wildcard: None,
10247            id: None,
10248            annotation: None,
10249            source: None,
10250            resolved_ref: None,
10251            resolved_attribute_groups: vec![nested_key],
10252            resolved_attributes: vec![ResolvedAttributeUse {
10253                resolved_type: None,
10254                resolved_ref: None,
10255            }],
10256            redefine_original: Some(original_key),
10257            redefine_requires_restriction_check: true,
10258        };
10259        schema_set.arenas.alloc_attribute_group(derived);
10260
10261        let mut errors = Vec::new();
10262        let mut stats = DerivationStats::default();
10263        validate_all_redefine_attribute_group_restrictions(&schema_set, &mut errors, &mut stats);
10264
10265        assert!(
10266            errors.is_empty(),
10267            "attribute admitted by inherited effective wildcard should not error; got: {:?}",
10268            errors
10269                .iter()
10270                .map(|e| format!("{:?}", e))
10271                .collect::<Vec<_>>()
10272        );
10273    }
10274}