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: add private fields preview support #31817

Closed
wants to merge 1 commit 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
7 changes: 7 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1689,6 +1689,13 @@ object.

An attempt was made to `require()` an [ES Module][].

<a id="ERR_PREVIEW_FAILURE"></a>
### `ERR_PREVIEW_FAILURE`

> Stability: 1 - Experimental

An attempt of previewing a JavaScript value was failed.

<a id="ERR_SCRIPT_EXECUTION_INTERRUPTED"></a>
### `ERR_SCRIPT_EXECUTION_INTERRUPTED`

Expand Down
43 changes: 43 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,49 @@ Otherwise, returns `false`.
See [`assert.deepStrictEqual()`][] for more information about deep strict
equality.

### `util.previewValue(value[, opts])`
<!-- YAML
added: REPLACEME
-->

* `object` {any} Any JavaScript primitive or `Object`.
* `options` {Object}
* `breakLength` {integer} The length at which input values are split across
multiple lines. Set to `Infinity` to format the input as a single line
(in combination with `compact` set to `true` or any number >= `1`).
**Default:** `80`.
* `colors` {boolean} If `true`, the output is styled with ANSI color
codes. Colors are customizable. See [Customizing `util.inspect` colors][].
**Default:** `false`.
* `compact` {boolean|integer} Setting this to `false` causes each object key
to be displayed on a new line. It will also add new lines to text that is
longer than `breakLength`. If set to a number, the most `n` inner elements
are united on a single line as long as all properties fit into
`breakLength`. Short array elements are also grouped together. No
text will be reduced below 16 characters, no matter the `breakLength` size.
For more information, see the example below. **Default:** `3`.
* Returns: {Promise<string>} A promise of the representation of `object`.

> Stability: 1 - Experimental

The `util.previewValue` method returns promise of a string representation
of `object` that is intended for debugging. The output of `util.previewValue`
may change at any time and should not be depended upon programmatically.
Additional `options` may be passed that alter the result. `util.previewValue()`
will use the constructor's name make an identifiable tag for an inspected value.

The difference of this method with `util.inspect` is `util.previewValue` can
reveal target own private fields.

```javascript
class Foo { #bar = 'baz' }

util.previewValue(new Foo())
.then((preview) => {
console.log(preview); // 'Foo { #bar: "baz" }'
});
```

