Skip to content

Commit 218deec

Browse files
loganfsmythjasonLaster
authored andcommitted
[Breakpoints] Use the union of all mapped and unmapped regions when fetching original breakpoints. (firefox-devtools#8014)
1 parent 69c5dcc commit 218deec

7 files changed

Lines changed: 321 additions & 7 deletions

File tree

packages/devtools-source-map/src/index.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,29 @@ export const getOriginalLocation = async (
7777
options: locationOptions = {}
7878
): Promise<SourceLocation> => _getOriginalLocation(location, options);
7979

80+
export const getGeneratedRangesForOriginal = async (
81+
sourceId: SourceId,
82+
url: string,
83+
mergeUnmappedRegions?: boolean
84+
): Promise<
85+
Array<{
86+
start: {
87+
line: number,
88+
column: number
89+
},
90+
end: {
91+
line: number,
92+
column: number
93+
}
94+
}>
95+
> =>
96+
dispatcher.invoke(
97+
"getGeneratedRangesForOriginal",
98+
sourceId,
99+
url,
100+
mergeUnmappedRegions
101+
);
102+
80103
export const getFileGeneratedRange = async (
81104
originalSource: Source
82105
): Promise<?{ start: any, end: any }> =>

packages/devtools-source-map/src/source-map.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,148 @@ async function getOriginalSourceText(
326326
};
327327
}
328328

