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

Added customDescriptionGenerator feature to customize the descriptions of the debugger #666

Merged
merged 12 commits into from
Aug 27, 2020
22 changes: 15 additions & 7 deletions OPTIONS.md

Large diffs are not rendered by default.

32 changes: 3 additions & 29 deletions src/adapter/breakpoints/conditions/logPoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*--------------------------------------------------------*/

import * as ts from 'typescript';
import { invalidLogPointSyntax, invalidBreakPointCondition } from '../../../dap/errors';
import { invalidBreakPointCondition } from '../../../dap/errors';
import { ProtocolError } from '../../../dap/protocolError';
import { ILogger } from '../../../common/logging';
import { IBreakpointCondition } from '.';
Expand All @@ -14,6 +14,7 @@ import Dap from '../../../dap/api';
import { injectable, inject } from 'inversify';
import { IEvaluator } from '../../evaluator';
import { RuntimeLogPoint } from './runtimeLogPoint';
import { returnErrorsFromStatements } from '../../../common/sourceCodeManipulations';

/**
* Compiles log point expressions to breakpoints.
Expand Down Expand Up @@ -45,34 +46,7 @@ export class LogPointCompiler {
}

private serializeLogStatements(statements: ReadonlyArray<ts.Statement>) {
const output = ['(() => {', ' try {'];

for (let i = 0; i < statements.length; i++) {
let stmt = statements[i].getText().trim();
if (!stmt.endsWith(';')) {
stmt += ';';
}

if (i === statements.length - 1) {
const returned = `return ${stmt}`;
if (!getSyntaxErrorIn(returned)) {
output.push(` ${returned}`);
break;
}
}

output.push(` ${stmt}`);
}

output.push(' } catch (e) {', ' return e.stack || e.message || String(e);', ' }', '})()');

const result = output.join('\n');
const error = getSyntaxErrorIn(result);
if (error) {
throw new ProtocolError(invalidLogPointSyntax(error.message));
}

return result;
return returnErrorsFromStatements('', statements, false);
}

/**
Expand Down
8 changes: 7 additions & 1 deletion src/adapter/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,12 @@ export class Thread implements IVariableStoreDelegate {
this._sourceContainer = sourceContainer;
this._cdp = cdp;
this.id = Thread._lastThreadId++;
this.replVariables = new VariableStore(this._cdp, this, launchConfig.__autoExpandGetters);
this.replVariables = new VariableStore(
this._cdp,
this,
launchConfig.__autoExpandGetters,
launchConfig.customDescriptionGenerator,
);
this._smartStepper = new SmartStepper(this.launchConfig, logger);
this._initialize();
}
Expand Down Expand Up @@ -725,6 +730,7 @@ export class Thread implements IVariableStoreDelegate {
this._cdp,
this,
this.launchConfig.__autoExpandGetters,
this.launchConfig.customDescriptionGenerator,
);
scheduledPauseOnAsyncCall = undefined;

Expand Down
107 changes: 88 additions & 19 deletions src/adapter/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { getSourceSuffix, RemoteException } from './templates';
import { getArrayProperties } from './templates/getArrayProperties';
import { getArraySlots } from './templates/getArraySlots';
import { invokeGetter } from './templates/invokeGetter';
import ts from 'typescript';
import { codeToFunctionReturningErrors } from '../common/sourceCodeManipulations';

const localize = nls.loadMessageBundle();

Expand Down Expand Up @@ -106,6 +108,7 @@ export class VariableStore {
cdp: Cdp.Api,
delegate: IVariableStoreDelegate,
private readonly autoExpandGetters: boolean,
private readonly customDescriptionGenerator: string | undefined,
) {
this._cdp = cdp;
this._delegate = delegate;
Expand Down Expand Up @@ -139,7 +142,7 @@ export class VariableStore {
args: [object.name],
});

return [this._createVariable('', object.parent.wrap(object.name, result), 'repl')];
return [await this._createVariable('', object.parent.wrap(object.name, result), 'repl')];
} catch (e) {
if (!(e instanceof RemoteException)) {
throw e;
Expand Down Expand Up @@ -167,7 +170,7 @@ export class VariableStore {
for (const extraProperty of object.extraProperties || [])
if (!existingVariables.has(extraProperty.name))
variables.push(
this._createVariable(
await this._createVariable(
extraProperty.name,
object.wrap(extraProperty.name, extraProperty.value),
'propertyValue',
Expand Down Expand Up @@ -292,7 +295,7 @@ export class VariableStore {
): Promise<number> {
let rootObjectVariable: Dap.Variable;
if (args.length === 1 && objectPreview.previewAsObject(args[0]) && !stackTrace) {
rootObjectVariable = this._createVariable('', new RemoteObject('', this._cdp, args[0]));
rootObjectVariable = await this._createVariable('', new RemoteObject('', this._cdp, args[0]));
rootObjectVariable.value = text;
} else {
const rootObjectReference =
Expand Down Expand Up @@ -323,7 +326,11 @@ export class VariableStore {
for (let i = 0; i < args.length; ++i) {
if (!objectPreview.previewAsObject(args[i])) continue;
params.push(
this._createVariable(`arg${i}`, new RemoteObject(`arg${i}`, this._cdp, args[i]), 'repl'),
await this._createVariable(
`arg${i}`,
new RemoteObject(`arg${i}`, this._cdp, args[i]),
'repl',
),
);
}

Expand Down Expand Up @@ -412,7 +419,7 @@ export class VariableStore {
presentationHint: { visibility: 'internal' },
};
} else {
variable = this._createVariable(p.name, object.wrap(p.name, p.value));
variable = await this._createVariable(p.name, object.wrap(p.name, p.value));
}

properties.push([
Expand Down Expand Up @@ -491,7 +498,7 @@ export class VariableStore {

// If the value is simply present, add that
if ('value' in p) {
result.push(this._createVariable(p.name, owner.wrap(p.name, p.value), 'propertyValue'));
result.push(await this._createVariable(p.name, owner.wrap(p.name, p.value), 'propertyValue'));
}

// if it's a getter, auto expand as requested
Expand All @@ -510,7 +517,7 @@ export class VariableStore {
}

if (value) {
result.push(this._createVariable(p.name, owner.wrap(p.name, value), 'propertyValue'));
result.push(await this._createVariable(p.name, owner.wrap(p.name, value), 'propertyValue'));
} else {
const obj = owner.wrap(p.name, p.get);
obj.evaluteOnInspect = true;
Expand All @@ -521,14 +528,18 @@ export class VariableStore {
// add setter if present
if (p.set && p.set.type !== 'undefined') {
result.push(
this._createVariable(`set ${p.name}`, owner.wrap(p.name, p.set), 'propertyValue'),
await this._createVariable(`set ${p.name}`, owner.wrap(p.name, p.set), 'propertyValue'),
);
}

return result;
}

private _createVariable(name: string, value?: RemoteObject, context?: string): Dap.Variable {
private async _createVariable(
name: string,
value?: RemoteObject,
context?: string,
): Promise<Dap.Variable> {
if (!value) {
return {
name,
Expand All @@ -538,11 +549,11 @@ export class VariableStore {
}

if (objectPreview.isArray(value.o)) {
return this._createArrayVariable(name, value, context);
return await this._createArrayVariable(name, value, context);
}

if (value.objectId && !objectPreview.subtypesWithoutPreview.has(value.o.subtype)) {
return this._createObjectVariable(name, value, context);
return await this._createObjectVariable(name, value, context);
}

return this._createPrimitiveVariable(name, value, context);
Expand Down Expand Up @@ -573,21 +584,81 @@ export class VariableStore {
};
}

private _createObjectVariable(name: string, value: RemoteObject, context?: string): Dap.Variable {
private async _createObjectVariable(
name: string,
value: RemoteObject,
context?: string,
): Promise<Dap.Variable> {
const variablesReference = this._createVariableReference(value);
const object = value.o;
return {
name,
value:
(name === '__proto__' && object.description) ||
objectPreview.previewRemoteObject(object, context),
value: await this._generateVariableValueDescription(name, value, object, context),
evaluateName: value.accessor,
type: object.subtype || object.type,
variablesReference,
};
}

private _createArrayVariable(name: string, value: RemoteObject, context?: string): Dap.Variable {
private async _generateVariableValueDescription(
name: string,
value: RemoteObject,
object: Cdp.Runtime.RemoteObject,
context?: string,
): Promise<string> {
const defaultValueDescription =
(name === '__proto__' && object.description) ||
objectPreview.previewRemoteObject(object, context);

if (!this.customDescriptionGenerator) {
return defaultValueDescription;
}

let errorDescription;
try {
const customValueDescription = await this._cdp.Runtime.callFunctionOn({
objectId: object.objectId,
functionDeclaration: this.extractFunctionFromCustomDescriptionGenerator(
this.customDescriptionGenerator,
),
arguments: [this._toCallArgument(defaultValueDescription)],
});
if (customValueDescription?.exceptionDetails === undefined) {
return '' + customValueDescription?.result.value;
} else if (customValueDescription.result.description) {
errorDescription = customValueDescription.result.description.split('\n', 1)[0];
} else {
errorDescription = localize('error.unknown', 'Unknown error');
}
} catch (e) {
errorDescription = e.stack || e.message || String(e);
}

return localize(
'error.customValueDescriptionGeneratorFailed',
"{0} (couldn't describe: {1})",
defaultValueDescription,
errorDescription,
);
}

private extractFunctionFromCustomDescriptionGenerator(generatorDefinition: string): string {
const sourceFile = ts.createSourceFile(
'customDescriptionGenerator.js',
generatorDefinition,
ts.ScriptTarget.ESNext,
true,
);

const code = codeToFunctionReturningErrors('defaultValue', sourceFile.statements);
return code;
}

private async _createArrayVariable(
name: string,
value: RemoteObject,
context?: string,
): Promise<Dap.Variable> {
const object = value.o;
const variablesReference = this._createVariableReference(value);
const match = String(object.description).match(/\(([0-9]+)\)/);
Expand All @@ -596,9 +667,7 @@ export class VariableStore {
// For small arrays (less than 100 items), pretend we don't have indexex properties.
return {
name,
value:
(name === '__proto__' && object.description) ||
objectPreview.previewRemoteObject(object, context),
value: await this._generateVariableValueDescription(name, value, object, context),
type: object.className || object.subtype || object.type,
variablesReference,
evaluateName: value.accessor,
Expand Down
5 changes: 5 additions & 0 deletions src/build/generate-contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ const baseConfigurationAttributes: ConfigurationAttributes<IBaseConfiguration> =
type: 'boolean',
description: refString('enableContentValidation.description'),
},
customDescriptionGenerator: {
type: 'string',
default: undefined,
description: refString('customDescriptionGenerator.description'),
},
cascadeTerminateToConfigurations: {
type: 'array',
items: {
Expand Down
5 changes: 5 additions & 0 deletions src/build/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ const strings = {
'If set, attaches to the process via the given port. This is generally no longer necessary for Node.js programs and loses the ability to debug child processes, but can be useful in more esoteric scenarios such as with Deno and Docker launches. If set to 0, a random port will be chosen and --inspect-brk added to the launch arguments automatically.',
'enableContentValidation.description':
'Toggles whether we verify the contents of files on disk match the ones loaded in the runtime. This is useful in a variety of scenarios and required in some, but can cause issues if you have server-side transformation of scripts, for example.',
'customDescriptionGenerator.description': `Customize the textual description the debugger shows for objects (local variables, etc...). Samples:
1. this.toString() // will call toString to print all objects
2. this.customDescription ? this.customDescription() : defaultValue // Use customDescription method if available, if not return defaultValue
3. function (def) { return this.customDescription ? this.customDescription() : def } // Use customDescription method if available, if not return defaultValue
`,

'longPredictionWarning.message':
"It's taking a while to configure your breakpoints. You can speed this up by updating the 'outFiles' in your launch.json.",
Expand Down
3 changes: 2 additions & 1 deletion src/common/hash/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { join } from 'path';
import { Hasher } from '.';
import { createFileTree, getTestDir } from '../../test/createFileTree';

describe('hash process', () => {
describe('hash process', function () {
this.timeout(10 * 1000); // 10 seconds timeout
let hasher: Hasher;
let testDir: string;

Expand Down
Loading