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.
297 lines
14 KiB
Swift
297 lines
14 KiB
Swift
private typealias SubTypeNode = PureXML.Schema.XSDNode
|
|
|
|
extension PureXML.Schema.XSDParser {
|
|
/// XSD 1.0 `e-props-correct.4`: the {type definition} of an element declaration
|
|
/// must be validly derived from the {type definition} of the head of every
|
|
/// substitution group it affiliates to (any type derives from the head's
|
|
/// ur-type, so an untyped head admits any member). A member whose type is
|
|
/// unrelated to the head's, for instance an element of type `xs:int` declaring
|
|
/// `substitutionGroup` of a head typed `xs:string`, leaves the schema wrongly
|
|
/// accepted.
|
|
///
|
|
/// Scope and conservatism:
|
|
/// - Checked only for a self-contained schema (no `import`/`include`/`redefine`),
|
|
/// where the derivation table is complete and, with one target namespace, the
|
|
/// names resolved by local name are unambiguous.
|
|
/// - The member's type is read from its own `type` attribute and the head's from
|
|
/// a map of the top-level (global) element declarations only, so a local
|
|
/// element sharing a name does not supply either type.
|
|
/// - `typeDerivesOrEqual` models the restriction/extension chain and the built-in
|
|
/// lattice but not the list/union variety rules (`cos-st-derived-ok` clauses
|
|
/// 2.3 and 2.4: a type derives from a union if it derives from one of the
|
|
/// union's member types). When either type is a list or union the check stands
|
|
/// down rather than reject a valid member, a disclosed under-rejection.
|
|
static func substitutionTypeFindings(
|
|
_ schema: XSDTree,
|
|
_ containers: [XSDTree],
|
|
_ tables: DerivationTables,
|
|
_ types: [String: PureXML.Schema.ElementType],
|
|
_ context: PureXML.Schema.XSDContext,
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
|
let sources = context.compositionLoaded ? containers : [schema]
|
|
let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: context.targetNamespace)
|
|
var globalElementType: [GlobalElementKey: String] = [:]
|
|
for index in sources.indices {
|
|
let source = sources[index]
|
|
guard SubTypeNode.localName(source) != "redefine" else { continue }
|
|
let namespaceURI = context.compositionLoaded
|
|
? (namespaceMap[index] ?? context.targetNamespace)
|
|
: context.targetNamespace
|
|
for global in SubTypeNode.elementChildren(source).filter({
|
|
SubTypeNode.localName($0) == "element" && $0.name?.namespaceURI == xsdNamespace
|
|
}) {
|
|
if let name = SubTypeNode.attribute(global, "name"), let type = SubTypeNode.attribute(global, "type") {
|
|
globalElementType[GlobalElementKey(namespaceURI: namespaceURI, name: name)] = SubTypeNode.stripPrefix(type)
|
|
}
|
|
}
|
|
}
|
|
return sources.indices.flatMap { index -> [PureXML.Schema.SchemaLocatedFinding] in
|
|
let source = sources[index]
|
|
guard SubTypeNode.localName(source) != "redefine" else { return [] }
|
|
let bindings = SubTypeNode.namespaceBindings(of: source)
|
|
return SubTypeNode.elementChildren(source).filter {
|
|
SubTypeNode.localName($0) == "element" && $0.name?.namespaceURI == xsdNamespace
|
|
}.flatMap { element in
|
|
checkSingleElement(element, bindings, globalElementType, tables, types)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct GlobalElementKey: Hashable {
|
|
let namespaceURI: String?
|
|
let name: String
|
|
}
|
|
|
|
private static func checkSingleElement(
|
|
_ element: XSDTree,
|
|
_ bindings: [String: String],
|
|
_ globalElementType: [GlobalElementKey: String],
|
|
_ tables: DerivationTables,
|
|
_ types: [String: PureXML.Schema.ElementType],
|
|
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
|
guard let member = SubTypeNode.attribute(element, "name"),
|
|
let headReference = SubTypeNode.attribute(element, "substitutionGroup")
|
|
else { return [] }
|
|
let head = SubTypeNode.stripPrefix(headReference)
|
|
let headNamespace = SubTypeNode.referenceNamespace(headReference, bindings)
|
|
let headKey = GlobalElementKey(namespaceURI: headNamespace, name: head)
|
|
guard let headType = globalElementType[headKey] else { return [] }
|
|
if isInlineListOrUnion(element) {
|
|
return []
|
|
}
|
|
let inlineDeriv = inlineTypeDerivation(element)
|
|
var isDerived: Bool
|
|
let memberType: String
|
|
if let inlineDeriv {
|
|
memberType = "anonymous"
|
|
isDerived = inlineDeriv.base == headType || PureXML.Schema.ParticleRestriction.typeDerivesOrEqual(inlineDeriv.base, headType, tables.typeDerivation, types)
|
|
} else if let typeAttr = SubTypeNode.attribute(element, "type").map(SubTypeNode.stripPrefix) {
|
|
memberType = typeAttr
|
|
isDerived = PureXML.Schema.ParticleRestriction.typeDerivesOrEqual(memberType, headType, tables.typeDerivation, types)
|
|
} else {
|
|
// A member without its own type uses the substitution head's type definition.
|
|
memberType = headType
|
|
isDerived = true
|
|
}
|
|
if isBuiltinAnySimpleType(headType, types) {
|
|
isDerived = inlineMemberHasSimpleContent(element) ?? memberTypeHasSimpleContent(memberType, types) ?? isDerived
|
|
}
|
|
// A member whose type is a list or union derives only from anySimpleType, so
|
|
// it may affiliate only to a head typed anySimpleType (or to the same type, or
|
|
// as a recorded restriction of the head's type, both of which `isDerived`
|
|
// covers). Affiliating it to any other head type is not a valid derivation.
|
|
if isListOrUnion(memberType, types), !isDerived, headType != "anySimpleType", headType != "anyType" {
|
|
return [
|
|
PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "element '\(member)' may not be in the substitution group of '\(head)': its list or union type is not derived from '\(headType)'",
|
|
node: element,
|
|
),
|
|
]
|
|
}
|
|
guard !isListOrUnion(memberType, types), !isListOrUnion(headType, types) else { return [] }
|
|
if !isDerived {
|
|
return [
|
|
PureXML.Schema.SchemaLocatedFinding(
|
|
reason: "element '\(member)' may not be in the substitution group of '\(head)': its type is not derived from '\(headType)'",
|
|
node: element,
|
|
),
|
|
]
|
|
}
|
|
return checkExclusions(
|
|
member: member,
|
|
memberType: memberType,
|
|
inlineDeriv: inlineDeriv,
|
|
headInfo: (head, headType),
|
|
tables: tables,
|
|
).map { PureXML.Schema.SchemaLocatedFinding(reason: $0, node: element) }
|
|
}
|
|
|
|
private static func checkExclusions(
|
|
member: String,
|
|
memberType: String,
|
|
inlineDeriv: PureXML.Schema.TypeDerivation?,
|
|
headInfo: (head: String, headType: String),
|
|
tables: DerivationTables,
|
|
) -> [String] {
|
|
guard let exclusions = tables.elementFinal[headInfo.head] else { return [] }
|
|
var errors: [String] = []
|
|
var pathMethods: Set<PureXML.Schema.DerivationMethod> = []
|
|
var currentType: String
|
|
if let inlineDeriv {
|
|
pathMethods.insert(inlineDeriv.method)
|
|
currentType = inlineDeriv.base
|
|
} else {
|
|
currentType = memberType
|
|
}
|
|
var visited: Set<String> = [currentType]
|
|
while currentType != headInfo.headType {
|
|
guard let derivation = tables.typeDerivation[currentType] else {
|
|
break
|
|
}
|
|
pathMethods.insert(derivation.method)
|
|
guard visited.insert(derivation.base).inserted else {
|
|
break
|
|
}
|
|
currentType = derivation.base
|
|
}
|
|
if currentType != headInfo.headType {
|
|
let member = PureXML.Schema.BuiltinType(rawValue: currentType)
|
|
let head = PureXML.Schema.BuiltinType(rawValue: headInfo.headType)
|
|
if let member, let head, member.derives(from: head) {
|
|
pathMethods.insert(.restriction)
|
|
}
|
|
}
|
|
if exclusions.contains(.extension), pathMethods.contains(.extension) {
|
|
errors.append(
|
|
"element '\(member)' may not be in the substitution group of '\(headInfo.head)': "
|
|
+ "its type '\(memberType)' is derived by extension from '\(headInfo.headType)' which is excluded by the head element",
|
|
)
|
|
}
|
|
if exclusions.contains(.restriction), pathMethods.contains(.restriction) {
|
|
errors.append(
|
|
"element '\(member)' may not be in the substitution group of '\(headInfo.head)': "
|
|
+ "its type '\(memberType)' is derived by restriction from '\(headInfo.headType)' which is excluded by the head element",
|
|
)
|
|
}
|
|
return errors
|
|
}
|
|
|
|
private static func inlineTypeDerivation(_ element: XSDTree) -> PureXML.Schema.TypeDerivation? {
|
|
if let complex = SubTypeNode.elementChildren(element).first(where: {
|
|
$0.name?.namespaceURI == xsdNamespace && SubTypeNode.localName($0) == "complexType"
|
|
}) {
|
|
if let info = derivationInfo(complex) {
|
|
return info
|
|
}
|
|
return PureXML.Schema.TypeDerivation(base: "anyType", method: .restriction)
|
|
}
|
|
if let simple = SubTypeNode.elementChildren(element).first(where: {
|
|
$0.name?.namespaceURI == xsdNamespace && SubTypeNode.localName($0) == "simpleType"
|
|
}) {
|
|
return simpleTypeNodeDerivation(simple)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func simpleTypeNodeDerivation(_ simple: XSDTree) -> PureXML.Schema.TypeDerivation? {
|
|
if let restriction = SubTypeNode.elementChildren(simple).first(where: {
|
|
$0.name?.namespaceURI == xsdNamespace && SubTypeNode.localName($0) == "restriction"
|
|
}) {
|
|
if let base = SubTypeNode.attribute(restriction, "base") {
|
|
return PureXML.Schema.TypeDerivation(base: SubTypeNode.stripPrefix(base), method: .restriction)
|
|
}
|
|
if let inlineBase = SubTypeNode.elementChildren(restriction).first(where: {
|
|
$0.name?.namespaceURI == xsdNamespace && SubTypeNode.localName($0) == "simpleType"
|
|
}) {
|
|
return simpleTypeNodeDerivation(inlineBase)
|
|
}
|
|
}
|
|
return PureXML.Schema.TypeDerivation(base: "anySimpleType", method: .restriction)
|
|
}
|
|
|
|
private static func isInlineListOrUnion(_ element: XSDTree) -> Bool {
|
|
guard let simple = SubTypeNode.elementChildren(element).first(where: {
|
|
$0.name?.namespaceURI == xsdNamespace && SubTypeNode.localName($0) == "simpleType"
|
|
}) else { return false }
|
|
return isSimpleNodeListOrUnion(simple)
|
|
}
|
|
|
|
private static func isSimpleNodeListOrUnion(_ simple: XSDTree) -> Bool {
|
|
if SubTypeNode.elementChildren(simple).contains(where: {
|
|
let local = SubTypeNode.localName($0)
|
|
return $0.name?.namespaceURI == xsdNamespace && (local == "list" || local == "union")
|
|
}) {
|
|
return true
|
|
}
|
|
if let restriction = SubTypeNode.elementChildren(simple).first(where: {
|
|
$0.name?.namespaceURI == xsdNamespace && SubTypeNode.localName($0) == "restriction"
|
|
}) {
|
|
if let inlineBase = SubTypeNode.elementChildren(restriction).first(where: {
|
|
$0.name?.namespaceURI == xsdNamespace && SubTypeNode.localName($0) == "simpleType"
|
|
}) {
|
|
return isSimpleNodeListOrUnion(inlineBase)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Whether the named type is a list- or union-variety simple type, whose
|
|
/// derivation rules `typeDerivesOrEqual` does not model. A built-in or unknown
|
|
/// name is treated as atomic.
|
|
private static func isListOrUnion(_ name: String, _ types: [String: PureXML.Schema.ElementType]) -> Bool {
|
|
guard case let .simple(simple)? = types[name] else { return false }
|
|
switch simple.variety {
|
|
case .atomic: return false
|
|
case .list, .union: return true
|
|
}
|
|
}
|
|
|
|
private static func isBuiltinAnySimpleType(_ name: String, _ types: [String: PureXML.Schema.ElementType]) -> Bool {
|
|
name == "anySimpleType" && types[name] == nil
|
|
}
|
|
|
|
/// Whether a member type can derive from the simple ur-type. Unknown types are
|
|
/// left undecided so the reference-resolution rule, not this check, owns them.
|
|
private static func memberTypeHasSimpleContent(_ name: String, _ types: [String: PureXML.Schema.ElementType]) -> Bool? {
|
|
if name == "anySimpleType" {
|
|
return true
|
|
}
|
|
if name == "anyType" {
|
|
return false
|
|
}
|
|
if PureXML.Schema.BuiltinType(rawValue: name) != nil {
|
|
return true
|
|
}
|
|
var current: PureXML.Schema.ElementType? = types[name]
|
|
var steps = 0
|
|
while let resolved = current, steps <= types.count {
|
|
switch resolved {
|
|
case .simple:
|
|
return true
|
|
case let .complex(complex):
|
|
if case .simpleContent = complex.content {
|
|
return true
|
|
}
|
|
return false
|
|
case let .typeReference(key):
|
|
current = types[key]
|
|
steps += 1
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func inlineMemberHasSimpleContent(_ element: XSDTree) -> Bool? {
|
|
if SubTypeNode.elementChildren(element).contains(where: {
|
|
$0.name?.namespaceURI == xsdNamespace && SubTypeNode.localName($0) == "simpleType"
|
|
}) {
|
|
return true
|
|
}
|
|
guard let complex = SubTypeNode.elementChildren(element).first(where: {
|
|
$0.name?.namespaceURI == xsdNamespace && SubTypeNode.localName($0) == "complexType"
|
|
}) else { return nil }
|
|
return SubTypeNode.firstChild(complex, named: "simpleContent") != nil
|
|
}
|
|
}
|