Skip to content

Commit

Permalink
feat: add Symbol.for("debug.description") as a way to generate obje…
Browse files Browse the repository at this point in the history
…ct descriptions

Fixes microsoft/vscode#102181
  • Loading branch information
connor4312 committed Aug 3, 2023
1 parent 9353ca1 commit 2051621
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This changelog records changes to stable releases since 1.50.2. "TBA" changes he

## Nightly (only)

- feat: add `Symbol.for("debug.description")` as a way to generate object descriptions ([vscode#102181](https://github.com/microsoft/vscode/issues/102181))
- fix: child processes from extension host not getting spawned during debug
- fix: support vite HMR source replacements ([#1761](https://github.com/microsoft/vscode-js-debug/issues/1761))
- fix: immediately log stdout/err unless EXT is encountered ([vscode#181785](https://github.com/microsoft/vscode/issues/181785))
Expand Down
59 changes: 51 additions & 8 deletions src/adapter/templates/getStringyProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,25 @@
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { templateFunction } from '.';
import { remoteFunction, templateFunction } from '.';

const enum DescriptionSymbols {
// Our generic symbol
Generic = 'debug.description',
// Node.js-specific symbol that is used for some Node types https://nodejs.org/api/util.html#utilinspectcustom
Node = 'nodejs.util.inspect.custom',
}

/**
* Separate function that initializes description symbols. V8 considers
* Symbol.for as having a "side effect" and would throw if we tried to first
* use them inside the description functions.
*/
export const getDescriptionSymbols = remoteFunction(function () {
return [Symbol.for(DescriptionSymbols.Generic), Symbol.for(DescriptionSymbols.Node)];
});

declare const runtimeArgs: [symbol[]];

/**
* Gets a mapping of property names with a custom `.toString()` method
Expand Down Expand Up @@ -33,11 +51,23 @@ export const getStringyProps = templateFunction(function (
}
}

if (typeof value === 'object' && value && !String(value.toString).includes('[native code]')) {
const str = String(value);
if (!str.startsWith('[object ')) {
if (typeof value === 'object' && value) {
let str: string | undefined;
for (const sym of runtimeArgs[0]) {
try {
str = value[sym]();
break;
} catch {
// ignored
}
}

if (!str && !String(value.toString).includes('[native code]')) {
str = String(value);
}

if (str && !str.startsWith('[object ')) {
out[key] = str.length >= maxLength ? str.slice(0, maxLength) + '…' : str;
continue;
}
}
}
Expand All @@ -62,9 +92,22 @@ export const getToStringIfCustom = templateFunction(function (
}
}

if (typeof this === 'object' && this && !String(this.toString).includes('[native code]')) {
const str = String(this);
if (!str.startsWith('[object ')) {
if (typeof this === 'object' && this) {
let str: string | undefined;
for (const sym of runtimeArgs[0]) {
try {
str = (this as Record<symbol, () => string>)[sym]();
break;
} catch {
// ignored
}
}

if (!str && !String(this.toString).includes('[native code]')) {
str = String(this);
}

if (str && !str.startsWith('[object ')) {
return str.length >= maxLength ? str.slice(0, maxLength) + '…' : str;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/adapter/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function templateFunctionStr<Args extends string[]>(stringified: string): Templa
`;
return {
expr: (...args: Args) => `(()=>{${inner(args)}})();\n${getSourceSuffix()}`,
decl: (...args: Args) => `function(){${inner(args)};\n${getSourceSuffix()}}`,
decl: (...args: Args) => `function(...runtimeArgs){${inner(args)};\n${getSourceSuffix()}}`,
};
}

Expand Down
32 changes: 30 additions & 2 deletions src/adapter/variableStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import { StackFrame, StackTrace } from './stackTrace';
import { getSourceSuffix, RemoteException, RemoteObjectId } from './templates';
import { getArrayProperties } from './templates/getArrayProperties';
import { getArraySlots } from './templates/getArraySlots';
import { getStringyProps, getToStringIfCustom } from './templates/getStringyProps';
import {
getDescriptionSymbols,
getStringyProps,
getToStringIfCustom,
} from './templates/getStringyProps';
import { invokeGetter } from './templates/invokeGetter';
import { readMemory } from './templates/readMemory';
import { writeMemory } from './templates/writeMemory';
Expand Down Expand Up @@ -170,6 +174,7 @@ interface IContextInit {
interface IContextSettings {
customDescriptionGenerator?: string;
customPropertiesGenerator?: string;
descriptionSymbols?: Promise<Cdp.Runtime.CallArgument>;
}

class VariableContext {
Expand Down Expand Up @@ -266,6 +271,23 @@ class VariableContext {
return this.createVariable(Variable, ctx, object);
}

/**
* Ensures symbols for custom descriptions are available, must be used
* before getStringProps/getToStringIfCustom
*/
public async getDescriptionSymbols(objectId: string): Promise<Cdp.Runtime.CallArgument> {
this.settings.descriptionSymbols ??= getDescriptionSymbols({
cdp: this.cdp,
args: [],
objectId,
}).then(
r => ({ objectId: r.objectId }),
() => ({ value: [] }),
);

return await this.settings.descriptionSymbols;
}

/**
* Creates Variables for each property on the RemoteObject.
*/
Expand Down Expand Up @@ -323,6 +345,7 @@ class VariableContext {
`${customStringReprMaxLength}`,
this.settings.customDescriptionGenerator || 'null',
),
arguments: [await this.getDescriptionSymbols(object.objectId)],
objectId: object.objectId,
throwOnSideEffect: true,
returnByValue: true,
Expand Down Expand Up @@ -757,13 +780,18 @@ class ObjectVariable extends Variable implements IMemoryReadable {
}

// for the first level of evaluations, toString it on-demand
if (!this.context.parent && this.customStringRepr !== NoCustomStringRepr) {
if (
!this.context.parent &&
this.remoteObject.objectId &&
this.customStringRepr !== NoCustomStringRepr
) {
try {
const ret = await this.context.cdp.Runtime.callFunctionOn({
functionDeclaration: getToStringIfCustom.decl(
`${customStringReprMaxLength}`,
this.context.customDescriptionGenerator || 'null',
),
arguments: [await this.context.getDescriptionSymbols(this.remoteObject.objectId)],
objectId: this.remoteObject.objectId,
returnByValue: true,
});
Expand Down
3 changes: 3 additions & 0 deletions src/test/console/console-format-custom-symbol.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
> result: hello a
> prop: hello b
> [[Prototype]]: Object
16 changes: 16 additions & 0 deletions src/test/console/console-format-popular-types.txt
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,19 @@ Evaluating: 'console.log([new class { toString() { return "long custom to string
stdout> (1) [{…}]
stdout> > (1) [{…}]

Evaluating: 'console.log(new class { [Symbol.for("debug.description")]() { return "some custom repr" } })'
stdout> {}
stdout> > some custom repr

Evaluating: 'console.log([new class { [Symbol.for("debug.description")]() { return "some custom repr" } }])'
stdout> (1) [{…}]
stdout> > (1) [{…}]

Evaluating: 'console.log(new class { [Symbol.for("nodejs.util.inspect.custom")]() { return "some node repr" } })'
stdout> {}
stdout> > some node repr

Evaluating: 'console.log([new class { [Symbol.for("nodejs.util.inspect.custom")]() { return "some node repr" } }])'
stdout> (1) [{…}]
stdout> > (1) [{…}]

15 changes: 15 additions & 0 deletions src/test/console/consoleFormatTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ describe('console format', () => {
'new Set([1, 2, 3, 4, 5, 6, 7, 8])',
'new class { toString() { return "custom to string" } }',
'new class { toString() { return "long custom to string".repeat(500) } }',
'new class { [Symbol.for("debug.description")]() { return "some custom repr" } }',
'new class { [Symbol.for("nodejs.util.inspect.custom")]() { return "some node repr" } }',
];
const expressions = variables.map(v => [`console.log(${v})`, `console.log([${v}])`]);
await p.logger.evaluateAndLog(([] as string[]).concat(...expressions), { depth: 0 });
Expand All @@ -153,6 +155,19 @@ describe('console format', () => {
p.assertLog();
});

itIntegrates('custom symbol', async ({ r }) => {
const p = await r.launchAndLoad('blank');
await p.logger.evaluateAndLog(`
new class A {
prop = new class B {
[Symbol.for("debug.description")]() { return "hello b" }
};
[Symbol.for("debug.description")]() { return "hello a" }
}
`);
p.assertLog();
});

itIntegrates('collections', async ({ r }) => {
const p = await r.launchAndLoad(`
<div style="display:none" class="c1 c2 c3">
Expand Down

0 comments on commit 2051621

Please sign in to comment.