Skip to content

Commit 736e56f

Browse files
szuendDevtools-frontend LUCI CQ
authored andcommitted
[sdk] Wire up scope info from AST into the source map
This CL wires up the fallback AST-based scope information into the source map. It is slightly awkward: SourceMap and SourceMapManager were on a generic "client" which in reality is either a SDK.Script or an SDK.CSSStyleHeader. To be able to pass the SDK.Script to the SourceMap, we allow creators of the SourceMapManager to pass a SourceMap factory function. This way, the DebuggerModel can pass the SDK.Script. Drive-by: Hide new scope view also behind experiment, since we now have SourceMapScopesInfo objects without proposal scopes. [email protected] Bug: 449575926 Change-Id: I26ba583e36102af293dd487eeb759766e456fce5 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7021137 Reviewed-by: Philip Pfaffe <[email protected]> Commit-Queue: Simon Zünd <[email protected]>
1 parent e0c3af6 commit 736e56f

File tree

8 files changed

+103
-17
lines changed

8 files changed

+103
-17
lines changed

front_end/core/sdk/DebuggerModel.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {Events as ResourceTreeModelEvents, ResourceTreeModel} from './ResourceTr
1616
import {type EvaluationOptions, type EvaluationResult, type ExecutionContext, RuntimeModel} from './RuntimeModel.js';
1717
import {Script} from './Script.js';
1818
import {SDKModel} from './SDKModel.js';
19+
import {SourceMap} from './SourceMap.js';
1920
import {SourceMapManager} from './SourceMapManager.js';
2021
import {Capability, type Target, Type} from './Target.js';
2122

@@ -169,7 +170,10 @@ export class DebuggerModel extends SDKModel<EventTypes> {
169170
this.agent = target.debuggerAgent();
170171
this.#runtimeModel = (target.model(RuntimeModel) as RuntimeModel);
171172

172-
this.#sourceMapManager = new SourceMapManager(target);
173+
this.#sourceMapManager = new SourceMapManager(
174+
target,
175+
(compiledURL, sourceMappingURL, payload, script) =>
176+
new SourceMap(compiledURL, sourceMappingURL, payload, script));
173177

174178
Common.Settings.Settings.instance()
175179
.moduleSetting('pause-on-exception-enabled')

front_end/core/sdk/SourceMap.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import * as Formatter from '../../models/formatter/formatter.js';
56
import * as TextUtils from '../../models/text_utils/text_utils.js';
67
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
78
import {encodeSourceMap} from '../../testing/SourceMapEncoder.js';
@@ -1347,4 +1348,30 @@ describeWithEnvironment('SourceMap', () => {
13471348

13481349
assert.doesNotThrow(() => sourceMap.mappings());
13491350
});
1351+
1352+
it('builds scopes fallback when the source map does not have any scope information', async () => {
1353+
const scopeTreeStub = sinon.stub(Formatter.FormatterWorkerPool.formatterWorkerPool(), 'javaScriptScopeTree')
1354+
.returns(Promise.resolve({start: 0, end: 10, variables: [], kind: 1, children: []}));
1355+
const script = sinon.createStubInstance(SDK.Script.Script, {
1356+
requestContentData: Promise.resolve(
1357+
new TextUtils.ContentData.ContentData('function f() { console.log("hello"); }', false, 'text/javascript'))
1358+
});
1359+
const sourceMap = new SDK.SourceMap.SourceMap(
1360+
compiledUrl, sourceMapJsonUrl, {
1361+
version: 3,
1362+
mappings: 'ACAA', // [0, 1, 0, 0]
1363+
sources: [],
1364+
names: [],
1365+
},
1366+
script);
1367+
1368+
sinon.assert.notCalled(scopeTreeStub);
1369+
1370+
// Trigger processing.
1371+
assert.isNotNull(sourceMap.findEntry(0, 0));
1372+
await new Promise(r => setTimeout(r, 0)); // Wait one event loop tick.
1373+
1374+
assert.isTrue(sourceMap.hasScopeInfo());
1375+
sinon.assert.calledOnceWithExactly(scopeTreeStub, 'function f() { console.log("hello"); }', 'script');
1376+
});
13501377
});

