Convert the three extension/substitution-pass compile checkers to located findings so each error attaches to its declaring node instead of the schema root (#169): an xs:all group reached through an extension over a base that already has content underlines the extension (extensionAllGroupFindings); an anonymous complex-type restriction that is not a subset of its base underlines the restriction (anonymousRestrictionFindings); and a substitution-group member whose type is not validly derived from its head, including the head's final-exclusion violations, underlines the member element (substitutionTypeFindings). Every predicate, iteration order, and reason string is unchanged; only the location each finding carries differs, verified conformance-neutral by the full XSTS suite (14383 groups, pinned counts unchanged). The substitution checker's checkExclusions helper stays [String] and is wrapped onto the member element at the call site. The three checkers had exactly one caller each (their dispatch), so each is renamed in place and the .unlocated wrap dropped. Adds a located-diagnostics test per checker.
287 lines
14 KiB
Swift
287 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
|
|
}
|
|
}
|