feat(schema): locate attribute-pass schema diagnostics
Convert the three attribute-pass compile checkers to located findings so each error attaches to its declaring node instead of the schema root (#169): an attribute declaration in the XSI namespace locates on the attribute (xsiNamespaceAttributeFindings); a duplicate attribute use or a second ID-typed attribute locates on its declaring complexType or attributeGroup, and an attribute reference contradicting a global fixed value locates on the reference (attributeUseFindings); and a complex-type restriction that relaxes a base attribute use or widens the attribute wildcard locates on the restriction (attributeRestrictionFindings). Each predicate, traversal order, and reason string is unchanged; only the location each finding carries differs. The set-level uniqueness/single-ID findings reuse the existing attributeSetErrors helper, mapping each onto its declaring container node. An attribute restriction violation has no source node of its own (the attribute use is a resolved model value), so it locates on the restriction performing the illegal narrowing. 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.
This commit is contained in:
parent
356e87581c
commit
49fe824601
@ -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.
|
||||
- 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`.
|
||||
|
||||
- DTD parse-time validity findings are warnings until strict validation promotes them; `validateAgainstInternalDTD` returns full findings (errors and warnings).
|
||||
|
||||
|
||||
@ -19,19 +19,26 @@ extension PureXML.Schema.XSDParser {
|
||||
/// comparing the two fixed values in the attribute type's value space, not
|
||||
/// lexically (e.g. a list type's `"1 2 3"` and `"1 2 3"` are the same value),
|
||||
/// which a string comparison gets wrong; that clause is left for a later change.
|
||||
static func attributeRestrictionErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] {
|
||||
static func attributeRestrictionFindings(
|
||||
_ schema: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
guard !skipsCrossDocumentRules(schema, compositionLoaded: context.compositionLoaded) else { return [] }
|
||||
let bindings = context.namespaceBindings
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for content in descendants(schema, named: "complexContent") {
|
||||
guard let restriction = AttrRestrictNode.firstChild(content, named: "restriction"),
|
||||
let base = AttrRestrictNode.attribute(restriction, "base"),
|
||||
AttrRestrictNode.referenceNamespace(base, bindings) == context.targetNamespace,
|
||||
case let .complex(complex)? = types[AttrRestrictNode.stripPrefix(base)]
|
||||
else { continue }
|
||||
// An attribute use resolves to a model value with no source node, so a
|
||||
// faithful-restriction violation locates on the restriction performing
|
||||
// the illegal narrowing, the node a developer would correct.
|
||||
for derived in attributeUses(under: restriction, context) {
|
||||
if let message = attributeRestrictionViolation(derived, complex.attributes, complex.attributeWildcard, restriction, context) {
|
||||
errors.append(message)
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(reason: message, node: restriction))
|
||||
}
|
||||
}
|
||||
// cos-ct-restricts.4: a restriction's own attribute wildcard must be a
|
||||
@ -41,14 +48,20 @@ extension PureXML.Schema.XSDParser {
|
||||
if let derivedWildcard = attributeWildcard(under: restriction, context) {
|
||||
if let baseWildcard = complex.attributeWildcard {
|
||||
if !PureXML.Schema.ParticleRestriction.narrows(derivedWildcard, baseWildcard) {
|
||||
errors.append("the restriction's attribute wildcard is not a subset of the base type's attribute wildcard")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "the restriction's attribute wildcard is not a subset of the base type's attribute wildcard",
|
||||
node: restriction,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
errors.append("a restriction may not introduce an attribute wildcard that the base type does not have")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a restriction may not introduce an attribute wildcard that the base type does not have",
|
||||
node: restriction,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
/// The way `derived` (an attribute the restriction redeclares) illegally relaxes
|
||||
|
||||
@ -59,20 +59,24 @@ extension PureXML.Schema.XSDParser {
|
||||
/// carry their resolved namespace, so a qualified and an unqualified attribute
|
||||
/// of the same local name do not collide. The own-declared set is checked, not
|
||||
/// the set inherited through extension (a disclosed under-rejection).
|
||||
static func attributeUseErrors(_ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [String] {
|
||||
var errors: [String] = []
|
||||
static func attributeUseFindings(_ containers: [XSDTree], _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for container in containers {
|
||||
for group in descendants(container, named: "attributeGroup") {
|
||||
guard let name = AttrNode.attribute(group, "name") else { continue }
|
||||
errors += attributeSetErrors(attributeDeclarations(under: group, context), "attribute group '\(name)'")
|
||||
// A uniqueness/single-ID clash is a property of the whole declared
|
||||
// attribute set, so it locates on the declaring group.
|
||||
findings += attributeSetErrors(attributeDeclarations(under: group, context), "attribute group '\(name)'")
|
||||
.map { PureXML.Schema.SchemaLocatedFinding(reason: $0, node: group) }
|
||||
}
|
||||
for type in descendants(container, named: "complexType") {
|
||||
let label = AttrNode.attribute(type, "name").map { "complex type '\($0)'" } ?? "an anonymous complex type"
|
||||
errors += attributeSetErrors(attributeDeclarations(under: attributeContainer(of: type), context), label)
|
||||
findings += attributeSetErrors(attributeDeclarations(under: attributeContainer(of: type), context), label)
|
||||
.map { PureXML.Schema.SchemaLocatedFinding(reason: $0, node: type) }
|
||||
}
|
||||
errors += attributeRefValueConstraintErrors(container, context)
|
||||
findings += attributeRefValueConstraintFindings(container, context)
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
/// XSD 1.0 `au-props-correct.2`: when an attribute use references a global
|
||||
@ -88,8 +92,8 @@ extension PureXML.Schema.XSDParser {
|
||||
/// rejects a same-local-name collision across namespaces (an imported `imp:a`
|
||||
/// versus a local `a`), where the local-name pool could otherwise surface the
|
||||
/// wrong global and contradict a valid reference.
|
||||
private static func attributeRefValueConstraintErrors(_ container: XSDTree, _ context: PureXML.Schema.XSDContext) -> [String] {
|
||||
var errors: [String] = []
|
||||
private static func attributeRefValueConstraintFindings(_ container: XSDTree, _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
for use in descendants(container, named: "attribute") {
|
||||
guard let ref = AttrNode.attribute(use, "ref"),
|
||||
let declaration = context.globalAttributes[AttrNode.stripPrefix(ref)],
|
||||
@ -100,13 +104,19 @@ extension PureXML.Schema.XSDParser {
|
||||
if let useFixed = AttrNode.attribute(use, "fixed") {
|
||||
let equal = resolved.type.valueMatches(useFixed, literal: globalFixed) || useFixed == globalFixed
|
||||
if !equal {
|
||||
errors.append("attribute reference to '\(AttrNode.stripPrefix(ref))' must use its fixed value '\(globalFixed)', not '\(useFixed)'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "attribute reference to '\(AttrNode.stripPrefix(ref))' must use its fixed value '\(globalFixed)', not '\(useFixed)'",
|
||||
node: use,
|
||||
))
|
||||
}
|
||||
} else if AttrNode.attribute(use, "default") != nil {
|
||||
errors.append("attribute reference to '\(AttrNode.stripPrefix(ref))' has a fixed value and may not specify a default")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "attribute reference to '\(AttrNode.stripPrefix(ref))' has a fixed value and may not specify a default",
|
||||
node: use,
|
||||
))
|
||||
}
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
/// The node whose direct children declare a complex type's attributes: the
|
||||
|
||||
@ -156,9 +156,7 @@ extension PureXML.Validation {
|
||||
|
||||
static var attributeUsesValid: Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("Attribute uses are unique and declare at most one ID attribute") { document in
|
||||
PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.XSDParser.attributeUseErrors(document.containers, document.context),
|
||||
)
|
||||
PureXML.Schema.XSDParser.attributeUseFindings(document.containers, document.context)
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,9 +201,7 @@ extension PureXML.Validation {
|
||||
static var attributeRestrictionsFaithful: Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("Attribute restrictions are faithful to their bases") { document in
|
||||
guard let namedTypes = document.namedTypes else { return [] }
|
||||
return PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.XSDParser.attributeRestrictionErrors(document.schema, document.context, namedTypes),
|
||||
)
|
||||
return PureXML.Schema.XSDParser.attributeRestrictionFindings(document.schema, document.context, namedTypes)
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,12 +328,10 @@ extension PureXML.Validation {
|
||||
|
||||
extension PureXML.Validation.SchemaCompile {
|
||||
/// xsi: Not Allowed (XSD 1.0 SCC §3.2.6): no attribute declaration lands in the
|
||||
/// XSI namespace. See ``PureXML/Schema/XSDParser/xsiNamespaceAttributeErrors``.
|
||||
/// XSI namespace. See ``PureXML/Schema/XSDParser/xsiNamespaceAttributeFindings``.
|
||||
static var attributeDeclarationsNotInXSI: PureXML.Validation.Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("Attribute declarations are not in the XSI namespace") { document in
|
||||
PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.XSDParser.xsiNamespaceAttributeErrors(document.schema, document.context),
|
||||
)
|
||||
PureXML.Schema.XSDParser.xsiNamespaceAttributeFindings(document.schema, document.context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,19 +10,19 @@ extension PureXML.Schema.XSDParser {
|
||||
/// XSI it is forbidden (corpus attKb018a: `attributeFormDefault="qualified"` puts
|
||||
/// its attribute-group attributes into XSI). The check is scoped to schemas whose
|
||||
/// target namespace is XSI, the only documents that can place an attribute there.
|
||||
static func xsiNamespaceAttributeErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext) -> [String] {
|
||||
static func xsiNamespaceAttributeFindings(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
guard context.targetNamespace == xsiInstanceNamespace else { return [] }
|
||||
let attributeFormDefault = PureXML.Schema.XSDNode.attribute(schema, "attributeFormDefault")
|
||||
var errors: [String] = []
|
||||
collectXSIAttributeErrors(schema, parentIsSchema: false, attributeFormDefault: attributeFormDefault, into: &errors)
|
||||
return errors
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
collectXSIAttributeFindings(schema, parentIsSchema: false, attributeFormDefault: attributeFormDefault, into: &findings)
|
||||
return findings
|
||||
}
|
||||
|
||||
private static func collectXSIAttributeErrors(
|
||||
private static func collectXSIAttributeFindings(
|
||||
_ node: XSDTree,
|
||||
parentIsSchema: Bool,
|
||||
attributeFormDefault: String?,
|
||||
into errors: inout [String],
|
||||
into findings: inout [PureXML.Schema.SchemaLocatedFinding],
|
||||
) {
|
||||
let local = PureXML.Schema.XSDNode.localName(node)
|
||||
if local == "appinfo" || local == "documentation" { return }
|
||||
@ -32,11 +32,14 @@ extension PureXML.Schema.XSDParser {
|
||||
let form = PureXML.Schema.XSDNode.attribute(node, "form")
|
||||
let qualified = parentIsSchema || form == "qualified" || (form == nil && attributeFormDefault == "qualified")
|
||||
if qualified {
|
||||
errors.append("attribute declaration '\(name)' is in the XSI namespace, which is not allowed (xsi: Not Allowed)")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "attribute declaration '\(name)' is in the XSI namespace, which is not allowed (xsi: Not Allowed)",
|
||||
node: node,
|
||||
))
|
||||
}
|
||||
}
|
||||
for child in PureXML.Schema.XSDNode.elementChildren(node) {
|
||||
collectXSIAttributeErrors(child, parentIsSchema: isSchema, attributeFormDefault: attributeFormDefault, into: &errors)
|
||||
collectXSIAttributeFindings(child, parentIsSchema: isSchema, attributeFormDefault: attributeFormDefault, into: &findings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
101
Tests/SchemaAttributeLocatedDiagnosticsTests.swift
Normal file
101
Tests/SchemaAttributeLocatedDiagnosticsTests.swift
Normal file
@ -0,0 +1,101 @@
|
||||
import Testing
|
||||
@testable import PureXML
|
||||
|
||||
/// Attribute-pass schema compile findings (an attribute declared in the XSI
|
||||
/// namespace, a duplicate attribute use within a complex type, an attribute
|
||||
/// reference contradicting a global `fixed` value, and a restriction that relaxes
|
||||
/// a base attribute) 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 attribute-pass located compile diagnostics")
|
||||
struct SchemaAttributeLocatedDiagnosticsTests {
|
||||
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("An attribute declared in the XSI namespace is located on the attribute")
|
||||
func test_xsiNamespaceAttributeLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
targetNamespace="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<xs:attribute name="foo" type="xs:string"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("is in the XSI namespace, which is not allowed") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:attribute"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 3)
|
||||
}
|
||||
|
||||
@Test("A duplicate attribute use is located on its declaring complex type")
|
||||
func test_duplicateAttributeUseLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="t">
|
||||
<xs:attribute name="a" type="xs:string"/>
|
||||
<xs:attribute name="a" type="xs:string"/>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("has more than one attribute named 'a'") })
|
||||
#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 attribute reference contradicting a fixed value is located on the reference")
|
||||
func test_attributeRefFixedValueLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:attribute name="a" type="xs:string" fixed="x"/>
|
||||
<xs:complexType name="t">
|
||||
<xs:attribute ref="a" fixed="y"/>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("must use its fixed value 'x', not 'y'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType", "xs:attribute"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 4)
|
||||
}
|
||||
|
||||
@Test("A restriction relaxing a required base attribute is located on the restriction")
|
||||
func test_attributeRestrictionLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:complexType name="base">
|
||||
<xs:attribute name="a" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
<xs:complexType name="d">
|
||||
<xs:complexContent>
|
||||
<xs:restriction base="base">
|
||||
<xs:attribute name="a" type="xs:string" use="optional"/>
|
||||
</xs:restriction>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("is required in the base type and a restriction may not make it optional") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:complexType", "xs:complexContent", "xs:restriction"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
#expect(tree.sourceRange(at: located.codingPath)?.start.line == 7)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user