diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b63e4a..61ec0f8 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`. +- 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`. - DTD parse-time validity findings are warnings until strict validation promotes them; `validateAgainstInternalDTD` returns full findings (errors and warnings). diff --git a/Sources/Schema/ContentModelDeterminism.swift b/Sources/Schema/ContentModelDeterminism.swift index ea06021..a6df60b 100644 --- a/Sources/Schema/ContentModelDeterminism.swift +++ b/Sources/Schema/ContentModelDeterminism.swift @@ -18,19 +18,24 @@ public extension PureXML.Schema { private static let modelGroupNames: Set = ["sequence", "choice", "all"] private static let particleNames: Set = ["element", "sequence", "choice", "all", "group", "any"] - /// Every UPA-violation message across the schema's complex-type content - /// models, deduplicated in first-seen order. - static func violations(in schema: XSDTree, context: XSDContext) -> [String] { + /// Every UPA-violation finding across the schema's complex-type content + /// models, deduplicated by message in first-seen order. Each finding is + /// located on the content-model node (`sequence`/`choice`/`all`/`group`) + /// whose particles are ambiguous; a message shared by two models locates on + /// the first, preserving the deduplicated count. + static func violationFindings(in schema: XSDTree, context: XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] { let bindings = namespaceBindings(schema) - var messages: [String] = [] + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] var seen: Set = [] forEachComplexType(schema) { complexType in guard let model = contentModelNode(complexType), let conflict = violation(model, bindings, context) else { return } - if seen.insert(conflict).inserted { messages.append(conflict) } + if seen.insert(conflict).inserted { + findings.append(PureXML.Schema.SchemaLocatedFinding(reason: conflict, node: model)) + } } - return messages + return findings } /// The prefix-to-namespace bindings declared on the schema element (its diff --git a/Sources/Schema/SchemaCompileValidation.swift b/Sources/Schema/SchemaCompileValidation.swift index f58be9b..06a1670 100644 --- a/Sources/Schema/SchemaCompileValidation.swift +++ b/Sources/Schema/SchemaCompileValidation.swift @@ -69,9 +69,7 @@ extension PureXML.Validation { static var componentNamesUnique: Validation { compileRule("Global schema component names are unique within their symbol spaces") { document in - PureXML.Schema.SchemaLocatedFinding.unlocated( - PureXML.Schema.XSDParser.componentNameErrors(document.schema, document.containers, document.context), - ) + PureXML.Schema.XSDParser.componentNameFindings(document.schema, document.containers, document.context) } } @@ -87,9 +85,7 @@ extension PureXML.Validation { static var contentModelsDeterministic: Validation { compileRule("Schema content models are deterministic (UPA)") { document in - PureXML.Schema.SchemaLocatedFinding.unlocated( - PureXML.Schema.ContentModelDeterminism.violations(in: document.schema, context: document.context), - ) + PureXML.Schema.ContentModelDeterminism.violationFindings(in: document.schema, context: document.context) } } @@ -209,9 +205,7 @@ extension PureXML.Validation { static var notationsValid: Validation { compileRule("Notation declarations are valid") { document in - PureXML.Schema.SchemaLocatedFinding.unlocated( - PureXML.Schema.XSDParser.notationValidityErrors(document.containers, document.context), - ) + PureXML.Schema.XSDParser.notationValidityFindings(document.containers, document.context) } } diff --git a/Sources/Schema/SchemaNameUniqueness.swift b/Sources/Schema/SchemaNameUniqueness.swift index 8b2ec41..4360062 100644 --- a/Sources/Schema/SchemaNameUniqueness.swift +++ b/Sources/Schema/SchemaNameUniqueness.swift @@ -10,7 +10,7 @@ extension PureXML.Schema.XSDParser { /// Only the document's own globals are examined (`xs:redefine` children and /// included documents are not direct globals here), so a redefinition is not /// mistaken for a clash. - static func componentNameErrors(_ schema: XSDTree, _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [String] { + static func componentNameFindings(_ schema: XSDTree, _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] { let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: context.targetNamespace) let uniqueIndices = findUniqueIndices( in: containers, @@ -19,20 +19,20 @@ extension PureXML.Schema.XSDParser { mainTargetNamespace: context.targetNamespace, ) - var errors = checkDuplicateGlobals( + var findings = checkDuplicateGlobalFindings( in: containers, indices: uniqueIndices, namespaceMap: namespaceMap, mainTargetNamespace: context.targetNamespace, ) - errors += identityConstraintNameErrors( + findings += identityConstraintNameFindings( containers, uniqueIndices, namespaceMap: namespaceMap, mainTargetNamespace: context.targetNamespace, ) - errors += keyrefReferErrors(schema, containers, context) - return errors + findings += keyrefReferFindings(schema, containers, context) + return findings } private static func findUniqueIndices( @@ -73,13 +73,13 @@ extension PureXML.Schema.XSDParser { let name: String } - private static func checkDuplicateGlobals( + private static func checkDuplicateGlobalFindings( in containers: [XSDTree], indices: [Int], namespaceMap: [Int: String?], mainTargetNamespace: String?, - ) -> [String] { - var errors: [String] = [] + ) -> [PureXML.Schema.SchemaLocatedFinding] { + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] var seenComponents: Set = [] for index in indices { let container = containers[index] @@ -99,11 +99,14 @@ extension PureXML.Schema.XSDParser { if !seenComponents.insert(key).inserted { let nsStr = namespaceURI.map { " in namespace '\($0)'" } ?? " in no namespace" let kindLabel = kind == "group" ? "model group" : (kind == "attributeGroup" ? "attribute group" : kind) - errors.append("duplicate \(kindLabel) name '\(name)'\(nsStr)") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "duplicate \(kindLabel) name '\(name)'\(nsStr)", + node: global, + )) } } } - return errors + return findings } private static func componentKind(for localName: String) -> String? { @@ -122,6 +125,7 @@ extension PureXML.Schema.XSDParser { let name: String let refer: String let arity: Int + let node: XSDTree } /// Findings for a `keyref` whose `refer` does not name a `key` or `unique` in @@ -129,7 +133,7 @@ extension PureXML.Schema.XSDParser { /// Skipped when the document pulls in external definitions /// (`import`/`include`/`redefine`), which the default compile does not load, /// so a `refer` into them is never flagged. - private static func keyrefReferErrors(_ schema: XSDTree, _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [String] { + private static func keyrefReferFindings(_ schema: XSDTree, _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] { if skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) { return [] } var keyArities: [String: Int] = [:] var keyrefs: [KeyrefInfo] = [] @@ -138,17 +142,23 @@ extension PureXML.Schema.XSDParser { collectKeysAndRefers(source, keyArities: &keyArities, keyrefs: &keyrefs) } - var errors: [String] = [] + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] for keyref in keyrefs { if let keyArity = keyArities[keyref.refer] { if keyArity != keyref.arity { - errors.append("keyref '\(keyref.name)' and its referenced key/unique '\(keyref.refer)' must have the same number of fields") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "keyref '\(keyref.name)' and its referenced key/unique '\(keyref.refer)' must have the same number of fields", + node: keyref.node, + )) } } else { - errors.append("keyref refers to undeclared key or unique '\(keyref.refer)'") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "keyref refers to undeclared key or unique '\(keyref.refer)'", + node: keyref.node, + )) } } - return errors + return findings } private static func collectKeysAndRefers( @@ -170,7 +180,7 @@ extension PureXML.Schema.XSDParser { if let name, let refer { let arity = PureXML.Schema.XSDNode.elementChildren(node) .count(where: { $0.name?.namespaceURI == xsdNamespace && PureXML.Schema.XSDNode.localName($0) == "field" }) - keyrefs.append(KeyrefInfo(name: name, refer: PureXML.Schema.XSDNode.stripPrefix(refer), arity: arity)) + keyrefs.append(KeyrefInfo(name: name, refer: PureXML.Schema.XSDNode.stripPrefix(refer), arity: arity, node: node)) } } } @@ -179,18 +189,18 @@ extension PureXML.Schema.XSDParser { } } - private static func identityConstraintNameErrors( + private static func identityConstraintNameFindings( _ containers: [XSDTree], _ uniqueIndices: [Int], namespaceMap: [Int: String?], mainTargetNamespace: String?, - ) -> [String] { + ) -> [PureXML.Schema.SchemaLocatedFinding] { struct KeyrefNameKey: Hashable { let targetNamespace: String? let name: String } var seen: Set = [] - var errors: [String] = [] + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] func walk(_ node: XSDTree, namespaceURI: String?) { let local = PureXML.Schema.XSDNode.localName(node) if local == "appinfo" || local == "documentation" { return } @@ -198,7 +208,10 @@ extension PureXML.Schema.XSDParser { let key = KeyrefNameKey(targetNamespace: namespaceURI, name: name) if !seen.insert(key).inserted { let nsStr = namespaceURI.map { " in namespace '\($0)'" } ?? " in no namespace" - errors.append("duplicate identity constraint name '\(name)'\(nsStr)") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "duplicate identity constraint name '\(name)'\(nsStr)", + node: node, + )) } } for child in PureXML.Schema.XSDNode.elementChildren(node) { @@ -211,7 +224,7 @@ extension PureXML.Schema.XSDParser { let namespaceURI = namespaceMap[index] ?? mainTargetNamespace walk(container, namespaceURI: namespaceURI) } - return errors + return findings } /// The `name` of `node` when it is an identity constraint (`unique`/`key`/ diff --git a/Sources/Schema/SchemaNotationValidity.swift b/Sources/Schema/SchemaNotationValidity.swift index 419134e..8e718a2 100644 --- a/Sources/Schema/SchemaNotationValidity.swift +++ b/Sources/Schema/SchemaNotationValidity.swift @@ -1,10 +1,10 @@ extension PureXML.Schema.XSDParser { - static func notationValidityErrors( + static func notationValidityFindings( _ containers: [XSDTree], _ context: PureXML.Schema.XSDContext, - ) -> [String] { + ) -> [PureXML.Schema.SchemaLocatedFinding] { let namespaceMap = resolveContainerNamespaces(containers, mainTargetNamespace: context.targetNamespace) - var errors: [String] = [] + var findings: [PureXML.Schema.SchemaLocatedFinding] = [] var declaredNotations: Set = [] // 1. Collect all declared notations and validate notation declarations @@ -19,10 +19,16 @@ extension PureXML.Schema.XSDParser { } let name = PureXML.Schema.XSDNode.attribute(child, "name") ?? "" if PureXML.Schema.XSDNode.attribute(child, "public") == nil, PureXML.Schema.XSDNode.attribute(child, "system") == nil { - errors.append("notation '\(name)' must specify at least one of public or system attributes") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "notation '\(name)' must specify at least one of public or system attributes", + node: child, + )) } if notationHasForbiddenContent(child) { - errors.append("notation '\(name)' may contain only an optional annotation, no other element or character content") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "notation '\(name)' may contain only an optional annotation, no other element or character content", + node: child, + )) } let qName = PureXML.Model.QualifiedName(localName: name, namespaceURI: containerNamespace) declaredNotations.insert(qName) @@ -32,10 +38,10 @@ extension PureXML.Schema.XSDParser { // 2. Validate notation enumeration constraints on simpleTypes for index in containers.indices { let container = containers[index] - walkSimpleTypes(container, context: context, declaredNotations: declaredNotations, into: &errors) + walkSimpleTypes(container, context: context, declaredNotations: declaredNotations, into: &findings) } - return errors + return findings } /// Whether a `notation` declaration carries content its `(annotation?)` model @@ -56,17 +62,17 @@ extension PureXML.Schema.XSDParser { _ node: XSDTree, context: PureXML.Schema.XSDContext, declaredNotations: Set, - into errors: inout [String], + into findings: inout [PureXML.Schema.SchemaLocatedFinding], ) { let local = PureXML.Schema.XSDNode.localName(node) if local == "appinfo" || local == "documentation" { return } if node.name?.namespaceURI == xsdNamespace, local == "restriction" { - validateNotationRestriction(node, context: context, declaredNotations: declaredNotations, into: &errors) + validateNotationRestriction(node, context: context, declaredNotations: declaredNotations, into: &findings) } for child in PureXML.Schema.XSDNode.elementChildren(node) { - walkSimpleTypes(child, context: context, declaredNotations: declaredNotations, into: &errors) + walkSimpleTypes(child, context: context, declaredNotations: declaredNotations, into: &findings) } } @@ -74,7 +80,7 @@ extension PureXML.Schema.XSDParser { _ node: XSDTree, context: PureXML.Schema.XSDContext, declaredNotations: Set, - into errors: inout [String], + into findings: inout [PureXML.Schema.SchemaLocatedFinding], ) { guard let parent = node.parent, parent.name?.namespaceURI == xsdNamespace, @@ -99,7 +105,10 @@ extension PureXML.Schema.XSDParser { return dec.localName == valQName.localName && decNS == valNS } if !isDeclared { - errors.append("notation enumeration value '\(value)' does not name a declared notation") + findings.append(PureXML.Schema.SchemaLocatedFinding( + reason: "notation enumeration value '\(value)' does not name a declared notation", + node: child, + )) } } } diff --git a/Tests/SchemaNamesUPANotationLocatedTests.swift b/Tests/SchemaNamesUPANotationLocatedTests.swift new file mode 100644 index 0000000..9d53577 --- /dev/null +++ b/Tests/SchemaNamesUPANotationLocatedTests.swift @@ -0,0 +1,132 @@ +import Testing +@testable import PureXML + +/// Names/UPA/notation-pass schema compile findings (component-name uniqueness, +/// identity-constraint name uniqueness, keyref `refer` resolution, Unique Particle +/// Attribution, and notation validity) carry validation coding paths that resolve +/// to source spans, the IDE underline flow for #169. Split from the other +/// located-diagnostics suites to keep each suite within the type-body length budget. +@Suite("Schema names/UPA/notation-pass located compile diagnostics") +struct SchemaNamesUPANotationLocatedTests { + 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("A duplicate global type name is located on the second declaration") + func test_duplicateGlobalLocated() throws { + let xsd = """ + + + + + """ + let findings = try inconsistentFindings(in: xsd) + let located = try #require(findings.first { $0.reason.contains("duplicate type name 't'") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType"]) + + let (tree, _) = PureXML.readTree(xsd) + #expect(tree.sourceRange(at: located.codingPath)?.start.line == 3) + } + + @Test("A duplicate identity-constraint name is located on the second constraint") + func test_duplicateIdentityConstraintLocated() throws { + let xsd = """ + + + + + + + + """ + let findings = try inconsistentFindings(in: xsd) + let located = try #require(findings.first { $0.reason.contains("duplicate identity constraint name 'dup'") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element", "xs:unique"]) + + let (tree, _) = PureXML.readTree(xsd) + #expect(tree.sourceRange(at: located.codingPath)?.start.line == 5) + } + + @Test("A keyref to an undeclared key is located on the keyref") + func test_keyrefUndeclaredLocated() throws { + let xsd = """ + + + + + + + """ + let findings = try inconsistentFindings(in: xsd) + let located = try #require(findings.first { $0.reason.contains("keyref refers to undeclared key or unique 'nope'") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element", "xs:keyref"]) + + let (tree, _) = PureXML.readTree(xsd) + #expect(tree.sourceRange(at: located.codingPath)?.start.line == 4) + } + + @Test("A UPA violation is located on the ambiguous content model") + func test_upaViolationLocated() throws { + let xsd = """ + + + + + + + + + """ + let findings = try inconsistentFindings(in: xsd) + let located = try #require(findings.first { $0.reason.contains("deterministic") || $0.reason.contains("ambiguous") || $0.reason.contains("Unique Particle") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType", "xs:choice"]) + + let (tree, _) = PureXML.readTree(xsd) + #expect(tree.sourceRange(at: located.codingPath)?.start.line == 3) + } + + @Test("A notation missing public and system is located on the notation") + func test_notationMissingAttributesLocated() throws { + let xsd = """ + + + + """ + let findings = try inconsistentFindings(in: xsd) + let located = try #require(findings.first { $0.reason.contains("must specify at least one of public or system") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:notation"]) + + let (tree, _) = PureXML.readTree(xsd) + #expect(tree.sourceRange(at: located.codingPath)?.start.line == 2) + } + + @Test("A notation enumeration naming no declared notation is located on the enumeration") + func test_notationEnumerationLocated() throws { + let xsd = """ + + + + + + + + + """ + let findings = try inconsistentFindings(in: xsd) + let located = try #require(findings.first { $0.reason.contains("notation enumeration value 'undeclared' does not name a declared notation") }) + #expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:simpleType", "xs:restriction", "xs:enumeration"]) + + let (tree, _) = PureXML.readTree(xsd) + #expect(tree.sourceRange(at: located.codingPath)?.start.line == 5) + } +}