PureXML/Tests/SchemaBlockDefaultTests.swift
Mihaela Mihaljevic 1fe670581f feat(schema): key the instance derivation/substitution backbone by namespaced identity; re-land blockDefault; invalid-instances-accepted 89 to 81 (#186, #147)
The instance-validity derivation and substitution tables were keyed by bare
local name, so two same-local-name types or elements in different namespaces
collided. In multi-document validation this let one schema's block/derivation
facts shadow another's, and it blocked honouring blockDefault (whose re-land
widened that collision into an over-rejection).

Key the instance subsystem by namespaced identity {ns}local instead. The shared
DerivationTables keeps its bare-keyed fields for the schema-validity machinery
(Particle-Valid-Restriction and the consistency rules, which resolve QNames to
bare names and walk the same backbone) and gains parallel ns* fields built in
the same pass, resolving each container's components through its target
namespace and prefix bindings. Only the instance consumers
(blockedSubstitutionError, notDerivedSubstitutionError, abstractTypeError, the
substitution-group closure, and referenceParticle) read the ns* tables, via a
derivationKey(fromReference:) that reduces a type:{ns}local reference, a bare
name, or an element ref to a {ns}local backbone key. A key miss stays permissive
(no constraint, substitution allowed), so the re-keying cannot over-reject.

With the namespaced backbone in place, blockDefault is re-landed FP-free: a
schema's blockDefault supplies the {disallowed substitutions} of every element
and complex type without an explicit block (own block overrides; block="" clears;
intersected with the methods each component admits), so blockDefault="extension"
bars an extension substitution and "#all" bars all, without one namespace's
defaults shadowing another's.

The pure re-keying preserves single-document behaviour exactly (the corpus is
overwhelmingly single-namespace, where {ns}local and bare coincide): all four
XSTS buckets held at vsr=0 / isa=69 / vir=0 / ivi=89 after re-keying alone.
blockDefault then drove invalid-instances-accepted 89 to 81, no false positive
(valid-instances-rejected and valid-schemas-rejected held at 0), no other bucket
moved. An adversarial review exercised same- and cross-namespace substitutions,
no-targetNamespace keys, simple-type restriction, transitive substitution groups,
and blockDefault override/over-block cases, and the prior multi-document
cross-namespace blockDefault over-rejection is now accepted while a same-namespace
blocked substitution is still rejected (precise, not merely permissive).
2026-06-18 12:41:18 +02:00

76 lines
3.7 KiB
Swift

@testable import PureXML
import Testing
/// A schema `blockDefault` supplies the `{disallowed substitutions}` of an element
/// and the `{prohibited substitutions}` of a complex type that state no `block` of
/// their own, blocking an `xsi:type` substitution by a listed derivation method
/// (XSD 1.0 Structures section 3.3.2 / 3.4.2; cvc-elt.4.3.2.1). An explicit
/// `block=""` overrides the default back to acceptance.
@Suite("schema blockDefault on xsi:type substitution")
struct SchemaBlockDefaultTests {
/// A schema with the given `blockDefault` plus optional explicit `block`
/// overrides on the element and its declared type. `ShapeT` is the declared
/// type; `CircleT` extends it and `PointT` restricts it.
private func schema(blockDefault: String, elementBlock: String? = nil, typeBlock: String? = nil) -> String {
let defaultAttribute = blockDefault.isEmpty ? "" : " blockDefault=\"\(blockDefault)\""
let elementAttribute = elementBlock.map { " block=\"\($0)\"" } ?? ""
let typeAttribute = typeBlock.map { " block=\"\($0)\"" } ?? ""
return """
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"\(defaultAttribute)>
<xs:complexType name="ShapeT"\(typeAttribute)>
<xs:sequence><xs:element name="name" type="xs:string"/></xs:sequence>
</xs:complexType>
<xs:complexType name="CircleT">
<xs:complexContent>
<xs:extension base="ShapeT">
<xs:sequence><xs:element name="radius" type="xs:integer"/></xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="PointT">
<xs:complexContent>
<xs:restriction base="ShapeT">
<xs:sequence><xs:element name="name" type="xs:string"/></xs:sequence>
</xs:restriction>
</xs:complexContent>
</xs:complexType>
<xs:element name="s" type="ShapeT"\(elementAttribute)/>
</xs:schema>
"""
}
private let extensionInstance =
"<s xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CircleT\"><name>a</name><radius>1</radius></s>"
private let restrictionInstance =
"<s xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"PointT\"><name>a</name></s>"
private func validate(_ xsd: String, _ instance: String) throws -> [PureXML.Validation.ValidationError] {
try PureXML.Schema.Document(xsd).validate(instance)
}
@Test("blockDefault=extension blocks an extension xsi:type")
func test_blockDefaultBlocksExtension() throws {
#expect(try !validate(schema(blockDefault: "extension"), extensionInstance).isEmpty)
}
@Test("blockDefault=extension still allows a restriction xsi:type")
func test_blockDefaultAllowsRestriction() throws {
#expect(try validate(schema(blockDefault: "extension"), restrictionInstance).isEmpty)
}
@Test("blockDefault=#all blocks both extension and restriction")
func test_blockDefaultAllBlocksBoth() throws {
#expect(try !validate(schema(blockDefault: "#all"), extensionInstance).isEmpty)
#expect(try !validate(schema(blockDefault: "#all"), restrictionInstance).isEmpty)
}
@Test("explicit block=\"\" on the element and its type overrides blockDefault back to acceptance")
func test_emptyBlockOverridesDefault() throws {
// cvc-elt.4.3.2.1 unions the element's {disallowed substitutions} with the
// declared type's {prohibited substitutions}, so both must be cleared.
let xsd = schema(blockDefault: "#all", elementBlock: "", typeBlock: "")
#expect(try validate(xsd, extensionInstance).isEmpty)
#expect(try validate(xsd, restrictionInstance).isEmpty)
}
}