UNPKG

chrome-devtools-frontend

Version:
833 lines (723 loc) • 38.8 kB
// 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}), ''); }); }); }); }); });