PureXML/Sources/Schema/SchemaSimpleTypeFinal.swift
Mihaela Mihaljevic 380160cc2b refactor(validation): decompose XSD instance validation into composable rules
The two monolithic XSD content/identity rules only satisfied the OpenAPIKit
validation idiom at the wrong granularity: no per-constraint removability,
no isolable failing/near-miss tests. Split them into 32 named rules, each
finding tagged at its origin and cached per XSDContext so the walk still
runs once per document (the #101 decision against duplicating XSD's
recursive type resolution still holds). Wired the streaming path's ID/IDREF
and identity checks through the same framework instead of calling the
validators imperatively, and added the unwrapAndLookup combinator the rule
already named as required.

Also includes the mandated repo-wide swiftformat/swiftlint pass.
2026-07-12 14:12:17 +02:00

119 lines
5.8 KiB
Swift

private typealias FinalNode = PureXML.Schema.XSDNode
extension PureXML.Schema.XSDParser {
/// XSD 1.0 simple-type `final` enforcement for the list and union derivation
/// directions (`st-props-correct` / Derivation Valid): a simple type whose
/// `final` contains `list` may not be the `itemType` of a list, and one whose
/// `final` contains `union` may not be a `memberType` of a union (`#all` forbids
/// both, along with `restriction`). The restriction and extension directions are
/// already enforced by `finalRespected`; the list and union directions were
/// unchecked because `final="list"`/`"union"` is not modelled by
/// `DerivationMethod`, so the `final` attribute is read directly here.
///
/// Checked only for a self-contained schema (no `import`/`include`/`redefine`),
/// where the single target namespace makes the names resolved by local name
/// unambiguous; with external definitions the check stands down (a disclosed
/// under-rejection).
static func simpleTypeFinalFindings(_ schema: XSDTree, compositionLoaded: Bool, containers: [XSDTree]) -> [PureXML.Schema.SchemaLocatedFinding] {
guard !skipsCrossDocumentRules(schema, compositionLoaded: compositionLoaded) else { return [] }
let finalOf = compositionLoaded ? mergedSimpleTypeFinalMap(containers) : simpleTypeFinalMap(schema)
guard finalOf.values.contains(where: { !$0.isEmpty }) else { return [] }
// The `final` map is keyed by local name; an `itemType`/`memberTypes`
// reference must resolve to this schema's own target namespace before it is
// matched, so a reference to a built-in (for example `xs:string`) is never
// confused with a user type of the same local name.
let bindings = FinalNode.namespaceBindings(of: schema)
let target = FinalNode.attribute(schema, "targetNamespace")
return listItemFinalFindings(schema, finalOf, bindings, target)
+ unionMemberFinalFindings(schema, finalOf, bindings, target)
}
private static func mergedSimpleTypeFinalMap(_ containers: [XSDTree]) -> [String: Set<String>] {
var finalOf: [String: Set<String>] = [:]
for container in containers where FinalNode.localName(container) != "redefine" {
for (name, tokens) in simpleTypeFinalMap(container) {
finalOf[name] = tokens
}
}
return finalOf
}
/// Each named simple type mapped to the derivation tokens its `final` forbids.
/// A type without its own `final` inherits the schema's `finalDefault` (its
/// `{final}` property; XSD 1.0 Schemas §3.14.2), so `finalDefault="list"`
/// makes every such type final for list derivation. An explicit `final=""`
/// still overrides the default back to no restriction.
private static func simpleTypeFinalMap(_ schema: XSDTree) -> [String: Set<String>] {
let fallback = finalTokens(FinalNode.attribute(schema, "finalDefault"))
var finalOf: [String: Set<String>] = [:]
for simpleType in descendants(schema, named: "simpleType") {
guard let name = FinalNode.attribute(simpleType, "name") else { continue }
if let own = FinalNode.attribute(simpleType, "final") {
finalOf[name] = finalTokens(own)
} else {
finalOf[name] = fallback
}
}
return finalOf
}
private static func listItemFinalFindings(
_ schema: XSDTree,
_ finalOf: [String: Set<String>],
_ bindings: [String: String],
_ target: String?,
) -> [PureXML.Schema.SchemaLocatedFinding] {
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
for list in descendants(schema, named: "list") {
guard let item = FinalNode.attribute(list, "itemType"),
FinalNode.referenceNamespace(item, bindings) == target
else { continue }
let name = FinalNode.stripPrefix(item)
if finalOf[name]?.contains("list") == true {
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "simple type '\(name)' is final for 'list' and may not be a list item type",
node: list,
))
}
}
return findings
}
private static func unionMemberFinalFindings(
_ schema: XSDTree,
_ finalOf: [String: Set<String>],
_ bindings: [String: String],
_ target: String?,
) -> [PureXML.Schema.SchemaLocatedFinding] {
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
for union in descendants(schema, named: "union") {
guard let members = FinalNode.attribute(union, "memberTypes") else { continue }
for token in members.split(whereSeparator: \.isWhitespace) {
guard FinalNode.referenceNamespace(String(token), bindings) == target else { continue }
let name = FinalNode.stripPrefix(String(token))
if finalOf[name]?.contains("union") == true {
findings.append(PureXML.Schema.SchemaLocatedFinding(
reason: "simple type '\(name)' is final for 'union' and may not be a union member type",
node: union,
))
}
}
}
return findings
}
/// The derivation tokens a `final` value forbids; `#all` forbids every simple-type
/// derivation (`restriction`, `list`, `union`).
private static func finalTokens(_ raw: String?) -> Set<String> {
guard let raw, !raw.isEmpty else { return [] }
var tokens: Set<String> = []
for token in raw.split(whereSeparator: \.isWhitespace) {
if token == "#all" {
return ["restriction", "list", "union"]
}
tokens.insert(String(token))
}
return tokens
}
}