fix(schema): flag a baseless or ancestor complex xsi:type as not derived; invalid-instances-accepted 60 to 53 (#181, #147)
A complex type that records no base of its own derives only from xsd:anyType, so it cannot validly substitute (cvc-elt.4.3.2.1) for an element whose declared type is a more specific complex type. The not-derived check required the substitute to be present in the derivation backbone, so naming the declared type's own complex ancestor, or an unrelated baseless complex type, via xsi:type was accepted. notDerivedSubstitutionError now treats a complex substitute as in scope even when it is absent from the backbone: its non-derivation from a non-ur declared type is certain (a baseless complex type derives only from anyType, and any real extension/restriction chain is recorded in the backbone, where derivationMethods finds it). An atomic substitute must still be in the backbone, so a built-in stays silent. The declared guard excludes the ur-types, so a baseless complex substitute under an anyType declaration is admitted; a genuine extension or restriction substitute is accepted. invalid-instances-accepted 60 to 53, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved. An adversarial review exercised deep, cross-namespace, redefine, and element-ref-path derivation chains plus anyType-declared baseless substitutes, and found no over-rejection.
This commit is contained in:
parent
31a4ebacfb
commit
b3a2bcfe6a
@ -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): the `xsi:type` not-derived check now also flags a complex substitute that records no base of its own. Such a type derives only from `anyType`, so it cannot validly substitute for a non-ur complex declared type; naming the declared type's own complex ancestor, or an unrelated baseless complex type, is rejected. A complex substitute is in scope even when absent from the derivation backbone (its non-derivation from a non-ur type is certain), while an atomic substitute must still be in the backbone so a built-in stays silent. A genuine extension/restriction substitute is accepted, and a baseless complex substitute under an `anyType` declaration is admitted. invalid-instances-accepted 60 → 53, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
- XSTS invalid-instances-accepted (#181, #147): the `xsi:type` not-derived check (`cvc-elt.4.3.2.1`) now covers ATOMIC simple substitutes, not only complex ones. An `xsi:type` naming an atomic type that is not derived from the element's declared type, such as a sibling restriction or the declared type's own ancestor, is rejected. The check still fires only when both the substitute and the declared type are recorded in the namespaced derivation backbone with no path between them, so a built-in substitute or declared type (absent from the backbone) and the ur-types stay silent (under-reject), avoiding any built-in-derivation false positive; a genuinely derived restriction substitute is accepted. invalid-instances-accepted 66 → 60, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
- XSTS invalid-instances-accepted (#181, #147): an `xsi:type` naming a list or union simple type is now rejected unless the element's declared type is a ur-type (`anySimpleType`/`anyType`). A list or union type derives only from `anySimpleType` (XSD Part 2), so it cannot validly substitute for an element of any more specific atomic or complex type (`cvc-elt.4.3.2.1`); naming one on, say, an `xs:int`-declared element is invalid. A genuinely derived (restriction) substitute, a list/union under an `anySimpleType` declaration, and a declared type that is itself a list or union are all left alone, so no valid instance is newly rejected. The declared type is resolved through the element-ref / type-reference chain, so the check also fires for an element reached via `<xs:element ref>` whose global declaration is atomic. This widening became safe once the derivation backbone was keyed by namespaced identity (#186); the conservative atomic/built-in not-derived cases remain under-rejected pending the full type-identity relation. invalid-instances-accepted 81 → 66, no false positive (valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket moved.
|
||||
|
||||
@ -98,6 +98,12 @@ extension PureXML.Schema.ComplexValidator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an element type resolves to a complex type.
|
||||
static func isComplexType(_ type: PureXML.Schema.ElementType) -> Bool {
|
||||
if case .complex = type { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@ -174,8 +180,14 @@ extension PureXML.Schema.ComplexValidator {
|
||||
else { return nil }
|
||||
let substituteKey = Self.derivationKey(fromReference: substituteRef)
|
||||
let declaredName = resolvedDeclaredTypeName(reference)
|
||||
// A complex substitute is in scope even when it records no base of its own:
|
||||
// a baseless complex type derives only from `anyType`, so it cannot derive
|
||||
// from the declared (non-ur) type and is correctly flagged. An atomic
|
||||
// substitute must be in the backbone, so a built-in (which derives by the
|
||||
// primitive hierarchy the backbone does not record) stays silent.
|
||||
let substituteInScope = Self.isComplexType(substituteType) || typeDerivation[substituteKey] != nil
|
||||
guard substituteKey != declaredName,
|
||||
typeDerivation[substituteKey] != nil,
|
||||
substituteInScope,
|
||||
typeDerivation[declaredName] != nil || isComplexBackboneRoot(declaredName),
|
||||
PureXML.Schema.XSDParser.derivationMethods(from: substituteKey, to: declaredName, typeDerivation) == nil
|
||||
else { return nil }
|
||||
|
||||
@ -83,4 +83,34 @@ struct SchemaListUnionXsiTypeTests {
|
||||
// A genuine restriction of A is validly derived: accepted.
|
||||
#expect(try schema.validate(doc("Derived")).isEmpty)
|
||||
}
|
||||
|
||||
/// A complex substitute that records no base of its own derives only from
|
||||
/// `anyType`, so it cannot validly substitute for a non-ur complex declared
|
||||
/// type: naming the declared type's complex ancestor, or an unrelated baseless
|
||||
/// complex type, is rejected; a genuine extension/restriction is accepted.
|
||||
@Test("a baseless or ancestor complex xsi:type is rejected, a derived one accepted")
|
||||
func test_baselessComplexNotDerivedRejected() throws {
|
||||
let schema = try PureXML.Schema.Document("""
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="e" type="Dr"/>
|
||||
<xs:complexType name="B"><xs:sequence><xs:element name="f" type="xs:string"/></xs:sequence></xs:complexType>
|
||||
<xs:complexType name="Dr"><xs:complexContent><xs:restriction base="B">
|
||||
<xs:sequence><xs:element name="f" type="xs:string"/></xs:sequence>
|
||||
</xs:restriction></xs:complexContent></xs:complexType>
|
||||
<xs:complexType name="Drr"><xs:complexContent><xs:restriction base="Dr">
|
||||
<xs:sequence><xs:element name="f" type="xs:string"/></xs:sequence>
|
||||
</xs:restriction></xs:complexContent></xs:complexType>
|
||||
<xs:complexType name="Other"/>
|
||||
</xs:schema>
|
||||
""")
|
||||
func doc(_ type: String) -> String {
|
||||
#"<e xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="\#(type)"><f>x</f></e>"#
|
||||
}
|
||||
// B is Dr's ancestor (Dr restricts B), not derived from Dr: rejected.
|
||||
#expect(try !schema.validate(doc("B")).isEmpty)
|
||||
// Other is an unrelated baseless complex type: rejected.
|
||||
#expect(try !schema.validate(#"<e xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="Other"/>"#).isEmpty)
|
||||
// Drr restricts Dr: validly derived, accepted.
|
||||
#expect(try schema.validate(doc("Drr")).isEmpty)
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,13 +150,13 @@ struct XSTSSuiteTests {
|
||||
private let knownSchemaValidRejected = 0
|
||||
private let knownSchemaInvalidAccepted = 69
|
||||
private let knownInstanceValidRejected = 0
|
||||
/// Instance bucket (133 -> 60), per-step deltas in CHANGELOG.md: xsi:type must derive from the
|
||||
/// Instance bucket (133 -> 53), 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; a complex type's attribute wildcard is the
|
||||
/// INTERSECTION of its `anyAttribute` sources.
|
||||
private let knownInstanceInvalidAccepted = 60
|
||||
private let knownInstanceInvalidAccepted = 53
|
||||
|
||||
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
|
||||
func test_suite() throws {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user