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:
Mihaela Mihaljevic 2026-06-21 17:45:41 +02:00
parent 2b465e9c9c
commit 4dd1d330cb
6 changed files with 161 additions and 64 deletions

View File

@ -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).

View File

@ -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) }
}
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}
}

View 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)
}
}