Skip to content

Commit

Permalink
Generalize low-resolution sourcemap handling and add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
lukastaegert committed Mar 3, 2022
1 parent eeca9a7 commit a25b540
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 49 deletions.
39 changes: 14 additions & 25 deletions src/utils/collapseSourcemaps.ts
Expand Up @@ -117,34 +117,23 @@ class Link {
const segments = this.mappings[line];
if (!segments) return null;

// Sometimes a high-resolution sourcemap will be preceded in the sourcemap chain
// by a low-resolution sourcemap. We can detect this by checking if the mappings
// array for this line only contains a segment for column zero. In that case, we
// want to fall back to a low-resolution mapping instead of returning null.
if (segments.length == 1 && segments[0][0] == 0) {
const segment = segments[0];
if (segment.length == 1) {
return null;
}
const source = this.sources[segment[1]] || null;
return source?.traceSegment(
segment[2],
segment[3],
segment.length === 5 ? this.names[segment[4]] : name
);
}

// binary search through segments for the given column
let i = 0;
let j = segments.length - 1;
let searchStart = 0;
let searchEnd = segments.length - 1;

while (i <= j) {
const m = (i + j) >> 1;
while (searchStart <= searchEnd) {
const m = (searchStart + searchEnd) >> 1;
const segment = segments[m];
if (segment[0] === column) {

// If a sourcemap does not have sufficient resolution to contain a
// necessary mapping, e.g. because it only contains line information, we
// use the best approximation we could find
if (segment[0] === column || searchStart === searchEnd) {
if (segment.length == 1) return null;
const source = this.sources[segment[1]];
if (!source) return null;
if (!source) {
return null;
}

return source.traceSegment(
segment[2],
Expand All @@ -153,9 +142,9 @@ class Link {
);
}
if (segment[0] > column) {
j = m - 1;
searchEnd = m - 1;
} else {
i = m + 1;
searchStart = m + 1;
}
}

Expand Down
38 changes: 14 additions & 24 deletions src/utils/getOriginalLocation.ts
Expand Up @@ -7,35 +7,25 @@ export function getOriginalLocation(
const filteredSourcemapChain = sourcemapChain.filter(
(sourcemap): sourcemap is ExistingDecodedSourceMap => !!sourcemap.mappings
);

while (filteredSourcemapChain.length > 0) {
traceSourcemap: while (filteredSourcemapChain.length > 0) {
const sourcemap = filteredSourcemapChain.pop()!;
const line = sourcemap.mappings[location.line - 1];
let locationFound = false;

if (line !== undefined) {
// Sometimes a high-resolution sourcemap will be preceded in the sourcemap chain
// by a low-resolution sourcemap. We can detect this by checking if the mappings
// array for this line only contains a segment for column zero. In that case, we
// want to fall back to a low-resolution mapping instead of throwing an error.
const segment =
line.length == 1 && line[0][0] == 0
? line[0]
: line.find(segment => segment[0] >= location.column);

if (segment && segment.length !== 1) {
locationFound = true;
location = {
column: segment[3],
line: segment[2] + 1,
name: segment.length === 5 ? sourcemap.names[segment[4]] : undefined,
source: sourcemap.sources[segment[1]]
};
if (line?.length) {
for (const segment of line) {
if (segment[0] >= location.column || line.length === 1) {
if (segment.length === 1) break;
location = {
column: segment[3],
line: segment[2] + 1,
name: segment.length === 5 ? sourcemap.names[segment[4]] : undefined,
source: sourcemap.sources[segment[1]]
};
continue traceSourcemap;
}
}
}
if (!locationFound) {
throw new Error("Can't resolve original location of error.");
}
throw new Error("Can't resolve original location of error.");
}
return location;
}
37 changes: 37 additions & 0 deletions test/function/samples/warning-low-resolution-location/_config.js
@@ -0,0 +1,37 @@
const path = require('path');
const { encode } = require('sourcemap-codec');
const ID_MAIN = path.join(__dirname, 'main.js');

module.exports = {
description: 'handles when a low resolution sourcemap is used to report an error',
options: {
plugins: {
name: 'test-plugin',
transform() {
// each entry of each line consist of
// [generatedColumn, sourceIndex, sourceLine, sourceColumn];
// this mapping only maps the first line to itself
const decodedMap = [[[0, 0, 0, 0]]];
return { code: 'export default this', map: { mappings: encode(decodedMap), sources: [] } };
}
}
},
warnings: [
{
code: 'THIS_IS_UNDEFINED',
frame: `
1: console.log('original source');
^`,
id: ID_MAIN,
loc: {
column: 0,
file: ID_MAIN,
line: 1
},
message:
"The 'this' keyword is equivalent to 'undefined' at the top level of an ES module, and has been rewritten",
pos: 15,
url: 'https://rollupjs.org/guide/en/#error-this-is-undefined'
}
]
};
@@ -0,0 +1 @@
console.log('original source');
47 changes: 47 additions & 0 deletions test/sourcemaps/samples/transform-low-resolution/_config.js
@@ -0,0 +1,47 @@
const assert = require('assert');
const MagicString = require('magic-string');
const { SourceMapConsumer } = require('source-map');
const { encode } = require('sourcemap-codec');
const getLocation = require('../../getLocation');

module.exports = {
description: 'handles combining low-resolution and high-resolution source-maps when transforming',
options: {
output: { name: 'bundle' },
plugins: [
{
transform(code) {
// each entry of each line consist of
// [generatedColumn, sourceIndex, sourceLine, sourceColumn];
// this mapping only maps the second line to the first with no column
// details
const decodedMap = [[], [[0, 0, 0, 0]]];
return {
code: `console.log('added');\n${code}`,
map: { mappings: encode(decodedMap) }
};
}
},
{
transform(code) {
const s = new MagicString(code);
s.prepend("console.log('second');\n");

return {
code: s.toString(),
map: s.generateMap({ hires: true })
};
}
}
]
},
async test(code, map) {
const smc = await new SourceMapConsumer(map);

const generatedLoc = getLocation(code, code.indexOf("'baz'"));
const originalLoc = smc.originalPositionFor(generatedLoc);

assert.strictEqual(originalLoc.line, 1);
assert.strictEqual(originalLoc.column, 0);
}
};
1 change: 1 addition & 0 deletions test/sourcemaps/samples/transform-low-resolution/main.js
@@ -0,0 +1 @@
export let foo = 'bar'; foo += 'baz';

0 comments on commit a25b540

Please sign in to comment.