module: add SourceMap.findOrigin

This adds the `SourceMap.findOrigin(lineNumber, columnNumber)` method,
for finding the origin source file and 1-indexed line and column numbers
corresponding to the 1-indexed line and column numbers from a call site
in generated source code.

Fix: #47770
PR-URL: https://github.com/nodejs/node/pull/47790
Fixes: https://github.com/nodejs/node/issues/47770
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
This commit is contained in:
isaacs 2023-06-23 12:17:14 -07:00 committed by GitHub
parent e934003811
commit e26ffe7358
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 114 additions and 21 deletions

View File

@ -293,23 +293,67 @@ Creates a new `sourceMap` instance.
Getter for the payload used to construct the [`SourceMap`][] instance.
#### `sourceMap.findEntry(lineNumber, columnNumber)`
#### `sourceMap.findEntry(lineOffset, columnOffset)`
* `lineNumber` {number}
* `columnNumber` {number}
* `lineOffset` {number} The zero-indexed line number offset in
the generated source
* `columnOffset` {number} The zero-indexed column number offset
in the generated source
* Returns: {Object}
Given a line number and column number in the generated source file, returns
an object representing the position in the original file. The object returned
consists of the following keys:
Given a line offset and column offset in the generated source
file, returns an object representing the SourceMap range in the
original file if found, or an empty object if not.
* generatedLine: {number}
* generatedColumn: {number}
* originalSource: {string}
* originalLine: {number}
* originalColumn: {number}
The object returned contains the following keys:
* generatedLine: {number} The line offset of the start of the
range in the generated source
* generatedColumn: {number} The column offset of start of the
range in the generated source
* originalSource: {string} The file name of the original source,
as reported in the SourceMap
* originalLine: {number} The line offset of the start of the
range in the original source
* originalColumn: {number} The column offset of start of the
range in the original source
* name: {string}
The returned value represents the raw range as it appears in the
SourceMap, based on zero-indexed offsets, _not_ 1-indexed line and
column numbers as they appear in Error messages and CallSite
objects.
To get the corresponding 1-indexed line and column numbers from a
lineNumber and columnNumber as they are reported by Error stacks
and CallSite objects, use `sourceMap.findOrigin(lineNumber,
columnNumber)`
#### `sourceMap.findOrigin(lineNumber, columnNumber)`
* `lineNumber` {number} The 1-indexed line number of the call
site in the generated source
* `columnOffset` {number} The 1-indexed column number
of the call site in the generated source
* Returns: {Object}
Given a 1-indexed lineNumber and columnNumber from a call site in
the generated source, find the corresponding call site location
in the original source.
If the lineNumber and columnNumber provided are not found in any
source map, then an empty object is returned. Otherwise, the
returned object contains the following keys:
* name: {string | undefined} The name of the range in the
source map, if one was provided
* fileName: {string} The file name of the original source, as
reported in the SourceMap
* lineNumber: {number} The 1-indexed lineNumber of the
corresponding call site in the original source
* columnNumber: {number} The 1-indexed columnNumber of the
corresponding call site in the original source
[CommonJS]: modules.md
[ES Modules]: esm.md
[Source map v3 format]: https://sourcemaps.info/spec.html#h.mofvlxcwqzej

View File

@ -169,19 +169,19 @@ class SourceMap {
};
/**
* @param {number} lineNumber in compiled resource
* @param {number} columnNumber in compiled resource
* @return {?Array}
* @param {number} lineOffset 0-indexed line offset in compiled resource
* @param {number} columnOffset 0-indexed column offset in compiled resource
* @return {object} representing start of range if found, or empty object
*/
findEntry(lineNumber, columnNumber) {
findEntry(lineOffset, columnOffset) {
let first = 0;
let count = this.#mappings.length;
while (count > 1) {
const step = count >> 1;
const middle = first + step;
const mapping = this.#mappings[middle];
if (lineNumber < mapping[0] ||
(lineNumber === mapping[0] && columnNumber < mapping[1])) {
if (lineOffset < mapping[0] ||
(lineOffset === mapping[0] && columnOffset < mapping[1])) {
count = step;
} else {
first = middle;
@ -189,8 +189,8 @@ class SourceMap {
}
}
const entry = this.#mappings[first];
if (!first && entry && (lineNumber < entry[0] ||
(lineNumber === entry[0] && columnNumber < entry[1]))) {
if (!first && entry && (lineOffset < entry[0] ||
(lineOffset === entry[0] && columnOffset < entry[1]))) {
return {};
} else if (!entry) {
return {};
@ -205,6 +205,32 @@ class SourceMap {
};
}
/**
* @param {number} lineNumber 1-indexed line number in compiled resource call site
* @param {number} columnNumber 1-indexed column number in compiled resource call site
* @return {object} representing origin call site if found, or empty object
*/
findOrigin(lineNumber, columnNumber) {
const range = this.findEntry(lineNumber - 1, columnNumber - 1);
if (
range.originalSource === undefined ||
range.originalLine === undefined ||
range.originalColumn === undefined ||
range.generatedLine === undefined ||
range.generatedColumn === undefined
) {
return {};
}
const lineOffset = lineNumber - range.generatedLine;
const columnOffset = columnNumber - range.generatedColumn;
return {
name: range.name,
fileName: range.originalSource,
lineNumber: range.originalLine + lineOffset,
columnNumber: range.originalColumn + columnOffset,
};
}
/**
* @override
*/

View File

@ -49,6 +49,14 @@ const { readFileSync } = require('fs');
assert.strictEqual(originalLine, 2);
assert.strictEqual(originalColumn, 4);
assert(originalSource.endsWith('disk.js'));
const {
fileName,
lineNumber,
columnNumber,
} = sourceMap.findOrigin(1, 30);
assert.strictEqual(fileName, originalSource);
assert.strictEqual(lineNumber, 3);
assert.strictEqual(columnNumber, 6);
}
// findSourceMap() can be used in Error.prepareStackTrace() to lookup
@ -89,6 +97,18 @@ const { readFileSync } = require('fs');
assert.strictEqual(originalLine, 17);
assert.strictEqual(originalColumn, 10);
assert(originalSource.endsWith('typescript-throw.ts'));
const {
fileName,
lineNumber,
columnNumber,
} = sourceMap.findOrigin(
callSite.getLineNumber(),
callSite.getColumnNumber()
);
assert.strictEqual(fileName, originalSource);
assert.strictEqual(lineNumber, 18);
assert.strictEqual(columnNumber, 11);
}
// SourceMap can be instantiated with Source Map V3 object as payload.
@ -112,8 +132,8 @@ const { readFileSync } = require('fs');
assert.notStrictEqual(payload.sources, sourceMap.payload.sources);
}
// findEntry() must return empty object instead error when
// receive a malformed mappings.
// findEntry() and findOrigin() must return empty object instead of
// error when receiving a malformed mappings.
{
const payload = JSON.parse(readFileSync(
require.resolve('../fixtures/source-map/disk.map'), 'utf8'
@ -124,6 +144,9 @@ const { readFileSync } = require('fs');
const result = sourceMap.findEntry(0, 5);
assert.strictEqual(typeof result, 'object');
assert.strictEqual(Object.keys(result).length, 0);
const origin = sourceMap.findOrigin(0, 5);
assert.strictEqual(typeof origin, 'object');
assert.strictEqual(Object.keys(origin).length, 0);
}
// SourceMap can be instantiated with Index Source Map V3 object as payload.