blob: a9fdd5b0115bbd6e5a0bb33a88ff95ba2a467dfd [file] [log] [blame]
// Copyright 2025 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 Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import {createTarget} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import {MockProtocolBackend} from '../../testing/MockScopeChain.js';
import {SnapshotTester} from '../../testing/SnapshotTester.js';
import * as Bindings from '../bindings/bindings.js';
import * as Workspace from '../workspace/workspace.js';
import * as SourceMapScopes from './source_map_scopes.js';
const {urlString} = Platform.DevToolsPath;
describeWithMockConnection('FunctionCodeResolver', function() {
const snapshotTester = new SnapshotTester(this, import.meta);
const URL = urlString`file:///tmp/example.js`;
let target: SDK.Target.Target;
let backend: MockProtocolBackend;
beforeEach(() => {
const workspace = Workspace.Workspace.WorkspaceImpl.instance();
const targetManager = SDK.TargetManager.TargetManager.instance();
const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace);
const ignoreListManager = Workspace.IgnoreListManager.IgnoreListManager.instance({forceNew: true});
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({
forceNew: true,
resourceMapping,
targetManager,
ignoreListManager,
workspace,
});
backend = new MockProtocolBackend();
target = createTarget();
});
// This was minified with 'esbuild --sourcemap=linked --minify' v0.25.9.
const exampleSource =
`"use strict";function fibonacci(e){return e<=1?1:fibonacci(e-1)+fibonacci(e-2)}const btn=document.querySelector("button"),params=new URLSearchParams(location.search);btn.addEventListener("click",()=>{console.log(fibonacci(Number(params.get("x")))),btn.style.backgroundColor="red"});const input=document.querySelector('input[type="text"]');input.addEventListener("input",()=>{console.log(fibonacci(Number(params.get("x"))))});\n//# sourceMappingURL=file:///tmp/example.js.min.map`;
const exampleRawPerformanceData = new Map([
[
1, new Map([
[1, 1], // "use strict"
[35, 67], // starting } of fibonacci
[43, 23], // e<=1
[50, 1000], // fibonacci(e-1)
[65, 999], // fibonacci(e-2)
[79, 13], // ending } of fibonacci
[213, 5000], // fibonacci(Number(params.get("x")))
[274, 333], // btn.style.backgroundColor="red"
])
],
]);
const exampleSourceMap = {
version: 3,
sources: ['index.js'],
sourcesContent: [
'function fibonacci(num) {\n if (num <= 1) return 1;\n\n return fibonacci(num - 1) + fibonacci(num - 2);\n}\n\nconst btn = document.querySelector(\'button\');\nconst params = new URLSearchParams(location.search);\n\nbtn.addEventListener(\'click\', () => {\n console.log(fibonacci(Number(params.get(\'x\'))));\n btn.style.backgroundColor = \'red\';\n});\n\nconst input = document.querySelector(\'input[type="text"]\');\ninput.addEventListener(\'input\', () => {\n console.log(fibonacci(Number(params.get(\'x\'))));\n});\n'
],
mappings:
'aAAA,SAAS,UAAUA,EAAK,CACtB,OAAIA,GAAO,EAAU,EAEd,UAAUA,EAAM,CAAC,EAAI,UAAUA,EAAM,CAAC,CAC/C,CAEA,MAAM,IAAM,SAAS,cAAc,QAAQ,EACrC,OAAS,IAAI,gBAAgB,SAAS,MAAM,EAElD,IAAI,iBAAiB,QAAS,IAAM,CAClC,QAAQ,IAAI,UAAU,OAAO,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,EAC9C,IAAI,MAAM,gBAAkB,KAC9B,CAAC,EAED,MAAM,MAAQ,SAAS,cAAc,oBAAoB,EACzD,MAAM,iBAAiB,QAAS,IAAM,CACpC,QAAQ,IAAI,UAAU,OAAO,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,CAChD,CAAC',
names: ['num']
};
describe('getFunctionCodeFromLocation', () => {
const source = exampleSource;
const sourceMapUrl = 'file:///tmp/example.js.min.map';
const sourceMapContent = JSON.stringify(exampleSourceMap);
const sourceMapContentButNoSources = JSON.stringify({...exampleSourceMap, sourcesContent: undefined});
const testCases = [
{
name: '[no source maps] lookup named function',
url: URL,
line: 0,
column: 35,
sourceMap: null,
expectedCode: `(e) {\n\treturn e <= 1 ? 1 : fibonacci(e - 1) + fibonacci(e - 2)\n}\n`
},
{
name: '[no source maps] lookup anonymous function',
url: URL,
line: 0,
column: 201,
sourceMap: null,
expectedCode:
`() => {\n\tconsole.log(fibonacci(Number(params.get(\"x\")))),\n\tbtn.style.backgroundColor = \"red\"\n}\n`
},
{
name: '[source maps] lookup named function with generated location',
url: URL,
line: 0,
column: 35,
sourceMap: {url: sourceMapUrl, content: sourceMapContent},
expectedCode:
`fibonacci(num) {\n if (num <= 1) return 1;\n\n return fibonacci(num - 1) + fibonacci(num - 2);\n}\n\n`
},
{
name: '[source maps] lookup named function with original location',
url: urlString`file:///tmp/index.js`,
line: 1,
column: 5,
sourceMap: {url: sourceMapUrl, content: sourceMapContent},
expectedCode:
`fibonacci(num) {\n if (num <= 1) return 1;\n\n return fibonacci(num - 1) + fibonacci(num - 2);\n}\n\n`
},
{
name: '[source maps, no source contents] lookup named function with generated location',
url: URL,
line: 0,
column: 35,
sourceMap: {url: sourceMapUrl, content: sourceMapContentButNoSources},
// TODO: createFromAst does not include function identifiers in the created scope start position.
expectedCode: `(e) {\n\treturn e <= 1 ? 1 : fibonacci(e - 1) + fibonacci(e - 2)\n}\n`
},
{
name: '[source maps, no source contents] lookup named function with original location',
url: urlString`file:///tmp/index.js`,
line: 1,
column: 5,
sourceMap: {url: sourceMapUrl, content: sourceMapContentButNoSources},
expectedCode: `(e) {\n\treturn e <= 1 ? 1 : fibonacci(e - 1) + fibonacci(e - 2)\n}\n`
},
{
name: '[source maps] lookup anonymous function with generated location',
url: URL,
line: 0,
column: 201,
sourceMap: {url: sourceMapUrl, content: sourceMapContent},
expectedCode:
`() => {\n console.log(fibonacci(Number(params.get('x'))));\n btn.style.backgroundColor = 'red';\n}`
},
{
name: '[source maps] lookup anonymous function with original location',
url: urlString`file:///tmp/index.js`,
line: 10,
column: 3,
sourceMap: {url: sourceMapUrl, content: sourceMapContent},
expectedCode:
`() => {\n console.log(fibonacci(Number(params.get('x'))));\n btn.style.backgroundColor = 'red';\n}`
},
{
name: '[source maps, no source contents] lookup anonymous function with generated location',
url: URL,
line: 0,
column: 201,
sourceMap: {url: sourceMapUrl, content: sourceMapContentButNoSources},
expectedCode:
`() => {\n\tconsole.log(fibonacci(Number(params.get(\"x\")))),\n\tbtn.style.backgroundColor = \"red\"\n}\n`
},
{
name: '[source maps, no source contents] lookup anonymous function with original location',
url: urlString`file:///tmp/index.js`,
line: 10,
column: 3,
sourceMap: {url: sourceMapUrl, content: sourceMapContentButNoSources},
expectedCode:
`() => {\n\tconsole.log(fibonacci(Number(params.get(\"x\")))),\n\tbtn.style.backgroundColor = \"red\"\n}\n`
},
];
for (const testCase of testCases) {
it(testCase.name, async function() {
const script = await backend.addScript(target, {url: URL, content: source}, testCase.sourceMap);
// Add raw performance data to script's UISourceCode.
const uiSourceCode =
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().uiSourceCodeForScript(script);
assert.isOk(uiSourceCode);
uiSourceCode.setDecorationData(Workspace.UISourceCode.DecoratorType.PERFORMANCE, exampleRawPerformanceData);
// Add mapped performance data to source map url's UISourceCode.
const sourceMap = script.sourceMap();
if (sourceMap) {
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
assert.isOk(debuggerModel);
const url = sourceMap.sourceURLForSourceIndex(0);
assert.isOk(url);
const uiSourceCode =
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().uiSourceCodeForSourceMapSourceURL(
debuggerModel, url, false);
assert.isOk(uiSourceCode);
const mappedPerformanceData =
Workspace.UISourceCode.createMappedProfileData(exampleRawPerformanceData, (line, column) => {
const entry = sourceMap.findEntry(line, column);
if (entry?.sourceURL) {
return [entry.sourceLineNumber, entry.sourceColumnNumber];
}
return null;
});
uiSourceCode.setDecorationData(Workspace.UISourceCode.DecoratorType.PERFORMANCE, mappedPerformanceData);
}
const code = await SourceMapScopes.FunctionCodeResolver.getFunctionCodeFromLocation(
target, testCase.url, testCase.line, testCase.column, {contextLength: 30, appendProfileData: true});
assert.isOk(code);
assert.strictEqual(code.code, testCase.expectedCode);
snapshotTester.assert(this, code.codeWithContext);
});
}
});
});