feat(schema): locate derivation/cycle-pass schema diagnostics
Convert the three derivation/cycle-pass compile checkers to located findings so each error attaches to its declaring node instead of the schema root (#169): an all-group referenced inside a sequence/choice/all locates on the offending group reference (allGroupReferencePlacementFindings); a type derived from itself locates on its complexType/simpleType (derivationCycleFindings); and an element that is a member of its own substitution group, or a model group or attribute group that references itself, locates on its declaration (circularReferenceFindings). The cycle checkers report a cycle by component name over a name-keyed graph, so each now records the declaring node in a parallel nodeOf map built in the same loop as the graph; a name that yields a finding always has a node, so a mutual cycle underlines each member on its own declaration. Every predicate, key ordering, and reason string is unchanged; only the location each finding carries differs. The three checkers had exactly one caller each, so each is renamed in place and the .unlocated wrap dropped. Adds a located-diagnostics test per checker family, covering both substitution and group-reference cycles.
This commit is contained in:
parent
49fe824601
commit
ee5c46929c
@ -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`.
|
||||
- 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.
|
||||
|
||||
- DTD parse-time validity findings are warnings until strict validation promotes them; `validateAgainstInternalDTD` returns full findings (errors and warnings).
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ extension PureXML.Schema.XSDParser {
|
||||
/// where that reference appears inside a compositor (so the `all` is not the
|
||||
/// whole content). The reference is followed only when it resolves to this
|
||||
/// schema's own target namespace.
|
||||
static func allGroupReferencePlacementErrors(_ containers: [XSDTree], _ bindings: [String: String], _ target: String?) -> [String] {
|
||||
static func allGroupReferencePlacementFindings(_ containers: [XSDTree], _ bindings: [String: String], _ target: String?) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
// A redefined group's effective content is the redefinition's, not the
|
||||
// original loaded definition, so a redefined group name is exempt: its
|
||||
// original `all` content is overridden (and the suite treats a reference to
|
||||
@ -41,21 +41,30 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
}
|
||||
guard !allGroups.isEmpty else { return [] }
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for container in containers {
|
||||
collectAllGroupRefPlacement(container, allGroups, bindings, target, into: &errors)
|
||||
collectAllGroupRefPlacement(container, allGroups, bindings, target, into: &findings)
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
private static func collectAllGroupRefPlacement(_ node: XSDTree, _ allGroups: Set<String>, _ bindings: [String: String], _ target: String?, into errors: inout [String]) {
|
||||
private static func collectAllGroupRefPlacement(
|
||||
_ node: XSDTree,
|
||||
_ allGroups: Set<String>,
|
||||
_ bindings: [String: String],
|
||||
_ target: String?,
|
||||
into findings: inout [PureXML.Schema.SchemaLocatedFinding],
|
||||
) {
|
||||
let kind = AllNode.localName(node)
|
||||
let insideCompositor = kind == "sequence" || kind == "choice" || kind == "all"
|
||||
for child in AllNode.elementChildren(node) {
|
||||
if insideCompositor, let referenced = allGroupReference(child, allGroups, bindings, target) {
|
||||
errors.append("the all-group '\(referenced)' may not be referenced inside a '\(kind ?? "")'; an all group must be the whole content model")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "the all-group '\(referenced)' may not be referenced inside a '\(kind ?? "")'; an all group must be the whole content model",
|
||||
node: child,
|
||||
))
|
||||
}
|
||||
collectAllGroupRefPlacement(child, allGroups, bindings, target, into: &errors)
|
||||
collectAllGroupRefPlacement(child, allGroups, bindings, target, into: &findings)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -95,36 +95,30 @@ extension PureXML.Validation {
|
||||
|
||||
static var typeDerivationAcyclic: Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("Type derivation chains contain no cycles") { document in
|
||||
PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.XSDParser.derivationCycleErrors(
|
||||
document.containers,
|
||||
document.context.namespaceBindings,
|
||||
document.context.targetNamespace,
|
||||
),
|
||||
PureXML.Schema.XSDParser.derivationCycleFindings(
|
||||
document.containers,
|
||||
document.context.namespaceBindings,
|
||||
document.context.targetNamespace,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static var typeReferencesAcyclic: Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("Schema type references contain no cycles") { document in
|
||||
PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.XSDParser.circularReferenceErrors(
|
||||
document.containers,
|
||||
document.context.namespaceBindings,
|
||||
document.context.targetNamespace,
|
||||
),
|
||||
PureXML.Schema.XSDParser.circularReferenceFindings(
|
||||
document.containers,
|
||||
document.context.namespaceBindings,
|
||||
document.context.targetNamespace,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static var allGroupReferencesPlaced: Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("xs:all group references appear only where permitted") { document in
|
||||
PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.XSDParser.allGroupReferencePlacementErrors(
|
||||
document.containers,
|
||||
document.context.namespaceBindings,
|
||||
document.context.targetNamespace,
|
||||
),
|
||||
PureXML.Schema.XSDParser.allGroupReferencePlacementFindings(
|
||||
document.containers,
|
||||
document.context.namespaceBindings,
|
||||
document.context.targetNamespace,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,9 +107,10 @@ extension PureXML.Schema.XSDParser {
|
||||
/// redefinition derives from its former self), so redefined types are treated as
|
||||
/// chain ends rather than self-cycles, matching the suite's "circular ref is
|
||||
/// allowed if parent is redefine".
|
||||
static func derivationCycleErrors(_ containers: [XSDTree], _ bindings: [String: String], _ target: String?) -> [String] {
|
||||
static func derivationCycleFindings(_ containers: [XSDTree], _ bindings: [String: String], _ target: String?) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let redefined = redefinedTypeNames(containers)
|
||||
var graph: [String: String] = [:]
|
||||
var nodeOf: [String: XSDTree] = [:]
|
||||
for container in containers {
|
||||
for kind in ["complexType", "simpleType"] {
|
||||
for type in descendants(container, named: kind) {
|
||||
@ -120,12 +121,13 @@ extension PureXML.Schema.XSDParser {
|
||||
// local names collide (a `local:X` extending an imported `other:X`).
|
||||
guard XSDDerivNode.referenceNamespace(base, bindings) == target else { continue }
|
||||
graph[name] = XSDDerivNode.stripPrefix(base)
|
||||
nodeOf[name] = type
|
||||
}
|
||||
}
|
||||
}
|
||||
return graph.keys.sorted().compactMap { name in
|
||||
guard !redefined.contains(name), derivesFromItself(name, graph, redefined) else { return nil }
|
||||
return "type '\(name)' must not be derived from itself"
|
||||
return PureXML.Schema.SchemaLocatedFinding(reason: "type '\(name)' must not be derived from itself", node: nodeOf[name])
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,36 +191,47 @@ extension PureXML.Schema.XSDParser {
|
||||
/// chain may not loop. A reference is followed only when it resolves to this
|
||||
/// schema's own target namespace, and a redefined group/attribute group is a
|
||||
/// chain end (its single self-reference is the legal redefinition).
|
||||
static func circularReferenceErrors(_ containers: [XSDTree], _ bindings: [String: String], _ target: String?) -> [String] {
|
||||
substitutionCycleErrors(containers, bindings, target)
|
||||
+ referenceCycleErrors(containers, "group", "model group", bindings, target)
|
||||
+ referenceCycleErrors(containers, "attributeGroup", "attribute group", bindings, target)
|
||||
static func circularReferenceFindings(_ containers: [XSDTree], _ bindings: [String: String], _ target: String?) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
substitutionCycleFindings(containers, bindings, target)
|
||||
+ referenceCycleFindings(containers, "group", "model group", bindings, target)
|
||||
+ referenceCycleFindings(containers, "attributeGroup", "attribute group", bindings, target)
|
||||
}
|
||||
|
||||
private static func substitutionCycleErrors(_ containers: [XSDTree], _ bindings: [String: String], _ target: String?) -> [String] {
|
||||
private static func substitutionCycleFindings(_ containers: [XSDTree], _ bindings: [String: String], _ target: String?) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
var graph: [String: String] = [:]
|
||||
var nodeOf: [String: XSDTree] = [:]
|
||||
for container in containers {
|
||||
for element in descendants(container, named: "element") {
|
||||
guard let name = XSDDerivNode.attribute(element, "name"),
|
||||
let head = XSDDerivNode.attribute(element, "substitutionGroup"),
|
||||
XSDDerivNode.referenceNamespace(head, bindings) == target else { continue }
|
||||
graph[name] = XSDDerivNode.stripPrefix(head)
|
||||
nodeOf[name] = element
|
||||
}
|
||||
}
|
||||
return graph.keys.sorted().compactMap { name in
|
||||
derivesFromItself(name, graph, []) ? "element '\(name)' must not be a member of its own substitution group" : nil
|
||||
guard derivesFromItself(name, graph, []) else { return nil }
|
||||
return PureXML.Schema.SchemaLocatedFinding(reason: "element '\(name)' must not be a member of its own substitution group", node: nodeOf[name])
|
||||
}
|
||||
}
|
||||
|
||||
/// A named-definition reference cycle: a `kind` definition (`group` /
|
||||
/// `attributeGroup`) whose `kind`-references reach itself. Multi-edge, since a
|
||||
/// definition may reference several others.
|
||||
private static func referenceCycleErrors(_ containers: [XSDTree], _ kind: String, _ label: String, _ bindings: [String: String], _ target: String?) -> [String] {
|
||||
private static func referenceCycleFindings(
|
||||
_ containers: [XSDTree],
|
||||
_ kind: String,
|
||||
_ label: String,
|
||||
_ bindings: [String: String],
|
||||
_ target: String?,
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let redefined = redefinedNames(containers, kind)
|
||||
var adjacency: [String: [String]] = [:]
|
||||
var nodeOf: [String: XSDTree] = [:]
|
||||
for container in containers {
|
||||
for definition in descendants(container, named: kind) {
|
||||
guard let name = XSDDerivNode.attribute(definition, "name") else { continue }
|
||||
if nodeOf[name] == nil { nodeOf[name] = definition }
|
||||
for ref in boundedReferences(definition, kind) where XSDDerivNode.referenceNamespace(ref, bindings) == target {
|
||||
adjacency[name, default: []].append(XSDDerivNode.stripPrefix(ref))
|
||||
}
|
||||
@ -226,7 +239,7 @@ extension PureXML.Schema.XSDParser {
|
||||
}
|
||||
return adjacency.keys.sorted().compactMap { name in
|
||||
guard !redefined.contains(name), reachesItself(name, adjacency, redefined) else { return nil }
|
||||
return "\(label) '\(name)' must not reference itself"
|
||||
return PureXML.Schema.SchemaLocatedFinding(reason: "\(label) '\(name)' must not reference itself", node: nodeOf[name])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
95
Tests/SchemaDerivationLocatedDiagnosticsTests.swift
Normal file
95
Tests/SchemaDerivationLocatedDiagnosticsTests.swift
Normal file
@ -0,0 +1,95 @@
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
|
||||
/// Derivation/cycle-pass schema compile findings (a type derived from itself, an
|
||||
/// element in its own substitution group, a model group referencing itself, and
|
||||
/// an all-group referenced inside a compositor) carry validation coding paths that
|
||||
/// resolve to source spans, the IDE underline flow for #169. The cycle checkers
|
||||
/// report by component name over a name-keyed graph, so each finding now locates
|
||||
/// on the declaring node recorded alongside that graph. Split from the other
|
||||
/// located-diagnostics suites to keep each suite within the type-body length budget.
|
||||
@Suite("Schema derivation/cycle-pass located compile diagnostics")
|
||||
struct SchemaDerivationLocatedDiagnosticsTests {
|
||||
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 type derived from itself is located on the type declaration")
|
||||
func test_derivationCycleLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:t" xmlns:t="urn:t">
|
||||
<xs:complexType name="a">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="t:a"/>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("type 'a' must not be derived from itself") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 2)
|
||||
}
|
||||
|
||||
@Test("An element in its own substitution group is located on the element")
|
||||
func test_substitutionCycleLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:t" xmlns:t="urn:t">
|
||||
<xs:element name="a" type="xs:string" substitutionGroup="t:a"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("element 'a' must not be a member of its own substitution group") })
|
||||
#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 model group referencing itself is located on the group declaration")
|
||||
func test_groupReferenceCycleLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:t" xmlns:t="urn:t">
|
||||
<xs:group name="g">
|
||||
<xs:sequence><xs:group ref="t:g"/></xs:sequence>
|
||||
</xs:group>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("model group 'g' must not reference itself") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:group"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 2)
|
||||
}
|
||||
|
||||
@Test("An all-group referenced inside a compositor is located on the reference")
|
||||
func test_allGroupReferencePlacementLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:group name="g"><xs:all><xs:element name="a" type="xs:string"/></xs:all></xs:group>
|
||||
<xs:complexType name="t">
|
||||
<xs:sequence><xs:group ref="g"/></xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("may not be referenced inside a 'sequence'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType", "xs:sequence", "xs:group"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 4)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user