Repository: eemeli/yaml
Pull request #243 (2021-03-13 15:13:33)

Re-prettify errors
This re-enables the functionality behind the `prettyErrors` option (default: `true`). Now in v2 as the information available to errors is a bit different, they also need to get prettified a bit differently.

Errors now point to a specific location in the source, rather than a range. This means that `error.linePos` contains only what was previously the "start" value.

In order to determine the line/col position, a LineCounter instance may be constructed internally when parsing. If one is provided via the `lineCounter` option, that'll be used instead.

For errors at the start of a line, the previous line may also be included in the `error.message` context for it.

```
> YAML.parse('foo: bar: baz')

YAMLParseError: Nested mappings are not allowed in compact mappings at line 1, column 6:

foo: bar: baz
     ^

    at [...] {
  offset: 5,
  linePos: { line: 1, col: 6 }
}
```

```
> YAML.parse('foo: bar\n- baz\n')

YAMLParseError: A block sequence may not be used as an implicit map key at line 2, column 1:

foo: bar
- baz
^

    at [...] {
  offset: 9,
  linePos: { line: 2, col: 1 }
}
```

```
> YAML.parse('foo: bar\n- baz\n', { prettyErrors: false })

YAMLParseError: A block sequence may not be used as an implicit map key
    at [...] {
  offset: 9
}
```


Patch:
diff --git a/docs/03_options.md b/docs/03_options.md
index 3c1cb65..923515a 100644
--- a/docs/03_options.md
+++ b/docs/03_options.md
@@ -26,7 +26,7 @@ Used by: `parse()`, `parseDocument()`, `parseAllDocuments()`, `new Composer()`,
 | ------------ | ------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------- |
 | intAsBigInt  | `boolean`     | `false` | Whether integers should be parsed into [BigInt] rather than `number` values.                                                            |
 | lineCounter  | `LineCounter` |         | If set, newlines will be tracked, to allow for `lineCounter.linePos(offset)` to provide the `{ line, col }` positions within the input. |
-| prettyErrors | `boolean`     | `false` | Include line position & node type directly in errors.                                                                                   |
+| prettyErrors | `boolean`     | `true`  | Include line/col position in errors, along with an extract of the source string.                                                        |
 | strict       | `boolean`     | `true`  | When parsing, do not ignore errors required by the YAML 1.2 spec, but caused by unambiguous content.                                    |
 
 [bigint]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/BigInt
diff --git a/docs/08_errors.md b/docs/08_errors.md
index dfbd98f..2014884 100644
--- a/docs/08_errors.md
+++ b/docs/08_errors.md
@@ -2,26 +2,30 @@
 
 Nearly all errors and warnings produced by the `yaml` parser functions contain the following fields:
 
-| Member  | Type     | Description                                                              |
-| ------- | -------- | ------------------------------------------------------------------------ |
-| name    | `string` | Either `YAMLParseError` or `YAMLWarning`                                 |
-| message | `string` | A human-readable description of the error                                |
-| offset  | `number` | The offset in the source at which this error or warning was encountered. |
+| Member  | Type                                          | Description                                                                                                                 |
+| ------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
+| linePos | `{ line: number, col: number } ⎮` `undefined` | If `prettyErrors` is enabled and `offset` is known, the one-indexed human-friendly source location.                         |
+| name    | `'YAMLParseError' ⎮` `'YAMLWarning'`          |                                                                                                                             |
+| message | `string`                                      | A human-readable description of the error                                                                                   |
+| offset  | `number`                                      | The offset in the source at which this error or warning was encountered. May be `-1` if the offset could not be determined. |
 
-If the `prettyErrors` option is enabled, the following fields are added with summary information regarding the error's source node, if available:
+A `YAMLParseError` is an error encountered while parsing a source as YAML.
+They are included in the `doc.errors` array.
+If that array is not empty when constructing a native representation of a document, the first error will be thrown.
 