329+
/**
330+
* Find the set of ranges on the generated file that map from the original
331+
* file's locations.
332+
*
333+
* @param sourceId - The original ID of the file we are processing.
334+
* @param url - The original URL of the file we are processing.
335+
* @param mergeUnmappedRegions - If unmapped regions are encountered between
336+
* two mappings for the given original file, allow the two mappings to be
337+
* merged anyway. This is useful if you are more interested in the general
338+
* contiguous ranges associated with a file, rather than the specifics of
339+
* the ranges provided by the sourcemap.
340+
*/
341+
const GENERATED_MAPPINGS = new WeakMap();
342+
async function getGeneratedRangesForOriginal(
343+
sourceId: SourceId,
344+
url: string,
345+
mergeUnmappedRegions: boolean = false
346+
): Promise<
347+
Array<{
348+
start: {
349+
line: number,
350+
column: number
351+
},
352+
end: {
353+
line: number,
354+
column: number
355+
}
356+
}>
357+
> {
358+
assert(isOriginalId(sourceId), "Source is not an original source");
359+
360+
const map = await getSourceMap(originalToGeneratedId(sourceId));
361+
if (!map) {
362+
return [];
363+
}
364+
365+
if (!COMPUTED_SPANS.has(map)) {
366+
COMPUTED_SPANS.add(map);
367+
map.computeColumnSpans();
368+
}
369+
370+
const cachedGeneratedMappingsForOriginal = GENERATED_MAPPINGS.get(map);
371+
if (cachedGeneratedMappingsForOriginal) {
372+
return cachedGeneratedMappingsForOriginal;
373+
}
374+
375+
// Gather groups of mappings on the generated file, with new groups created
376+
// if we cross a mapping for a different file.
377+
let currentGroup = [];
378+
const originalGroups = [currentGroup];
379+
map.eachMapping(
380+
mapping => {
381+
if (mapping.source === url) {
382+
currentGroup.push({
383+
start: {
384+
line: mapping.generatedLine,
385+
column: mapping.generatedColumn
386+
},
387+
end: {
388+
line: mapping.generatedLine,
389+
// The lastGeneratedColumn value is an inclusive value so we add
390+
// one to it to get the exclusive end position.
391+
column: mapping.lastGeneratedColumn + 1
392+
}
393+
});
394+
} else if (
395+
typeof mapping.source === "string" &&
396+
currentGroup.length > 0
397+
) {
398+
// If there is a URL, but it is for a _different_ file, we create a
399+
// new group of mappings so that we can tell
400+
currentGroup = [];
401+
originalGroups.push(currentGroup);
402+
}
403+
},
404+
null,
405+
SourceMapConsumer.GENERATED_ORDER
406+
);
407+
408+
const generatedMappingsForOriginal = [];
409+
if (mergeUnmappedRegions) {
410+
// If we don't care about excluding unmapped regions, then we just need to
411+
// create a range that is the fully encompasses each group, ignoring the
412+
// empty space between each individual range.
413+
for (const group of originalGroups) {
414+
if (group.length > 0) {
415+
generatedMappingsForOriginal.push({
416+
start: group[0].start,
417+
end: group[group.length - 1].end
418+
});
419+
}
420+
}
421+
} else {
422+
let lastEntry;
423+
for (const group of originalGroups) {
424+
lastEntry = null;
425+
for (const { start, end } of group) {
426+
const lastEnd = lastEntry
427+
? wrappedMappingPosition(lastEntry.end)
428+
: null;
429+
430+
// If this entry comes immediately after the previous one, extend the
431+
// range of the previous entry instead of adding a new one.
432+
if (
433+
lastEntry &&
434+
lastEnd &&
435+
lastEnd.line === start.line &&
436+
lastEnd.column === start.column
437+
) {
438+
lastEntry.end = end;
439+
} else {
440+
const newEntry = { start, end };
441+
generatedMappingsForOriginal.push(newEntry);
442+
lastEntry = newEntry;
443+
}
444+
}
445+
}
446+
}
447+
448+
GENERATED_MAPPINGS.set(map, generatedMappingsForOriginal);
449+
return generatedMappingsForOriginal;
450+
}
451+
452+
function wrappedMappingPosition(pos: {
453+
line: number,
454+
column: number
455+
}): {
456+
line: number,
457+
column: number
458+
} {
459+
if (pos.column !== Infinity) {
460+
return pos;
461+
}
462+
463+
// If the end of the entry consumes the whole line, treat it as wrapping to
464+
// the next line.
465+
return {
466+
line: pos.line + 1,
467+
column: 0
468+
};
469+
}
470+
329471
async function getFileGeneratedRange(
330472
originalSource: Source
331473
): Promise<?{ start: any, end: any }> {
@@ -394,6 +536,7 @@ module.exports = {
394536
getAllGeneratedLocations,
395537
getOriginalLocation,
396538
getOriginalSourceText,
539+
getGeneratedRangesForOriginal,
397540
getFileGeneratedRange,
398541
applySourceMap,
399542
clearSourceMaps,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"use strict";
2+
3+
var decl = function () {
4+
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
5+
return regeneratorRuntime.wrap(function _callee$(_context) {
6+
while (1) {
7+
switch (_context.prev = _context.next) {
8+
case 0:
9+
console.log("2");
10+
11+
case 1:
12+
case "end":
13+
return _context.stop();
14+
}
15+
}
16+
}, _callee, this);
17+
}));
18+
19+
return function decl() {
20+
return _ref.apply(this, arguments);
21+
};
22+
}();
23+
24+
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
25+
26+
console.log("1");
27+
28+
console.log("3");

packages/devtools-source-map/src/tests/fixtures/intermingled-sources.js.map

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/devtools-source-map/src/tests/source-map.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const {
99
getOriginalURLs,
1010
hasMappedSource,
1111
getOriginalLocation,
12+
getGeneratedRangesForOriginal,
1213
clearSourceMaps
1314
} = require("../source-map");
1415

@@ -51,6 +52,93 @@ describe("source maps", () => {
5152
});
5253
});
5354

