Skip to main content

xsd_schema/schema/
edc.rs

1//! XSD 1.1 dynamic Element Declarations Consistent (§3.4.6.4 / cvc-complex-type rule 5).
2//!
3//! When a wildcard accepts an element and resolves it (via lax/strict process
4//! contents) to a global element declaration, that *governing* declaration must
5//! be consistent with any locally declared element binding for the same QName
6//! in the same content model — including bindings inherited from base types.
7//! This is the "dynamic EDC" check.
8//!
9//! Schema-time EDC (cos-element-consistent) is a separate concern; this module
10//! covers only the runtime obligation that fires after a wildcard match.
11
12#![cfg(feature = "xsd11")]
13
14use std::collections::HashMap;
15
16use crate::compiler::SubstitutionGroupMap;
17use crate::ids::{ComplexTypeKey, ElementKey, NameId, TypeKey};
18use crate::parser::frames::{ComplexContentResult, ParticleResult, ParticleTerm};
19use crate::schema::model::{DerivationSet, SchemaSet};
20
21/// Default binding tracked for a QName in a content model.
22#[derive(Debug, Clone, Copy)]
23pub enum DefaultBinding {
24    /// A local element declaration governs this QName. The element key may be
25    /// the head of a substitution group when the binding was reached through
26    /// substitution propagation.
27    Element {
28        key: ElementKey,
29        /// Resolved type of the binding, captured eagerly because the local
30        /// arena entry sometimes leaves `resolved_type` empty for inline
31        /// shapes that resolve through `resolved_content_particle_types`.
32        resolved_type: Option<TypeKey>,
33    },
34    /// A wildcard with `processContents = strict` covers this binding.
35    #[allow(dead_code)]
36    Strict,
37    /// A wildcard with `processContents = lax`.
38    #[allow(dead_code)]
39    Lax,
40    /// A wildcard with `processContents = skip`.
41    #[allow(dead_code)]
42    Skip,
43}
44
45/// Result of a dynamic EDC check.
46#[derive(Debug, Clone)]
47pub enum EdcOutcome {
48    /// No local binding for this QName — wildcard match is unconstrained.
49    NoLocalBinding,
50    /// Local binding subsumes the governing decl/type — match is valid.
51    Subsumes,
52    /// Local binding does NOT subsume — emit `cvc-complex-type.5`.
53    Mismatch { reason: String },
54}
55
56/// Dynamic EDC check for a wildcard-matched element.
57pub fn check_dynamic_edc(
58    schema_set: &SchemaSet,
59    subst_groups: Option<&SubstitutionGroupMap>,
60    parent_ct: ComplexTypeKey,
61    qname: (Option<NameId>, NameId),
62    governing_type: Option<TypeKey>,
63    governing_decl: Option<ElementKey>,
64) -> EdcOutcome {
65    let bindings = collect_local_bindings(schema_set, subst_groups, parent_ct);
66    let Some(local) = bindings.get(&qname).copied() else {
67        return EdcOutcome::NoLocalBinding;
68    };
69
70    if let DefaultBinding::Element { resolved_type, .. } = local {
71        let Some(local_type) = resolved_type else {
72            return EdcOutcome::Subsumes;
73        };
74        let gov_type = governing_type
75            .or_else(|| governing_decl.and_then(|k| schema_set.arenas.elements[k].resolved_type));
76        let Some(gov_type) = gov_type else {
77            return EdcOutcome::Subsumes;
78        };
79        if !schema_set.is_type_derived_from(gov_type, local_type, DerivationSet::empty()) {
80            return EdcOutcome::Mismatch {
81                reason: "governing type is not validly substitutable for the local element's type"
82                    .to_string(),
83            };
84        }
85    }
86    EdcOutcome::Subsumes
87}
88
89/// Build a (namespace, name) → DefaultBinding map for a complex type's
90/// content model, walking up the base-type chain so that bindings inherited
91/// from a restricted/extended base are visible.
92pub fn collect_local_bindings(
93    schema_set: &SchemaSet,
94    subst_groups: Option<&SubstitutionGroupMap>,
95    ct_key: ComplexTypeKey,
96) -> HashMap<(Option<NameId>, NameId), DefaultBinding> {
97    let mut out = HashMap::new();
98    let mut visited = std::collections::HashSet::new();
99    let mut current = Some(ct_key);
100    while let Some(k) = current {
101        if !visited.insert(k) {
102            break;
103        }
104        let ct = &schema_set.arenas.complex_types[k];
105        let target_ns = ct.target_namespace;
106        if let ComplexContentResult::Complex(content) = &ct.content {
107            if let Some(particle) = content.particle.as_ref() {
108                let mut flat_idx = 0usize;
109                walk_particle(
110                    schema_set,
111                    subst_groups,
112                    particle,
113                    target_ns,
114                    &ct.resolved_content_particle_elements,
115                    &ct.resolved_content_particle_types,
116                    &mut flat_idx,
117                    &mut out,
118                    0,
119                );
120            }
121        }
122        // Walk up the base type chain (extension or restriction).
123        current = match ct.resolved_base_type {
124            Some(TypeKey::Complex(parent)) => Some(parent),
125            _ => None,
126        };
127    }
128    out
129}
130
131/// Walk a particle tree, recording each Element particle's binding into `out`.
132/// `flat_idx` and the parallel `local_keys` / `local_types` arrays follow the
133/// same flat depth-first scheme used by `allocate_content_particle_elements`.
134#[allow(clippy::too_many_arguments)]
135fn walk_particle(
136    schema_set: &SchemaSet,
137    subst_groups: Option<&SubstitutionGroupMap>,
138    particle: &ParticleResult,
139    target_ns: Option<NameId>,
140    local_keys: &[Option<ElementKey>],
141    local_types: &[Option<TypeKey>],
142    flat_idx: &mut usize,
143    out: &mut HashMap<(Option<NameId>, NameId), DefaultBinding>,
144    depth: usize,
145) {
146    if depth > 64 {
147        return;
148    }
149    match &particle.term {
150        ParticleTerm::Element(elem) => {
151            let (qname_ns, qname_local, key, resolved_type) = if let Some(ref_qn) = &elem.ref_name {
152                let key = schema_set.lookup_element(ref_qn.namespace, ref_qn.local_name);
153                let ty = key.and_then(|k| schema_set.arenas.elements[k].resolved_type);
154                let idx = *flat_idx;
155                *flat_idx += 1;
156                let _ = idx; // ref slot is None in local_keys; ignore
157                (ref_qn.namespace, ref_qn.local_name, key, ty)
158            } else if let Some(name) = elem.name {
159                let ns = elem.target_namespace.or(target_ns);
160                let idx = *flat_idx;
161                *flat_idx += 1;
162                let key = local_keys.get(idx).copied().flatten();
163                let ty = local_types
164                    .get(idx)
165                    .copied()
166                    .flatten()
167                    .or_else(|| key.and_then(|k| schema_set.arenas.elements[k].resolved_type));
168                (ns, name, key, ty)
169            } else {
170                return;
171            };
172
173            let Some(binding_key) = key else { return };
174
175            // Insert if absent: derived bindings shadow base bindings.
176            out.entry((qname_ns, qname_local))
177                .or_insert(DefaultBinding::Element {
178                    key: binding_key,
179                    resolved_type,
180                });
181
182            // Substitution-group members: their own QName resolves to the
183            // head's binding (head's resolved_type).
184            if let Some(map) = subst_groups {
185                if let Some(members) = map.get(&binding_key) {
186                    for &(member_name, member_ns) in members.iter() {
187                        if (member_ns, member_name) == (qname_ns, qname_local) {
188                            continue;
189                        }
190                        out.entry((member_ns, member_name))
191                            .or_insert(DefaultBinding::Element {
192                                key: binding_key,
193                                resolved_type,
194                            });
195                    }
196                }
197            }
198        }
199        ParticleTerm::Group(mg) => {
200            if let Some(ref_qn) = &mg.ref_name {
201                if let Some(group_key) =
202                    schema_set.lookup_model_group(ref_qn.namespace, ref_qn.local_name)
203                {
204                    let group = &schema_set.arenas.model_groups[group_key];
205                    let mut group_flat_idx = 0usize;
206                    for child in &group.particles {
207                        walk_particle(
208                            schema_set,
209                            subst_groups,
210                            child,
211                            group.target_namespace.or(target_ns),
212                            &group.resolved_particle_elements,
213                            &group.resolved_particle_types,
214                            &mut group_flat_idx,
215                            out,
216                            depth + 1,
217                        );
218                    }
219                }
220                // Group refs do NOT increment our flat_idx (mirrors
221                // `collect_content_particle_elements_recursive`).
222            } else {
223                for child in &mg.particles {
224                    walk_particle(
225                        schema_set,
226                        subst_groups,
227                        child,
228                        target_ns,
229                        local_keys,
230                        local_types,
231                        flat_idx,
232                        out,
233                        depth + 1,
234                    );
235                }
236            }
237        }
238        ParticleTerm::Any(_) => {
239            // Wildcards do not increment flat_idx.
240        }
241    }
242}