UNPKG

chrome-devtools-frontend

Version:
1,377 lines (1,258 loc) • 59.4 kB
// 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 '../common/common.js'; import {ls} from '../platform/platform.js'; import * as SDK from '../sdk/sdk.js'; import * as Workspace from '../workspace/workspace.js'; import {ContentProviderBasedProject} from './ContentProviderBasedProject.js'; import {DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js'; // eslint-disable-line no-unused-vars import {NetworkProject} from './NetworkProject.js'; class SourceType { /** * @param {!TypeInfo} typeInfo * @param {!Array<!SourceType>} members * @param {!Map<*, !SourceType>} typeMap */ constructor(typeInfo, members, typeMap) { this.typeInfo = typeInfo; this.members = members; this.typeMap = typeMap; } /** Create a type graph * @param {!Array<!TypeInfo>} typeInfos * @return {?SourceType} */ static create(typeInfos) { if (typeInfos.length === 0) { return null; } /** @type Map<*, !SourceType> */ const typeMap = new Map(); 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 {!SDK.Script.Script} script * @return the unique raw module ID for the script. */ function rawModuleIdForScript(script) { return `${script.sourceURL}@${script.hash}`; } /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @return {!RawLocation} */ function getRawLocation(callFrame) { const {script} = callFrame; return { rawModuleId: rawModuleIdForScript(script), codeOffset: callFrame.location().columnNumber - (script.codeOffset() || 0), inlineFrameIndex: callFrame.inlineFrameIndex }; } /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {!SDK.RemoteObject.RemoteObject} object * @return {!Promise<*>} */ async function resolveRemoteObject(callFrame, object) { 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 { /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {string|undefined} objectId * @param {string} type * @param {string|undefined} subtype * @param {*} value * @param {number|undefined} inspectableAddress * @param {!Protocol.Runtime.UnserializableValue=} unserializableValue * @param {string=} description * @param {!Protocol.Runtime.ObjectPreview=} preview * @param {!Protocol.Runtime.CustomPreview=} customPreview * @param {string=} className */ constructor( callFrame, objectId, type, subtype, value, inspectableAddress, unserializableValue, description, preview, customPreview, className) { super( callFrame.debuggerModel.runtimeModel(), objectId, type, subtype, value, unserializableValue, description, preview, customPreview, className); this.inspectableAddress = inspectableAddress; this.callFrame = callFrame; } } // 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. * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {!DebuggerLanguagePlugin} plugin * @param {string} expression * @param {!SDK.RuntimeModel.EvaluationOptions} evalOptions * @return {!Promise<!SDK.RemoteObject.RemoteObject>} */ async function getValueTreeForExpression(callFrame, plugin, expression, evalOptions) { const location = getRawLocation(callFrame); let typeInfo; try { typeInfo = await plugin.getTypeInfo(expression, location); } catch (e) { FormattingError.throwLocal(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. * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {!DebuggerLanguagePlugin} plugin * @param {!SourceType} sourceType * @param {!EvalBase} base * @param {!Array<!FieldInfo>} field * @param {!SDK.RuntimeModel.EvaluationOptions} evalOptions * @return {!Promise<!FormattedValueNode>} */ async function formatSourceValue(callFrame, plugin, sourceType, base, field, evalOptions) { const location = getRawLocation(callFrame); let evalCode = 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; /** * @param {!FormattedValueNode} object * @return {!Promise<?FormattedValueNode>} */ async function unpackResultObject(object) { const {tag, value, inspectableAddress} = await object.findProperties('tag', 'value', 'inspectableAddress'); 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; } 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 { /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {!DebuggerLanguagePlugin} plugin * @param {!SourceType} sourceType * @param {!Protocol.Runtime.RemoteObject} object * @param {?{className: string, symbol: string}} formatterTag * @param {!SDK.RuntimeModel.EvaluationOptions} evalOptions * @param {number|undefined} inspectableAddress */ constructor(callFrame, sourceType, plugin, object, formatterTag, evalOptions, inspectableAddress) { 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. /** @type {?{className: string, symbol: string }} */ this.formatterTag = formatterTag; this._evalOptions = evalOptions; } /** * @param {...string} properties * @return {!Promise<!Object<string, !FormattedValueNode|undefined>>} */ async findProperties(...properties) { /** @type {!Object<string, !FormattedValueNode|undefined>} */ const result = {}; for (const prop of (await this.getOwnProperties(false)).properties || []) { if (properties.indexOf(prop.name) >= 0) { if (prop.value) { result[prop.name] = /** @type {!FormattedValueNode|undefined} */ (prop.value); } } } return result; } /** * Hook into RemoteObject creation for properties to check whether a property is a marker. * @override * @param {!Protocol.Runtime.RemoteObject} newObject */ async _createRemoteObject(newObject) { // 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. * @param {!Protocol.Runtime.RemoteObject} object * @return {!Promise<?EvalBase>} */ async _getEvalBaseFromObject(object) { 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}]}); 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', 'rootType'); 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 { /** * @param {!SDK.RemoteObject.RemoteObject} exception * @param {!Protocol.Runtime.ExceptionDetails} exceptionDetails */ constructor(exception, exceptionDetails) { const {description} = exceptionDetails.exception || {}; super(description || exceptionDetails.text); this.exception = exception; this.exceptionDetails = exceptionDetails; } /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {string} message */ static throwLocal(callFrame, message) { /** @type {!Protocol.Runtime.RemoteObject} */ const exception = { type: Protocol.Runtime.RemoteObjectType.Object, subtype: Protocol.Runtime.RemoteObjectSubtype.Error, description: message }; /** @type {!Protocol.Runtime.ExceptionDetails} */ const exceptionDetails = {text: 'Uncaught', exceptionId: -1, columnNumber: 0, lineNumber: 0, exception}; const errorObject = callFrame.debuggerModel.runtimeModel().createRemoteObject(exception); throw 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 { /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {!DebuggerLanguagePlugin} plugin * @param {!SourceType} sourceType The source type for this node. * @param {?EvalBase} base Base type information for the root of the current statically typed subtree. * @param {!Array<!FieldInfo>} fieldChain A sequence of `FieldInfo`s gathered on the path from the base to this node. * @param {!SDK.RuntimeModel.EvaluationOptions} evalOptions * @param {number|undefined} inspectableAddress */ constructor(callFrame, plugin, sourceType, base, fieldChain, evalOptions, inspectableAddress) { 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; /** @type {!SourceType} */ this._sourceType = sourceType; this._base = base; this._fieldChain = fieldChain; this._hasChildren = true; this._evalOptions = evalOptions; } /** * @override * @return {string} */ get type() { return this._variableType; } /** * @param {!SourceType} sourceType * @param {!FieldInfo} fieldInfo * @return {!Promise<!SDK.RemoteObject.RemoteObject>} */ async _expandMember(sourceType, fieldInfo) { 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); } /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {!DebuggerLanguagePlugin} plugin * @param {EvalBase?} base * @param {!Array<!FieldInfo>} field * @param {!SDK.RuntimeModel.EvaluationOptions} evalOptions * @return {!Promise<number|undefined>} */ static async getInspectableAddress(callFrame, plugin, base, field, evalOptions) { 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; } /** * @override * @param {boolean} ownProperties * @param {boolean} accessorPropertiesOnly * @param {boolean} generatePreview * @return {!Promise<!SDK.RemoteObject.GetPropertiesResult>} */ async doGetProperties(ownProperties, accessorPropertiesOnly, generatePreview) { const {typeInfo} = this._sourceType; if (accessorPropertiesOnly || !typeInfo.canExpand) { return /** @type {!SDK.RemoteObject.GetPropertiesResult} */ ({properties: [], internalProperties: []}); } 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]; /** @type {!Array<!SDK.RemoteObject.RemoteObjectProperty>} */ const properties = []; 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 /** @type {!SDK.RemoteObject.GetPropertiesResult} */ ({properties, internalProperties: []}); } // 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 /** @type {!SDK.RemoteObject.GetPropertiesResult} */ ({properties: await members, internalProperties: []}); } return /** @type {!SDK.RemoteObject.GetPropertiesResult} */ ({properties: [], internalProperties: []}); } } class NamespaceObject extends SDK.RemoteObject.LocalJSONObject { /** * @param {*} value */ constructor(value) { super(value); } /** * @override * @return {string} */ get description() { return this.type; } /** * @override * @return {string} */ get type() { return 'namespace'; } } class SourceScopeRemoteObject extends SDK.RemoteObject.RemoteObjectImpl { /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {!DebuggerLanguagePlugin} plugin * @param {!RawLocation} location */ constructor(callFrame, plugin, location) { super(callFrame.debuggerModel.runtimeModel(), undefined, 'object', undefined, null); /** @type {!Array<!Variable>} */ this.variables = []; this._callFrame = callFrame; this._plugin = plugin; this._location = location; } /** * @override * @param {boolean} ownProperties * @param {boolean} accessorPropertiesOnly * @param {boolean} generatePreview * @return {!Promise<!SDK.RemoteObject.GetPropertiesResult>} */ async doGetProperties(ownProperties, accessorPropertiesOnly, generatePreview) { if (accessorPropertiesOnly) { return /** @type {!SDK.RemoteObject.GetPropertiesResult} */ ({properties: [], internalProperties: []}); } const properties = []; /** @type {!Object<string, !SDK.RemoteObject.RemoteObject>} */ const namespaces = {}; /** * @param {string} name * @param {!SDK.RemoteObject.RemoteObject} obj */ function makeProperty(name, obj) { 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, /** @type {!SDK.RuntimeModel.EvaluationOptions} */ ({ generatePreview: false, includeCommandLineAPI: true, objectGroup: 'backtrace', returnByValue: false, silent: false })); } catch (e) { console.warn(e); sourceVar = new SDK.RemoteObject.LocalJSONObject(undefined); } if (variable.nestedName && variable.nestedName.length > 1) { let parent = namespaces; for (let index = 0; index < variable.nestedName.length - 1; index++) { const nestedName = variable.nestedName[index]; let child = 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, /** @type {!SDK.RemoteObject.RemoteObject} */ (namespaces[namespace]))); } return /** @type {!SDK.RemoteObject.GetPropertiesResult} */ ({properties: properties, internalProperties: []}); } } /** * @implements {SDK.DebuggerModel.ScopeChainEntry} */ export class SourceScope { /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {string} type * @param {string} typeName * @param {string|undefined} icon * @param {!DebuggerLanguagePlugin} plugin * @param {!RawLocation} location */ constructor(callFrame, type, typeName, icon, plugin, location) { this._callFrame = callFrame; this._type = type; this._typeName = typeName; this._icon = icon; this._object = new SourceScopeRemoteObject(callFrame, plugin, location); this._name = type; /** @type {?SDK.DebuggerModel.Location} */ this._startLocation = null; /** @type {?SDK.DebuggerModel.Location} */ this._endLocation = null; } /** * @param {string} name * @return {!Promise<?SDK.RemoteObject.RemoteObject>} */ async getVariableValue(name) { for (let v = 0; v < this._object.variables.length; ++v) { if (this._object.variables[v].name !== name) { continue; } const properties = await this._object.getAllProperties(false, false); if (!properties.properties) { continue; } const {value} = properties.properties[v]; if (value) { return value; } } return null; } /** * @override * @return {!SDK.DebuggerModel.CallFrame} */ callFrame() { return this._callFrame; } /** * @override * @return {string} */ type() { return this._type; } /** * @override * @return {string} */ typeName() { return this._typeName; } /** * @override * @return {string|undefined} */ name() { return undefined; } /** * @override * @return {?SDK.DebuggerModel.Location} */ startLocation() { return this._startLocation; } /** * @override * @return {?SDK.DebuggerModel.Location} */ endLocation() { return this._endLocation; } /** * @override * @return {!SourceScopeRemoteObject} */ object() { return this._object; } /** * @override * @return {string} */ description() { return ''; } /** * @override */ icon() { return this._icon; } } /** * @implements {SDK.SDKModel.SDKModelObserver<!SDK.DebuggerModel.DebuggerModel>} */ export class DebuggerLanguagePluginManager { /** * @param {!SDK.SDKModel.TargetManager} targetManager * @param {!Workspace.Workspace.WorkspaceImpl} workspace * @param {!DebuggerWorkspaceBinding} debuggerWorkspaceBinding */ constructor(targetManager, workspace, debuggerWorkspaceBinding) { this._workspace = workspace; this._debuggerWorkspaceBinding = debuggerWorkspaceBinding; /** @type {!Array<!DebuggerLanguagePlugin>} */ this._plugins = []; /** @type {!Map<!SDK.DebuggerModel.DebuggerModel, !ModelData>} */ this._debuggerModelToData = new Map(); targetManager.observeModels(SDK.DebuggerModel.DebuggerModel, this); /** * @type {!Map<string, !{ * rawModuleId: string, * plugin: !DebuggerLanguagePlugin, * scripts: !Array<!SDK.Script.Script>, * addRawModulePromise: !Promise<!Array<string>> * }>} */ this._rawModuleHandles = new Map(); } /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @param {!SDK.RuntimeModel.EvaluationOptions} options * @returns {!Promise<?SDK.RuntimeModel.EvaluationResult>} */ async _evaluateOnCallFrame(callFrame, options) { 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}; } return {error: error.message}; } } /** * @param {!Array<!SDK.DebuggerModel.CallFrame>} callFrames * @return {!Promise<!Array<!SDK.DebuggerModel.CallFrame>>} */ _expandCallFrames(callFrames) { return Promise .all(callFrames.map(async callFrame => { const {frames} = await this.getFunctionInfo(callFrame); if (frames.length) { return frames.map(({name}, index) => callFrame.createVirtualCallFrame(index, name)); } return callFrame; })) .then(callFrames => callFrames.flat()); } /** * @override * @param {!SDK.DebuggerModel.DebuggerModel} debuggerModel */ modelAdded(debuggerModel) { 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.setEvaluateOnCallFrameCallback(this._evaluateOnCallFrame.bind(this)); debuggerModel.setExpandCallFramesCallback(this._expandCallFrames.bind(this)); } /** * @override * @param {!SDK.DebuggerModel.DebuggerModel} debuggerModel */ modelRemoved(debuggerModel) { debuggerModel.removeEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this._globalObjectCleared, this); debuggerModel.removeEventListener(SDK.DebuggerModel.Events.ParsedScriptSource, this._parsedScriptSource, 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(ls`Error in debugger language plugin: ${error.message}`); }); this._rawModuleHandles.delete(rawModuleId); } else { rawModuleHandle.scripts = scripts; } }); } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _globalObjectCleared(event) { const debuggerModel = /** @type {!SDK.DebuggerModel.DebuggerModel} */ (event.data); this.modelRemoved(debuggerModel); this.modelAdded(debuggerModel); } /** * @param {!DebuggerLanguagePlugin} plugin */ addPlugin(plugin) { 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}); } } } /** * @param {!DebuggerLanguagePlugin} plugin */ removePlugin(plugin) { this._plugins = this._plugins.filter(p => p !== plugin); const scripts = new Set(); 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 = /** @type {!ModelData} */ (this._debuggerModelToData.get(script.debuggerModel)); 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}); } } /** * @param {!SDK.Script.Script} script * @return {boolean} */ hasPluginForScript(script) { 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. * * @param {!SDK.Script.Script} script * @return {!Promise<!{rawModuleId: string, plugin: ?DebuggerLanguagePlugin}>} */ async _rawModuleIdAndPluginForScript(script) { 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}; } /** * @param {!SDK.DebuggerModel.Location} rawLocation * @return {!Promise<?Workspace.UISourceCode.UILocation>} */ async rawLocationToUILocation(rawLocation) { 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 modelData = this._debuggerModelToData.get(script.debuggerModel); if (!modelData) { continue; } const uiSourceCode = modelData._project.uiSourceCodeForURL(sourceLocation.sourceFileURL); 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(ls`Error in debugger language plugin: ${error.message}`); } return null; } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @param {number} lineNumber * @param {number=} columnNumber * @return {!Promise<?Array<!{start: !SDK.DebuggerModel.Location, end: !SDK.DebuggerModel.Location}>>} Returns null if this manager does not have a plugin for it. */ uiLocationToRawLocationRanges(uiSourceCode, lineNumber, columnNumber = -1) { /** @type {!Array<!Promise<!Array<!{start: !SDK.DebuggerModel.Location, end: !SDK.DebuggerModel.Location}>>>} */ const locationPromises = []; 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(ls`Error in debugger language plugin: ${error.message}`); return null; }); /** * @param {string} rawModuleId * @param {!DebuggerLanguagePlugin} plugin * @param {!SDK.Script.Script} script * @return {!Promise<!Array<!{start: !SDK.DebuggerModel.Location, end: !SDK.DebuggerModel.Location}>>} */ async function getLocations(rawModuleId, plugin, script) { 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)) })); } } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @param {number} lineNumber * @param {number=} columnNumber * @return {!Promise<?Array<!SDK.DebuggerModel.Location>>} Returns null if this manager does not have a plugin for it. */ async uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber) { const locationRanges = await this.uiLocationToRawLocationRanges(uiSourceCode, lineNumber, columnNumber); if (!locationRanges) { return null; } return locationRanges.map(({start}) => start); } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @return {!Array<!SDK.Script.Script>} */ scriptsForUISourceCode(uiSourceCode) { for (const modelData of this._debuggerModelToData.values()) { const scripts = modelData._uiSourceCodeToScripts.get(uiSourceCode); if (scripts) { return scripts; } } return []; } /** * @param {!Common.EventTarget.EventTargetEvent} event */ _parsedScriptSource(event) { const script = /** @type {!SDK.Script.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 () => { const console = Common.Console.Console.instance(); const url = script.sourceURL; const symbolsUrl = (script.debugSymbols && script.debugSymbols.externalURL) || ''; if (symbolsUrl) { console.log(ls`[${plugin.name}] Loading debug symbols for ${url} (via ${symbolsUrl})...`); } else { console.log(ls`[${plugin.name}] Loading debug symbols for ${url}...`); } try { const code = (!symbolsUrl && url.startsWith('wasm://')) ? await script.getWasmBytecode() : undefined; const sourceFileURLs = 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 we're done, while in reality it was a stale call that finished. if (rawModuleHandle !== this._rawModuleHandles.get(rawModuleId)) { return []; } if (sourceFileURLs.length === 0) { console.warn(ls`[${plugin.name}] Loaded debug symbols for ${url}, but didn't find any source files`); } else { console.log( ls`[${plugin.name}] Loaded debug symbols for ${url}, found ${sourceFileURLs.length} source file(s)`); } return sourceFileURLs; } catch (error) { console.error(ls`[${plugin.name}] Failed to load debug symbols for ${url} (${error.message})`); this._rawModuleHandles.delete(rawModuleId); return []; } })(); rawModuleHandle = {rawModuleId, plugin, scripts: [script], addRawModulePromise: sourceFileURLsPromise}; this._rawModuleHandles.set(rawModuleId, rawModuleHandle); } else { rawModuleHandle.scripts.push(script); } // Wait for the addRawModule call to finish and // update the project. It's important to check // for the DebuggerModel again, which may disappear // in the meantime... rawModuleHandle.addRawModulePromise.then(sourceFileURLs => { // The script might have disappeared meanwhile... if (script.debuggerModel.scriptForId(script.scriptId) === script) { const modelData = this._debuggerModelToData.get(script.debuggerModel); if (modelData) { // The DebuggerModel could have disappeared meanwhile... modelData._addSourceFiles(script, sourceFileURLs); this._debuggerWorkspaceBinding.updateLocations(script); } } }); return; } } /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @return {!Promise<?Array<!SourceScope>>} */ async resolveScopeChain(callFrame) { const script = callFrame.script; const {rawModuleId, plugin} = await this._rawModuleIdAndPluginForScript(script); if (!plugin) { return null; } const location = { rawModuleId, codeOffset: callFrame.location().columnNumber - (script.codeOffset() || 0), inlineFrameIndex: callFrame.inlineFrameIndex }; try { const sourceMapping = await plugin.rawLocationToSourceLocation(location); if (sourceMapping.length === 0) { return null; } /** @type {!Map<string, !SourceScope>} */ const scopes = new Map(); const variables = await plugin.listVariablesInScope(location); for (const variable of variables || []) { let scope = scopes.get(variable.scope); if (!scope) { const {type, typeName, icon} = await plugin.getScopeInfo(variable.scope); scope = new SourceScope(callFrame, type, typeName, icon, plugin, location); scopes.set(variable.scope, scope); } scope.object().variables.push(variable); } return Array.from(scopes.values()); } catch (error) { Common.Console.Console.instance().error(ls`Error in debugger language plugin: ${error.message}`); return null; } } /** * @param {!SDK.DebuggerModel.CallFrame} callFrame * @return {!Promise<!{frames: !Array<!FunctionInfo>}>} */ async getFunctionInfo(callFrame) { const noDwarfInfo = {frames: []}; if (!callFrame) { return noDwarfInfo; } const script = callFrame.script; const {rawModuleId, plugin} = await this._rawModuleIdAndPluginForScript(script); if (!plugin) { return noDwarfInfo; } /** @type {!RawLocation}} */ const location = { rawModuleId, codeOffset: callFrame.location().columnNumber - (script.codeOffset() || 0), }; try { return await plugin.getFunctionInfo(location); } catch (error) { Common.Console.Console.instance().warn(ls`Error in debugger language plugin: ${error.message}`); return noDwarfInfo; } } /** * @param {!SDK.DebuggerModel.Location} rawLocation * @return {!Promise<!Array<!{start: !SDK.DebuggerModel.Location, end: !SDK.DebuggerModel.Location}>>} Returns an empty list if this manager does not have a plugin for it. */ async getInlinedFunctionRanges(rawLocation) { const script = rawLocation.script(); if (!script) { return []; } const {rawModuleId, plugin} = await this._rawModuleIdAndPluginForScript(script); if (!plugin) { return []; } 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), }; try { const locations = await plugin.getInlinedFunctionRanges(pluginLocation); return locations.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)) })); } catch (error) { Common.Console.Console.instance().warn(ls`Error in debugger language plugin: ${error.message}`); return []; } } /** * @param {!SDK.DebuggerModel.Location} rawLocation * @return {!Promise<!Array<!{start: !SDK.DebuggerModel.Location, end: !SDK.DebuggerModel.Location}>>} Returns an empty list if this manager does not have a plugin for it. */ async getInlinedCalleesRanges(rawLocation) { const script = rawLocation.script(); if (!script) { return []; } const {rawModuleId, plugin} = await this._rawModuleIdAndPluginForScript(script); if (!plugin) { return []; } 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), }; try { const locations = await plugin.getInlinedCalleesRanges(pluginLocation); return locations.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)) })); } catch (error) { Common.Console.Console.instance().warn(ls`Error in debugger language plugin: ${error.message}`); return []; } } /** * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode */ async getMappedLines(uiSourceCode) { const rawModuleIds = await Promise.all(this.scriptsForUISourceCode(uiSourceCode).map(s => this._rawModuleIdAndPluginForScript(s))); /** @type {Set<number> | undefined} */ let mappedLines; for (const {rawModuleId, plugin} of rawModuleIds) { if (!plugin) { continue; } const lines = await plugin.getMappedLines(rawModuleId, uiSourceCode.url()); if (lines === undefined) { continue; } if (mappedLines === undefined) { mappedLines = new Set(lines); } else { /** * @param {number} l */ lines.forEach(l => /** @type {!Set<number>} */ (mappedLines).add(l)); } } return mappedLines; } } class ModelData { /** * @param {!SDK.DebuggerModel.DebuggerModel} debuggerModel * @param {!Workspace.Workspace.WorkspaceImpl} workspace */ constructor(debuggerModel, workspace) { this._debuggerModel = debuggerModel; this._project = new ContentProviderBased