Files
react-native-devtools-frontend/front_end/bindings/DebuggerLanguagePlugins.js
T
Tim van der Lippe e4d64e7ae0 Remove unrestricted from comments that include other annotations
For all comments that contained both the unrestricted annotation
and any other potential annotation, remove only the unrestricted
annotation.

R=szuend@chromium.org

No-Presubmit: True
Bug: 1011811
Change-Id: I27dd67f657cdb1a9233d768eea81cabe2591f5f7
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2569759
Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
2020-12-02 15:31:52 +00:00

1509 lines
49 KiB
JavaScript

// 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 * 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;
}
class EvalNodeBase extends SDK.RemoteObject.RemoteObjectImpl {
/**
* @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
*/
constructor(callFrame, sourceType, plugin, object, formatterTag, evalOptions) {
super(
callFrame.debuggerModel.runtimeModel(), object.objectId, object.type, object.subtype, object.value,
object.unserializableValue, object.description, object.preview, object.customPreview, object.className);
this._plugin = plugin;
this._sourceType = sourceType;
this._callFrame = callFrame;
/** @type {?{className: string, symbol: string }} */
this.formatterTag = formatterTag;
this._evalOptions = evalOptions;
}
/**
* @param {...string} properties
* @return {!Promise<!Object<string, !EvalNodeBase|undefined>>}
*/
async findProperties(...properties) {
/** @type {!Object<string, !EvalNodeBase|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 {!EvalNodeBase|undefined} */ (prop.value);
}
}
}
return result;
}
/**
* @override
* @param {!Protocol.Runtime.RemoteObject} newObject
*/
async _createRemoteObject(newObject) {
const base = await this._getEvalBaseFromObject(newObject);
if (!base) {
return new EvalNodeBase(
this._callFrame, this._sourceType, this._plugin, newObject, this.formatterTag, this._evalOptions);
}
const newSourceType = this._sourceType.typeMap.get(base.rootType.typeId);
if (!newSourceType) {
throw new Error('Unknown typeId in eval base');
}
if (base.rootType.hasValue && !base.rootType.canExpand && base) {
return EvalNode.evaluate(this._callFrame, this._plugin, newSourceType, base, [], this._evalOptions);
}
return new EvalNode(this._callFrame, this._plugin, newSourceType, base, [], this._evalOptions);
}
/**
* @param {!Protocol.Runtime.RemoteObject} object
*/
async _getEvalBaseFromObject(object) {
const {objectId} = object;
if (!object || !this.formatterTag) {
return null;
}
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;
}
const baseObject =
new EvalNodeBase(this._callFrame, this._sourceType, this._plugin, result, null, this._evalOptions);
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 EvalError extends Error {
/**
* @param {!SDK.RemoteObject.RemoteObject} exception
* @param {!Protocol.Runtime.ExceptionDetails} exceptionDetails
*/
constructor(exception, exceptionDetails) {
super();
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 EvalError(errorObject, exceptionDetails);
}
}
class EvalNode extends SDK.RemoteObject.RemoteObjectImpl {
/**
* @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<!SDK.RemoteObject.RemoteObject>}
*/
static async evaluate(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 EvalError(callFrame.debuggerModel.runtimeModel().createRemoteObject(result), exceptionDetails);
}
const object = new EvalNodeBase(callFrame, sourceType, plugin, result, null, evalOptions);
const unpackedResultObject = await unpackResultObject(object);
const node = unpackedResultObject || object;
if (typeof node.value === 'undefined') {
node.description = sourceType.typeInfo.typeNames[0];
}
return node;
/**
* @param {!EvalNodeBase} object
* @return {!Promise<?EvalNodeBase>}
*/
async function unpackResultObject(object) {
const {tag, value} = await object.findProperties('tag', 'value');
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};
return value;
}
}
/**
* @param {!SDK.DebuggerModel.CallFrame} callFrame
* @param {!DebuggerLanguagePlugin} plugin
* @param {string} expression
* @param {!SDK.RuntimeModel.EvaluationOptions} evalOptions
* @return {!Promise<!SDK.RemoteObject.RemoteObject>}
*/
static async get(callFrame, plugin, expression, evalOptions) {
const location = getRawLocation(callFrame);
let typeInfo;
try {
typeInfo = await plugin.getTypeInfo(expression, location);
} catch (e) {
EvalError.throwLocal(callFrame, e.message);
}
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) {
return EvalNode.evaluate(callFrame, plugin, sourceType, base, [], evalOptions);
}
return new EvalNode(callFrame, plugin, sourceType, base, [], evalOptions);
}
/**
* @param {!SDK.DebuggerModel.CallFrame} callFrame
* @param {!DebuggerLanguagePlugin} plugin
* @param {!SourceType} sourceType
* @param {?EvalBase} base
* @param {!Array<!FieldInfo>} fieldChain
* @param {!SDK.RuntimeModel.EvaluationOptions} evalOptions
*/
constructor(callFrame, plugin, sourceType, base, fieldChain, evalOptions) {
const typeName = sourceType.typeInfo.typeNames[0] || '<anonymous>';
const variableType = 'object';
super(
callFrame.debuggerModel.runtimeModel(),
/* objectId=*/ undefined,
/* type=*/ variableType,
/* subtype=*/ undefined, /* value=*/ null, /* unserializableValue=*/ undefined,
/* description=*/ typeName, /* preview=*/ undefined, /* customPreview=*/ undefined, /* className=*/ typeName);
this._variableType = variableType;
this._callFrame = callFrame;
this._plugin = plugin;
/** @type {!SourceType} */
this._sourceType = sourceType;
this._base = base;
this._fieldChain = fieldChain;
this._hasChildren = true; // FIXME for top-level stuff with a value
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) {
if (sourceType.typeInfo.hasValue && !sourceType.typeInfo.canExpand && this._base) {
return EvalNode.evaluate(
this._callFrame, this._plugin, sourceType, this._base, this._fieldChain.concat(fieldInfo), this._evalOptions);
}
return new EvalNode(
this._callFrame, this._plugin, sourceType, this._base, this._fieldChain.concat(fieldInfo), this._evalOptions);
}
/**
* @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) {
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 EvalNode.get(
this._callFrame, this._plugin, variable.name,
/** @type {!SDK.RuntimeModel.EvaluationOptions} */
({
generatePreview: false,
includeCommandLineAPI: true,
objectGroup: 'backtrace',
returnByValue: false,
silent: false
}));
} catch {
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
* @suppress {missingProperties}
*/
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 EvalNode.get(callFrame, plugin, expression, options);
return {object, exceptionDetails: undefined};
} catch (error) {
if (error instanceof EvalError) {
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 && 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});
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 => {
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),
// TODO(crbug.com/1134110): Once closure->typescript migration is complete, delete this and
// change type definition to show that this field is optional.
inlineFrameIndex: undefined
};
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),
// TODO(crbug.com/1134110): Once closure->typescript migration is complete, delete this and
// change type definition to show that this field is optional.
inlineFrameIndex: undefined
};
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),
// TODO(crbug.com/1134110): Once closure->typescript migration is complete, delete this and
// change type definition to show that this field is optional.
inlineFrameIndex: undefined
};
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 [];
}
}
}
class ModelData {
/**
* @param {!SDK.DebuggerModel.DebuggerModel} debuggerModel
* @param {!Workspace.Workspace.WorkspaceImpl} workspace
*/
constructor(debuggerModel, workspace) {
this._debuggerModel = debuggerModel;
this._project = new ContentProviderBasedProject(
workspace, 'language_plugins::' + debuggerModel.target().id(), Workspace.Workspace.projectTypes.Network, '',
false /* isServiceProject */);
NetworkProject.setTargetForProject(this._project, debuggerModel.target());
/** @type {!Map<!Workspace.UISourceCode.UISourceCode, !Array<!SDK.Script.Script>>} */
this._uiSourceCodeToScripts = new Map();
}
/**
* @param {!SDK.Script.Script} script
* @param {!Array<string>} urls
*/
_addSourceFiles(script, urls) {
const initiator = script.createPageResourceLoadInitiator();
for (const url of urls) {
let uiSourceCode = this._project.uiSourceCodeForURL(url);
if (!uiSourceCode) {
uiSourceCode = this._project.createUISourceCode(url, Common.ResourceType.resourceTypes.SourceMapScript);
NetworkProject.setInitialFrameAttribution(uiSourceCode, script.frameId);
// Bind the uiSourceCode to the script first before we add the
// uiSourceCode to the project and thereby notify the rest of
// the system about the new source file.
// https://crbug.com/1150295 is an example where the breakpoint
// resolution logic kicks in right after adding the uiSourceCode
// and at that point we already need to have the mapping in place
// otherwise we will not get the breakpoint right.
this._uiSourceCodeToScripts.set(uiSourceCode, [script]);
const contentProvider = new SDK.CompilerSourceMappingContentProvider.CompilerSourceMappingContentProvider(
url, Common.ResourceType.resourceTypes.SourceMapScript, initiator);
const mimeType = Common.ResourceType.ResourceType.mimeFromURL(url) || 'text/javascript';
this._project.addUISourceCodeWithProvider(uiSourceCode, contentProvider, null, mimeType);
} else {
// The same uiSourceCode can be provided by different scripts,
// but we don't expect that to happen frequently.
const scripts = /** @type {!Array<!SDK.Script.Script>} */ (this._uiSourceCodeToScripts.get(uiSourceCode));
if (!scripts.includes(script)) {
scripts.push(script);
}
}
}
}
/**
* @param {!SDK.Script.Script} script
*/
_removeScript(script) {
this._uiSourceCodeToScripts.forEach((scripts, uiSourceCode) => {
scripts = scripts.filter(s => s !== script);
if (scripts.length === 0) {
this._uiSourceCodeToScripts.delete(uiSourceCode);
this._project.removeUISourceCode(uiSourceCode.url());
} else {
this._uiSourceCodeToScripts.set(uiSourceCode, scripts);
}
});
}
_dispose() {
this._project.dispose();
}
}
/** Raw modules represent compiled JavaScript Scripts or Wasm Modules
* @typedef {{
* url: string,
* code: (ArrayBuffer|undefined)
* }}
*/
// @ts-ignore typedef
export let RawModule;
/** Code ranges in raw modules
* @typedef {{
* rawModuleId:string,
* startOffset:number,
* endOffset:number
* }}
*/
// @ts-ignore typedef
export let RawLocationRange;
/** Offsets in raw modules
* @typedef {{
* rawModuleId: string,
* codeOffset: number,
* inlineFrameIndex: (number|undefined)
* }}
*/
// @ts-ignore typedef
export let RawLocation;
/** Locations in source files. A columnNumber of `-1` indicates that the location refers to the whole line.
* @typedef {{
* rawModuleId: string,
* sourceFileURL: string,
* lineNumber: number,
* columnNumber: number
* }}
*/
// @ts-ignore typedef
export let SourceLocation;
/** A source language variable
* @typedef {{
* scope: string,
* name: string,
* type: string,
* nestedName: ?Array<string>
* }}
*/
// @ts-ignore typedef
export let Variable;
/** The value of a source language variable
* @typedef {{
* value: (string|!Array<!VariableValue>),
* js_type: string,
* type: string,
* name: string
* }}
*/
// @ts-ignore typedef
export let VariableValue;
/** Either the code of an evaluator module or a constant representation of a variable
* @typedef {{
* code: (!ArrayBuffer|undefined),
* constantValue: (!VariableValue|undefined)
* }}
*/
// @ts-ignore typedef
export let EvaluatorModule;
/** Description of a scope
* @typedef {{
* type: string,
* typeName: string,
* icon: (string|undefined)
* }}
*/
// @ts-ignore typedef
export let ScopeInfo;
/** Either the code of an evaluator module or a constant representation of a variable
* @typedef {{
* name: string
* }}
*/
// @ts-ignore typedef
export let FunctionInfo;
/**
* @typedef {{
* name: (string|undefined),
* offset: number,
* typeId: *
*}}
*/
// @ts-ignore typedef
export let FieldInfo;
/**
* @typedef {{
* typeNames: !Array<string>,
* typeId: *,
* members: !Array<!FieldInfo>,
* alignment: number,
* arraySize: number,
* size: number,
* canExpand: boolean,
* hasValue: boolean
* }}
*/
// @ts-ignore typedef
export let TypeInfo;
/**
* @typedef {{
* rootType: TypeInfo,
* payload: *
*}}
*/
// @ts-ignore typedef
export let EvalBase;
export class DebuggerLanguagePlugin {
/**
* @param {string} name The user visible name for the plugin.
*/
constructor(name) {
this.name = name;
}
/**
* @param {!SDK.Script.Script} script
* @return {boolean} True if this plugin should handle this script
*/
handleScript(script) {
throw new Error('Not implemented yet');
}
dispose() {
}
/** Notify the plugin about a new script
* @param {string} rawModuleId
* @param {string} symbolsURL - URL of a file providing the debug symbols for this module
* @param {!RawModule} rawModule
* @return {!Promise<!Array<string>>} - An array of URLs for the source files for the raw module
*/
async addRawModule(rawModuleId, symbolsURL, rawModule) {
throw new Error('Not implemented yet');
}
/** Find locations in raw modules from a location in a source file
* @param {!SourceLocation} sourceLocation
* @return {!Promise<!Array<!RawLocationRange>>}
*/
async sourceLocationToRawLocation(sourceLocation) {
throw new Error('Not implemented yet');
}
/** Find locations in source files from a location in a raw module
* @param {!RawLocation} rawLocation
* @return {!Promise<!Array<!SourceLocation>>}
*/
async rawLocationToSourceLocation(rawLocation) {
throw new Error('Not implemented yet');
}
/** Return detailed information about a scope
* @param {string} type
* @return {!Promise<!ScopeInfo>}
*/
async getScopeInfo(type) {
throw new Error('Not implemented yet');
}
/** List all variables in lexical scope at a given location in a raw module
* @param {!RawLocation} rawLocation
* @return {!Promise<!Array<!Variable>>}
*/
async listVariablesInScope(rawLocation) {
throw new Error('Not implemented yet');
}
/** Evaluate the content of a variable in a given lexical scope
* @param {string} name
* @param {!RawLocation} location
* @return {!Promise<?EvaluatorModule>}
*/
async evaluateVariable(name, location) {
throw new Error('Not implemented yet');
}
/**
* Notifies the plugin that a script is removed.
* @param {string} rawModuleId
* @return {!Promise<void>}
*/
removeRawModule(rawModuleId) {
throw new Error('Not implemented yet');
}
/**
* @param {string} expression
* @param {!RawLocation} context
* @return {!Promise<?{typeInfos: !Array<!TypeInfo>, base: !EvalBase}>}
*/
getTypeInfo(expression, context) {
throw new Error('Not implemented yet');
}
/**
* @param {string|!{base: !EvalBase, field: !Array<!FieldInfo>}} expressionOrField
* @param {!RawLocation} context
* @return {!Promise<?{js:string}>}
*/
getFormatter(expressionOrField, context) {
throw new Error('Not implemented yet');
}
/** Find locations in source files from a location in a raw module
* @param {!RawLocation} rawLocation
* @return {!Promise<!{frames: !Array<!FunctionInfo>}>}
*/
async getFunctionInfo(rawLocation) {
throw new Error('Not implemented yet');
}
/** Find locations in raw modules corresponding to the inline function
* that rawLocation is in. Used for stepping out of an inline function.
* @param {!RawLocation} rawLocation
* @return {!Promise<!Array<!RawLocationRange>>}
*/
async getInlinedFunctionRanges(rawLocation) {
throw new Error('Not implemented yet');
}
/** Find locations in raw modules corresponding to inline functions
* called by the function or inline frame that rawLocation is in.
* Used for stepping over inline functions.
* @param {!RawLocation} rawLocation
* @return {!Promise<!Array<!RawLocationRange>>}
*/
async getInlinedCalleesRanges(rawLocation) {
throw new Error('Not implemented yet');
}
}