diff --git a/src/utils/collapseSourcemaps.ts b/src/utils/collapseSourcemaps.ts index 1d8c302006a..df26e9d25d7 100644 --- a/src/utils/collapseSourcemaps.ts +++ b/src/utils/collapseSourcemaps.ts @@ -31,7 +31,10 @@ interface SourceMapSegmentObject { source: Source; } +let linkIndex = 0; + class Link { + index: number; mappings: SourceMapSegment[][]; names: string[]; sources: (Source | Link)[]; @@ -43,6 +46,7 @@ class Link { this.sources = sources; this.names = map.names; this.mappings = map.mappings; + this.index = linkIndex++; } traceMappings() { @@ -117,34 +121,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], @@ -153,9 +146,9 @@ class Link { ); } if (segment[0] > column) { - j = m - 1; + searchEnd = m - 1; } else { - i = m + 1; + searchStart = m + 1; } } diff --git a/src/utils/getOriginalLocation.ts b/src/utils/getOriginalLocation.ts index 94fa68eb6b0..216acfef814 100644 --- a/src/utils/getOriginalLocation.ts +++ b/src/utils/getOriginalLocation.ts @@ -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; } diff --git a/test/function/samples/warning-low-resolution-location/_config.js b/test/function/samples/warning-low-resolution-location/_config.js new file mode 100644 index 00000000000..01d2d4001c7 --- /dev/null +++ b/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' + } + ] +}; diff --git a/test/function/samples/warning-low-resolution-location/main.js b/test/function/samples/warning-low-resolution-location/main.js new file mode 100644 index 00000000000..2f40c1f1894 --- /dev/null +++ b/test/function/samples/warning-low-resolution-location/main.js @@ -0,0 +1 @@ +console.log('original source'); diff --git a/test/sourcemaps/samples/transform-low-resolution/_config.js b/test/sourcemaps/samples/transform-low-resolution/_config.js new file mode 100644 index 00000000000..ad66c2b15d7 --- /dev/null +++ b/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); + } +}; diff --git a/test/sourcemaps/samples/transform-low-resolution/main.js b/test/sourcemaps/samples/transform-low-resolution/main.js new file mode 100644 index 00000000000..e4161efb281 --- /dev/null +++ b/test/sourcemaps/samples/transform-low-resolution/main.js @@ -0,0 +1 @@ +export let foo = 'bar'; foo += 'baz';