Skip to content

Commit

Permalink
module: Add SourceMap.findOrigin
Browse files Browse the repository at this point in the history
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
  • Loading branch information
isaacs committed Apr 30, 2023
1 parent e8cfa95 commit 9f3cf34
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 22 deletions.
68 changes: 56 additions & 12 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,23 +198,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:
* generatedLine: {number}
* generatedColumn: {number}
* originalSource: {string}
* originalLine: {number}
* originalColumn: {number}
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.
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
Expand Down
42 changes: 34 additions & 8 deletions lib/internal/source_map/source_map.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,28 +169,28 @@ class SourceMap {
};

/**
* @param {number} lineNumber in compiled resource
* @param {number} columnNumber in compiled resource
* @return {?Array}
* @param {number} 0-indexed line offset in compiled resource
* @param {number} 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;
count -= step;
}
}
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 {};
Expand All @@ -205,6 +205,32 @@ class SourceMap {
};
}

/**
* @param {number} 1-indexed line number in compiled resource call site
* @param {number} 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
*/
Expand Down
27 changes: 25 additions & 2 deletions test/parallel/test-source-map-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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'
Expand All @@ -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.
Expand Down

0 comments on commit 9f3cf34

Please sign in to comment.