diff --git a/modules/callsite-record/index.js b/modules/callsite-record/index.js new file mode 100644 index 00000000000..67ade30249d --- /dev/null +++ b/modules/callsite-record/index.js @@ -0,0 +1,306 @@ +var fs = require('fs'); +var Promise = require('pinkie-promise'); +var callsite = require('callsite'); +var stackParser = require('error-stack-parser'); +var padStart = require('lodash').padStart; +var defaults = require('lodash').defaults; +var highlight = require('highlight-es'); +var wrapCallSite = require('source-map-support').wrapCallSite; + +var renderers = { + default: require('./renderers/default'), + noColor: require('./renderers/no-color'), + html: require('./renderers/html') +}; + + +var NEWLINE = /\r\n|[\n\r\u2028\u2029]/; + +// Utils +function parseStack (error) { + try { + return stackParser.parse(error); + } + catch (err) { + return null; + } +} + +function getFrameTypeName (frame) { + // NOTE: this throws in node 10 for non-methods + try { + return frame.getTypeName(); + } + catch (err) { + return null; + } +} + +function findClosestNonNativeAncestorFrameIdx (stackFrames, curIdx) { + for (var i = curIdx + 1; i < stackFrames.length; i++) { + if (!stackFrames[i].isNative()) + return i; + } + + return null; +} + +function isV8StackFrame (frame) { + return /CallSite/.test(frame.constructor); +} + +function getFrameMethodName (frame, funcName) { + // NOTE: Code was partially adopted from the V8 code + // (see: https://github.com/v8/v8/blob/3c3d7e7be80f45eeea0dc74a71d7552e2afc2985/src/js/messages.js#L647) + var typeName = frame.getTypeName(); + var methodName = frame.getMethodName(); + + if (funcName) { + var name = ''; + var funcNameStartsWithTypeName = typeName && funcName.indexOf(typeName) === 0; + var funcNameEndsWithMethodName = methodName && + funcName.indexOf('.' + methodName) === funcName.length - methodName.length - 1; + + if (!funcNameStartsWithTypeName) + name = typeName + '.'; + + name += funcName; + + if (!funcNameEndsWithMethodName) + name += ' [as ' + methodName + ']'; + + return name; + } + + return typeName + '.' + (methodName || ''); +} + + +// CallsiteRecord +var CallsiteRecord = function (filename, lineNum, callsiteFrameIdx, stackFrames) { + this.filename = filename; + this.lineNum = lineNum; + this.callsiteFrameIdx = callsiteFrameIdx; + this.stackFrames = stackFrames; + this.isV8Frames = isV8StackFrame(this.stackFrames[0]); +}; + +CallsiteRecord.prototype._getFrameName = function (frame) { + // NOTE: Code was partially adopted from the V8 code + // (see: https://github.com/v8/v8/blob/3c3d7e7be80f45eeea0dc74a71d7552e2afc2985/src/js/messages.js#L647) + var funcName = frame.getFunctionName(); + + if (!this.isV8Frames) + return funcName || ''; + + var isCtor = frame.isConstructor(); + var isMethod = !frame.isToplevel() && !isCtor; + + if (isMethod) + return getFrameMethodName(frame, funcName); + + funcName = funcName || ''; + + return isCtor ? 'new ' + funcName : funcName; +}; + +CallsiteRecord.prototype._getFrameLocation = function (frame) { + // NOTE: Code was partially adopted from the V8 code + // (see: https://github.com/v8/v8/blob/3c3d7e7be80f45eeea0dc74a71d7552e2afc2985/src/js/messages.js#L647) + if (this.isV8Frames && frame.isNative()) + return 'native'; + + var location = frame.getFileName(); + var lineNum = frame.getLineNumber(); + var colNum = frame.getColumnNumber(); + + if (this.isV8Frames && !location) { + location = frame.isEval() ? frame.getEvalOrigin() + ', ' : ''; + location += ''; + } + + if (lineNum) { + location += ':' + lineNum; + + if (colNum) + location += ':' + colNum; + } + + return location; +}; + +CallsiteRecord.prototype._getCodeFrameLines = function (fileContent, frameSize) { + var lines = fileContent.split(NEWLINE); + var startLineIdx = Math.max(0, this.lineNum - frameSize); + var endLineIdx = Math.min(lines.length - 1, this.lineNum + frameSize); + var maxLineNumDigits = 0; + var frameLines = []; + + for (var i = startLineIdx; i <= endLineIdx; i++) { + var num = String(i + 1); + + maxLineNumDigits = Math.max(maxLineNumDigits, num.length); + + frameLines.push({ + num: num, + src: lines[i], + base: i === this.lineNum + }); + } + + frameLines.forEach(function (line) { + line.num = padStart(line.num, maxLineNumDigits); + }); + + return frameLines; +}; + +CallsiteRecord.prototype._renderCodeFrame = function (fileContent, renderer, frameSize) { + if (renderer.syntax) + fileContent = highlight(fileContent, renderer.syntax); + + var lines = this._getCodeFrameLines(fileContent, frameSize); + var lastIdx = lines.length - 1; + + var frame = lines + .reduce(function (sourceFrame, line, idx) { + var isLast = idx === lastIdx; + + return sourceFrame + renderer.codeLine(line.num, line.base, line.src, isLast); + }, ''); + + return renderer.codeFrame(frame); +}; + + +CallsiteRecord.prototype._renderStack = function (renderer, stackFilter) { + var record = this; + var entries = this.stackFrames.slice(this.callsiteFrameIdx); + + if (stackFilter) { + entries = entries.filter(function (frame, idx) { + return stackFilter(frame, idx, record.isV8Frames); + }); + } + + var lastIdx = entries.length - 1; + + var rendered = entries.reduce(function (str, frame, idx) { + var isLast = idx === lastIdx; + var name = record._getFrameName(frame); + var location = record._getFrameLocation(frame); + + return str + renderer.stackLine(name, location, isLast); + }, ''); + + return rendered ? renderer.stack(rendered) : ''; +}; + +CallsiteRecord.prototype._renderRecord = function (fileContent, opts) { + opts = defaults({}, opts, { + renderer: renderers.default, + frameSize: 5, + stack: true, + codeFrame: true, + stackFilter: null + }, opts); + + var codeFrame = opts.codeFrame ? this._renderCodeFrame(fileContent, opts.renderer, opts.frameSize) : ''; + var stack = opts.stack ? this._renderStack(opts.renderer, opts.stackFilter) : ''; + + return codeFrame + stack; +}; + +CallsiteRecord.prototype.renderSync = function (opts) { + var fileContent = fs.readFileSync(this.filename).toString(); + + return this._renderRecord(fileContent, opts); +}; + +CallsiteRecord.prototype.render = function (opts) { + var record = this; + + return new Promise(function (resolve, reject) { + fs.readFile(record.filename, function (err, fileContent) { + if (err) + reject(err); + else + resolve(record._renderRecord(fileContent.toString(), opts)); + }); + }); +}; + +// Static +CallsiteRecord.fromStackFrames = function (stackFrames, fnName, typeName) { + if (typeName && fnName === 'constructor') + fnName = typeName; + + for (var i = 0; i < stackFrames.length; i++) { + var frame = stackFrames[i]; + var fnNameMatch = frame.getFunctionName() === fnName || frame.getMethodName() === fnName; + var typeNameMatch = !typeName || getFrameTypeName(frame) === typeName; + + if (fnNameMatch && typeNameMatch) { + var callsiteFrameIdx = findClosestNonNativeAncestorFrameIdx(stackFrames, i); + + if (callsiteFrameIdx !== null) { + var callsiteFrame = stackFrames[callsiteFrameIdx]; + var filename = callsiteFrame.getFileName(); + var lineNum = callsiteFrame.getLineNumber() - 1; + + return new CallsiteRecord(filename, lineNum, callsiteFrameIdx, stackFrames); + } + + return null; + } + } + + return null; +}; + +CallsiteRecord.fromError = function (error, isCallsiteFrame) { + var stackFrames = parseStack(error); + + if (stackFrames) { + if (typeof isCallsiteFrame === 'function') { + while (stackFrames.length) { + if (isCallsiteFrame(stackFrames[0])) + break; + + stackFrames.shift(); + } + } + + if (stackFrames.length) { + var filename = stackFrames[0].getFileName(); + var lineNum = stackFrames[0].getLineNumber() - 1; + + return filename && !isNaN(lineNum) ? new CallsiteRecord(filename, lineNum, 0, stackFrames) : null; + } + } + + return null; +}; + +// API +module.exports = function createCallsiteRecord (/* err, isCallsiteFrame || fnName, typeName */) { + if (arguments[0] instanceof Error) + return CallsiteRecord.fromError(arguments[0], arguments[1]); + + else if (typeof arguments[0] === 'string') { + var stackFrames = callsite(); + + stackFrames = stackFrames.map(function (frame) { + return wrapCallSite(frame) + }); + + // NOTE: remove API call + stackFrames.shift(); + + return CallsiteRecord.fromStackFrames(stackFrames, arguments[0], arguments[1]); + } + + return null; +}; + +module.exports.renderers = renderers; diff --git a/modules/callsite-record/renderers/default.js b/modules/callsite-record/renderers/default.js new file mode 100644 index 00000000000..967f433310e --- /dev/null +++ b/modules/callsite-record/renderers/default.js @@ -0,0 +1,44 @@ +var chalk = require('chalk'); +var asIs = require('lodash').identity; + +module.exports = { + syntax: { + string: chalk.green, + punctuator: chalk.grey, + keyword: chalk.cyan, + number: chalk.magenta, + regex: chalk.magenta, + comment: chalk.grey.bold, + invalid: chalk.inverse + }, + + codeFrame: asIs, + + codeLine: function (num, base, src, isLast) { + var prefix = base ? ' > ' : ' '; + var lineNum = prefix + num + ' '; + + if (base) + lineNum = chalk.bgRed(lineNum); + + var line = lineNum + '|' + src; + + if (!isLast) + line += '\n'; + + return line; + }, + + stackLine: function (name, location, isLast) { + var line = ' at ' + chalk.bold(name) + ' (' + chalk.grey.underline(location) + ')'; + + if (!isLast) + line += '\n'; + + return line; + }, + + stack: function (stack) { + return '\n\n' + stack; + } +}; diff --git a/modules/callsite-record/renderers/html.js b/modules/callsite-record/renderers/html.js new file mode 100644 index 00000000000..372021c01ca --- /dev/null +++ b/modules/callsite-record/renderers/html.js @@ -0,0 +1,38 @@ +var escapeHtml = require('lodash').escape; + +module.exports = { + syntax: ['string', 'punctuator', 'keyword', 'number', 'regex', 'comment', 'invalid'].reduce(function (syntaxRenderer, tokenType) { + syntaxRenderer[tokenType] = function (str) { + return '' + escapeHtml(str) + ''; + }; + + return syntaxRenderer; + }, {}), + + codeFrame: function (str) { + return '
' + str + '
'; + }, + + codeLine: function (num, base, src, isLast) { + var lineClass = isLast ? 'code-line-last' : 'code-line'; + var numClass = base ? 'code-line-num-base' : 'code-line-num'; + + return '
' + + '
' + num + '
' + + '
' + src + '
' + + '
'; + }, + + stackLine: function (name, location, isLast) { + var lineClass = isLast ? 'stack-line-last' : 'stack-line'; + + return '
' + + '
' + escapeHtml(name) + '
' + + '
' + escapeHtml(location) + '
' + + '
'; + }, + + stack: function (stack) { + return '
' + stack + '
'; + } +}; diff --git a/modules/callsite-record/renderers/no-color.js b/modules/callsite-record/renderers/no-color.js new file mode 100644 index 00000000000..602b9038b61 --- /dev/null +++ b/modules/callsite-record/renderers/no-color.js @@ -0,0 +1,28 @@ +var asIs = require('lodash').identity; + +module.exports = { + codeFrame: asIs, + + codeLine: function (num, base, src, isLast) { + var prefix = base ? ' > ' : ' '; + var line = prefix + num + ' |' + src; + + if (!isLast) + line += '\n'; + + return line; + }, + + stackLine: function (name, location, isLast) { + var line = ' at ' + name + ' (' + location + ')'; + + if (!isLast) + line += '\n'; + + return line; + }, + + stack: function (stack) { + return '\n\n' + stack; + } +}; diff --git a/src/compiler/es-next/compile-client-function.js b/src/compiler/es-next/compile-client-function.js index 9fc69a2de50..fb55ceb6fad 100644 --- a/src/compiler/es-next/compile-client-function.js +++ b/src/compiler/es-next/compile-client-function.js @@ -41,9 +41,9 @@ var babelArtifactPolyfills = { 'typeof': { re: new RegExp(escapeRe( 'var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? ' + - 'function (obj) {return typeof obj;} : ' + - 'function (obj) {return obj && typeof Symbol === "function" && obj.constructor === Symbol ' + - '&& obj !== Symbol.prototype ? "symbol" : typeof obj;};' + 'function (obj) { return typeof obj; } : ' + + 'function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ' + + '&& obj !== Symbol.prototype ? "symbol" : typeof obj; };' ), 'g'), getCode: () => 'var _typeof = function(obj) { return typeof obj; };', @@ -58,7 +58,6 @@ function getBabelOptions () { return { presets: [presetFallback], sourceMaps: false, - retainLines: true, ast: false, babelrc: false, highlightCode: false @@ -94,7 +93,7 @@ function addBabelArtifactsPolyfills (fnCode, dependenciesDefinition) { return polyfillsCode; }, ''); - return `(function(){${dependenciesDefinition}${polyfills} return ${modifiedFnCode}})();`; + return `(function(){${dependenciesDefinition}${polyfills} return ${modifiedFnCode.trim()}})();`; } function getDependenciesDefinition (dependencies) { diff --git a/src/compiler/es-next/index.js b/src/compiler/es-next/index.js index af54624deae..80f1183dad0 100644 --- a/src/compiler/es-next/index.js +++ b/src/compiler/es-next/index.js @@ -47,7 +47,6 @@ export default class ESNextCompiler { ], filename: filename, sourceMaps: true, - retainLines: true, ast: false, babelrc: false, highlightCode: false, diff --git a/src/errors/get-callsite.js b/src/errors/get-callsite.js index ab179e20bba..49e58ba1e83 100644 --- a/src/errors/get-callsite.js +++ b/src/errors/get-callsite.js @@ -1,4 +1,4 @@ -import createCallsiteRecord from 'callsite-record'; +import createCallsiteRecord from '../../modules/callsite-record'; import stackCleaningHook from './stack-cleaning-hook'; const STACK_TRACE_LIMIT = 2000; diff --git a/test/server/compiler-test.js b/test/server/compiler-test.js index 659e904e66a..6048ffd10b2 100644 --- a/test/server/compiler-test.js +++ b/test/server/compiler-test.js @@ -452,5 +452,29 @@ describe('Compiler', function () { expect(stack[2].source).to.have.string('testfile.js'); }); }); + + it('Incorrect callsite stack for failed assertion in a method of some class (GH-1267)', function () { + return compile('test/server/data/test-suites/regression-gh-1267/testfile.js') + .then(function (compiled) { + return compiled.tests[0].fn(testRunMock); + }) + .then(function () { + throw 'Promise rejection expected'; + }) + .catch(function (err) { + var callsite = err.callsite.renderSync({ renderer: renderers.noColor }); + + expect(callsite).to.contains( + ' 13 |}\n' + + ' 14 |\n' + + " 15 |test('test', async t => {\n" + + ' 16 | const page = new Page();\n' + + ' 17 |\n' + + ' > 18 | await page.expect(t);\n' + + ' 19 |});\n' + + ' 20 |\n' + ); + }); + }); }); }); diff --git a/test/server/data/test-suites/regression-gh-1267/testfile.js b/test/server/data/test-suites/regression-gh-1267/testfile.js new file mode 100644 index 00000000000..041f89e5f23 --- /dev/null +++ b/test/server/data/test-suites/regression-gh-1267/testfile.js @@ -0,0 +1,19 @@ +import { APIError } from '../../../../../lib/errors/runtime'; + +fixture `f`; + +class Page { + async expect (t) { + throw new APIError('expect'); + } +} + +async function fn (t) { + +} + +test('test', async t => { + const page = new Page(); + + await page.expect(t); +});