feat(schema): locate content-derivation-pass schema diagnostics
Convert the seven checkers the complexExtensionBaseValid compile rule dispatches (the cos-ct-extends/src-ct content-derivation family) to located findings so each error attaches to its derivation node instead of the schema root (#169): a complexContent extension adding element content over a simpleContent base, a simpleContent restriction whose base is a built-in or user simple type, an unfaithful inline type in a simpleContent restriction, a complexContent base that is a simple type, a simpleContent extension whose base is xs:anyType, and a mismatched mixed setting on a complexContent extension each underline their extension/restriction node; an element carrying a default/fixed value over element-only or empty content underlines the element. Each predicate, iteration order, and reason string is unchanged; only the location each finding carries differs. The seven checkers had exactly one caller each (the dispatch), so each is renamed in place and the .unlocated wrap dropped. To keep the file under the 400-line limit, the seven functions and their two private helpers move to a new SchemaContentDerivation.swift; the shared extensionAddsElementContent helper, still used by the all-group checker that stays behind, becomes module-internal. Adds a located- diagnostics test per checker.
This commit is contained in:
parent
ee5c46929c
commit
2b465e9c9c
@ -32,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Schema compile diagnostics are now located: `SchemaError.inconsistent` carries `[ValidationError]` with coding paths that resolve to `TreeNode.sourceRange` on a ranged schema tree (#169). Parse-time facet and structure findings attach to their declaring nodes where possible; `SchemaError.inconsistentFindings` exposes the full list. Schematron reuses the shared `validationCodingPath()` helper. Value-constraint errors now locate to their declaring element or attribute: a `default`/`fixed` on an ID-derived type, a value that is invalid for its built-in, named user, or inline type, and an illegal combination of value constraints each underline the offending declaration rather than the schema root. The structure-pass checkers are likewise located: a facet inapplicable to a list or union variety, a derivation by restriction of `xs:anySimpleType` and a constraining facet on `anySimpleType`-based simple content, a top-level definition missing its name or carrying a local-only attribute, a nested named definition, an empty `targetNamespace` or `import` namespace, and an all-group reference with an illegal `minOccurs`/`maxOccurs` each underline their own node instead of the schema root. The simple-type-pass checkers join them: a list whose item type is itself a list or a union containing a list, a list declaring both or neither of `itemType`/inline `simpleType`, an empty union, a simpleType restriction whose base resolves to a complex type, an `itemType`/`memberType` referencing a type that is `final` for list/union derivation, and an attribute typed by a complex type each underline the offending `list`, `union`, `restriction`, or `attribute` node rather than the schema root. The attribute-pass checkers follow: an attribute declaration that lands in the XSI namespace underlines the `attribute`; a duplicate attribute use or a second ID-typed attribute underlines its declaring `complexType` or `attributeGroup`; an attribute reference that contradicts a global `fixed` value underlines the reference; and a complex-type restriction that relaxes a base attribute use (making a required attribute optional, narrowing its type unfaithfully, dropping a fixed value, or widening the attribute wildcard) underlines the `restriction`. The derivation/cycle-pass checkers, which report a cycle by component name over a name-keyed graph, now record the declaring node alongside that graph and underline it: a type derived from itself underlines its `complexType`/`simpleType`, an element that is a member of its own substitution group underlines its `element`, a model group or attribute group that references itself underlines its `group`/`attributeGroup` declaration, and an all-group referenced inside a `sequence`/`choice`/`all` underlines the offending `group` reference.
|
||||
- Schema compile diagnostics are now located: `SchemaError.inconsistent` carries `[ValidationError]` with coding paths that resolve to `TreeNode.sourceRange` on a ranged schema tree (#169). Parse-time facet and structure findings attach to their declaring nodes where possible; `SchemaError.inconsistentFindings` exposes the full list. Schematron reuses the shared `validationCodingPath()` helper. Value-constraint errors now locate to their declaring element or attribute: a `default`/`fixed` on an ID-derived type, a value that is invalid for its built-in, named user, or inline type, and an illegal combination of value constraints each underline the offending declaration rather than the schema root. The structure-pass checkers are likewise located: a facet inapplicable to a list or union variety, a derivation by restriction of `xs:anySimpleType` and a constraining facet on `anySimpleType`-based simple content, a top-level definition missing its name or carrying a local-only attribute, a nested named definition, an empty `targetNamespace` or `import` namespace, and an all-group reference with an illegal `minOccurs`/`maxOccurs` each underline their own node instead of the schema root. The simple-type-pass checkers join them: a list whose item type is itself a list or a union containing a list, a list declaring both or neither of `itemType`/inline `simpleType`, an empty union, a simpleType restriction whose base resolves to a complex type, an `itemType`/`memberType` referencing a type that is `final` for list/union derivation, and an attribute typed by a complex type each underline the offending `list`, `union`, `restriction`, or `attribute` node rather than the schema root. The attribute-pass checkers follow: an attribute declaration that lands in the XSI namespace underlines the `attribute`; a duplicate attribute use or a second ID-typed attribute underlines its declaring `complexType` or `attributeGroup`; an attribute reference that contradicts a global `fixed` value underlines the reference; and a complex-type restriction that relaxes a base attribute use (making a required attribute optional, narrowing its type unfaithfully, dropping a fixed value, or widening the attribute wildcard) underlines the `restriction`. The derivation/cycle-pass checkers, which report a cycle by component name over a name-keyed graph, now record the declaring node alongside that graph and underline it: a type derived from itself underlines its `complexType`/`simpleType`, an element that is a member of its own substitution group underlines its `element`, a model group or attribute group that references itself underlines its `group`/`attributeGroup` declaration, and an all-group referenced inside a `sequence`/`choice`/`all` underlines the offending `group` reference. The content-derivation-pass checkers (the `cos-ct-extends`/`src-ct` base-kind family) follow: a complexContent extension adding element content over a simpleContent base, a simpleContent restriction whose base is a built-in or user simple type, an unfaithful inline type in a simpleContent restriction, a complexContent base that is a simple type, a simpleContent extension whose base is `xs:anyType`, and a mismatched `mixed` setting on a complexContent extension each underline their `extension`/`restriction` derivation node, and an element carrying a `default`/`fixed` value over element-only or empty content underlines the `element`.
|
||||
|
||||
- DTD parse-time validity findings are warnings until strict validation promotes them; `validateAgainstInternalDTD` returns full findings (errors and warnings).
|
||||
|
||||
|
||||
@ -358,15 +358,13 @@ extension PureXML.Validation.SchemaCompile {
|
||||
static var complexExtensionBaseValid: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("A content-model derivation is consistent with its base type") { document in
|
||||
guard let namedTypes = document.namedTypes else { return [] }
|
||||
return PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.XSDParser.simpleContentExtensionBaseErrors(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.extensionMixedAgreementErrors(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.simpleContentRestrictionBaseErrors(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.simpleContentRestrictionTypeErrors(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.complexContentBaseKindErrors(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.simpleContentExtensionBaseKindErrors(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.elementValueConstraintContentErrors(document.schema, document.context, namedTypes),
|
||||
)
|
||||
return PureXML.Schema.XSDParser.simpleContentExtensionBaseFindings(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.extensionMixedAgreementFindings(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.simpleContentRestrictionBaseFindings(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.simpleContentRestrictionTypeFindings(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.complexContentBaseKindFindings(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.simpleContentExtensionBaseKindFindings(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.elementValueConstraintContentFindings(document.schema, document.context, namedTypes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
294
Sources/Schema/SchemaContentDerivation.swift
Normal file
294
Sources/Schema/SchemaContentDerivation.swift
Normal file
@ -0,0 +1,294 @@
|
||||
private typealias ContentDerivNode = PureXML.Schema.XSDNode
|
||||
|
||||
extension PureXML.Schema.XSDParser {
|
||||
/// XSD 1.0 `cos-ct-extends.1.4.2.2`: a `complexContent` extension that adds
|
||||
/// element content (a model group) produces an element-only/mixed content type,
|
||||
/// so its base must have complex or empty content. A `simpleContent` base has a
|
||||
/// simple content type and cannot gain element content this way. An extension
|
||||
/// that adds only attributes keeps the base's simple content and is valid (it
|
||||
/// is the `complexContent`-around-`simpleContent` idiom), so the model group is
|
||||
/// required for the error. Resolved in this schema's own target namespace.
|
||||
static func simpleContentExtensionBaseFindings(
|
||||
_ schema: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for content in descendants(schema, named: "complexContent") {
|
||||
guard let ext = ContentDerivNode.firstChild(content, named: "extension"),
|
||||
extensionAddsElementContent(ext),
|
||||
let base = ContentDerivNode.attribute(ext, "base"),
|
||||
ContentDerivNode.referenceNamespace(base, bindings) == context.targetNamespace,
|
||||
case let .complex(complex)? = types[ContentDerivNode.stripPrefix(base)],
|
||||
case .simpleContent = complex.content
|
||||
else { continue }
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a complexContent extension that adds element content may not extend '\(ContentDerivNode.stripPrefix(base))', which has simple content",
|
||||
node: ext,
|
||||
))
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
/// XSD 1.0 `src-ct.2` / Derivation Valid (Restriction): the base of a
|
||||
/// `simpleContent` RESTRICTION must be a complex type (whose content is simple).
|
||||
/// A built-in (`xs:integer`, `xs:anySimpleType`) or a user `simpleType` base is
|
||||
/// invalid: a complex type with simple content is built by EXTENDING a simple
|
||||
/// type, never by restricting one through `simpleContent`. Self-contained schema
|
||||
/// only; a foreign or unresolved base is left alone.
|
||||
static func simpleContentRestrictionBaseFindings(
|
||||
_ schema: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for content in descendants(schema, named: "simpleContent") {
|
||||
guard let restriction = ContentDerivNode.firstChild(content, named: "restriction"),
|
||||
let base = ContentDerivNode.attribute(restriction, "base")
|
||||
else { continue }
|
||||
let namespace = ContentDerivNode.referenceNamespace(base, bindings)
|
||||
let baseName = ContentDerivNode.stripPrefix(base)
|
||||
if namespace == xsdNamespace {
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a simpleContent restriction's base must be a complex type, not the built-in simple type '\(baseName)'",
|
||||
node: restriction,
|
||||
))
|
||||
} else if namespace == context.targetNamespace, case .simple? = types[baseName] {
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a simpleContent restriction's base must be a complex type, not the simple type '\(baseName)'",
|
||||
node: restriction,
|
||||
))
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
/// XSD 1.0 Derivation Valid (Restriction, Simple): when a `simpleContent`
|
||||
/// restriction supplies an inline `simpleType`, that inline type must itself be
|
||||
/// a valid restriction of the base complex type's simple content type. A list
|
||||
/// of `xs:int`, for example, is not a restriction of `xs:decimal` because it
|
||||
/// changes the variety from atomic to list (W3C msData particlesZ018).
|
||||
///
|
||||
/// Only local, resolved complex bases with simple content are checked. Built-in,
|
||||
/// foreign, unresolved, and composed bases are left to the surrounding base-kind
|
||||
/// and cross-document rules so this cannot reject a schema whose base type is
|
||||
/// unknown to this compile pass.
|
||||
static func simpleContentRestrictionTypeFindings(
|
||||
_ schema: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for content in descendants(schema, named: "simpleContent") {
|
||||
guard let restriction = ContentDerivNode.firstChild(content, named: "restriction"),
|
||||
let inline = ContentDerivNode.firstChild(restriction, named: "simpleType"),
|
||||
let base = ContentDerivNode.attribute(restriction, "base")
|
||||
else { continue }
|
||||
let bindings = namespaceBindingsInScope(of: restriction, defaultBindings: context.namespaceBindings)
|
||||
let namespace = ContentDerivNode.referenceNamespace(base, bindings)
|
||||
let baseName = ContentDerivNode.stripPrefix(base)
|
||||
guard namespace == context.targetNamespace,
|
||||
case let .complex(complex)? = types[baseName],
|
||||
case let .simpleContent(baseSimpleType) = complex.content
|
||||
else { continue }
|
||||
let derivedSimpleType = scopedSimpleType(inline, context)
|
||||
if !isSimpleTypeRestrictionOK(derived: derivedSimpleType, base: baseSimpleType) {
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "the inline simpleType in a simpleContent restriction is not a valid restriction of base type '\(baseName)'",
|
||||
node: restriction,
|
||||
))
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
/// XSD 1.0 `src-ct.1`: the base of a `complexContent` derivation (restriction or
|
||||
/// extension) must be a COMPLEX type. The ur-type `xs:anyType` is complex and is
|
||||
/// the legitimate base of most complex types; every other XSD built-in, and a
|
||||
/// user `simpleType`, is a simple type and may not be a complexContent base. A
|
||||
/// LOCALLY-declared type takes precedence over the built-in reading: a schema may
|
||||
/// target the XSD namespace and define its own components there (the
|
||||
/// schema-for-schemas extends its own `xs:openAttrs`), so resolve `types` first
|
||||
/// and only fall back to the built-in table when there is no local declaration.
|
||||
/// Self-contained schema only; a foreign or unresolved base is left alone.
|
||||
static func complexContentBaseKindFindings(
|
||||
_ schema: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for content in descendants(schema, named: "complexContent") {
|
||||
guard let derivation = ContentDerivNode.firstChild(content, named: "extension")
|
||||
?? ContentDerivNode.firstChild(content, named: "restriction"),
|
||||
let base = ContentDerivNode.attribute(derivation, "base")
|
||||
else { continue }
|
||||
let namespace = ContentDerivNode.referenceNamespace(base, bindings)
|
||||
let baseName = ContentDerivNode.stripPrefix(base)
|
||||
if namespace == context.targetNamespace, let local = types[baseName] {
|
||||
if case .simple = local {
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a complexContent base must be a complex type, not the simple type '\(baseName)'",
|
||||
node: derivation,
|
||||
))
|
||||
}
|
||||
} else if namespace == xsdNamespace, types[baseName] == nil, baseName != "anyType" {
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a complexContent base must be a complex type, not the built-in simple type '\(baseName)'",
|
||||
node: derivation,
|
||||
))
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
/// XSD 1.0 `src-ct.2` (extension): the base of a `simpleContent` EXTENSION must be
|
||||
/// a simple type or a complex type whose content type is simple. A complex type
|
||||
/// with element-only, mixed, or empty content (including the ur-type
|
||||
/// `xs:anyType`) may not be a simpleContent extension base, since the result must
|
||||
/// have simple content. A LOCALLY-declared type takes precedence over the
|
||||
/// built-in reading (a schema may target the XSD namespace), so resolve `types`
|
||||
/// first. A simple-type base (built-in or user) is valid; only `xs:anyType` as a
|
||||
/// built-in, or a local complex type without simple content, is an error.
|
||||
/// Self-contained schema only; a foreign or unresolved base is left alone.
|
||||
static func simpleContentExtensionBaseKindFindings(
|
||||
_ schema: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for content in descendants(schema, named: "simpleContent") {
|
||||
guard let ext = ContentDerivNode.firstChild(content, named: "extension"),
|
||||
let base = ContentDerivNode.attribute(ext, "base")
|
||||
else { continue }
|
||||
let namespace = ContentDerivNode.referenceNamespace(base, bindings)
|
||||
let baseName = ContentDerivNode.stripPrefix(base)
|
||||
if namespace == context.targetNamespace, case let .complex(complex)? = types[baseName] {
|
||||
switch complex.content {
|
||||
case .simpleContent: break
|
||||
case .mixed, .elementOnly, .empty:
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a simpleContent extension's base complex type '\(baseName)' must have simple content",
|
||||
node: ext,
|
||||
))
|
||||
}
|
||||
} else if namespace == xsdNamespace, types[baseName] == nil, baseName == "anyType" {
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a simpleContent extension's base must be a simple type or a complex type with simple content, not '\(baseName)'",
|
||||
node: ext,
|
||||
))
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
/// XSD 1.0 `cos-valid-default` / `e-props-correct`: an element with a `default`
|
||||
/// or `fixed` value constraint must have a content type that admits character
|
||||
/// data, i.e. a simple type or mixed content. An element whose type is (or whose
|
||||
/// inline complex type has) element-only or empty content may not carry a value
|
||||
/// constraint. Conservative by construction: it flags only a content type that is
|
||||
/// CERTAINLY element-only or empty. A simple type, mixed content, the ur-type
|
||||
/// `xs:anyType` (mixed), an untyped element (also `anyType`), a complexContent
|
||||
/// derivation (mixedness may be inherited), and a foreign or unresolved type are
|
||||
/// all left alone, so this cannot raise a false positive. Self-contained schema.
|
||||
static func elementValueConstraintContentFindings(
|
||||
_ schema: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for element in descendants(schema, named: "element") where element.name?.namespaceURI == xsdNamespace {
|
||||
guard ContentDerivNode.attribute(element, "fixed") != nil || ContentDerivNode.attribute(element, "default") != nil,
|
||||
valueConstraintForbiddenByContent(element, bindings, context, types)
|
||||
else { continue }
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "an element with a 'default' or 'fixed' value must have simple or mixed content, not element-only or empty content",
|
||||
node: element,
|
||||
))
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
/// Whether `element`'s content type is certainly element-only or empty, so a
|
||||
/// value constraint on it is invalid. Returns false (no error) for anything that
|
||||
/// admits character data or that cannot be resolved with certainty.
|
||||
private static func valueConstraintForbiddenByContent(
|
||||
_ element: XSDTree,
|
||||
_ bindings: [String: String],
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> Bool {
|
||||
if let inline = ContentDerivNode.firstChild(element, named: "complexType") {
|
||||
if ContentDerivNode.firstChild(inline, named: "simpleContent") != nil { return false }
|
||||
if ["true", "1"].contains(ContentDerivNode.attribute(inline, "mixed")) { return false }
|
||||
if ContentDerivNode.firstChild(inline, named: "complexContent") != nil { return false }
|
||||
return ["all", "sequence", "choice", "group"].contains { ContentDerivNode.firstChild(inline, named: $0) != nil }
|
||||
}
|
||||
guard let typeName = ContentDerivNode.attribute(element, "type") else { return false }
|
||||
let namespace = ContentDerivNode.referenceNamespace(typeName, bindings)
|
||||
let local = ContentDerivNode.stripPrefix(typeName)
|
||||
if namespace == xsdNamespace { return false }
|
||||
guard namespace == context.targetNamespace, case let .complex(complex)? = types[local] else { return false }
|
||||
switch complex.content {
|
||||
case .elementOnly, .empty: return true
|
||||
case .simpleContent, .mixed: return false
|
||||
}
|
||||
}
|
||||
|
||||
/// XSD 1.0 `cos-ct-extends.1.4.3.2.2.1`: an explicit `mixed` on a
|
||||
/// `complexContent` extension must match the base. If `mixed` is omitted, an
|
||||
/// extension with no element particle inherits the base content unchanged
|
||||
/// (`ctZ012b`), while an extension that adds element content uses the schema
|
||||
/// default `false` (`ctZ010d`). Checked for a self-contained schema, and only
|
||||
/// when the base content is definitely element-only or mixed (an empty or
|
||||
/// simple-content base is governed by other rules and is left alone).
|
||||
static func extensionMixedAgreementFindings(
|
||||
_ schema: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for content in descendants(schema, named: "complexContent") {
|
||||
guard let ext = ContentDerivNode.firstChild(content, named: "extension"),
|
||||
let base = ContentDerivNode.attribute(ext, "base"),
|
||||
ContentDerivNode.referenceNamespace(base, bindings) == context.targetNamespace,
|
||||
case let .complex(complex)? = types[ContentDerivNode.stripPrefix(base)]
|
||||
else { continue }
|
||||
let baseIsMixed: Bool
|
||||
switch complex.content {
|
||||
case .mixed: baseIsMixed = true
|
||||
case .elementOnly: baseIsMixed = false
|
||||
case .empty, .simpleContent: continue
|
||||
}
|
||||
guard let derivedMixed = declaredMixed(content, addsElementContent: extensionAddsElementContent(ext)) else { continue }
|
||||
if baseIsMixed != derivedMixed {
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a complexContent extension of '\(ContentDerivNode.stripPrefix(base))' must have the same mixed setting as its base",
|
||||
node: ext,
|
||||
))
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
/// The declared `mixed` value of a `complexContent` derivation. An omitted
|
||||
/// value matters only when the extension adds element content; otherwise the
|
||||
/// base content is inherited unchanged and there is no mixedness conflict.
|
||||
private static func declaredMixed(_ complexContent: XSDTree, addsElementContent: Bool) -> Bool? {
|
||||
if let own = ContentDerivNode.attribute(complexContent, "mixed") { return own == "true" || own == "1" }
|
||||
if let parent = complexContent.parent, let outer = ContentDerivNode.attribute(parent, "mixed") { return outer == "true" || outer == "1" }
|
||||
return addsElementContent ? false : nil
|
||||
}
|
||||
}
|
||||
@ -89,244 +89,12 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
|
||||
/// XSD 1.0 `cos-ct-extends.1.4.2.2`: a `complexContent` extension that adds
|
||||
/// element content (a model group) produces an element-only/mixed content type,
|
||||
/// so its base must have complex or empty content. A `simpleContent` base has a
|
||||
/// simple content type and cannot gain element content this way. An extension
|
||||
/// that adds only attributes keeps the base's simple content and is valid (it
|
||||
/// is the `complexContent`-around-`simpleContent` idiom), so the model group is
|
||||
/// required for the error. Resolved in this schema's own target namespace.
|
||||
static func simpleContentExtensionBaseErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var errors: [String] = []
|
||||
for content in descendants(schema, named: "complexContent") {
|
||||
guard let ext = ExtAllNode.firstChild(content, named: "extension"),
|
||||
extensionAddsElementContent(ext),
|
||||
let base = ExtAllNode.attribute(ext, "base"),
|
||||
ExtAllNode.referenceNamespace(base, bindings) == context.targetNamespace,
|
||||
case let .complex(complex)? = types[ExtAllNode.stripPrefix(base)],
|
||||
case .simpleContent = complex.content
|
||||
else { continue }
|
||||
errors.append("a complexContent extension that adds element content may not extend '\(ExtAllNode.stripPrefix(base))', which has simple content")
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/// XSD 1.0 `src-ct.2` / Derivation Valid (Restriction): the base of a
|
||||
/// `simpleContent` RESTRICTION must be a complex type (whose content is simple).
|
||||
/// A built-in (`xs:integer`, `xs:anySimpleType`) or a user `simpleType` base is
|
||||
/// invalid: a complex type with simple content is built by EXTENDING a simple
|
||||
/// type, never by restricting one through `simpleContent`. Self-contained schema
|
||||
/// only; a foreign or unresolved base is left alone.
|
||||
static func simpleContentRestrictionBaseErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var errors: [String] = []
|
||||
for content in descendants(schema, named: "simpleContent") {
|
||||
guard let restriction = ExtAllNode.firstChild(content, named: "restriction"),
|
||||
let base = ExtAllNode.attribute(restriction, "base")
|
||||
else { continue }
|
||||
let namespace = ExtAllNode.referenceNamespace(base, bindings)
|
||||
let baseName = ExtAllNode.stripPrefix(base)
|
||||
if namespace == xsdNamespace {
|
||||
errors.append("a simpleContent restriction's base must be a complex type, not the built-in simple type '\(baseName)'")
|
||||
} else if namespace == context.targetNamespace, case .simple? = types[baseName] {
|
||||
errors.append("a simpleContent restriction's base must be a complex type, not the simple type '\(baseName)'")
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/// XSD 1.0 Derivation Valid (Restriction, Simple): when a `simpleContent`
|
||||
/// restriction supplies an inline `simpleType`, that inline type must itself be
|
||||
/// a valid restriction of the base complex type's simple content type. A list
|
||||
/// of `xs:int`, for example, is not a restriction of `xs:decimal` because it
|
||||
/// changes the variety from atomic to list (W3C msData particlesZ018).
|
||||
///
|
||||
/// Only local, resolved complex bases with simple content are checked. Built-in,
|
||||
/// foreign, unresolved, and composed bases are left to the surrounding base-kind
|
||||
/// and cross-document rules so this cannot reject a schema whose base type is
|
||||
/// unknown to this compile pass.
|
||||
static func simpleContentRestrictionTypeErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
var errors: [String] = []
|
||||
for content in descendants(schema, named: "simpleContent") {
|
||||
guard let restriction = ExtAllNode.firstChild(content, named: "restriction"),
|
||||
let inline = ExtAllNode.firstChild(restriction, named: "simpleType"),
|
||||
let base = ExtAllNode.attribute(restriction, "base")
|
||||
else { continue }
|
||||
let bindings = namespaceBindingsInScope(of: restriction, defaultBindings: context.namespaceBindings)
|
||||
let namespace = ExtAllNode.referenceNamespace(base, bindings)
|
||||
let baseName = ExtAllNode.stripPrefix(base)
|
||||
guard namespace == context.targetNamespace,
|
||||
case let .complex(complex)? = types[baseName],
|
||||
case let .simpleContent(baseSimpleType) = complex.content
|
||||
else { continue }
|
||||
let derivedSimpleType = scopedSimpleType(inline, context)
|
||||
if !isSimpleTypeRestrictionOK(derived: derivedSimpleType, base: baseSimpleType) {
|
||||
errors.append("the inline simpleType in a simpleContent restriction is not a valid restriction of base type '\(baseName)'")
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/// XSD 1.0 `src-ct.1`: the base of a `complexContent` derivation (restriction or
|
||||
/// extension) must be a COMPLEX type. The ur-type `xs:anyType` is complex and is
|
||||
/// the legitimate base of most complex types; every other XSD built-in, and a
|
||||
/// user `simpleType`, is a simple type and may not be a complexContent base. A
|
||||
/// LOCALLY-declared type takes precedence over the built-in reading: a schema may
|
||||
/// target the XSD namespace and define its own components there (the
|
||||
/// schema-for-schemas extends its own `xs:openAttrs`), so resolve `types` first
|
||||
/// and only fall back to the built-in table when there is no local declaration.
|
||||
/// Self-contained schema only; a foreign or unresolved base is left alone.
|
||||
static func complexContentBaseKindErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var errors: [String] = []
|
||||
for content in descendants(schema, named: "complexContent") {
|
||||
guard let derivation = ExtAllNode.firstChild(content, named: "extension")
|
||||
?? ExtAllNode.firstChild(content, named: "restriction"),
|
||||
let base = ExtAllNode.attribute(derivation, "base")
|
||||
else { continue }
|
||||
let namespace = ExtAllNode.referenceNamespace(base, bindings)
|
||||
let baseName = ExtAllNode.stripPrefix(base)
|
||||
if namespace == context.targetNamespace, let local = types[baseName] {
|
||||
if case .simple = local {
|
||||
errors.append("a complexContent base must be a complex type, not the simple type '\(baseName)'")
|
||||
}
|
||||
} else if namespace == xsdNamespace, types[baseName] == nil, baseName != "anyType" {
|
||||
errors.append("a complexContent base must be a complex type, not the built-in simple type '\(baseName)'")
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/// XSD 1.0 `src-ct.2` (extension): the base of a `simpleContent` EXTENSION must be
|
||||
/// a simple type or a complex type whose content type is simple. A complex type
|
||||
/// with element-only, mixed, or empty content (including the ur-type
|
||||
/// `xs:anyType`) may not be a simpleContent extension base, since the result must
|
||||
/// have simple content. A LOCALLY-declared type takes precedence over the
|
||||
/// built-in reading (a schema may target the XSD namespace), so resolve `types`
|
||||
/// first. A simple-type base (built-in or user) is valid; only `xs:anyType` as a
|
||||
/// built-in, or a local complex type without simple content, is an error.
|
||||
/// Self-contained schema only; a foreign or unresolved base is left alone.
|
||||
static func simpleContentExtensionBaseKindErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var errors: [String] = []
|
||||
for content in descendants(schema, named: "simpleContent") {
|
||||
guard let ext = ExtAllNode.firstChild(content, named: "extension"),
|
||||
let base = ExtAllNode.attribute(ext, "base")
|
||||
else { continue }
|
||||
let namespace = ExtAllNode.referenceNamespace(base, bindings)
|
||||
let baseName = ExtAllNode.stripPrefix(base)
|
||||
if namespace == context.targetNamespace, case let .complex(complex)? = types[baseName] {
|
||||
switch complex.content {
|
||||
case .simpleContent: break
|
||||
case .mixed, .elementOnly, .empty:
|
||||
errors.append("a simpleContent extension's base complex type '\(baseName)' must have simple content")
|
||||
}
|
||||
} else if namespace == xsdNamespace, types[baseName] == nil, baseName == "anyType" {
|
||||
errors.append("a simpleContent extension's base must be a simple type or a complex type with simple content, not '\(baseName)'")
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/// XSD 1.0 `cos-valid-default` / `e-props-correct`: an element with a `default`
|
||||
/// or `fixed` value constraint must have a content type that admits character
|
||||
/// data, i.e. a simple type or mixed content. An element whose type is (or whose
|
||||
/// inline complex type has) element-only or empty content may not carry a value
|
||||
/// constraint. Conservative by construction: it flags only a content type that is
|
||||
/// CERTAINLY element-only or empty. A simple type, mixed content, the ur-type
|
||||
/// `xs:anyType` (mixed), an untyped element (also `anyType`), a complexContent
|
||||
/// derivation (mixedness may be inherited), and a foreign or unresolved type are
|
||||
/// all left alone, so this cannot raise a false positive. Self-contained schema.
|
||||
static func elementValueConstraintContentErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var errors: [String] = []
|
||||
for element in descendants(schema, named: "element") where element.name?.namespaceURI == xsdNamespace {
|
||||
guard ExtAllNode.attribute(element, "fixed") != nil || ExtAllNode.attribute(element, "default") != nil,
|
||||
valueConstraintForbiddenByContent(element, bindings, context, types)
|
||||
else { continue }
|
||||
errors.append("an element with a 'default' or 'fixed' value must have simple or mixed content, not element-only or empty content")
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/// Whether `element`'s content type is certainly element-only or empty, so a
|
||||
/// value constraint on it is invalid. Returns false (no error) for anything that
|
||||
/// admits character data or that cannot be resolved with certainty.
|
||||
private static func valueConstraintForbiddenByContent(
|
||||
_ element: XSDTree,
|
||||
_ bindings: [String: String],
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> Bool {
|
||||
if let inline = ExtAllNode.firstChild(element, named: "complexType") {
|
||||
if ExtAllNode.firstChild(inline, named: "simpleContent") != nil { return false }
|
||||
if ["true", "1"].contains(ExtAllNode.attribute(inline, "mixed")) { return false }
|
||||
if ExtAllNode.firstChild(inline, named: "complexContent") != nil { return false }
|
||||
return ["all", "sequence", "choice", "group"].contains { ExtAllNode.firstChild(inline, named: $0) != nil }
|
||||
}
|
||||
guard let typeName = ExtAllNode.attribute(element, "type") else { return false }
|
||||
let namespace = ExtAllNode.referenceNamespace(typeName, bindings)
|
||||
let local = ExtAllNode.stripPrefix(typeName)
|
||||
if namespace == xsdNamespace { return false }
|
||||
guard namespace == context.targetNamespace, case let .complex(complex)? = types[local] else { return false }
|
||||
switch complex.content {
|
||||
case .elementOnly, .empty: return true
|
||||
case .simpleContent, .mixed: return false
|
||||
}
|
||||
}
|
||||
|
||||
/// XSD 1.0 `cos-ct-extends.1.4.3.2.2.1`: an explicit `mixed` on a
|
||||
/// `complexContent` extension must match the base. If `mixed` is omitted, an
|
||||
/// extension with no element particle inherits the base content unchanged
|
||||
/// (`ctZ012b`), while an extension that adds element content uses the schema
|
||||
/// default `false` (`ctZ010d`). Checked for a self-contained schema, and only
|
||||
/// when the base content is definitely element-only or mixed (an empty or
|
||||
/// simple-content base is governed by other rules and is left alone).
|
||||
static func extensionMixedAgreementErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var errors: [String] = []
|
||||
for content in descendants(schema, named: "complexContent") {
|
||||
guard let ext = ExtAllNode.firstChild(content, named: "extension"),
|
||||
let base = ExtAllNode.attribute(ext, "base"),
|
||||
ExtAllNode.referenceNamespace(base, bindings) == context.targetNamespace,
|
||||
case let .complex(complex)? = types[ExtAllNode.stripPrefix(base)]
|
||||
else { continue }
|
||||
let baseIsMixed: Bool
|
||||
switch complex.content {
|
||||
case .mixed: baseIsMixed = true
|
||||
case .elementOnly: baseIsMixed = false
|
||||
case .empty, .simpleContent: continue
|
||||
}
|
||||
guard let derivedMixed = declaredMixed(content, addsElementContent: extensionAddsElementContent(ext)) else { continue }
|
||||
if baseIsMixed != derivedMixed {
|
||||
errors.append("a complexContent extension of '\(ExtAllNode.stripPrefix(base))' must have the same mixed setting as its base")
|
||||
}
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
/// The declared `mixed` value of a `complexContent` derivation. An omitted
|
||||
/// value matters only when the extension adds element content; otherwise the
|
||||
/// base content is inherited unchanged and there is no mixedness conflict.
|
||||
private static func declaredMixed(_ complexContent: XSDTree, addsElementContent: Bool) -> Bool? {
|
||||
if let own = ExtAllNode.attribute(complexContent, "mixed") { return own == "true" || own == "1" }
|
||||
if let parent = complexContent.parent, let outer = ExtAllNode.attribute(parent, "mixed") { return outer == "true" || outer == "1" }
|
||||
return addsElementContent ? false : nil
|
||||
}
|
||||
|
||||
/// Whether a `complexContent` extension adds element content: an `element` or
|
||||
/// a wildcard `any` somewhere in its model group. An empty group (`<sequence/>`)
|
||||
/// or an attribute-only extension adds no element content, so it keeps the
|
||||
/// base's simple content and is not flagged.
|
||||
private static func extensionAddsElementContent(_ extension: XSDTree) -> Bool {
|
||||
/// base's simple content and is not flagged. Shared with the content-derivation
|
||||
/// checkers in `SchemaContentDerivation.swift`, so it is module-internal.
|
||||
static func extensionAddsElementContent(_ extension: XSDTree) -> Bool {
|
||||
let hasElement = descendants(`extension`, named: "element").contains { $0.name?.namespaceURI == xsdNamespace }
|
||||
let hasWildcard = descendants(`extension`, named: "any").contains { $0.name?.namespaceURI == xsdNamespace }
|
||||
return hasElement || hasWildcard
|
||||
|
||||
174
Tests/SchemaContentDerivationLocatedTests.swift
Normal file
174
Tests/SchemaContentDerivationLocatedTests.swift
Normal file
@ -0,0 +1,174 @@
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
|
||||
/// Content-derivation-pass schema compile findings (the `complexExtensionBaseValid`
|
||||
/// bundle: simpleContent/complexContent base-kind rules, a simpleContent
|
||||
/// restriction's inline-type faithfulness, the mixed-agreement rule, and an
|
||||
/// element value constraint over element-only content) carry validation coding
|
||||
/// paths that resolve to source spans, the IDE underline flow for #169. Each
|
||||
/// derivation error underlines its `extension`/`restriction` node (or the element
|
||||
/// declaration). Split from the other located-diagnostics suites to keep each
|
||||
/// suite within the type-body length budget.
|
||||
@Suite("Schema content-derivation-pass located compile diagnostics")
|
||||
struct SchemaContentDerivationLocatedTests {
|
||||
private func inconsistentFindings(in xsd: String) throws -> [PureXML.Validation.ValidationError] {
|
||||
do {
|
||||
_ = try PureXML.Schema.Document(xsd)
|
||||
Issue.record("expected schema compilation to fail")
|
||||
return []
|
||||
} catch let error as PureXML.Schema.SchemaError {
|
||||
guard case let .inconsistent(findings) = error else {
|
||||
Issue.record("expected .inconsistent, got \(error)")
|
||||
return []
|
||||
}
|
||||
return findings
|
||||
}
|
||||
}
|
||||
|
||||
@Test("A complexContent extension adding element content over a simpleContent base is located on the extension")
|
||||
func test_simpleContentExtensionBaseLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="base">
|
||||
<xs:simpleContent><xs:extension base="xs:string"/></xs:simpleContent>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="d">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="base">
|
||||
<xs:sequence><xs:element name="e" type="xs:string"/></xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("may not extend 'base', which has simple content") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType", "xs:complexContent", "xs:extension"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 7)
|
||||
}
|
||||
|
||||
@Test("A simpleContent restriction with a built-in base is located on the restriction")
|
||||
func test_simpleContentRestrictionBaseLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="t">
|
||||
<xs:simpleContent>
|
||||
<xs:restriction base="xs:string"/>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("a simpleContent restriction's base must be a complex type, not the built-in simple type 'string'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType", "xs:simpleContent", "xs:restriction"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 4)
|
||||
}
|
||||
|
||||
@Test("An unfaithful inline type in a simpleContent restriction is located on the restriction")
|
||||
func test_simpleContentRestrictionTypeLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="base">
|
||||
<xs:simpleContent><xs:extension base="xs:decimal"/></xs:simpleContent>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="d">
|
||||
<xs:simpleContent>
|
||||
<xs:restriction base="base">
|
||||
<xs:simpleType><xs:list itemType="xs:int"/></xs:simpleType>
|
||||
</xs:restriction>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("the inline simpleType in a simpleContent restriction is not a valid restriction of base type 'base'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType", "xs:simpleContent", "xs:restriction"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 7)
|
||||
}
|
||||
|
||||
@Test("A complexContent base that is a built-in simple type is located on the derivation")
|
||||
func test_complexContentBaseKindLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="t">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="xs:string"/>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("a complexContent base must be a complex type, not the built-in simple type 'string'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType", "xs:complexContent", "xs:extension"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 4)
|
||||
}
|
||||
|
||||
@Test("A simpleContent extension whose base is xs:anyType is located on the extension")
|
||||
func test_simpleContentExtensionBaseKindLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="t">
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:anyType"/>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings
|
||||
.first { $0.reason.contains("a simpleContent extension's base must be a simple type or a complex type with simple content, not 'anyType'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType", "xs:simpleContent", "xs:extension"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 4)
|
||||
}
|
||||
|
||||
@Test("An element value constraint over element-only content is located on the element")
|
||||
func test_elementValueConstraintContentLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="e" fixed="x">
|
||||
<xs:complexType><xs:sequence><xs:element name="c" type="xs:string"/></xs:sequence></xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("an element with a 'default' or 'fixed' value must have simple or mixed content") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 2)
|
||||
}
|
||||
|
||||
@Test("A complexContent extension with a mismatched mixed setting is located on the extension")
|
||||
func test_extensionMixedAgreementLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="base" mixed="true">
|
||||
<xs:sequence><xs:element name="a" type="xs:string"/></xs:sequence>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="d">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="base">
|
||||
<xs:sequence><xs:element name="b" type="xs:string"/></xs:sequence>
|
||||
</xs:extension>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("must have the same mixed setting as its base") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType", "xs:complexContent", "xs:extension"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 7)
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@ import Testing
|
||||
/// depend on: a bidirectional "derived mixed must equal base mixed" rule would
|
||||
/// wrongly reject the legal narrowing direction (it regressed four valid schemas
|
||||
/// when tried). The targets ctZ010d (extension) and ctZ010e (restriction) are
|
||||
/// already rejected by `extensionMixedAgreementErrors` and `ParticleRestriction`
|
||||
/// already rejected by `extensionMixedAgreementFindings` and `ParticleRestriction`
|
||||
/// respectively; this guards the restriction half from regressing.
|
||||
@Suite("XSD restriction mixed-content asymmetry (#184)")
|
||||
struct SchemaRestrictionMixedTests {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user