-| Member   | Type                                | Description                                                                                   |
-| -------- | ----------------------------------- | --------------------------------------------------------------------------------------------- |
-| nodeType | `string`                            | A string constant identifying the type of node                                                |
-| range    | `{ start: number, end: ?number }`   | Character offset in the input string                                                          |
-| linePos  | `{ start: LinePos, end: ?LinePos }` | One-indexed human-friendly source location. `LinePos` here is `{ line: number, col: number }` |
+A `YAMLWarning` is not an error, but a spec-mandated warning about unsupported directives or a fallback resolution being used for a node with an unavailable tag.
+They are included in the `doc.warnings` array.
 
 In rare cases, the library may produce a more generic error. In particular, `TypeError` may occur when parsing invalid input using the `json` schema, and `ReferenceError` when the `maxAliasCount` limit is enountered.
 
-## YAMLParseError
+## Silencing Errors and Warnings
 
-An error encountered while parsing a source as YAML.
+Some of the errors encountered during parsing are required by the spec, but are caused by content that may be parsed unambiguously.
+To ignore these errors, use the `strict: false` option:
 
-## YAMLWarning
+- Comments must be separated from other tokens by white space characters (only when preceding node is known to have ended)
+- Implicit keys of flow sequence pairs need to be on a single line
+- The : indicator must be at most 1024 chars after the start of an implicit block mapping key
 
