feat(schema): locate value-constraint type errors
Convert the three value-constraint checkers (built-in, named user type, and inline type) to located findings so a default/fixed value that is invalid for its declared type, or an illegal combination of value constraints, is attached to its declaring element or attribute instead of the schema root. An editor can underline the offending declaration by line and column. Error predicates and reason strings are unchanged; only the location each finding carries differs. Adds located-diagnostics tests for the built-in, user-type, and inline-type paths.
This commit is contained in:
parent
f5059de42e
commit
76ef2d5b83
@ -188,10 +188,8 @@ extension PureXML.Validation {
|
||||
static var userTypeValueConstraintsValid: Validation<PureXML.Schema.SchemaCompileRoot, PureXML.Schema.SchemaCompileContext> {
|
||||
compileRule("Element value constraints are valid against their declared types") { document in
|
||||
guard let namedTypes = document.namedTypes else { return [] }
|
||||
return PureXML.Schema.SchemaLocatedFinding.unlocated(
|
||||
PureXML.Schema.XSDParser.userTypeValueConstraintErrors(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.inlineTypeValueConstraintErrors(document.schema, document.context),
|
||||
)
|
||||
return PureXML.Schema.XSDParser.userTypeValueConstraintFindings(document.schema, document.context, namedTypes)
|
||||
+ PureXML.Schema.XSDParser.inlineTypeValueConstraintFindings(document.schema, document.context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -37,19 +37,25 @@ extension PureXML.Schema.XSDParser {
|
||||
/// `fixed="abc"` on an `xs:integer` was wrongly accepted. A named or inline user
|
||||
/// type, or a type in another namespace, is left alone, so no valid value
|
||||
/// constraint is rejected.
|
||||
static func valueConstraintErrors(_ schema: XSDTree) -> [String] {
|
||||
static func valueConstraintFindings(_ schema: XSDTree) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let bindings = namespaceBindings(schema)
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
forEachValueConstrained(schema) { node in
|
||||
// The value constraint attributes are the unprefixed, no-namespace
|
||||
// `default`/`fixed` on an XSD-namespace declaration; a foreign same-local
|
||||
// attribute is not the value constraint and is not structurally checked.
|
||||
if hasBothValueConstraints(node) {
|
||||
errors.append("a '\(PureXML.Schema.XSDNode.localName(node) ?? "")' declaration may not have both a 'default' and a 'fixed' value constraint")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "a '\(PureXML.Schema.XSDNode.localName(node) ?? "")' declaration may not have both a 'default' and a 'fixed' value constraint",
|
||||
node: node,
|
||||
))
|
||||
return
|
||||
}
|
||||
if hasDefaultWithNonOptionalUse(node) {
|
||||
errors.append("an 'attribute' with a 'default' value constraint must have use='optional'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "an 'attribute' with a 'default' value constraint must have use='optional'",
|
||||
node: node,
|
||||
))
|
||||
return
|
||||
}
|
||||
guard let typeName = PureXML.Schema.XSDNode.attribute(node, "type") else { return }
|
||||
@ -60,9 +66,12 @@ extension PureXML.Schema.XSDParser {
|
||||
guard uri == xsdNamespace, let builtin = PureXML.Schema.BuiltinType(rawValue: local),
|
||||
!PureXML.Schema.SimpleType(base: builtin).isValid(value)
|
||||
else { return }
|
||||
errors.append("the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for type '\(typeName)'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for type '\(typeName)'",
|
||||
node: node,
|
||||
))
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
/// A `default`/`fixed` value on an `element` or `attribute` whose `type` names a
|
||||
@ -79,9 +88,13 @@ extension PureXML.Schema.XSDParser {
|
||||
/// inline anonymous type is left alone, so no valid value constraint is rejected.
|
||||
/// The compiled `SimpleType` validator is the same one instance validation uses,
|
||||
/// so its whitespace and facet handling match.
|
||||
static func userTypeValueConstraintErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext, _ types: [String: PureXML.Schema.ElementType]) -> [String] {
|
||||
static func userTypeValueConstraintFindings(
|
||||
_ schema: XSDTree,
|
||||
_ context: PureXML.Schema.XSDContext,
|
||||
_ types: [String: PureXML.Schema.ElementType],
|
||||
) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
let bindings = context.namespaceBindings
|
||||
var errors: [String] = []
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
forEachValueConstrained(schema) { node in
|
||||
guard node.name?.namespaceURI == xsdNamespace,
|
||||
let typeName = unprefixedValue(node, "type")
|
||||
@ -93,9 +106,12 @@ extension PureXML.Schema.XSDParser {
|
||||
guard uri == context.targetNamespace, let resolved = types[local],
|
||||
let simple = simpleContentType(of: resolved), !simple.isValid(value)
|
||||
else { return }
|
||||
errors.append("the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for type '\(typeName)'")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for type '\(typeName)'",
|
||||
node: node,
|
||||
))
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
/// Like ``userTypeValueConstraintErrors`` but for an element or attribute whose
|
||||
@ -104,8 +120,8 @@ extension PureXML.Schema.XSDParser {
|
||||
/// against that type's value space. Validated against the simpleContent base's
|
||||
/// value space (not the local restriction facets), so only a clear value-space
|
||||
/// mismatch (a non-numeric `default` on a decimal-based type) is flagged.
|
||||
static func inlineTypeValueConstraintErrors(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext) -> [String] {
|
||||
var errors: [String] = []
|
||||
static func inlineTypeValueConstraintFindings(_ schema: XSDTree, _ context: PureXML.Schema.XSDContext) -> [PureXML.Schema.SchemaLocatedFinding] {
|
||||
var findings: [PureXML.Schema.SchemaLocatedFinding] = []
|
||||
forEachValueConstrained(schema) { node in
|
||||
guard node.name?.namespaceURI == xsdNamespace,
|
||||
unprefixedValue(node, "type") == nil
|
||||
@ -114,9 +130,12 @@ extension PureXML.Schema.XSDParser {
|
||||
guard let value = fixed ?? unprefixedValue(node, "default"),
|
||||
let simple = inlineSimpleType(of: node, context), !simple.isValid(value)
|
||||
else { return }
|
||||
errors.append("the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for the declared inline type")
|
||||
findings.append(PureXML.Schema.SchemaLocatedFinding(
|
||||
reason: "the \(fixed != nil ? "fixed" : "default") value '\(value)' is not valid for the declared inline type",
|
||||
node: node,
|
||||
))
|
||||
}
|
||||
return errors
|
||||
return findings
|
||||
}
|
||||
|
||||
/// The simple value space of an element/attribute's INLINE type: an inline
|
||||
|
||||
@ -62,7 +62,7 @@ extension PureXML.Schema.XSDParser {
|
||||
let bindings = PureXML.Schema.XSDNode.namespaceBindings(of: schema)
|
||||
collectStructure(schema, bindings: bindings, into: &findings)
|
||||
append(simpleTypeVarietyFacetErrors(schema), at: schema, into: &findings)
|
||||
append(valueConstraintErrors(schema), at: schema, into: &findings)
|
||||
findings += valueConstraintFindings(schema)
|
||||
append(topLevelDeclarationErrors(schema), at: schema, into: &findings)
|
||||
append(nestedNamedDefinitionErrors(schema), at: schema, into: &findings)
|
||||
append(anySimpleTypeRestrictionErrors(schema) + anySimpleTypeFacetErrors(schema), at: schema, into: &findings)
|
||||
|
||||
@ -72,6 +72,60 @@ struct SchemaLocatedDiagnosticsTests {
|
||||
#expect(range?.start.column == 3)
|
||||
}
|
||||
|
||||
@Test("A bad value constraint against a named user type is located on its declaring element")
|
||||
func test_userTypeValueConstraintLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:t" xmlns:t="urn:t">
|
||||
<xs:simpleType name="count"><xs:restriction base="xs:integer"/></xs:simpleType>
|
||||
<xs:element name="a" type="t:count" default="notanumber"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("is not valid for type 't:count'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
let range = tree.sourceRange(at: located.codingPath)
|
||||
#expect(range?.start.line == 3)
|
||||
#expect(range?.start.column == 3)
|
||||
}
|
||||
|
||||
@Test("A bad value constraint against an inline type is located on its declaring element")
|
||||
func test_inlineTypeValueConstraintLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="a" default="x">
|
||||
<xs:simpleType><xs:restriction base="xs:integer"/></xs:simpleType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("is not valid for the declared inline type") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
let range = tree.sourceRange(at: located.codingPath)
|
||||
#expect(range?.start.line == 2)
|
||||
#expect(range?.start.column == 3)
|
||||
}
|
||||
|
||||
@Test("A bad value constraint against a built-in type is located on its declaring element")
|
||||
func test_builtinValueConstraintLocated() throws {
|
||||
let xsd = """
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:element name="a" type="xs:integer" default="notanumber"/>
|
||||
</xs:schema>
|
||||
"""
|
||||
let findings = try inconsistentFindings(in: xsd)
|
||||
let located = try #require(findings.first { $0.reason.contains("is not valid for type 'xs:integer'") })
|
||||
#expect(located.codingPath.map(\.stringValue) == ["xs:schema", "xs:element"])
|
||||
|
||||
let (tree, _) = PureXML.readTree(xsd)
|
||||
let range = tree.sourceRange(at: located.codingPath)
|
||||
#expect(range?.start.line == 2)
|
||||
#expect(range?.start.column == 3)
|
||||
}
|
||||
|
||||
@Test("An xs:include targetNamespace mismatch is located on the include directive")
|
||||
func test_includeMismatchLocated() throws {
|
||||
let included = """
|
||||
|
||||
Loading…
Reference in New Issue
Block a user