Skip to content

Commit

Permalink
Re-prettify errors (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
eemeli committed Mar 13, 2021
1 parent 7009f79 commit 59398d2
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 189 deletions.
2 changes: 1 addition & 1 deletion docs/03_options.md
Expand Up @@ -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
Expand Down
34 changes: 19 additions & 15 deletions docs/08_errors.md
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion src/compose/compose-doc.ts
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/compose/composer.ts
Expand Up @@ -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
})
Expand Down
81 changes: 42 additions & 39 deletions 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`
}
}
37 changes: 31 additions & 6 deletions 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
Expand All @@ -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.
*
Expand All @@ -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>[],
Expand All @@ -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') {
Expand All @@ -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
}

/**
Expand Down

0 comments on commit 59398d2

Please sign in to comment.