index.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. 'use strict';
  2. module.exports = StackUtils;
  3. function StackUtils(opts) {
  4. if (!(this instanceof StackUtils)) {
  5. throw new Error('StackUtils constructor must be called with new');
  6. }
  7. opts = opts || {};
  8. this._cwd = (opts.cwd || process.cwd()).replace(/\\/g, '/');
  9. this._internals = opts.internals || [];
  10. this._wrapCallSite = opts.wrapCallSite || false;
  11. }
  12. module.exports.nodeInternals = nodeInternals;
  13. function nodeInternals() {
  14. if (!module.exports.natives) {
  15. module.exports.natives = Object.keys(process.binding('natives'));
  16. module.exports.natives.push('bootstrap_node', 'node',
  17. 'internal/bootstrap/node');
  18. }
  19. return module.exports.natives.map(function (n) {
  20. return new RegExp('\\(' + n + '\\.js:\\d+:\\d+\\)$');
  21. }).concat([
  22. /\s*at (bootstrap_)?node\.js:\d+:\d+?$/,
  23. /\(internal\/[^:]+:\d+:\d+\)$/,
  24. /\/\.node-spawn-wrap-\w+-\w+\/node:\d+:\d+\)?$/
  25. ]);
  26. }
  27. StackUtils.prototype.clean = function (stack) {
  28. if (!Array.isArray(stack)) {
  29. stack = stack.split('\n');
  30. }
  31. if (!(/^\s*at /.test(stack[0])) &&
  32. (/^\s*at /.test(stack[1]))) {
  33. stack = stack.slice(1);
  34. }
  35. var outdent = false;
  36. var lastNonAtLine = null;
  37. var result = [];
  38. stack.forEach(function (st) {
  39. st = st.replace(/\\/g, '/');
  40. var isInternal = this._internals.some(function (internal) {
  41. return internal.test(st);
  42. });
  43. if (isInternal) {
  44. return null;
  45. }
  46. var isAtLine = /^\s*at /.test(st);
  47. if (outdent) {
  48. st = st.replace(/\s+$/, '').replace(/^(\s+)at /, '$1');
  49. } else {
  50. st = st.trim();
  51. if (isAtLine) {
  52. st = st.substring(3);
  53. }
  54. }
  55. st = st.replace(this._cwd + '/', '');
  56. if (st) {
  57. if (isAtLine) {
  58. if (lastNonAtLine) {
  59. result.push(lastNonAtLine);
  60. lastNonAtLine = null;
  61. }
  62. result.push(st);
  63. } else {
  64. outdent = true;
  65. lastNonAtLine = st;
  66. }
  67. }
  68. }, this);
  69. stack = result.join('\n').trim();
  70. if (stack) {
  71. return stack + '\n';
  72. }
  73. return '';
  74. };
  75. StackUtils.prototype.captureString = function (limit, fn) {
  76. if (typeof limit === 'function') {
  77. fn = limit;
  78. limit = Infinity;
  79. }
  80. if (!fn) {
  81. fn = this.captureString;
  82. }
  83. var limitBefore = Error.stackTraceLimit;
  84. if (limit) {
  85. Error.stackTraceLimit = limit;
  86. }
  87. var obj = {};
  88. Error.captureStackTrace(obj, fn);
  89. var stack = obj.stack;
  90. Error.stackTraceLimit = limitBefore;
  91. return this.clean(stack);
  92. };
  93. StackUtils.prototype.capture = function (limit, fn) {
  94. if (typeof limit === 'function') {
  95. fn = limit;
  96. limit = Infinity;
  97. }
  98. if (!fn) {
  99. fn = this.capture;
  100. }
  101. var prepBefore = Error.prepareStackTrace;
  102. var limitBefore = Error.stackTraceLimit;
  103. var wrapCallSite = this._wrapCallSite;
  104. Error.prepareStackTrace = function (obj, site) {
  105. if (wrapCallSite) {
  106. return site.map(wrapCallSite);
  107. }
  108. return site;
  109. };
  110. if (limit) {
  111. Error.stackTraceLimit = limit;
  112. }
  113. var obj = {};
  114. Error.captureStackTrace(obj, fn);
  115. var stack = obj.stack;
  116. Error.prepareStackTrace = prepBefore;
  117. Error.stackTraceLimit = limitBefore;
  118. return stack;
  119. };
  120. StackUtils.prototype.at = function at(fn) {
  121. if (!fn) {
  122. fn = at;
  123. }
  124. var site = this.capture(1, fn)[0];
  125. if (!site) {
  126. return {};
  127. }
  128. var res = {
  129. line: site.getLineNumber(),
  130. column: site.getColumnNumber()
  131. };
  132. this._setFile(res, site.getFileName());
  133. if (site.isConstructor()) {
  134. res.constructor = true;
  135. }
  136. if (site.isEval()) {
  137. res.evalOrigin = site.getEvalOrigin();
  138. }
  139. // Node v10 stopped with the isNative() on callsites, apparently
  140. /* istanbul ignore next */
  141. if (site.isNative()) {
  142. res.native = true;
  143. }
  144. var typename = null;
  145. try {
  146. typename = site.getTypeName();
  147. } catch (er) {}
  148. if (typename &&
  149. typename !== 'Object' &&
  150. typename !== '[object Object]') {
  151. res.type = typename;
  152. }
  153. var fname = site.getFunctionName();
  154. if (fname) {
  155. res.function = fname;
  156. }
  157. var meth = site.getMethodName();
  158. if (meth && fname !== meth) {
  159. res.method = meth;
  160. }
  161. return res;
  162. };
  163. StackUtils.prototype._setFile = function (result, filename) {
  164. if (filename) {
  165. filename = filename.replace(/\\/g, '/');
  166. if ((filename.indexOf(this._cwd + '/') === 0)) {
  167. filename = filename.substr(this._cwd.length + 1);
  168. }
  169. result.file = filename;
  170. }
  171. };
  172. var re = new RegExp(
  173. '^' +
  174. // Sometimes we strip out the ' at' because it's noisy
  175. '(?:\\s*at )?' +
  176. // $1 = ctor if 'new'
  177. '(?:(new) )?' +
  178. // $2 = function name (can be literally anything)
  179. // May contain method at the end as [as xyz]
  180. '(?:(.*?) \\()?' +
  181. // (eval at <anonymous> (file.js:1:1),
  182. // $3 = eval origin
  183. // $4:$5:$6 are eval file/line/col, but not normally reported
  184. '(?:eval at ([^ ]+) \\((.+?):(\\d+):(\\d+)\\), )?' +
  185. // file:line:col
  186. // $7:$8:$9
  187. // $10 = 'native' if native
  188. '(?:(.+?):(\\d+):(\\d+)|(native))' +
  189. // maybe close the paren, then end
  190. // if $11 is ), then we only allow balanced parens in the filename
  191. // any imbalance is placed on the fname. This is a heuristic, and
  192. // bound to be incorrect in some edge cases. The bet is that
  193. // having weird characters in method names is more common than
  194. // having weird characters in filenames, which seems reasonable.
  195. '(\\)?)$'
  196. );
  197. var methodRe = /^(.*?) \[as (.*?)\]$/;
  198. StackUtils.prototype.parseLine = function parseLine(line) {
  199. var match = line && line.match(re);
  200. if (!match) {
  201. return null;
  202. }
  203. var ctor = match[1] === 'new';
  204. var fname = match[2];
  205. var evalOrigin = match[3];
  206. var evalFile = match[4];
  207. var evalLine = Number(match[5]);
  208. var evalCol = Number(match[6]);
  209. var file = match[7];
  210. var lnum = match[8];
  211. var col = match[9];
  212. var native = match[10] === 'native';
  213. var closeParen = match[11] === ')';
  214. var res = {};
  215. if (lnum) {
  216. res.line = Number(lnum);
  217. }
  218. if (col) {
  219. res.column = Number(col);
  220. }
  221. if (closeParen && file) {
  222. // make sure parens are balanced
  223. // if we have a file like "asdf) [as foo] (xyz.js", then odds are
  224. // that the fname should be += " (asdf) [as foo]" and the file
  225. // should be just "xyz.js"
  226. // walk backwards from the end to find the last unbalanced (
  227. var closes = 0;
  228. for (var i = file.length - 1; i > 0; i--) {
  229. if (file.charAt(i) === ')') {
  230. closes ++;
  231. } else if (file.charAt(i) === '(' && file.charAt(i - 1) === ' ') {
  232. closes --;
  233. if (closes === -1 && file.charAt(i - 1) === ' ') {
  234. var before = file.substr(0, i - 1);
  235. var after = file.substr(i + 1);
  236. file = after;
  237. fname += ' (' + before;
  238. break;
  239. }
  240. }
  241. }
  242. }
  243. if (fname) {
  244. var methodMatch = fname.match(methodRe);
  245. if (methodMatch) {
  246. fname = methodMatch[1];
  247. var meth = methodMatch[2];
  248. }
  249. }
  250. this._setFile(res, file);
  251. if (ctor) {
  252. res.constructor = true;
  253. }
  254. if (evalOrigin) {
  255. res.evalOrigin = evalOrigin;
  256. res.evalLine = evalLine;
  257. res.evalColumn = evalCol;
  258. res.evalFile = evalFile && evalFile.replace(/\\/g, '/');
  259. }
  260. if (native) {
  261. res.native = true;
  262. }
  263. if (fname) {
  264. res.function = fname;
  265. }
  266. if (meth && fname !== meth) {
  267. res.method = meth;
  268. }
  269. return res;
  270. };
  271. var bound = new StackUtils();
  272. Object.keys(StackUtils.prototype).forEach(function (key) {
  273. StackUtils[key] = bound[key].bind(bound);
  274. });