feat(schema): locate names/UPA/notation-pass schema diagnostics
Convert the three names/UPA/notation-pass compile checkers to located findings so each error attaches to its declaring node instead of the schema root (#169): component-name uniqueness underlines the second duplicate global, identity-constraint name uniqueness underlines the second constraint, a keyref whose refer names no key/unique (or whose arity differs) underlines the keyref (KeyrefInfo gains the keyref node), a Unique Particle Attribution violation underlines the ambiguous content-model group, and notation validity underlines the notation or enumeration. Every predicate, aggregation order, reason string, and the message-dedup of the UPA pass are unchanged; only the location each finding carries differs, verified conformance-neutral by the full XSTS suite (14383 groups, pinned counts unchanged). The findUniqueIndices circular-import (rootIndex) workaround is left byte-identical, since it governs which containers are examined, not where findings locate. 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 sub-check.
This commit is contained in:
parent
c6dd0f7d2f
commit
96ab396eb3
@ -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. 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`. The reference-resolution rule is now genuinely located: `referenceFindings` previously wrapped its whole aggregate in `.unlocated`, so every undeclared-reference error collapsed onto the schema root despite the rule being dispatched as located; each of its six sub-collectors (the core type/base/itemType/memberTypes/ref/substitutionGroup walk, the `xs:`-namespace builtin check, the simpleContent base-kind check, the un-imported-namespace check, the redefine-component-existence check, and the import-namespace-agreement check) now attaches to its referencing node, `import`, or `redefine` component. The extension/substitution-pass checkers follow: an `all` group reached through an extension over a base with content underlines the `extension`, an anonymous complex-type restriction that is not a subset of its base underlines the `restriction`, and a substitution-group member whose type is not validly derived from its head underlines the member `element`.
|
||||
- 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`. The reference-resolution rule is now genuinely located: `referenceFindings` previously wrapped its whole aggregate in `.unlocated`, so every undeclared-reference error collapsed onto the schema root despite the rule being dispatched as located; each of its six sub-collectors (the core type/base/itemType/memberTypes/ref/substitutionGroup walk, the `xs:`-namespace builtin check, the simpleContent base-kind check, the un-imported-namespace check, the redefine-component-existence check, and the import-namespace-agreement check) now attaches to its referencing node, `import`, or `redefine` component. The extension/substitution-pass checkers follow: an `all` group reached through an extension over a base with content underlines the `extension`, an anonymous complex-type restriction that is not a subset of its base underlines the `restriction`, and a substitution-group member whose type is not validly derived from its head underlines the member `element`. The names/UPA/notation-pass checkers follow: a duplicate global component name underlines the second declaration, a duplicate identity-constraint name underlines the second constraint, a `keyref` whose `refer` names no key/unique (or whose field arity differs) underlines the `keyref`, a Unique Particle Attribution violation underlines the ambiguous content-model group, and a notation missing both `public` and `system`, a notation with forbidden content, or a `NOTATION` enumeration naming no declared notation underline the `notation` or `enumeration`.
|
||||
|
||||
- DTD parse-time validity findings are warnings until strict validation promotes them; `validateAgainstInternalDTD` returns full findings (errors and warnings).
|
||||
|
||||
|
||||
@ -18,19 +18,24 @@ public extension PureXML.Schema {
|
||||
private static let modelGroupNames: Set<String> = ["sequence", "choice", "all"]
|
||||
private static let particleNames: Set<String> = ["element", "sequence", "choice", "all", "group", "any"]
|
||||
|
||||
/// Every UPA-violation message across the schema's complex-type content
|
||||
/// models, deduplicated in first-seen order.
|
||||
static func violations(in schema: XSDTree, context: XSDContext) -> [String] {
|
||||
/// Every UPA-violation finding across the schema's complex-type content
|
||||
/// models, deduplicated by message in first-seen order. Each finding is
|
||||
/// located on the content-model node (`sequence`/`choice`/`all`/`group`)
|
||||
/// whose particles are ambiguous; a message shared by two models locates on
|
||||
/// the first, preserving the deduplicated count.
|
||||
static func violationFindings(in schema: XSDTree, context: XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let bindings = namespaceBindings(schema)
|
||||
var messages: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
var seen: Set<String> = []
|
||||
forEachComplexType(schema) { complexType in
|
||||
guard let model = contentModelNode(complexType),
|
||||
let conflict = violation(model, bindings, context)
|
||||
else { return }
|
||||
if seen.insert(conflict).inserted { messages.append(conflict) }
|
||||
if seen.insert(conflict).inserted {
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(reason: conflict, node: model))
|
||||
}
|
||||
}
|
||||
return messages
|
||||
return findings
|
||||
}
|
||||
|
||||
/// The prefix-to-namespace bindings declared on the schema element (its
|
||||
|
||||
@ -69,9 +69,7 @@ extension PureXML.Validation {
|
||||
|
||||
static var componentNamesUnique: Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("Global schema component names are unique within their symbol spaces") { document in
|
||||
PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.XSDParser.componentNameErrors(document.schema, document.containers, document.context),
|
||||
)
|
||||
PureXML.Schema.XSDParser.componentNameFindings(document.schema, document.containers, document.context)
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,9 +85,7 @@ extension PureXML.Validation {
|
||||
|
||||
static var contentModelsDeterministic: Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("Schema content models are deterministic (UPA)") { document in
|
||||
PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.ContentModelDeterminism.violations(in: document.schema, context: document.context),
|
||||
)
|
||||
PureXML.Schema.ContentModelDeterminism.violationFindings(in: document.schema, context: document.context)
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,9 +205,7 @@ extension PureXML.Validation {
|
||||
|
||||
static var notationsValid: Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("Notation declarations are valid") { document in
|
||||
PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.XSDParser.notationValidityErrors(document.containers, document.context),
|
||||
)
|
||||
PureXML.Schema.XSDParser.notationValidityFindings(document.containers, document.context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ extension PureXML.Schema.XSDParser {
|
||||
/// Only the document's own globals are examined (`xs:redefine` children and
|
||||
/// included documents are not direct globals here), so a redefinition is not
|
||||
/// mistaken for a clash.
|
||||
static func componentNameErrors(_ schema: XSDTree, _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [String] {
|
||||
static func componentNameFindings(_ schema: XSDTree, _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: context.targetNamespace)
|
||||
let uniqueIndices = findUniqueIndices(
|
||||
in: containers,
|
||||
@ -19,20 +19,20 @@ extension PureXML.Schema.XSDParser {
|
||||
mainTargetNamespace: context.targetNamespace,
|
||||
)
|
||||
|
||||
var errors = checkDuplicateGlobals(
|
||||
var findings = checkDuplicateGlobalFindings(
|
||||
in: containers,
|
||||
indices: uniqueIndices,
|
||||
namespaceMap: namespaceMap,
|
||||
mainTargetNamespace: context.targetNamespace,
|
||||
)
|
||||
errors += identityConstraintNameErrors(
|
||||
findings += identityConstraintNameFindings(
|
||||
containers,
|
||||
uniqueIndices,
|
||||
namespaceMap: namespaceMap,
|
||||
mainTargetNamespace: context.targetNamespace,
|
||||
)
|
||||
errors += keyrefReferErrors(schema, containers, context)
|
||||
return errors
|
||||
findings += keyrefReferFindings(schema, containers, context)
|
||||
return findings
|
||||
}
|
||||
|
||||
private static func findUniqueIndices(
|
||||
@ -73,13 +73,13 @@ extension PureXML.Schema.XSDParser {
|
||||
let name: String
|
||||
}
|
||||
|
||||
private static func checkDuplicateGlobals(
|
||||
private static func checkDuplicateGlobalFindings(
|
||||
in containers: [XSDTree],
|
||||
indices: [Int],
|
||||
namespaceMap: [Int: String?],
|
||||
mainTargetNamespace: String?,
|
||||
) -> [String] {
|
||||
var errors: [String] = []
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
var seenComponents: Set<ComponentKey> = []
|
||||
for index in indices {
|
||||
let container = containers[index]
|
||||
@ -99,11 +99,14 @@ extension PureXML.Schema.XSDParser {
|
||||
if !seenComponents.insert(key).inserted {
|
||||
let nsStr = namespaceURI.map { " in namespace '\($0)'" } ?? " in no namespace"
|
||||
let kindLabel = kind == "group" ? "model group" : (kind == "attributeGroup" ? "attribute group" : kind)
|
||||
errors.append("duplicate \(kindLabel) name '\(name)'\(nsStr)")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "duplicate \(kindLabel) name '\(name)'\(nsStr)",
|
||||
node: global,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
private static func componentKind(for localName: String) -> String? {
|
||||
@ -122,6 +125,7 @@ extension PureXML.Schema.XSDParser {
|
||||
let name: String
|
||||
let refer: String
|
||||
let arity: Int
|
||||
let node: XSDTree
|
||||
}
|
||||
|
||||
/// Findings for a `keyref` whose `refer` does not name a `key` or `unique` in
|
||||
@ -129,7 +133,7 @@ extension PureXML.Schema.XSDParser {
|
||||
/// Skipped when the document pulls in external definitions
|
||||
/// (`import`/`include`/`redefine`), which the default compile does not load,
|
||||
/// so a `refer` into them is never flagged.
|
||||
private static func keyrefReferErrors(_ schema: XSDTree, _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [String] {
|
||||
private static func keyrefReferFindings(_ schema: XSDTree, _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
if skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) { return [] }
|
||||
var keyArities: [String: Int] = [:]
|
||||
var keyrefs: [KeyrefInfo] = []
|
||||
@ -138,17 +142,23 @@ extension PureXML.Schema.XSDParser {
|
||||
collectKeysAndRefers(source, keyArities: &keyArities, keyrefs: &keyrefs)
|
||||
}
|
||||
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for keyref in keyrefs {
|
||||
if let keyArity = keyArities[keyref.refer] {
|
||||
if keyArity != keyref.arity {
|
||||
errors.append("keyref '\(keyref.name)' and its referenced key/unique '\(keyref.refer)' must have the same number of fields")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "keyref '\(keyref.name)' and its referenced key/unique '\(keyref.refer)' must have the same number of fields",
|
||||
node: keyref.node,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
errors.append("keyref refers to undeclared key or unique '\(keyref.refer)'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "keyref refers to undeclared key or unique '\(keyref.refer)'",
|
||||
node: keyref.node,
|
||||
))
|
||||
}
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
private static func collectKeysAndRefers(
|
||||
@ -170,7 +180,7 @@ extension PureXML.Schema.XSDParser {
|
||||
if let name, let refer {
|
||||
let arity = PureXML.Schema.XSDNode.elementChildren(node)
|
||||
.count(where: { $0.name?.namespaceURI == xsdNamespace && PureXML.Schema.XSDNode.localName($0) == "field" })
|
||||
keyrefs.append(KeyrefInfo(name: name, refer: PureXML.Schema.XSDNode.stripPrefix(refer), arity: arity))
|
||||
keyrefs.append(KeyrefInfo(name: name, refer: PureXML.Schema.XSDNode.stripPrefix(refer), arity: arity, node: node))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -179,18 +189,18 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
|
||||
private static func identityConstraintNameErrors(
|
||||
private static func identityConstraintNameFindings(
|
||||
_ containers: [XSDTree],
|
||||
_ uniqueIndices: [Int],
|
||||
namespaceMap: [Int: String?],
|
||||
mainTargetNamespace: String?,
|
||||
) -> [String] {
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
struct KeyrefNameKey: Hashable {
|
||||
let targetNamespace: String?
|
||||
let name: String
|
||||
}
|
||||
var seen: Set<KeyrefNameKey> = []
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
func walk(_ node: XSDTree, namespaceURI: String?) {
|
||||
let local = PureXML.Schema.XSDNode.localName(node)
|
||||
if local == "appinfo" || local == "documentation" { return }
|
||||
@ -198,7 +208,10 @@ extension PureXML.Schema.XSDParser {
|
||||
let key = KeyrefNameKey(targetNamespace: namespaceURI, name: name)
|
||||
if !seen.insert(key).inserted {
|
||||
let nsStr = namespaceURI.map { " in namespace '\($0)'" } ?? " in no namespace"
|
||||
errors.append("duplicate identity constraint name '\(name)'\(nsStr)")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "duplicate identity constraint name '\(name)'\(nsStr)",
|
||||
node: node,
|
||||
))
|
||||
}
|
||||
}
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
||||
@ -211,7 +224,7 @@ extension PureXML.Schema.XSDParser {
|
||||
let namespaceURI = namespaceMap[index] ?? mainTargetNamespace
|
||||
walk(container, namespaceURI: namespaceURI)
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
/// The `name` of `node` when it is an identity constraint (`unique`/`key`/
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
extension PureXML.Schema.XSDParser {
|
||||
static func notationValidityErrors(
|
||||
static func notationValidityFindings(
|
||||
_ containers: [XSDTree],
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
) -> [String] {
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: context.targetNamespace)
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
var declaredNotations: Set<PureXML.Model.QualifiedName> = []
|
||||
|
||||
// 1. Collect all declared notations and validate notation declarations
|
||||
@ -19,10 +19,16 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
let name = PureXML.Schema.XSDNode.attribute(child, "name") ?? ""
|
||||
if PureXML.Schema.XSDNode.attribute(child, "public") == nil, PureXML.Schema.XSDNode.attribute(child, "system") == nil {
|
||||
errors.append("notation '\(name)' must specify at least one of public or system attributes")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "notation '\(name)' must specify at least one of public or system attributes",
|
||||
node: child,
|
||||
))
|
||||
}
|
||||
if notationHasForbiddenContent(child) {
|
||||
errors.append("notation '\(name)' may contain only an optional annotation, no other element or character content")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "notation '\(name)' may contain only an optional annotation, no other element or character content",
|
||||
node: child,
|
||||
))
|
||||
}
|
||||
let qName = PureXML.Model.QualifiedName(localName: name, namespaceURI: containerNamespace)
|
||||
declaredNotations.insert(qName)
|
||||
@ -32,10 +38,10 @@ extension PureXML.Schema.XSDParser {
|
||||
// 2. Validate notation enumeration constraints on simpleTypes
|
||||
for index in containers.indices {
|
||||
let container = containers[index]
|
||||
walkSimpleTypes(container, context: context, declaredNotations: declaredNotations, into: &errors)
|
||||
walkSimpleTypes(container, context: context, declaredNotations: declaredNotations, into: &findings)
|
||||
}
|
||||
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
/// Whether a `notation` declaration carries content its `(annotation?)` model
|
||||
@ -56,17 +62,17 @@ extension PureXML.Schema.XSDParser {
|
||||
_ node: XSDTree,
|
||||
context: PureXML.Schema.XSDContext,
|
||||
declaredNotations: Set<PureXML.Model.QualifiedName>,
|
||||
into errors: inout [String],
|
||||
into findings: inout [PureXML.Schema.SchemaLocatedFinding],
|
||||
) {
|
||||
let local = PureXML.Schema.XSDNode.localName(node)
|
||||
if local == "appinfo" || local == "documentation" { return }
|
||||
|
||||
if node.name?.namespaceURI == xsdNamespace, local == "restriction" {
|
||||
validateNotationRestriction(node, context: context, declaredNotations: declaredNotations, into: &errors)
|
||||
validateNotationRestriction(node, context: context, declaredNotations: declaredNotations, into: &findings)
|
||||
}
|
||||
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
||||
walkSimpleTypes(child, context: context, declaredNotations: declaredNotations, into: &errors)
|
||||
walkSimpleTypes(child, context: context, declaredNotations: declaredNotations, into: &findings)
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,7 +80,7 @@ extension PureXML.Schema.XSDParser {
|
||||
_ node: XSDTree,
|
||||
context: PureXML.Schema.XSDContext,
|
||||
declaredNotations: Set<PureXML.Model.QualifiedName>,
|
||||
into errors: inout [String],
|
||||
into findings: inout [PureXML.Schema.SchemaLocatedFinding],
|
||||
) {
|
||||
guard let parent = node.parent,
|
||||
parent.name?.namespaceURI == xsdNamespace,
|
||||
@ -99,7 +105,10 @@ extension PureXML.Schema.XSDParser {
|
||||
return dec.localName == valQName.localName && decNS == valNS
|
||||
}
|
||||
if !isDeclared {
|
||||
errors.append("notation enumeration value '\(value)' does not name a declared notation")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "notation enumeration value '\(value)' does not name a declared notation",
|
||||
node: child,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
132
Tests/SchemaNamesUPANotationLocatedTests.swift
Normal file
132
Tests/SchemaNamesUPANotationLocatedTests.swift
Normal file
@ -0,0 +1,132 @@
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
|
||||
/// Names/UPA/notation-pass schema compile findings (component-name uniqueness,
|
||||
/// identity-constraint name uniqueness, keyref `refer` resolution, Unique Particle
|
||||
/// Attribution, and notation validity) carry validation coding paths that resolve
|
||||
/// to source spans, the IDE underline flow for #169. Split from the other
|
||||
/// located-diagnostics suites to keep each suite within the type-body length budget.
|
||||
@Suite("Schema names/UPA/notation-pass located compile diagnostics")
|
||||
struct SchemaNamesUPANotationLocatedTests {
|
||||
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 duplicate global type name is located on the second declaration")
|
||||
func test_duplicateGlobalLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="t"><xs:sequence/></xs:complexType>
|
||||
<xs:complexType name="t"><xs:sequence/></xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("duplicate type name 't'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 3)
|
||||
}
|
||||
|
||||
@Test("A duplicate identity-constraint name is located on the second constraint")
|
||||
func test_duplicateIdentityConstraintLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="root">
|
||||
<xs:complexType><xs:sequence><xs:element name="c" type="xs:string"/></xs:sequence></xs:complexType>
|
||||
<xs:key name="dup"><xs:selector xpath="c"/><xs:field xpath="."/></xs:key>
|
||||
<xs:unique name="dup"><xs:selector xpath="c"/><xs:field xpath="."/></xs:unique>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("duplicate identity constraint name 'dup'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element", "xs:unique"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 5)
|
||||
}
|
||||
|
||||
@Test("A keyref to an undeclared key is located on the keyref")
|
||||
func test_keyrefUndeclaredLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="root">
|
||||
<xs:complexType><xs:sequence/></xs:complexType>
|
||||
<xs:keyref name="kr" refer="nope"><xs:selector xpath="."/><xs:field xpath="@a"/></xs:keyref>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("keyref refers to undeclared key or unique 'nope'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element", "xs:keyref"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 4)
|
||||
}
|
||||
|
||||
@Test("A UPA violation is located on the ambiguous content model")
|
||||
func test_upaViolationLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="t">
|
||||
<xs:choice>
|
||||
<xs:element name="a" type="xs:string"/>
|
||||
<xs:element name="a" type="xs:int"/>
|
||||
</xs:choice>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("deterministic") || $0.reason.contains("ambiguous") || $0.reason.contains("Unique Particle") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType", "xs:choice"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 3)
|
||||
}
|
||||
|
||||
@Test("A notation missing public and system is located on the notation")
|
||||
func test_notationMissingAttributesLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:notation name="n"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("must specify at least one of public or system") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:notation"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 2)
|
||||
}
|
||||
|
||||
@Test("A notation enumeration naming no declared notation is located on the enumeration")
|
||||
func test_notationEnumerationLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:notation name="n" system="x"/>
|
||||
<xs:simpleType name="t">
|
||||
<xs:restriction base="xs:NOTATION">
|
||||
<xs:enumeration value="undeclared"/>
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("notation enumeration value 'undeclared' does not name a declared notation") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:simpleType", "xs:restriction", "xs:enumeration"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 5)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user