-Not an error, but a spec-mandated warning about unsupported directives or a fallback resolution being used for a node with an unavailable tag. Not used by the CST parser.
+For additional control, set the `logLevel` option to `'error'` (default: `'warn'`) to silence all warnings.
+Setting `logLevel: 'silent'` will ignore parsing errors completely, resulting in output that may well be rather broken.
diff --git a/src/compose/compose-doc.ts b/src/compose/compose-doc.ts
index bbc48a0..9b80ce1 100644
--- a/src/compose/compose-doc.ts
+++ b/src/compose/compose-doc.ts
@@ -7,7 +7,7 @@ import { resolveEnd } from './resolve-end.js'
 import { resolveProps } from './resolve-props.js'
 
 export function composeDoc(
-  options: Options | undefined,
+  options: Options,
   directives: Directives,
   { offset, start, value, end }: Tokens.Document,
   onError: (offset: number, message: string, warning?: boolean) => void
diff --git a/src/compose/composer.ts b/src/compose/composer.ts
index 1ff0754..5590353 100644
--- a/src/compose/composer.ts
+++ b/src/compose/composer.ts
@@ -50,13 +50,13 @@ export class Composer {
   private directives: Directives
   private doc: Document.Parsed | null = null
   private onDocument: (doc: Document.Parsed) => void
-  private options: Options | undefined
+  private options: Options
   private atDirectives = false
   private prelude: string[] = []
   private errors: YAMLParseError[] = []
   private warnings: YAMLWarning[] = []
 
-  constructor(onDocument: Composer['onDocument'], options?: Options) {
+  constructor(onDocument: Composer['onDocument'], options: Options = {}) {
     this.directives = new Directives({
       version: options?.version || defaultOptions.version
     })
diff --git a/src/errors.ts b/src/errors.ts
index d9f4d03..b7bbb44 100644
--- a/src/errors.ts
+++ b/src/errors.ts
@@ -1,60 +1,63 @@
-// import { Type } from './constants.js'
-// interface LinePos { line: number; col: number }
+import type { LineCounter } from './parse/line-counter'
 
 export class YAMLError extends Error {
   name: 'YAMLParseError' | 'YAMLWarning'
   message: string
-  offset?: number
+  offset: number
+  linePos?: { line: number; col: number }
 
-  // nodeType?: Type
-  // range?: CST.Range
-  // linePos?: { start: LinePos; end: LinePos }
-
-  constructor(name: YAMLError['name'], offset: number | null, message: string) {
+  constructor(name: YAMLError['name'], offset: number, message: string) {
     if (!message) throw new Error(`Invalid arguments for new ${name}`)
     super()
     this.name = name
     this.message = message
-    if (typeof offset === 'number') this.offset = offset
-  }
-
-  /**
-   * Drops `source` and adds `nodeType`, `range` and `linePos`, as well as
-   * adding details to `message`. Run automatically for document errors if
-   * the `prettyErrors` option is set.
-   */
-  makePretty() {
-    // this.nodeType = this.source.type
-    // const cst = this.source.context && this.source.context.root
-    // if (typeof this.offset === 'number') {
-    //   this.range = new Range(this.offset, this.offset + 1)
-    //   const start = cst && getLinePos(this.offset, cst)
-    //   if (start) {
-    //     const end = { line: start.line, col: start.col + 1 }
-    //     this.linePos = { start, end }
-    //   }
-    //   delete this.offset
-    // } else {
-    //   this.range = this.source.range
-    //   this.linePos = this.source.rangeAsLinePos
-    // }
-    // if (this.linePos) {
-    //   const { line, col } = this.linePos.start
-    //   this.message += ` at line ${line}, column ${col}`
-    //   const ctx = cst && getPrettyContext(this.linePos, cst)
-    //   if (ctx) this.message += `:\n\n${ctx}\n`
-    // }
+    this.offset = offset
   }
 }
 
 export class YAMLParseError extends YAMLError {
-  constructor(offset: number | null, message: string) {
+  constructor(offset: number, message: string) {
     super('YAMLParseError', offset, message)
   }
 }
 
 export class YAMLWarning extends YAMLError {
-  constructor(offset: number | null, message: string) {
+  constructor(offset: number, message: string) {
     super('YAMLWarning', offset, message)
   }
 }
+
+export const prettifyError = (src: string, lc: LineCounter) => (
+  error: YAMLError
+) => {
+  if (error.offset === -1) return
+  error.linePos = lc.linePos(error.offset)
+  const { line, col } = error.linePos
+  error.message += ` at line ${line}, column ${col}`
+
+  let ci = col - 1
+  let lineStr = src
+    .substring(lc.lineStarts[line - 1], lc.lineStarts[line])
+    .replace(/[\n\r]+$/, '')
+
+  // Trim to max 80 chars, keeping col position near the middle
+  if (ci >= 60 && lineStr.length > 80) {
+    const trimStart = Math.min(ci - 39, lineStr.length - 79)
+    lineStr = '…' + lineStr.substring(trimStart)
+    ci -= trimStart - 1
+  }
+  if (lineStr.length > 80) lineStr = lineStr.substring(0, 79) + '…'
+
+  // Include previous line in context if pointing at line start
+  if (line > 1 && /^ *$/.test(lineStr.substring(0, ci))) {
+    // Regexp won't match if start is trimmed
+    let prev = src.substring(lc.lineStarts[line - 2], lc.lineStarts[line - 1])
+    if (prev.length > 80) prev = prev.substring(0, 79) + '…\n'
+    lineStr = prev + lineStr
+  }
+
+  if (/[^ ]/.test(lineStr)) {
+    const pointer = ' '.repeat(ci) + '^'
+    error.message += `:\n\n${lineStr}\n${pointer}\n`
+  }
+}
diff --git a/src/public-api.ts b/src/public-api.ts
index a740731..1e6d8d1 100644
--- a/src/public-api.ts
+++ b/src/public-api.ts
@@ -1,10 +1,11 @@
 import { Composer } from './compose/composer.js'
 import type { Reviver } from './doc/applyReviver.js'
 import { Document, Replacer } from './doc/Document.js'
-import { YAMLParseError } from './errors.js'
+import { prettifyError, YAMLParseError } from './errors.js'
 import { warn } from './log.js'
 import type { ParsedNode } from './nodes/Node.js'
 import type { Options, ToJSOptions, ToStringOptions } from './options.js'
+import { LineCounter } from './parse/line-counter.js'
 import { Parser } from './parse/parser.js'
 
 export interface EmptyStream
@@ -13,6 +14,15 @@ export interface EmptyStream
   empty: true
 }
 
+function parseOptions(options: Options | undefined) {
+  const prettyErrors = !options || options.prettyErrors !== false
+  const lineCounter =
+    (options && options.lineCounter) ||
+    (prettyErrors && new LineCounter()) ||
+    null
+  return { lineCounter, prettyErrors }
+}
+
 /**
  * Parse the input as a stream of YAML documents.
  *
@@ -26,15 +36,23 @@ export function parseAllDocuments<T extends ParsedNode = ParsedNode>(
   source: string,
   options?: Options
 ): Document.Parsed<T>[] | EmptyStream {
+  const { lineCounter, prettyErrors } = parseOptions(options)
+
   const docs: Document.Parsed<T>[] = []
   const composer = new Composer(
     doc => docs.push(doc as Document.Parsed<T>),
     options
   )
-  const parser = new Parser(composer.next, options?.lineCounter?.addNewLine)
+  const parser = new Parser(composer.next, lineCounter?.addNewLine)
   parser.parse(source)
   composer.end()
 
+  if (prettyErrors && lineCounter)
+    for (const doc of docs) {
+      doc.errors.forEach(prettifyError(source, lineCounter))
+      doc.warnings.forEach(prettifyError(source, lineCounter))
+    }
+
   if (docs.length > 0) return docs
   return Object.assign<
     Document.Parsed<T>[],
@@ -48,7 +66,10 @@ export function parseDocument<T extends ParsedNode = ParsedNode>(
   source: string,
   options?: Options
 ) {
-  let doc: Document.Parsed<T> | null = null
+  const { lineCounter, prettyErrors } = parseOptions(options)
+
+  // `doc` is always set by compose.end(true) at the very latest
+  let doc: Document.Parsed<T> = null as any
   const composer = new Composer(_doc => {
     if (!doc) doc = _doc as Document.Parsed<T>
     else if (doc.options.logLevel !== 'silent') {
@@ -57,11 +78,15 @@ export function parseDocument<T extends ParsedNode = ParsedNode>(
       doc.errors.push(new YAMLParseError(_doc.range[0], errMsg))
     }
   }, options)
-  const parser = new Parser(composer.next, options?.lineCounter?.addNewLine)
+  const parser = new Parser(composer.next, lineCounter?.addNewLine)
   parser.parse(source)
   composer.end(true, source.length)
-  // `doc` is always set by compose.end(true) at the very latest
-  return (doc as unknown) as Document.Parsed<T>
+
+  if (prettyErrors && lineCounter) {
+    doc.errors.forEach(prettifyError(source, lineCounter))
+    doc.warnings.forEach(prettifyError(source, lineCounter))
+  }
+  return doc
 }
 
 /**


Test patch:
diff --git a/tests/doc/errors.js b/tests/doc/errors.js
index b8f8ab7..bc61c81 100644
--- a/tests/doc/errors.js
+++ b/tests/doc/errors.js
@@ -1,12 +1,5 @@
 import * as YAML from 'yaml'
 
-test('require a message and source for all errors', () => {
-  const exp = /Invalid arguments/
-  expect(() => new YAML.YAMLError()).toThrow(exp)
-  expect(() => new YAML.YAMLError('Foo')).toThrow(exp)
-  expect(() => new YAML.YAMLError('Foo', {})).toThrow(exp)
-})
-
 test('fail on map value indented with tab', () => {
   const src = 'a:\n\t1\nb:\n\t2\n'
   const doc = YAML.parseDocument(src)
@@ -19,57 +12,39 @@ test('fail on map value indented with tab', () => {
 test('eemeli/yaml#6', () => {
   const src = 'abc: 123\ndef'
   const doc = YAML.parseDocument(src)
-  expect(doc.errors).toMatchObject([{ name: 'YAMLParseError', offset: 9 }])
+  expect(doc.errors).toMatchObject([{ offset: 9 }])
 })
 
-describe.skip('eemeli/yaml#7', () => {
+describe('eemeli/yaml#7', () => {
   test('map', () => {
     const src = '{ , }\n---\n{ 123,,, }\n'
     const docs = YAML.parseAllDocuments(src)
-    expect(docs[0].errors).toMatchObject([
-      { name: 'YAMLParseError', offset: 2 }
-    ])
-    expect(docs[1].errors).toMatchObject([
-      { name: 'YAMLParseError', offset: 16 },
-      { name: 'YAMLParseError', offset: 17 }
-    ])
-    // const node = docs[0].errors[0].source
-    // expect(node).toBeInstanceOf(Node)
-    // expect(node.rangeAsLinePos).toMatchObject({
-    //   start: { line: 1, col: 1 },
-    //   end: { line: 1, col: 6 }
-    // })
+    expect(docs[0].errors).toMatchObject([{ offset: 2 }])
+    expect(docs[1].errors).toMatchObject([{ offset: 16 }, { offset: 17 }])
   })
+
   test('seq', () => {
     const src = '[ , ]\n---\n[ 123,,, ]\n'
     const docs = YAML.parseAllDocuments(src)
-    expect(docs[0].errors).toMatchObject([
-      { name: 'YAMLParseError', offset: 2 }
-    ])
-    expect(docs[1].errors).toMatchObject([
-      { name: 'YAMLParseError', offset: 16 },
-      { name: 'YAMLParseError', offset: 17 }
-    ])
-    // const node = docs[1].errors[0].source
-    // expect(node).toBeInstanceOf(Node)
-    // expect(node.rangeAsLinePos).toMatchObject({
-    //   start: { line: 3, col: 1 },
-    //   end: { line: 3, col: 11 }
-    // })
+    expect(docs[0].errors).toMatchObject([{ offset: 2 }])
+    expect(docs[1].errors).toMatchObject([{ offset: 16 }, { offset: 17 }])
   })
 })
 
 describe('block scalars', () => {
   test('invalid header', () => {
     const doc = YAML.parseDocument('>99\n foo\n')
-    expect(doc.errors).toMatchObject([
-      { message: 'Block scalar header includes extra characters: >99' },
-      { message: 'Unexpected scalar at node end' }
-    ])
+    expect(doc.errors).toMatchObject(
+      [
+        'Block scalar header includes extra characters: >99',
+        'Unexpected scalar at node end'
+      ].map(msg => ({ message: expect.stringContaining(msg) }))
+    )
   })
   test('missing newline at header end', () => {
     const doc = YAML.parseDocument('> foo\n')
-    expect(doc.errors).toMatchObject([{ message: 'Not a YAML token: foo' }])
+    expect(doc.errors).toHaveLength(1)
+    expect(doc.errors[0].message).toMatch('Not a YAML token: foo')
   })
 })
 
@@ -77,9 +52,10 @@ describe('block collections', () => {
   test('mapping with bad indentation', () => {
     const src = 'foo: "1"\n bar: 2\n'
     const doc = YAML.parseDocument(src)
-    expect(doc.errors).toMatchObject([
-      { message: 'All mapping items must start at the same column' }
-    ])
+    expect(doc.errors).toHaveLength(1)
+    expect(doc.errors[0].message).toMatch(
+      'All mapping items must start at the same column'
+    )
     expect(doc.contents).toMatchObject({
       items: [
         { key: { value: 'foo' }, value: { value: '1' } },
@@ -91,9 +67,10 @@ describe('block collections', () => {
   test('sequence with bad indentation', () => {
     const src = '- "foo"\n - bar\n'
     const doc = YAML.parseDocument(src)
-    expect(doc.errors).toMatchObject([
-      { message: 'All sequence items must start at the same column' }
-    ])
+    expect(doc.errors).toHaveLength(1)
+    expect(doc.errors[0].message).toMatch(
+      'All sequence items must start at the same column'
+    )
     expect(doc.contents).toMatchObject({
       items: [{ value: 'foo' }, { items: [{ value: 'bar' }] }]
     })
@@ -102,11 +79,13 @@ describe('block collections', () => {
   test('seq item in mapping', () => {
     const src = 'foo: "1"\n- bar\n'
     const doc = YAML.parseDocument(src)
-    expect(doc.errors).toMatchObject([
-      { message: 'A block sequence may not be used as an implicit map key' },
-      { message: 'Implicit keys need to be on a single line' },
-      { message: 'Implicit map keys need to be followed by map values' }
-    ])
+    expect(doc.errors).toMatchObject(
+      [
+        'A block sequence may not be used as an implicit map key',
+        'Implicit keys need to be on a single line',
+        'Implicit map keys need to be followed by map values'
+      ].map(msg => ({ message: expect.stringContaining(msg) }))
+    )
     expect(doc.contents).toMatchObject({
       items: [
         { key: { value: 'foo' }, value: { value: '1' } },
@@ -118,7 +97,12 @@ describe('block collections', () => {
   test('doubled value indicator', () => {
     const doc = YAML.parseDocument('foo : : bar\n')
     expect(doc.errors).toMatchObject([
-      { message: 'Nested mappings are not allowed in compact mappings' }
+      {
+        message:
+          'Nested mappings are not allowed in compact mappings at line 1, column 7:\n\n' +
+          'foo : : bar\n' +
+          '      ^\n'
+      }
     ])
   })
 
@@ -127,7 +111,9 @@ describe('block collections', () => {
     expect(doc.errors).toMatchObject([
       {
         message:
-          'The : indicator must be at most 1024 chars after the start of an implicit block mapping key'
+          'The : indicator must be at most 1024 chars after the start of an implicit block mapping key at line 1, column 1032:\n\n' +
+          '…xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx : bar\n' +
+          '                                                                             ^\n'
       }
     ])
   })
@@ -136,39 +122,30 @@ describe('block collections', () => {
 describe('flow collections', () => {
   test('start only of flow map (eemeli/yaml#8)', () => {
     const doc = YAML.parseDocument('{')
-    expect(doc.errors).toMatchObject([
-      {
-        name: 'YAMLParseError',
-        message: 'Expected flow map to end with }',
-        offset: 1
-      }
-    ])
+    const message = expect.stringContaining('Expected flow map to end with }')
+    expect(doc.errors).toMatchObject([{ message, offset: 1 }])
   })
 
   test('start only of flow sequence (eemeli/yaml#8)', () => {
     const doc = YAML.parseDocument('[')
-    expect(doc.errors).toMatchObject([
-      {
-        name: 'YAMLParseError',
-        message: 'Expected flow sequence to end with ]',
-        offset: 1
-      }
-    ])
+    const message = expect.stringContaining(
+      'Expected flow sequence to end with ]'
+    )
+    expect(doc.errors).toMatchObject([{ message, offset: 1 }])
   })
 
   test('flow sequence without end', () => {
     const doc = YAML.parseDocument('[ foo, bar,')
-    expect(doc.errors).toMatchObject([
-      {
-        name: 'YAMLParseError',
-        message: 'Expected flow sequence to end with ]',
-        offset: 11
-      }
-    ])
+    const message = expect.stringContaining(
+      'Expected flow sequence to end with ]'
+    )
+    expect(doc.errors).toMatchObject([{ message, offset: 11 }])
   })
 
   test('doc-end within flow sequence', () => {
-    const doc = YAML.parseDocument('[ foo, bar,\n...\n]')
+    const doc = YAML.parseDocument('[ foo, bar,\n...\n]', {
+      prettyErrors: false
+    })
     expect(doc.errors).toMatchObject([
       { message: 'Expected flow sequence to end with ]' },
       { message: 'Unexpected flow-seq-end token in YAML document: "]"' },
@@ -181,59 +158,57 @@ describe('flow collections', () => {
 
   test('block scalar in flow collection', () => {
     const doc = YAML.parseDocument('{ |\n foo\n}')
-    expect(doc.errors).toMatchObject([
-      { message: 'Plain value cannot start with block scalar indicator |' }
-    ])
+    expect(doc.errors).toHaveLength(1)
+    expect(doc.errors[0].message).toMatch(
+      'Plain value cannot start with block scalar indicator |'
+    )
   })
 
   test('block seq in flow collection', () => {
     const doc = YAML.parseDocument('{\n- foo\n}')
-    expect(doc.errors).toMatchObject([
-      { message: 'Block collections are not allowed within flow collections' }
-    ])
+    expect(doc.errors).toHaveLength(1)
+    expect(doc.errors[0].message).toMatch(
+      'Block collections are not allowed within flow collections'
+    )
   })
 
   test('anchor before explicit key indicator', () => {
     const doc = YAML.parseDocument('{ &a ? A }')
-    expect(doc.errors).toMatchObject([
-      { message: 'Anchors and tags must be after the ? indicator' }
-    ])
+    expect(doc.errors).toHaveLength(1)
+    expect(doc.errors[0].message).toMatch(
+      'Anchors and tags must be after the ? indicator'
+    )
   })
 })
 
 describe('comments', () => {
   test('comment without whitespace after tag', () => {
     const doc = YAML.parseDocument('!<a>#cc\nA')
-    expect(doc.errors).toMatchObject([
-      {
-        message:
-          'Comments must be separated from other tokens by white space characters'
-      }
-    ])
+    expect(doc.errors).toHaveLength(1)
+    expect(doc.errors[0].message).toMatch(
+      'Comments must be separated from other tokens by white space characters'
+    )
   })
 
   test('comment without whitespace after value', () => {
     const doc = YAML.parseDocument('foo: "bar"#cc')
-    expect(doc.errors).toMatchObject([
-      {
-        message:
-          'Comments must be separated from other tokens by white space characters'
-      }
-    ])
+    expect(doc.errors).toHaveLength(1)
+    expect(doc.errors[0].message).toMatch(
+      'Comments must be separated from other tokens by white space characters'
+    )
   })
 })
 
-describe.skip('pretty errors', () => {
+describe('pretty errors', () => {
   test('eemeli/yaml#6', () => {
     const src = 'abc: 123\ndef'
     const doc = YAML.parseDocument(src, { prettyErrors: true })
     expect(doc.errors).toMatchObject([
       {
-        name: 'YAMLParseError',
         message:
-          'Implicit map keys need to be followed by map values at line 2, column 1:\n\ndef\n^^^\n',
+          'Implicit map keys need to be followed by map values at line 2, column 1:\n\nabc: 123\ndef\n^\n',
         offset: 9,
-        linePos: { start: { line: 2, col: 1 }, end: { line: 2, col: 4 } }
+        linePos: { line: 2, col: 1 }
       }
     ])
     expect(doc.errors[0]).not.toHaveProperty('source')
@@ -244,25 +219,25 @@ describe.skip('pretty errors', () => {
     const docs = YAML.parseAllDocuments(src, { prettyErrors: true })
     expect(docs[0].errors).toMatchObject([
       {
-        name: 'YAMLParseError',
-        message: 'Unexpected , in flow map',
+        message:
+          'Unexpected , in flow map at line 1, column 3:\n\n{ , }\n  ^\n',
         offset: 2,
-        linePos: { start: { line: 1, col: 3 }, end: { line: 1, col: 4 } }
+        linePos: { line: 1, col: 3 }
       }
     ])
     expect(docs[0].errors[0]).not.toHaveProperty('source')
     expect(docs[1].errors).toMatchObject([
       {
-        name: 'YAMLParseError',
-        message: 'Unexpected , in flow map',
+        message:
+          'Unexpected , in flow map at line 3, column 7:\n\n{ 123,,, }\n      ^\n',
         offset: 16,
-        linePos: { start: { line: 3, col: 7 }, end: { line: 3, col: 8 } }
+        linePos: { line: 3, col: 7 }
       },
       {
-        name: 'YAMLParseError',
-        message: 'Unexpected , in flow map',
+        message:
+          'Unexpected , in flow map at line 3, column 8:\n\n{ 123,,, }\n       ^\n',
         offset: 17,
-        linePos: { start: { line: 3, col: 8 }, end: { line: 3, col: 9 } }
+        linePos: { line: 3, col: 8 }
       }
     ])
     expect(docs[1].errors[0]).not.toHaveProperty('source')
@@ -279,17 +254,19 @@ describe.skip('pretty errors', () => {
 describe('tags on invalid nodes', () => {
   test('!!map on scalar', () => {
     const doc = YAML.parseDocument('!!map foo')
-    expect(doc.warnings).toMatchObject([
-      { message: 'Unresolved tag: tag:yaml.org,2002:map' }
-    ])
+    expect(doc.warnings).toHaveLength(1)
+    expect(doc.warnings[0].message).toMatch(
+      'Unresolved tag: tag:yaml.org,2002:map'
+    )
     expect(doc.toJS()).toBe('foo')
   })
 
   test('!!str on map', () => {
     const doc = YAML.parseDocument('!!str { answer: 42 }')
-    expect(doc.warnings).toMatchObject([
-      { message: 'Unresolved tag: tag:yaml.org,2002:str' }
-    ])
+    expect(doc.warnings).toHaveLength(1)
+    expect(doc.warnings[0].message).toMatch(
+      'Unresolved tag: tag:yaml.org,2002:str'
+    )
     expect(doc.toJS()).toMatchObject({ answer: 42 })
   })
 })
@@ -309,10 +286,10 @@ describe('invalid options', () => {
 })
 
 test('broken document with comment before first node', () => {
-  const doc = YAML.parseDocument('#c\n*x\nfoo\n')
+  const doc = YAML.parseDocument('#c\n*x\nfoo\n', { prettyErrors: false })
   expect(doc.errors).toMatchObject([
-    { name: 'YAMLParseError', message: 'Aliased anchor not found: x' },
-    { name: 'YAMLParseError', message: 'Unexpected scalar at node end' }
+    { message: 'Aliased anchor not found: x' },
+    { message: 'Unexpected scalar at node end' }
   ])
 })
 
@@ -320,20 +297,20 @@ describe('broken directives', () => {
   for (const tag of ['%TAG', '%YAML'])
     test(`incomplete ${tag} directive`, () => {
       const doc = YAML.parseDocument(`${tag}\n---\n`)
-      expect(doc.errors).toMatchObject([{ name: 'YAMLParseError', offset: 0 }])
+      expect(doc.errors).toMatchObject([{ offset: 0 }])
     })
 
   test('missing separator', () => {
     const doc = YAML.parseDocument(`%YAML 1.2\n`)
-    expect(doc.errors).toMatchObject([{ name: 'YAMLParseError', offset: 10 }])
+    expect(doc.errors).toMatchObject([{ offset: 10 }])
   })
 })
 
 test('multiple tags on one node', () => {
   const doc = YAML.parseDocument('!foo !bar baz\n')
   expect(doc.contents).toMatchObject({ value: 'baz', type: 'PLAIN' })
-  expect(doc.errors).toMatchObject([{ name: 'YAMLParseError' }])
-  expect(doc.warnings).toMatchObject([{}])
+  expect(doc.errors).toHaveLength(1)
+  expect(doc.warnings).toHaveLength(1)
 })
 
 describe('logLevel', () => {
@@ -346,7 +323,8 @@ describe('logLevel', () => {
 
   test('by default, warn for tag fallback', () => {
     YAML.parse('!foo bar')
-    const message = 'Unresolved tag: !foo'
+    const message =
+      'Unresolved tag: !foo at line 1, column 1:\n\n' + '!foo bar\n^\n'
     expect(mock.mock.calls).toMatchObject([[{ message }]])
   })
 
diff --git a/tests/doc/types.js b/tests/doc/types.js
index b5041b1..5646608 100644
--- a/tests/doc/types.js
+++ b/tests/doc/types.js
@@ -479,7 +479,7 @@ date (00:00:00Z): 2002-12-14\n`)
 
     test('require unique keys', () => {
       const src = `!!omap\n- a: 1\n- b: 2\n- b: 9\n`
-      const doc = YAML.parseDocument(src, { version: '1.1' })
+      const doc = YAML.parseDocument(src, { prettyErrors: false, version: '1.1' })
       expect(doc.errors).toMatchObject([
         {
           name: 'YAMLParseError',
@@ -530,7 +530,7 @@ date (00:00:00Z): 2002-12-14\n`)
 
     test('require null values', () => {
       const src = `!!set\n? a\n? b\nc: d\n`
-      const doc = YAML.parseDocument(src, { version: '1.1' })
+      const doc = YAML.parseDocument(src, { prettyErrors: false, version: '1.1' })
       expect(doc.errors).toMatchObject([
         {
           name: 'YAMLParseError',