Skip to content

Commit

Permalink
Add support for ancestors, cause fields
Browse files Browse the repository at this point in the history
  • Loading branch information
wooorm committed Jun 12, 2023
1 parent 026820e commit 3c7ae2a
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 33 deletions.
164 changes: 131 additions & 33 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
* [color is supported][supports-color], or `false`).
*
* [supports-color]: https://github.com/chalk/supports-color
* @property {string | null | undefined} [defaultName='<stdin>']
* Label to use for files without file path (default: `'<stdin>'`); if one
* file and no `defaultName` is given, no name will show up in the report.
* @property {boolean | null | undefined} [verbose=false]
* Show message [`note`][message-note]s (default: `false`); notes are
* optional, additional, long descriptions.
Expand All @@ -22,17 +25,15 @@
* @property {boolean | null | undefined} [silent=false]
* Show errors only (default: `false`); this hides info and warning messages,
* and sets `quiet: true`.
* @property {string | null | undefined} [defaultName='<stdin>']
* Label to use for files without file path (default: `'<stdin>'`); if one
* file and no `defaultName` is given, no name will show up in the report.
* @property {number | null | undefined} [traceLimit=10]
* Max number of nodes to show in ancestors trace (default: `10`).
*/

/**
* @typedef State
* Info passed around.
* @property {boolean} colorEnabled
* Whether color is enabled; can be turned on explicitly or implicitly in
* Node.js based on whether stderr supports color.
* @property {string | undefined} defaultName
* Default name to use.
* @property {boolean} oneFileMode
* Whether explicitly a single file is passed.
* @property {boolean} verbose
Expand All @@ -41,8 +42,24 @@
* Whether to hide files without messages.
* @property {boolean} silent
* Whether to hide warnings and info messages.
* @property {string | undefined} defaultName
* Default name to use.
* @property {number} traceLimit
* Max number of nodes to show in ancestors trace.
* @property {string} bold
* Bold style.
* @property {string} underline
* Underline style.
* @property {string} normalIntensity
* Regular style.
* @property {string} noUnderline
* Regular style.
* @property {string} red
* Color.
* @property {string} green
* Color.
* @property {string} yellow
* Color.
* @property {string} defaultColor
* Regular color.
*/

import stringWidth from 'string-width'
Expand Down Expand Up @@ -76,6 +93,8 @@ export function reporter(files, options) {
}

const settings = options || {}
const colorEnabled =
typeof settings.color === 'boolean' ? settings.color : color
let oneFileMode = false

