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.
179 lines
7.7 KiB
Swift
179 lines
7.7 KiB
Swift
import Testing
|
|
@testable import PureXML
|
|
|
|
/// Schema compile findings carry validation coding paths that resolve to source
|
|
/// spans on a ranged schema tree, the IDE underline flow for #169.
|
|
@Suite("Schema located compile diagnostics")
|
|
struct SchemaLocatedDiagnosticsTests {
|
|
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 xs:ID is located on the offending component")
|
|
func test_duplicateIdLocated() throws {
|
|
let xsd = """
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
<xs:element name="a" id="dup"/>
|
|
<xs:element name="b" id="dup"/>
|
|
</xs:schema>
|
|
"""
|
|
let findings = try inconsistentFindings(in: xsd)
|
|
let duplicate = try #require(findings.first { $0.reason.contains("duplicate id") })
|
|
#expect(duplicate.codingPath.map(\.stringValue) == ["xs:schema", "xs:element"])
|
|
#expect(duplicate.codingPath.last?.intValue == 2)
|
|
|
|
let (tree, _) = PureXML.readTree(xsd)
|
|
let range = tree.sourceRange(at: duplicate.codingPath)
|
|
#expect(range?.start.line == 3)
|
|
#expect(range?.start.column == 3)
|
|
}
|
|
|
|
@Test("An invalid xs:ID value is located on its declaring component")
|
|
func test_invalidIdLocated() throws {
|
|
let xsd = """
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
<xs:element name="a" id="123"/>
|
|
</xs:schema>
|
|
"""
|
|
let findings = try inconsistentFindings(in: xsd)
|
|
let invalid = try #require(findings.first { $0.reason.contains("not a valid NCName") })
|
|
#expect(invalid.codingPath.map(\.stringValue) == ["xs:schema", "xs:element"])
|
|
|
|
let (tree, _) = PureXML.readTree(xsd)
|
|
let range = tree.sourceRange(at: invalid.codingPath)
|
|
#expect(range?.start.line == 2)
|
|
#expect(range?.start.column == 3)
|
|
}
|
|
|
|
@Test("An ID-typed default/fixed value constraint is located on its declaring component")
|
|
func test_idValueConstraintLocated() throws {
|
|
let xsd = """
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
<xs:element name="a" type="xs:ID" default="x"/>
|
|
</xs:schema>
|
|
"""
|
|
let findings = try inconsistentFindings(in: xsd)
|
|
let located = try #require(findings.first { $0.reason.contains("must not have a default or fixed value") })
|
|
#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 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 = """
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:other">
|
|
<xs:element name="e" type="xs:string"/>
|
|
</xs:schema>
|
|
"""
|
|
let main = """
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="urn:main">
|
|
<xs:include schemaLocation="other.xsd"/>
|
|
<xs:element name="root" type="xs:string"/>
|
|
</xs:schema>
|
|
"""
|
|
let findings: [PureXML.Validation.ValidationError]
|
|
do {
|
|
_ = try PureXML.Schema.Document(main, schemaLoader: { $0 == "other.xsd" ? included : nil })
|
|
Issue.record("expected include mismatch to be rejected")
|
|
return
|
|
} catch let error as PureXML.Schema.SchemaError {
|
|
guard case let .inconsistent(found) = error else {
|
|
Issue.record("expected .inconsistent, got \(error)")
|
|
return
|
|
}
|
|
findings = found
|
|
}
|
|
let mismatch = try #require(findings.first { $0.reason.contains("included schema targetNamespace") })
|
|
#expect(mismatch.codingPath.map(\.stringValue) == ["xs:schema", "xs:include"])
|
|
|
|
let (tree, _) = PureXML.readTree(main)
|
|
let range = tree.sourceRange(at: mismatch.codingPath)
|
|
#expect(range?.start.line == 2)
|
|
#expect(range?.start.column == 3)
|
|
}
|
|
|
|
@Test("SchemaError.inconsistentFindings exposes the validation errors")
|
|
func test_inconsistentFindingsAccessor() throws {
|
|
let xsd = """
|
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
<xs:element name="a" id="123"/>
|
|
</xs:schema>
|
|
"""
|
|
do {
|
|
_ = try PureXML.Schema.Document(xsd)
|
|
Issue.record("expected schema compilation to fail")
|
|
} catch let error as PureXML.Schema.SchemaError {
|
|
let findings = try #require(error.inconsistentFindings)
|
|
#expect(findings.contains { $0.reason.contains("not a valid NCName") })
|
|
}
|
|
}
|
|
}
|