refactor(schema): redefine self-derivation as a Validation; drop the redundant all-group throw (#161)

Continues incorporating every schema-validity check into the Validator framework
(Rules/universal/validation-rules.md) rather than ad-hoc parse-pipeline throws.

- checkRedefine (src-redefine.5: a redefined type restricts or extends itself)
  becomes the pre-compile Validation redefinitionsDeriveFromThemselves;
  redefineDerivationErrors returns findings instead of throwing.

- checkAllGroups is removed entirely: every check it made is already produced by
  Validations -- "a member must be an element" by schemaStructureValid
  (allowedChildren[all]), and the occurrence limits by allGroupLimitedErrors
  (cos-all-limited.2). Verified redundant and not load-bearing: an empirical
  probe with the calls removed left invalid-schemas-accepted at 81 unchanged,
  the fuzz suite's "compile and validation never crash or hang" still passes
  (the NFA builder's own guards hold), and false positives stayed 0.

compileRule is now internal so composition Validations can live in their own
file (SchemaCompositionValidations.swift). Both new pre-compile rules are pinned
in ValidationConfigurationTests. The pre-existing public SchemaError.invalidAllGroup
case is kept (released API) though no longer thrown. No bucket change
(invalid-schemas-accepted 81, valid-schemas-rejected 0); full suite + fuzz pass.
This commit is contained in:
Mihaela Mihaljevic 2026-06-17 23:49:30 +02:00
parent d3a0b60d67
commit 4def6a2c07
6 changed files with 46 additions and 64 deletions

View File

@ -243,11 +243,11 @@ extension PureXML.Validation {
static func preCompileValidator() -> Validator<PureXML.Schema.SchemaCompileContext> {
Validator<PureXML.Schema.SchemaCompileContext>.defaults(
nonReference: [
AnyValidation(idAttributesValid),
AnyValidation(schemaStructureValid),
AnyValidation(idAttributesValid), AnyValidation(schemaStructureValid),
AnyValidation(componentNamesUnique),
AnyValidation(simpleTypeFinalControlsValid),
AnyValidation(contentModelsDeterministic), AnyValidation(referencedSchemasResolveToSchemas),
AnyValidation(contentModelsDeterministic),
AnyValidation(referencedSchemasResolveToSchemas), AnyValidation(redefinitionsDeriveFromThemselves),
],
reference: [
AnyValidation(typeDerivationAcyclic),
@ -320,7 +320,7 @@ extension PureXML.Validation {
))
}
private static func compileRule(
static func compileRule(
_ description: String,
_ findings: @escaping (PureXML.Schema.SchemaCompileContext) -> [PureXML.Schema.SchemaLocatedFinding],
) -> Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
@ -341,17 +341,6 @@ extension PureXML.Validation {
}
extension PureXML.Validation.SchemaCompile {
/// src-resolve: a resolved include/import/redefine schemaLocation must be a schema.
static var referencedSchemasResolveToSchemas: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
compileRule("A resolved schemaLocation reference is a valid schema") { document in
PureXML.Schema.SchemaLocatedFinding.unlocated(
document.context.failedSchemaReferences.map {
"the referenced schema document '\($0)' is not a valid schema"
},
)
}
}
/// au-props-correct: an attribute's type is a simple type, never complex.
static var attributeTypesSimple: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
compileRule("An attribute's type is a simple type") { document in

View File

@ -0,0 +1,27 @@
extension PureXML.Validation.SchemaCompile {
/// src-resolve: a resolved `include`/`import`/`redefine` `schemaLocation` (one the
/// loader returned content for) must be a well-formed schema document. The
/// failures are recorded on the context during composition; an unresolved
/// location (the loader returned nothing) is not recorded, since resolution is
/// the processor's choice.
static var referencedSchemasResolveToSchemas: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
compileRule("A resolved schemaLocation reference is a valid schema") { document in
PureXML.Schema.SchemaLocatedFinding.unlocated(
document.context.failedSchemaReferences.map {
"the referenced schema document '\($0)' is not a valid schema"
},
)
}
}
/// src-redefine.5: every type inside `xs:redefine` must restrict or extend the
/// type it redefines (same local name, in the redefining schema's own target
/// namespace).
static var redefinitionsDeriveFromThemselves: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
compileRule("A redefined type restricts or extends itself") { document in
PureXML.Schema.SchemaLocatedFinding.unlocated(
PureXML.Schema.XSDParser.redefineDerivationErrors(document.containers),
)
}
}
}

View File

@ -300,7 +300,12 @@ extension PureXML.Schema.XSDParser {
/// Throws when a type inside an `xs:redefine` does not derive from itself: a
/// redefinition's `base` must name the type being redefined.
static func checkRedefine(_ containers: [XSDTree]) throws {
/// src-redefine.5: every type inside `xs:redefine` must restrict or extend the
/// type it redefines (same local name, in the redefining schema's own target
/// namespace). Returns one finding per offending type, for a validation to
/// report; it does not throw, so the finding flows through the validator.
static func redefineDerivationErrors(_ containers: [XSDTree]) -> [String] {
var errors: [String] = []
for container in containers where XSDDerivNode.localName(container) == "redefine" {
for type in XSDDerivNode.children(container, named: "complexType") {
guard let name = XSDDerivNode.attribute(type, "name") else { continue }
@ -310,22 +315,19 @@ extension PureXML.Schema.XSDParser {
.flatMap { XSDDerivNode.firstChild($0, named: "extension") ?? XSDDerivNode.firstChild($0, named: "restriction") }
.flatMap { XSDDerivNode.attribute($0, "base") }
if derivationInfo(type)?.base != name || redefineBaseIsForeign(type, rawBase) {
throw SchemaFault.redefineIncompatible(type: name)
errors.append("redefined type '\(name)' must restrict or extend itself")
}
}
for type in XSDDerivNode.children(container, named: "simpleType") {
try checkRedefinedSimpleType(type)
guard let name = XSDDerivNode.attribute(type, "name") else { continue }
let rawBase = XSDDerivNode.firstChild(type, named: "restriction")
.flatMap { XSDDerivNode.attribute($0, "base") }
if rawBase.map(XSDDerivNode.stripPrefix) != name || redefineBaseIsForeign(type, rawBase) {
errors.append("redefined type '\(name)' must restrict or extend itself")
}
}
}
}
private static func checkRedefinedSimpleType(_ type: XSDTree) throws {
guard let name = XSDDerivNode.attribute(type, "name") else { return }
let rawBase = XSDDerivNode.firstChild(type, named: "restriction")
.flatMap { XSDDerivNode.attribute($0, "base") }
if rawBase.map(XSDDerivNode.stripPrefix) != name || redefineBaseIsForeign(type, rawBase) {
throw SchemaFault.redefineIncompatible(type: name)
}
return errors
}
/// Drops from each substitution group the members a head's `block` forbids: a
@ -352,37 +354,4 @@ extension PureXML.Schema.XSDParser {
}
return result
}
/// Throws when any `xs:all` group violates its XSD 1.0 constraints: its members
/// must all be elements, each occurring at most once, and the group itself may
/// occur at most once. Nested model groups inside an `all` are not allowed.
static func checkAllGroups(_ containers: [XSDTree]) throws {
for container in containers {
for all in descendants(container, named: "all") {
try checkAllGroup(all)
}
}
}
private static func checkAllGroup(_ all: XSDTree) throws {
if !atMostOnce(XSDDerivNode.attribute(all, "maxOccurs")) {
throw SchemaFault.invalidAllGroup(reason: "the group's maxOccurs must be 0 or 1")
}
for member in XSDDerivNode.elementChildren(all) {
let kind = XSDDerivNode.localName(member) ?? ""
if kind == "annotation" { continue }
guard kind == "element" else {
throw SchemaFault.invalidAllGroup(reason: "a member may only be an element, not '\(kind)'")
}
if !atMostOnce(XSDDerivNode.attribute(member, "maxOccurs")) {
let label = XSDDerivNode.attribute(member, "name") ?? XSDDerivNode.attribute(member, "ref") ?? ""
throw SchemaFault.invalidAllGroup(reason: "element '\(label)' must have maxOccurs 0 or 1")
}
}
}
private static func atMostOnce(_ maxOccurs: String?) -> Bool {
guard let maxOccurs else { return true }
return maxOccurs == "0" || maxOccurs == "1"
}
}

View File

@ -42,8 +42,6 @@ extension PureXML.Schema {
let failedSchemaReferences = XSDNode.failedSchemaReferences(schema, wrappedLoader, &locationCheckVisited)
let containers = containerTuples.map(\.tree)
let derivation = derivationTables(containers)
try checkRedefine(containers)
try checkAllGroups(containers)
let containerLocations = containerLocationMap(containerTuples, rootLocation: rootLocation)
let compositionLoaded = XSDNode.compositionLoaded(from: containerTuples)
var context = createContext(

View File

@ -46,8 +46,6 @@ extension PureXML.Schema.XSDParser {
let containers = containerTuples.map(\.tree)
let derivation = derivationTables(containers)
try checkRedefine(containers)
try checkAllGroups(containers)
let containerLocations = containerLocationMap(containerTuples, rootLocation: nil)
let compositionLoaded = XSDNode.compositionLoaded(from: containerTuples)
var context = createContext(

View File

@ -91,6 +91,7 @@ struct ValidationConfigurationTests {
"Simple-type final controls are declared consistently",
"Schema content models are deterministic (UPA)",
"A resolved schemaLocation reference is a valid schema",
"A redefined type restricts or extends itself",
"Type derivation chains contain no cycles",
"Schema type references contain no cycles",
"xs:all group references appear only where permitted",