UNPKG

@quick-game/cli

Version:

Command line interface for rapid qg development

898 lines 40.2 kB
// Copyright 2020 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Common from '../../core/common/common.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as TextUtils from '../text_utils/text_utils.js'; import * as Workspace from '../workspace/workspace.js'; import { ContentProviderBasedProject } from './ContentProviderBasedProject.js'; import { assertNotNullOrUndefined } from '../../core/platform/platform.js'; import { NetworkProject } from './NetworkProject.js'; const UIStrings = { /** *@description Error message that is displayed in the Console when language #plugins report errors *@example {File not found} PH1 */ errorInDebuggerLanguagePlugin: 'Error in debugger language plugin: {PH1}', /** *@description Status message that is shown in the Console when debugging information is being *loaded. The 2nd and 3rd placeholders are URLs. *@example {C/C++ DevTools Support (DWARF)} PH1 *@example {http://web.dev/file.wasm} PH2 *@example {http://web.dev/file.wasm.debug.wasm} PH3 */ loadingDebugSymbolsForVia: '[{PH1}] Loading debug symbols for {PH2} (via {PH3})...', /** *@description Status message that is shown in the Console when debugging information is being loaded *@example {C/C++ DevTools Support (DWARF)} PH1 *@example {http://web.dev/file.wasm} PH2 */ loadingDebugSymbolsFor: '[{PH1}] Loading debug symbols for {PH2}...', /** *@description Warning message that is displayed in the Console when debugging information was loaded, but no source files were found *@example {C/C++ DevTools Support (DWARF)} PH1 *@example {http://web.dev/file.wasm} PH2 */ loadedDebugSymbolsForButDidnt: '[{PH1}] Loaded debug symbols for {PH2}, but didn\'t find any source files', /** *@description Status message that is shown in the Console when debugging information is successfully loaded *@example {C/C++ DevTools Support (DWARF)} PH1 *@example {http://web.dev/file.wasm} PH2 *@example {42} PH3 */ loadedDebugSymbolsForFound: '[{PH1}] Loaded debug symbols for {PH2}, found {PH3} source file(s)', /** *@description Error message that is displayed in the Console when debugging information cannot be loaded *@example {C/C++ DevTools Support (DWARF)} PH1 *@example {http://web.dev/file.wasm} PH2 *@example {File not found} PH3 */ failedToLoadDebugSymbolsFor: '[{PH1}] Failed to load debug symbols for {PH2} ({PH3})', /** *@description Error message that is displayed in UI debugging information cannot be found for a call frame *@example {main} PH1 */ failedToLoadDebugSymbolsForFunction: 'No debug information for function "{PH1}"', /** *@description Error message that is displayed in UI when a file needed for debugging information for a call frame is missing *@example {mainp.debug.wasm.dwp} PH1 */ debugSymbolsIncomplete: 'The debug information for function {PH1} is incomplete', }; const str_ = i18n.i18n.registerUIStrings('models/bindings/DebuggerLanguagePlugins.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); /** * Generates the raw module ID for a script, which is used * to uniquely identify the debugging data for a script on * the responsible language #plugin. * * @param script the unique raw module ID for the script. */ function rawModuleIdForScript(script) { return `${script.sourceURL}@${script.hash}`; } function getRawLocation(callFrame) { const { script } = callFrame; return { rawModuleId: rawModuleIdForScript(script), codeOffset: callFrame.location().columnNumber - (script.codeOffset() || 0), inlineFrameIndex: callFrame.inlineFrameIndex, }; } class FormattingError extends Error { exception; exceptionDetails; constructor(exception, exceptionDetails) { const { description } = exceptionDetails.exception || {}; super(description || exceptionDetails.text); this.exception = exception; this.exceptionDetails = exceptionDetails; } static makeLocal(callFrame, message) { const exception = { type: "object" /* Protocol.Runtime.RemoteObjectType.Object */, subtype: "error" /* Protocol.Runtime.RemoteObjectSubtype.Error */, description: message, }; const exceptionDetails = { text: 'Uncaught', exceptionId: -1, columnNumber: 0, lineNumber: 0, exception }; const errorObject = callFrame.debuggerModel.runtimeModel().createRemoteObject(exception); return new FormattingError(errorObject, exceptionDetails); } } class NamespaceObject extends SDK.RemoteObject.LocalJSONObject { // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(value) { super(value); } get description() { return this.type; } get type() { return 'namespace'; } } class SourceScopeRemoteObject extends SDK.RemoteObject.RemoteObjectImpl { variables; #callFrame; #plugin; stopId; constructor(callFrame, stopId, plugin) { super(callFrame.debuggerModel.runtimeModel(), undefined, 'object', undefined, null); this.variables = []; this.#callFrame = callFrame; this.#plugin = plugin; this.stopId = stopId; } async doGetProperties(ownProperties, accessorPropertiesOnly, _generatePreview) { if (accessorPropertiesOnly) { return { properties: [], internalProperties: [] }; } const properties = []; const namespaces = {}; 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 { const evalResult = await this.#plugin.evaluate(variable.name, getRawLocation(this.#callFrame), this.stopId); sourceVar = evalResult ? new ExtensionRemoteObject(this.#callFrame, evalResult, this.#plugin) : new SDK.RemoteObject.LocalJSONObject(undefined); } catch (e) { console.warn(e); sourceVar = new SDK.RemoteObject.LocalJSONObject(undefined); } if (variable.nestedName && variable.nestedName.length > 1) { let parent = namespaces; for (let index = 0; index < variable.nestedName.length - 1; index++) { const nestedName = variable.nestedName[index]; let child = parent[nestedName]; if (!child) { child = new NamespaceObject({}); parent[nestedName] = child; } parent = child.value; } const name = variable.nestedName[variable.nestedName.length - 1]; parent[name] = sourceVar; } else { properties.push(makeProperty(variable.name, sourceVar)); } } for (const namespace in namespaces) { properties.push(makeProperty(namespace, namespaces[namespace])); } return { properties: properties, internalProperties: [] }; } } export class SourceScope { #callFrameInternal; #typeInternal; #typeNameInternal; #iconInternal; #objectInternal; constructor(callFrame, stopId, type, typeName, icon, plugin) { if (icon && new URL(icon).protocol !== 'data:') { throw new Error('The icon must be a data:-URL'); } this.#callFrameInternal = callFrame; this.#typeInternal = type; this.#typeNameInternal = typeName; this.#iconInternal = icon; this.#objectInternal = new SourceScopeRemoteObject(callFrame, stopId, plugin); } async getVariableValue(name) { for (let v = 0; v < this.#objectInternal.variables.length; ++v) { if (this.#objectInternal.variables[v].name !== name) { continue; } const properties = await this.#objectInternal.getAllProperties(false, false); if (!properties.properties) { continue; } const { value } = properties.properties[v]; if (value) { return value; } } return null; } callFrame() { return this.#callFrameInternal; } type() { return this.#typeInternal; } typeName() { return this.#typeNameInternal; } name() { return undefined; } range() { return null; } object() { return this.#objectInternal; } description() { return ''; } icon() { return this.#iconInternal; } } export class ExtensionRemoteObject extends SDK.RemoteObject.RemoteObject { extensionObject; plugin; callFrame; constructor(callFrame, extensionObject, plugin) { super(); this.extensionObject = extensionObject; this.plugin = plugin; this.callFrame = callFrame; } get linearMemoryAddress() { return this.extensionObject.linearMemoryAddress; } get linearMemorySize() { return this.extensionObject.linearMemorySize; } get objectId() { return this.extensionObject.objectId; } get type() { if (this.extensionObject.type === 'array' || this.extensionObject.type === 'null') { return 'object'; } return this.extensionObject.type; } get subtype() { if (this.extensionObject.type === 'array' || this.extensionObject.type === 'null') { return this.extensionObject.type; } return undefined; } get value() { return this.extensionObject.value; } unserializableValue() { return undefined; } get description() { return this.extensionObject.description; } set description(description) { } get hasChildren() { return this.extensionObject.hasChildren; } get preview() { return undefined; } get className() { return this.extensionObject.className ?? null; } arrayLength() { return 0; } arrayBufferByteLength() { return 0; } getOwnProperties(_generatePreview, _nonIndexedPropertiesOnly) { return this.getAllProperties(false, _generatePreview, _nonIndexedPropertiesOnly); } async getAllProperties(_accessorPropertiesOnly, _generatePreview, _nonIndexedPropertiesOnly) { const { objectId } = this.extensionObject; if (objectId) { assertNotNullOrUndefined(this.plugin.getProperties); const extensionObjectProperties = await this.plugin.getProperties(objectId); const properties = extensionObjectProperties.map(p => new SDK.RemoteObject.RemoteObjectProperty(p.name, new ExtensionRemoteObject(this.callFrame, p.value, this.plugin))); return { properties, internalProperties: null }; } return { properties: null, internalProperties: null }; } release() { const { objectId } = this.extensionObject; if (objectId) { assertNotNullOrUndefined(this.plugin.releaseObject); void this.plugin.releaseObject(objectId); } } debuggerModel() { return this.callFrame.debuggerModel; } runtimeModel() { return this.callFrame.debuggerModel.runtimeModel(); } } export class DebuggerLanguagePluginManager { #workspace; #debuggerWorkspaceBinding; #plugins; #debuggerModelToData; #rawModuleHandles; callFrameByStopId = new Map(); stopIdByCallFrame = new Map(); nextStopId = 0n; constructor(targetManager, workspace, debuggerWorkspaceBinding) { this.#workspace = workspace; this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding; this.#plugins = []; this.#debuggerModelToData = new Map(); targetManager.observeModels(SDK.DebuggerModel.DebuggerModel, this); this.#rawModuleHandles = new Map(); } async evaluateOnCallFrame(callFrame, options) { const { script } = callFrame; const { expression, returnByValue, throwOnSideEffect } = 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; } if (returnByValue) { return { error: 'Cannot return by value' }; } if (throwOnSideEffect) { return { error: 'Cannot guarantee side-effect freedom' }; } try { const object = await plugin.evaluate(expression, location, this.stopIdForCallFrame(callFrame)); if (object) { return { object: new ExtensionRemoteObject(callFrame, object, plugin), exceptionDetails: undefined }; } return { object: new SDK.RemoteObject.LocalJSONObject(undefined), exceptionDetails: undefined }; } catch (error) { if (error instanceof FormattingError) { const { exception: object, exceptionDetails } = error; return { object, exceptionDetails }; } const { exception: object, exceptionDetails } = FormattingError.makeLocal(callFrame, error.message); return { object, exceptionDetails }; } } stopIdForCallFrame(callFrame) { let stopId = this.stopIdByCallFrame.get(callFrame); if (stopId !== undefined) { return stopId; } stopId = this.nextStopId++; this.stopIdByCallFrame.set(callFrame, stopId); this.callFrameByStopId.set(stopId, callFrame); return stopId; } callFrameForStopId(stopId) { return this.callFrameByStopId.get(stopId); } expandCallFrames(callFrames) { return Promise .all(callFrames.map(async (callFrame) => { const functionInfo = await this.getFunctionInfo(callFrame.script, callFrame.location()); if (functionInfo) { if ('frames' in functionInfo && functionInfo.frames.length) { return functionInfo.frames.map(({ name }, index) => callFrame.createVirtualCallFrame(index, name)); } if ('missingSymbolFiles' in functionInfo && functionInfo.missingSymbolFiles.length) { const resources = functionInfo.missingSymbolFiles; const details = i18nString(UIStrings.debugSymbolsIncomplete, { PH1: callFrame.functionName }); callFrame.setMissingDebugInfoDetails({ details, resources }); } else { callFrame.setMissingDebugInfoDetails({ resources: [], details: i18nString(UIStrings.failedToLoadDebugSymbolsForFunction, { PH1: callFrame.functionName }), }); } } return callFrame; })) .then(callFrames => callFrames.flat()); } modelAdded(debuggerModel) { this.#debuggerModelToData.set(debuggerModel, new ModelData(debuggerModel, this.#workspace)); debuggerModel.addEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this); debuggerModel.addEventListener(SDK.DebuggerModel.Events.ParsedScriptSource, this.parsedScriptSource, this); debuggerModel.addEventListener(SDK.DebuggerModel.Events.DebuggerResumed, this.debuggerResumed, this); debuggerModel.setEvaluateOnCallFrameCallback(this.evaluateOnCallFrame.bind(this)); debuggerModel.setExpandCallFramesCallback(this.expandCallFrames.bind(this)); } modelRemoved(debuggerModel) { debuggerModel.removeEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this); debuggerModel.removeEventListener(SDK.DebuggerModel.Events.ParsedScriptSource, this.parsedScriptSource, this); debuggerModel.removeEventListener(SDK.DebuggerModel.Events.DebuggerResumed, this.debuggerResumed, this); debuggerModel.setEvaluateOnCallFrameCallback(null); debuggerModel.setExpandCallFramesCallback(null); const modelData = this.#debuggerModelToData.get(debuggerModel); if (modelData) { modelData.dispose(); this.#debuggerModelToData.delete(debuggerModel); } this.#rawModuleHandles.forEach((rawModuleHandle, rawModuleId) => { const scripts = rawModuleHandle.scripts.filter(script => script.debuggerModel !== debuggerModel); if (scripts.length === 0) { rawModuleHandle.plugin.removeRawModule(rawModuleId).catch(error => { Common.Console.Console.instance().error(i18nString(UIStrings.errorInDebuggerLanguagePlugin, { PH1: error.message })); }); this.#rawModuleHandles.delete(rawModuleId); } else { rawModuleHandle.scripts = scripts; } }); } globalObjectCleared(event) { const debuggerModel = event.data; this.modelRemoved(debuggerModel); this.modelAdded(debuggerModel); } 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 }); } } } 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 = 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 }); } } hasPluginForScript(script) { const rawModuleId = rawModuleIdForScript(script); const rawModuleHandle = this.#rawModuleHandles.get(rawModuleId); return rawModuleHandle !== undefined && rawModuleHandle.scripts.includes(script); } /** * Returns the responsible language #plugin and the raw module ID for a script. * * This ensures that the `addRawModule` call finishes first such that the * caller can immediately issue calls to the returned #plugin without the * risk of racing with the `addRawModule` call. The returned #plugin will be * set to undefined to indicate that there's no #plugin for the script. */ 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 }; } uiSourceCodeForURL(debuggerModel, url) { const modelData = this.#debuggerModelToData.get(debuggerModel); if (modelData) { return modelData.getProject().uiSourceCodeForURL(url); } return null; } 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 uiSourceCode = this.uiSourceCodeForURL(script.debuggerModel, 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(i18nString(UIStrings.errorInDebuggerLanguagePlugin, { PH1: error.message })); } return null; } uiLocationToRawLocationRanges(uiSourceCode, lineNumber, columnNumber = -1) { 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(i18nString(UIStrings.errorInDebuggerLanguagePlugin, { PH1: error.message })); return null; }); 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)), })); } } async uiLocationToRawLocations(uiSourceCode, lineNumber, columnNumber) { const locationRanges = await this.uiLocationToRawLocationRanges(uiSourceCode, lineNumber, columnNumber); if (!locationRanges) { return null; } return locationRanges.map(({ start }) => start); } async uiLocationRangeToRawLocationRanges(uiSourceCode, textRange) { const locationRangesPromises = []; for (let line = textRange.startLine; line <= textRange.endLine; ++line) { locationRangesPromises.push(this.uiLocationToRawLocationRanges(uiSourceCode, line)); } const ranges = []; for (const locationRanges of await Promise.all(locationRangesPromises)) { if (locationRanges === null) { return null; } for (const range of locationRanges) { const [startLocation, endLocation] = await Promise.all([ this.rawLocationToUILocation(range.start), this.rawLocationToUILocation(range.end), ]); if (startLocation === null || endLocation === null) { continue; } // Report all ranges that somehow intersect with the `textRange`. It's the // responsibility of the caller to filter / clamp these ranges appropriately. const overlap = textRange.intersection(new TextUtils.TextRange.TextRange(startLocation.lineNumber, startLocation.columnNumber ?? 0, endLocation.lineNumber, endLocation.columnNumber ?? Infinity)); if (!overlap.isEmpty()) { ranges.push(range); } } } return ranges; } scriptsForUISourceCode(uiSourceCode) { for (const modelData of this.#debuggerModelToData.values()) { const scripts = modelData.uiSourceCodeToScripts.get(uiSourceCode); if (scripts) { return scripts; } } return []; } setDebugInfoURL(script, externalURL) { if (this.hasPluginForScript(script)) { return; } script.debugSymbols = { type: "ExternalDWARF" /* Protocol.Debugger.DebugSymbolsType.ExternalDWARF */, externalURL }; this.parsedScriptSource({ data: script }); void script.debuggerModel.setDebugInfoURL(script, externalURL); } parsedScriptSource(event) { const script = event.data; if (!script.sourceURL) { return; } for (const plugin of this.#plugins) { if (!plugin.handleScript(script)) { continue; } 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(i18nString(UIStrings.loadingDebugSymbolsForVia, { PH1: plugin.name, PH2: url, PH3: symbolsUrl })); } else { console.log(i18nString(UIStrings.loadingDebugSymbolsFor, { PH1: plugin.name, PH2: url })); } try { const code = (!symbolsUrl && url.startsWith('wasm://')) ? await script.getWasmBytecode() : undefined; const addModuleResult = await plugin.addRawModule(rawModuleId, symbolsUrl, { url, code }); // Check that the handle isn't stale by now. This works because the code that assigns to // `rawModuleHandle` below will run before this code because of the `await` in the preceding // line. This is primarily to avoid logging the message below, which would give the developer // the misleading information that we're done, while in reality it was a stale call that finished. if (rawModuleHandle !== this.#rawModuleHandles.get(rawModuleId)) { return []; } if ('missingSymbolFiles' in addModuleResult) { return { missingSymbolFiles: addModuleResult.missingSymbolFiles }; } const sourceFileURLs = addModuleResult; if (sourceFileURLs.length === 0) { console.warn(i18nString(UIStrings.loadedDebugSymbolsForButDidnt, { PH1: plugin.name, PH2: url })); } else { console.log(i18nString(UIStrings.loadedDebugSymbolsForFound, { PH1: plugin.name, PH2: url, PH3: sourceFileURLs.length })); } return sourceFileURLs; } catch (error) { console.error(i18nString(UIStrings.failedToLoadDebugSymbolsFor, { PH1: plugin.name, PH2: url, PH3: 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... void rawModuleHandle.addRawModulePromise.then(sourceFileURLs => { if (!('missingSymbolFiles' in sourceFileURLs)) { // The script might have disappeared meanwhile... if (script.debuggerModel.scriptForId(script.scriptId) === script) { const modelData = this.#debuggerModelToData.get(script.debuggerModel); if (modelData) { // The DebuggerModel could have disappeared meanwhile... modelData.addSourceFiles(script, sourceFileURLs); void this.#debuggerWorkspaceBinding.updateLocations(script); } } } }); return; } } debuggerResumed(event) { const resumedFrames = Array.from(this.callFrameByStopId.values()).filter(callFrame => callFrame.debuggerModel === event.data); for (const callFrame of resumedFrames) { const stopId = this.stopIdByCallFrame.get(callFrame); assertNotNullOrUndefined(stopId); this.stopIdByCallFrame.delete(callFrame); this.callFrameByStopId.delete(stopId); } } getSourcesForScript(script) { const rawModuleId = rawModuleIdForScript(script); const rawModuleHandle = this.#rawModuleHandles.get(rawModuleId); if (rawModuleHandle) { return rawModuleHandle.addRawModulePromise; } return Promise.resolve(undefined); } 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, }; const stopId = this.stopIdForCallFrame(callFrame); try { const sourceMapping = await plugin.rawLocationToSourceLocation(location); if (sourceMapping.length === 0) { return null; } 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, stopId, type, typeName, icon, plugin); scopes.set(variable.scope, scope); } scope.object().variables.push(variable); } return Array.from(scopes.values()); } catch (error) { Common.Console.Console.instance().error(i18nString(UIStrings.errorInDebuggerLanguagePlugin, { PH1: error.message })); return null; } } async getFunctionInfo(script, location) { const { rawModuleId, plugin } = await this.rawModuleIdAndPluginForScript(script); if (!plugin) { return null; } const rawLocation = { rawModuleId, codeOffset: location.columnNumber - (script.codeOffset() || 0), inlineFrameIndex: 0, }; try { const functionInfo = await plugin.getFunctionInfo(rawLocation); return functionInfo; } catch (error) { Common.Console.Console.instance().warn(i18nString(UIStrings.errorInDebuggerLanguagePlugin, { PH1: error.message })); return { frames: [] }; } } async getInlinedFunctionRanges(rawLocation) { const script = rawLocation.script(); if (!script) { return []; } const { rawModuleId, plugin } = await this.rawModuleIdAndPluginForScript(script); if (!plugin) { return []; } const pluginLocation = { rawModuleId, // RawLocation.#columnNumber is the byte offset in the full raw wasm module. Plugins expect the offset in the code // section, so subtract the offset of the code section in the module here. codeOffset: rawLocation.columnNumber - (script.codeOffset() || 0), }; try { // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // @ts-ignore 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(i18nString(UIStrings.errorInDebuggerLanguagePlugin, { PH1: error.message })); return []; } } async getInlinedCalleesRanges(rawLocation) { const script = rawLocation.script(); if (!script) { return []; } const { rawModuleId, plugin } = await this.rawModuleIdAndPluginForScript(script); if (!plugin) { return []; } const pluginLocation = { rawModuleId, // RawLocation.#columnNumber is the byte offset in the full raw wasm module. Plugins expect the offset in the code // section, so subtract the offset of the code section in the module here. codeOffset: rawLocation.columnNumber - (script.codeOffset() || 0), }; try { // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) // @ts-ignore 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(i18nString(UIStrings.errorInDebuggerLanguagePlugin, { PH1: error.message })); return []; } } async getMappedLines(uiSourceCode) { const rawModuleIds = await Promise.all(this.scriptsForUISourceCode(uiSourceCode).map(s => this.rawModuleIdAndPluginForScript(s))); let mappedLines = null; for (const { rawModuleId, plugin } of rawModuleIds) { if (!plugin) { continue; } const lines = await plugin.getMappedLines(rawModuleId, uiSourceCode.url()); if (lines === undefined) { continue; } if (mappedLines === null) { mappedLines = new Set(lines); } else { lines.forEach(l => mappedLines.add(l)); } } return mappedLines; } } class ModelData { project; uiSourceCodeToScripts; constructor(debuggerModel, workspace) { this.project = new ContentProviderBasedProject(workspace, 'language_plugins::' + debuggerModel.target().id(), Workspace.Workspace.projectTypes.Network, '', false /* isServiceProject */); NetworkProject.setTargetForProject(this.project, debuggerModel.target()); this.uiSourceCodeToScripts = new Map(); } 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 = this.uiSourceCodeToScripts.get(uiSourceCode); if (!scripts.includes(script)) { scripts.push(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(); } getProject() { return this.project; } } //# sourceMappingURL=DebuggerLanguagePlugins.js.map