Skip to content
Open
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
6 changes: 5 additions & 1 deletion doc/api/repl.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ The following special commands are supported by all REPL instances:
* `.clear`: Resets the REPL `context` to an empty object and clears any
multi-line expression being input.
* `.exit`: Close the I/O stream, causing the REPL to exit.
* `.help`: Show this list of special commands.
* `.help`: Show this list of special commands. When followed by an
expression, `.help expression` evaluates the expression and prints
introspection information about the resulting value instead: its type,
function signature, properties, methods, and prototype chain.
`> .help process.versions`
* `.save`: Save the current REPL session to a file:
`> .save ./file/to/save.js`
* `.load`: Load a file into the current REPL session.
Expand Down
172 changes: 172 additions & 0 deletions lib/internal/repl/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
const {
ArrayPrototypeFilter,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeSort,
Boolean,
FunctionPrototypeBind,
FunctionPrototypeToString,
MathMin,
ObjectGetOwnPropertyDescriptor,
ObjectGetOwnPropertyNames,
ObjectGetPrototypeOf,
RegExpPrototypeExec,
SafeSet,
SafeStringIterator,
Expand Down Expand Up @@ -841,6 +848,170 @@ function setReplBuiltinLibs(value) {
_builtinLibs = value;
}

/**
* Prints introspection info for a value evaluated via `.help expression`:
* type, function signature, properties, methods, and prototype chain.
*
* Uses `util.styleText` to dogfood core coloring. Output goes to
* `repl.output` so it respects the terminal's color capabilities.
* @param {object} repl The REPLServer instance.
* @param {any} value The value to introspect.
*/
function printValueHelp(repl, value) {
const { styleText } = require('util');
const kStyleOpts = { __proto__: null, validateStream: false };

function style(color, text) {
if (!repl.useColors) return `${text}`;
return styleText(color, `${text}`, kStyleOpts);
}

const lines = [];

const type = value === null ? 'null' : typeof value;

// Header: type and constructor info.
if (type === 'function') {
const fnStr = FunctionPrototypeToString(value);
const isAsync = RegExpPrototypeExec(/^async\s/, fnStr) !== null;
const isGenerator = RegExpPrototypeExec(/^(async\s+)?function\s*\*/, fnStr) !== null;
const isClass = RegExpPrototypeExec(/^class[\s{]/, fnStr) !== null;

let kind = 'Function';
if (isClass) kind = 'Class';
else if (isAsync && isGenerator) kind = 'AsyncGeneratorFunction';
else if (isAsync) kind = 'AsyncFunction';
else if (isGenerator) kind = 'GeneratorFunction';

ArrayPrototypePush(lines,
`${style('yellow', kind)}: ${style('bold', value.name || '(anonymous)')}`);

// Show parameter info from function source.
const match = RegExpPrototypeExec(
/(?:^(?:async\s+)?(?:function\s*\*?\s*)?(\w*)\s*\(([^)]*)\)|^(\w*)\s*=>\s*)/,
fnStr,
);
if (match) {
const params = (match[2] || '').trim();
if (params) {
ArrayPrototypePush(lines,
` ${style('gray', 'Parameters:')} ${style('cyan', params)}`);
}
}

ArrayPrototypePush(lines,
` ${style('gray', 'Length:')} ${value.length}`);

if (value.prototype !== undefined) {
const protoMethods = [];
try {
const protoNames = ObjectGetOwnPropertyNames(value.prototype);
for (let i = 0; i < protoNames.length; i++) {
const pn = protoNames[i];
if (pn === 'constructor') continue;
const desc = ObjectGetOwnPropertyDescriptor(value.prototype, pn);
if (desc && typeof desc.value === 'function') {
ArrayPrototypePush(protoMethods, pn);
}
}
} catch {
// Some prototypes throw on access; skip.
}
if (protoMethods.length > 0) {
ArrayPrototypePush(lines,
` ${style('gray', 'Prototype methods:')}`,
` ${style('magenta', ArrayPrototypeJoin(ArrayPrototypeSort(protoMethods), ', '))}`);
}
}
} else if (type === 'object' || type === 'null' ||
type === 'undefined') {
if (value === null) {
ArrayPrototypePush(lines, style('yellow', 'null'));
} else if (value === undefined) {
ArrayPrototypePush(lines, style('yellow', 'undefined'));
} else {
const ctor = value.constructor?.name || 'Object';
ArrayPrototypePush(lines,
`${style('yellow', ctor)} {`);

// Categorize own properties.
const methods = [];
const gettersSetters = [];
const properties = [];

try {
const ownNames = ObjectGetOwnPropertyNames(value);
for (let i = 0; i < ownNames.length; i++) {
const name = ownNames[i];
const desc = ObjectGetOwnPropertyDescriptor(value, name);
if (!desc) continue;
if (desc.get || desc.set) {
const accessors = [];
if (desc.get) ArrayPrototypePush(accessors, 'get');
if (desc.set) ArrayPrototypePush(accessors, 'set');
ArrayPrototypePush(gettersSetters,
` ${style('green', name)} [${ArrayPrototypeJoin(accessors, '/')}]`);
} else if (typeof desc.value === 'function') {
ArrayPrototypePush(methods,
` ${style('magenta', name)}()`);
} else {
const valStr = inspect(desc.value, {
depth: 0,
colors: repl.useColors,
maxStringLength: 40,
});
ArrayPrototypePush(properties,
` ${style('cyan', name)}: ${valStr}`);
}
}
} catch {
ArrayPrototypePush(lines,
` ${style('gray', '(properties not enumerable)')}`);
}

if (methods.length > 0) {
ArrayPrototypePush(lines, style('gray', ' Methods:'));
for (let i = 0; i < methods.length; i++) {
ArrayPrototypePush(lines, methods[i]);
}
}
if (gettersSetters.length > 0) {
ArrayPrototypePush(lines, style('gray', ' Accessors:'));
for (let i = 0; i < gettersSetters.length; i++) {
ArrayPrototypePush(lines, gettersSetters[i]);
}
}
if (properties.length > 0) {
ArrayPrototypePush(lines, style('gray', ' Properties:'));
for (let i = 0; i < properties.length; i++) {
ArrayPrototypePush(lines, properties[i]);
}
}

ArrayPrototypePush(lines, '}');

// Show prototype chain.
const chain = [];
let proto = ObjectGetPrototypeOf(value);
while (proto && proto !== ObjectGetPrototypeOf({})) {
const protoName = proto.constructor?.name || '(anonymous)';
ArrayPrototypePush(chain, protoName);
proto = ObjectGetPrototypeOf(proto);
}
if (chain.length > 0) {
ArrayPrototypePush(lines,
`${style('gray', 'Prototype chain:')} ${ArrayPrototypeJoin(chain, ' -> ')}`);
}
}
} else {
// Primitive: string, number, boolean, bigint, symbol.
ArrayPrototypePush(lines,
`${style('yellow', type)}: ${inspect(value, { colors: repl.useColors })}`);
}

repl.output.write(ArrayPrototypeJoin(lines, '\n') + '\n');
}

module.exports = {
REPL_MODE_SLOPPY: Symbol('repl-sloppy'),
REPL_MODE_STRICT,
Expand All @@ -856,4 +1027,5 @@ module.exports = {
getReplBuiltinLibs,
setReplBuiltinLibs,
fixReplRequire,
printValueHelp,
};
19 changes: 17 additions & 2 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ const {
getReplBuiltinLibs,
setReplBuiltinLibs,
fixReplRequire,
printValueHelp,
} = require('internal/repl/utils');
const {
complete,
Expand Down Expand Up @@ -1378,8 +1379,22 @@ function defineDefaultCommands(repl) {
});

repl.defineCommand('help', {
help: 'Print this help message',
action: function() {
help: 'Print this help message, or inspect a value: .help expression',
action: function(expr) {
expr = StringPrototypeTrim(expr || '');
if (expr !== '') {
this.eval(`${expr}\n`, this.context, getREPLResourceName(),
(err, value) => {
if (err) {
this.output.write(`Failed to evaluate: '${expr}'. ` +
`${err.message || err}\n`);
} else {
printValueHelp(this, value);
}
this.displayPrompt();
});
return;
}
const names = ArrayPrototypeSort(ObjectKeys(this.commands));
const longestNameLength = MathMaxApply(
ArrayPrototypeMap(names, (name) => name.length),
Expand Down
Loading
Loading