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>

PR-URL: #32332
Fixes: #32270
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Anto Aravinth <anto.aravinth.cse@gmail.com>
  • Loading branch information
BridgeAR authored and codebytere committed Jun 18, 2020
1 parent e62a8b5 commit 2380d90
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 1 deletion.
37 changes: 37 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 = /^(\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,39 @@ function getBoxedBase(value, ctx, keys, constructor, tag) {
return ctx.stylize(base, type.toLowerCase());
}

function getClassBase(value, constructor, tag) {
const hasName = ObjectPrototypeHasOwnProperty(value, 'name');
const name = (hasName && value.name) || '(anonymous)';
let base = `class ${name}`;
if (constructor !== 'Function' && constructor !== null) {
base += ` [${constructor}]`;
}
if (tag !== '' && constructor !== tag) {
base += ` [${tag}]`;
}
if (constructor !== null) {
const superName = ObjectGetPrototypeOf(value).name;
if (superName) {
base += ` extends ${superName}`;
}
} else {
base += ' extends [null prototype]';
}
return `[${base}]`;
}

function getFunctionBase(value, constructor, tag) {
const stringified = FunctionPrototypeToString(value);
if (stringified.slice(0, 5) === 'class' && stringified.endsWith('}')) {
const slice = stringified.slice(5, -1);
const bracketIndex = slice.indexOf('{');
if (bracketIndex !== -1 &&
(!slice.slice(0, bracketIndex).includes('(') ||
// Slow path to guarantee that it's indeed a class.
classRegExp.test(slice.replace(stripCommentsRegExp)))) {
return getClassBase(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
82 changes: 82 additions & 0 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -1973,6 +1973,88 @@ 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(' ')
);
if (rest.length) {
rest[rest.length - 1] = rest[rest.length - 1].slice(0, -1);
rest.length = 1;
}
Object.setPrototypeOf(clazz, null);
assert.strictEqual(
util.inspect(clazz),
['[class', name, ...rest, 'extends [null prototype]]'].join(' ')
);
Object.defineProperty(clazz, 'name', { value: 'Foo' });
const res = ['[class', 'Foo', ...rest, 'extends [null prototype]]'].join(' ');
assert.strictEqual(util.inspect(clazz), res);
clazz.foo = true;
assert.strictEqual(util.inspect(clazz), `${res} { foo: true }`);
});

// "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]'
);
function fn() {}
Object.defineProperty(fn, 'name', { value: 'class Foo {}' });
assert.strictEqual(
util.inspect(fn),
'[Function: class Foo {}]'
);
}

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

0 comments on commit 2380d90

Please sign in to comment.