Reformat `description` of `YamlError`

Add:
- `YamlError.Context` represents context information
- `YamlError.Mark` represents location in Yaml string
This commit is contained in:
Norio Nomura 2017-03-28 23:43:52 +09:00
parent 18af892042
commit 4bd7fdbb06
No known key found for this signature in database
GPG Key ID: D4A7318EB7F7138D
3 changed files with 126 additions and 106 deletions

View File

@ -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? {

View File

@ -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)
}
}

View File

@ -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")
)
}
}
}