Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sourcemap): fall back to low-resolution line mapping #4334

Merged
merged 6 commits into from Mar 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 32 additions & 29 deletions src/utils/collapseSourcemaps.ts
Expand Up @@ -47,6 +47,7 @@ class Link {

traceMappings() {
const sources: string[] = [];
const sourceIndexMap = new Map<string, number>();
const sourcesContent: string[] = [];
const names: string[] = [];
const nameIndexMap = new Map<string, number>();
Expand All @@ -57,7 +58,7 @@ class Link {
const tracedLine: SourceMapSegment[] = [];

for (const segment of line) {
if (segment.length == 1) continue;
if (segment.length === 1) continue;
const source = this.sources[segment[1]];
if (!source) continue;

Expand All @@ -68,36 +69,34 @@ class Link {
);

if (traced) {
// newer sources are more likely to be used, so search backwards.
let sourceIndex = sources.lastIndexOf(traced.source.filename);
if (sourceIndex === -1) {
const {
column,
line,
name,
source: { content, filename }
} = traced;
let sourceIndex = sourceIndexMap.get(filename);
if (sourceIndex === undefined) {
sourceIndex = sources.length;
sources.push(traced.source.filename);
sourcesContent[sourceIndex] = traced.source.content;
sources.push(filename);
sourceIndexMap.set(filename, sourceIndex);
sourcesContent[sourceIndex] = content;
} else if (sourcesContent[sourceIndex] == null) {
sourcesContent[sourceIndex] = traced.source.content;
} else if (
traced.source.content != null &&
sourcesContent[sourceIndex] !== traced.source.content
) {
sourcesContent[sourceIndex] = content;
} else if (content != null && sourcesContent[sourceIndex] !== content) {
return error({
message: `Multiple conflicting contents for sourcemap source ${traced.source.filename}`
message: `Multiple conflicting contents for sourcemap source ${filename}`
});
}

const tracedSegment: SourceMapSegment = [
segment[0],
sourceIndex,
traced.line,
traced.column
];
const tracedSegment: SourceMapSegment = [segment[0], sourceIndex, line, column];

if (traced.name) {
let nameIndex = nameIndexMap.get(traced.name);
if (name) {
let nameIndex = nameIndexMap.get(name);
if (nameIndex === undefined) {
nameIndex = names.length;
names.push(traced.name);
nameIndexMap.set(traced.name, nameIndex);
names.push(name);
nameIndexMap.set(name, nameIndex);
}

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

// 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;
Expand All @@ -136,9 +139,9 @@ class Link {
);
}
if (segment[0] > column) {
j = m - 1;
searchEnd = m - 1;
} else {
i = m + 1;
searchStart = m + 1;
}
}

Expand Down
29 changes: 12 additions & 17 deletions src/utils/getOriginalLocation.ts
Expand Up @@ -2,35 +2,30 @@ import type { DecodedSourceMapOrMissing, ExistingDecodedSourceMap } from '../rol

export function getOriginalLocation(
sourcemapChain: readonly DecodedSourceMapOrMissing[],
location: { column: number; line: number; name?: string; source?: string }
location: { column: number; line: number }
): { column: number; line: number } {
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) {
for (const segment of line) {
if (segment[0] >= location.column) {
if (segment.length === 1) break;
if (line) {
const filteredLine = line.filter(
(segment): segment is [number, number, number, number] => segment.length > 1
);
const lastSegment = filteredLine[filteredLine.length - 1];
for (const segment of filteredLine) {
if (segment[0] >= location.column || segment === lastSegment) {
location = {
column: segment[3],
line: segment[2] + 1,
name: segment.length === 5 ? sourcemap.names[segment[4]] : undefined,
source: sourcemap.sources[segment[1]]
line: segment[2] + 1
};
locationFound = true;
break;
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;
}
Expand Up @@ -7,7 +7,7 @@ module.exports = {
plugins: {
name: 'test-plugin',
transform() {
return { code: 'export default this', map: { mappings: 'X' } };
return { code: 'export default this', map: { mappings: '' } };
}
}
},
Expand Down
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, 0], [1]]];
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';