Skip to content

Commit 5fc506c

Browse files
szuendDevtools-frontend LUCI CQ
authored andcommitted
[scopes] Add 'findOriginalFunctionName' method
This CL adds a new method that takes a generated position and spits out the original function name of the surrounding function. The method works with both GeneratedRanges and mappings. The reason for this is twofold: "pasta" source map scopes only have "OriginalScopes" and no generated ranges. And the proposed extension API in https://crrev.com/c/5774044 also does not contain generated ranges. In both of these cases the implementation utilizes the normal source map "mappings" to get to the original function scope. As a follow-up, we'll consolidate the "ScopesEntry" interface (for "pasta" source maps) and the "SourceMapScopesInfo" class (for the official "scopes" proposal). We'll do this by converting the "pasta" scopes into "OriginalScope" trees. Bug: 40277685 Change-Id: I1a50eaca5c719616db3c2f7eb4e0227cddee2e2f Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6108439 Reviewed-by: Alex Rudenko <[email protected]> Commit-Queue: Simon Zünd <[email protected]>
1 parent 5a6af76 commit 5fc506c

File tree

2 files changed

+147
-1
lines changed

2 files changed

+147
-1
lines changed

front_end/core/sdk/SourceMapScopesInfo.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,4 +737,88 @@ describe('SourceMapScopesInfo', () => {
737737
}
738738
});
739739
});
740+
741+
describe('findOriginalFunctionName', () => {
742+
const [scopeInfoWithRanges, scopeInfoWithMappings] = (function() {
743+
// Separate sandbox, otherwise global beforeEach/afterAll will reset our source map.
744+
const sandbox = sinon.createSandbox();
745+
const sourceMap = sandbox.createStubInstance(SDK.SourceMap.SourceMap);
746+
sourceMap.findEntry.callsFake((line, column) => {
747+
assert.strictEqual(line, 0);
748+
switch (column) {
749+
case 10:
750+
return new SDK.SourceMap.SourceMapEntry(
751+
line, column, /* sourceIndex */ 0, /* sourceUrl */ undefined, /* sourceLine */ 5, /* sourceColumn */ 0);
752+
case 30:
753+
return new SDK.SourceMap.SourceMapEntry(
754+
line, column, /* sourceIndex */ 0, /* sourceUrl */ undefined, /* sourceLine */ 15,
755+
/* sourceColumn */ 2);
756+
case 50:
757+
return new SDK.SourceMap.SourceMapEntry(
758+
line, column, /* sourceIndex */ 0, /* sourceUrl */ undefined, /* sourceLine */ 25,
759+
/* sourceColumn */ 4);
760+
case 110:
761+
return new SDK.SourceMap.SourceMapEntry(
762+
line, column, /* sourceIndex */ 0, /* sourceUrl */ undefined, /* sourceLine */ 55,
763+
/* sourceColumn */ 2);
764+
case 150:
765+
return null;
766+
}
767+
return null;
768+
});
769+
const names: string[] = [];
770+
const originalScopes = [new OriginalScopeBuilder(names)
771+
.start(0, 0, {kind: 'global'})
772+
.start(10, 10, {kind: 'function', name: 'myAuthoredFunction', isStackFrame: true})
773+
.start(20, 15, {kind: 'block'})
774+
.end(30, 3)
775+
.end(40, 1)
776+
.start(50, 10, {kind: 'function', isStackFrame: true})
777+
.end(60, 1)
778+
.end(70, 0)
779+
.build()];
780+
const scopeInfoWithMappings =
781+
SourceMapScopesInfo.parseFromMap(sourceMap, {names, originalScopes, generatedRanges: ''});
782+
const generatedRanges = new GeneratedRangeBuilder(names)
783+
.start(0, 0, {definition: {sourceIdx: 0, scopeIdx: 0}})
784+
.start(0, 20, {definition: {sourceIdx: 0, scopeIdx: 1}})
785+
.start(0, 40, {definition: {sourceIdx: 0, scopeIdx: 2}})
786+
.end(0, 60)
787+
.end(0, 80)
788+
.start(0, 100, {definition: {sourceIdx: 0, scopeIdx: 5}})
789+
.end(0, 120)
790+
.start(0, 140)
791+
.end(0, 160)
792+
.end(0, 180)
793+
.build();
794+
const scopeInfoWithRanges = SourceMapScopesInfo.parseFromMap(sourceMap, {names, originalScopes, generatedRanges});
795+
return [scopeInfoWithRanges, scopeInfoWithMappings];
796+
})();
797+
798+
[{name: 'with GeneratedRanges', scopeInfo: scopeInfoWithRanges},
799+
{name: 'with mappings', scopeInfo: scopeInfoWithMappings},
800+
].forEach(({name, scopeInfo}) => {
801+
describe(name, () => {
802+
it('provides the original name for a position inside a function', () => {
803+
assert.strictEqual(scopeInfo.findOriginalFunctionName({line: 0, column: 30}), 'myAuthoredFunction');
804+
});
805+
806+
it('provides the original name for a position inside a block scope of a function', () => {
807+
assert.strictEqual(scopeInfo.findOriginalFunctionName({line: 0, column: 50}), 'myAuthoredFunction');
808+
});
809+
810+
it('returns null for a position inside the global scope', () => {
811+
assert.isNull(scopeInfo.findOriginalFunctionName({line: 0, column: 10}));
812+
});
813+
814+
it('returns null for a position inside a range with no corresponding original scope', () => {
815+
assert.isNull(scopeInfo.findOriginalFunctionName({line: 0, column: 150}));
816+
});
817+
818+
it('returns the empty string for an unnamed function (not null)', () => {
819+
assert.strictEqual(scopeInfo.findOriginalFunctionName({line: 0, column: 110}), '');
820+
});
821+
});
822+
});
823+
});
740824
});

