mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-01 17:57:36 +08:00
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>
1509 lines
49 KiB
JavaScript
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');
|
|
}
|
|
}
|