diff --git a/CHANGELOG.md b/CHANGELOG.md index 46d6c24..b98b793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- A substitution-group `block` on a head element now bars substituting a member whose INLINE (anonymous) type reaches the head's type by a blocked method. The substitution-group filter already dropped a member when the head's `{disallowed substitutions}` (or the head type's `{prohibited substitutions}`) blocked the member's derivation method, but only for members with a NAMED type recorded in the derivation backbone; an element with an inline complex type had no backbone entry, so a member extending the head's type was wrongly kept even when the head blocked `extension`. Inline-typed elements now record their derivation under a synthetic key (an element key plus a `#` marker no QName can contain, so it cannot collide with a real type and is unreachable as any other derivation base), so the filter sees the inline member's method. A member reaching the head's type by an UNblocked method, a plain inline type with no derivation, and same-local-name elements in different namespaces are all unaffected. Catches disallowedSubst00105m (and a sibling). XSTS invalid-instances-accepted 25 → 23, valid-instances-rejected held at 0. - An identity-constraint (`key`/`unique`/`keyref`) field that identifies a node with complex content is now rejected during instance validation. Per cvc-identity-constraint.3 / c-fields-xpaths a field must evaluate to a node with a simple type (a simple-typed element, an element with simple content, or an attribute); the cardinality clause (at most one node) was enforced, but the simple-content clause was not. The check is structural and false-positive-free: a field whose selected node is an element carrying element children has complex (element-only or mixed) content and can never be simple-typed, so it is flagged, while a simple-typed or simple-content element (text only), an empty or text-only node, and an attribute are left alone. Catches idK012. XSTS invalid-instances-accepted 26 → 25, valid-instances-rejected held at 0. - The attribute-use uniqueness check (`ag-props-correct.2` / `ct-props-correct.4`) now flattens a redefined attribute group's self-reference and excludes prohibited uses. When an attribute group flattens its uses through `` references, a reference to a redefined group whose redefinition self-references its former self previously stopped at the cycle guard, so the original's attributes were never reached and a clash they cause was missed. The self-reference now expands to the group's pre-redefine base definition (already retained for this purpose), so the original's attributes join the set; a genuine non-redefine cycle still terminates without contributing. Separately, a `use="prohibited"` attribute is now excluded from the duplicate-name and single-ID counts, since a prohibited use is a removal marker, not a member of `{attribute uses}` (XSD §3.4.2/§3.6.2); this also keeps a self-reference that re-imports an attribute and then prohibits it from reading as a duplicate. Catches attQ011 and attQ017 (an attribute group referencing both a global attribute and a redefined self-referencing group that supplies the same-named attribute). XSTS invalid-schemas-accepted 22 → 20, valid-schemas-rejected held at 0. - A redefinition of an `attributeGroup` that SELF-references the original and also directly re-declares one of its attributes is now rejected. The self-reference re-imports all of the original's attribute uses, so a same-named direct re-declaration produces two attribute uses of the same name, a duplicate forbidden by ag-props-correct.2. The check fires only for a re-declared, non-prohibited attribute the original directly declares, and only when both the original and the re-declaration land in no namespace (a qualified attribute's target namespace depends on cross-document `form`/chameleon resolution, so it is not compared, staying lenient); a self-referencing redefinition that adds genuinely new attributes (attgC007/attgC038/groupB018) stays valid. Catches schM8 (`c-g2` self-references and re-declares `a1`). XSTS invalid-schemas-accepted 23 → 22 (schM8), valid-schemas-rejected held at 0. diff --git a/Sources/Schema/XSDDerivationTables.swift b/Sources/Schema/XSDDerivationTables.swift index 6d753b3..2510ce5 100644 --- a/Sources/Schema/XSDDerivationTables.swift +++ b/Sources/Schema/XSDDerivationTables.swift @@ -102,9 +102,27 @@ extension PureXML.Schema.XSDParser { if let type = XSDDerivNode.attribute(element, "type") { tables.elementTypeNames[name] = XSDDerivNode.stripPrefix(type) tables.nsElementTypeNames[key] = resolvedKey(of: type, on: element, default: namespace) + } else { + recordInlineElementDerivation(element, key: key, in: namespace, into: &tables) } } + /// Records the derivation of an element's INLINE complex type, which carries no + /// `type` name and so is absent from the named backbone. The entry is keyed by a + /// synthetic key (the element key plus a `#` marker no QName can contain, so it + /// cannot collide with a real type and is unreachable as anyone else's derivation + /// base) so a substitution-group `block` on the head can drop a member whose inline + /// type reaches the head's type by a blocked method (cos-equiv-derived-ok-rec; XSTS + /// disallowedSubst00105m). A plain inline type with no derivation records nothing. + private static func recordInlineElementDerivation(_ element: XSDTree, key: String, in namespace: String?, into tables: inout DerivationTables) { + guard let inlineType = XSDDerivNode.firstChild(element, named: "complexType"), + let derivation = namespacedDerivationInfo(inlineType, in: namespace) + else { return } + let inlineKey = key + "#inline" + tables.nsElementTypeNames[key] = inlineKey + tables.nsTypeDerivation[inlineKey] = derivation + } + /// The namespace a local element's name takes at instance level: the target /// namespace when the element is qualified (its own `form`, else the schema's /// `elementFormDefault`), otherwise no namespace. diff --git a/Tests/SchemaSubstitutionTypeTests.swift b/Tests/SchemaSubstitutionTypeTests.swift index 79b9ab5..2c1b81c 100644 --- a/Tests/SchemaSubstitutionTypeTests.swift +++ b/Tests/SchemaSubstitutionTypeTests.swift @@ -9,6 +9,23 @@ struct SchemaSubstitutionTypeTests { (try? PureXML.Schema.Document("\(body)")) != nil } + @Test("a head's block=extension forbids substituting an extension-derived member, even an inline type (disallowedSubst00105m)") + func test_blockExtensionForbidsSubstitution() throws { + let schema = "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + let doc = try PureXML.Schema.Document(schema) + // Member3 derives by extension, which Head blocks: the substitution is forbidden. + #expect(try !doc.validate("en").isEmpty) + // The head itself, and a restriction-derived member, remain valid substitutes. + #expect(try doc.validate("e").isEmpty) + } + @Test("A member whose type is unrelated to the head's type is rejected") func test_unrelatedMemberType() { #expect(!compiles( diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift index 64801ee..8aaac2e 100644 --- a/Tests/XSTSSuiteTests.swift +++ b/Tests/XSTSSuiteTests.swift @@ -150,13 +150,13 @@ struct XSTSSuiteTests { private let knownSchemaValidRejected = 0 private let knownSchemaInvalidAccepted = 20 private let knownInstanceValidRejected = 0 - /// Instance bucket (133 -> 25), per-step deltas in CHANGELOG.md: xsi:type must derive from the - /// declared type (via the element-ref chain); anyType cannot stand in for anySimpleType; an untyped - /// substitutionGroup member inherits its head's type; a non-nillable element carries no xsi:nil; block/ - /// blockDefault bar substitutions; ur-type anyType wildcards are `lax`; identity fields compare in - /// decimal value space, taking a default/fixed when absent/empty; an attribute wildcard is the - /// INTERSECTION of its sources; year 0000 is invalid; an identity field must be simple-content (idK012). - private let knownInstanceInvalidAccepted = 25 + /// Instance bucket (133 -> 23), per-step deltas in CHANGELOG.md: xsi:type must derive from the declared + /// type; anyType cannot stand in for anySimpleType; an untyped substitutionGroup member inherits its + /// head's type; a non-nillable element carries no xsi:nil; block/blockDefault bar substitutions (incl. an + /// inline-typed member reached by a blocked method, disallowedSubst00105m); ur-type anyType wildcards are + /// `lax`; identity fields compare in decimal value space, with default/fixed when absent, and must be + /// simple-content (idK012); an attribute wildcard is the INTERSECTION of its sources; year 0000 is invalid. + private let knownInstanceInvalidAccepted = 23 @Test("Every XSTS case behaves: compile, reject, validate, invalidate") func test_suite() throws {