Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 38 additions & 19 deletions src/errors/errors/pretty-print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,47 @@ const formatSuggestions = (suggestions?: string[]): string | undefined => {
return `${label}\n${indent(multiple, 2)}`
}

export default function prettyPrint(error: Error & PrettyPrintableError & CLIErrorDisplayOptions): string | undefined {
if (settings.debug) {
return error.stack
}
type CombinedErrorType = Error & PrettyPrintableError & CLIErrorDisplayOptions

function isCombinedErrorType(obj: any): obj is CombinedErrorType {
return Boolean(obj) && 'name' in obj && 'message' in obj
}

export default function prettyPrint(error: CombinedErrorType): string | undefined {
const prettyPrintedErrors: string[] = []
let currentError: unknown = error
let isDeep: boolean = false
while (isCombinedErrorType(currentError)) {
if (settings.debug && currentError.stack) {
prettyPrintedErrors.push(`${isDeep ? 'Caused by: ' : ''}${currentError.stack}`)
} else {
const {bang, code, message, name: errorSuffix, ref, suggestions} = currentError

const {bang, code, message, name: errorSuffix, ref, suggestions} = error
// errorSuffix is pulled from the 'name' property on CLIError
// and is like either Error or Warning
const formattedHeader = message ? `${errorSuffix || 'Error'}: ${message}` : undefined
const formattedCode = code ? `Code: ${code}` : undefined
const formattedSuggestions = formatSuggestions(suggestions)
const formattedReference = ref ? `Reference: ${ref}` : undefined

// errorSuffix is pulled from the 'name' property on CLIError
// and is like either Error or Warning
const formattedHeader = message ? `${errorSuffix || 'Error'}: ${message}` : undefined
const formattedCode = code ? `Code: ${code}` : undefined
const formattedSuggestions = formatSuggestions(suggestions)
const formattedReference = ref ? `Reference: ${ref}` : undefined
const formatted = [formattedHeader, formattedCode, formattedSuggestions, formattedReference]
.filter(Boolean)
.join('\n')

const formatted = [formattedHeader, formattedCode, formattedSuggestions, formattedReference]
.filter(Boolean)
.join('\n')
let output = `${isDeep ? 'Caused by: ' : ''}${formatted}`
output = wrap(output, errtermwidth - 6, {hard: true, trim: false} as any)
if (!settings.debug) {
output = indent(output, 3)
output = indent(output, 1, {includeEmptyLines: true, indent: bang || ''} as any)
output = indent(output, 1)
}

let output = wrap(formatted, errtermwidth - 6, {hard: true, trim: false} as any)
output = indent(output, 3)
output = indent(output, 1, {includeEmptyLines: true, indent: bang || ''} as any)
output = indent(output, 1)
prettyPrintedErrors.push(output)
}

isDeep = true
currentError = currentError.cause ?? null
}

return output
return prettyPrintedErrors.join('\n')
}
61 changes: 60 additions & 1 deletion test/errors/pretty-print.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import prettyPrint from '../../src/errors/errors/pretty-print'
import {PrettyPrintableError} from '../../src/interfaces/errors'

describe('pretty-print', () => {
it('pretty prints an error', async () => {
it('pretty prints a simple error', async () => {
const sampleError: Error & PrettyPrintableError = new Error('Something very serious has gone wrong with the flags!')
sampleError.ref = 'https://oclif.io/docs/flags'
sampleError.code = 'OCLIF_BAD_FLAG'
Expand All @@ -20,6 +20,23 @@ describe('pretty-print', () => {
Reference: https://oclif.io/docs/flags`)
})

it('pretty prints an error and its causative error chain', async () => {
const rootError: Error & PrettyPrintableError = new Error('This error is the root error')
const errorOnceRemoved: Error & PrettyPrintableError = new Error(
'This error is one level removed from the root error',
{cause: rootError},
)
const errorTwiceRemoved: Error & PrettyPrintableError = new Error(
'This error is two levels removed from the root error',
{cause: errorOnceRemoved},
)

expect(ansis.strip(prettyPrint(errorTwiceRemoved) ?? '')).to
.equal(` Error: This error is two levels removed from the root error
Caused by: Error: This error is one level removed from the root error
Caused by: Error: This error is the root error`)
})

it('pretty prints multiple suggestions', async () => {
const sampleError: Error & PrettyPrintableError = new Error('Something very serious has gone wrong with the flags!')
sampleError.suggestions = ['Use a good flag', 'Use no flags']
Expand Down Expand Up @@ -75,5 +92,47 @@ describe('pretty-print', () => {
error.stack = 'this is the error stack property'
expect(prettyPrint(error)).to.equal('this is the error stack property')
})

it('shows the stack for causative errors', async () => {
const rootError: Error & PrettyPrintableError = new Error('This error is the root error')
rootError.stack = String.raw`Root error stack. In the wild, this would be something like 'Error: someMessage\n\tcodeStack'`
const errorOnceRemoved: Error & PrettyPrintableError = new Error(
'This error is one level removed from the root error',
{cause: rootError},
)
errorOnceRemoved.stack = String.raw`Once-removed error stack. In the wild, this would be something like 'Error: someMessage\n\tcodeStack'`
const errorTwiceRemoved: Error & PrettyPrintableError = new Error(
'This error is two levels removed from the root error',
{cause: errorOnceRemoved},
)
errorTwiceRemoved.stack = String.raw`Twice-removed error stack. In the wild, this would be something like 'Error: someMessage\n\tcodeStack'`

expect(prettyPrint(errorTwiceRemoved)).to.equal(
"Twice-removed error stack. In the wild, this would be something like 'Error: someMessage\\n\\tcodeStack'\n" +
"Caused by: Once-removed error stack. In the wild, this would be something like 'Error: someMessage\\n\\tcodeStack'\n" +
String.raw`Caused by: Root error stack. In the wild, this would be something like 'Error: someMessage\n\tcodeStack'`,
)
})

it('when no stack is available, standard pretty print is used instead', () => {
const rootError: Error & PrettyPrintableError = new Error('This error is the root error')
rootError.stack = String.raw`Root error stack. In the wild, this would be something like 'Error: someMessage\n\tcodeStack'`
const errorOnceRemoved: Error & PrettyPrintableError = new Error(
'This error is one level removed from the root error',
{cause: rootError},
)
delete errorOnceRemoved.stack
const errorTwiceRemoved: Error & PrettyPrintableError = new Error(
'This error is two levels removed from the root error',
{cause: errorOnceRemoved},
)
errorTwiceRemoved.stack = String.raw`Twice-removed error stack. In the wild, this would be something like 'Error: someMessage\n\tcodeStack'`

expect(prettyPrint(errorTwiceRemoved)).to.equal(
"Twice-removed error stack. In the wild, this would be something like 'Error: someMessage\\n\\tcodeStack'\n" +
'Caused by: Error: This error is one level removed from the root error\n' +
String.raw`Caused by: Root error stack. In the wild, this would be something like 'Error: someMessage\n\tcodeStack'`,
)
})
})
})
Loading