## `util.promisify(original)`
<!-- YAML
added: v8.0.0
Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1280,6 +1280,7 @@ E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => {
pkgPath} imported from ${base}`;
}
}, Error);
E('ERR_PREVIEW_FAILURE', 'Preview value failed for reason: "%s"', Error);
E('ERR_REQUIRE_ESM',
(filename, parentPath = null, packageJsonPath = null) => {
let msg = `Must use import to load ES Module: ${filename}`;
Expand Down
201 changes: 200 additions & 1 deletion lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const {
ObjectPrototypeHasOwnProperty,
ObjectPrototypePropertyIsEnumerable,
ObjectSeal,
Promise,
RegExp,
RegExpPrototypeToString,
Set,
Expand Down Expand Up @@ -73,7 +74,8 @@ const {

const {
codes: {
ERR_INVALID_ARG_TYPE
ERR_INVALID_ARG_TYPE,
ERR_PREVIEW_FAILURE
},
isStackOverflowError
} = require('internal/errors');
Expand Down Expand Up @@ -120,6 +122,12 @@ const assert = require('internal/assert');

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

const { hasInspector } = internalBinding('config');

let inspector;
let privateInspectionSession;
let privatePreviewSessionCount = 0;

const setSizeGetter = uncurryThis(
ObjectGetOwnPropertyDescriptor(SetPrototype, 'size').get);
const mapSizeGetter = uncurryThis(
Expand Down Expand Up @@ -2027,11 +2035,202 @@ function stripVTControlCharacters(str) {
return str.replace(ansi, '');
}

async function previewValue(target, opts) {
const ctx = {
budget: {},
indentationLvl: 0,
currentDepth: 0,
stylize: stylizeNoColor,
compact: inspectDefaultOptions.compact,
breakLength: inspectDefaultOptions.breakLength,
};
if (opts) {
const optKeys = ObjectKeys(opts);
for (const key of optKeys) {
if (ObjectPrototypeHasOwnProperty(inspectDefaultOptions, key)) {
ctx[key] = opts[key];
} else if (ctx.userOptions === undefined) {
// This is required to pass through the actual user input.
ctx.userOptions = opts;
}
}
}
if (ctx.colors) {
ctx.stylize = stylizeWithColor;
}

return previewRaw(ctx, target);
}

async function previewRaw(ctx, target) {
if (typeof target !== 'object' &&
typeof target !== 'function' &&
!isUndetectableObject(target)) {
return formatPrimitive(ctx.stylize, target, ctx);
}
if (target === null) {
return ctx.stylize('null', 'null');
}
if (typeof target === 'function') {
return formatRaw(ctx, target);
}

const preview = await getValuePreviewAsync(target);
if (preview == null) {
return undefined;
}
return formatInspectorPropertyPreview(ctx, target, preview);
}

function previewPrelude() {
if (!hasInspector) {
return false;
}
if (privateInspectionSession == null) {
inspector = require('inspector');
privateInspectionSession = new inspector.Session();
privateInspectionSession.connect();
}
return true;
}

function getValuePreviewAsync(target) {
if (!previewPrelude()) {
return undefined;
}
if (privatePreviewSessionCount === 0) {
privateInspectionSession.post('Runtime.enable');
privateInspectionSession.post('Debugger.enable');
}
privatePreviewSessionCount++;

return new Promise((resolve, reject) => {
privateInspectionSession.once('Debugger.paused', (pausedContext) => {
const callFrames = pausedContext.params.callFrames;
/** USED */target;
privateInspectionSession.post(
'Debugger.evaluateOnCallFrame', {
/**
* 0. inspector.dispatch,
* 1. inspector.post,
* 2. new Promise,
* 3. previewValueAsync
*/
callFrameId: callFrames[3].callFrameId,
expression: 'target',
}, (err, result) => {
if (err) {
return reject(err);
}
const { result: evalResult } = result;

if (evalResult.type !== 'object') {
return resolve();
}
if (evalResult.subtype === 'error') {
return reject(ERR_PREVIEW_FAILURE(evalResult.description));
}
resolve(getValuePreviewByObjectIdAsync(evalResult.objectId));
});
});
privateInspectionSession.post('Debugger.pause', () => {
/* pause doesn't return anything. */
});
}).finally(() => {
privatePreviewSessionCount--;
if (privatePreviewSessionCount === 0) {
privateInspectionSession.post('Runtime.disable');
privateInspectionSession.post('Debugger.disable');
}
});
}

function getValuePreviewByObjectIdAsync(objectId, options = {}) {
return new Promise((resolve, reject) => {
privateInspectionSession.post('Runtime.getProperties', {
objectId: objectId,
ownProperties: true,
...options
}, (err, result) => {
if (err) {
return reject(err);
}
resolve(result);
});
});
}

async function formatInspectorPropertyPreview(
ctx, target, preview, isJsValue = true
) {
const { result, privateProperties } = preview;

const output = [];
for (var item of [
// __proto__ is an own property
...result.filter((it) => it.name !== '__proto__'),
...(privateProperties ?? [])
]) {
const valueDescriptor = item.value;
const valueType = valueDescriptor.type;
const subtype = valueDescriptor.subtype;
const stylize = ctx.stylize;
let str;
if (valueType === 'string') {
str = stylize(strEscape(valueDescriptor.value), valueType);
} else if (valueType === 'number') {
str = stylize(
// -0 is unserializable to JSON representation
valueDescriptor.value ?? valueDescriptor.unserializableValue,
valueType);
} else if (valueType === 'boolean') {
str = stylize(valueDescriptor.value, valueType);
} else if (valueType === 'symbol') {
str = stylize(valueDescriptor.description, valueType);
} else if (valueType === 'undefined') {
str = stylize(valueType, valueType);
} else if (subtype === 'null') {
str = stylize(subtype, 'null');
} else if (valueType === 'bigint') {
str = stylize(valueDescriptor.unserializableValue, valueType);
} else if (valueType === 'function') {
str = await previewFunction(ctx, valueDescriptor.objectId);
} else if (valueType === 'object') {
// TODO(legendecas): Inspect the whole object, not only the class name.
str = `#[${valueDescriptor.className}]`;
} else {
/* c8 ignore next */
assert.fail(`Unknown value type '${valueType}'`);
}
output.push(`${item.name}: ${str}`);
}

const constructor = isJsValue ?
getConstructorName(target, ctx, 0) :
target.className;
const braces = [`${getPrefix(constructor, '', 'Object')}{`, '}'];
const base = '';
const res = reduceToSingleString(
ctx, output, base, braces, kObjectType, ctx.currentDepth);
return res;
}

async function previewFunction(ctx, objectId) {
const { result } = await getValuePreviewByObjectIdAsync(
objectId, { ownProperties: false });
const nameDesc = result.find((it) => it.name === 'name');
if (nameDesc == null) {
return ctx.stylize('[Function]', 'function');
}
return ctx.stylize(`[Function: ${nameDesc.value.value}]`, 'function');
}

module.exports = {
inspect,
format,
formatWithOptions,
getStringWidth,
inspectDefaultOptions,
previewValue,
stripVTControlCharacters
};
4 changes: 3 additions & 1 deletion lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ const {
const {
format,
formatWithOptions,
inspect
inspect,
previewValue
} = require('internal/util/inspect');
const { debuglog } = require('internal/util/debuglog');
const { validateNumber } = require('internal/validators');
Expand Down Expand Up @@ -269,6 +270,7 @@ module.exports = {
isFunction,
isPrimitive,
log,
previewValue,
promisify,
TextDecoder,
TextEncoder,
Expand Down
Loading