UNPKG

chrome-devtools-frontend

Version:
624 lines (546 loc) • 29.4 kB
// 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 Platform from '../../core/platform/platform.js'; import * as Root from '../../core/root/root.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 {encodeVlqList} from '../../testing/SourceMapEncoder.js'; import {createContentProviderUISourceCode} from '../../testing/UISourceCodeHelpers.js'; import * as Bindings from '../bindings/bindings.js'; import * as SourceMapScopes from '../source_map_scopes/source_map_scopes.js'; import * as Workspace from '../workspace/workspace.js'; const {urlString} = Platform.DevToolsPath; describeWithMockConnection('NameResolver', () => { 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 debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ forceNew: true, resourceMapping, targetManager, }); Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: true, debuggerWorkspaceBinding}); target = createTarget(); backend = new MockProtocolBackend(); }); // Given a function scope <fn-start>,<fn-end> and a nested scope <start>,<end>, // we expect the scope parser to return a list of identifiers of the form [{name, offset}] // for the nested scope. (The nested scope may be the same as the function scope.) // // For example, say we want to assert that the block scope '{let a = x, return a}' // in function 'function f(x) { g(x); {let a = x, return a} }' // - defines and uses variable 'a' at the correct offsets, and // - uses free variable 'x'. // Such assertions could be expressed roughly as follows: // // expect.that( // scopeIdentifiers(functionScope: {start: 10, end: 45}, scope:{start: 21, end: 43}).bound) // .equals([Identifier(name: a, offsets: [27, 41])]). // expect.that( // scopeIdentifiers(functionScope: {start: 10, end: 45}, scope:{start: 21, end: 43}).free) // .equals([Identifier(name: x, offsets: [31])]). // // This is not ideal because the explicit offsets are hard to read and maintain. // To avoid typing the exact offset we encode the offsets in a scope assertion string // that can be easily aligned with the source code. For example, the assertion above // will be written as // source: 'function f(x) { g(x); {let a = x, return a} }' // scopes: ' { < B F B> }' // // In the assertion string, '{' and '}' characters mark the positions of function // offset start and end, '<' and '>' mark the positions of the nested scope // start and end (if '<', '>' are missing then the nested scope is the function scope), // the character 'B', 'F' mark the positions of bound and free identifiers that // we expect to be returned by the scopeIdentifiers function. it('test helper parses identifiers from test descriptor', () => { const source = 'function f(x) { g(x); {let a = x, return a} }'; const scopes = ' { < B F B> }'; const identifiers = getIdentifiersFromScopeDescriptor(source, scopes); assert.deepEqual(identifiers.bound, [ new SourceMapScopes.NamesResolver.IdentifierPositions( 'a', [{lineNumber: 0, columnNumber: 27}, {lineNumber: 0, columnNumber: 41}]), ]); assert.deepEqual(identifiers.free, [ new SourceMapScopes.NamesResolver.IdentifierPositions('x', [{lineNumber: 0, columnNumber: 31}]), ]); }); const tests = [ { name: 'computes identifiers for a simple function', source: 'function f(x) { return x }', scopes: ' {B B }', }, { name: 'computes identifiers for a function with a let local', source: 'function f(x) { let a = 42; return a; }', scopes: ' {B B B }', }, { name: 'computes identifiers for a nested scope', source: 'function f(x) { let outer = x; { let inner = outer; return inner } }', scopes: ' { < BBBBB FFFFF BBBBB > }', }, { name: 'computes identifiers for second nested scope', source: 'function f(x) { { let a = 1; } { let b = x; return b } }', scopes: ' { < B F B > }', }, { name: 'computes identifiers with nested scopes', source: 'function f(x) { let outer = x; { let a = outer; } { let b = x; return b } }', scopes: ' {B BBBBB B BBBBB B }', }, { name: 'computes identifiers with nested scopes, var lifting', source: 'function f(x) { let outer = x; { var b = x; return b } }', scopes: ' {B BBBBB B B B B }', }, { name: 'computes identifiers with nested scopes, var lifting', source: 'function f(x) { let outer = x; { var b = x; return b } }', scopes: ' {B BBBBB B B B B }', }, { name: 'computes identifiers in catch clause', source: 'function f(x) { try { } catch (e) { let a = e + x; } }', scopes: ' { <B B F > }', }, { name: 'computes identifiers in catch clause', source: 'function f(x) { try { } catch (e) { let a = e; return a; } }', scopes: ' { < B F B > }', }, { name: 'computes identifiers in for-let', source: 'function f(x) { for (let i = 0; i < 10; i++) { let j = i; console.log(j)} }', scopes: ' { < B B B B FFFFFFF > }', }, { name: 'computes identifiers in for-let body', source: 'function f(x) { for (let i = 0; i < 10; i++) { let j = i; console.log(j)} }', scopes: ' { < B F FFFFFFF B > }', }, { name: 'computes identifiers in for-var function', source: 'function f(x) { for (var i = 0; i < 10; i++) { let j = i; console.log(j)} }', scopes: ' {B B B B B FFFFFFF }', }, { name: 'computes identifiers in for-let-of', source: 'function f(x) { for (let i of x) { console.log(i)} }', scopes: ' { < B F FFFFFFF B > }', }, { name: 'computes identifiers in nested arrow function', source: 'function f(x) { return (i) => { let j = i; return j } }', scopes: ' { <B B B B > }', }, { name: 'computes identifiers in arrow function', source: 'const f = (x) => { let i = 1; return x + i; }', scopes: ' {B B B B }', }, { name: 'computes identifiers in an arrow function\'s nested scope', source: 'const f = (x) => { let i = 1; { let j = i + x; return j; } }', scopes: ' { < B F F B > }', }, { name: 'computes identifiers in an async arrow function\'s nested scope', source: 'const f = async (x) => { let i = 1; { let j = i + await x; return j; } }', scopes: ' { < B F F B > }', }, { name: 'computes identifiers in a function with yield and await', source: 'async function* f(x) { return yield x + await p; }', scopes: ' {B B F }', }, { name: 'computes identifiers in a function with yield*', source: 'function* f(x) { return yield* g(x) + 2; }', scopes: ' {B F B }', }, ]; const dummyMapContent = JSON.stringify({ version: 3, sources: [], }); for (const test of tests) { it(test.name, async () => { const callFrame = await backend.createCallFrame( target, {url: URL, content: test.source}, test.scopes, {url: 'file:///dummy.map', content: dummyMapContent}); const parsedScopeChain = await SourceMapScopes.NamesResolver.findScopeChainForDebuggerScope(callFrame.scopeChain()[0]); const scope = parsedScopeChain.pop(); assert.exists(scope); const identifiers = await SourceMapScopes.NamesResolver.scopeIdentifiers(callFrame.script, scope, parsedScopeChain); const boundIdentifiers = identifiers?.boundVariables ?? []; const freeIdentifiers = identifiers?.freeVariables ?? []; boundIdentifiers.sort( (l, r) => l.positions[0].lineNumber - r.positions[0].lineNumber || l.positions[0].columnNumber - r.positions[0].columnNumber); freeIdentifiers.sort( (l, r) => l.positions[0].lineNumber - r.positions[0].lineNumber || l.positions[0].columnNumber - r.positions[0].columnNumber); assert.deepEqual(boundIdentifiers, getIdentifiersFromScopeDescriptor(test.source, test.scopes).bound); assert.deepEqual(freeIdentifiers, getIdentifiersFromScopeDescriptor(test.source, test.scopes).free); }); } it('resolves name tokens merged with commas (without source map names)', async () => { const sourceMapUrl = 'file:///tmp/example.js.min.map'; // This was minified with 'esbuild --sourcemap=linked --minify' v0.14.31. const sourceMapContent = JSON.stringify({ version: 3, sources: ['index.js'], sourcesContent: ['function f(par1, par2) {\n console.log(par1, par2);\n}\nf(1, 2);\n'], mappings: 'AAAA,WAAW,EAAM,EAAM,CACrB,QAAQ,IAAI,EAAM,CAAI,CACxB,CACA,EAAE,EAAG,CAAC', names: [], }); const source = `function f(o,n){console.log(o,n)}f(1,2);\n//# sourceMappingURL=${sourceMapUrl}`; const scopes = ' { }'; const scopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 1}, {name: 'n', value: 2}]); const callFrame = await backend.createCallFrame( target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]); const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]); const properties = await resolvedScopeObject.getAllProperties(false, false); const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? []; assert.sameDeepMembers(namesAndValues, [{name: 'par1', value: 1}, {name: 'par2', value: 2}]); }); it('resolves name tokens merged with equals (without source map names)', async () => { const sourceMapUrl = 'file:///tmp/example.js.min.map'; // This was minified with 'esbuild --sourcemap=linked --minify' v0.14.31. const sourceMapContent = JSON.stringify({ version: 3, sources: ['index.js'], sourcesContent: ['function f(n) {\n for (let i = 0; i < n; i++) {\n console.log("hi");\n }\n}\nf(10);\n'], mappings: 'AAAA,WAAW,EAAG,CACZ,OAAS,GAAI,EAAG,EAAI,EAAG,IACrB,QAAQ,IAAI,IAAI,CAEpB,CACA,EAAE,EAAE', names: [], }); const source = `function f(i){for(let o=0;o<i;o++)console.log("hi")}f(10);\n//# sourceMappingURL=${sourceMapUrl}`; const scopes = ' { < >}'; const scopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 4}]); const callFrame = await backend.createCallFrame( target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]); const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]); const properties = await resolvedScopeObject.getAllProperties(false, false); const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? []; assert.sameDeepMembers(namesAndValues, [{name: 'i', value: 4}]); }); it('resolves name tokens with source map names', async () => { const sourceMapUrl = 'file:///tmp/example.js.min.map'; // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map" --toplevel' v5.7.0. const sourceMapContent = JSON.stringify({ version: 3, names: ['f', 'par1', 'par2', 'console', 'log'], sources: ['index.js'], sourcesContent: ['function f(par1, par2) {\n console.log(par1, par2);\n}\nf(1, 2);\n'], mappings: 'AAAA,SAASA,EAAEC,EAAMC,GACfC,QAAQC,IAAIH,EAAMC,GAEpBF,EAAE,EAAG', }); const source = `function o(o,n){console.log(o,n)}o(1,2);\n//# sourceMappingURL=${sourceMapUrl}`; const scopes = ' { }'; const scopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 1}, {name: 'n', value: 2}]); const callFrame = await backend.createCallFrame( target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]); const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]); const properties = await resolvedScopeObject.getAllProperties(false, false); const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? []; assert.sameDeepMembers(namesAndValues, [{name: 'par1', value: 1}, {name: 'par2', value: 2}]); }); it('resolves names in constructors with super call', async () => { const sourceMapUrl = 'file:///tmp/example.js.min.map'; // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map"' v5.7.0. const sourceMapContent = JSON.stringify({ version: 3, names: ['C', 'B', 'constructor', 'par1', 'super', 'console', 'log'], sources: ['index.js'], mappings: 'AAAA,MAAMA,UAAUC,EACdC,YAAYC,GACVC,MAAMD,GACNE,QAAQC,IAAIH', }); const source = `class C extends B{constructor(s){super(s),console.log(s)}}\n//# sourceMappingURL=${sourceMapUrl}`; const scopes = ' { }'; const scopeObject = backend.createSimpleRemoteObject([{name: 's', value: 42}]); const callFrame = await backend.createCallFrame( target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]); const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]); const properties = await resolvedScopeObject.getAllProperties(false, false); const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? []; assert.sameDeepMembers(namesAndValues, [{name: 'par1', value: 42}]); }); it('resolves names for variables in TDZ', async () => { const sourceMapUrl = 'file:///tmp/example.js.min.map'; // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map" v5.7.0. const sourceMapContent = JSON.stringify({ version: 3, names: ['adder', 'arg1', 'arg2', 'console', 'log', 'result'], sources: ['index.js'], sourcesContent: [ 'function adder(arg1, arg2) {\n console.log(arg1, arg2);\n const result = arg1 + arg2;\n return result;\n}\n', ], mappings: 'AAAA,SAASA,MAAMC,EAAMC,GACnBC,QAAQC,IAAIH,EAAMC,GAClB,MAAMG,EAASJ,EAAOC,EACtB,OAAOG,CACT', }); const source = `function adder(n,o){console.log(n,o);const c=n+o;return c}\n//# sourceMappingURL=${sourceMapUrl}`; const scopes = ' { }'; const scopeObject = backend.createSimpleRemoteObject([{name: 'n', value: 42}, {name: 'o', value: 5}, {name: 'c'}]); const callFrame = await backend.createCallFrame( target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]); const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]); const properties = await resolvedScopeObject.getAllProperties(false, false); const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? []; assert.sameDeepMembers( namesAndValues, [{name: 'arg1', value: 42}, {name: 'arg2', value: 5}, {name: 'result', value: undefined}]); }); it('resolves inner scope clashing names from let -> var transpilation', async () => { // This tests the behavior where the TypeScript compiler renames a variable when transforming let-variables // to var-variables to avoid clash, and DevTools then (somewhat questionably) deobfuscates the var variables // back to the original names in the function scope (as opposed to the original block scopes). Ideally, DevTools // would do some scoping inference rather than relying on the pruned scope chain from V8. const sourceMapUrl = 'file:///tmp/index.js.map'; // The source map was obtained with 'tsc --target es5 --sourceMap --inlineSources index.ts'. const sourceMapContent = JSON.stringify({ version: 3, file: 'index.js', sourceRoot: '', sources: ['index.ts'], names: [], mappings: 'AAAA,SAAS,CAAC;IACR,IAAI,GAAG,GAAG,EAAE,CAAC;' + 'IACb,KAAK,IAAI,KAAG,GAAG,CAAC,EAAE,KAAG,GAAG,CAAC,EAAE,KAAG,EAAE,EAAE;' + 'QAChC,OAAO,CAAC,GAAG,CAAC,KAAG,CAAC,CAAC;KAClB;' + 'AACH,CAAC;' + 'AACD,CAAC,EAAE,CAAC', sourcesContent: [ 'function f() {\n let pos = 10;\n for (let pos = 0; pos < 5; pos++) {\n console.log(pos);\n }\n}\nf();\n', ], }); const source: string[] = []; const scopes: string[] = []; source[0] = 'function f() {'; scopes[0] = ' {'; // Mark for scope start. source[1] = ' var pos = 10;'; source[2] = ' for (var pos_1 = 0; pos_1 < 5; pos_1++) {'; source[3] = ' console.log(pos_1);'; source[4] = ' }'; source[5] = '}'; scopes[5] = '}'; // Mark for scope end. source[6] = 'f();'; source[7] = `//# sourceMappingURL=${sourceMapUrl}`; for (let i = 0; i < source.length; i++) { scopes[i] ??= ''; } const scopeObject = backend.createSimpleRemoteObject([{name: 'pos', value: 10}, {name: 'pos_1', value: 4}]); const callFrame = await backend.createCallFrame( target, {url: URL, content: source.join('\n')}, scopes.join('\n'), {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]); const resolvedScopeObject = await SourceMapScopes.NamesResolver.resolveScopeInObject(callFrame.scopeChain()[0]); const properties = await resolvedScopeObject.getAllProperties(false, false); const namesAndValues = properties.properties?.map(p => ({name: p.name, value: p.value?.value})) ?? []; assert.deepEqual(namesAndValues, [{name: 'pos', value: 10}, {name: 'pos', value: 4}]); }); describe('Function name resolving', () => { let callFrame: SDK.DebuggerModel.CallFrame; beforeEach(async () => { const sourceMapUrl = 'file:///tmp/example.js.min.map'; // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map"' v5.7.0. const sourceMapContent = JSON.stringify({ version: 3, names: ['unminified', 'par1', 'par2', 'console', 'log'], sources: ['index.js'], sourcesContent: ['function unminified(par1, par2) {\n console.log(par1, par2);\n}\n'], mappings: 'AAAA,SAASA,EAAWC,EAAMC,GACxBC,QAAQC,IAAIH,EAAMC', }); const source = `function o(o,n){console.log(o,n)}o(1,2);\n//# sourceMappingURL=${sourceMapUrl}`; const scopes = ' { }'; const scopeObject = backend.createSimpleRemoteObject([{name: 's', value: 42}]); callFrame = await backend.createCallFrame( target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]); }); it('resolves function names at scope start for a debugger frame', async () => { const functionName = await SourceMapScopes.NamesResolver.resolveDebuggerFrameFunctionName(callFrame); assert.strictEqual(functionName, 'unminified'); }); it('resolves function names at scope start for a profiler frame', async () => { const scopeLocation = callFrame.location(); const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); const script = debuggerModel?.scripts()[0]; const scriptId = script?.scriptId; if (scriptId === undefined) { assert.fail('Script id not found'); return; } const {lineNumber, columnNumber} = scopeLocation; await script?.requestContentData(); const functionName = await SourceMapScopes.NamesResolver.resolveProfileFrameFunctionName( {scriptId, columnNumber, lineNumber}, target); assert.strictEqual(functionName, 'unminified'); }); }); describe('Function name resolving from scopes', () => { it('resolves function scope name at scope start for a debugger frame', async () => { Root.Runtime.experiments.enableForTest('use-source-map-scopes'); const sourceMapUrl = 'file:///tmp/example.js.min.map'; const sourceMapContent = JSON.stringify({ version: 3, names: [ '<toplevel>', '<anonymous>', 'log', 'main', ], sources: ['main.js'], sourcesContent: [ '(function () {\n function log(m) {\n console.log(m);\n }\n\n function main() {\n\t log("hello");\n\t log("world");\n }\n \n main();\n})();', ], mappings: 'CAAA,WACE,SAAS,EAAI,GACX,QAAQ,IAAI,EACd,CAEA,SAAS,IACR,EAAI,SACJ,EAAI,QACL,CAEA,GACD,EAXD', x_com_bloomberg_sourcesFunctionMappings: [[ encodeVlqList([0, 0, 0, 11, 5]), encodeVlqList([1, -11, 1, 11, -4]), encodeVlqList([1, -10, 1, 2, 2]), encodeVlqList([1, 2, 0, 3, 0]), ].join(',')], }); const source = '(function(){function o(o){console.log(o)}function n(){o("hello");o("world")}n()})();\n'; const scopes = ' { }'; const callFrame = await backend.createCallFrame( target, {url: URL, content: source + `//# sourceMappingURL=${sourceMapUrl}`}, scopes, {url: sourceMapUrl, content: sourceMapContent}); const functionName = await SourceMapScopes.NamesResolver.resolveDebuggerFrameFunctionName(callFrame); assert.strictEqual(functionName, 'main'); Root.Runtime.experiments.disableForTest('use-source-map-scopes'); }); }); it('ignores the argument name during arrow function name resolution', async () => { const sourceMapUrl = 'file:///tmp/example.js.min.map'; // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map"' v5.7.0. const sourceMapContent = JSON.stringify({ version: 3, names: ['unminified', 'par1', 'console', 'log'], sources: ['index.js'], sourcesContent: ['const unminified = par1 => {\n console.log(par1);\n}\n'], mappings: 'AAAA,MAAMA,EAAaC,IACjBC,QAAQC,IAAIF', }); const source = `const o=o=>{console.log(o)};\n//# sourceMappingURL=${sourceMapUrl}`; const scopes = ' { }'; const scopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 42}]); const callFrame = await backend.createCallFrame( target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapContent}, [scopeObject]); assert.isNull(await SourceMapScopes.NamesResolver.resolveDebuggerFrameFunctionName(callFrame)); }); describe('allVariablesAtPosition', () => { let script: SDK.Script.Script; beforeEach(async () => { const originalContent = ` function mulWithOffset(param1, param2, offset) { const intermediate = param1 * param2; const result = intermediate; if (offset !== undefined) { const intermediate = result + offset; return intermediate; } return result; } `; const sourceMapUrl = 'file:///tmp/example.js.min.map'; // This was minified with 'terser -m -o example.min.js --source-map "includeSources;url=example.min.js.map"' v5.7.0. const sourceMapContent = JSON.stringify({ version: 3, names: ['mulWithOffset', 'param1', 'param2', 'offset', 'intermediate', 'result', 'undefined'], sources: ['example.js'], sourcesContent: [originalContent], mappings: 'AACA,SAASA,cAAcC,EAAQC,EAAQC,GACrC,MAAMC,EAAeH,EAASC,EAC9B,MAAMG,EAASD,EACf,GAAID,IAAWG,UAAW,CACxB,MAAMF,EAAeC,EAASF,EAC9B,OAAOC,CACT,CACA,OAAOC,CACT', }); const scriptContent = 'function mulWithOffset(n,t,e){const f=n*t;const u=f;if(e!==undefined){const n=u+e;return n}return u}'; script = await backend.addScript( target, {url: 'file:///tmp/bundle.js', content: scriptContent}, {url: sourceMapUrl, content: sourceMapContent}); }); it('has the right mapping on a function scope without shadowing', async () => { const location = script.rawLocation(0, 30); // Beginning of function scope. assert.exists(location); const mapping = await SourceMapScopes.NamesResolver.allVariablesAtPosition(location); assert.strictEqual(mapping.get('param1'), 'n'); assert.strictEqual(mapping.get('param2'), 't'); assert.strictEqual(mapping.get('offset'), 'e'); assert.strictEqual(mapping.get('intermediate'), 'f'); assert.strictEqual(mapping.get('result'), 'u'); }); it('has the right mapping in a block scope with shadowing in the authored code', async () => { const location = script.rawLocation(0, 70); // Beginning of block scope. assert.exists(location); const mapping = await SourceMapScopes.NamesResolver.allVariablesAtPosition(location); // Block scope {intermediate} shadows function scope {intermediate}. assert.strictEqual(mapping.get('intermediate'), 'n'); }); it('has the right mapping in a block scope with shadowing in the compiled code', async () => { const location = script.rawLocation(0, 70); // Beginning of block scope. assert.exists(location); const mapping = await SourceMapScopes.NamesResolver.allVariablesAtPosition(location); assert.isNull(mapping.get('param1')); }); }); describe('getTextFor', () => { it('caches Text instances for scripts', async () => { const script = await backend.addScript(target, {url: URL, content: 'console.log(42)'}, null); const text1 = await SourceMapScopes.NamesResolver.getTextFor(script); const text2 = await SourceMapScopes.NamesResolver.getTextFor(script); assert.strictEqual(text1, text2); }); it('caches Text instances for UISourceCodes', async () => { const {uiSourceCode} = createContentProviderUISourceCode( {target, url: URL, mimeType: 'text/typescript', content: 'console.log(42)'}); const text1 = await SourceMapScopes.NamesResolver.getTextFor(uiSourceCode); const text2 = await SourceMapScopes.NamesResolver.getTextFor(uiSourceCode); assert.strictEqual(text1, text2); }); }); }); function getIdentifiersFromScopeDescriptor(source: string, scopeDescriptor: string): { bound: SourceMapScopes.NamesResolver.IdentifierPositions[], free: SourceMapScopes.NamesResolver.IdentifierPositions[], } { const bound = new Map<string, SourceMapScopes.NamesResolver.IdentifierPositions>(); const free = new Map<string, SourceMapScopes.NamesResolver.IdentifierPositions>(); let current = 0; while (current < scopeDescriptor.length) { while (current < scopeDescriptor.length) { if (scopeDescriptor[current] === 'B' || scopeDescriptor[current] === 'F') { break; } current++; } if (current >= scopeDescriptor.length) { break; } const kind = scopeDescriptor[current]; const start = current; let end = start + 1; while (end < scopeDescriptor.length && scopeDescriptor[end] === kind) { end++; } if (kind === 'B') { addPosition(bound, start, end); } else { console.assert(kind === 'F'); addPosition(free, start, end); } current = end + 1; } return {bound: [...bound.values()], free: [...free.values()]}; function addPosition( collection: Map<string, SourceMapScopes.NamesResolver.IdentifierPositions>, start: number, end: number) { const name = source.substring(start, end); let id = collection.get(name); if (!id) { id = new SourceMapScopes.NamesResolver.IdentifierPositions(name); collection.set(name, id); } id.addPosition(0, start); } }