chrome-devtools-frontend
Version:
Chrome DevTools UI
833 lines (723 loc) • 38.8 kB
text/typescript
// Copyright 2024 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 Protocol from '../../generated/protocol.js';
import {createTarget} from '../../testing/EnvironmentHelpers.js';
import {describeWithMockConnection} from '../../testing/MockConnection.js';
import {GeneratedRangeBuilder, OriginalScopeBuilder} from '../../testing/SourceMapEncoder.js';
import * as Platform from '../platform/platform.js';
import * as SDK from './sdk.js';
const {urlString} = Platform.DevToolsPath;
const {SourceMapScopesInfo} = SDK.SourceMapScopesInfo;
describe('SourceMapScopesInfo', () => {
function parseFromMap(
sourceMap: SDK.SourceMap.SourceMap,
sourceMapJson: Pick<SDK.SourceMap.SourceMapV3Object, 'names'|'originalScopes'|'generatedRanges'>):
SDK.SourceMapScopesInfo.SourceMapScopesInfo {
const {originalScopes, generatedRanges} = SDK.SourceMapScopes.decodeScopes(sourceMapJson);
return new SourceMapScopesInfo(sourceMap, originalScopes, generatedRanges);
}
describe('findInlinedFunctions', () => {
it('returns the single original function name if nothing was inlined', () => {
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global'})
.start(5, 0, {kind: 'function', name: 'foo'})
.end(10, 0)
.end(20, 0)
.build()];
const generatedRanges = new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 1}, isStackFrame: true})
.end(0, 5)
.end(0, 5)
.build();
const info =
parseFromMap(sinon.createStubInstance(SDK.SourceMap.SourceMap), {names, originalScopes, generatedRanges});
assert.deepEqual(info.findInlinedFunctions(0, 3), {originalFunctionName: 'foo', inlinedFunctions: []});
});
it('returns the names of the surrounding function plus all the inlined function names', () => {
// 'foo' calls 'bar', 'bar' calls 'baz'. 'bar' and 'baz' are inlined into 'foo'.
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global'})
.start(10, 0, {kind: 'function', name: 'foo'})
.end(20, 0)
.start(30, 0, {kind: 'function', name: 'bar'})
.end(40, 0)
.start(50, 0, {kind: 'function', name: 'baz'})
.end(60, 0)
.end(70, 0)
.build()];
const generatedRanges =
new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 1}, isStackFrame: true})
.start(0, 5, {definition: {sourceIdx: 0, scopeIdx: 3}, callsite: {sourceIdx: 0, line: 15, column: 0}})
.start(0, 5, {definition: {sourceIdx: 0, scopeIdx: 5}, callsite: {sourceIdx: 0, line: 35, column: 0}})
.end(0, 10)
.end(0, 10)
.end(0, 10)
.end(0, 10)
.build();
const info =
parseFromMap(sinon.createStubInstance(SDK.SourceMap.SourceMap), {names, originalScopes, generatedRanges});
assert.deepEqual(info.findInlinedFunctions(0, 4), {originalFunctionName: 'foo', inlinedFunctions: []});
assert.deepEqual(info.findInlinedFunctions(0, 7), {
originalFunctionName: 'foo',
inlinedFunctions: [
{name: 'baz', callsite: {sourceIndex: 0, line: 35, column: 0}},
{name: 'bar', callsite: {sourceIndex: 0, line: 15, column: 0}},
],
});
});
});
describeWithMockConnection('expandCallFrame', () => {
function setUpCallFrame(generatedPausedPosition: {line: number, column: number}, name: string) {
const target = createTarget();
const callFrame = new SDK.DebuggerModel.CallFrame(
target.model(SDK.DebuggerModel.DebuggerModel)!, sinon.createStubInstance(SDK.Script.Script), {
callFrameId: '0' as Protocol.Debugger.CallFrameId,
location: {
lineNumber: generatedPausedPosition.line,
columnNumber: generatedPausedPosition.column,
scriptId: '0' as Protocol.Runtime.ScriptId,
},
functionName: name,
scopeChain: [],
this: {type: Protocol.Runtime.RemoteObjectType.Undefined},
url: '',
},
undefined, name);
return callFrame;
}
it('does nothing for frames that don\'t contain inlined code', () => {
//
// orig. code gen. code
// 10 20 10 20 30
// 012345678901234567890 0123456789012345678901234567890
//
// 0: function inner() { function n(){print('hello')}
// 1: print('hello'); function m(){if(true){n()}}
// 2: } m();
// 3:
// 4: function outer() {
// 5: if (true) {
// 6: inner();
// 7: }
// 8: }
// 9:
// 10: outer();
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global'})
.start(0, 14, {kind: 'function', name: 'inner'})
.end(2, 1)
.start(4, 14, {kind: 'function', name: 'outer'})
.start(5, 12, {kind: 'block'})
.end(7, 3)
.end(8, 1)
.end(11, 0)
.build()];
const generatedRanges = new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 10, {definition: {sourceIdx: 0, scopeIdx: 1}, isStackFrame: true})
.end(0, 28)
.start(1, 10, {definition: {sourceIdx: 0, scopeIdx: 3}, isStackFrame: true})
.start(1, 21, {definition: {sourceIdx: 0, scopeIdx: 4}})
.end(1, 26)
.end(1, 27)
.end(3, 0)
.build();
const info =
parseFromMap(sinon.createStubInstance(SDK.SourceMap.SourceMap), {names, originalScopes, generatedRanges});
{
const callFrame = setUpCallFrame({line: 0, column: 13}, 'n'); // Pause on 'print'.
const expandedFrames = info.expandCallFrame(callFrame);
assert.lengthOf(expandedFrames, 1);
assert.strictEqual(expandedFrames[0].functionName, 'inner');
assert.strictEqual(expandedFrames[0].inlineFrameIndex, 0);
}
{
const callFrame = setUpCallFrame({line: 1, column: 22}, 'm'); // Pause on 'n()'.
const expandedFrames = info.expandCallFrame(callFrame);
assert.lengthOf(expandedFrames, 1);
assert.strictEqual(expandedFrames[0].functionName, 'outer');
assert.strictEqual(expandedFrames[0].inlineFrameIndex, 0);
}
{
const callFrame = setUpCallFrame({line: 2, column: 0}, ''); // Pause on 'm()'.
const expandedFrames = info.expandCallFrame(callFrame);
assert.lengthOf(expandedFrames, 1);
assert.strictEqual(expandedFrames[0].functionName, '');
assert.strictEqual(expandedFrames[0].inlineFrameIndex, 0);
}
});
it('returns two frames for a function inlined into another', () => {
//
// orig. code gen. code
// 10 20 10 20 30 40
// 012345678901234567890 01234567890123456789012345678901234567890
//
// 0: function inner() { function m(){if(true){print('hello')}}
// 1: print('hello'); m();
// 2: }
// 3:
// 4: function outer() {
// 5: if (true) {
// 6: inner();
// 7: }
// 8: }
// 9:
// 10: outer();
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global'})
.start(0, 14, {kind: 'function', name: 'inner'})
.end(2, 1)
.start(4, 14, {kind: 'function', name: 'outer'})
.start(5, 12, {kind: 'block'})
.end(7, 3)
.end(8, 1)
.end(11, 0)
.build()];
const generatedRanges =
new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 10, {definition: {sourceIdx: 0, scopeIdx: 3}, isStackFrame: true})
.start(0, 21, {definition: {sourceIdx: 0, scopeIdx: 4}})
.start(0, 22, {definition: {sourceIdx: 0, scopeIdx: 1}, callsite: {sourceIdx: 0, line: 6, column: 4}})
.end(0, 36)
.end(0, 37)
.end(0, 38)
.end(2, 0)
.build();
const info =
parseFromMap(sinon.createStubInstance(SDK.SourceMap.SourceMap), {names, originalScopes, generatedRanges});
{
const callFrame = setUpCallFrame({line: 0, column: 22}, 'm'); // Pause on 'print'.
const expandedFrames = info.expandCallFrame(callFrame);
assert.lengthOf(expandedFrames, 2);
assert.strictEqual(expandedFrames[0].functionName, 'inner');
assert.strictEqual(expandedFrames[0].inlineFrameIndex, 0);
assert.strictEqual(expandedFrames[1].functionName, 'outer');
assert.strictEqual(expandedFrames[1].inlineFrameIndex, 1);
}
{
const callFrame = setUpCallFrame({line: 0, column: 13}, 'm'); // Pause on 'if'.
const expandedFrames = info.expandCallFrame(callFrame);
assert.lengthOf(expandedFrames, 1);
assert.strictEqual(expandedFrames[0].functionName, 'outer');
assert.strictEqual(expandedFrames[0].inlineFrameIndex, 0);
}
{
const callFrame = setUpCallFrame({line: 1, column: 0}, 'm'); // Pause on 'm'.
const expandedFrames = info.expandCallFrame(callFrame);
assert.lengthOf(expandedFrames, 1);
assert.strictEqual(expandedFrames[0].functionName, '');
assert.strictEqual(expandedFrames[0].inlineFrameIndex, 0);
}
});
it('returns three frames for two functions inlined into the global scope', () => {
//
// orig. code gen. code
// 10 20 10 20
// 012345678901234567890 012345678901234567890
//
// 0: function inner() { print('hello')
// 1: print('hello');
// 2: }
// 3:
// 4: function outer() {
// 5: if (true) {
// 6: inner();
// 7: }
// 8: }
// 9:
// 10: outer();
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global'})
.start(0, 14, {kind: 'function', name: 'inner'})
.end(2, 1)
.start(4, 14, {kind: 'function', name: 'outer'})
.start(5, 12, {kind: 'block'})
.end(7, 3)
.end(8, 1)
.end(11, 0)
.build()];
const generatedRanges =
new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 3}, callsite: {sourceIdx: 0, line: 10, column: 0}})
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 4}})
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 1}, callsite: {sourceIdx: 0, line: 6, column: 4}})
.end(0, 14)
.end(0, 14)
.end(0, 14)
.end(1, 0)
.build();
const info =
parseFromMap(sinon.createStubInstance(SDK.SourceMap.SourceMap), {names, originalScopes, generatedRanges});
{
const callFrame = setUpCallFrame({line: 0, column: 0}, ''); // Pause on 'print'.
const expandedFrames = info.expandCallFrame(callFrame);
assert.lengthOf(expandedFrames, 3);
assert.strictEqual(expandedFrames[0].functionName, 'inner');
assert.strictEqual(expandedFrames[0].inlineFrameIndex, 0);
assert.strictEqual(expandedFrames[1].functionName, 'outer');
assert.strictEqual(expandedFrames[1].inlineFrameIndex, 1);
assert.strictEqual(expandedFrames[2].functionName, '');
assert.strictEqual(expandedFrames[2].inlineFrameIndex, 2);
}
});
});
describe('hasVariablesAndBindings', () => {
it('returns false for scope info without variables or bindings', () => {
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global'})
.start(10, 0, {kind: 'function', name: 'foo'})
.end(20, 0)
.end(30, 0)
.build()];
const generatedRanges = new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 10, {definition: {sourceIdx: 0, scopeIdx: 1}, isStackFrame: true})
.end(0, 20)
.end(0, 30)
.build();
const info =
parseFromMap(sinon.createStubInstance(SDK.SourceMap.SourceMap), {names, originalScopes, generatedRanges});
assert.isFalse(info.hasVariablesAndBindings());
});
it('returns false for scope info with variables but no bindings', () => {
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global'})
.start(10, 0, {kind: 'function', name: 'foo', variables: ['variable1', 'variable2']})
.end(20, 0)
.end(30, 0)
.build()];
const generatedRanges = new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 10, {definition: {sourceIdx: 0, scopeIdx: 1}, isStackFrame: true})
.end(0, 20)
.end(0, 30)
.build();
const info =
parseFromMap(sinon.createStubInstance(SDK.SourceMap.SourceMap), {names, originalScopes, generatedRanges});
assert.isFalse(info.hasVariablesAndBindings());
});
it('returns true for scope info with variables and bindings', () => {
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global'})
.start(10, 0, {kind: 'function', name: 'foo', variables: ['variable1', 'variable2']})
.end(20, 0)
.end(30, 0)
.build()];
const generatedRanges =
new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 10, {definition: {sourceIdx: 0, scopeIdx: 1}, isStackFrame: true, bindings: ['a', 'b']})
.end(0, 20)
.end(0, 30)
.build();
const info =
parseFromMap(sinon.createStubInstance(SDK.SourceMap.SourceMap), {names, originalScopes, generatedRanges});
assert.isTrue(info.hasVariablesAndBindings());
});
});
describeWithMockConnection('resolveMappedScopeChain', () => {
function setUpCallFrameAndSourceMap(options: {
generatedPausedPosition: {line: number, column: number},
mappedPausedPosition?: {sourceIndex: number, line: number, column: number},
returnValue?: SDK.RemoteObject.RemoteObject,
}) {
const callFrame = sinon.createStubInstance(SDK.DebuggerModel.CallFrame);
const target = createTarget();
callFrame.debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel)!;
const {generatedPausedPosition, mappedPausedPosition, returnValue} = options;
callFrame.location.returns(new SDK.DebuggerModel.Location(
callFrame.debuggerModel, '0' as Protocol.Runtime.ScriptId, generatedPausedPosition.line,
generatedPausedPosition.column));
callFrame.returnValue.returns(returnValue ?? null);
const sourceMap = sinon.createStubInstance(SDK.SourceMap.SourceMap);
if (mappedPausedPosition) {
sourceMap.findEntry.returns({
lineNumber: generatedPausedPosition.line,
columnNumber: generatedPausedPosition.column,
sourceIndex: mappedPausedPosition.sourceIndex,
sourceLineNumber: mappedPausedPosition.line,
sourceColumnNumber: mappedPausedPosition.column,
sourceURL: urlString``,
name: undefined,
});
} else {
sourceMap.findEntry.returns(null);
}
return {sourceMap, callFrame};
}
it('returns null when the inner-most generated range doesn\'t have an original scope', () => {
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names).start(0, 0, {kind: 'global'}).end(20, 0).build()];
const generatedRanges = new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 10) // Small range that doesn't map to anything.
.end(0, 20)
.end(0, 100)
.build();
const {sourceMap, callFrame} = setUpCallFrameAndSourceMap({generatedPausedPosition: {line: 0, column: 15}});
const info = parseFromMap(sourceMap, {names, originalScopes, generatedRanges});
const scopeChain = info.resolveMappedScopeChain(callFrame);
assert.isNull(scopeChain);
});
it('returns the original global scope when paused in the global scope', () => {
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names).start(0, 0, {kind: 'global'}).end(20, 0).build()];
const generatedRanges =
new GeneratedRangeBuilder(names).start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}}).end(0, 100).build();
const {sourceMap, callFrame} = setUpCallFrameAndSourceMap({
generatedPausedPosition: {line: 0, column: 50},
mappedPausedPosition: {sourceIndex: 0, line: 10, column: 0},
});
const info = parseFromMap(sourceMap, {names, originalScopes, generatedRanges});
const scopeChain = info.resolveMappedScopeChain(callFrame);
assert.isNotNull(scopeChain);
assert.lengthOf(scopeChain, 1);
assert.strictEqual(scopeChain[0].type(), Protocol.Debugger.ScopeType.Global);
});
it('returns the inner-most function scope as type "Local" and surrounding function scopes as type "Closure"',
() => {
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'function', name: 'outer'})
.start(5, 0, {kind: 'function', name: 'inner'})
.end(15, 0)
.end(20, 0)
.build()];
const generatedRanges = new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 25, {definition: {sourceIdx: 0, scopeIdx: 1}})
.end(0, 75)
.end(0, 100)
.build();
const {sourceMap, callFrame} = setUpCallFrameAndSourceMap({
generatedPausedPosition: {line: 0, column: 50},
mappedPausedPosition: {sourceIndex: 0, line: 10, column: 0},
});
const info = parseFromMap(sourceMap, {names, originalScopes, generatedRanges});
const scopeChain = info.resolveMappedScopeChain(callFrame);
assert.isNotNull(scopeChain);
assert.lengthOf(scopeChain, 2);
assert.strictEqual(scopeChain[0].type(), Protocol.Debugger.ScopeType.Local);
assert.strictEqual(scopeChain[0].name(), 'inner');
assert.strictEqual(scopeChain[1].type(), Protocol.Debugger.ScopeType.Closure);
assert.strictEqual(scopeChain[1].name(), 'outer');
});
it('drops inner block scopes if a return value is present to account for V8 oddity', () => {
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'function', name: 'someFn'})
.start(5, 0, {kind: 'block'})
.end(15, 0)
.end(20, 0)
.build()];
const generatedRanges = new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 25, {definition: {sourceIdx: 0, scopeIdx: 1}})
.end(0, 75)
.end(0, 100)
.build();
const {sourceMap, callFrame} = setUpCallFrameAndSourceMap({
generatedPausedPosition: {line: 0, column: 50},
mappedPausedPosition: {sourceIndex: 0, line: 10, column: 0},
returnValue: new SDK.RemoteObject.LocalJSONObject(42),
});
const info = parseFromMap(sourceMap, {names, originalScopes, generatedRanges});
const scopeChain = info.resolveMappedScopeChain(callFrame);
assert.isNotNull(scopeChain);
assert.lengthOf(scopeChain, 1);
assert.strictEqual(scopeChain[0].type(), Protocol.Debugger.ScopeType.Local);
});
it('prefers inner ranges when the range chain has multiple ranges for the same original scope', async () => {
// This frequently happens when transpiling async/await or generators.
//
// orig. scope gen. ranges
//
// | global | global
// | |
// | | someFn | | someFn
// | | | |
// | x (mapped paused position) | | | someFn
// | | | | |
// | | | x (V8 paused position)
// | | | |
// | | |
// | |
//
// Expectation: Report global scope and function scope for 'someFn'. Use bindings from inner 'someFn' range.
//
// TODO(crbug.com/40277685): Combine the ranges as some variables might be available in one range, but
// not the other. This requires us to be able to evaluate binding expressions in arbitrary
// CDP scopes to work well.
const names: string[] = [];
const originalScopes =
[new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global'})
.start(10, 0, {kind: 'function', name: 'someFn', variables: ['fooVariable', 'barVariable']})
.end(20, 0)
.end(30, 0)
.build()];
const generatedRanges = new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 20, {definition: {sourceIdx: 0, scopeIdx: 1}, bindings: [undefined, 'b']})
.start(0, 40, {definition: {sourceIdx: 0, scopeIdx: 1}, bindings: ['f', undefined]})
.end(0, 60)
.end(0, 80)
.end(0, 100)
.build();
const {sourceMap, callFrame} = setUpCallFrameAndSourceMap({
generatedPausedPosition: {line: 0, column: 50},
mappedPausedPosition: {sourceIndex: 0, line: 15, column: 0},
});
const info = parseFromMap(sourceMap, {names, originalScopes, generatedRanges});
const scopeChain = info.resolveMappedScopeChain(callFrame);
assert.isNotNull(scopeChain);
assert.lengthOf(scopeChain, 2);
assert.strictEqual(scopeChain[0].type(), Protocol.Debugger.ScopeType.Local);
assert.strictEqual(scopeChain[0].name(), 'someFn');
assert.strictEqual(scopeChain[1].type(), Protocol.Debugger.ScopeType.Global);
// Attempt to get `someFn`s variables and check that we only call callFrame.evaluate once.
callFrame.evaluate.callsFake(({expression}) => {
assert.strictEqual(expression, 'f');
return Promise.resolve({object: new SDK.RemoteObject.LocalJSONObject(42)});
});
const {properties} = await scopeChain[0].object().getAllProperties(
/* accessorPropertiesOnly */ false, /* generatePreview */ true, /* nonIndexedPropertiesOnly */ false);
assert.isNotNull(properties);
assert.lengthOf(properties, 2);
assert.strictEqual(properties[0].name, 'fooVariable');
assert.strictEqual(properties[0].value?.value, 42);
assert.strictEqual(properties[1].name, 'barVariable');
assert.isUndefined(properties[1].value);
assert.isUndefined(properties[1].getter);
assert.isTrue(callFrame.evaluate.calledOnce);
});
it('works when generated ranges from outer scopes overlay ranges from inner scopes', async () => {
// This happens when expressions (but not full functions) are inlined.
//
// orig. scope gen. ranges
//
// | global | global
// | |
// x (mapped paused position) | | someFn
// | | |
// | | someFn | | | global
// | | | | |
// | | | | x (V8 paused position)
// | | | |
// | | |
// | |
//
// Expectation: Report global scope and use bindings from the inner generated range for 'global'.
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global', variables: ['fooConstant', 'barVariable']})
.start(10, 0, {kind: 'function', name: 'someFn'})
.end(20, 0)
.end(30, 0)
.build()];
const generatedRanges = new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}, bindings: ['42', '"n"']})
.start(0, 20, {definition: {sourceIdx: 0, scopeIdx: 1}})
.start(0, 40, {definition: {sourceIdx: 0, scopeIdx: 0}, bindings: ['42', undefined]})
.end(0, 60)
.end(0, 80)
.end(0, 100)
.build();
const {sourceMap, callFrame} = setUpCallFrameAndSourceMap({
generatedPausedPosition: {line: 0, column: 50},
mappedPausedPosition: {sourceIndex: 0, line: 5, column: 0},
});
const info = parseFromMap(sourceMap, {names, originalScopes, generatedRanges});
const scopeChain = info.resolveMappedScopeChain(callFrame);
assert.isNotNull(scopeChain);
assert.lengthOf(scopeChain, 1);
assert.strictEqual(scopeChain[0].type(), Protocol.Debugger.ScopeType.Global);
// Attempt to get the global scope's variables and check that we only call callFrame.evaluate once.
callFrame.evaluate.callsFake(({expression}) => {
assert.strictEqual(expression, '42');
return Promise.resolve({object: new SDK.RemoteObject.LocalJSONObject(42)});
});
const {properties} = await scopeChain[0].object().getAllProperties(
/* accessorPropertiesOnly */ false, /* generatePreview */ true, /* nonIndexedPropertiesOnly */ false);
assert.isNotNull(properties);
assert.lengthOf(properties, 2);
assert.strictEqual(properties[0].name, 'fooConstant');
assert.strictEqual(properties[0].value?.value, 42);
assert.strictEqual(properties[1].name, 'barVariable');
assert.isUndefined(properties[1].value);
assert.isUndefined(properties[1].getter);
});
it('returns the correct scopes for inlined functions', async () => {
//
// orig. code gen. code
// 10 20 10 20
// 012345678901234567890 012345678901234567890
//
// 0: function inner(x) { print(42);debugger;
// 1: print(x);
// 2: debugger;
// 3: }
// 4:
// 5: function outer(y) {
// 6: if (y) {
// 7: inner(y);
// 8: }
// 9: }
// 10:
// 11: outer(42);
//
// Expectation: The scopes for the virtual call frame of outer are accurate.
// In particular we also add a block scope that must be there.
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global', variables: ['inner', 'outer']})
.start(0, 14, {kind: 'function', name: 'inner', variables: ['x']})
.end(3, 1)
.start(5, 14, {kind: 'function', name: 'outer', variables: ['y']})
.start(6, 9, {kind: 'block'})
.end(8, 3)
.end(9, 1)
.end(12, 0)
.build()];
const generatedRanges = new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 0, {
definition: {sourceIdx: 0, scopeIdx: 3},
callsite: {sourceIdx: 0, line: 11, column: 0},
bindings: ['42'],
})
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 4}})
.start(0, 0, {
definition: {sourceIdx: 0, scopeIdx: 1},
callsite: {sourceIdx: 0, line: 7, column: 4},
bindings: ['42'],
})
.end(0, 19)
.end(0, 19)
.end(0, 19)
.end(0, 19)
.build();
const {sourceMap, callFrame} = setUpCallFrameAndSourceMap({
generatedPausedPosition: {line: 0, column: 10},
mappedPausedPosition: {sourceIndex: 0, line: 3, column: 2},
});
const info = parseFromMap(sourceMap, {names, originalScopes, generatedRanges});
{
const scopeChain = info.resolveMappedScopeChain(callFrame);
assert.isNotNull(scopeChain);
assert.lengthOf(scopeChain, 2);
assert.strictEqual(scopeChain[0].type(), Protocol.Debugger.ScopeType.Local);
assert.strictEqual(scopeChain[0].name(), 'inner');
}
// @ts-expect-error stubbing readonly property.
callFrame['inlineFrameIndex'] = 1;
{
const scopeChain = info.resolveMappedScopeChain(callFrame);
assert.isNotNull(scopeChain);
assert.lengthOf(scopeChain, 3);
assert.strictEqual(scopeChain[0].type(), Protocol.Debugger.ScopeType.Block);
assert.strictEqual(scopeChain[1].type(), Protocol.Debugger.ScopeType.Local);
assert.strictEqual(scopeChain[1].name(), 'outer');
}
// @ts-expect-error stubbing readonly property.
callFrame['inlineFrameIndex'] = 2;
{
const scopeChain = info.resolveMappedScopeChain(callFrame);
assert.isNotNull(scopeChain);
assert.lengthOf(scopeChain, 1);
assert.strictEqual(scopeChain[0].type(), Protocol.Debugger.ScopeType.Global);
}
});
});
describe('findOriginalFunctionName', () => {
const [scopeInfoWithRanges, scopeInfoWithMappings] = (function() {
// Separate sandbox, otherwise global beforeEach/afterAll will reset our source map.
const sandbox = sinon.createSandbox();
const sourceMap = sandbox.createStubInstance(SDK.SourceMap.SourceMap);
sourceMap.findEntry.callsFake((line, column) => {
assert.strictEqual(line, 0);
switch (column) {
case 10:
return new SDK.SourceMap.SourceMapEntry(
line, column, /* sourceIndex */ 0, /* sourceUrl */ undefined, /* sourceLine */ 5, /* sourceColumn */ 0);
case 30:
return new SDK.SourceMap.SourceMapEntry(
line, column, /* sourceIndex */ 0, /* sourceUrl */ undefined, /* sourceLine */ 15,
/* sourceColumn */ 2);
case 50:
return new SDK.SourceMap.SourceMapEntry(
line, column, /* sourceIndex */ 0, /* sourceUrl */ undefined, /* sourceLine */ 25,
/* sourceColumn */ 4);
case 110:
return new SDK.SourceMap.SourceMapEntry(
line, column, /* sourceIndex */ 0, /* sourceUrl */ undefined, /* sourceLine */ 55,
/* sourceColumn */ 2);
case 150:
return null;
}
return null;
});
const names: string[] = [];
const originalScopes = [new OriginalScopeBuilder(names)
.start(0, 0, {kind: 'global'})
.start(10, 10, {kind: 'function', name: 'myAuthoredFunction', isStackFrame: true})
.start(20, 15, {kind: 'block'})
.end(30, 3)
.end(40, 1)
.start(50, 10, {kind: 'function', isStackFrame: true})
.end(60, 1)
.end(70, 0)
.build()];
const scopeInfoWithMappings = parseFromMap(sourceMap, {names, originalScopes, generatedRanges: ''});
const generatedRanges = new GeneratedRangeBuilder(names)
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
.start(0, 20, {definition: {sourceIdx: 0, scopeIdx: 1}})
.start(0, 40, {definition: {sourceIdx: 0, scopeIdx: 2}})
.end(0, 60)
.end(0, 80)
.start(0, 100, {definition: {sourceIdx: 0, scopeIdx: 5}})
.end(0, 120)
.start(0, 140)
.end(0, 160)
.end(0, 180)
.build();
const scopeInfoWithRanges = parseFromMap(sourceMap, {names, originalScopes, generatedRanges});
return [scopeInfoWithRanges, scopeInfoWithMappings];
})();
[{name: 'with GeneratedRanges', scopeInfo: scopeInfoWithRanges},
{name: 'with mappings', scopeInfo: scopeInfoWithMappings},
].forEach(({name, scopeInfo}) => {
describe(name, () => {
it('provides the original name for a position inside a function', () => {
assert.strictEqual(scopeInfo.findOriginalFunctionName({line: 0, column: 30}), 'myAuthoredFunction');
});
it('provides the original name for a position inside a block scope of a function', () => {
assert.strictEqual(scopeInfo.findOriginalFunctionName({line: 0, column: 50}), 'myAuthoredFunction');
});
it('returns null for a position inside the global scope', () => {
assert.isNull(scopeInfo.findOriginalFunctionName({line: 0, column: 10}));
});
it('returns null for a position inside a range with no corresponding original scope', () => {
assert.isNull(scopeInfo.findOriginalFunctionName({line: 0, column: 150}));
});
it('returns the empty string for an unnamed function (not null)', () => {
assert.strictEqual(scopeInfo.findOriginalFunctionName({line: 0, column: 110}), '');
});
});
});
});
});