front_end/core/sdk/SourceMapScopesInfo.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type GeneratedRange,
1414
type OriginalPosition,
1515
type OriginalScope,
16+
type Position,
1617
} from './SourceMapScopes.js';
1718

1819
export class SourceMapScopesInfo {
@@ -32,7 +33,7 @@ export class SourceMapScopesInfo {
3233
static parseFromMap(
3334
sourceMap: SourceMap,
3435
sourceMapJson: Pick<SourceMapV3Object, 'names'|'originalScopes'|'generatedRanges'>): SourceMapScopesInfo {
35-
if (!sourceMapJson.originalScopes || !sourceMapJson.generatedRanges) {
36+
if (!sourceMapJson.originalScopes || sourceMapJson.generatedRanges === undefined) {
3637
throw new Error('Cant create SourceMapScopesInfo without encoded scopes');
3738
}
3839
const scopeTrees = decodeOriginalScopes(sourceMapJson.originalScopes, sourceMapJson.names ?? []);
@@ -234,6 +235,67 @@ export class SourceMapScopesInfo {
234235

235236
return rangeChain;
236237
}
238+
239+
/**
240+
* Returns the authored function name of the function containing the provided generated position.
241+
*/
242+
findOriginalFunctionName({line, column}: Position): string|null {
243+
// There are 2 approaches:
244+
// 1) Find the inner-most generated range containing the provided generated position
245+
// and use it's OriginalScope (then walk it outwards until we hit a function).
246+
// 2) Use the mappings to turn the generated position into an original position.
247+
// Then find the inner-most original scope containing that original position.
248+
// Then walk it outwards until we hit a function.
249+
//
250+
// Both approaches should yield the same result (assuming the mappings are spec compliant
251+
// w.r.t. generated ranges). But in the case of "pasta" scopes and extension provided
252+
// scope info, we only have the OriginalScope parts and mappings without GeneratedRanges.
253+
254+
let originalInnerMostScope: OriginalScope|undefined;
255+
256+
if (this.#generatedRanges.length > 0) {
257+
const rangeChain = this.#findGeneratedRangeChain(line, column);
258+
originalInnerMostScope = rangeChain.at(-1)?.originalScope;
259+
} else {
260+
// No GeneratedRanges. Try to use mappings.
261+
const entry = this.#sourceMap.findEntry(line, column);
262+
if (entry === null || entry.sourceIndex === undefined) {
263+
return null;
264+
}
265+
originalInnerMostScope =
266+
this.#findOriginalScopeChain(
267+
{sourceIndex: entry.sourceIndex, line: entry.sourceLineNumber, column: entry.sourceColumnNumber})
268+
.at(-1);
269+
}
270+
271+
// Walk the original scope chain outwards until we find a function.
272+
for (let originalScope = originalInnerMostScope; originalScope; originalScope = originalScope.parent) {
273+
if (originalScope.isStackFrame) {
274+
return originalScope.name ?? '';
275+
}
276+
}
277+
return null;
278+
}
279+
280+
/**
281+
* Given an original position, this returns all the surrounding original scopes from outer
282+
* to inner.
283+
*/
284+
#findOriginalScopeChain({sourceIndex, line, column}: OriginalPosition): OriginalScope[] {
285+
const result: OriginalScope[] = [];
286+
287+
(function walkScopes(scopes: OriginalScope[]) {
288+
for (const scope of scopes) {
289+
if (!contains(scope, line, column)) {
290+
continue;
291+
}
292+
result.push(scope);
293+
walkScopes(scope.children);
294+
}
295+
})([this.#originalScopes[sourceIndex]]);
296+
297+
return result;
298+
}
237299
}
238300

239301
/**

0 commit comments

Comments
 (0)