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