# lib/goog/testing/stacktrace.js

    1// Copyright 2009 The Closure Library Authors. All Rights Reserved.2//3// Licensed under the Apache License, Version 2.0 (the "License");4// you may not use this file except in compliance with the License.5// You may obtain a copy of the License at6//7//      http://www.apache.org/licenses/LICENSE-2.08//9// Unless required by applicable law or agreed to in writing, software10// distributed under the License is distributed on an "AS-IS" BASIS,11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12// See the License for the specific language governing permissions and13// limitations under the License.1415/**16 * @fileoverview Tools for parsing and pretty printing error stack traces.17 *18 */1920goog.provide('goog.testing.stacktrace');21goog.provide('goog.testing.stacktrace.Frame');22232425/**26 * Class representing one stack frame.27 * @param {string} context Context object, empty in case of global functions or28 *     if the browser doesn't provide this information.29 * @param {string} name Function name, empty in case of anonymous functions.30 * @param {string} alias Alias of the function if available. For example the31 *     function name will be 'c' and the alias will be 'b' if the function is32 *     defined as <code>a.b = function c() {};</code>.33 * @param {string} args Arguments of the function in parentheses if available.34 * @param {string} path File path or URL including line number and optionally35 *     column number separated by colons.36 * @constructor37 * @final38 */39goog.testing.stacktrace.Frame = function(context, name, alias, args, path) {40  this.context_ = context;41  this.name_ = name;42  this.alias_ = alias;43  this.args_ = args;44  this.path_ = path;45};464748/**49 * @return {string} The function name or empty string if the function is50 *     anonymous and the object field which it's assigned to is unknown.51 */52goog.testing.stacktrace.Frame.prototype.getName = function() {53  return this.name_;54};555657/**58 * @return {boolean} Whether the stack frame contains an anonymous function.59 */60goog.testing.stacktrace.Frame.prototype.isAnonymous = function() {61  return !this.name_ || this.context_ == '[object Object]';62};636465/**66 * Brings one frame of the stack trace into a common format across browsers.67 * @return {string} Pretty printed stack frame.68 */69goog.testing.stacktrace.Frame.prototype.toCanonicalString = function() {70  var htmlEscape = goog.testing.stacktrace.htmlEscape_;71  var deobfuscate = goog.testing.stacktrace.maybeDeobfuscateFunctionName_;7273  var canonical = [74    this.context_ ? htmlEscape(this.context_) + '.' : '',75    this.name_ ? htmlEscape(deobfuscate(this.name_)) : 'anonymous',76    htmlEscape(this.args_),77    this.alias_ ? ' [as ' + htmlEscape(deobfuscate(this.alias_)) + ']' : ''78  ];7980  if (this.path_) {81    canonical.push(' at ');82    canonical.push(htmlEscape(this.path_));83  }84  return canonical.join('');85};868788/**89 * Maximum number of steps while the call chain is followed.90 * @private {number}91 * @const92 */93goog.testing.stacktrace.MAX_DEPTH_ = 20;949596/**97 * Maximum length of a string that can be matched with a RegExp on98 * Firefox 3x. Exceeding this approximate length will cause string.match99 * to exceed Firefox's stack quota. This situation can be encountered100 * when goog.globalEval is invoked with a long argument; such as101 * when loading a module.102 * @private {number}103 * @const104 */105goog.testing.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_ = 500000;106107108/**109 * RegExp pattern for JavaScript identifiers. We don't support Unicode110 * identifiers defined in ECMAScript v3.111 * @private {string}112 * @const113 */114goog.testing.stacktrace.IDENTIFIER_PATTERN_ = '[a-zA-Z_$][\\w$]*';115116117/**118 * RegExp pattern for function name alias in the V8 stack trace.119 * @private {string}120 * @const121 */122goog.testing.stacktrace.V8_ALIAS_PATTERN_ =123    '(?: \\[as (' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')\\])?';124125126/**127 * RegExp pattern for the context of a function call in a V8 stack trace.128 * Creates an optional submatch for the namespace identifier including the129 * "new" keyword for constructor calls (e.g. "new foo.Bar").130 * @private {string}131 * @const132 */133goog.testing.stacktrace.V8_CONTEXT_PATTERN_ =134    '(?:((?:new )?(?:\\[object Object\\]|' +135    goog.testing.stacktrace.IDENTIFIER_PATTERN_ +136    '(?:\\.' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')*))\\.)?';137138139/**140 * RegExp pattern for function names and constructor calls in the V8 stack141 * trace.142 * @private {string}143 * @const144 */145goog.testing.stacktrace.V8_FUNCTION_NAME_PATTERN_ =146    '(?:new )?(?:' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ +147    '|<anonymous>)';148149150/**151 * RegExp pattern for function call in the V8 stack trace. Creates 3 submatches152 * with context object (optional), function name and function alias (optional).153 * @private {string}154 * @const155 */156goog.testing.stacktrace.V8_FUNCTION_CALL_PATTERN_ =157    ' ' + goog.testing.stacktrace.V8_CONTEXT_PATTERN_ +158    '(' + goog.testing.stacktrace.V8_FUNCTION_NAME_PATTERN_ + ')' +159    goog.testing.stacktrace.V8_ALIAS_PATTERN_;160161162/**163 * RegExp pattern for an URL + position inside the file.164 * @private {string}165 * @const166 */167goog.testing.stacktrace.URL_PATTERN_ =168    '((?:http|https|file)://[^\\s)]+|javascript:.*)';169170171/**172 * RegExp pattern for an URL + line number + column number in V8.173 * The URL is either in submatch 1 or submatch 2.174 * @private {string}175 * @const176 */177goog.testing.stacktrace.CHROME_URL_PATTERN_ = ' (?:' +178    '\\(unknown source\\)' + '|' +179    '\\(native\\)' + '|' +180    '\\((.+)\\)|(.+))';181182183/**184 * Regular expression for parsing one stack frame in V8. For more information185 * on V8 stack frame formats, see186 * https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi.187 * @private {!RegExp}188 * @const189 */190goog.testing.stacktrace.V8_STACK_FRAME_REGEXP_ = new RegExp('^    at' +191    '(?:' + goog.testing.stacktrace.V8_FUNCTION_CALL_PATTERN_ + ')?' +192    goog.testing.stacktrace.CHROME_URL_PATTERN_ + '$');193194195/**196 * RegExp pattern for function call in the Firefox stack trace.197 * Creates 2 submatches with function name (optional) and arguments.198 * @private {string}199 * @const200 */201goog.testing.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ =202    '(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')?' +203    '(\\(.*\\))?@';204205206/**207 * Regular expression for parsing one stack frame in Firefox.208 * @private {!RegExp}209 * @const210 */211goog.testing.stacktrace.FIREFOX_STACK_FRAME_REGEXP_ = new RegExp('^' +212    goog.testing.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ +213    '(?::0|' + goog.testing.stacktrace.URL_PATTERN_ + ')$');214215216/**217 * RegExp pattern for an anonymous function call in an Opera stack frame.218 * Creates 2 (optional) submatches: the context object and function name.219 * @private {string}220 * @const221 */222goog.testing.stacktrace.OPERA_ANONYMOUS_FUNCTION_NAME_PATTERN_ =223    '<anonymous function(?:\\: ' +224    '(?:(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ +225    '(?:\\.' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')*)\\.)?' +226    '(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + '))?>';227228229/**230 * RegExp pattern for a function call in an Opera stack frame.231 * Creates 4 (optional) submatches: the function name (if not anonymous),232 * the aliased context object and function name (if anonymous), and the233 * function call arguments.234 * @private {string}235 * @const236 */237goog.testing.stacktrace.OPERA_FUNCTION_CALL_PATTERN_ =238    '(?:(?:(' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')|' +239    goog.testing.stacktrace.OPERA_ANONYMOUS_FUNCTION_NAME_PATTERN_ +240    ')(\\(.*\\)))?@';241242243/**244 * Regular expression for parsing on stack frame in Opera 11.68 - 12.17.245 * Newer versions of Opera use V8 and stack frames should match against246 * goog.testing.stacktrace.V8_STACK_FRAME_REGEXP_.247 * @private {!RegExp}248 * @const249 */250goog.testing.stacktrace.OPERA_STACK_FRAME_REGEXP_ = new RegExp('^' +251    goog.testing.stacktrace.OPERA_FUNCTION_CALL_PATTERN_ +252    goog.testing.stacktrace.URL_PATTERN_ + '?$');253254255/**256 * Regular expression for finding the function name in its source.257 * @private {!RegExp}258 * @const259 */260goog.testing.stacktrace.FUNCTION_SOURCE_REGEXP_ = new RegExp(261    '^function (' + goog.testing.stacktrace.IDENTIFIER_PATTERN_ + ')');262263264/**265 * RegExp pattern for function call in a IE stack trace. This expression allows266 * for identifiers like 'Anonymous function', 'eval code', and 'Global code'.267 * @private {string}268 * @const269 */270goog.testing.stacktrace.IE_FUNCTION_CALL_PATTERN_ = '(' +271    goog.testing.stacktrace.IDENTIFIER_PATTERN_ + '(?:\\s+\\w+)*)';272273274/**275 * Regular expression for parsing a stack frame in IE.276 * @private {!RegExp}277 * @const278 */279goog.testing.stacktrace.IE_STACK_FRAME_REGEXP_ = new RegExp('^   at ' +280    goog.testing.stacktrace.IE_FUNCTION_CALL_PATTERN_ +281    '\\s*\\((eval code:[^)]*|' + goog.testing.stacktrace.URL_PATTERN_ +282    ')\\)?$');283284285/**286 * Creates a stack trace by following the call chain. Based on287 * {@link goog.debug.getStacktrace}.288 * @return {!Array.<!goog.testing.stacktrace.Frame>} Stack frames.289 * @private290 * @suppress {es5Strict}291 */292goog.testing.stacktrace.followCallChain_ = function() {293  var frames = [];294  var fn = arguments.callee.caller;295  var depth = 0;296297  while (fn && depth < goog.testing.stacktrace.MAX_DEPTH_) {298    var fnString = Function.prototype.toString.call(fn);299    var match = fnString.match(goog.testing.stacktrace.FUNCTION_SOURCE_REGEXP_);300    var functionName = match ? match[1] : '';301302    var argsBuilder = ['('];303    if (fn.arguments) {304      for (var i = 0; i < fn.arguments.length; i++) {305        var arg = fn.arguments[i];306        if (i > 0) {307          argsBuilder.push(', ');308        }309        if (goog.isString(arg)) {310          argsBuilder.push('"', arg, '"');311        } else {312          // Some args are mocks, and we don't want to fail from them not having313          // expected a call to toString, so instead insert a static string.314          if (arg && arg['$replay']) {315            argsBuilder.push('goog.testing.Mock');316          } else {317            argsBuilder.push(String(arg));318          }319        }320      }321    } else {322      // Opera 10 doesn't know the arguments of native functions.323      argsBuilder.push('unknown');324    }325    argsBuilder.push(')');326    var args = argsBuilder.join('');327328    frames.push(new goog.testing.stacktrace.Frame('', functionName, '', args,329        ''));330331    /** @preserveTry */332    try {333      fn = fn.caller;334    } catch (e) {335      break;336    }337    depth++;338  }339340  return frames;341};342343344/**345 * Parses one stack frame.346 * @param {string} frameStr The stack frame as string.347 * @return {goog.testing.stacktrace.Frame} Stack frame object or null if the348 *     parsing failed.349 * @private350 */351goog.testing.stacktrace.parseStackFrame_ = function(frameStr) {352  // This match includes newer versions of Opera (15+).353  var m = frameStr.match(goog.testing.stacktrace.V8_STACK_FRAME_REGEXP_);354  if (m) {355    return new goog.testing.stacktrace.Frame(m[1] || '', m[2] || '', m[3] || '',356        '', m[4] || m[5] || m[6] || '');357  }358359  if (frameStr.length >360      goog.testing.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_) {361    return goog.testing.stacktrace.parseLongFirefoxFrame_(frameStr);362  }363364  m = frameStr.match(goog.testing.stacktrace.FIREFOX_STACK_FRAME_REGEXP_);365  if (m) {366    return new goog.testing.stacktrace.Frame('', m[1] || '', '', m[2] || '',367        m[3] || '');368  }369370  // Match against Presto Opera 11.68 - 12.17.371  m = frameStr.match(goog.testing.stacktrace.OPERA_STACK_FRAME_REGEXP_);372  if (m) {373    return new goog.testing.stacktrace.Frame(m[2] || '', m[1] || m[3] || '',374        '', m[4] || '', m[5] || '');375  }376377  m = frameStr.match(goog.testing.stacktrace.IE_STACK_FRAME_REGEXP_);378  if (m) {379    return new goog.testing.stacktrace.Frame('', m[1] || '', '', '',380        m[2] || '');381  }382383  return null;384};385386387/**388 * Parses a long firefox stack frame.389 * @param {string} frameStr The stack frame as string.390 * @return {!goog.testing.stacktrace.Frame} Stack frame object.391 * @private392 */393goog.testing.stacktrace.parseLongFirefoxFrame_ = function(frameStr) {394  var firstParen = frameStr.indexOf('(');395  var lastAmpersand = frameStr.lastIndexOf('@');396  var lastColon = frameStr.lastIndexOf(':');397  var functionName = '';398  if ((firstParen >= 0) && (firstParen < lastAmpersand)) {399    functionName = frameStr.substring(0, firstParen);400  }401  var loc = '';402  if ((lastAmpersand >= 0) && (lastAmpersand + 1 < lastColon)) {403    loc = frameStr.substring(lastAmpersand + 1);404  }405  var args = '';406  if ((firstParen >= 0 && lastAmpersand > 0) &&407      (firstParen < lastAmpersand)) {408    args = frameStr.substring(firstParen, lastAmpersand);409  }410  return new goog.testing.stacktrace.Frame('', functionName, '', args, loc);411};412413414/**415 * Function to deobfuscate function names.416 * @type {function(string): string}417 * @private418 */419goog.testing.stacktrace.deobfuscateFunctionName_;420421422/**423 * Sets function to deobfuscate function names.424 * @param {function(string): string} fn function to deobfuscate function names.425 */426goog.testing.stacktrace.setDeobfuscateFunctionName = function(fn) {427  goog.testing.stacktrace.deobfuscateFunctionName_ = fn;428};429430431/**432 * Deobfuscates a compiled function name with the function passed to433 * {@link #setDeobfuscateFunctionName}. Returns the original function name if434 * the deobfuscator hasn't been set.435 * @param {string} name The function name to deobfuscate.436 * @return {string} The deobfuscated function name.437 * @private438 */439goog.testing.stacktrace.maybeDeobfuscateFunctionName_ = function(name) {440  return goog.testing.stacktrace.deobfuscateFunctionName_ ?441      goog.testing.stacktrace.deobfuscateFunctionName_(name) : name;442};443444445/**446 * Escapes the special character in HTML.447 * @param {string} text Plain text.448 * @return {string} Escaped text.449 * @private450 */451goog.testing.stacktrace.htmlEscape_ = function(text) {452  return text.replace(/&/g, '&amp;').453             replace(/</g, '&lt;').454             replace(/>/g, '&gt;').455             replace(/"/g, '&quot;');456};457458459/**460 * Converts the stack frames into canonical format. Chops the beginning and the461 * end of it which come from the testing environment, not from the test itself.462 * @param {!Array.<goog.testing.stacktrace.Frame>} frames The frames.463 * @return {string} Canonical, pretty printed stack trace.464 * @private465 */466goog.testing.stacktrace.framesToString_ = function(frames) {467  // Removes the anonymous calls from the end of the stack trace (they come468  // from testrunner.js, testcase.js and asserts.js), so the stack trace will469  // end with the test... method.470  var lastIndex = frames.length - 1;471  while (frames[lastIndex] && frames[lastIndex].isAnonymous()) {472    lastIndex--;473  }474475  // Removes the beginning of the stack trace until the call of the private476  // _assert function (inclusive), so the stack trace will begin with a public477  // asserter. Does nothing if _assert is not present in the stack trace.478  var privateAssertIndex = -1;479  for (var i = 0; i < frames.length; i++) {480    if (frames[i] && frames[i].getName() == '_assert') {481      privateAssertIndex = i;482      break;483    }484  }485486  var canonical = [];487  for (var i = privateAssertIndex + 1; i <= lastIndex; i++) {488    canonical.push('> ');489    if (frames[i]) {490      canonical.push(frames[i].toCanonicalString());491    } else {492      canonical.push('(unknown)');493    }494    canonical.push('\n');495  }496  return canonical.join('');497};498499500/**501 * Parses the browser's native stack trace.502 * @param {string} stack Stack trace.503 * @return {!Array.<goog.testing.stacktrace.Frame>} Stack frames. The504 *     unrecognized frames will be nulled out.505 * @private506 */507goog.testing.stacktrace.parse_ = function(stack) {508  var lines = stack.replace(/\s*$/, '').split('\n');509  var frames = [];510  for (var i = 0; i < lines.length; i++) {511    frames.push(goog.testing.stacktrace.parseStackFrame_(lines[i]));512  }513  return frames;514};515516517/**518 * Brings the stack trace into a common format across browsers.519 * @param {string} stack Browser-specific stack trace.520 * @return {string} Same stack trace in common format.521 */522goog.testing.stacktrace.canonicalize = function(stack) {523  var frames = goog.testing.stacktrace.parse_(stack);524  return goog.testing.stacktrace.framesToString_(frames);525};526527528/**529 * Returns the native stack trace.530 * @return {string|!Array.<!CallSite>}531 * @private532 */533goog.testing.stacktrace.getNativeStack_ = function() {534  var tmpError = new Error();535  if (tmpError.stack) {536    return tmpError.stack;537  }538539  // IE10 will only create a stack trace when the Error is thrown.540  // We use null.x() to throw an exception because the closure compiler may541  // replace "throw" with a function call in an attempt to minimize the binary542  // size, which in turn has the side effect of adding an unwanted stack frame.543  try {544    null.x();545  } catch (e) {546    return e.stack;547  }548  return '';549};550551552/**553 * Gets the native stack trace if available otherwise follows the call chain.554 * @return {string} The stack trace in canonical format.555 */556goog.testing.stacktrace.get = function() {557  var stack = goog.testing.stacktrace.getNativeStack_();558  var frames;559  if (!stack) {560    frames = goog.testing.stacktrace.followCallChain_();561  } else if (goog.isArray(stack)) {562    frames = goog.testing.stacktrace.callSitesToFrames_(stack);563  } else {564    frames = goog.testing.stacktrace.parse_(stack);565  }566  return goog.testing.stacktrace.framesToString_(frames);567};568569570/**571 * Converts an array of CallSite (elements of a stack trace in V8) to an array572 * of Frames.573 * @param {!Array.<!CallSite>} stack The stack as an array of CallSites.574 * @return {!Array.<!goog.testing.stacktrace.Frame>} The stack as an array of575 *     Frames.576 * @private577 */578goog.testing.stacktrace.callSitesToFrames_ = function(stack) {579  var frames = [];580  for (var i = 0; i < stack.length; i++) {581    var callSite = stack[i];582    var functionName = callSite.getFunctionName() || 'unknown';583    var fileName = callSite.getFileName();584    var path = fileName ? fileName + ':' + callSite.getLineNumber() + ':' +585        callSite.getColumnNumber() : 'unknown';586    frames.push(587        new goog.testing.stacktrace.Frame('', functionName, '', '', path));588  }589  return frames;590};591592593goog.exportSymbol('setDeobfuscateFunctionName',594    goog.testing.stacktrace.setDeobfuscateFunctionName);