UNPKG

chrome-devtools-frontend

Version:
344 lines (295 loc) • 13.2 kB
// Copyright 2012 The Chromium Authors // 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 Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as TextUtils from '../../core/text_utils/text_utils.js'; import * as Formatter from '../formatter/formatter.js'; import * as Workspace from '../workspace/workspace.js'; import {ContentProviderBasedProject} from './ContentProviderBasedProject.js'; import {type DebuggerSourceMapping, DebuggerWorkspaceBinding} from './DebuggerWorkspaceBinding.js'; import {NetworkProject} from './NetworkProject.js'; import {metadataForURL} from './ResourceUtils.js'; export class ResourceScriptMapping implements DebuggerSourceMapping { readonly debuggerModel: SDK.DebuggerModel.DebuggerModel; #workspace: Workspace.Workspace.WorkspaceImpl; readonly debuggerWorkspaceBinding: DebuggerWorkspaceBinding; readonly #uiSourceCodeToScriptFile: Map<Workspace.UISourceCode.UISourceCode, ResourceScriptFile>; readonly #projects: Map<string, ContentProviderBasedProject>; readonly #scriptToUISourceCode: Map<SDK.Script.Script, Workspace.UISourceCode.UISourceCode>; readonly #eventListeners: Common.EventTarget.EventDescriptor[]; constructor( debuggerModel: SDK.DebuggerModel.DebuggerModel, workspace: Workspace.Workspace.WorkspaceImpl, debuggerWorkspaceBinding: DebuggerWorkspaceBinding) { this.debuggerModel = debuggerModel; this.#workspace = workspace; this.debuggerWorkspaceBinding = debuggerWorkspaceBinding; this.#uiSourceCodeToScriptFile = new Map(); this.#projects = new Map(); this.#scriptToUISourceCode = new Map(); const runtimeModel = debuggerModel.runtimeModel(); this.#eventListeners = [ this.debuggerModel.addEventListener( SDK.DebuggerModel.Events.ParsedScriptSource, event => this.addScript(event.data), this), this.debuggerModel.addEventListener(SDK.DebuggerModel.Events.GlobalObjectCleared, this.globalObjectCleared, this), runtimeModel.addEventListener( SDK.RuntimeModel.Events.ExecutionContextDestroyed, this.executionContextDestroyed, this), runtimeModel.target().targetManager().addEventListener( SDK.TargetManager.Events.INSPECTED_URL_CHANGED, this.inspectedURLChanged, this), ]; } private project(script: SDK.Script.Script): ContentProviderBasedProject { const prefix = script.isContentScript() ? 'js:extensions:' : 'js::'; const projectId = prefix + this.debuggerModel.target().id() + ':' + script.frameId; let project = this.#projects.get(projectId); if (!project) { const projectType = script.isContentScript() ? Workspace.Workspace.projectTypes.ContentScripts : Workspace.Workspace.projectTypes.Network; project = new ContentProviderBasedProject(this.#workspace, projectId, projectType, '' /* displayName */, false /* isServiceProject */); NetworkProject.setTargetForProject(project, this.debuggerModel.target()); this.#projects.set(projectId, project); } return project; } uiSourceCodeForScript(script: SDK.Script.Script): Workspace.UISourceCode.UISourceCode|null { return this.#scriptToUISourceCode.get(script) ?? null; } rawLocationToUILocation(rawLocation: SDK.DebuggerModel.Location): Workspace.UISourceCode.UILocation|null { const script = rawLocation.script(); if (!script) { return null; } const uiSourceCode = this.#scriptToUISourceCode.get(script); if (!uiSourceCode) { return null; } const scriptFile = this.#uiSourceCodeToScriptFile.get(uiSourceCode); if (!scriptFile) { return null; } if (scriptFile.script !== script) { return null; } const {lineNumber, columnNumber = 0} = rawLocation; return uiSourceCode.uiLocation(lineNumber, columnNumber); } uiLocationToRawLocations(uiSourceCode: Workspace.UISourceCode.UISourceCode, lineNumber: number, columnNumber: number): SDK.DebuggerModel.Location[] { const scriptFile = this.#uiSourceCodeToScriptFile.get(uiSourceCode); if (!scriptFile) { return []; } const {script} = scriptFile; if (!script) { return []; } return [this.debuggerModel.createRawLocation(script, lineNumber, columnNumber)]; } uiLocationRangeToRawLocationRanges( uiSourceCode: Workspace.UISourceCode.UISourceCode, {startLine, startColumn, endLine, endColumn}: TextUtils.TextRange.TextRange): SDK.DebuggerModel.LocationRange[]|null { const scriptFile = this.#uiSourceCodeToScriptFile.get(uiSourceCode); if (!scriptFile) { return null; } const {script} = scriptFile; if (!script) { return null; } const start = this.debuggerModel.createRawLocation(script, startLine, startColumn); const end = this.debuggerModel.createRawLocation(script, endLine, endColumn); return [{start, end}]; } private inspectedURLChanged(event: Common.EventTarget.EventTargetEvent<SDK.Target.Target>): void { for (let target: SDK.Target.Target|null = this.debuggerModel.target(); target !== event.data; target = target.parentTarget()) { if (target === null) { return; } } // Just remove and readd all scripts to ensure their URLs are reflected correctly. for (const script of Array.from(this.#scriptToUISourceCode.keys())) { this.removeScripts([script]); this.addScript(script); } } async functionBoundsAtRawLocation(rawLocation: SDK.DebuggerModel.Location): Promise<Workspace.UISourceCode.UIFunctionBounds|null> { const script = rawLocation.script(); if (!script) { return null; } const uiSourceCode = this.#scriptToUISourceCode.get(script); if (!uiSourceCode) { return null; } const scopeTreeAndText = script ? await SDK.ScopeTreeCache.scopeTreeForScript(script) : null; if (!scopeTreeAndText) { return null; } // Find the inner-most scope that maps to the given position. const offset = scopeTreeAndText.text.offsetFromPosition(rawLocation.lineNumber, rawLocation.columnNumber); const results = []; (function walk(nodes: Formatter.FormatterWorkerPool.ScopeTreeNode[]) { for (const node of nodes) { if (!(offset >= node.start && offset < node.end)) { continue; } results.push(node); walk(node.children); } })([scopeTreeAndText.scopeTree]); const result = results.findLast( node => node.kind === Formatter.FormatterWorkerPool.ScopeKind.FUNCTION || node.kind === Formatter.FormatterWorkerPool.ScopeKind.ARROW_FUNCTION); if (!result) { return null; } // Map back to positions. const startPosition = scopeTreeAndText.text.positionFromOffset(result.start); const endPosition = scopeTreeAndText.text.positionFromOffset(result.end); const name = ''; // TODO(crbug.com/452333154): update ScopeVariableAnalysis to include function name. const range = new TextUtils.TextRange.TextRange( startPosition.lineNumber, startPosition.columnNumber, endPosition.lineNumber, endPosition.columnNumber); return new Workspace.UISourceCode.UIFunctionBounds(uiSourceCode, range, name); } private addScript(script: SDK.Script.Script): void { if (script.isBreakpointCondition) { return; } let url = script.sourceURL; if (!url) { return; } if (script.hasSourceURL) { // Try to resolve `//# sourceURL=` annotations relative to // the base URL, according to the sourcemap specification. url = SDK.SourceMapManager.SourceMapManager.resolveRelativeSourceURL(script.debuggerModel.target(), url); } else { // Ignore inline <script>s without `//# sourceURL` annotation here. if (script.isInlineScript()) { return; } // Filter out embedder injected content scripts. if (script.isContentScript()) { const parsedURL = new Common.ParsedURL.ParsedURL(url); if (!parsedURL.isValid) { return; } } } // Remove previous UISourceCode, if any const project = this.project(script); const oldUISourceCode = project.uiSourceCodeForURL(url); if (oldUISourceCode) { const oldScriptFile = this.#uiSourceCodeToScriptFile.get(oldUISourceCode); if (oldScriptFile?.script) { this.removeScripts([oldScriptFile.script]); } } // Create UISourceCode. const originalContentProvider = script.originalContentProvider(); const uiSourceCode = project.createUISourceCode(url, originalContentProvider.contentType()); NetworkProject.setInitialFrameAttribution(uiSourceCode, script.frameId); const metadata = metadataForURL(this.debuggerModel.target(), script.frameId, url); // Bind UISourceCode to scripts. const scriptFile = new ResourceScriptFile(this, uiSourceCode, script); this.#uiSourceCodeToScriptFile.set(uiSourceCode, scriptFile); this.#scriptToUISourceCode.set(script, uiSourceCode); const mimeType = script.isWasm() ? 'application/wasm' : 'text/javascript'; project.addUISourceCodeWithProvider(uiSourceCode, originalContentProvider, metadata, mimeType); void this.debuggerWorkspaceBinding.updateLocations(script); } scriptFile(uiSourceCode: Workspace.UISourceCode.UISourceCode): ResourceScriptFile|null { return this.#uiSourceCodeToScriptFile.get(uiSourceCode) || null; } private removeScripts(scripts: SDK.Script.Script[]): void { const uiSourceCodesByProject = new Platform.MapUtilities.Multimap<ContentProviderBasedProject, Workspace.UISourceCode.UISourceCode>(); for (const script of scripts) { const uiSourceCode = this.#scriptToUISourceCode.get(script); if (!uiSourceCode) { continue; } this.#uiSourceCodeToScriptFile.delete(uiSourceCode); this.#scriptToUISourceCode.delete(script); uiSourceCodesByProject.set(uiSourceCode.project() as ContentProviderBasedProject, uiSourceCode); void this.debuggerWorkspaceBinding.updateLocations(script); } for (const project of uiSourceCodesByProject.keysArray()) { const uiSourceCodes = uiSourceCodesByProject.get(project); // Check if all the ui source codes in the project are in |uiSourceCodes|. let allInProjectRemoved = true; for (const projectSourceCode of project.uiSourceCodes()) { if (!uiSourceCodes.has(projectSourceCode)) { allInProjectRemoved = false; break; } } // Drop the whole project if no source codes are left in it. if (allInProjectRemoved) { this.#projects.delete(project.id()); project.removeProject(); } else { // Otherwise, announce the removal of each UI source code individually. uiSourceCodes.forEach(c => project.removeUISourceCode(c.url())); } } } private executionContextDestroyed(event: Common.EventTarget.EventTargetEvent<SDK.RuntimeModel.ExecutionContext>): void { const executionContext = event.data; this.removeScripts(this.debuggerModel.scriptsForExecutionContext(executionContext)); } private globalObjectCleared(): void { const scripts = Array.from(this.#scriptToUISourceCode.keys()); this.removeScripts(scripts); } resetForTest(): void { this.globalObjectCleared(); } dispose(): void { Common.EventTarget.removeEventListeners(this.#eventListeners); this.globalObjectCleared(); } } export class ResourceScriptFile { readonly #resourceScriptMapping: ResourceScriptMapping; readonly uiSourceCode: Workspace.UISourceCode.UISourceCode; readonly script: SDK.Script.Script|null; constructor( resourceScriptMapping: ResourceScriptMapping, uiSourceCode: Workspace.UISourceCode.UISourceCode, script: SDK.Script.Script) { this.#resourceScriptMapping = resourceScriptMapping; this.uiSourceCode = uiSourceCode; this.script = this.uiSourceCode.contentType().isScript() ? script : null; } addSourceMapURL(sourceMapURL: Platform.DevToolsPath.UrlString): void { if (!this.script) { return; } this.script.debuggerModel.setSourceMapURL(this.script, sourceMapURL); } addDebugInfoURL(debugInfoURL: Platform.DevToolsPath.UrlString): void { if (!this.script) { return; } // eslint-disable-next-line @devtools/no-instance-of-migrated-singletons const {pluginManager} = DebuggerWorkspaceBinding.instance(); pluginManager.setDebugInfoURL(this.script, debugInfoURL); } hasSourceMapURL(): boolean { return Boolean(this.script?.sourceMapURL); } async missingSymbolFiles(): Promise<SDK.DebuggerModel.MissingDebugFiles[]|null> { if (!this.script) { return null; } const {pluginManager} = this.#resourceScriptMapping.debuggerWorkspaceBinding; const sources = await pluginManager.getSourcesForScript(this.script); return sources && 'missingSymbolFiles' in sources ? sources.missingSymbolFiles : null; } }