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.
This commit is contained in:
parent
96ab396eb3
commit
97c07e91f4
File diff suppressed because one or more lines are too long
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,11 +6,12 @@ extension PureXML.Validation.SchemaCompile {
|
||||
/// the processor's choice.
|
||||
static var referencedSchemasResolveToSchemas: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
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<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
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<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
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<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
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<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -148,8 +148,9 @@ extension PureXML.Schema {
|
||||
_ schema: XSDTree,
|
||||
_ loader: (String) -> String?,
|
||||
_ visited: inout Set<String>,
|
||||
) -> [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] = [:]
|
||||
|
||||
@ -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
|
||||
|
||||
100
Tests/SchemaCompositionLocatedDiagnosticsTests.swift
Normal file
100
Tests/SchemaCompositionLocatedDiagnosticsTests.swift
Normal file
@ -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 = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:include schemaLocation="broken.xsd"/>
|
||||
<xs:element name="root" type="xs:string"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd) { _ in "<notSchema/>" }
|
||||
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 = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:t">
|
||||
<xs:complexType name="ct"><xs:sequence><xs:element name="a" type="xs:string"/></xs:sequence></xs:complexType>
|
||||
<xs:complexType name="other"><xs:sequence/></xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:t="urn:t" targetNamespace="urn:t">
|
||||
<xs:redefine schemaLocation="base.xsd">
|
||||
<xs:complexType name="ct">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="t:other"/>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
</xs:redefine>
|
||||
</xs:schema>
|
||||
"""
|
||||
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 = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:t">
|
||||
<xs:group name="g"><xs:sequence><xs:element name="a" type="xs:string"/></xs:sequence></xs:group>
|
||||
</xs:schema>
|
||||
"""
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:t="urn:t" targetNamespace="urn:t">
|
||||
<xs:redefine schemaLocation="base.xsd">
|
||||
<xs:group name="g">
|
||||
<xs:sequence>
|
||||
<xs:group ref="t:g" maxOccurs="2"/>
|
||||
</xs:sequence>
|
||||
</xs:group>
|
||||
</xs:redefine>
|
||||
</xs:schema>
|
||||
"""
|
||||
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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user