[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 <bmeurer@chromium.org>
Commit-Queue: Jaroslav Sevcik <jarin@chromium.org>
This commit is contained in:
Jaroslav Sevcik
2022-11-21 08:02:46 +00:00
committed by Devtools-frontend LUCI CQ
parent 31648274e3
commit ec855d6e98
4 changed files with 157 additions and 9 deletions
@@ -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)];
}
@@ -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<string, string>();
#sourceMapContents = new Map<string, string>();
@@ -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<SDK.Script.Script> {
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 ?? '',
});
@@ -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",
@@ -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 = `<p>
Hello!
</p>
<script>console.log("Hi!");
debugger;
console.log("There!");
//# sourceURL=test.js
</script>`;
const contentWithoutSourceUrl = `<p>
Hello!
</p>
<script>console.log("Hi!");
debugger;
console.log("There!");
</script>`;
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;
}
});