55+
describe("getGeneratedRangesForOriginal", () => {
56+
test("the overall generated ranges on the source", async () => {
57+
const urls = await setupBundleFixture("intermingled-sources");
58+
59+
const ranges = await getGeneratedRangesForOriginal(
60+
"intermingled-sources.js/originalSource-01",
61+
urls[0]
62+
);
63+
64+
expect(ranges).toEqual([
65+
{
66+
start: {
67+
line: 4,
68+
column: 69
69+
},
70+
end: {
71+
line: 9,
72+
column: Infinity
73+
}
74+
},
75+
{
76+
start: {
77+
line: 11,
78+
column: 0
79+
},
80+
end: {
81+
line: 17,
82+
column: 3
83+
}
84+
},
85+
{
86+
start: {
87+
line: 19,
88+
column: 18
89+
},
90+
end: {
91+
line: 19,
92+
column: 22
93+
}
94+
},
95+
{
96+
start: {
97+
line: 26,
98+
column: 0
99+
},
100+
end: {
101+
line: 26,
102+
column: Infinity
103+
}
104+
},
105+
{
106+
start: {
107+
line: 28,
108+
column: 0
109+
},
110+
end: {
111+
line: 28,
112+
column: Infinity
113+
}
114+
}
115+
]);
116+
});
117+
118+
test("the merged generated ranges on the source", async () => {
119+
const urls = await setupBundleFixture("intermingled-sources");
120+
121+
const ranges = await getGeneratedRangesForOriginal(
122+
"intermingled-sources.js/originalSource-01",
123+
urls[0],
124+
true
125+
);
126+
127+
expect(ranges).toEqual([
128+
{
129+
start: {
130+
line: 4,
131+
column: 69
132+
},
133+
end: {
134+
line: 28,
135+
column: Infinity
136+
}
137+
}
138+
]);
139+
});
140+
});
141+
54142
describe("hasMappedSource", async () => {
55143
test("has original location", async () => {
56144
await setupBundleFixture("bundle");

packages/devtools-source-map/src/worker.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
getAllGeneratedLocations,
1313
getOriginalLocation,
1414
getOriginalSourceText,
15+
getGeneratedRangesForOriginal,
1516
getFileGeneratedRange,
1617
hasMappedSource,
1718
clearSourceMaps,
@@ -38,6 +39,7 @@ self.onmessage = workerHandler({
3839
getOriginalLocation,
3940
getOriginalSourceText,
4041
getOriginalStackFrames,
42+
getGeneratedRangesForOriginal,
4143
getFileGeneratedRange,
4244
hasMappedSource,
4345
applySourceMap,

src/actions/breakpoints/breakpointPositions.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,39 @@ export function setBreakpointPositions(location: SourceLocation) {
5050

5151
let source = getSourceFromId(getState(), sourceId);
5252

53-
let range;
53+
let results = {};
5454
if (isOriginalId(sourceId)) {
55-
range = await sourceMaps.getFileGeneratedRange(source);
55+
const ranges = await sourceMaps.getGeneratedRangesForOriginal(
56+
sourceId,
57+
source.url,
58+
true
59+
);
5660
sourceId = originalToGeneratedId(sourceId);
5761
source = getSourceFromId(getState(), sourceId);
58-
}
5962

60-
const results = await client.getBreakpointPositions(
61-
source.actors[0],
62-
range
63-
);
63+
// Note: While looping here may not look ideal, in the vast majority of
64+
// cases, the number of ranges here should be very small, and is quite
65+
// likely to only be a single range.
66+
for (const range of ranges) {
67+
// Wrap infinite end positions to the next line to keep things simple
68+
// and because we know we don't care about the end-line whitespace
69+
// in this case.
70+
if (range.end.column === Infinity) {
71+
range.end.line += 1;
72+
range.end.column = 0;
73+
}
74+
75+
const bps = await client.getBreakpointPositions(
76+
source.actors[0],
77+
range
78+
);
79+
for (const line in bps) {
80+
results[line] = (results[line] || []).concat(bps[line]);
81+
}
82+
}
83+
} else {
84+
results = await client.getBreakpointPositions(source.actors[0]);
85+
}
6486

6587
let positions = convertToList(results, sourceId);
6688
positions = await mapLocations(positions, getState(), source, sourceMaps);

0 commit comments

Comments
 (0)