feat(schema): locate reference-resolution schema diagnostics
Close the last superficially-located checker (#169): referenceFindings was dispatched as located but its body wrapped the whole aggregate in .unlocated, so every reference-resolution error still collapsed onto the schema root. Convert all six sub-collectors to located findings, each attaching to the node in scope: the core type/base/itemType/memberTypes/ref/ substitutionGroup walk and the xs:-namespace builtin check locate on the referencing element; the simpleContent base-kind check on the restriction; the un-imported-namespace check on the referencing element; the redefine-component-existence check on the redefine's child component; and the import-namespace-agreement check on the import element. Every predicate, aggregation 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). Removes two dead helpers exposed along the way: the unused referenceErrors [String] wrapper and the unused locatedFindings helper in SchemaCompileValidation. The outer aggregation is rewritten from a single + chain to incremental appends to keep the type-checker within budget. Adds a located-diagnostics test per node-locatable sub-collector; the loader-dependent redefine/import families are exercised by the XSTS suite.
This commit is contained in:
parent
2b465e9c9c
commit
4dd1d330cb
@ -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`.
|
||||
- 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.
|
||||
|
||||
- DTD parse-time validity findings are warnings until strict validation promotes them; `validateAgainstInternalDTD` returns full findings (errors and warnings).
|
||||
|
||||
|
||||
@ -313,10 +313,6 @@ extension PureXML.Validation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func locatedFindings(_ reasons: [String], at node: PureXML.Model.TreeNode?) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
reasons.map { PureXML.Schema.SchemaLocatedFinding(reason: $0, node: node) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,13 +8,13 @@ extension PureXML.Schema.XSDParser {
|
||||
/// container (an unresolved import is the processor's choice and not an error),
|
||||
/// which is the false-positive boundary. Imports keep their own target namespace
|
||||
/// (no chameleon), so the imported document's literal `targetNamespace` is used.
|
||||
static func importNamespaceErrors(_ context: PureXML.Schema.XSDContext, _ containers: [XSDTree]) -> [String] {
|
||||
static func importNamespaceFindings(_ context: PureXML.Schema.XSDContext, _ containers: [XSDTree]) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
var locationTargetNS: [String: String?] = [:]
|
||||
for container in containers where ImportNode.localName(container) == "schema" {
|
||||
guard let location = context.containerLocations[ObjectIdentifier(container)] ?? nil else { continue }
|
||||
locationTargetNS[location] = ImportNode.attribute(container, "targetNamespace")
|
||||
}
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for container in containers {
|
||||
for node in descendants(container, named: "import") where node.name?.namespaceURI == xsdNamespace {
|
||||
guard let location = ImportNode.attribute(node, "schemaLocation"),
|
||||
@ -23,11 +23,14 @@ extension PureXML.Schema.XSDParser {
|
||||
let imported = locationTargetNS[location] ?? nil
|
||||
let declared = ImportNode.attribute(node, "namespace")
|
||||
if declared != imported {
|
||||
errors.append("import of '\(location)' declares namespace '\(declared ?? "(none)")', not the imported target namespace '\(imported ?? "(none)")'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "import of '\(location)' declares namespace '\(declared ?? "(none)")', not the imported target namespace '\(imported ?? "(none)")'",
|
||||
node: node,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
/// src-redefine.5: a type inside `xs:redefine` must restrict or extend the type it
|
||||
|
||||
@ -30,37 +30,26 @@ extension PureXML.Schema.XSDParser {
|
||||
/// Skipped when the document pulls in external definitions through
|
||||
/// `import`/`include`/`redefine`: the default compile does not load them, so
|
||||
/// the pools would be incomplete and a reference into them must not be flagged.
|
||||
static func referenceErrors(
|
||||
_ schema: XSDTree,
|
||||
in context: PureXML.Schema.XSDContext,
|
||||
elements: [String: PureXML.Schema.ElementType],
|
||||
containers: [XSDTree],
|
||||
) -> [String] {
|
||||
collectReferenceErrors(schema, in: context, elements: elements, containers: containers)
|
||||
}
|
||||
|
||||
static func referenceFindings(
|
||||
_ schema: XSDTree,
|
||||
in context: PureXML.Schema.XSDContext,
|
||||
elements: [String: PureXML.Schema.ElementType],
|
||||
containers: [XSDTree],
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
collectReferenceErrors(schema, in: context, elements: elements, containers: containers),
|
||||
)
|
||||
collectReferenceFindings(schema, in: context, elements: elements, containers: containers)
|
||||
}
|
||||
|
||||
private static func collectReferenceErrors(
|
||||
private static func collectReferenceFindings(
|
||||
_ schema: XSDTree,
|
||||
in context: PureXML.Schema.XSDContext,
|
||||
elements: [String: PureXML.Schema.ElementType],
|
||||
containers: [XSDTree],
|
||||
) -> [String] {
|
||||
let xsdErrors = xsdNamespaceReferenceErrors(schema)
|
||||
let simpleContentErrors = simpleContentBaseErrors(schema, in: context)
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let xsdFindings = xsdNamespaceReferenceFindings(schema)
|
||||
let simpleContentFindings = simpleContentBaseFindings(schema, in: context)
|
||||
if skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) {
|
||||
return xsdErrors + simpleContentErrors
|
||||
+ undeclaredNamespaceReferenceErrors(schema, containers: containers)
|
||||
return xsdFindings + simpleContentFindings
|
||||
+ undeclaredNamespaceReferenceFindings(schema, containers: containers)
|
||||
}
|
||||
let types = referenceBuiltins.union(context.simpleTypes.keys).union(context.complexTypeNodes.keys)
|
||||
let pools: [String: Set<String>] = [
|
||||
@ -73,7 +62,7 @@ extension PureXML.Schema.XSDParser {
|
||||
let foreignPools = context.compositionLoaded
|
||||
? foreignComponentPools(containers, mainTargetNamespace: targetNamespace)
|
||||
: [:]
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
let referenceSources: [(XSDTree, String?)]
|
||||
if context.compositionLoaded {
|
||||
let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: targetNamespace)
|
||||
@ -97,10 +86,14 @@ extension PureXML.Schema.XSDParser {
|
||||
foreignPools: foreignPools,
|
||||
chameleonNamespace: chameleonNamespace,
|
||||
)
|
||||
collectReferenceErrors(source, in: resolutionContext, inheritedBindings: sourceBindings, into: &errors)
|
||||
collectReferenceFindings(source, in: resolutionContext, inheritedBindings: sourceBindings, into: &findings)
|
||||
}
|
||||
return xsdErrors + simpleContentErrors + errors + redefineComponentExistenceErrors(containers)
|
||||
+ importNamespaceErrors(context, containers)
|
||||
var result = xsdFindings
|
||||
result += simpleContentFindings
|
||||
result += findings
|
||||
result += redefineComponentExistenceFindings(containers)
|
||||
result += importNamespaceFindings(context, containers)
|
||||
return result
|
||||
}
|
||||
|
||||
/// XSD 1.0 `src-redefine.6`/`7.2.1`/`7.2.2`: each component a `redefine` names
|
||||
@ -110,7 +103,7 @@ extension PureXML.Schema.XSDParser {
|
||||
/// redefinition whose name matches nothing in any non-`redefine` container (the
|
||||
/// loaded targets and the root) names a component that does not exist, and a
|
||||
/// name redefined twice in one `redefine` is a duplicate redefinition.
|
||||
private static func redefineComponentExistenceErrors(_ containers: [XSDTree]) -> [String] {
|
||||
private static func redefineComponentExistenceFindings(_ containers: [XSDTree]) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let kinds = ["complexType", "simpleType", "group", "attributeGroup"]
|
||||
var defined: [String: Set<String>] = [:]
|
||||
for container in containers where PureXML.Schema.XSDNode.localName(container) != "redefine" {
|
||||
@ -122,21 +115,27 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
}
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for container in containers where PureXML.Schema.XSDNode.localName(container) == "redefine" {
|
||||
var redefinedHere: [String: Set<String>] = [:]
|
||||
for kind in kinds {
|
||||
for node in PureXML.Schema.XSDNode.children(container, named: kind) {
|
||||
guard let name = PureXML.Schema.XSDNode.attribute(node, "name") else { continue }
|
||||
if !redefinedHere[kind, default: []].insert(name).inserted {
|
||||
errors.append("a redefine redefines \(kind) '\(name)' more than once")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a redefine redefines \(kind) '\(name)' more than once",
|
||||
node: node,
|
||||
))
|
||||
} else if defined[kind]?.contains(name) != true {
|
||||
errors.append("a redefine of \(kind) '\(name)' has no '\(name)' to redefine in the redefined schema")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a redefine of \(kind) '\(name)' has no '\(name)' to redefine in the redefined schema",
|
||||
node: node,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
/// Whether cross-document schema-validity rules should stand down: an external
|
||||
@ -173,11 +172,11 @@ extension PureXML.Schema.XSDParser {
|
||||
return local == "import" || local == "include" || local == "redefine"
|
||||
}
|
||||
|
||||
private static func collectReferenceErrors(
|
||||
private static func collectReferenceFindings(
|
||||
_ node: XSDTree,
|
||||
in resolutionContext: ResolutionContext,
|
||||
inheritedBindings: [String: String],
|
||||
into errors: inout [String],
|
||||
into findings: inout [PureXML.Schema.SchemaLocatedFinding],
|
||||
) {
|
||||
let local = PureXML.Schema.XSDNode.localName(node)
|
||||
if local == "appinfo" || local == "documentation" { return }
|
||||
@ -191,45 +190,45 @@ extension PureXML.Schema.XSDParser {
|
||||
chameleonNamespace: resolutionContext.check.chameleonNamespace,
|
||||
)
|
||||
if node.name?.namespaceURI == xsdNamespace, let local {
|
||||
errors += referenceErrors(at: node, local: local, in: nodeContext)
|
||||
findings += referenceFindings(at: node, local: local, in: nodeContext)
|
||||
}
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
||||
collectReferenceErrors(child, in: resolutionContext, inheritedBindings: bindings, into: &errors)
|
||||
collectReferenceFindings(child, in: resolutionContext, inheritedBindings: bindings, into: &findings)
|
||||
}
|
||||
}
|
||||
|
||||
private static func referenceErrors(
|
||||
private static func referenceFindings(
|
||||
at node: XSDTree,
|
||||
local: String,
|
||||
in resolutionContext: ResolutionContext,
|
||||
) -> [String] {
|
||||
var errors: [String] = []
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
let context = resolutionContext.check
|
||||
for attribute in ["type", "base", "itemType"] {
|
||||
guard let qname = PureXML.Schema.XSDNode.attribute(node, attribute) else { continue }
|
||||
if isUndeclaredReferenceType(qname, in: context) {
|
||||
errors.append("\(attribute) references undeclared type '\(qname)'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "\(attribute) references undeclared type '\(qname)'", node: node))
|
||||
}
|
||||
}
|
||||
if let members = PureXML.Schema.XSDNode.attribute(node, "memberTypes") {
|
||||
for token in members.split(whereSeparator: \.isWhitespace) {
|
||||
let qname = String(token)
|
||||
if isUndeclaredReferenceType(qname, in: context) {
|
||||
errors.append("memberTypes references undeclared type '\(qname)'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "memberTypes references undeclared type '\(qname)'", node: node))
|
||||
}
|
||||
}
|
||||
}
|
||||
if let head = PureXML.Schema.XSDNode.attribute(node, "substitutionGroup") {
|
||||
if isUndeclaredReferenceRef(head, poolName: "element", in: context) {
|
||||
errors.append("substitutionGroup references undeclared element '\(head)'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "substitutionGroup references undeclared element '\(head)'", node: node))
|
||||
}
|
||||
}
|
||||
if let reference = PureXML.Schema.XSDNode.attribute(node, "ref"), context.pools[local] != nil {
|
||||
if isUndeclaredReferenceRef(reference, poolName: local, in: context) {
|
||||
errors.append("\(local) ref references undeclared '\(reference)'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "\(local) ref references undeclared '\(reference)'", node: node))
|
||||
}
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
static func simpleTypeBaseNotComplexFindings(_ schema: XSDTree, in context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
@ -310,12 +309,12 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
|
||||
private static func xsdNamespaceReferenceErrors(_ schema: XSDTree) -> [String] {
|
||||
private static func xsdNamespaceReferenceFindings(_ schema: XSDTree) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let targetNamespace = PureXML.Schema.XSDNode.attribute(schema, "targetNamespace")
|
||||
if targetNamespace == xsdNamespace {
|
||||
return []
|
||||
}
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
|
||||
|
||||
func isUndeclaredBuiltin(_ qname: String) -> Bool {
|
||||
@ -335,14 +334,14 @@ extension PureXML.Schema.XSDParser {
|
||||
if node.name?.namespaceURI == xsdNamespace {
|
||||
for attribute in ["type", "base", "itemType"] {
|
||||
if let qname = PureXML.Schema.XSDNode.attribute(node, attribute), isUndeclaredBuiltin(qname) {
|
||||
errors.append("\(attribute) references undeclared type '\(qname)'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "\(attribute) references undeclared type '\(qname)'", node: node))
|
||||
}
|
||||
}
|
||||
if let members = PureXML.Schema.XSDNode.attribute(node, "memberTypes") {
|
||||
for token in members.split(whereSeparator: \.isWhitespace) {
|
||||
let qname = String(token)
|
||||
if isUndeclaredBuiltin(qname) {
|
||||
errors.append("memberTypes references undeclared type '\(qname)'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "memberTypes references undeclared type '\(qname)'", node: node))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -353,11 +352,11 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
check(schema)
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
private static func simpleContentBaseErrors(_ schema: XSDTree, in context: PureXML.Schema.XSDContext) -> [String] {
|
||||
var errors: [String] = []
|
||||
private static func simpleContentBaseFindings(_ schema: XSDTree, in context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
|
||||
let targetNamespace = context.targetNamespace
|
||||
|
||||
@ -380,7 +379,10 @@ extension PureXML.Schema.XSDParser {
|
||||
child.name?.namespaceURI == xsdNamespace && PureXML.Schema.XSDNode.localName(child) == "simpleContent"
|
||||
}
|
||||
if !hasSimpleContent {
|
||||
errors.append("base type '\(base)' of simpleContent restriction must be a simple type or complex type with simpleContent")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "base type '\(base)' of simpleContent restriction must be a simple type or complex type with simpleContent",
|
||||
node: node,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -392,6 +394,6 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
check(schema)
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ extension PureXML.Schema.XSDParser {
|
||||
/// unknown (that document might import the very namespace a reference names, as in
|
||||
/// corpus schZ004, where the main doc imports only `a` but `a`'s document imports
|
||||
/// `b`, making `b:b` resolvable), so the whole check stands down to stay lenient.
|
||||
static func undeclaredNamespaceReferenceErrors(_ schema: XSDTree, containers: [XSDTree]) -> [String] {
|
||||
static func undeclaredNamespaceReferenceFindings(_ schema: XSDTree, containers: [XSDTree]) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let sources = containers.isEmpty ? [schema] : containers
|
||||
if hasLocatedExternal(sources) { return [] }
|
||||
var declared: Set<String> = [xsdNamespace]
|
||||
@ -27,23 +27,23 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
}
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for container in sources where PureXML.Schema.XSDNode.localName(container) == "schema" {
|
||||
collectUndeclaredNamespaceReferences(
|
||||
container,
|
||||
inheritedBindings: PureXML.Schema.XSDNode.namespaceBindings(of: container),
|
||||
declared: declared,
|
||||
into: &errors,
|
||||
into: &findings,
|
||||
)
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
private static func collectUndeclaredNamespaceReferences(
|
||||
_ node: XSDTree,
|
||||
inheritedBindings: [String: String],
|
||||
declared: Set<String>,
|
||||
into errors: inout [String],
|
||||
into findings: inout [PureXML.Schema.SchemaLocatedFinding],
|
||||
) {
|
||||
let local = PureXML.Schema.XSDNode.localName(node)
|
||||
if local == "appinfo" || local == "documentation" { return }
|
||||
@ -54,11 +54,14 @@ extension PureXML.Schema.XSDParser {
|
||||
let uri = referenceURI(for: qname, bindings: bindings), !uri.isEmpty,
|
||||
!declared.contains(uri)
|
||||
else { continue }
|
||||
errors.append("reference to '\(qname)' names namespace '\(uri)', which is not the target namespace and is not imported")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "reference to '\(qname)' names namespace '\(uri)', which is not the target namespace and is not imported",
|
||||
node: node,
|
||||
))
|
||||
}
|
||||
}
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
||||
collectUndeclaredNamespaceReferences(child, inheritedBindings: bindings, declared: declared, into: &errors)
|
||||
collectUndeclaredNamespaceReferences(child, inheritedBindings: bindings, declared: declared, into: &findings)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
93
Tests/SchemaReferenceLocatedDiagnosticsTests.swift
Normal file
93
Tests/SchemaReferenceLocatedDiagnosticsTests.swift
Normal file
@ -0,0 +1,93 @@
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
|
||||
/// Reference-resolution-pass schema compile findings now carry validation coding
|
||||
/// paths that resolve to source spans, the IDE underline flow for #169.
|
||||
/// `referenceFindings` previously wrapped its whole aggregate in `.unlocated`, so
|
||||
/// every undeclared-reference error collapsed onto the schema root; each of its six
|
||||
/// sub-collectors now attaches to the referencing node. The redefine-existence and
|
||||
/// import-namespace families (which need a loaded external document) are exercised
|
||||
/// by the XSTS suite rather than here. Split from the other located-diagnostics
|
||||
/// suites to keep each suite within the type-body length budget.
|
||||
@Suite("Schema reference-pass located compile diagnostics")
|
||||
struct SchemaReferenceLocatedDiagnosticsTests {
|
||||
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("An undeclared element reference is located on the referencing element")
|
||||
func test_undeclaredElementRefLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="root">
|
||||
<xs:complexType><xs:sequence><xs:element ref="missing"/></xs:sequence></xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("element ref references undeclared 'missing'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element", "xs:complexType", "xs:sequence", "xs:element"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 3)
|
||||
}
|
||||
|
||||
@Test("An undeclared type reference is located on the referencing element")
|
||||
func test_undeclaredTypeRefLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="root" type="missing"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("type references undeclared type 'missing'") })
|
||||
#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 simpleContent restriction over a non-simpleContent base is located on the restriction")
|
||||
func test_simpleContentBaseLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="base"><xs:sequence/></xs:complexType>
|
||||
<xs:complexType name="t">
|
||||
<xs:simpleContent><xs:restriction base="base"/></xs:simpleContent>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("of simpleContent restriction must be a simple type or complex type with simpleContent") })
|
||||
#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("A reference to an un-imported namespace is located on the referencing element")
|
||||
func test_undeclaredNamespaceRefLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:foo="urn:foo" targetNamespace="urn:t">
|
||||
<xs:import namespace="urn:bar"/>
|
||||
<xs:element name="root" type="foo:T"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("names namespace 'urn:foo', which is not the target namespace and is not imported") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 3)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user