diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc6af39..af25603 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
+- XSTS invalid-instances-accepted (#147): a `block` on a LOCAL element declaration is now keyed by the element's instance (qualified) name in the derivation backbone, so an xsi:type substitution blocked on an unqualified local element is barred. The namespaced backbone keyed every element by the target namespace, but a local element is in the target namespace only when qualified (its own `form`, else the schema's elementFormDefault); an unqualified local element's block went unfound (typeDef00801). invalid-instances-accepted 32 → 31, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
+
- XSTS invalid-instances-accepted (#147): an instance `xsi:type` may not name an abstract type. An abstract type cannot be the type of an instance element (cvc-elt.4.3.2), so it is invalid as a substitution even when the declared type would otherwise admit it (typeDef01101); a concrete type is unaffected. This complements the existing check for an abstract DECLARED type with no `xsi:type`. invalid-instances-accepted 34 → 32, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
- XSTS invalid-instances-accepted (#147): an element with a `fixed` value constraint and a mixed content type must carry its fixed character value with no element children (cvc-elt.5.2.2.2.1). The fixed check validated only the concatenated text, so a mixed `fixed` element whose text matched but which also contained child elements (ctZ013) was accepted; element children are now rejected. An empty element (which takes the fixed value) and text equal to the fixed value remain valid. invalid-instances-accepted 37 → 34, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
diff --git a/Sources/Schema/XSDDerivationTables.swift b/Sources/Schema/XSDDerivationTables.swift
index e5e86e1..6d753b3 100644
--- a/Sources/Schema/XSDDerivationTables.swift
+++ b/Sources/Schema/XSDDerivationTables.swift
@@ -81,28 +81,43 @@ extension PureXML.Schema.XSDParser {
private static func gatherElement(_ element: XSDTree, in namespace: String?, into tables: inout DerivationTables) {
guard let name = XSDDerivNode.attribute(element, "name") else { return }
- let key = derivationKey(name, in: namespace)
+ let isGlobal = element.parent.map {
+ $0.name?.namespaceURI == xsdNamespace && XSDDerivNode.localName($0) == "schema"
+ } ?? false
+ // A global element is always in the target namespace; a local element is in
+ // it only when qualified (its `form`, else the schema's elementFormDefault),
+ // so its namespaced key matches the instance element's qualified name.
+ let elementNamespace = isGlobal ? namespace : localElementNamespace(element, in: namespace)
+ let key = derivationKey(name, in: elementNamespace)
if XSDDerivNode.attribute(element, "abstract") == "true" { tables.abstractElements.insert(name) }
let block = blockMethods(of: element, admitting: [.extension, .restriction, .substitution])
if !block.isEmpty {
tables.elementBlock[name] = block
tables.nsElementBlock[key] = block
}
-
- let isGlobal = element.parent.map {
- $0.name?.namespaceURI == xsdNamespace && XSDDerivNode.localName($0) == "schema"
- } ?? false
if isGlobal {
let finalSet = finalMethods(of: element)
if !finalSet.isEmpty { tables.elementFinal[name] = finalSet }
}
-
if let type = XSDDerivNode.attribute(element, "type") {
tables.elementTypeNames[name] = XSDDerivNode.stripPrefix(type)
tables.nsElementTypeNames[key] = resolvedKey(of: type, on: element, default: namespace)
}
}
+ /// 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.
+ private static func localElementNamespace(_ element: XSDTree, in namespace: String?) -> String? {
+ switch XSDDerivNode.attribute(element, "form") {
+ case "qualified": return namespace
+ case "unqualified": return nil
+ default:
+ let schema = XSDDerivNode.schemaOwner(element)
+ return XSDDerivNode.attribute(schema, "elementFormDefault") == "qualified" ? namespace : nil
+ }
+ }
+
/// The namespaced (`{ns}local`-based) derivation backbone of a complex type:
/// the same `complexContent`/`simpleContent` extension/restriction base as
/// ``derivationInfo(_:)``, but with the base resolved to a namespaced key.
diff --git a/Tests/SchemaLocalElementBlockKeyTests.swift b/Tests/SchemaLocalElementBlockKeyTests.swift
new file mode 100644
index 0000000..2532c04
--- /dev/null
+++ b/Tests/SchemaLocalElementBlockKeyTests.swift
@@ -0,0 +1,27 @@
+@testable import PureXML
+import Testing
+
+/// A `block` on a LOCAL element declaration applies to that element by its instance
+/// (qualified) name. When the schema is unqualified (no elementFormDefault), the
+/// local element is in no namespace, so the block must be keyed there for an
+/// xsi:type substitution on it to be barred (W3C sun typeDef00801).
+@Suite("local element block keying")
+struct SchemaLocalElementBlockKeyTests {
+ @Test("block on an unqualified local element bars an xsi:type extension")
+ func test_unqualifiedLocalElementBlock() throws {
+ let schema = try PureXML.Schema.Document("""
+
+
+
+
+
+
+
+
+
+ """)
+ // Element is unqualified (no elementFormDefault): in instance it is in no namespace.
+ let doc = #""#
+ #expect(try !schema.validate(doc).isEmpty)
+ }
+}
diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift
index ccdc350..49594ea 100644
--- a/Tests/XSTSSuiteTests.swift
+++ b/Tests/XSTSSuiteTests.swift
@@ -150,13 +150,13 @@ struct XSTSSuiteTests {
private let knownSchemaValidRejected = 0
private let knownSchemaInvalidAccepted = 69
private let knownInstanceValidRejected = 0
- /// Instance bucket (133 -> 32), per-step deltas in CHANGELOG.md: xsi:type must derive from the
+ /// Instance bucket (133 -> 31), per-step deltas in CHANGELOG.md: xsi:type must derive from the
/// declared type (complex, then atomic and list/union, resolved through the element-ref chain);
/// block/blockDefault (incl. "substitution" and an untyped head) bar substitutions after the
/// derivation backbone was re-keyed by namespace; ur-type `xsd:anyType` wildcards are `lax`;
/// identity fields compare in the decimal value space and take a declared default/fixed when
/// absent or empty; a complex type's attribute wildcard is the INTERSECTION of its sources.
- private let knownInstanceInvalidAccepted = 32
+ private let knownInstanceInvalidAccepted = 31
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
func test_suite() throws {