Reformat `description` of `YamlError`
Add: - `YamlError.Context` represents context information - `YamlError.Mark` represents location in Yaml string
This commit is contained in:
parent
18af892042
commit
4bd7fdbb06
|
@ -151,10 +151,9 @@ public final class Parser {
|
|||
let event = try parse()
|
||||
if event.type != YAML_STREAM_END_EVENT {
|
||||
throw YamlError.composer(
|
||||
context: "expected a single document in the stream",
|
||||
problem: "but found another document",
|
||||
line: event.event.start_mark.line,
|
||||
column: event.event.start_mark.column,
|
||||
context: YamlError.Context(text: "expected a single document in the stream",
|
||||
mark: .init(line: 0, column: 0)),
|
||||
problem: "but found another document", event.startMark,
|
||||
yaml: yaml
|
||||
)
|
||||
}
|
||||
|
@ -212,11 +211,9 @@ extension Parser {
|
|||
fatalError("unreachable")
|
||||
}
|
||||
guard let node = anchors[alias] else {
|
||||
throw YamlError.composer(
|
||||
context: nil,
|
||||
problem: "found undefined alias",
|
||||
line: event.event.start_mark.line, column: event.event.start_mark.column,
|
||||
yaml: yaml)
|
||||
throw YamlError.composer(context: nil,
|
||||
problem: "found undefined alias", event.startMark,
|
||||
yaml: yaml)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
@ -328,6 +325,11 @@ fileprivate class Event {
|
|||
return event.data.mapping_start.implicit != 0
|
||||
? nil : string(from: event.data.sequence_start.tag)
|
||||
}
|
||||
|
||||
// start_mark
|
||||
var startMark: YamlError.Mark {
|
||||
return YamlError.Mark(line: event.start_mark.line, column: event.start_mark.column)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func string(from pointer: UnsafePointer<UInt8>!) -> String? {
|
||||
|
|
|
@ -16,11 +16,11 @@ public enum YamlError: Swift.Error {
|
|||
|
||||
// line and column start from 0, column is counted by unicodeScalars
|
||||
/// YAML_SCANNER_ERROR. Cannot scan the input stream.
|
||||
case scanner(context: String, problem: String, line: Int, column: Int, yaml: String)
|
||||
case scanner(context: Context, problem: String, Mark, yaml: String)
|
||||
/// YAML_PARSER_ERROR. Cannot parse the input stream.
|
||||
case parser(context: String?, problem: String, line: Int, column: Int, yaml: String)
|
||||
case parser(context: Context?, problem: String, Mark, yaml: String)
|
||||
/// YAML_COMPOSER_ERROR. Cannot compose a YAML document.
|
||||
case composer(context: String?, problem: String, line: Int, column: Int, yaml: String)
|
||||
case composer(context: Context?, problem: String, Mark, yaml: String)
|
||||
|
||||
// Used in `yaml_emitter_t`
|
||||
/// YAML_WRITER_ERROR. Cannot write to the output stream.
|
||||
|
@ -30,10 +30,35 @@ public enum YamlError: Swift.Error {
|
|||
|
||||
// Used in `NodeRepresentable`
|
||||
case representer(problem: String)
|
||||
|
||||
public struct Mark: CustomStringConvertible {
|
||||
public let line: Int, column: Int
|
||||
public var description: String { return "\(line):\(column)" }
|
||||
}
|
||||
|
||||
public struct Context: CustomStringConvertible {
|
||||
public let text: String
|
||||
public let mark: Mark
|
||||
public var description: String {
|
||||
return text + " in line \(mark.line), column \(mark.column)\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension YamlError {
|
||||
init(from parser: yaml_parser_t, with yaml: String) {
|
||||
func context(from parser: yaml_parser_t) -> Context? {
|
||||
guard let context = parser.context else { return nil }
|
||||
return Context(
|
||||
text: String(cString: context),
|
||||
mark: Mark(line: parser.context_mark.line, column: parser.context_mark.column)
|
||||
)
|
||||
}
|
||||
|
||||
func problemMark(from parser: yaml_parser_t) -> Mark {
|
||||
return Mark(line: parser.problem_mark.line, column: parser.problem_mark.column)
|
||||
}
|
||||
|
||||
switch parser.error {
|
||||
case YAML_MEMORY_ERROR:
|
||||
self = .memory
|
||||
|
@ -43,22 +68,16 @@ extension YamlError {
|
|||
value: parser.problem_value,
|
||||
yaml: yaml)
|
||||
case YAML_SCANNER_ERROR:
|
||||
self = .scanner(context: parser.context != nil ? String(cString: parser.context) : "",
|
||||
problem: String(cString: parser.problem),
|
||||
line: parser.problem_mark.line,
|
||||
column: parser.problem_mark.column,
|
||||
self = .scanner(context: context(from: parser)!,
|
||||
problem: String(cString: parser.problem), problemMark(from: parser),
|
||||
yaml: yaml)
|
||||
case YAML_PARSER_ERROR:
|
||||
self = .parser(context: parser.context != nil ? String(cString: parser.context) : nil,
|
||||
problem: String(cString: parser.problem),
|
||||
line: parser.problem_mark.line,
|
||||
column: parser.problem_mark.column,
|
||||
yaml: yaml)
|
||||
self = .parser(context: context(from: parser),
|
||||
problem: String(cString: parser.problem), problemMark(from: parser),
|
||||
yaml: yaml)
|
||||
case YAML_COMPOSER_ERROR:
|
||||
self = .composer(context: parser.context != nil ? String(cString: parser.context) : nil,
|
||||
problem: String(cString: parser.problem),
|
||||
line: parser.problem_mark.line,
|
||||
column: parser.problem_mark.column,
|
||||
self = .composer(context: context(from: parser),
|
||||
problem: String(cString: parser.problem), problemMark(from: parser),
|
||||
yaml: yaml)
|
||||
default:
|
||||
fatalError("Parser has unknown error: \(parser.error)!")
|
||||
|
@ -85,45 +104,44 @@ extension YamlError: CustomStringConvertible {
|
|||
case .memory:
|
||||
return "Memory error"
|
||||
case let .reader(problem, byteOffset, value, yaml):
|
||||
#if USE_UTF8
|
||||
guard let (_, column, contents) = yaml.utf8LineNumberColumnAndContents(at: byteOffset)
|
||||
else { return "\(problem) at byte offset: \(byteOffset), value: \(value)" }
|
||||
#else
|
||||
guard let (_, column, contents) = yaml.utf16LineNumberColumnAndContents(at: byteOffset / 2)
|
||||
else { return "\(problem) at byte offset: \(byteOffset), value: \(value)" }
|
||||
#endif
|
||||
return contents.endingWithNewLine
|
||||
+ String(repeating: " ", count: column - 1) + "^ " + problem
|
||||
case let .scanner(context, problem, line, column, yaml):
|
||||
return describing(with: yaml, context, problem, line, column)
|
||||
case let .parser(context, problem, line, column, yaml):
|
||||
return describing(with: yaml, context ?? "", problem, line, column)
|
||||
case let .composer(context, problem, line, column, yaml):
|
||||
return describing(with: yaml, context ?? "", problem, line, column)
|
||||
case let .emitter(problem):
|
||||
guard let (mark, contents) = markAndSnippet(from: yaml, byteOffset)
|
||||
else { return "\(problem) at byte offset: \(byteOffset), value: \(value)" }
|
||||
return "\(mark): error: reader: \(problem):\n" + contents.endingWithNewLine
|
||||
+ String(repeating: " ", count: mark.column - 1) + "^"
|
||||
case let .scanner(context, problem, mark, yaml):
|
||||
return "\(mark): error: scanner: \(context)\(problem):\n" + snippet(from: yaml, mark)
|
||||
case let .parser(context, problem, mark, yaml):
|
||||
return "\(mark): error: parser: \(context?.description ?? "")\(problem):\n" + snippet(from: yaml, mark)
|
||||
case let .composer(context, problem, mark, yaml):
|
||||
return "\(mark): error: composer: \(context?.description ?? "")\(problem):\n" + snippet(from: yaml, mark)
|
||||
case let .writer(problem), let .emitter(problem), let .representer(problem):
|
||||
return problem
|
||||
default:
|
||||
fatalError("unreachable")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension YamlError {
|
||||
fileprivate func describing(
|
||||
with yaml: String,
|
||||
_ context: String,
|
||||
_ problem: String,
|
||||
_ line: Int,
|
||||
_ column: Int // libYAML counts column by unicodeScalars.
|
||||
) -> String {
|
||||
let contents = yaml.substring(at: line)
|
||||
fileprivate func snippet(from yaml: String, _ mark: Mark) -> String {
|
||||
let contents = yaml.substring(at: mark.line)
|
||||
// libYAML counts column by unicodeScalars.
|
||||
let columnIndex = contents.unicodeScalars
|
||||
.index(contents.unicodeScalars.startIndex,
|
||||
offsetBy: column,
|
||||
offsetBy: mark.column,
|
||||
limitedBy: contents.unicodeScalars.endIndex)?
|
||||
.samePosition(in: contents) ?? contents.endIndex
|
||||
let column = contents.distance(from: contents.startIndex, to: columnIndex)
|
||||
return contents.endingWithNewLine +
|
||||
String(repeating: " ", count: column) + "^ " + problem + " " + context
|
||||
String(repeating: " ", count: column) + "^"
|
||||
}
|
||||
|
||||
fileprivate func markAndSnippet(from yaml: String, _ byteOffset: Int) -> (Mark, String)? {
|
||||
#if USE_UTF8
|
||||
guard let (line, column, contents) = yaml.utf8LineNumberColumnAndContents(at: byteOffset)
|
||||
else { return nil }
|
||||
#else
|
||||
guard let (line, column, contents) = yaml.utf16LineNumberColumnAndContents(at: byteOffset / 2)
|
||||
else { return nil }
|
||||
#endif
|
||||
return (Mark(line: line, column: column), contents)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,49 +21,42 @@ class YamlErrorTests: XCTestCase {
|
|||
func testYamlErrorReader() throws {
|
||||
// reader
|
||||
let yaml = "test: 'テスト\u{12}'"
|
||||
do {
|
||||
_ = try Parser(yaml: yaml).nextRoot()
|
||||
XCTFail("should not happen")
|
||||
} catch let error as YamlError {
|
||||
let expected = [
|
||||
XCTAssertThrowsError(_ = try Parser(yaml: yaml).nextRoot()) { error in
|
||||
XCTAssertTrue(error is YamlError)
|
||||
XCTAssertEqual("\(error)", [
|
||||
"1:11: error: reader: control characters are not allowed:",
|
||||
"test: 'テスト\u{12}'",
|
||||
" ^ control characters are not allowed"
|
||||
" ^"
|
||||
].joined(separator: "\n")
|
||||
XCTAssertEqual(error.description, expected)
|
||||
} catch {
|
||||
XCTFail("should not happen")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func testYamlErrorScanner() throws {
|
||||
let yaml = "test: 'テスト"
|
||||
do {
|
||||
_ = try Parser(yaml: yaml).nextRoot()
|
||||
XCTFail("should not happen")
|
||||
} catch let error as YamlError {
|
||||
let expected = [
|
||||
XCTAssertThrowsError(_ = try Parser(yaml: yaml).nextRoot()) { error in
|
||||
XCTAssertTrue(error is YamlError)
|
||||
XCTAssertEqual("\(error)", [
|
||||
"0:10: error: scanner: while scanning a quoted scalar in line 0, column 6",
|
||||
"found unexpected end of stream:",
|
||||
"test: 'テスト",
|
||||
" ^ found unexpected end of stream while scanning a quoted scalar"
|
||||
" ^"
|
||||
].joined(separator: "\n")
|
||||
XCTAssertEqual(error.description, expected)
|
||||
} catch {
|
||||
XCTFail("should not happen")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func testYamlErrorParser() throws {
|
||||
let yaml = "- [キー1: 値1]\n- [key1: value1, key2: ,"
|
||||
do {
|
||||
_ = try Parser(yaml: yaml).nextRoot()
|
||||
XCTFail("should not happen")
|
||||
} catch let error as YamlError {
|
||||
let expected = [
|
||||
XCTAssertThrowsError(_ = try Parser(yaml: yaml).nextRoot()) { error in
|
||||
XCTAssertTrue(error is YamlError)
|
||||
XCTAssertEqual("\(error)", [
|
||||
"2:0: error: parser: while parsing a flow node in line 2, column 0",
|
||||
"did not find expected node content:",
|
||||
"- [key1: value1, key2: ,",
|
||||
"^ did not find expected node content while parsing a flow node"
|
||||
"^"
|
||||
].joined(separator: "\n")
|
||||
XCTAssertEqual(error.description, expected)
|
||||
} catch {
|
||||
XCTFail("should not happen")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,12 +67,14 @@ class YamlErrorTests: XCTestCase {
|
|||
// first iteration returns scalar
|
||||
XCTAssertEqual(try parser.nextRoot(), Node("", Tag(.null), .literal))
|
||||
// second iteration throws error
|
||||
XCTAssertThrowsError(try parser.nextRoot()) {
|
||||
if let error = $0 as? YamlError {
|
||||
XCTAssertEqual(error.description, "a\n^ did not find expected <document start> ")
|
||||
} else {
|
||||
XCTFail()
|
||||
}
|
||||
XCTAssertThrowsError(try parser.nextRoot()) { error in
|
||||
XCTAssertTrue(error is YamlError)
|
||||
XCTAssertEqual("\(error)", [
|
||||
"1:0: error: parser: did not find expected <document start>:",
|
||||
"a",
|
||||
"^"
|
||||
].joined(separator: "\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,38 +82,43 @@ class YamlErrorTests: XCTestCase {
|
|||
let invalidYAML = "|\na"
|
||||
|
||||
let parser = try Parser(yaml: invalidYAML)
|
||||
XCTAssertThrowsError(try parser.singleRoot()) {
|
||||
if let error = $0 as? YamlError {
|
||||
XCTAssertEqual(error.description, "a\n^ did not find expected <document start> ")
|
||||
} else {
|
||||
XCTFail()
|
||||
}
|
||||
XCTAssertThrowsError(try parser.singleRoot()) { error in
|
||||
XCTAssertTrue(error is YamlError)
|
||||
XCTAssertEqual("\(error)", [
|
||||
"1:0: error: parser: did not find expected <document start>:",
|
||||
"a",
|
||||
"^"
|
||||
].joined(separator: "\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func testSingleRootThrowsOnMultipleDocuments() throws {
|
||||
let multipleDocuments = "document 1\n---\ndocument 2\n"
|
||||
let parser = try Parser(yaml: multipleDocuments)
|
||||
XCTAssertThrowsError(try parser.singleRoot()) {
|
||||
if let error = $0 as? YamlError {
|
||||
XCTAssertEqual(error.description,
|
||||
"---\n^ but found another document expected a single document in the stream")
|
||||
} else {
|
||||
XCTFail()
|
||||
}
|
||||
XCTAssertThrowsError(try parser.singleRoot()) { error in
|
||||
XCTAssertTrue(error is YamlError)
|
||||
XCTAssertEqual("\(error)", [
|
||||
"1:0: error: composer: expected a single document in the stream in line 0, column 0",
|
||||
"but found another document:",
|
||||
"---",
|
||||
"^"
|
||||
].joined(separator: "\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func testUndefinedAliasCausesError() throws {
|
||||
let undefinedAlias = "*undefinedAlias\n"
|
||||
let parser = try Parser(yaml: undefinedAlias)
|
||||
XCTAssertThrowsError(try parser.singleRoot()) {
|
||||
if let error = $0 as? YamlError {
|
||||
XCTAssertEqual(error.description,
|
||||
"*undefinedAlias\n^ found undefined alias ")
|
||||
} else {
|
||||
XCTFail()
|
||||
}
|
||||
XCTAssertThrowsError(try parser.singleRoot()) { error in
|
||||
XCTAssertTrue(error is YamlError)
|
||||
XCTAssertEqual("\(error)", [
|
||||
"0:0: error: composer: found undefined alias:",
|
||||
"*undefinedAlias",
|
||||
"^"
|
||||
].joined(separator: "\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue