From 97c07e91f4d1a5b9dfc02fb7c536674aabaecea2 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Sun, 21 Jun 2026 19:15:38 +0200 Subject: [PATCH] feat(schema): locate composition/redefine-pass schema diagnostics Convert the five composition/redefine compile checkers to located findings, the last of the #169 burndown (#210-#217): a resolved schemaLocation whose content is not a schema underlines the include/import/redefine reference, a redefined type that does not restrict or extend itself underlines the redefined type, a redefined group or attribute group with a malformed self-reference underlines the offending reference or redefinition, and a redefined attribute group or model group that does not restrict its original underlines the redefinition. To locate the referenced-schema rule, failedSchemaReferences carries the referencing node, captured at the load-failure site in XSDContext. Only a top-level reference's node belongs to the main schema tree the diagnostic resolves against; a failure nested in a loaded sub-document is captured from a throwaway parse, so it stays unlocated (the prior schema-root behavior) rather than mis-resolve, a strict improvement. 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). The redefine checkers had exactly one caller each, so each is renamed in place and the .unlocated wrap dropped. Adds a located-diagnostics test per node-locatable checker; the attribute-group and model-group restriction redefine checkers share the same redefinition-node idiom and are exercised by the XSTS suite. --- CHANGELOG.md | 2 +- Sources/Schema/SchemaCompileValidation.swift | 4 - .../Schema/SchemaCompositionValidations.swift | 35 +++--- .../Schema/SchemaRedefineSelfReference.swift | 42 +++++--- Sources/Schema/XSDContext.swift | 15 ++- Sources/Schema/XSDDerivation.swift | 10 +- ...maCompositionLocatedDiagnosticsTests.swift | 100 ++++++++++++++++++ 7 files changed, 157 insertions(+), 51 deletions(-) create mode 100644 Tests/SchemaCompositionLocatedDiagnosticsTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 61ec0f8..19cb57f 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`. 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`. +- 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`. The composition/redefine-pass checkers complete the burndown (every schema compile checker now reports through `SchemaLocatedFinding`, with no `.unlocated` fallback left): a resolved `schemaLocation` whose content is not a schema underlines the `include`/`import`/`redefine` reference (its node is captured at the load-failure site), a redefined type that does not restrict or extend itself underlines the redefined type, a redefined group or attribute group with a malformed self-reference underlines the offending reference or redefinition, and a redefined attribute group or model group that does not restrict its original underlines the redefinition. - 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 06a1670..6331151 100644 --- a/Sources/Schema/SchemaCompileValidation.swift +++ b/Sources/Schema/SchemaCompileValidation.swift @@ -40,10 +40,6 @@ extension PureXML.Schema { struct SchemaLocatedFinding { let reason: String let node: PureXML.Model.TreeNode? - - static func unlocated(_ reasons: [String]) -> [SchemaLocatedFinding] { - reasons.map { SchemaLocatedFinding(reason: $0, node: nil) } - } } } diff --git a/Sources/Schema/SchemaCompositionValidations.swift b/Sources/Schema/SchemaCompositionValidations.swift index aaf5aa4..e353d21 100644 --- a/Sources/Schema/SchemaCompositionValidations.swift +++ b/Sources/Schema/SchemaCompositionValidations.swift @@ -6,11 +6,12 @@ extension PureXML.Validation.SchemaCompile { /// the processor's choice. static var referencedSchemasResolveToSchemas: PureXML.Validation.Validation { compileRule("A resolved schemaLocation reference is a valid schema") { document in - PureXML.Schema.SchemaLocatedFinding.unlocated( - document.context.failedSchemaReferences.map { - "the referenced schema document '\($0)' is not a valid schema" - }, - ) + document.context.failedSchemaReferences.map { + PureXML.Schema.SchemaLocatedFinding( + reason: "the referenced schema document '\($0.location)' is not a valid schema", + node: $0.node, + ) + } } } @@ -19,9 +20,7 @@ extension PureXML.Validation.SchemaCompile { /// namespace). static var redefinitionsDeriveFromThemselves: PureXML.Validation.Validation { compileRule("A redefined type restricts or extends itself") { document in - PureXML.Schema.SchemaLocatedFinding.unlocated( - PureXML.Schema.XSDParser.redefineDerivationErrors(document.containers), - ) + PureXML.Schema.XSDParser.redefineDerivationFindings(document.containers) } } @@ -29,9 +28,7 @@ extension PureXML.Validation.SchemaCompile { /// self-reference, and a group self-reference occurs exactly once. static var redefineSelfReferencesAreWellFormed: PureXML.Validation.Validation { compileRule("A redefined group or attribute group has a well-formed self-reference") { document in - PureXML.Schema.SchemaLocatedFinding.unlocated( - PureXML.Schema.XSDParser.redefineSelfReferenceErrors(document.containers), - ) + PureXML.Schema.XSDParser.redefineSelfReferenceFindings(document.containers) } } @@ -39,9 +36,7 @@ extension PureXML.Validation.SchemaCompile { /// it may not eliminate a required attribute use the original declares. static var redefinedAttributeGroupsKeepRequired: PureXML.Validation.Validation { compileRule("A redefined attribute group keeps the original's required attributes") { document in - PureXML.Schema.SchemaLocatedFinding.unlocated( - PureXML.Schema.XSDParser.redefineAttributeGroupRestrictionErrors(document.containers), - ) + PureXML.Schema.XSDParser.redefineAttributeGroupRestrictionFindings(document.containers) } } @@ -52,13 +47,11 @@ extension PureXML.Validation.SchemaCompile { static var redefinedGroupsRestrictBase: PureXML.Validation.Validation { compileRule("A redefined model group restricts the group it redefines") { document in guard let namedTypes = document.namedTypes, let derivation = document.derivation else { return [] } - return PureXML.Schema.SchemaLocatedFinding.unlocated( - PureXML.Schema.XSDParser.redefineGroupRestrictionErrors( - document.containers, - context: document.context, - types: namedTypes, - derivation: derivation.typeDerivation, - ), + return PureXML.Schema.XSDParser.redefineGroupRestrictionFindings( + document.containers, + context: document.context, + types: namedTypes, + derivation: derivation.typeDerivation, ) } } diff --git a/Sources/Schema/SchemaRedefineSelfReference.swift b/Sources/Schema/SchemaRedefineSelfReference.swift index cb3a663..a05e1a5 100644 --- a/Sources/Schema/SchemaRedefineSelfReference.swift +++ b/Sources/Schema/SchemaRedefineSelfReference.swift @@ -14,8 +14,8 @@ extension PureXML.Schema.XSDParser { /// BOUNDED: it stops at `element`/`complexType` scopes, so a recursive reference /// inside a nested element's content (a data-structure recursion) is not /// miscounted as a redefinition self-reference. - static func redefineSelfReferenceErrors(_ containers: [XSDTree]) -> [String] { - var errors: [String] = [] + static func redefineSelfReferenceFindings(_ containers: [XSDTree]) -> [PureXML.Schema.SchemaLocatedFinding] { + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] for container in containers where RedefineNode.localName(container) == "redefine" { let schema = RedefineNode.schemaOwner(container) let target = RedefineNode.attribute(schema, "targetNamespace") @@ -34,17 +34,23 @@ extension PureXML.Schema.XSDParser { let minOccurs = RedefineNode.attribute(ref, "minOccurs") ?? "1" let maxOccurs = RedefineNode.attribute(ref, "maxOccurs") ?? "1" if maxOccurs == "unbounded" || canonicalMagnitude(minOccurs) != "1" || canonicalMagnitude(maxOccurs) != "1" { - errors.append("a redefined group's self-reference must have minOccurs and maxOccurs of 1") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "a redefined group's self-reference must have minOccurs and maxOccurs of 1", + node: ref, + )) } } } if selfReferences > 1 { - errors.append("a redefined \(kind) may contain at most one self-reference") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "a redefined \(kind) may contain at most one self-reference", + node: definition, + )) } } } } - return errors + return findings } /// A redefinition of an `attributeGroup` is a RESTRICTION of the original @@ -55,18 +61,21 @@ extension PureXML.Schema.XSDParser { /// reference, which could re-introduce the attribute); the re-introduced- /// prohibited check fires only when the original has no attribute wildcard (which /// could otherwise admit the attribute). - static func redefineAttributeGroupRestrictionErrors(_ containers: [XSDTree]) -> [String] { + static func redefineAttributeGroupRestrictionFindings(_ containers: [XSDTree]) -> [PureXML.Schema.SchemaLocatedFinding] { let base = baseComponents(in: containers, named: "attributeGroup") - var errors: [String] = [] + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] for container in containers where RedefineNode.localName(container) == "redefine" { for redefinition in RedefineNode.children(container, named: "attributeGroup") { guard let name = RedefineNode.attribute(redefinition, "name"), let baseGroup = resolveBaseComponent(base, name: name, owner: redefinition) else { continue } - errors += attributeGroupRestrictionErrors(name: name, base: baseGroup, redefinition: redefinition) + // Each restriction violation is a property of this redefinition's + // attribute group, so it locates on the redefinition declaration. + findings += attributeGroupRestrictionErrors(name: name, base: baseGroup, redefinition: redefinition) + .map { PureXML.Schema.SchemaLocatedFinding(reason: $0, node: redefinition) } } } - return errors + return findings } /// The `{target-namespace}name` identity of a named component, keyed by its @@ -81,7 +90,7 @@ extension PureXML.Schema.XSDParser { /// keyed by namespaced identity: an attribute group or model group is identified by /// {name, target namespace}, so a same-local-name component imported from a /// different namespace is not mistaken for the redefinition's base (the namespace - /// guard `redefineSelfReferenceErrors` already uses). This is the pool a + /// guard `redefineSelfReferenceFindings` already uses). This is the pool a /// redefinition's base is drawn from. private static func baseComponents(in containers: [XSDTree], named kind: String) -> [String: XSDTree] { var base: [String: XSDTree] = [:] @@ -171,14 +180,14 @@ extension PureXML.Schema.XSDParser { /// well-formedness rule instead, and cannot be re-parsed reliably here). An /// order-independent `all` reordering accepts the same language, so the oracle /// correctly does not flag it; only a genuine content widening is reported. - static func redefineGroupRestrictionErrors( + static func redefineGroupRestrictionFindings( _ containers: [XSDTree], context: PureXML.Schema.XSDContext, types: [String: PureXML.Schema.ElementType], derivation: [String: PureXML.Schema.TypeDerivation], - ) -> [String] { + ) -> [PureXML.Schema.SchemaLocatedFinding] { let base = baseComponents(in: containers, named: "group") - var errors: [String] = [] + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] for container in containers where RedefineNode.localName(container) == "redefine" { for redefinition in RedefineNode.children(container, named: "group") { guard let name = RedefineNode.attribute(redefinition, "name"), @@ -197,11 +206,14 @@ extension PureXML.Schema.XSDParser { if let reason = PureXML.Schema.ParticleRestriction.violation( restricted: .elementOnly(redefined), base: .elementOnly(baseline), types: types, derivation: derivation, ) { - errors.append("a redefined model group '\(name)' must restrict the group it redefines: \(reason)") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "a redefined model group '\(name)' must restrict the group it redefines: \(reason)", + node: redefinition, + )) } } } - return errors + return findings } /// Whether `particle` or any of its descendants can never occur (`maxOccurs=0`), a diff --git a/Sources/Schema/XSDContext.swift b/Sources/Schema/XSDContext.swift index 3eb7952..a01fda9 100644 --- a/Sources/Schema/XSDContext.swift +++ b/Sources/Schema/XSDContext.swift @@ -148,8 +148,9 @@ extension PureXML.Schema { _ schema: XSDTree, _ loader: (String) -> String?, _ visited: inout Set, - ) -> [String] { - var failures: [String] = [] + topLevel: Bool = true, + ) -> [(location: String, node: XSDTree?)] { + var failures: [(location: String, node: XSDTree?)] = [] for child in elementChildren(schema) { let kind = localName(child) guard kind == "include" || kind == "import" || kind == "redefine", @@ -160,10 +161,14 @@ extension PureXML.Schema { guard let root = try? PureXML.parseTree(content), let sub = elementChildren(root).first(where: { localName($0) == "schema" }) else { - failures.append(location) + // Only a top-level reference's node belongs to the main schema tree + // the diagnostic resolves against. A failure nested in a loaded + // sub-document is captured from a throwaway parse, so it stays + // unlocated (the prior schema-root behavior) rather than mis-resolve. + failures.append((location: location, node: topLevel ? child : nil)) continue } - failures += failedSchemaReferences(sub, loader, &visited) + failures += failedSchemaReferences(sub, loader, &visited, topLevel: false) } return failures } @@ -346,7 +351,7 @@ extension PureXML.Schema { /// resolved to content which is not a well-formed schema document. Recorded /// during composition so a validation can report each (`src-resolve`); an /// unresolved location (the loader returned nothing) is not recorded. - var failedSchemaReferences: [String] = [] + var failedSchemaReferences: [(location: String, node: XSDTree?)] = [] /// Target namespace imposed on an included schema with no `targetNamespace` /// (chameleon include), keyed by that included schema container. var chameleonTargetNamespaces: [ObjectIdentifier: String] = [:] diff --git a/Sources/Schema/XSDDerivation.swift b/Sources/Schema/XSDDerivation.swift index 723e944..385b199 100644 --- a/Sources/Schema/XSDDerivation.swift +++ b/Sources/Schema/XSDDerivation.swift @@ -283,8 +283,8 @@ extension PureXML.Schema.XSDParser { /// type it redefines (same local name, in the redefining schema's own target /// namespace). Returns one finding per offending type, for a validation to /// report; it does not throw, so the finding flows through the validator. - static func redefineDerivationErrors(_ containers: [XSDTree]) -> [String] { - var errors: [String] = [] + static func redefineDerivationFindings(_ containers: [XSDTree]) -> [PureXML.Schema.SchemaLocatedFinding] { + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] for container in containers where XSDDerivNode.localName(container) == "redefine" { for type in XSDDerivNode.children(container, named: "complexType") { guard let name = XSDDerivNode.attribute(type, "name") else { continue } @@ -294,7 +294,7 @@ extension PureXML.Schema.XSDParser { .flatMap { XSDDerivNode.firstChild($0, named: "extension") ?? XSDDerivNode.firstChild($0, named: "restriction") } .flatMap { XSDDerivNode.attribute($0, "base") } if derivationInfo(type)?.base != name || redefineBaseIsForeign(type, rawBase) { - errors.append("redefined type '\(name)' must restrict or extend itself") + findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "redefined type '\(name)' must restrict or extend itself", node: type)) } } for type in XSDDerivNode.children(container, named: "simpleType") { @@ -302,11 +302,11 @@ extension PureXML.Schema.XSDParser { let rawBase = XSDDerivNode.firstChild(type, named: "restriction") .flatMap { XSDDerivNode.attribute($0, "base") } if rawBase.map(XSDDerivNode.stripPrefix) != name || redefineBaseIsForeign(type, rawBase) { - errors.append("redefined type '\(name)' must restrict or extend itself") + findings.append(PureXML.Schema.SchemaLocatedFinding(reason: "redefined type '\(name)' must restrict or extend itself", node: type)) } } } - return errors + return findings } /// Drops from each substitution group the members a head's `block` forbids: a diff --git a/Tests/SchemaCompositionLocatedDiagnosticsTests.swift b/Tests/SchemaCompositionLocatedDiagnosticsTests.swift new file mode 100644 index 0000000..517a35c --- /dev/null +++ b/Tests/SchemaCompositionLocatedDiagnosticsTests.swift @@ -0,0 +1,100 @@ +import Testing +@testable import PureXML + +/// Composition/redefine-pass schema compile findings (a resolved schemaLocation +/// whose content is not a schema, a redefined type that does not restrict or extend +/// itself, and a redefined group self-reference with an illegal occurrence) carry +/// validation coding paths that resolve to source spans, the IDE underline flow for +/// #169. These rules require a loaded external document, so the tests supply a +/// `schemaLoader`; the redefine attribute-group and model-group restriction checkers +/// share the same node-location idiom (the redefinition declaration) and are +/// exercised by the XSTS suite. Split from the other located-diagnostics suites to +/// keep each suite within the type-body length budget. +@Suite("Schema composition/redefine-pass located compile diagnostics") +struct SchemaCompositionLocatedDiagnosticsTests { + private func inconsistentFindings( + in xsd: String, + loader: @escaping (String) -> String?, + ) throws -> [PureXML.Validation.ValidationError] { + do { + _ = try PureXML.Schema.Document(xsd, schemaLoader: loader) + 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 schemaLocation resolving to non-schema content is located on the reference") + func test_referencedSchemaNotValidLocated() throws { + let xsd = """ + + + + + """ + let findings = try inconsistentFindings(in: xsd) { _ in "" } + let located = try #require(findings.first { $0.reason.contains("the referenced schema document 'broken.xsd' is not a valid schema") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:include"]) + + let (tree, _) = PureXML.readTree(xsd) + #expect(tree.sourceRange(at: located.codingPath)?.start.line == 2) + } + + @Test("A redefined type that does not restrict or extend itself is located on the type") + func test_redefineDerivationLocated() throws { + let base = """ + + + + + """ + let xsd = """ + + + + + + + + + + """ + let findings = try inconsistentFindings(in: xsd) { _ in base } + let located = try #require(findings.first { $0.reason.contains("redefined type 'ct' must restrict or extend itself") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:redefine", "xs:complexType"]) + + let (tree, _) = PureXML.readTree(xsd) + #expect(tree.sourceRange(at: located.codingPath)?.start.line == 3) + } + + @Test("A redefined group self-reference with an illegal occurrence is located on the reference") + func test_redefineSelfReferenceLocated() throws { + let base = """ + + + + """ + let xsd = """ + + + + + + + + + + """ + let findings = try inconsistentFindings(in: xsd) { _ in base } + let located = try #require(findings.first { $0.reason.contains("a redefined group's self-reference must have minOccurs and maxOccurs of 1") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:redefine", "xs:group", "xs:sequence", "xs:group"]) + + let (tree, _) = PureXML.readTree(xsd) + #expect(tree.sourceRange(at: located.codingPath)?.start.line == 5) + } +}