diff --git a/CHANGELOG.md b/CHANGELOG.md
index e21a396..1a44982 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 (#181, #147): an instance `xsi:type` naming a complex type must be derived from the element's declared type (`cvc-elt.4.3.2.1`). A complex `xsi:type` on a different branch of the type hierarchy (not derived from the declared type) is now rejected, where previously only a derived-but-blocked substitution was caught. Conservatively scoped to substitutes that resolve to a complex type recorded in the derivation backbone, so simple/list/union substitutes and unresolvable references are left alone (no over-rejection). invalid-instances-accepted 133 → 107, no false positive (valid-instances-rejected held at 0, valid-schemas-rejected held at 0), no other bucket moved. Disclosed bound: the derivation backbone is keyed by bare local name (shared with the existing block-substitution rule), so a cross-namespace same-local-name collision could in principle be misjudged; the corpus exercises none, and the holistic fix is to key the backbone by namespaced type identity.
+
- XSTS invalid-schemas-accepted (#145): an `xs:include` and `xs:redefine` require a `schemaLocation` attribute (only `xs:import`'s is optional); one declared without it is rejected. invalid-schemas-accepted 71 → 69, no false positive (valid-schemas-rejected held at 0; a required-attribute check with no name resolution cannot over-reject), no other bucket moved.
- XSTS invalid-schemas-accepted (#161): a schema referenced by `xs:redefine` must have the same `targetNamespace` as the redefining schema, or none (chameleon), exactly as for `xs:include`; redefining a different-namespace schema is rejected. The existing include-namespace-matching check now also covers `redefine`, selecting the loaded redefined schema document (not the redefine wrapper) so its real target namespace is compared. invalid-schemas-accepted 74 → 71, no false positive (valid-schemas-rejected held at 0; an adversarial review confirmed same-namespace, chameleon, and no-namespace redefines are not flagged), no other bucket moved.
diff --git a/Sources/Schema/ComplexValidator.swift b/Sources/Schema/ComplexValidator.swift
index 8c5de5e..94ca6cd 100644
--- a/Sources/Schema/ComplexValidator.swift
+++ b/Sources/Schema/ComplexValidator.swift
@@ -273,12 +273,8 @@ public extension PureXML.Schema {
// reference so re-entry does not re-read the same xsi:type. A `block` on
// the declared type can forbid the substitution; an abstract declared
// type requires one.
- if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path) {
- errors.append(blocked)
- return
- }
- if case let .typeReference(name) = declared, let missing = abstractTypeError(named: name, child: child, at: path) {
- errors.append(missing)
+ if let xsiError = xsiTypeOverrideError(declared: declared, child: child, at: path) {
+ errors.append(xsiError)
return
}
guard let declared = overriddenType(declared, for: child, namespaceBindings: namespaceBindings, at: path, into: &errors) else { return }
diff --git a/Sources/Schema/ComplexValidatorDerivation.swift b/Sources/Schema/ComplexValidatorDerivation.swift
index 095f7fc..132713b 100644
--- a/Sources/Schema/ComplexValidatorDerivation.swift
+++ b/Sources/Schema/ComplexValidatorDerivation.swift
@@ -1,4 +1,18 @@
extension PureXML.Schema.ComplexValidator {
+ /// The first `xsi:type` override error for `child`, if any: a substitution
+ /// blocked by `block`, a substitute not validly derived from the declared type
+ /// (cvc-elt.4.3.2.1), or a missing `xsi:type` on an abstract declared type.
+ func xsiTypeOverrideError(
+ declared: PureXML.Schema.ElementType,
+ child: PureXML.Model.Element,
+ at path: [PureXML.Validation.PathKey],
+ ) -> PureXML.Validation.ValidationError? {
+ if let blocked = blockedSubstitutionError(declared: declared, child: child, at: path) { return blocked }
+ if let notDerived = notDerivedSubstitutionError(declared: declared, child: child, at: path) { return notDerived }
+ if case let .typeReference(name) = declared { return abstractTypeError(named: name, child: child, at: path) }
+ return nil
+ }
+
/// The error when an element's declared type is an abstract complex type and
/// the element supplies no `xsi:type` to name a concrete derived type. An
/// abstract type cannot itself be the type of an instance element.
@@ -61,4 +75,68 @@ extension PureXML.Schema.ComplexValidator {
at: path,
)
}
+
+ /// cvc-elt.4.3.2.1: an `xsi:type` must resolve to a type DERIVED from the
+ /// element's declared type. This flags ONLY the case where both the declared
+ /// type and the substitute resolve to COMPLEX types recorded in the derivation
+ /// backbone and there is no derivation path between them (a different branch of
+ /// the hierarchy). It deliberately does NOT touch a simple/list/union substitute,
+ /// a substitute or declared type absent from the backbone, or the ur-types: there
+ /// the relationship cannot be confirmed from the complex-type backbone alone, so
+ /// the rule stays silent (under-reject) rather than risk a false positive.
+ ///
+ /// Disclosed bound: the derivation backbone (`typeDerivation`) is keyed by bare
+ /// local name, shared with `blockedSubstitutionError`. Two imported complex types
+ /// with the same local name in different namespaces collide in one slot, so a
+ /// cross-namespace substitution could in principle be misjudged. This is a
+ /// pre-existing, subsystem-wide limitation (not introduced here); the holistic
+ /// fix is to key the backbone by namespaced type identity and resolve `xsi:type`
+ /// through `xsiTypeReference`. The XSTS corpus exercises no such collision.
+ func notDerivedSubstitutionError(
+ declared: PureXML.Schema.ElementType,
+ child: PureXML.Model.Element,
+ at path: [PureXML.Validation.PathKey],
+ ) -> PureXML.Validation.ValidationError? {
+ guard case let .typeReference(reference) = declared,
+ let substitute = Self.xsiTypeName(child),
+ case .complex? = Self.resolveNamedType(substitute, in: types)
+ else { return nil }
+ let substituteName = PureXML.Schema.XSDParser.bareTypeLocalName(substitute)
+ let declaredName = resolvedDeclaredTypeName(reference)
+ guard substituteName != declaredName,
+ typeDerivation[substituteName] != nil,
+ typeDerivation[declaredName] != nil || isComplexBackboneRoot(declaredName),
+ PureXML.Schema.XSDParser.derivationMethods(from: substituteName, to: declaredName, typeDerivation) == nil
+ else { return nil }
+ return PureXML.Validation.ValidationError(
+ reason: "xsi:type '\(substitute)' is not validly derived from the declared type '\(declaredName)' of '\(child.name.localName)'",
+ at: path,
+ )
+ }
+
+ /// A complex type that heads a derivation backbone (some recorded type derives
+ /// from it) even though it records no base of its own, so it is a legitimate
+ /// declared-type target for the not-derived check.
+ private func isComplexBackboneRoot(_ name: String) -> Bool {
+ guard name != "anyType", name != "anySimpleType" else { return false }
+ return typeDerivation.values.contains { $0.base == name }
+ }
+
+ /// Follows an element-ref / type-reference chain (`element:foo` -> concrete type)
+ /// to the declared type's bare local name, which the derivation backbone is
+ /// keyed by.
+ private func resolvedDeclaredTypeName(_ reference: String) -> String {
+ var key = reference
+ var name = PureXML.Schema.XSDParser.bareTypeLocalName(reference)
+ var steps = 0
+ while steps <= types.count {
+ guard let resolved = types[key] ?? Self.resolveNamedType(key, in: types),
+ case let .typeReference(next) = resolved
+ else { break }
+ key = next
+ name = PureXML.Schema.XSDParser.bareTypeLocalName(next)
+ steps += 1
+ }
+ return name
+ }
}
diff --git a/Tests/SchemaSubstitutionBlockTests.swift b/Tests/SchemaSubstitutionBlockTests.swift
index 06bca8c..4bbe6a0 100644
--- a/Tests/SchemaSubstitutionBlockTests.swift
+++ b/Tests/SchemaSubstitutionBlockTests.swift
@@ -39,4 +39,28 @@ struct SchemaSubstitutionBlockTests {
func test_memberAllowedWithoutBlock() throws {
#expect(try schema(headBlock: "").validate(withMember).isEmpty)
}
+
+ /// cvc-elt.4.3.2.1: an xsi:type naming a complex type must be derived from the
+ /// element's declared type. A complex type on a different branch of the
+ /// hierarchy is rejected; the declared type itself is valid.
+ @Test("an xsi:type must be derived from the element's declared type")
+ func test_xsiTypeMustDeriveFromDeclared() throws {
+ let schema = try PureXML.Schema.Document("""
+
+
+
+
+
+
+
+
+ """)
+ func doc(_ type: String) -> String {
+ #"x"#
+ }
+ // Dr (restriction of B) is on a different branch, not derived from De: invalid.
+ #expect(try !schema.validate(doc("Dr")).isEmpty)
+ // The declared type itself is valid.
+ #expect(try schema.validate(doc("De")).isEmpty)
+ }
}
diff --git a/Tests/XSTSSuiteTests.swift b/Tests/XSTSSuiteTests.swift
index 26f9be8..cd018dd 100644
--- a/Tests/XSTSSuiteTests.swift
+++ b/Tests/XSTSSuiteTests.swift
@@ -150,7 +150,11 @@ struct XSTSSuiteTests {
private let knownSchemaValidRejected = 0
private let knownSchemaInvalidAccepted = 69
private let knownInstanceValidRejected = 0
- private let knownInstanceInvalidAccepted = 133
+ /// Instance bucket: an xsi:type naming a complex type must be derived from the
+ /// element's declared type (cvc-elt.4.3.2.1); a non-derived complex substitute is
+ /// rejected, conservatively scoped to the complex-type derivation backbone so no
+ /// valid instance is over-rejected. 133 -> 107.
+ private let knownInstanceInvalidAccepted = 107
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
func test_suite() throws {