chrome-devtools-frontend
Version:
Chrome DevTools UI
1,242 lines (1,106 loc) • 62.1 kB
text/typescript
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';
import * as Workspace from '../workspace/workspace.js';
import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; // eslint-disable-line rulesdir/es_modules_import
import {ContentProviderBasedProject} from './ContentProviderBasedProject.js';
import type {DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js';
import {NetworkProject} from './NetworkProject.js';
import {assertNotNullOrUndefined} from '../../core/platform/platform.js';
const UIStrings = {
/**
*@description Error message that is displayed in the Console when language #plugins report errors
*@example {File not found} PH1
*/
errorInDebuggerLanguagePlugin: 'Error in debugger language plugin: {PH1}',
/**
*@description Status message that is shown in the Console when debugging information is being
*loaded. The 2nd and 3rd placeholders are URLs.
*@example {C/C++ DevTools Support (DWARF)} PH1
*@example {http://web.dev/file.wasm} PH2
*@example {http://web.dev/file.wasm.debug.wasm} PH3
*/
loadingDebugSymbolsForVia: '[{PH1}] Loading debug symbols for {PH2} (via {PH3})...',
/**
*@description Status message that is shown in the Console when debugging information is being loaded
*@example {C/C++ DevTools Support (DWARF)} PH1
*@example {http://web.dev/file.wasm} PH2
*/
loadingDebugSymbolsFor: '[{PH1}] Loading debug symbols for {PH2}...',
/**
*@description Warning message that is displayed in the Console when debugging information was loaded, but no source files were found
*@example {C/C++ DevTools Support (DWARF)} PH1
*@example {http://web.dev/file.wasm} PH2
*/
loadedDebugSymbolsForButDidnt: '[{PH1}] Loaded debug symbols for {PH2}, but didn\'t find any source files',
/**
*@description Status message that is shown in the Console when debugging information is successfully loaded
*@example {C/C++ DevTools Support (DWARF)} PH1
*@example {http://web.dev/file.wasm} PH2
*@example {42} PH3
*/
loadedDebugSymbolsForFound: '[{PH1}] Loaded debug symbols for {PH2}, found {PH3} source file(s)',
/**
*@description Error message that is displayed in the Console when debugging information cannot be loaded
*@example {C/C++ DevTools Support (DWARF)} PH1
*@example {http://web.dev/file.wasm} PH2
*@example {File not found} PH3
*/
failedToLoadDebugSymbolsFor: '[{PH1}] Failed to load debug symbols for {PH2} ({PH3})',
/**
*@description Error message that is displayed in UI debugging information cannot be found for a call frame
*@example {main} PH1
*/
failedToLoadDebugSymbolsForFunction: 'No debug information for function "{PH1}"',
/**
*@description Error message that is displayed in UI when a file needed for debugging information for a call frame is missing
*@example {mainp.debug.wasm.dwp} PH1
*/
debugSymbolsIncomplete: 'The debug information for function {PH1} is incomplete',
};
const str_ = i18n.i18n.registerUIStrings('models/bindings/DebuggerLanguagePlugins.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
class SourceType {
typeInfo: Chrome.DevTools.TypeInfo;
members: SourceType[];
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
typeMap: Map<any, SourceType>;
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(typeInfo: Chrome.DevTools.TypeInfo, members: SourceType[], typeMap: Map<any, SourceType>) {
this.typeInfo = typeInfo;
this.members = members;
this.typeMap = typeMap;
}
/**
* Create a type graph
*/
static create(typeInfos: Chrome.DevTools.TypeInfo[]): SourceType|null {
if (typeInfos.length === 0) {
return null;
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const typeMap = new Map<any, SourceType>();
for (const typeInfo of typeInfos) {
typeMap.set(typeInfo.typeId, new SourceType(typeInfo, [], typeMap));
}
for (const sourceType of typeMap.values()) {
sourceType.members = sourceType.typeInfo.members.map(({typeId}) => {
const memberType = typeMap.get(typeId);
if (!memberType) {
throw new Error(`Incomplete type information for type ${typeInfos[0].typeNames[0] || '<anonymous>'}`);
}
return memberType;
});
}
return typeMap.get(typeInfos[0].typeId) || null;
}
}
/**
* Generates the raw module ID for a script, which is used
* to uniquely identify the debugging data for a script on
* the responsible language #plugin.
*
* @param script the unique raw module ID for the script.
*/
function rawModuleIdForScript(script: SDK.Script.Script): string {
return `${script.sourceURL}@${script.hash}`;
}
function getRawLocation(callFrame: SDK.DebuggerModel.CallFrame): Chrome.DevTools.RawLocation {
const {script} = callFrame;
return {
rawModuleId: rawModuleIdForScript(script),
codeOffset: callFrame.location().columnNumber - (script.codeOffset() || 0),
inlineFrameIndex: callFrame.inlineFrameIndex,
};
}
async function resolveRemoteObject(
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
callFrame: SDK.DebuggerModel.CallFrame, object: SDK.RemoteObject.RemoteObject): Promise<any> {
if (typeof object.value !== 'undefined') {
return object.value;
}
const response = await callFrame.debuggerModel.target().runtimeAgent().invoke_callFunctionOn(
{functionDeclaration: 'function() { return this; }', objectId: object.objectId, returnByValue: true});
const {result} = response;
if (!result) {
return undefined;
}
return result.value;
}
export class ValueNode extends SDK.RemoteObject.RemoteObjectImpl {
inspectableAddress?: number;
callFrame: SDK.DebuggerModel.CallFrame;
constructor(
callFrame: SDK.DebuggerModel.CallFrame, objectId: Protocol.Runtime.RemoteObjectId|undefined, type: string,
subtype: string|undefined,
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any, inspectableAddress?: number, unserializableValue?: string, description?: string,
preview?: Protocol.Runtime.ObjectPreview, customPreview?: Protocol.Runtime.CustomPreview, className?: string) {
super(
callFrame.debuggerModel.runtimeModel(), objectId, type, subtype, value, unserializableValue, description,
preview, customPreview, className);
this.inspectableAddress = inspectableAddress;
this.callFrame = callFrame;
}
get sourceType(): SourceType {
throw new Error('Not Implemented');
}
}
// Debugger language #plugins present source-language values as trees with mixed dynamic and static structural
// information. The static structure is defined by the variable's static type in the source language. Formatters are
// able to present source-language values in an arbitrary user-friendly way, which contributes the dynamic structural
// information. The classes StaticallyTypedValue and FormatedValueNode respectively implement the static and dynamic
// parts in the RemoteObject tree that defines the presentation of the source-language value in the debugger UI.
//
// struct S {
// int i;
// struct A {
// int j;
// } a[3];
// } s
//
// The RemoteObject tree representing the C struct above could look like the graph below with a formatter for the type
// struct A[3], interleaving static and dynamic representations:
//
// StaticallyTypedValueNode --> s: struct S
// \
// |\
// StaticallyTypedValueNode --> | i: int
// \
// \
// StaticallyTypedValueNode --> a: struct A[3]
// \
// |\
// FormattedValueNode --> | 0: struct A
// | \
// | \
// StaticallyTypedValueNode --> | j: int
// .
// .
// .
/** Create a new value tree from an expression.
*/
async function getValueTreeForExpression(
callFrame: SDK.DebuggerModel.CallFrame, plugin: DebuggerLanguagePlugin, expression: string,
evalOptions: SDK.RuntimeModel.EvaluationOptions): Promise<SDK.RemoteObject.RemoteObject> {
const location = getRawLocation(callFrame);
let typeInfo;
try {
typeInfo = await plugin.getTypeInfo(expression, location);
} catch (e) {
throw FormattingError.makeLocal(callFrame, e.message);
}
// If there's no type information we cannot represent this expression.
if (!typeInfo) {
return new SDK.RemoteObject.LocalJSONObject(undefined);
}
const {base, typeInfos} = typeInfo;
const sourceType = SourceType.create(typeInfos);
if (!sourceType) {
return new SDK.RemoteObject.LocalJSONObject(undefined);
}
if (sourceType.typeInfo.hasValue && !sourceType.typeInfo.canExpand && base) {
// Need to run the formatter for the expression result.
return formatSourceValue(callFrame, plugin, sourceType, base, [], evalOptions);
}
// Create a new value tree with static information for the root.
const address = await StaticallyTypedValueNode.getInspectableAddress(callFrame, plugin, base, [], evalOptions);
return new StaticallyTypedValueNode(callFrame, plugin, sourceType, base, [], evalOptions, address);
}
/** Run the formatter for the value defined by the pair of #base and #fieldChain.
*/
async function formatSourceValue(
callFrame: SDK.DebuggerModel.CallFrame, plugin: DebuggerLanguagePlugin, sourceType: SourceType,
base: Chrome.DevTools.EvalBase, field: Chrome.DevTools.FieldInfo[],
evalOptions: SDK.RuntimeModel.EvaluationOptions): Promise<FormattedValueNode> {
const location = getRawLocation(callFrame);
let evalCode: {
js: string,
}|({
js: string,
} | null) = await plugin.getFormatter({base, field}, location);
if (!evalCode) {
evalCode = {js: ''};
}
const response = await callFrame.debuggerModel.target().debuggerAgent().invoke_evaluateOnCallFrame({
callFrameId: callFrame.id,
expression: evalCode.js,
objectGroup: evalOptions.objectGroup,
includeCommandLineAPI: evalOptions.includeCommandLineAPI,
silent: evalOptions.silent,
returnByValue: evalOptions.returnByValue,
generatePreview: evalOptions.generatePreview,
throwOnSideEffect: evalOptions.throwOnSideEffect,
timeout: evalOptions.timeout,
});
const error = response.getError();
if (error) {
throw new Error(error);
}
const {result, exceptionDetails} = response;
if (exceptionDetails) {
throw new FormattingError(callFrame.debuggerModel.runtimeModel().createRemoteObject(result), exceptionDetails);
}
// Wrap the formatted result into a FormattedValueNode.
const object = new FormattedValueNode(callFrame, sourceType, plugin, result, null, evalOptions, undefined);
// Check whether the formatter returned a plain object or and object alongisde a formatter tag.
const unpackedResultObject = await unpackResultObject(object);
const node = unpackedResultObject || object;
if (typeof node.value === 'undefined' && node.type !== 'undefined') {
node.description = sourceType.typeInfo.typeNames[0];
}
return node;
async function unpackResultObject(object: FormattedValueNode): Promise<FormattedValueNode|null> {
const {tag, value, inspectableAddress, description} =
await object.findProperties('tag', 'value', 'inspectableAddress', 'description');
if (!tag || !value) {
return null;
}
const {className, symbol} = await tag.findProperties('className', 'symbol');
if (!className || !symbol) {
return null;
}
const resolvedClassName = className.value;
if (typeof resolvedClassName !== 'string' || typeof symbol.objectId === 'undefined') {
return null;
}
const descriptionText = description?.value;
if (typeof descriptionText === 'string') {
value.description = descriptionText;
}
value.formatterTag = {symbol: symbol.objectId, className: resolvedClassName};
value.inspectableAddress = inspectableAddress ? inspectableAddress.value : undefined;
return value;
}
}
// Formatters produce proper JavaScript objects, which are mirrored as RemoteObjects. To implement interleaving of
// formatted and statically typed values, formatters may insert markers in the JavaScript objects. The markers contain
// the static type information (`EvalBase`)to create a new StaticallyTypedValueNode tree root. Markers are identified by
// their className and the presence of a special Symbol property. Both the class name and the symbol are defined by the
// `formatterTag` property.
//
// A FormattedValueNode is a RemoteObject whose properties can be either FormattedValueNodes or
// StaticallyTypedValueNodes. The class hooks into the creation of RemoteObjects for properties to check whether a
// property is a marker.
class FormattedValueNode extends ValueNode {
readonly #plugin: DebuggerLanguagePlugin;
readonly #sourceType: SourceType;
formatterTag: {
className: string,
symbol: string,
}|null;
readonly #evalOptions: SDK.RuntimeModel.EvaluationOptions;
constructor(
callFrame: SDK.DebuggerModel.CallFrame, sourceType: SourceType, plugin: DebuggerLanguagePlugin,
object: Protocol.Runtime.RemoteObject, formatterTag: {
className: string,
symbol: string,
}|null,
evalOptions: SDK.RuntimeModel.EvaluationOptions, inspectableAddress: number|undefined) {
super(
callFrame, object.objectId, object.type, object.subtype, object.value, inspectableAddress,
object.unserializableValue, object.description, object.preview, object.customPreview, object.className);
this.#plugin = plugin;
this.#sourceType = sourceType;
// The tag describes how to identify a marker by its className and its identifier symbol's object id.
this.formatterTag = formatterTag;
this.#evalOptions = evalOptions;
}
async findProperties(...properties: string[]): Promise<{
[x: string]: FormattedValueNode | undefined,
}> {
const result: {
[x: string]: FormattedValueNode|undefined,
} = {};
for (const prop of (await this.getOwnProperties(false)).properties || []) {
if (properties.indexOf(prop.name) >= 0) {
if (prop.value) {
result[prop.name] = (prop.value as FormattedValueNode | undefined);
}
}
}
return result;
}
/**
* Hook into RemoteObject creation for properties to check whether a property is a marker.
*/
async createRemoteObject(newObject: Protocol.Runtime.RemoteObject):
Promise<FormattedValueNode|StaticallyTypedValueNode> {
// Check if the property RemoteObject is a marker
const base = await this.getEvalBaseFromObject(newObject);
if (!base) {
return new FormattedValueNode(
this.callFrame, this.#sourceType, this.#plugin, newObject, this.formatterTag, this.#evalOptions, undefined);
}
// Property is a marker, check if it's just static type information or if we need to run formatters for the value.
const newSourceType = this.#sourceType.typeMap.get(base.rootType.typeId);
if (!newSourceType) {
throw new Error('Unknown typeId in eval base');
}
// The marker refers to a value that needs to be formatted, so run the formatter.
if (base.rootType.hasValue && !base.rootType.canExpand && base) {
return formatSourceValue(this.callFrame, this.#plugin, newSourceType, base, [], this.#evalOptions);
}
// The marker is just static information, so start a new subtree with a static type info root.
const address =
await StaticallyTypedValueNode.getInspectableAddress(this.callFrame, this.#plugin, base, [], this.#evalOptions);
return new StaticallyTypedValueNode(
this.callFrame, this.#plugin, newSourceType, base, [], this.#evalOptions, address);
}
/**
* Check whether an object is a marker and if so return the EvalBase it contains.
*/
private async getEvalBaseFromObject(object: Protocol.Runtime.RemoteObject): Promise<Chrome.DevTools.EvalBase|null> {
const {objectId} = object;
if (!object || !this.formatterTag) {
return null;
}
// A marker is definitively identified by the symbol property. To avoid checking the properties of all objects,
// check the className first for an early exit.
const {className, symbol} = this.formatterTag;
if (className !== object.className) {
return null;
}
const response = await this.debuggerModel().target().runtimeAgent().invoke_callFunctionOn({
functionDeclaration: 'function(sym) { return this[sym]; }',
objectId,
arguments: [{objectId: symbol as Protocol.Runtime.RemoteObjectId}],
});
const {result} = response;
if (!result || result.type === 'undefined') {
return null;
}
// The object is a marker, so pull the static type information from its symbol property. The symbol property is not
// a formatted value per se, but we wrap it as one to be able to call `findProperties`.
const baseObject = new FormattedValueNode(
this.callFrame, this.#sourceType, this.#plugin, result, null, this.#evalOptions, undefined);
const {payload, rootType} = await baseObject.findProperties('payload', 'rootType');
if (typeof payload === 'undefined' || typeof rootType === 'undefined') {
return null;
}
const value = await resolveRemoteObject(this.callFrame, payload);
const {typeId} = await rootType.findProperties('typeId');
if (typeof value === 'undefined' || typeof typeId === 'undefined') {
return null;
}
const newSourceType = this.#sourceType.typeMap.get(typeId.value);
if (!newSourceType) {
return null;
}
return {payload: value, rootType: newSourceType.typeInfo};
}
}
class FormattingError extends Error {
exception: SDK.RemoteObject.RemoteObject;
exceptionDetails: Protocol.Runtime.ExceptionDetails;
constructor(exception: SDK.RemoteObject.RemoteObject, exceptionDetails: Protocol.Runtime.ExceptionDetails) {
const {description} = exceptionDetails.exception || {};
super(description || exceptionDetails.text);
this.exception = exception;
this.exceptionDetails = exceptionDetails;
}
static makeLocal(callFrame: SDK.DebuggerModel.CallFrame, message: string): FormattingError {
const exception: Protocol.Runtime.RemoteObject = {
type: Protocol.Runtime.RemoteObjectType.Object,
subtype: Protocol.Runtime.RemoteObjectSubtype.Error,
description: message,
};
const exceptionDetails: Protocol.Runtime
.ExceptionDetails = {text: 'Uncaught', exceptionId: -1, columnNumber: 0, lineNumber: 0, exception};
const errorObject = callFrame.debuggerModel.runtimeModel().createRemoteObject(exception);
return new FormattingError(errorObject, exceptionDetails);
}
}
// This class implements a `RemoteObject` for source language value whose immediate properties are defined purely by
// static type information. Static type information is expressed by an `EvalBase` together with a `#fieldChain`. The
// latter is necessary to express navigating through type members. We don't know how to make sense of an `EvalBase`'s
// payload here, which is why member navigation is relayed to the formatter via the `#fieldChain`.
class StaticallyTypedValueNode extends ValueNode {
readonly #variableType: string;
readonly #plugin: DebuggerLanguagePlugin;
readonly #sourceType: SourceType;
readonly #base: Chrome.DevTools.EvalBase|null;
readonly #fieldChain: Chrome.DevTools.FieldInfo[];
readonly #evalOptions: SDK.RuntimeModel.EvaluationOptions;
constructor(
callFrame: SDK.DebuggerModel.CallFrame, plugin: DebuggerLanguagePlugin, sourceType: SourceType,
base: Chrome.DevTools.EvalBase|null, fieldChain: Chrome.DevTools.FieldInfo[],
evalOptions: SDK.RuntimeModel.EvaluationOptions, inspectableAddress: number|undefined) {
const typeName = sourceType.typeInfo.typeNames[0] || '<anonymous>';
const variableType = 'object';
super(
callFrame,
/* objectId=*/ undefined,
/* type=*/ variableType,
/* subtype=*/ undefined, /* value=*/ null, inspectableAddress, /* unserializableValue=*/ undefined,
/* description=*/ typeName, /* preview=*/ undefined, /* customPreview=*/ undefined, /* className=*/ typeName);
this.#variableType = variableType;
this.#plugin = plugin;
this.#sourceType = sourceType;
this.#base = base;
this.#fieldChain = fieldChain;
this.hasChildrenInternal = true;
this.#evalOptions = evalOptions;
}
get type(): string {
return this.#variableType;
}
get sourceType(): SourceType {
return this.#sourceType;
}
private async expandMember(sourceType: SourceType, fieldInfo: Chrome.DevTools.FieldInfo):
Promise<SDK.RemoteObject.RemoteObject> {
const fieldChain = this.#fieldChain.concat(fieldInfo);
if (sourceType.typeInfo.hasValue && !sourceType.typeInfo.canExpand && this.#base) {
return formatSourceValue(this.callFrame, this.#plugin, sourceType, this.#base, fieldChain, this.#evalOptions);
}
const address = this.inspectableAddress !== undefined ? this.inspectableAddress + fieldInfo.offset : undefined;
return new StaticallyTypedValueNode(
this.callFrame, this.#plugin, sourceType, this.#base, fieldChain, this.#evalOptions, address);
}
static async getInspectableAddress(
callFrame: SDK.DebuggerModel.CallFrame, plugin: DebuggerLanguagePlugin, base: Chrome.DevTools.EvalBase|null,
field: Chrome.DevTools.FieldInfo[], evalOptions: SDK.RuntimeModel.EvaluationOptions): Promise<number|undefined> {
if (!base) {
return undefined;
}
const addressCode = await plugin.getInspectableAddress({base, field});
if (!addressCode.js) {
return undefined;
}
const response = await callFrame.debuggerModel.target().debuggerAgent().invoke_evaluateOnCallFrame({
callFrameId: callFrame.id,
expression: addressCode.js,
objectGroup: evalOptions.objectGroup,
includeCommandLineAPI: evalOptions.includeCommandLineAPI,
silent: evalOptions.silent,
returnByValue: true,
generatePreview: evalOptions.generatePreview,
throwOnSideEffect: evalOptions.throwOnSideEffect,
timeout: evalOptions.timeout,
});
const error = response.getError();
if (error) {
throw new Error(error);
}
const {result, exceptionDetails} = response;
if (exceptionDetails) {
throw new FormattingError(callFrame.debuggerModel.runtimeModel().createRemoteObject(result), exceptionDetails);
}
const address = result.value;
if (!Number.isSafeInteger(address) || address < 0) {
console.error(`Inspectable address is not a positive, safe integer: ${address}`);
return undefined;
}
return address;
}
async doGetProperties(_ownProperties: boolean, accessorPropertiesOnly: boolean, _generatePreview: boolean):
Promise<SDK.RemoteObject.GetPropertiesResult> {
const {typeInfo} = this.#sourceType;
if (accessorPropertiesOnly || !typeInfo.canExpand) {
return {properties: [], internalProperties: []} as SDK.RemoteObject.GetPropertiesResult;
}
if (typeInfo.members.length > 0) {
// This value doesn't have a formatter, but we can eagerly expand arrays in the frontend if the size is known.
if (typeInfo.arraySize > 0) {
const {typeId} = this.#sourceType.typeInfo.members[0];
const properties: SDK.RemoteObject.RemoteObjectProperty[] = [];
const elementTypeInfo = this.#sourceType.members[0];
for (let i = 0; i < typeInfo.arraySize; ++i) {
const name = `${i}`;
const elementField = {name, typeId, offset: elementTypeInfo.typeInfo.size * i};
properties.push(new SDK.RemoteObject.RemoteObjectProperty(
name, await this.expandMember(elementTypeInfo, elementField), /* enumerable=*/ false,
/* writable=*/ false,
/* isOwn=*/ true,
/* wasThrown=*/ false));
}
return {properties, internalProperties: []} as SDK.RemoteObject.GetPropertiesResult;
}
// The node is expanded, just make remote objects for its members
const members = Promise.all(this.#sourceType.members.map(async (memberTypeInfo, idx) => {
const fieldInfo = this.#sourceType.typeInfo.members[idx];
const propertyObject = await this.expandMember(memberTypeInfo, fieldInfo);
const name = fieldInfo.name || '';
return new SDK.RemoteObject.RemoteObjectProperty(
name, propertyObject, /* enumerable=*/ false, /* writable=*/ false, /* isOwn=*/ true,
/* wasThrown=*/ false);
}));
return {properties: await members, internalProperties: []} as SDK.RemoteObject.GetPropertiesResult;
}
return {properties: [], internalProperties: []} as SDK.RemoteObject.GetPropertiesResult;
}
}
class NamespaceObject extends SDK.RemoteObject.LocalJSONObject {
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(value: any) {
super(value);
}
get description(): string {
return this.type;
}
get type(): string {
return 'namespace';
}
}
class SourceScopeRemoteObject extends SDK.RemoteObject.RemoteObjectImpl {
variables: Chrome.DevTools.Variable[];
#callFrame: SDK.DebuggerModel.CallFrame;
#plugin: DebuggerLanguagePlugin;
stopId: StopId;
constructor(callFrame: SDK.DebuggerModel.CallFrame, stopId: StopId, plugin: DebuggerLanguagePlugin) {
super(callFrame.debuggerModel.runtimeModel(), undefined, 'object', undefined, null);
this.variables = [];
this.#callFrame = callFrame;
this.#plugin = plugin;
this.stopId = stopId;
}
async doGetProperties(ownProperties: boolean, accessorPropertiesOnly: boolean, _generatePreview: boolean):
Promise<SDK.RemoteObject.GetPropertiesResult> {
if (accessorPropertiesOnly) {
return {properties: [], internalProperties: []} as SDK.RemoteObject.GetPropertiesResult;
}
const properties = [];
const namespaces: {
[x: string]: SDK.RemoteObject.RemoteObject,
} = {};
function makeProperty(name: string, obj: SDK.RemoteObject.RemoteObject): SDK.RemoteObject.RemoteObjectProperty {
return new SDK.RemoteObject.RemoteObjectProperty(
name, obj,
/* enumerable=*/ false, /* writable=*/ false, /* isOwn=*/ true, /* wasThrown=*/ false);
}
for (const variable of this.variables) {
let sourceVar;
try {
sourceVar = await getValueTreeForExpression(this.#callFrame, this.#plugin, variable.name, ({
generatePreview: false,
includeCommandLineAPI: true,
objectGroup: 'backtrace',
returnByValue: false,
silent: false,
} as SDK.RuntimeModel.EvaluationOptions));
} catch (e) {
console.warn(e);
sourceVar = new SDK.RemoteObject.LocalJSONObject(undefined);
}
if (variable.nestedName && variable.nestedName.length > 1) {
let parent: {
[x: string]: SDK.RemoteObject.RemoteObject,
} = namespaces;
for (let index = 0; index < variable.nestedName.length - 1; index++) {
const nestedName = variable.nestedName[index];
let child: NamespaceObject|SDK.RemoteObject.RemoteObject = parent[nestedName];
if (!child) {
child = new NamespaceObject({});
parent[nestedName] = child;
}
parent = child.value;
}
const name = variable.nestedName[variable.nestedName.length - 1];
parent[name] = sourceVar;
} else {
properties.push(makeProperty(variable.name, sourceVar));
}
}
for (const namespace in namespaces) {
properties.push(makeProperty(namespace, (namespaces[namespace] as SDK.RemoteObject.RemoteObject)));
}
return {properties: properties, internalProperties: []};
}
}
export class SourceScope implements SDK.DebuggerModel.ScopeChainEntry {
readonly #callFrameInternal: SDK.DebuggerModel.CallFrame;
readonly #typeInternal: string;
readonly #typeNameInternal: string;
readonly #iconInternal: string|undefined;
readonly #objectInternal: SourceScopeRemoteObject;
readonly #startLocationInternal: SDK.DebuggerModel.Location|null;
readonly #endLocationInternal: SDK.DebuggerModel.Location|null;
constructor(
callFrame: SDK.DebuggerModel.CallFrame, stopId: StopId, type: string, typeName: string, icon: string|undefined,
plugin: DebuggerLanguagePlugin) {
this.#callFrameInternal = callFrame;
this.#typeInternal = type;
this.#typeNameInternal = typeName;
this.#iconInternal = icon;
this.#objectInternal = new SourceScopeRemoteObject(callFrame, stopId, plugin);
this.#startLocationInternal = null;
this.#endLocationInternal = null;
}
async getVariableValue(name: string): Promise<SDK.RemoteObject.RemoteObject|null> {
for (let v = 0; v < this.#objectInternal.variables.length; ++v) {
if (this.#objectInternal.variables[v].name !== name) {
continue;
}
const properties = await this.#objectInternal.getAllProperties(false, false);
if (!properties.properties) {
continue;
}
const {value} = properties.properties[v];
if (value) {
return value;
}
}
return null;
}
callFrame(): SDK.DebuggerModel.CallFrame {
return this.#callFrameInternal;
}
type(): string {
return this.#typeInternal;
}
typeName(): string {
return this.#typeNameInternal;
}
name(): string|undefined {
return undefined;
}
startLocation(): SDK.DebuggerModel.Location|null {
return this.#startLocationInternal;
}
endLocation(): SDK.DebuggerModel.Location|null {
return this.#endLocationInternal;
}
object(): SourceScopeRemoteObject {
return this.#objectInternal;
}
description(): string {
return '';
}
icon(): string|undefined {
return this.#iconInternal;
}
}
export class ExtensionRemoteObject extends SDK.RemoteObject.RemoteObject {
private readonly extensionObject: Chrome.DevTools.RemoteObject;
private readonly plugin: DebuggerLanguagePlugin;
readonly callFrame: SDK.DebuggerModel.CallFrame;
constructor(
callFrame: SDK.DebuggerModel.CallFrame, extensionObject: Chrome.DevTools.RemoteObject,
plugin: DebuggerLanguagePlugin) {
super();
this.extensionObject = extensionObject;
this.plugin = plugin;
this.callFrame = callFrame;
}
get linearMemoryAddress(): number|undefined {
return this.extensionObject.linearMemoryAddress;
}
get objectId(): Protocol.Runtime.RemoteObjectId|undefined {
return this.extensionObject.objectId as Protocol.Runtime.RemoteObjectId;
}
get type(): string {
if (this.extensionObject.type === 'array' || this.extensionObject.type === 'null') {
return 'object';
}
return this.extensionObject.type;
}
get subtype(): string|undefined {
if (this.extensionObject.type === 'array' || this.extensionObject.type === 'null') {
return this.extensionObject.type;
}
return undefined;
}
get value(): unknown {
return this.extensionObject.value;
}
unserializableValue(): string|undefined {
return undefined;
}
get description(): string|undefined {
return this.extensionObject.description;
}
set description(description: string|undefined) {
}
get hasChildren(): boolean {
return this.extensionObject.hasChildren;
}
get preview(): Protocol.Runtime.ObjectPreview|undefined {
return undefined;
}
get className(): string|null {
return this.extensionObject.className ?? null;
}
arrayLength(): number {
return 0;
}
arrayBufferByteLength(): number {
return 0;
}
getOwnProperties(_generatePreview: boolean, _nonIndexedPropertiesOnly?: boolean):
Promise<SDK.RemoteObject.GetPropertiesResult> {
return this.getAllProperties(false, _generatePreview, _nonIndexedPropertiesOnly);
}
async getAllProperties(
_accessorPropertiesOnly: boolean, _generatePreview: boolean,
_nonIndexedPropertiesOnly?: boolean): Promise<SDK.RemoteObject.GetPropertiesResult> {
const {objectId} = this.extensionObject;
if (objectId) {
assertNotNullOrUndefined(this.plugin.getProperties);
const extensionObjectProperties = await this.plugin.getProperties(objectId);
const properties = extensionObjectProperties.map(
p => new SDK.RemoteObject.RemoteObjectProperty(
p.name, new ExtensionRemoteObject(this.callFrame, p.value, this.plugin)));
return {properties, internalProperties: null};
}
return {properties: null, internalProperties: null};
}
release(): void {
const {objectId} = this.extensionObject;
if (objectId) {
assertNotNullOrUndefined(this.plugin.releaseObject);
void this.plugin.releaseObject(objectId);
}
}
debuggerModel(): SDK.DebuggerModel.DebuggerModel {
return this.callFrame.debuggerModel;
}
runtimeModel(): SDK.RuntimeModel.RuntimeModel {
return this.callFrame.debuggerModel.runtimeModel();
}
}
export type StopId = bigint;
export class DebuggerLanguagePluginManager implements
SDK.TargetManager.SDKModelObserver<SDK.DebuggerModel.DebuggerModel> {
readonly #workspace: Workspace.Workspace.WorkspaceImpl;
readonly #debuggerWorkspaceBinding: DebuggerWorkspaceBinding;
#plugins: DebuggerLanguagePlugin[];
readonly #debuggerModelToData: Map<SDK.DebuggerModel.DebuggerModel, ModelData>;
readonly #rawModuleHandles: Map<string, {
rawModuleId: string,
plugin: DebuggerLanguagePlugin,
scripts: Array<SDK.Script.Script>,
addRawModulePromise: Promise<Array<Platform.DevToolsPath.UrlString>|{missingSymbolFiles: string[]}>,
}>;
private readonly callFrameByStopId: Map<StopId, SDK.DebuggerModel.CallFrame> = new Map();
private readonly stopIdByCallFrame: Map<SDK.DebuggerModel.CallFrame, StopId> = new Map();
private nextStopId: StopId = 0n;
constructor(
targetManager: SDK.TargetManager.TargetManager, workspace: Workspace.Workspace.WorkspaceImpl,
debuggerWorkspaceBinding: DebuggerWorkspaceBinding) {
this.#workspace = workspace;
this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding;
this.#plugins = [];
this.#debuggerModelToData = new Map();
targetManager.observeModels(SDK.DebuggerModel.DebuggerModel, this);
this.#rawModuleHandles = new Map();
}
private async evaluateOnCallFrame(
callFrame: SDK.DebuggerModel.CallFrame, options: SDK.RuntimeModel.EvaluationOptions): Promise<{
object: SDK.RemoteObject.RemoteObject,
exceptionDetails: Protocol.Runtime.ExceptionDetails|undefined,
}|{
error: string,
}|null> {
const {script} = callFrame;
const {expression} = options;
const {plugin} = await this.rawModuleIdAndPluginForScript(script);
if (!plugin) {
return null;
}
const location = getRawLocation(callFrame);
const sourceLocations = await plugin.rawLocationToSourceLocation(location);
if (sourceLocations.length === 0) {
return null;
}
try {
const object = await getValueTreeForExpression(callFrame, plugin, expression, options);
return {object, exceptionDetails: undefined};
} catch (error) {
if (error instanceof FormattingError) {
const {exception: object, exceptionDetails} = error;
return {object, exceptionDetails};
}
const {exception: object, exceptionDetails} = FormattingError.makeLocal(callFrame, error.message);
return {object, exceptionDetails};
}
}
stopIdForCallFrame(callFrame: SDK.DebuggerModel.CallFrame): StopId {
let stopId = this.stopIdByCallFrame.get(callFrame);
if (stopId !== undefined) {
return stopId;
}
stopId = this.nextStopId++;
this.stopIdByCallFrame.set(callFrame, stopId);
this.callFrameByStopId.set(stopId, callFrame);
return stopId;
}
callFrameForStopId(stopId: StopId): SDK.DebuggerModel.CallFrame|undefined {
return this.callFrameByStopId.get(stopId);
}
private expandCallFrames(callFrames: SDK.DebuggerModel.CallFrame[]): Promise<SDK.DebuggerModel.CallFrame[]> {
return Promise
.all(callFrames.map(async callFrame => {
const functionInfo = await this.getFunctionInfo(callFrame.script, callFrame.location());
if (functionInfo) {
if ('frames' in functionInfo && functionInfo.frames.length) {
return functionInfo.frames.map(({name}, index) => callFrame.createVirtualCallFrame(index, name));
}
if ('missingSymbolFiles' in functionInfo && functionInfo.missingSymbolFiles.length) {
const resources = functionInfo.missingSymbolFiles;
const details = i18nString(UIStrings.debugSymbolsIncomplete, {PH1: callFrame.functionName});
callFrame.setMissingDebugInfoDetails({details, resources});
} else {
callFrame.setMissingDebugInfoDetails({
resources: [],
details: i18nString(UIStrings.failedToLoadDebugSymbolsForFunction, {PH1: callFrame.functionName}),
});
}
}
return callFrame;
}))
.then(callFrames => callFrames.flat());
}
modelAdded(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
this.#debuggerModelToData.set(debuggerModel, new ModelData(debuggerModel, this.#workspace));
debuggerModel.addEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this);
debuggerModel.addEventListener(SDK.DebuggerModel.Events.ParsedScriptSource, this.parsedScriptSource, this);
debuggerModel.addEventListener(SDK.DebuggerModel.Events.DebuggerResumed, this.debuggerResumed, this);
debuggerModel.setEvaluateOnCallFrameCallback(this.evaluateOnCallFrame.bind(this));
debuggerModel.setExpandCallFramesCallback(this.expandCallFrames.bind(this));
}
modelRemoved(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
debuggerModel.removeEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this);
debuggerModel.removeEventListener(SDK.DebuggerModel.Events.ParsedScriptSource, this.parsedScriptSource, this);
debuggerModel.removeEventListener(SDK.DebuggerModel.Events.DebuggerResumed, this.debuggerResumed, this);
debuggerModel.setEvaluateOnCallFrameCallback(null);
debuggerModel.setExpandCallFramesCallback(null);
const modelData = this.#debuggerModelToData.get(debuggerModel);
if (modelData) {
modelData.dispose();
this.#debuggerModelToData.delete(debuggerModel);
}
this.#rawModuleHandles.forEach((rawModuleHandle, rawModuleId) => {
const scripts = rawModuleHandle.scripts.filter(script => script.debuggerModel !== debuggerModel);
if (scripts.length === 0) {
rawModuleHandle.plugin.removeRawModule(rawModuleId).catch(error => {
Common.Console.Console.instance().error(
i18nString(UIStrings.errorInDebuggerLanguagePlugin, {PH1: error.message}));
});
this.#rawModuleHandles.delete(rawModuleId);
} else {
rawModuleHandle.scripts = scripts;
}
});
}
private globalObjectCleared(event: Common.EventTarget.EventTargetEvent<SDK.DebuggerModel.DebuggerModel>): void {
const debuggerModel = event.data;
this.modelRemoved(debuggerModel);
this.modelAdded(debuggerModel);
}
addPlugin(plugin: DebuggerLanguagePlugin): void {
this.#plugins.push(plugin);
for (const debuggerModel of this.#debuggerModelToData.keys()) {
for (const script of debuggerModel.scripts()) {
if (this.hasPluginForScript(script)) {
continue;
}
this.parsedScriptSource({data: script});
}
}
}
removePlugin(plugin: DebuggerLanguagePlugin): void {
this.#plugins = this.#plugins.filter(p => p !== plugin);
const scripts = new Set<SDK.Script.Script>();
this.#rawModuleHandles.forEach((rawModuleHandle, rawModuleId) => {
if (rawModuleHandle.plugin !== plugin) {
return;
}
rawModuleHandle.scripts.forEach(script => scripts.add(script));
this.#rawModuleHandles.delete(rawModuleId);
});
for (const script of scripts) {
const modelData = (this.#debuggerModelToData.get(script.debuggerModel) as ModelData);
modelData.removeScript(script);
// Let's see if we have another #plugin that's happy to
// take this orphaned script now. This is important to
// get right, since the same #plugin might race during
// unregister/register and we might already have the
// new instance of the #plugin added before we remove
// the previous instance.
this.parsedScriptSource({data: script});
}
}
hasPluginForScript(script: SDK.Script.Script): boolean {
const rawModuleId = rawModuleIdForScript(script);
const rawModuleHandle = this.#rawModuleHandles.get(rawModuleId);
return rawModuleHandle !== undefined && rawModuleHandle.scripts.includes(script);
}
/**
* Returns the responsible language #plugin and the raw module ID for a script.
*
* This ensures that the `addRawModule` call finishes first such that the
* caller can immediately issue calls to the returned #plugin without the
* risk of racing with the `addRawModule` call. The returned #plugin will be
* set to undefined to indicate that there's no #plugin for the script.
*/
private async rawModuleIdAndPluginForScript(script: SDK.Script.Script): Promise<{
rawModuleId: string,
plugin: DebuggerLanguagePlugin|null,
}> {
const rawModuleId = rawModuleIdForScript(script);
const rawModuleHandle = this.#rawModuleHandles.get(rawModuleId);
if (rawModuleHandle) {
await rawModuleHandle.addRawModulePromise;
if (rawModuleHandle === this.#rawModuleHandles.get(rawModuleId)) {
return {rawModuleId, plugin: rawModuleHandle.plugin};
}
}
return {rawModuleId, plugin: null};
}
uiSourceCodeForURL(debuggerModel: SDK.DebuggerModel.DebuggerModel, url: Platform.DevToolsPath.UrlString):
Workspace.UISourceCode.UISourceCode|null {
const modelData = this.#debuggerModelToData.get(debuggerModel);
if (modelData) {
return modelData.getProject().uiSourceCodeForURL(url);
}
return null;
}
async rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location):
Promise<Workspace.UISourceCode.UILocation|null> {
const script = rawLocation.script();
if (!script) {
return null;
}
const {rawModuleId, plugin} = await this.rawModuleIdAndPluginForScript(script);
if (!plugin) {
return null;
}
const pluginLocation = {
rawModuleId,
// RawLocation.#columnNumber is the byte offset in the full raw wasm module. Plugins expect the offset in the code
// section, so subtract the offset of the code section in the module here.
codeOffset: rawLocation.columnNumber - (script.codeOffset() || 0),
inlineFrameIndex: rawLocation.inlineFrameIndex,
};
try {
const sourceLocations = await plugin.rawLocationToSourceLocation(pluginLocation);
for (const sourceLocation of sourceLocations) {
const uiSourceCode = this.uiSourceCodeForURL(
script.debuggerModel, sourceLocation.sourceFileURL as Platform.DevToolsPath.UrlString);
if (!uiSourceCode) {
continue;
}
// Absence of column information is indicated by the value `-1` in talking to language #plugins.
return uiSourceCode.uiLocation(
sourceLocation.lineNumber, sourceLocation.columnNumber >= 0 ? sourceLocation.columnNumber : undefined);
}
} catch (error) {
Common.Console.Console.instance().error(
i18nString(UIStrings.errorInDebuggerLanguagePlugin, {PH1: error.message}));
}
return null;
}
uiLocationToRawLocationRanges(
uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number,
columnNumber: number|undefined = -1): Promise<{
start: SDK.DebuggerModel.Location,
end: SDK.DebuggerModel.Location,
}[]|null> {
const locationPromises: Promise<{
start: SDK.DebuggerModel.Location,
end: SDK.DebuggerModel.Location,
}[]>[] = [];
this.scriptsForUISourceCode(uiSourceCode).forEach(script => {
const rawModuleId = rawModuleIdForScript(script);
const rawModuleHandle = this.#rawModuleHandles.get(rawModuleId);
if (!rawModuleHandle) {
return;
}
const {plugin} = rawModuleHandle;
locationPromises.push(getLocations(rawModuleId, plugin, script));
});
if (locationPromises.length === 0) {
return Promise.resolve(null);
}
return Promise.all(locationPromises).then(locations => locations.flat()).catch(error => {
Common.Console.Console.instance().error(
i18nString(UIStrings.errorInDebuggerLanguagePlugin, {PH1: error.message}));
return null;
});
async function getLocations(
rawModuleId: string, plugin: DebuggerLanguagePlugin, script: SDK.Script.Script): Promise<{
start: SDK.DebuggerModel.Location,
end: SDK.DebuggerModel.Location,
}[]> {
const pluginLocation = {rawModuleId, sourceFileURL: uiSourceCode.url(), lineNumber, columnNumber};
const rawLocations = await plugin.sourceLocationToRawLocation(pluginLocation);
if (!rawLocations) {
return [];
}
return rawLocations.map(
m => ({
start: new SDK.DebuggerModel.Location(
script.debuggerModel, script.scriptId, 0, Number(m.startOffset) + (script.codeOffset() || 0)),
end: new SDK.DebuggerModel.Location(
script.debuggerModel, script.scriptId, 0, Number(m.endOffset) + (script.codeOffset() || 0)),
}));
}
}
async uiLocationToRawLocations(
uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number,
columnNumber?: number): Promise<SDK.DebuggerModel.Location[]|null> {
const locationRanges = await this.uiLocationToRawLocationRanges(uiSourceCode, lineNumber, columnNumber);
if (!locationRanges) {
return null;
}
return locationRanges.map(({start}) => start);
}
scriptsForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): SDK.Script.Script[] {
for (const modelData of this.#debuggerModelToData.values()) {
const scripts = modelData.uiSourceCodeToScripts.get(uiSourceCode);
if (scripts) {
return scripts;
}
}
return [];
}
setDebugInfoURL(script: SDK.Script.Script, externalURL: Platform.DevToolsPath.UrlString): void {
if (this.hasPluginForScript(script)) {
return;
}
script.debugSymbols = {type: Protocol.Debugger.DebugSymbolsType.ExternalDWARF, externalURL};
this.parsedScriptSource({data: script});
void script.debuggerModel.setDebugInfoURL(script, externalURL);
}
private parsedScriptSource(event: Common.EventTarget.EventTargetEvent<SDK.Script.Script>): void {
const script = event.data;
if (!script.sourceURL) {
return;
}
for (const plugin of this.#plugins) {
if (!plugin.handleScript(script)) {
return;
}
const rawModuleId = rawModuleIdForScript(script);
let rawModuleHandle = this.#rawModuleHandles.get(rawModuleId);
if (!rawModuleHandle) {
const sourceFileURLsPromise =
(async(): Promise<Platform.DevToolsPath.UrlString[]|{missingSymbolFiles: string[]}> => {
const console = Common.Console.Console.instance();
const url = script.sourceURL;
const symbolsUrl = (script.debugSymbols && script.debugSymbols.externalURL) || '';
if (symbolsUrl) {
console.log(
i18nString(UIStrings.loadingDebugSymbolsForVia, {PH1: plugin.name, PH2: url, PH3: symbolsUrl}));
} else {
console.log(i18nString(UIStrings.loadingDebugSymbolsFor, {PH1: plugin.name, PH2: url}));
}
try {
const code = (!symbolsUrl && url.startsWith('wasm://')) ? await script.getWasmBytecode() : undefined;
const addModuleResult = await plugin.addRawModule(rawModuleId, symbolsUrl, {url, code});
// Check that the handle isn't stale by now. This works because the code that assigns to
// `rawModuleHandle` below will run before this code because of the `await` in the preceding
// line. This is primarily to avoid logging the message below, which would give the developer
// the misleading information that