From 9f3cf34e17b709907a2ae6bc584f4aa93cd2902a Mon Sep 17 00:00:00 2001 From: isaacs Date: Sat, 29 Apr 2023 22:38:17 -0700 Subject: [PATCH] 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 --- doc/api/module.md | 68 ++++++++++++++++++++++----- lib/internal/source_map/source_map.js | 42 +++++++++++++---- test/parallel/test-source-map-api.js | 27 ++++++++++- 3 files changed, 115 insertions(+), 22 deletions(-) diff --git a/doc/api/module.md b/doc/api/module.md index d52ec34dd12a54..19ba0f981cc200 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -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 diff --git a/lib/internal/source_map/source_map.js b/lib/internal/source_map/source_map.js index 8a441903bb9519..761be9ff7077ca 100644 --- a/lib/internal/source_map/source_map.js +++ b/lib/internal/source_map/source_map.js @@ -169,19 +169,19 @@ 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; @@ -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} 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 */ diff --git a/test/parallel/test-source-map-api.js b/test/parallel/test-source-map-api.js index 39d523b3e1500b..8def7275fc39a5 100644 --- a/test/parallel/test-source-map-api.js +++ b/test/parallel/test-source-map-api.js @@ -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.