Skip to content

Commit

Permalink
feat: use custom toString() methods to generate object descriptions
Browse files Browse the repository at this point in the history
Closes #1284
  • Loading branch information
connor4312 committed Jun 8, 2022
1 parent d816d6a commit 9633741
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This changelog records changes to stable releases since 1.50.2. "TBA" changes he
## Nightly (only)

- feat: simplify pretty print to align with devtools ([vscode#151410](https://github.com/microsoft/vscode/issues/151410))
- feat: use custom `toString()` methods to generate object descriptions ([#1284](https://github.com/microsoft/vscode-js-debug/issues/1284))
- fix: debugged child processes in ext host causing teardown ([#1289](https://github.com/microsoft/vscode-js-debug/issues/1289))
- fix: errors thrown in process tree lookup not being visible ([vscode#150754](https://github.com/microsoft/vscode/issues/150754))
- chore: adopt new restartFrame semantics from Chrome 104 ([#1283](https://github.com/microsoft/vscode-js-debug/issues/1283))
Expand Down
30 changes: 30 additions & 0 deletions src/adapter/templates/getStringyProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { remoteFunction } from '.';

/**
* Gets a mapping of property names with a custom `.toString()` method
* to their string representations.
*/
export const getStringyProps = remoteFunction(function (this: unknown, maxLength: number) {
const out: Record<string, string> = {};
if (typeof this !== 'object' || !this) {
return out;
}

for (const [key, value] of Object.entries(this)) {
if (typeof value === 'object' && value && !String(this.toString).includes('[native code]')) {
out[key] = String(value).slice(0, maxLength);
}
}

return out;
});

export const getToStringIfCustom = remoteFunction(function (this: unknown, maxLength: number) {
if (typeof this === 'object' && this && !String(this.toString).includes('[native code]')) {
return String(this).slice(0, maxLength);
}
});
78 changes: 66 additions & 12 deletions src/adapter/variableStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { StackFrame, StackTrace } from './stackTrace';
import { getSourceSuffix, RemoteException } from './templates';
import { getArrayProperties } from './templates/getArrayProperties';
import { getArraySlots } from './templates/getArraySlots';
import { getStringyProps, getToStringIfCustom } from './templates/getStringyProps';
import { invokeGetter } from './templates/invokeGetter';
import { readMemory } from './templates/readMemory';
import { writeMemory } from './templates/writeMemory';
Expand Down Expand Up @@ -220,13 +221,17 @@ class VariableContext {
return v;
}

public createVariableByType(ctx: IContextInit, object: Cdp.Runtime.RemoteObject) {
public createVariableByType(
ctx: IContextInit,
object: Cdp.Runtime.RemoteObject,
customStringRepr?: string,
) {
if (objectPreview.isArray(object)) {
return this.createVariable(ArrayVariable, ctx, object);
}

if (object.objectId && !objectPreview.subtypesWithoutPreview.has(object.subtype)) {
return this.createVariable(ObjectVariable, ctx, object);
return this.createVariable(ObjectVariable, ctx, object, customStringRepr);
}

return this.createVariable(Variable, ctx, object);
Expand Down Expand Up @@ -265,7 +270,7 @@ class VariableContext {
return [];
}

const [accessorsProperties, ownProperties] = await Promise.all([
const [accessorsProperties, ownProperties, stringyProps] = await Promise.all([
this.cdp.Runtime.getProperties({
objectId: object.objectId,
accessorPropertiesOnly: true,
Expand All @@ -277,6 +282,13 @@ class VariableContext {
ownProperties: true,
generatePreview: true,
}),
getStringyProps({
cdp: this.cdp,
args: [64],
objectId: object.objectId,
throwOnSideEffect: true,
returnByValue: true,
}).catch(() => ({ value: {} as Record<string, string> })),
]);
if (!accessorsProperties || !ownProperties) return [];

Expand Down Expand Up @@ -311,7 +323,7 @@ class VariableContext {
// Push own properties & accessors and symbols
for (const propertiesCollection of [propertiesMap.values(), propertySymbols.values()]) {
for (const p of propertiesCollection) {
properties.push(this.createPropertyVar(p, object));
properties.push(this.createPropertyVar(p, object, stringyProps.value[p.name]));
}
}

Expand Down Expand Up @@ -393,6 +405,7 @@ class VariableContext {
private async createPropertyVar(
p: AnyPropertyDescriptor,
owner: Cdp.Runtime.RemoteObject,
customStringRepr: string,
): Promise<Variable[]> {
const result: Variable[] = [];
const ctx: Required<IContextInit> = {
Expand Down Expand Up @@ -421,7 +434,7 @@ class VariableContext {

// If the value is simply present, add that
if ('value' in p && p.value) {
result.push(this.createVariableByType(ctx, p.value));
result.push(this.createVariableByType(ctx, p.value, customStringRepr));
}

// if it's a getter, auto expand as requested
Expand Down Expand Up @@ -680,26 +693,67 @@ class ErrorVariable extends Variable {
}
}

const NoCustomStringRepr = Symbol('NoStringRepr');

class ObjectVariable extends Variable implements IMemoryReadable {
constructor(
context: VariableContext,
remoteObject: Cdp.Runtime.RemoteObject,
private customStringRepr?: string | typeof NoCustomStringRepr,
) {
super(context, remoteObject);
}

public override async toDap(
previewContext: PreviewContextType,
valueFormat?: Dap.ValueFormat,
): Promise<Dap.Variable> {
const [parentDap, value] = await Promise.all([
await super.toDap(previewContext, valueFormat),
await this.getValueRepresentation(previewContext),
]);

return {
...(await super.toDap(previewContext, valueFormat)),
...parentDap,
type: this.remoteObject.className || this.remoteObject.subtype || this.remoteObject.type,
variablesReference: this.id,
memoryReference: memoryReadableTypes.has(this.remoteObject.subtype)
? String(this.id)
: undefined,
value: await this.context.getCustomObjectDescription(
this.context.name,
this.remoteObject,
previewContext,
),
value,
};
}

private async getValueRepresentation(previewContext: PreviewContextType) {
if (typeof this.customStringRepr === 'string') {
return this.customStringRepr;
}

// for the first level of evaluations, toString it on-demand
if (!this.context.parent && this.customStringRepr !== NoCustomStringRepr) {
try {
const result = await getToStringIfCustom({
cdp: this.context.cdp,
args: [64],
objectId: this.remoteObject.objectId,
returnByValue: true,
});
if (result.value) {
return (this.customStringRepr = result.value);
}
} catch (e) {
this.customStringRepr = NoCustomStringRepr;
// ignored
}
}

return await this.context.getCustomObjectDescription(
this.context.name,
this.remoteObject,
previewContext,
);
}

/** @inheritdoc */
public async readMemory(offset: number, count: number): Promise<Buffer | undefined> {
const result = await readMemory({
Expand Down Expand Up @@ -733,7 +787,7 @@ class ArrayVariable extends ObjectVariable {
private length = 0;

constructor(context: VariableContext, remoteObject: Cdp.Runtime.RemoteObject) {
super(context, remoteObject);
super(context, remoteObject, NoCustomStringRepr);
const match = String(remoteObject.description).match(/\(([0-9]+)\)/);
this.length = match ? +match[1] : 0;
}
Expand Down
2 changes: 1 addition & 1 deletion src/targets/browser/spawn/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export async function getWSEndpoint(
if (!jsonList) {
// no-op
} else if (!jsonList.ok) {
logger.verbose(LogTag.RuntimeLaunch, 'Error looking up /json/list', jsonList);
logger.verbose(LogTag.RuntimeLaunch, 'Error looking up 33', jsonList);
} else {
const fixed = fixRemoteUrl(jsonList.url, jsonList.body[0].webSocketDebuggerUrl);
logger.verbose(LogTag.RuntimeLaunch, 'Discovered target URL from /json/list', {
Expand Down
6 changes: 6 additions & 0 deletions src/test/console/console-format-popular-types.txt
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,9 @@ stdout> > Set(8) {size: 8, 1, 2, 3, 4, 5, …}
Evaluating: 'console.log([new Set([1, 2, 3, 4, 5, 6, 7, 8])])'
stdout> > (1) [Set(8)]

Evaluating: 'console.log(new class { toString() { return "custom to string" } })'
stdout> > custom to string

Evaluating: 'console.log([new class { toString() { return "custom to string" } }])'
stdout> > (1) [{…}]

1 change: 1 addition & 0 deletions src/test/console/consoleFormatTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ describe('console format', () => {
'new Boolean(true)',
'new Set([1, 2, 3, 4])',
'new Set([1, 2, 3, 4, 5, 6, 7, 8])',
'new class { toString() { return "custom to string" } }',
];
const expressions = variables.map(v => [`console.log(${v})`, `console.log([${v}])`]);
await p.logger.evaluateAndLog(([] as string[]).concat(...expressions), { depth: 0 });
Expand Down

0 comments on commit 9633741

Please sign in to comment.