From 4dd1d330cb27de8e325e72dd0b7426a229a66bf7 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Sun, 21 Jun 2026 17:45:41 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 2 +- Sources/Schema/SchemaCompileValidation.swift | 4 - Sources/Schema/SchemaImportNamespace.swift | 11 ++- Sources/Schema/SchemaReferences.swift | 98 ++++++++++--------- .../Schema/SchemaUndeclaredNamespace.swift | 17 ++-- ...hemaReferenceLocatedDiagnosticsTests.swift | 93 ++++++++++++++++++ 6 files changed, 161 insertions(+), 64 deletions(-) create mode 100644 Tests/SchemaReferenceLocatedDiagnosticsTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index a88dcd7..33d9f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/Sources/Schema/SchemaCompileValidation.swift b/Sources/Schema/SchemaCompileValidation.swift index e7e6bf3..64eece7 100644 --- a/Sources/Schema/SchemaCompileValidation.swift +++ b/Sources/Schema/SchemaCompileValidation.swift @@ -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) } - } } } diff --git a/Sources/Schema/SchemaImportNamespace.swift b/Sources/Schema/SchemaImportNamespace.swift index 9d7fd34..949caa2 100644 --- a/Sources/Schema/SchemaImportNamespace.swift +++ b/Sources/Schema/SchemaImportNamespace.swift @@ -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 diff --git a/Sources/Schema/SchemaReferences.swift b/Sources/Schema/SchemaReferences.swift index 9ef73c5..60f1435 100644 --- a/Sources/Schema/SchemaReferences.swift +++ b/Sources/Schema/SchemaReferences.swift @@ -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] = [ @@ -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] = [:] 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] = [:] 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 } } diff --git a/Sources/Schema/SchemaUndeclaredNamespace.swift b/Sources/Schema/SchemaUndeclaredNamespace.swift index c48a555..cb6843c 100644 --- a/Sources/Schema/SchemaUndeclaredNamespace.swift +++ b/Sources/Schema/SchemaUndeclaredNamespace.swift @@ -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 = [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, - 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) } } diff --git a/Tests/SchemaReferenceLocatedDiagnosticsTests.swift b/Tests/SchemaReferenceLocatedDiagnosticsTests.swift new file mode 100644 index 0000000..60cc920 --- /dev/null +++ b/Tests/SchemaReferenceLocatedDiagnosticsTests.swift @@ -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 = """ + + + + + + """ + 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 = """ + + + + """ + 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 = """ + + + + + + + """ + 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 = """ + + + + + """ + 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) + } +}