Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: highlight stack frames #27052

Closed
wants to merge 4 commits into from
Closed
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
24 changes: 14 additions & 10 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -633,22 +633,25 @@ via the `util.inspect.styles` and `util.inspect.colors` properties.

The default styles and associated colors are:

* `number` - `yellow`
* `boolean` - `yellow`
* `string` - `green`
* `date` - `magenta`
* `regexp` - `red`
* `null` - `bold`
* `undefined` - `grey`
* `special` - `cyan` (only applied to functions at this time)
* `name` - (no styling)
* `bigint` - `yellow`
* `boolean` - `yellow`
* `date` - `magenta`
* `module` - `underline`
* `name` - (no styling)
* `null` - `bold`
* `number` - `yellow`
* `regexp` - `red`
* `special` - `cyan` (e.g., `Proxies`)
* `string` - `green`
* `symbol` - `green`
* `undefined` - `grey`

The predefined color codes are: `white`, `grey`, `black`, `blue`, `cyan`,
`green`, `magenta`, `red` and `yellow`. There are also `bold`, `italic`,
`underline` and `inverse` codes.

Color styling uses ANSI control codes that may not be supported on all
terminals.
terminals. To verify color support use [`tty.hasColors()`][].
BridgeAR marked this conversation as resolved.
Show resolved Hide resolved

### Custom inspection functions on Objects

