Skip to content

Commit

Permalink
util: mark classes while inspecting them
Browse files Browse the repository at this point in the history
This outlines the basic class setup when inspecting a class.

Signed-off-by: Ruben Bridgewater <ruben@bridgewater.de>
  • Loading branch information
BridgeAR committed May 8, 2020
1 parent e454e9b commit 3e7470e
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 1 deletion.
47 changes: 47 additions & 0 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
DatePrototypeToString,
ErrorPrototypeToString,
Float32Array,
FunctionPrototypeToString,
JSONStringify,
Map,
MapPrototype,
Expand Down Expand Up @@ -170,6 +171,10 @@ const numberRegExp = /^(0|[1-9][0-9]*)$/;
const coreModuleRegExp = /^ at (?:[^/\\(]+ \(|)((?<![/\\]).+)\.js:\d+:\d+\)?$/;
const nodeModulesRegExp = /[/\\]node_modules[/\\](.+?)(?=[/\\])/g;

const classRegExp = /^(class\s+[^(]*?)\s*{/;
// eslint-disable-next-line node-core/no-unescaped-regexp-dot
const stripCommentsRegExp = /(\/\/.*?\n)|(\/\*(.|\n)*?\*\/)/g;

const kMinLineLength = 16;

// Constants to map the iterator state.
Expand Down Expand Up @@ -1060,7 +1065,49 @@ function getBoxedBase(value, ctx, keys, constructor, tag) {
return ctx.stylize(base, type.toLowerCase());
}

function getClassBase(string, value, constructor, tag) {
const parts = string.split(/\s+/);
if (parts[1] === 'extends') {
parts[3] = parts[2];
parts[1] = '';
}
const [, name, , superClass] = parts;
let base = `class ${name}`;
if (name !== value.name) {
if (name === '') {
base += value.name;
} else if (value.name) {
base += ` [${value.name}]`;
}
} else if (name === '') {
base += '(anonymous)';
}
if (constructor !== 'Function') {
if (constructor === null) {
base += ' [null prototype]';
} else {
base += ` [${constructor}]`;
}
}
if (tag !== '' && constructor !== tag) {
base += ` [${tag}]`;
}
if (superClass !== undefined) {
base += ` extends ${superClass}`;
}
return `[${base}]`;
}

function getFunctionBase(value, constructor, tag) {
const stringified = FunctionPrototypeToString(value);
if (stringified.slice(0, 5) === 'class') {
const match = stringified
.replace(stripCommentsRegExp, ' ')
.match(classRegExp);
if (match !== null) {
return getClassBase(match[1], value, constructor, tag);
}
}
let type = 'Function';
if (isGeneratorFunction(value)) {
type = `Generator${type}`;
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-repl-top-level-await.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ async function ordinaryTests() {
['await 0; function foo() {}'],
['foo', '[Function: foo]'],
['class Foo {}; await 1;', '1'],
['Foo', '[Function: Foo]'],
['Foo', '[class Foo]'],
['if (await true) { function bar() {}; }'],
['bar', '[Function: bar]'],
['if (await true) { class Bar {}; }'],
Expand Down
83 changes: 83 additions & 0 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -1973,6 +1973,89 @@ assert.strictEqual(util.inspect('"\'${a}'), "'\"\\'${a}'");
);
});

// Verify that classes are properly inspected.
[
/* eslint-disable spaced-comment, no-multi-spaces, brace-style */
// The whitespace is intentional.
[class { }, '[class (anonymous)]'],
[class extends Error { log() {} }, '[class (anonymous) extends Error]'],
[class A { constructor(a) { this.a = a; } log() { return this.a; } },
'[class A]'],
[class
// Random { // comments /* */ are part of the toString() result
/* eslint-disable-next-line space-before-blocks */
äß/**/extends/*{*/TypeError{}, '[class äß extends TypeError]'],
/* The whitespace and new line is intended! */
// Foobar !!!
[class X extends /****/ Error
// More comments
{}, '[class X extends Error]']
/* eslint-enable spaced-comment, no-multi-spaces, brace-style */
].forEach(([clazz, string]) => {
const inspected = util.inspect(clazz);
assert.strictEqual(inspected, string);
Object.defineProperty(clazz, Symbol.toStringTag, {
value: 'Woohoo'
});
const parts = inspected.slice(0, -1).split(' ');
const [, name, ...rest] = parts;
rest.unshift('[Woohoo]');
if (rest.length) {
rest[rest.length - 1] += ']';
}
assert.strictEqual(
util.inspect(clazz),
['[class', name, ...rest].join(' ')
);
Object.setPrototypeOf(clazz, null);
assert.strictEqual(
util.inspect(clazz),
['[class', name, '[null prototype]', ...rest].join(' ')
);
Object.defineProperty(clazz, 'name', { value: 'Foo' });
const newName = name === '(anonymous)' ? 'Foo' : `${name} [Foo]`;
assert.strictEqual(
util.inspect(clazz),
['[class', newName, '[null prototype]', ...rest].join(' ')
);
Object.setPrototypeOf(clazz, Number.prototype);
assert.strictEqual(
util.inspect(clazz),
['[class', newName, '[Number]', ...rest].join(' ')
);
clazz.foo = true;
assert.strictEqual(
util.inspect(clazz),
['[class', newName, '[Number]', ...rest, '{ foo: true }'].join(' ')
);
});

// "class" properties should not be detected as "class".
{
// eslint-disable-next-line space-before-function-paren
let obj = { class () {} };
assert.strictEqual(
util.inspect(obj),
'{ class: [Function: class] }'
);
obj = { class: () => {} };
assert.strictEqual(
util.inspect(obj),
'{ class: [Function: class] }'
);
obj = { ['class Foo {}']() {} };
assert.strictEqual(
util.inspect(obj),
"{ 'class Foo {}': [Function: class Foo {}] }"
);
function Foo() {}
Object.defineProperty(Foo, 'toString', { value: () => 'class Foo {}' });
assert.strictEqual(
util.inspect(Foo),
'[Function: Foo]'
);
}

// Verify that throwing in valueOf and toString still produces nice results.
[
[new String(55), "[String: '55']"],
Expand Down

0 comments on commit 3e7470e

Please sign in to comment.