From ec855d6e9813fd0b4bb3f87fcc0fbb11e176d4b7 Mon Sep 17 00:00:00 2001 From: Jaroslav Sevcik Date: Fri, 18 Nov 2022 15:07:11 +0100 Subject: [PATCH] [sources] Fix location mapping for inline scripts If the debugger stops in an inline script without source URL before the embedding HTML arrives, the stopping position in the source view is off. This adjusts the source positions to account for the fact that for scripts without sourceURL, v8 provides source positions in embedder coordinates, but the devtools maps to the embedded script locations. Bug: chromium:1325457 Change-Id: Ia56bec7fe1c3db678c4167a0ef01d4fffbfccada Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4038144 Reviewed-by: Benedikt Meurer Commit-Queue: Jaroslav Sevcik --- .../models/bindings/DefaultScriptMapping.ts | 11 +- .../front_end/helpers/MockScopeChain.ts | 32 +++-- .../front_end/models/bindings/BUILD.gn | 1 + .../bindings/DefaultScriptMapping_test.ts | 122 ++++++++++++++++++ 4 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 test/unittests/front_end/models/bindings/DefaultScriptMapping_test.ts diff --git a/front_end/models/bindings/DefaultScriptMapping.ts b/front_end/models/bindings/DefaultScriptMapping.ts index 702ac4d798..c2bad5e3f3 100644 --- a/front_end/models/bindings/DefaultScriptMapping.ts +++ b/front_end/models/bindings/DefaultScriptMapping.ts @@ -82,7 +82,12 @@ export class DefaultScriptMapping implements DebuggerSourceMapping { if (!uiSourceCode) { return null; } - const {lineNumber, columnNumber = 0} = rawLocation; + const isInlineScriptWithoutSourceUrl = !script.hasSourceURL && script.isInlineScript(); + const lineNumber = rawLocation.lineNumber - (isInlineScriptWithoutSourceUrl ? script.lineOffset : 0); + let columnNumber = rawLocation.columnNumber || 0; + if (isInlineScriptWithoutSourceUrl && !lineNumber && columnNumber) { + columnNumber -= script.columnOffset; + } return uiSourceCode.uiLocation(lineNumber, columnNumber); } @@ -92,6 +97,10 @@ export class DefaultScriptMapping implements DebuggerSourceMapping { if (!script) { return []; } + if (!script.hasSourceURL && script.isInlineScript()) { + return [this.#debuggerModel.createRawLocation( + script, lineNumber + script.lineOffset, lineNumber ? columnNumber : columnNumber + script.columnOffset)]; + } return [this.#debuggerModel.createRawLocation(script, lineNumber, columnNumber)]; } diff --git a/test/unittests/front_end/helpers/MockScopeChain.ts b/test/unittests/front_end/helpers/MockScopeChain.ts index 2ce611fa04..0aefd4326e 100644 --- a/test/unittests/front_end/helpers/MockScopeChain.ts +++ b/test/unittests/front_end/helpers/MockScopeChain.ts @@ -17,6 +17,14 @@ interface LoadResult { errorDescription: Host.ResourceLoader.LoadErrorDescription; } +interface ScriptDescription { + url: string; + content: string; + hasSourceURL?: boolean; + startLine?: number; + startColumn?: number; +} + export class MockProtocolBackend { #scriptSources = new Map(); #sourceMapContents = new Map(); @@ -36,22 +44,30 @@ export class MockProtocolBackend { }); } - async addScript(target: SDK.Target.Target, script: {url: string, content: string}, sourceMap: { + async addScript(target: SDK.Target.Target, scriptDescription: ScriptDescription, sourceMap: { url: string, content: string, }|null): Promise { const scriptId = 'SCRIPTID.' + this.#nextScriptIndex++; - this.#scriptSources.set(scriptId, script.content); + this.#scriptSources.set(scriptId, scriptDescription.content); + const startLine = scriptDescription.startLine ?? 0; + const startColumn = scriptDescription.startColumn ?? 0; + const endLine = startLine + (scriptDescription.content.match(/^/gm)?.length ?? 1) - 1; + let endColumn = scriptDescription.content.length - scriptDescription.content.lastIndexOf('\n') - 1; + if (startLine === endLine) { + endColumn += startColumn; + } + dispatchEvent(target, 'Debugger.scriptParsed', { scriptId, - url: script.url, - startLine: 0, - startColumn: 0, - endLine: (script.content.match(/^/gm)?.length ?? 1) - 1, - endColumn: script.content.length - script.content.lastIndexOf('\n') - 1, + url: scriptDescription.url, + startLine, + startColumn, + endLine, + endColumn, executionContextId: 1, hash: '', - hasSourceURL: false, + hasSourceURL: Boolean(scriptDescription.hasSourceURL), sourceMapURL: sourceMap?.url ?? '', }); diff --git a/test/unittests/front_end/models/bindings/BUILD.gn b/test/unittests/front_end/models/bindings/BUILD.gn index e87bba1a08..d8578b0964 100644 --- a/test/unittests/front_end/models/bindings/BUILD.gn +++ b/test/unittests/front_end/models/bindings/BUILD.gn @@ -8,6 +8,7 @@ ts_library("bindings") { testonly = true sources = [ "BreakpointManager_test.ts", + "DefaultScriptMapping_test.ts", "FileUtils_test.ts", "IgnoreListManager_test.ts", "LiveLocation_test.ts", diff --git a/test/unittests/front_end/models/bindings/DefaultScriptMapping_test.ts b/test/unittests/front_end/models/bindings/DefaultScriptMapping_test.ts new file mode 100644 index 0000000000..795423394f --- /dev/null +++ b/test/unittests/front_end/models/bindings/DefaultScriptMapping_test.ts @@ -0,0 +1,122 @@ +// Copyright 2022 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 SDK from '../../../../../front_end/core/sdk/sdk.js'; +import * as Bindings from '../../../../../front_end/models/bindings/bindings.js'; +import * as Workspace from '../../../../../front_end/models/workspace/workspace.js'; + +import type * as Platform from '../../../../../front_end/core/platform/platform.js'; +import {assertNotNullOrUndefined} from '../../../../../front_end/core/platform/platform.js'; +import {createTarget} from '../../helpers/EnvironmentHelpers.js'; +import {MockProtocolBackend} from '../../helpers/MockScopeChain.js'; +import {describeWithMockConnection} from '../../helpers/MockConnection.js'; + +describeWithMockConnection('Inline variable view scope helpers', () => { + const url = 'file:///tmp/example.js' as Platform.DevToolsPath.UrlString; + let target: SDK.Target.Target; + let backend: MockProtocolBackend; + let defaultScriptMapping: Bindings.DefaultScriptMapping.DefaultScriptMapping; + const contentWithSourceUrl = `

+ Hello! +

+`; + const contentWithoutSourceUrl = `

+ Hello! +

+`; + + beforeEach(() => { + const workspace = Workspace.Workspace.WorkspaceImpl.instance(); + const targetManager = SDK.TargetManager.TargetManager.instance(); + const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); + const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ + forceNew: true, + resourceMapping, + targetManager, + }); + Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: true, debuggerWorkspaceBinding}); + target = createTarget(); + backend = new MockProtocolBackend(); + defaultScriptMapping = new Bindings.DefaultScriptMapping.DefaultScriptMapping( + target.model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel, + Workspace.Workspace.WorkspaceImpl.instance(), + Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance()); + }); + + it('maps raw locations on first line in inline scripts without sourceURL', async () => { + const {scriptId, debuggerModel} = await backend.addScript( + target, {content: contentWithoutSourceUrl, url, startLine: 3, startColumn: 8, hasSourceURL: false}, null); + const uiLocation = + defaultScriptMapping.rawLocationToUILocation(new SDK.DebuggerModel.Location(debuggerModel, scriptId, 3, 9, 0)); + assert.strictEqual(uiLocation?.lineNumber, 0); + assert.strictEqual(uiLocation?.columnNumber, 1); + }); + + it('maps raw locations in inline scripts without sourceURL', async () => { + const {scriptId, debuggerModel} = await backend.addScript( + target, {content: contentWithoutSourceUrl, url, startLine: 3, startColumn: 8, hasSourceURL: false}, null); + const uiLocation = + defaultScriptMapping.rawLocationToUILocation(new SDK.DebuggerModel.Location(debuggerModel, scriptId, 4, 2, 0)); + assert.strictEqual(uiLocation?.lineNumber, 1); + assert.strictEqual(uiLocation?.columnNumber, 2); + }); + + it('maps raw locations in inline scripts with sourceURL', async () => { + const {scriptId, debuggerModel} = await backend.addScript( + target, {content: contentWithSourceUrl, url, startLine: 3, startColumn: 8, hasSourceURL: true}, null); + const uiLocation = + defaultScriptMapping.rawLocationToUILocation(new SDK.DebuggerModel.Location(debuggerModel, scriptId, 4, 2, 0)); + assert.strictEqual(uiLocation?.lineNumber, 4); + assert.strictEqual(uiLocation?.columnNumber, 2); + }); + + it('maps UI locations on first line in inline scripts without sourceURL', async () => { + const script = await backend.addScript( + target, {content: contentWithoutSourceUrl, url, startLine: 3, startColumn: 8, hasSourceURL: false}, null); + const uiSourceCode = uiSourceCodeFromScript(script); + assertNotNullOrUndefined(uiSourceCode); + + const rawLocations = defaultScriptMapping.uiLocationToRawLocations(uiSourceCode, 0, 1); + assert.strictEqual(rawLocations.length, 1); + assert.strictEqual(rawLocations[0].lineNumber, 3); + assert.strictEqual(rawLocations[0].columnNumber, 9); + }); + + it('maps UI locations in inline scripts without sourceURL', async () => { + const script = await backend.addScript( + target, {content: contentWithoutSourceUrl, url, startLine: 3, startColumn: 8, hasSourceURL: false}, null); + const uiSourceCode = uiSourceCodeFromScript(script); + assertNotNullOrUndefined(uiSourceCode); + + const rawLocations = defaultScriptMapping.uiLocationToRawLocations(uiSourceCode, 1, 2); + assert.strictEqual(rawLocations.length, 1); + assert.strictEqual(rawLocations[0].lineNumber, 4); + assert.strictEqual(rawLocations[0].columnNumber, 2); + }); + + it('maps UI locations in inline scripts with sourceURL', async () => { + const script = await backend.addScript( + target, {content: contentWithSourceUrl, url, startLine: 3, startColumn: 8, hasSourceURL: true}, null); + const uiSourceCode = uiSourceCodeFromScript(script); + assertNotNullOrUndefined(uiSourceCode); + + const rawLocations = defaultScriptMapping.uiLocationToRawLocations(uiSourceCode, 4, 2); + assert.strictEqual(rawLocations.length, 1); + assert.strictEqual(rawLocations[0].lineNumber, 4); + assert.strictEqual(rawLocations[0].columnNumber, 2); + }); + + function uiSourceCodeFromScript(script: SDK.Script.Script): Workspace.UISourceCode.UISourceCode|undefined { + return defaultScriptMapping + .rawLocationToUILocation(new SDK.DebuggerModel.Location(script.debuggerModel, script.scriptId, 0, 0, 0)) + ?.uiSourceCode; + } +});