PureXML/Sources/Schema/SchemaSimpleTypeFinal.swift
Mihaela Mihaljevic 356e87581c feat(schema): locate simple-type-pass schema diagnostics
Convert the four simple-type-pass compile checkers to located findings so
each error attaches to its own declaring node instead of the schema root
(#169): a list whose item type is itself a list or a union containing a
list and a list declaring both or neither of itemType/inline simpleType
(simpleTypeVarietyFindings), an empty union, a simpleType restriction whose
base resolves to a complex type (simpleTypeBaseNotComplexFindings), an
itemType/memberType referencing a type final for list/union derivation
(simpleTypeFinalFindings), and an attribute typed by a complex type
(attributeTypeMustBeSimpleFindings).

Each predicate, traversal order, and reason string is unchanged; only the
location each finding carries differs, from the schema root to the
offending list, union, restriction, or attribute node. The four checkers
had exactly one caller each (their compile-rule dispatch), so each is
renamed in place and the .unlocated wrap dropped, leaving no dead wrapper.
Adds a located-diagnostics test per checker in a new suite kept within the
type-body budget.
2026-06-21 15:55:59 +02:00

117 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
}
}