front_end/core/sdk/SourceMap.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import * as Platform from '../platform/platform.js';
99
import * as Root from '../root/root.js';
1010

1111
import type {CallFrame, ScopeChainEntry} from './DebuggerModel.js';
12+
import {scopeTreeForScript} from './ScopeTreeCache.js';
13+
import type {Script} from './Script.js';
1214
import {buildOriginalScopes, decodePastaRanges, type NamedFunctionRange} from './SourceMapFunctionRanges.js';
1315
import {SourceMapScopesInfo} from './SourceMapScopesInfo.js';
1416

@@ -131,18 +133,22 @@ export class SourceMap {
131133
readonly #sourceInfos: SourceInfo[] = [];
132134
readonly #sourceInfoByURL = new Map<Platform.DevToolsPath.UrlString, SourceInfo>();
133135

136+
readonly #script?: Script;
134137
#scopesInfo: SourceMapScopesInfo|null = null;
135138

136139
readonly #debugId?: DebugId;
137140

141+
scopesFallbackPromiseForTest?: Promise<unknown>;
142+
138143
/**
139144
* Implements Source Map V3 model. See https://github.com/google/closure-compiler/wiki/Source-Maps
140145
* for format description.
141146
*/
142147
constructor(
143148
compiledURL: Platform.DevToolsPath.UrlString, sourceMappingURL: Platform.DevToolsPath.UrlString,
144-
payload: SourceMapV3) {
149+
payload: SourceMapV3, script?: Script) {
145150
this.#json = payload;
151+
this.#script = script;
146152
this.#compiledURL = compiledURL;
147153
this.#sourceMappingURL = sourceMappingURL;
148154
this.#baseURL = (Common.ParsedURL.schemeIs(sourceMappingURL, 'data:')) ? compiledURL : sourceMappingURL;
@@ -163,7 +169,7 @@ export class SourceMap {
163169
}
164170

165171
augmentWithScopes(scriptUrl: Platform.DevToolsPath.UrlString, ranges: NamedFunctionRange[]): void {
166-
this.#ensureMappingsProcessed();
172+
this.#ensureSourceMapProcessed();
167173
if (this.#json && this.#json.version > 3) {
168174
throw new Error('Only support augmenting source maps up to version 3.');
169175
}
@@ -212,12 +218,12 @@ export class SourceMap {
212218
}
213219

214220
hasScopeInfo(): boolean {
215-
this.#ensureMappingsProcessed();
216-
return this.#scopesInfo !== null;
221+
this.#ensureSourceMapProcessed();
222+
return this.#scopesInfo !== null && !this.#scopesInfo.isEmpty();
217223
}
218224

219225
findEntry(lineNumber: number, columnNumber: number, inlineFrameIndex?: number): SourceMapEntry|null {
220-
this.#ensureMappingsProcessed();
226+
this.#ensureSourceMapProcessed();
221227
if (inlineFrameIndex && this.#scopesInfo !== null) {
222228
// For inlineFrameIndex != 0 we use the callsite info for the corresponding inlining site.
223229
// Note that the callsite for "inlineFrameIndex" is actually in the previous frame.
@@ -372,20 +378,40 @@ export class SourceMap {
372378
}
373379

374380
mappings(): SourceMapEntry[] {
375-
this.#ensureMappingsProcessed();
381+
this.#ensureSourceMapProcessed();
376382
return this.#mappings ?? [];
377383
}
378384

385+
/**
386+
* If the source map does not contain scope information by itself (e.g. "scopes proposal"
387+
* or "pasta" scopes), then we'll use this getter to calculate basic function name information from
388+
* the AST and mappings.
389+
*/
390+
async #buildScopesFallback(): Promise<SourceMapScopesInfo|null> {
391+
const scopeTreeAndText = this.#script ? await scopeTreeForScript(this.#script) : null;
392+
if (!scopeTreeAndText) {
393+
return null;
394+
}
395+
396+
const {scopeTree, text} = scopeTreeAndText;
397+
return SourceMapScopesInfo.createFromAst(this, scopeTree, text);
398+
}
399+
379400
private reversedMappings(sourceURL: Platform.DevToolsPath.UrlString): number[] {
380-
this.#ensureMappingsProcessed();
401+
this.#ensureSourceMapProcessed();
381402
return this.#sourceInfoByURL.get(sourceURL)?.reverseMappings ?? [];
382403
}
383404

384-
#ensureMappingsProcessed(): void {
405+
#ensureSourceMapProcessed(): void {
385406
if (this.#mappings === null) {
386407
this.#mappings = [];
387408
try {
388409
this.eachSection(this.parseMap.bind(this));
410+
if (!this.hasScopeInfo()) {
411+
this.scopesFallbackPromiseForTest = this.#buildScopesFallback().then(info => {
412+
this.#scopesInfo = info;
413+
});
414+
}
389415
} catch (e) {
390416
console.error('Failed to parse source map', e);
391417
this.#mappings = [];
@@ -728,7 +754,7 @@ export class SourceMap {
728754
}
729755

730756
expandCallFrame(frame: CallFrame): CallFrame[] {
731-
this.#ensureMappingsProcessed();
757+
this.#ensureSourceMapProcessed();
732758
if (this.#scopesInfo === null) {
733759
return [frame];
734760
}
@@ -737,7 +763,7 @@ export class SourceMap {
737763
}
738764

739765
resolveScopeChain(frame: CallFrame): ScopeChainEntry[]|null {
740-
this.#ensureMappingsProcessed();
766+
this.#ensureSourceMapProcessed();
741767
if (this.#scopesInfo === null) {
742768
return null;
743769
}
@@ -746,7 +772,7 @@ export class SourceMap {
746772
}
747773

748774
findOriginalFunctionName(position: ScopesCodec.Position): string|null {
749-
this.#ensureMappingsProcessed();
775+
this.#ensureSourceMapProcessed();
750776
return this.#scopesInfo?.findOriginalFunctionName(position) ?? null;
751777
}
752778
}

front_end/core/sdk/SourceMapManager.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,24 @@ import {type DebugId, parseSourceMap, SourceMap, type SourceMapV3} from './Sourc
1111
import {SourceMapCache} from './SourceMapCache.js';
1212
import {type Target, Type} from './Target.js';
1313

14+
export type SourceMapFactory<T> =
15+
(compiledURL: Platform.DevToolsPath.UrlString, sourceMappingURL: Platform.DevToolsPath.UrlString,
16+
payload: SourceMapV3, client: T) => SourceMap;
17+
1418
export class SourceMapManager<T extends FrameAssociated> extends Common.ObjectWrapper.ObjectWrapper<EventTypes<T>> {
1519
readonly #target: Target;
20+
readonly #factory: SourceMapFactory<T>;
1621
#isEnabled = true;
1722
readonly #clientData = new Map<T, ClientData>();
1823
readonly #sourceMaps = new Map<SourceMap, T>();
1924
#attachingClient: T|null = null;
2025

21-
constructor(target: Target) {
26+
constructor(target: Target, factory?: SourceMapFactory<T>) {
2227
super();
2328

2429
this.#target = target;
30+
this.#factory =
31+
factory ?? ((compiledURL, sourceMappingURL, payload) => new SourceMap(compiledURL, sourceMappingURL, payload));
2532
}
2633

2734
setEnabled(isEnabled: boolean): void {
@@ -109,7 +116,7 @@ export class SourceMapManager<T extends FrameAssociated> extends Common.ObjectWr
109116
loadSourceMap(sourceMapURL, client.debugId(), initiator)
110117
.then(
111118
payload => {
112-
const sourceMap = new SourceMap(sourceURL, sourceMapURL, payload);
119+
const sourceMap = this.#factory(sourceURL, sourceMapURL, payload, client);
113120
if (this.#clientData.get(client) === clientData) {
114121
clientData.sourceMap = sourceMap;
115122
this.#sourceMaps.set(sourceMap, client);
@@ -167,6 +174,10 @@ export class SourceMapManager<T extends FrameAssociated> extends Common.ObjectWr
167174
this.dispatchEventToListeners(Events.SourceMapFailedToAttach, {client});
168175
}
169176
}
177+
178+
waitForSourceMapsProcessedForTest(): Promise<unknown> {
179+
return Promise.all(this.#sourceMaps.keys().map(sourceMap => sourceMap.scopesFallbackPromiseForTest));
180+
}
170181
}
171182

172183
export async function loadSourceMap(

front_end/core/sdk/SourceMapScopesInfo.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ export class SourceMapScopesInfo {
9595
return Boolean(this.#originalScopes[sourceIdx]);
9696
}
9797

98+
isEmpty(): boolean {
99+
return !this.#originalScopes.length && !this.#generatedRanges.length;
100+
}
101+
98102
addOriginalScopesAtIndex(sourceIdx: number, scope: ScopesCodec.OriginalScope): void {
99103
if (!this.#originalScopes[sourceIdx]) {
100104
this.#originalScopes[sourceIdx] = scope;

front_end/models/bindings/CompilerScriptMapping.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type * as Protocol from '../../generated/protocol.js';
88
import {createTarget} from '../../testing/EnvironmentHelpers.js';
99
import {describeWithMockConnection} from '../../testing/MockConnection.js';
1010
import {MockProtocolBackend} from '../../testing/MockScopeChain.js';
11-
import {encodeSourceMap} from '../../testing/SourceMapEncoder.js';
11+
import {encodeSourceMap, waitForAllSourceMapsProcessed} from '../../testing/SourceMapEncoder.js';
1212
import * as TextUtils from '../text_utils/text_utils.js';
1313
import * as Workspace from '../workspace/workspace.js';
1414

@@ -35,6 +35,10 @@ describeWithMockConnection('CompilerScriptMapping', () => {
3535
backend = new MockProtocolBackend();
3636
});
3737

38+
afterEach(async () => {
39+
await waitForAllSourceMapsProcessed();
40+
});
41+
3842
const waitForUISourceCodeAdded =
3943
(url: string, target: SDK.Target.Target): Promise<Workspace.UISourceCode.UISourceCode> =>
4044
debuggerWorkspaceBinding.waitForUISourceCodeAdded(urlString`${url}`, target);

front_end/models/source_map_scopes/NamesResolver.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// found in the LICENSE file.
44

55
import * as Common from '../../core/common/common.js';
6+
import * as Root from '../../core/root/root.js';
67
import * as SDK from '../../core/sdk/sdk.js';
78
import * as Protocol from '../../generated/protocol.js';
89
import * as Bindings from '../bindings/bindings.js';
@@ -370,7 +371,9 @@ export const resolveScopeChain =
370371
return scopeChain;
371372
}
372373

373-
scopeChain = callFrame.script.sourceMap()?.resolveScopeChain(callFrame);
374+
scopeChain = Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.USE_SOURCE_MAP_SCOPES) ?
375+
callFrame.script.sourceMap()?.resolveScopeChain(callFrame) :
376+
null;
374377
if (scopeChain) {
375378
return scopeChain;
376379
}

front_end/testing/SourceMapEncoder.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import type * as SDK from '../core/sdk/sdk.js';
5+
import * as SDK from '../core/sdk/sdk.js';
66

77
const base64Digits = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
88

@@ -130,3 +130,10 @@ export function encodeSourceMap(textMap: string[], sourceRoot?: string): SDK.Sou
130130
return array.length - 1;
131131
}
132132
}
133+
134+
export function waitForAllSourceMapsProcessed(): Promise<unknown> {
135+
return Promise.all(SDK.TargetManager.TargetManager.instance().targets().map(target => {
136+
const model = target.model(SDK.DebuggerModel.DebuggerModel) as SDK.DebuggerModel.DebuggerModel;
137+
return model.sourceMapManager().waitForSourceMapsProcessedForTest();
138+
}));
139+
}

0 commit comments

Comments
 (0)