diff options
author | bcoe <bencoe@google.com> | 2019-09-29 14:15:39 -0700 |
---|---|---|
committer | Benjamin Coe <bencoe@google.com> | 2019-10-05 17:08:00 -0700 |
commit | e1e2f669f65fd53323b8a58d80ed3cee039706b7 (patch) | |
tree | 582baba1511e57a9915b379d0f263cb29fb6c38f /lib | |
parent | 739f113ba63367a93e1567032d85573a079b97b5 (diff) | |
download | node-new-e1e2f669f65fd53323b8a58d80ed3cee039706b7.tar.gz |
process: add source-map support to stack traces
PR-URL: https://github.com/nodejs/node/pull/29564
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Diffstat (limited to 'lib')
-rw-r--r-- | lib/internal/bootstrap/pre_execution.js | 12 | ||||
-rw-r--r-- | lib/internal/modules/cjs/loader.js | 27 | ||||
-rw-r--r-- | lib/internal/modules/esm/translators.js | 2 | ||||
-rw-r--r-- | lib/internal/source_map/source_map.js | 301 | ||||
-rw-r--r-- | lib/internal/source_map/source_map_cache.js (renamed from lib/internal/source_map.js) | 93 |
5 files changed, 423 insertions, 12 deletions
diff --git a/lib/internal/bootstrap/pre_execution.js b/lib/internal/bootstrap/pre_execution.js index 174ffcd018..c1636d87f4 100644 --- a/lib/internal/bootstrap/pre_execution.js +++ b/lib/internal/bootstrap/pre_execution.js @@ -21,6 +21,15 @@ function prepareMainThreadExecution(expandArgv1 = false) { setupCoverageHooks(process.env.NODE_V8_COVERAGE); } + // If source-map support has been enabled, we substitute in a new + // prepareStackTrace method, replacing the default in errors.js. + if (getOptionValue('--enable-source-maps')) { + const { prepareStackTrace } = + require('internal/source_map/source_map_cache'); + const { setPrepareStackTraceCallback } = internalBinding('errors'); + setPrepareStackTraceCallback(prepareStackTrace); + } + setupDebugEnv(); // Only main thread receives signals. @@ -119,7 +128,8 @@ function setupCoverageHooks(dir) { const cwd = require('internal/process/execution').tryGetCwd(); const { resolve } = require('path'); const coverageDirectory = resolve(cwd, dir); - const { sourceMapCacheToObject } = require('internal/source_map'); + const { sourceMapCacheToObject } = + require('internal/source_map/source_map_cache'); if (process.features.inspector) { internalBinding('profiler').setCoverageDirectory(coverageDirectory); diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 6f727b4510..9bee9130d2 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -31,7 +31,10 @@ const { } = primordials; const { NativeModule } = require('internal/bootstrap/loaders'); -const { maybeCacheSourceMap } = require('internal/source_map'); +const { + maybeCacheSourceMap, + rekeySourceMap +} = require('internal/source_map/source_map_cache'); const { pathToFileURL, fileURLToPath, URL } = require('internal/url'); const { deprecate } = require('internal/util'); const vm = require('vm'); @@ -51,6 +54,7 @@ const { loadNativeModule } = require('internal/modules/cjs/helpers'); const { getOptionValue } = require('internal/options'); +const enableSourceMaps = getOptionValue('--enable-source-maps'); const preserveSymlinks = getOptionValue('--preserve-symlinks'); const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); const experimentalModules = getOptionValue('--experimental-modules'); @@ -707,7 +711,19 @@ Module._load = function(request, parent, isMain) { let threw = true; try { - module.load(filename); + // Intercept exceptions that occur during the first tick and rekey them + // on error instance rather than module instance (which will immediately be + // garbage collected). + if (enableSourceMaps) { + try { + module.load(filename); + } catch (err) { + rekeySourceMap(Module._cache[filename], err); + throw err; /* node-do-not-add-exception-line */ + } + } else { + module.load(filename); + } threw = false; } finally { if (threw) { @@ -846,9 +862,7 @@ Module.prototype.require = function(id) { var resolvedArgv; let hasPausedEntry = false; -function wrapSafe(filename, content, cjsModuleInstance) { - maybeCacheSourceMap(filename, content, cjsModuleInstance); - +function wrapSafe(filename, content) { if (patched) { const wrapper = Module.wrap(content); return vm.runInThisContext(wrapper, { @@ -913,7 +927,8 @@ Module.prototype._compile = function(content, filename) { manifest.assertIntegrity(moduleURL, content); } - const compiledWrapper = wrapSafe(filename, content, this); + maybeCacheSourceMap(filename, content, this); + const compiledWrapper = wrapSafe(filename, content); var inspectorWrapper = null; if (getOptionValue('--inspect-brk') && process._eval == null) { diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index 352b937766..056bf64bf5 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -31,7 +31,7 @@ const { } = require('internal/errors').codes; const readFileAsync = promisify(fs.readFile); const JsonParse = JSON.parse; -const { maybeCacheSourceMap } = require('internal/source_map'); +const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); const debug = debuglog('esm'); diff --git a/lib/internal/source_map/source_map.js b/lib/internal/source_map/source_map.js new file mode 100644 index 0000000000..9044521b6d --- /dev/null +++ b/lib/internal/source_map/source_map.js @@ -0,0 +1,301 @@ +// This file is a modified version of: +// https://cs.chromium.org/chromium/src/v8/tools/SourceMap.js?rcl=dd10454c1d +// from the V8 codebase. Logic specific to WebInspector is removed and linting +// is made to match the Node.js style guide. + +// Copyright 2013 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// This is a copy from blink dev tools, see: +// http://src.chromium.org/viewvc/blink/trunk/Source/devtools/front_end/SourceMap.js +// revision: 153407 + +/* + * Copyright (C) 2012 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +'use strict'; + +let base64Map; + +const VLQ_BASE_SHIFT = 5; +const VLQ_BASE_MASK = (1 << 5) - 1; +const VLQ_CONTINUATION_MASK = 1 << 5; + +class StringCharIterator { + /** + * @constructor + * @param {string} string + */ + constructor(string) { + this._string = string; + this._position = 0; + } + + /** + * @return {string} + */ + next() { + return this._string.charAt(this._position++); + } + + /** + * @return {string} + */ + peek() { + return this._string.charAt(this._position); + } + + /** + * @return {boolean} + */ + hasNext() { + return this._position < this._string.length; + } +} + +/** + * Implements Source Map V3 model. See http://code.google.com/p/closure-compiler/wiki/SourceMaps + * for format description. + * @constructor + * @param {string} sourceMappingURL + * @param {SourceMapV3} payload + */ +class SourceMap { + #reverseMappingsBySourceURL = []; + #mappings = []; + #sources = {}; + #sourceContentByURL = {}; + + /** + * @constructor + * @param {SourceMapV3} payload + */ + constructor(payload) { + if (!base64Map) { + const base64Digits = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + base64Map = {}; + for (let i = 0; i < base64Digits.length; ++i) + base64Map[base64Digits[i]] = i; + } + this.#parseMappingPayload(payload); + } + + /** + * @param {SourceMapV3} mappingPayload + */ + #parseMappingPayload = (mappingPayload) => { + if (mappingPayload.sections) + this.#parseSections(mappingPayload.sections); + else + this.#parseMap(mappingPayload, 0, 0); + } + + /** + * @param {Array.<SourceMapV3.Section>} sections + */ + #parseSections = (sections) => { + for (let i = 0; i < sections.length; ++i) { + const section = sections[i]; + this.#parseMap(section.map, section.offset.line, section.offset.column); + } + } + + /** + * @param {number} lineNumber in compiled resource + * @param {number} columnNumber in compiled resource + * @return {?Array} + */ + findEntry(lineNumber, columnNumber) { + 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])) { + count = step; + } else { + first = middle; + count -= step; + } + } + const entry = this.#mappings[first]; + if (!first && entry && (lineNumber < entry[0] || + (lineNumber === entry[0] && columnNumber < entry[1]))) { + return null; + } + return entry; + } + + /** + * @param {string} sourceURL of the originating resource + * @param {number} lineNumber in the originating resource + * @return {Array} + */ + findEntryReversed(sourceURL, lineNumber) { + const mappings = this.#reverseMappingsBySourceURL[sourceURL]; + for (; lineNumber < mappings.length; ++lineNumber) { + const mapping = mappings[lineNumber]; + if (mapping) + return mapping; + } + return this.#mappings[0]; + } + + /** + * @override + */ + #parseMap = (map, lineNumber, columnNumber) => { + let sourceIndex = 0; + let sourceLineNumber = 0; + let sourceColumnNumber = 0; + + const sources = []; + const originalToCanonicalURLMap = {}; + for (let i = 0; i < map.sources.length; ++i) { + const url = map.sources[i]; + originalToCanonicalURLMap[url] = url; + sources.push(url); + this.#sources[url] = true; + + if (map.sourcesContent && map.sourcesContent[i]) + this.#sourceContentByURL[url] = map.sourcesContent[i]; + } + + const stringCharIterator = new StringCharIterator(map.mappings); + let sourceURL = sources[sourceIndex]; + + while (true) { + if (stringCharIterator.peek() === ',') + stringCharIterator.next(); + else { + while (stringCharIterator.peek() === ';') { + lineNumber += 1; + columnNumber = 0; + stringCharIterator.next(); + } + if (!stringCharIterator.hasNext()) + break; + } + + columnNumber += decodeVLQ(stringCharIterator); + if (isSeparator(stringCharIterator.peek())) { + this.#mappings.push([lineNumber, columnNumber]); + continue; + } + + const sourceIndexDelta = decodeVLQ(stringCharIterator); + if (sourceIndexDelta) { + sourceIndex += sourceIndexDelta; + sourceURL = sources[sourceIndex]; + } + sourceLineNumber += decodeVLQ(stringCharIterator); + sourceColumnNumber += decodeVLQ(stringCharIterator); + if (!isSeparator(stringCharIterator.peek())) + // Unused index into the names list. + decodeVLQ(stringCharIterator); + + this.#mappings.push([lineNumber, columnNumber, sourceURL, + sourceLineNumber, sourceColumnNumber]); + } + + for (let i = 0; i < this.#mappings.length; ++i) { + const mapping = this.#mappings[i]; + const url = mapping[2]; + if (!url) + continue; + if (!this.#reverseMappingsBySourceURL[url]) + this.#reverseMappingsBySourceURL[url] = []; + const reverseMappings = this.#reverseMappingsBySourceURL[url]; + const sourceLine = mapping[3]; + if (!reverseMappings[sourceLine]) + reverseMappings[sourceLine] = [mapping[0], mapping[1]]; + } + }; +} + +/** + * @param {string} char + * @return {boolean} + */ +function isSeparator(char) { + return char === ',' || char === ';'; +} + +/** + * @param {SourceMap.StringCharIterator} stringCharIterator + * @return {number} + */ +function decodeVLQ(stringCharIterator) { + // Read unsigned value. + let result = 0; + let shift = 0; + let digit; + do { + digit = base64Map[stringCharIterator.next()]; + result += (digit & VLQ_BASE_MASK) << shift; + shift += VLQ_BASE_SHIFT; + } while (digit & VLQ_CONTINUATION_MASK); + + // Fix the sign. + const negative = result & 1; + result >>= 1; + return negative ? -result : result; +} + +module.exports = { + SourceMap +}; diff --git a/lib/internal/source_map.js b/lib/internal/source_map/source_map_cache.js index 4b198ff598..94a4165546 100644 --- a/lib/internal/source_map.js +++ b/lib/internal/source_map/source_map_cache.js @@ -5,6 +5,7 @@ const { Buffer } = require('buffer'); const debug = require('internal/util/debuglog').debuglog('source_map'); const { dirname, resolve } = require('path'); const fs = require('fs'); +const { getOptionValue } = require('internal/options'); const { normalizeReferrerURL, } = require('internal/modules/cjs/helpers'); @@ -16,10 +17,14 @@ const cjsSourceMapCache = new WeakMap(); // on filenames. const esmSourceMapCache = new Map(); const { fileURLToPath, URL } = require('url'); +const { overrideStackTrace } = require('internal/errors'); +let experimentalSourceMaps; function maybeCacheSourceMap(filename, content, cjsModuleInstance) { - if (!process.env.NODE_V8_COVERAGE) return; - + if (experimentalSourceMaps === undefined) { + experimentalSourceMaps = getOptionValue('--enable-source-maps'); + } + if (!(process.env.NODE_V8_COVERAGE || experimentalSourceMaps)) return; let basePath; try { filename = normalizeReferrerURL(filename); @@ -35,6 +40,7 @@ function maybeCacheSourceMap(filename, content, cjsModuleInstance) { if (match) { if (cjsModuleInstance) { cjsSourceMapCache.set(cjsModuleInstance, { + filename, url: match.groups.sourceMappingURL, data: dataFromUrl(basePath, match.groups.sourceMappingURL) }); @@ -119,6 +125,16 @@ function sourcesToAbsolute(base, data) { return data; } +// Move source map from garbage collected module to alternate key. +function rekeySourceMap(cjsModuleInstance, newInstance) { + const sourceMap = cjsSourceMapCache.get(cjsModuleInstance); + if (sourceMap) { + cjsSourceMapCache.set(newInstance, sourceMap); + } +} + +// Get serialized representation of source-map cache, this is used +// to persist a cache of source-maps to disk when NODE_V8_COVERAGE is enabled. function sourceMapCacheToObject() { const obj = Object.create(null); @@ -136,17 +152,86 @@ function sourceMapCacheToObject() { // Since WeakMap can't be iterated over, we use Module._cache's // keys to facilitate Source Map serialization. +// +// TODO(bcoe): this means we don't currently serialize source-maps attached +// to error instances, only module instances. function appendCJSCache(obj) { const { Module } = require('internal/modules/cjs/loader'); Object.keys(Module._cache).forEach((key) => { const value = cjsSourceMapCache.get(Module._cache[key]); if (value) { - obj[`file://${key}`] = value; + obj[`file://${key}`] = { + url: value.url, + data: value.data + }; + } + }); +} + +// Create a prettified stacktrace, inserting context from source maps +// if possible. +const ErrorToString = Error.prototype.toString; // Capture original toString. +const prepareStackTrace = (globalThis, error, trace) => { + // API for node internals to override error stack formatting + // without interfering with userland code. + // TODO(bcoe): add support for source-maps to repl. + if (overrideStackTrace.has(error)) { + const f = overrideStackTrace.get(error); + overrideStackTrace.delete(error); + return f(error, trace); + } + + const { SourceMap } = require('internal/source_map/source_map'); + const errorString = ErrorToString.call(error); + + if (trace.length === 0) { + return errorString; + } + const preparedTrace = trace.map((t, i) => { + let str = i !== 0 ? '\n at ' : ''; + str = `${str}${t}`; + try { + const sourceMap = findSourceMap(t.getFileName(), error); + if (sourceMap && sourceMap.data) { + const sm = new SourceMap(sourceMap.data); + // Source Map V3 lines/columns use zero-based offsets whereas, in + // stack traces, they start at 1/1. + const [, , url, line, col] = + sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1); + if (url && line !== undefined && col !== undefined) { + str += + `\n -> ${url.replace('file://', '')}:${line + 1}:${col + 1}`; + } + } + } catch (err) { + debug(err.stack); } + return str; }); + return `${errorString}\n at ${preparedTrace.join('')}`; +}; + +// Attempt to lookup a source map, which is either attached to a file URI, or +// keyed on an error instance. +function findSourceMap(uri, error) { + const { Module } = require('internal/modules/cjs/loader'); + let sourceMap = cjsSourceMapCache.get(Module._cache[uri]); + if (!uri.startsWith('file://')) uri = normalizeReferrerURL(uri); + if (sourceMap === undefined) { + sourceMap = esmSourceMapCache.get(uri); + } + if (sourceMap === undefined) { + const candidateSourceMap = cjsSourceMapCache.get(error); + if (candidateSourceMap && uri === candidateSourceMap.filename) { + sourceMap = candidateSourceMap; + } + } + return sourceMap; } module.exports = { + maybeCacheSourceMap, + prepareStackTrace, + rekeySourceMap, sourceMapCacheToObject, - maybeCacheSourceMap }; |