Skip to content

Commit b255b52

Browse files
fix(sourcemap): fall back to low-resolution line mapping (#4334)
* fix(sourcemap): fall back to low-resolution line mapping …inside `Link.traceSegment` instead of returning null, so that a low-resolution sourcemap preceding a high-resolution sourcemap in the sourcemap chain doesn't fail. * fix: fall back to low resolution mapping in `getOriginalLocation` * Generalize low-resolution sourcemap handling and add tests * Slightly speed up mapping generation * Improve coverage Co-authored-by: Lukas Taegert-Atkinson <[email protected]> Co-authored-by: Lukas Taegert-Atkinson <[email protected]>
1 parent 10dc326 commit b255b52

7 files changed

Lines changed: 131 additions & 47 deletions

File tree

src/utils/collapseSourcemaps.ts

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class Link {
4747

4848
traceMappings() {
4949
const sources: string[] = [];
50+
const sourceIndexMap = new Map<string, number>();
5051
const sourcesContent: string[] = [];
5152
const names: string[] = [];
5253
const nameIndexMap = new Map<string, number>();
@@ -57,7 +58,7 @@ class Link {
5758
const tracedLine: SourceMapSegment[] = [];
5859

5960
for (const segment of line) {
60-
if (segment.length == 1) continue;
61+
if (segment.length === 1) continue;
6162
const source = this.sources[segment[1]];
6263
if (!source) continue;
6364

@@ -68,36 +69,34 @@ class Link {
6869
);
6970

7071
if (traced) {
71-
// newer sources are more likely to be used, so search backwards.
72-
let sourceIndex = sources.lastIndexOf(traced.source.filename);
73-
if (sourceIndex === -1) {
72+
const {
73+
column,
74+
line,
75+
name,
76+
source: { content, filename }
77+
} = traced;
78+
let sourceIndex = sourceIndexMap.get(filename);
79+
if (sourceIndex === undefined) {
7480
sourceIndex = sources.length;
75-
sources.push(traced.source.filename);
76-
sourcesContent[sourceIndex] = traced.source.content;
81+
sources.push(filename);
82+
sourceIndexMap.set(filename, sourceIndex);
83+
sourcesContent[sourceIndex] = content;
7784
} else if (sourcesContent[sourceIndex] == null) {
78-
sourcesContent[sourceIndex] = traced.source.content;
79-
} else if (
80-
traced.source.content != null &&
81-
sourcesContent[sourceIndex] !== traced.source.content
82-
) {
85+
sourcesContent[sourceIndex] = content;
86+
} else if (content != null && sourcesContent[sourceIndex] !== content) {
8387
return error({
84-
message: `Multiple conflicting contents for sourcemap source ${traced.source.filename}`
88+
message: `Multiple conflicting contents for sourcemap source ${filename}`
8589
});
8690
}
8791

88-
const tracedSegment: SourceMapSegment = [
89-
segment[0],
90-
sourceIndex,
91-
traced.line,
92-
traced.column
93-
];
92+
const tracedSegment: SourceMapSegment = [segment[0], sourceIndex, line, column];
9493

95-
if (traced.name) {
96-
let nameIndex = nameIndexMap.get(traced.name);
94+
if (name) {
95+
let nameIndex = nameIndexMap.get(name);
9796
if (nameIndex === undefined) {
9897
nameIndex = names.length;
99-
names.push(traced.name);
100-
nameIndexMap.set(traced.name, nameIndex);
98+
names.push(name);
99+
nameIndexMap.set(name, nameIndex);
101100
}
102101

103102
(tracedSegment as SourceMapSegment)[4] = nameIndex;
@@ -118,13 +117,17 @@ class Link {
118117
if (!segments) return null;
119118

120119
// binary search through segments for the given column
121-
let i = 0;
122-
let j = segments.length - 1;
120+
let searchStart = 0;
121+
let searchEnd = segments.length - 1;
123122

124-
while (i <= j) {
125-
const m = (i + j) >> 1;
123+
while (searchStart <= searchEnd) {
124+
const m = (searchStart + searchEnd) >> 1;
126125
const segment = segments[m];
127-
if (segment[0] === column) {
126+
127+
// If a sourcemap does not have sufficient resolution to contain a
128+
// necessary mapping, e.g. because it only contains line information, we
129+
// use the best approximation we could find
130+
if (segment[0] === column || searchStart === searchEnd) {
128131
if (segment.length == 1) return null;
129132
const source = this.sources[segment[1]];
130133
if (!source) return null;
@@ -136,9 +139,9 @@ class Link {
136139
);
137140
}
138141
if (segment[0] > column) {
139-
j = m - 1;
142+
searchEnd = m - 1;
140143
} else {
141-
i = m + 1;
144+
searchStart = m + 1;
142145
}
143146
}
144147

src/utils/getOriginalLocation.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,30 @@ import type { DecodedSourceMapOrMissing, ExistingDecodedSourceMap } from '../rol
22

33
export function getOriginalLocation(
44
sourcemapChain: readonly DecodedSourceMapOrMissing[],
5-
location: { column: number; line: number; name?: string; source?: string }
5+
location: { column: number; line: number }
66
): { column: number; line: number } {
77
const filteredSourcemapChain = sourcemapChain.filter(
88
(sourcemap): sourcemap is ExistingDecodedSourceMap => !!sourcemap.mappings
99
);
10-
11-
while (filteredSourcemapChain.length > 0) {
10+
traceSourcemap: while (filteredSourcemapChain.length > 0) {
1211
const sourcemap = filteredSourcemapChain.pop()!;
1312
const line = sourcemap.mappings[location.line - 1];
14-
let locationFound = false;
15-
16-
if (line !== undefined) {
17-
for (const segment of line) {
18-
if (segment[0] >= location.column) {
19-
if (segment.length === 1) break;
13+
if (line) {
14+
const filteredLine = line.filter(
15+
(segment): segment is [number, number, number, number] => segment.length > 1
16+
);
17+
const lastSegment = filteredLine[filteredLine.length - 1];
18+
for (const segment of filteredLine) {
19+
if (segment[0] >= location.column || segment === lastSegment) {
2020
location = {
2121
column: segment[3],
22-
line: segment[2] + 1,
23-
name: segment.length === 5 ? sourcemap.names[segment[4]] : undefined,
24-
source: sourcemap.sources[segment[1]]
22+
line: segment[2] + 1
2523
};
26-
locationFound = true;
27-
break;
24+
continue traceSourcemap;
2825
}
2926
}
3027
}
31-
if (!locationFound) {
32-
throw new Error("Can't resolve original location of error.");
33-
}
28+
throw new Error("Can't resolve original location of error.");
3429
}
3530
return location;
3631
}

test/function/samples/cannot-resolve-sourcemap-warning/_config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module.exports = {
77
plugins: {
88
name: 'test-plugin',
99
transform() {
10-
return { code: 'export default this', map: { mappings: 'X' } };
10+
return { code: 'export default this', map: { mappings: '' } };
1111
}
1212
}
1313
},
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const path = require('path');
2+
const { encode } = require('sourcemap-codec');
3+
const ID_MAIN = path.join(__dirname, 'main.js');
4+
5+
module.exports = {
6+
description: 'handles when a low resolution sourcemap is used to report an error',
7+
options: {
8+
plugins: {
9+
name: 'test-plugin',
10+
transform() {
11+
// each entry of each line consist of
12+
// [generatedColumn, sourceIndex, sourceLine, sourceColumn];
13+
// this mapping only maps the first line to itself
14+
const decodedMap = [[[0], [0, 0, 0, 0], [1]]];
15+
return { code: 'export default this', map: { mappings: encode(decodedMap), sources: [] } };
16+
}
17+
}
18+
},
19+
warnings: [
20+
{
21+
code: 'THIS_IS_UNDEFINED',
22+
frame: `
23+
1: console.log('original source');
24+
^`,
25+
id: ID_MAIN,
26+
loc: {
27+
column: 0,
28+
file: ID_MAIN,
29+
line: 1
30+
},
31+
message:
32+
"The 'this' keyword is equivalent to 'undefined' at the top level of an ES module, and has been rewritten",
33+
pos: 15,
34+
url: 'https://rollupjs.org/guide/en/#error-this-is-undefined'
35+
}
36+
]
37+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('original source');
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const assert = require('assert');
2+
const MagicString = require('magic-string');
3+
const { SourceMapConsumer } = require('source-map');
4+
const { encode } = require('sourcemap-codec');
5+
const getLocation = require('../../getLocation');
6+
7+
module.exports = {
8+
description: 'handles combining low-resolution and high-resolution source-maps when transforming',
9+
options: {
10+
output: { name: 'bundle' },
11+
plugins: [
12+
{
13+
transform(code) {
14+
// each entry of each line consist of
15+
// [generatedColumn, sourceIndex, sourceLine, sourceColumn];
16+
// this mapping only maps the second line to the first with no column
17+
// details
18+
const decodedMap = [[], [[0, 0, 0, 0]]];
19+
return {
20+
code: `console.log('added');\n${code}`,
21+
map: { mappings: encode(decodedMap) }
22+
};
23+
}
24+
},
25+
{
26+
transform(code) {
27+
const s = new MagicString(code);
28+
s.prepend("console.log('second');\n");
29+
30+
return {
31+
code: s.toString(),
32+
map: s.generateMap({ hires: true })
33+
};
34+
}
35+
}
36+
]
37+
},
38+
async test(code, map) {
39+
const smc = await new SourceMapConsumer(map);
40+
41+
const generatedLoc = getLocation(code, code.indexOf("'baz'"));
42+
const originalLoc = smc.originalPositionFor(generatedLoc);
43+
44+
assert.strictEqual(originalLoc.line, 1);
45+
assert.strictEqual(originalLoc.column, 0);
46+
}
47+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export let foo = 'bar'; foo += 'baz';

0 commit comments

Comments
 (0)