Expand Down Expand Up @@ -2185,6 +2188,7 @@ util.log('Timestamped message.');
[`assert.deepStrictEqual()`]: assert.html#assert_assert_deepstrictequal_actual_expected_message
[`console.error()`]: console.html#console_console_error_data_args
[`target` and `handler`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Terminology
[`tty.hasColors()`]: tty.html#tty_writestream_hascolors_count_env
[`util.format()`]: #util_util_format_format_args
[`util.inspect()`]: #util_util_inspect_object_options
[`util.promisify()`]: #util_util_promisify_original
Expand Down
50 changes: 43 additions & 7 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ const {

const assert = require('internal/assert');

const { NativeModule } = require('internal/bootstrap/loaders');

let hexSlice;

const inspectDefaultOptions = Object.seal({
Expand Down Expand Up @@ -115,6 +117,9 @@ const strEscapeSequencesReplacerSingle = /[\x00-\x1f\x5c]/g;
const keyStrRegExp = /^[a-zA-Z_][a-zA-Z_0-9]*$/;
const numberRegExp = /^(0|[1-9][0-9]*)$/;

const coreModuleRegExp = /^ at (?:[^/\\(]+ \(|)((?<![/\\]).+)\.js:\d+:\d+\)?$/;
const nodeModulesRegExp = /[/\\]node_modules[/\\](.+?)(?=[/\\])/g;

const readableRegExps = {};

const kMinLineLength = 16;
Expand Down Expand Up @@ -253,7 +258,8 @@ inspect.styles = Object.assign(Object.create(null), {
symbol: 'green',
date: 'magenta',
// "name": intentionally not styling
regexp: 'red'
regexp: 'red',
module: 'underline'
});

function addQuotes(str, quotes) {
Expand Down Expand Up @@ -866,10 +872,37 @@ function formatError(err, constructor, tag, ctx) {
}
}
}
// Ignore the error message if it's contained in the stack.
let pos = err.message && stack.indexOf(err.message) || -1;
if (pos !== -1)
pos += err.message.length;
// Wrap the error in brackets in case it has no stack trace.
const stackStart = stack.indexOf('\n at');
const stackStart = stack.indexOf('\n at', pos);
if (stackStart === -1) {
stack = `[${stack}]`;
} else if (ctx.colors) {
// Highlight userland code and node modules.
let newStack = stack.slice(0, stackStart);
const lines = stack.slice(stackStart + 1).split('\n');
for (const line of lines) {
const core = line.match(coreModuleRegExp);
if (core !== null && NativeModule.exists(core[1])) {
newStack += `\n${ctx.stylize(line, 'undefined')}`;
} else {
// This adds underscores to all node_modules to quickly identify them.
let nodeModule;
newStack += '\n';
let pos = 0;
while (nodeModule = nodeModulesRegExp.exec(line)) {
// '/node_modules/'.length === 14
newStack += line.slice(pos, nodeModule.index + 14);
newStack += ctx.stylize(nodeModule[1], 'module');
pos = nodeModule.index + nodeModule[0].length;
}
newStack += pos === 0 ? line : line.slice(pos);
}
}
stack = newStack;
}
// The message and the stack have to be indented as well!
if (ctx.indentationLvl !== 0) {
Expand Down Expand Up @@ -1194,9 +1227,10 @@ function formatSetIterInner(ctx, recurseTimes, entries, state) {
output[i] = formatValue(ctx, entries[i], recurseTimes);
}
ctx.indentationLvl -= 2;
if (state === kWeak) {
if (state === kWeak && !ctx.sorted) {
// Sort all entries to have a halfway reliable output (if more entries than
// retrieved ones exist, we can not reliably return the same output).
// retrieved ones exist, we can not reliably return the same output) if the
// output is not sorted anyway.
output = output.sort();
}
const remaining = entries.length - maxLength;
Expand All @@ -1221,9 +1255,11 @@ function formatMapIterInner(ctx, recurseTimes, entries, state) {
output[i] = `${formatValue(ctx, entries[pos], recurseTimes)}` +
` => ${formatValue(ctx, entries[pos + 1], recurseTimes)}`;
}
// Sort all entries to have a halfway reliable output (if more entries
// than retrieved ones exist, we can not reliably return the same output).
output = output.sort();
// Sort all entries to have a halfway reliable output (if more entries than
// retrieved ones exist, we can not reliably return the same output) if the
// output is not sorted anyway.
if (!ctx.sorted)
output = output.sort();
} else {
for (; i < maxLength; i++) {
const pos = i * 2;
Expand Down
2 changes: 0 additions & 2 deletions test/fixtures/node_modules/node_modules/bar.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -2306,3 +2306,43 @@ assert.strictEqual(

assert.strictEqual(out, expected);
}

{
// Use a fake stack to verify the expected colored outcome.
const stack = [
'TypedError: Wonderful message!',
' at A.<anonymous> (/test/node_modules/foo/node_modules/bar/baz.js:2:7)',
' at Module._compile (internal/modules/cjs/loader.js:827:30)',
' at Fancy (vm.js:697:32)',
// This file is not an actual Node.js core file.
' at tryModuleLoad (internal/modules/cjs/foo.js:629:12)',
' at Function.Module._load (internal/modules/cjs/loader.js:621:3)',
// This file is not an actual Node.js core file.
' at Module.require [as weird/name] (internal/aaaaaa/loader.js:735:19)',
' at require (internal/modules/cjs/helpers.js:14:16)',
' at /test/test-util-inspect.js:2239:9',
' at getActual (assert.js:592:5)'
];
const isNodeCoreFile = [
false, false, true, true, false, true, false, true, false, true
];
const err = new TypeError('Wonderful message!');
err.stack = stack.join('\n');
util.inspect(err, { colors: true }).split('\n').forEach((line, i) => {
let actual = stack[i].replace(/node_modules\/([a-z]+)/g, (a, m) => {
return `node_modules/\u001b[4m${m}\u001b[24m`;
});
if (isNodeCoreFile[i]) {
actual = `\u001b[90m${actual}\u001b[39m`;
}
assert.strictEqual(actual, line);
});
}

{
// Cross platform checks.
const err = new Error('foo');
util.inspect(err, { colors: true }).split('\n').forEach((line, i) => {
assert(i < 2 || line.startsWith('\u001b[90m'));
});
}
13 changes: 13 additions & 0 deletions test/pseudo-tty/console_colors.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
'use strict';
require('../common');
const vm = require('vm');
// Make this test OS-independent by overriding stdio getColorDepth().
process.stdout.getColorDepth = () => 8;
process.stderr.getColorDepth = () => 8;

console.log({ foo: 'bar' });
console.log('%s q', 'string');
console.log('%o with object format param', { foo: 'bar' });

console.log(
new Error('test\n at abc (../fixtures/node_modules/bar.js:4:4)\nfoobar')
);

try {
require('../fixtures/node_modules/node_modules/bar.js');
} catch (err) {
console.log(err);
}

vm.runInThisContext('console.log(new Error())');
35 changes: 35 additions & 0 deletions test/pseudo-tty/console_colors.out
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
{ foo: *[32m'bar'*[39m }
string q
{ foo: *[32m'bar'*[39m } with object format param

Error: test
at abc (../fixtures/node_modules/bar.js:4:4)
foobar
at * (*console_colors.js:*:*)
*[90m at * (internal*:*:*)*[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m

Error: Should not ever get here.
at * (*node_modules*[4m*node_modules*[24m*bar.js:*:*)
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
at * (*console_colors.js:*:*)
*[90m at *[39m
*[90m at *[39m

Error
at evalmachine.<anonymous>:*:*
*[90m at Script.runInThisContext (vm.js:*:*)*[39m
*[90m at Object.runInThisContext (vm.js:*:*)*[39m
at * (*console_colors.js:*:*)
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m
*[90m at *[39m