if (Array.isArray(files)) {
Expand All @@ -88,13 +107,21 @@ export function reporter(files, options) {
return serializeRows(
createRows(
{
colorEnabled:
typeof settings.color === 'boolean' ? settings.color : color,
defaultName: settings.defaultName || undefined,
oneFileMode,
quiet: settings.quiet || false,
silent: settings.silent || false,
verbose: settings.verbose || false
traceLimit:
typeof settings.traceLimit === 'number' ? settings.traceLimit : 10,
verbose: settings.verbose || false,
bold: colorEnabled ? '\u001B[1m' : '',
underline: colorEnabled ? '\u001B[4m' : '',
normalIntensity: colorEnabled ? '\u001B[22m' : '',
noUnderline: colorEnabled ? '\u001B[24m' : '',
red: colorEnabled ? '\u001B[31m' : '',
green: colorEnabled ? '\u001B[32m' : '',
yellow: colorEnabled ? '\u001B[33m' : '',
defaultColor: colorEnabled ? '\u001B[39m' : ''
},
files
)
Expand Down Expand Up @@ -240,21 +267,96 @@ function createMessageLine(state, message) {
const row = [
'',
stringifyPosition(message.place),
state.colorEnabled
? (label === 'error'
? '\u001B[31m' /* Red. */
: '\u001B[33m') /* Yellow. */ +
label +
'\u001B[39m'
: label,
(label === 'error' ? state.red : state.yellow) + label + state.defaultColor,
reason,
message.ruleId || '',
message.source || ''
]

if (message.cause) {
rest.push(...createCauseLines(state, message.cause))
}

if (message.ancestors) {
rest.push(...createAncestorsLines(state, message.ancestors))
}

return [row, ...rest]
}

/**
* Create lines for cause.
*
* @param {State} state
* Info passed around.
* @param {NonNullable<VFileMessage['cause']>} cause
* Cause.
* @returns {Array<string>}
* Lines.
*/
function createCauseLines(state, cause) {
const lines = [' ' + state.bold + '[cause]' + state.normalIntensity + ':']
/* c8 ignore next -- stacks can be missing for weird reasons or in weird places. */
const stackLines = (cause.stack || cause.message).split(eol)
stackLines[0] = ' ' + stackLines[0]
lines.push(...stackLines)

return lines
}

/**
* Create lines for ancestors.
*
* @param {State} state
* Info passed around.
* @param {NonNullable<VFileMessage['ancestors']>} ancestors
* Ancestors.
* @returns {Array<string>}
* Lines.
*/
function createAncestorsLines(state, ancestors) {
const min =
ancestors.length > state.traceLimit
? ancestors.length - state.traceLimit
: 0
let index = ancestors.length

/** @type {Array<string>} */
const lines = []

if (index > min) {
lines.unshift(' ' + state.bold + '[trace]' + state.normalIntensity + ':')
}

while (index-- > min) {
const node = ancestors[index]
/** @type {Record<string, unknown>} */
// @ts-expect-error: TypeScript is wrong: objects can be indexed.
const value = node
const name =
// `hast`
typeof value.tagName === 'string'
? value.tagName
: // `xast` (and MDX JSX elements)
typeof value.name === 'string'
? value.name
: undefined

const position = stringifyPosition(node.position)

lines.push(
' at ' +
state.yellow +
node.type +
(name ? '<' + name + '>' : '') +
state.defaultColor +
(position ? ' (' + position + ')' : '')
)
}

return lines
}

/**
* Create a summary of problems for a file.
*
Expand All @@ -276,24 +378,18 @@ function createFileLine(state, file) {
const name = fromPath || state.defaultName || '<stdin>'

left =
(state.colorEnabled
? '\u001B[4m' /* Underline. */ +
(stats.fatal
? '\u001B[31m' /* Red. */
: stats.total
? '\u001B[33m' /* Yellow. */
: '\u001B[32m') /* Green. */ +
name +
'\u001B[39m\u001B[24m'
: name) + (file.stored && name !== toPath ? ' > ' + toPath : '')
state.underline +
(stats.fatal ? state.red : stats.total ? state.yellow : state.green) +
name +
state.defaultColor +
state.noUnderline +
(file.stored && name !== toPath ? ' > ' + toPath : '')
}

// To do: always expose `written` if stored?
if (!stats.total) {
right += file.stored
? state.colorEnabled
? '\u001B[33mwritten\u001B[39m' /* Yellow. */
: 'written'
? state.yellow + 'written' + state.defaultColor
: 'no issues found'
}

Expand All @@ -315,7 +411,9 @@ function createByline(state, stats) {

if (stats.fatal) {
result =
(state.colorEnabled ? /* Red. */ '\u001B[31m✖\u001B[39m' : '✖') +
state.red +
'✖' +
state.defaultColor +
' ' +
stats.fatal +
' ' +
Expand All @@ -325,7 +423,7 @@ function createByline(state, stats) {
if (stats.warn) {
result =
(result ? result + ', ' : '') +
(state.colorEnabled ? /* Yellow. */ '\u001B[33m⚠\u001B[39m' : '⚠') +
(state.yellow + '⚠' + state.defaultColor) +
' ' +
stats.warn +
' ' +
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@
"vfile-statistics": "^3.0.0"
},
"devDependencies": {
"@types/hast": "^2.0.0",
"@types/node": "^20.0.0",
"c8": "^7.0.0",
"cross-env": "^7.0.0",
"mdast-util-mdx-jsx": "^2.0.0",
"prettier": "^2.0.0",
"remark-cli": "^11.0.0",
"remark-preset-wooorm": "^9.0.0",
Expand Down
86 changes: 86 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* @typedef {import('hast').Element} Element
* @typedef {import('hast').Root} Root
* @typedef {import('hast').Text} Text
* @typedef {import('mdast-util-mdx-jsx').MdxJsxTextElementHast} MdxJsxTextElementHast
*
*/

import assert from 'node:assert/strict'
import test from 'node:test'
import strip from 'strip-ansi'
Expand Down Expand Up @@ -399,6 +407,84 @@ test('reporter', async function () {
'<stdin>: no issues found',
'should use `<stdin>` for files w/o path if multiple are given'
)

assert.equal(
strip(reporter([new VFile()])),
'<stdin>: no issues found',
'should use `<stdin>` for files w/o path if multiple are given'
)

file = new VFile()
file.message('Something failed terribly', {cause: exception})

assert.equal(
strip(reporter(file)),
[
' warning Something failed terribly',
' [cause]:',
' ReferenceError: variable is not defined',
' at test.js:1:1',
' at ModuleJob.run (module_job:1:1)',
'',
'⚠ 1 warning'
].join('\n'),
'should support a `message.cause`'
)

/** @type {Text} */
const text = {type: 'text', value: 'a'}
/** @type {MdxJsxTextElementHast} */
const jsx = {
type: 'mdxJsxTextElement',
name: 'b',
attributes: [],
children: [text]
}
/** @type {Element} */
const element = {
type: 'element',
tagName: 'p',
properties: {},
children: [jsx],
position: {start: {line: 1, column: 1}, end: {line: 1, column: 9}}
}
/** @type {Root} */
const root = {
type: 'root',
children: [element],
position: {start: {line: 1, column: 1}, end: {line: 1, column: 9}}
}

file = new VFile()
file.message('x', {ancestors: [root, element, jsx, text]})

assert.equal(
strip(reporter(file)),
[
' warning x',
' [trace]:',
' at text',
' at mdxJsxTextElement<b>',
' at element<p> (1:1-1:9)',
' at root (1:1-1:9)',
'',
'⚠ 1 warning'
].join('\n'),
'should support `message.ancestors`'
)

assert.equal(
strip(reporter(file, {traceLimit: 2})),
[
' warning x',
' [trace]:',
' at text',
' at mdxJsxTextElement<b>',
'',
'⚠ 1 warning'
].join('\n'),
'should support `options.traceLimit`'
)
})

/**
Expand Down

0 comments on commit 3c7ae2a

Please sign in to comment.