Skip to content

Commit c92cc13

Browse files
authored
Merge pull request #1296 from microsoft/feat/to-string-object-descriptions
feat: use custom toString() methods to generate object descriptions
2 parents f350a29 + 92f38f2 commit c92cc13

File tree

7 files changed

+114
-13
lines changed

7 files changed

+114
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This changelog records changes to stable releases since 1.50.2. "TBA" changes he
55
## Nightly (only)
66

77
- feat: simplify pretty print to align with devtools ([vscode#151410](https://github.com/microsoft/vscode/issues/151410))
8+
- feat: use custom `toString()` methods to generate object descriptions ([#1284](https://github.com/microsoft/vscode-js-debug/issues/1284))
89
- fix: debugged child processes in ext host causing teardown ([#1289](https://github.com/microsoft/vscode-js-debug/issues/1289))
910
- fix: errors thrown in process tree lookup not being visible ([vscode#150754](https://github.com/microsoft/vscode/issues/150754))
1011
- fix: extension debugging not working with two ipv6 interfaces ([vscode#144315](https://github.com/microsoft/vscode/issues/144315))
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*---------------------------------------------------------
2+
* Copyright (C) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------*/
4+
5+
import { remoteFunction } from '.';
6+
7+
/**
8+
* Gets a mapping of property names with a custom `.toString()` method
9+
* to their string representations.
10+
*/
11+
export const getStringyProps = remoteFunction(function (this: unknown, maxLength: number) {
12+
const out: Record<string, string> = {};
13+
if (typeof this !== 'object' || !this) {
14+
return out;
15+
}
16+
17+
for (const [key, value] of Object.entries(this)) {
18+
if (typeof value === 'object' && value && !String(this.toString).includes('[native code]')) {
19+
out[key] = String(value).slice(0, maxLength);
20+
}
21+
}
22+
23+
return out;
24+
});
25+
26+
export const getToStringIfCustom = remoteFunction(function (this: unknown, maxLength: number) {
27+
if (typeof this === 'object' && this && !String(this.toString).includes('[native code]')) {
28+
return String(this).slice(0, maxLength);
29+
}
30+
});

src/adapter/variableStore.ts

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { StackFrame, StackTrace } from './stackTrace';
2121
import { getSourceSuffix, RemoteException } from './templates';
2222
import { getArrayProperties } from './templates/getArrayProperties';
2323
import { getArraySlots } from './templates/getArraySlots';
24+
import { getStringyProps, getToStringIfCustom } from './templates/getStringyProps';
2425
import { invokeGetter } from './templates/invokeGetter';
2526
import { readMemory } from './templates/readMemory';
2627
import { writeMemory } from './templates/writeMemory';
@@ -220,13 +221,17 @@ class VariableContext {
220221
return v;
221222
}
222223

223-
public createVariableByType(ctx: IContextInit, object: Cdp.Runtime.RemoteObject) {
224+
public createVariableByType(
225+
ctx: IContextInit,
226+
object: Cdp.Runtime.RemoteObject,
227+
customStringRepr?: string,
228+
) {
224229
if (objectPreview.isArray(object)) {
225230
return this.createVariable(ArrayVariable, ctx, object);
226231
}
227232

228233
if (object.objectId && !objectPreview.subtypesWithoutPreview.has(object.subtype)) {
229-
return this.createVariable(ObjectVariable, ctx, object);
234+
return this.createVariable(ObjectVariable, ctx, object, customStringRepr);
230235
}
231236

232237
return this.createVariable(Variable, ctx, object);
@@ -265,7 +270,7 @@ class VariableContext {
265270
return [];
266271
}
267272

268-
const [accessorsProperties, ownProperties] = await Promise.all([
273+
const [accessorsProperties, ownProperties, stringyProps] = await Promise.all([
269274
this.cdp.Runtime.getProperties({
270275
objectId: object.objectId,
271276
accessorPropertiesOnly: true,
@@ -277,6 +282,13 @@ class VariableContext {
277282
ownProperties: true,
278283
generatePreview: true,
279284
}),
285+
getStringyProps({
286+
cdp: this.cdp,
287+
args: [64],
288+
objectId: object.objectId,
289+
throwOnSideEffect: true,
290+
returnByValue: true,
291+
}).catch(() => ({ value: {} as Record<string, string> })),
280292
]);
281293
if (!accessorsProperties || !ownProperties) return [];
282294

@@ -311,7 +323,13 @@ class VariableContext {
311323
// Push own properties & accessors and symbols
312324
for (const propertiesCollection of [propertiesMap.values(), propertySymbols.values()]) {
313325
for (const p of propertiesCollection) {
314-
properties.push(this.createPropertyVar(p, object));
326+
properties.push(
327+
this.createPropertyVar(
328+
p,
329+
object,
330+
stringyProps.hasOwnProperty(p.name) ? stringyProps.value[p.name] : undefined,
331+
),
332+
);
315333
}
316334
}
317335

@@ -393,6 +411,7 @@ class VariableContext {
393411
private async createPropertyVar(
394412
p: AnyPropertyDescriptor,
395413
owner: Cdp.Runtime.RemoteObject,
414+
customStringRepr: string | undefined,
396415
): Promise<Variable[]> {
397416
const result: Variable[] = [];
398417
const ctx: Required<IContextInit> = {
@@ -421,7 +440,7 @@ class VariableContext {
421440

422441
// If the value is simply present, add that
423442
if ('value' in p && p.value) {
424-
result.push(this.createVariableByType(ctx, p.value));
443+
result.push(this.createVariableByType(ctx, p.value, customStringRepr));
425444
}
426445

427446
// if it's a getter, auto expand as requested
@@ -680,26 +699,67 @@ class ErrorVariable extends Variable {
680699
}
681700
}
682701

702+
const NoCustomStringRepr = Symbol('NoStringRepr');
703+
683704
class ObjectVariable extends Variable implements IMemoryReadable {
705+
constructor(
706+
context: VariableContext,
707+
remoteObject: Cdp.Runtime.RemoteObject,
708+
private customStringRepr?: string | typeof NoCustomStringRepr,
709+
) {
710+
super(context, remoteObject);
711+
}
712+
684713
public override async toDap(
685714
previewContext: PreviewContextType,
686715
valueFormat?: Dap.ValueFormat,
687716
): Promise<Dap.Variable> {
717+
const [parentDap, value] = await Promise.all([
718+
await super.toDap(previewContext, valueFormat),
719+
await this.getValueRepresentation(previewContext),
720+
]);
721+
688722
return {
689-
...(await super.toDap(previewContext, valueFormat)),
723+
...parentDap,
690724
type: this.remoteObject.className || this.remoteObject.subtype || this.remoteObject.type,
691725
variablesReference: this.id,
692726
memoryReference: memoryReadableTypes.has(this.remoteObject.subtype)
693727
? String(this.id)
694728
: undefined,
695-
value: await this.context.getCustomObjectDescription(
696-
this.context.name,
697-
this.remoteObject,
698-
previewContext,
699-
),
729+
value,
700730
};
701731
}
702732

733+
private async getValueRepresentation(previewContext: PreviewContextType) {
734+
if (typeof this.customStringRepr === 'string') {
735+
return this.customStringRepr;
736+
}
737+
738+
// for the first level of evaluations, toString it on-demand
739+
if (!this.context.parent && this.customStringRepr !== NoCustomStringRepr) {
740+
try {
741+
const result = await getToStringIfCustom({
742+
cdp: this.context.cdp,
743+
args: [64],
744+
objectId: this.remoteObject.objectId,
745+
returnByValue: true,
746+
});
747+
if (result.value) {
748+
return (this.customStringRepr = result.value);
749+
}
750+
} catch (e) {
751+
this.customStringRepr = NoCustomStringRepr;
752+
// ignored
753+
}
754+
}
755+
756+
return await this.context.getCustomObjectDescription(
757+
this.context.name,
758+
this.remoteObject,
759+
previewContext,
760+
);
761+
}
762+
703763
/** @inheritdoc */
704764
public async readMemory(offset: number, count: number): Promise<Buffer | undefined> {
705765
const result = await readMemory({
@@ -733,7 +793,7 @@ class ArrayVariable extends ObjectVariable {
733793
private length = 0;
734794

735795
constructor(context: VariableContext, remoteObject: Cdp.Runtime.RemoteObject) {
736-
super(context, remoteObject);
796+
super(context, remoteObject, NoCustomStringRepr);
737797
const match = String(remoteObject.description).match(/\(([0-9]+)\)/);
738798
this.length = match ? +match[1] : 0;
739799
}

src/test/console/console-format-popular-types.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,3 +256,9 @@ stdout> > Set(8) {size: 8, 1, 2, 3, 4, 5, …}
256256
Evaluating: 'console.log([new Set([1, 2, 3, 4, 5, 6, 7, 8])])'
257257
stdout> > (1) [Set(8)]
258258

259+
Evaluating: 'console.log(new class { toString() { return "custom to string" } })'
260+
stdout> > custom to string
261+
262+
Evaluating: 'console.log([new class { toString() { return "custom to string" } }])'
263+
stdout> > (1) [{…}]
264+

src/test/console/consoleFormatTest.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ describe('console format', () => {
129129
'new Boolean(true)',
130130
'new Set([1, 2, 3, 4])',
131131
'new Set([1, 2, 3, 4, 5, 6, 7, 8])',
132+
'new class { toString() { return "custom to string" } }',
132133
];
133134
const expressions = variables.map(v => [`console.log(${v})`, `console.log([${v}])`]);
134135
await p.logger.evaluateAndLog(([] as string[]).concat(...expressions), { depth: 0 });

src/test/evaluate/evaluate-toplevelawait.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ Evaluating: 'await Promise.resolve(1)'
22
result: 1
33
Evaluating: '{a:await Promise.resolve(1)}'
44
> result: {a: 1}
5+
Evaluating: '4'
6+
result: 4
57
Evaluating: '$_'
6-
> result: {a: 1}
8+
result: 4
79
Evaluating: 'let {a,b} = await Promise.resolve({a: 1, b:2}), f = 5;'
810
result: undefined
911
Evaluating: 'a'

src/test/evaluate/evaluate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ describe('evaluate', () => {
283283
const exprs = [
284284
'await Promise.resolve(1)',
285285
'{a:await Promise.resolve(1)}',
286+
'4',
286287
'$_',
287288
'let {a,b} = await Promise.resolve({a: 1, b:2}), f = 5;',
288289
'a',

0 commit comments

Comments
 (0)