# lib/webdriver/stacktrace.js

    1// Copyright 2009 The Closure Library Authors. All Rights Reserved.2// Copyright 2012 Selenium comitters3// Copyright 2012 Software Freedom Conservancy4//5// Licensed under the Apache License, Version 2.0 (the "License");6// you may not use this file except in compliance with the License.7// You may obtain a copy of the License at8//9//      http://www.apache.org/licenses/LICENSE-2.010//11// Unless required by applicable law or agreed to in writing, software12// distributed under the License is distributed on an "AS-IS" BASIS,13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.14// See the License for the specific language governing permissions and15// limitations under the License.1617/**18 * @fileoverview Tools for parsing and pretty printing error stack traces. This19 * file is based on goog.testing.stacktrace.20 */2122goog.provide('webdriver.stacktrace');23goog.provide('webdriver.stacktrace.Snapshot');2425goog.require('goog.array');26goog.require('goog.string');27goog.require('goog.userAgent');28293031/**32 * Stores a snapshot of the stack trace at the time this instance was created.33 * The stack trace will always be adjusted to exclude this function call.34 * @param {number=} opt_slice The number of frames to remove from the top of35 *     the generated stack trace.36 * @constructor37 */38webdriver.stacktrace.Snapshot = function(opt_slice) {3940  /** @private {number} */41  this.slice_ = opt_slice || 0;4243  var error;44  if (webdriver.stacktrace.CAN_CAPTURE_STACK_TRACE_) {45    error = Error();46    Error.captureStackTrace(error, webdriver.stacktrace.Snapshot);47  } else {48    // Remove 1 extra frame for the call to this constructor.49    this.slice_ += 1;50    // IE will only create a stack trace when the Error is thrown.51    // We use null.x() to throw an exception instead of throw this.error_52    // because the closure compiler may optimize throws to a function call53    // in an attempt to minimize the binary size which in turn has the side54    // effect of adding an unwanted stack frame.55    try {56      null.x();57    } catch (e) {58      error = e;59    }60  }6162  /**63   * The error's stacktrace.  This must be accessed immediately to ensure Opera64   * computes the context correctly.65   * @private {string}66   */67  this.stack_ = webdriver.stacktrace.getStack_(error);68};697071/**72 * Whether the current environment supports the Error.captureStackTrace73 * function (as of 10/17/2012, only V8).74 * @private {boolean}75 * @const76 */77webdriver.stacktrace.CAN_CAPTURE_STACK_TRACE_ =78    goog.isFunction(Error.captureStackTrace);798081/**82 * Whether the current browser supports stack traces.83 *84 * @type {boolean}85 * @const86 */87webdriver.stacktrace.BROWSER_SUPPORTED =88    webdriver.stacktrace.CAN_CAPTURE_STACK_TRACE_ || (function() {89      try {90        throw Error();91      } catch (e) {92        return !!e.stack;93      }94    })();959697/**98 * The parsed stack trace. This list is lazily generated the first time it is99 * accessed.100 * @private {Array.<!webdriver.stacktrace.Frame>}101 */102webdriver.stacktrace.Snapshot.prototype.parsedStack_ = null;103104105/**106 * @return {!Array.<!webdriver.stacktrace.Frame>} The parsed stack trace.107 */108webdriver.stacktrace.Snapshot.prototype.getStacktrace = function() {109  if (goog.isNull(this.parsedStack_)) {110    this.parsedStack_ = webdriver.stacktrace.parse_(this.stack_);111    if (this.slice_) {112      this.parsedStack_ = goog.array.slice(this.parsedStack_, this.slice_);113    }114    delete this.slice_;115    delete this.stack_;116  }117  return this.parsedStack_;118};119120121122/**123 * Class representing one stack frame.124 * @param {(string|undefined)} context Context object, empty in case of global125 *     functions or if the browser doesn't provide this information.126 * @param {(string|undefined)} name Function name, empty in case of anonymous127 *     functions.128 * @param {(string|undefined)} alias Alias of the function if available. For129 *     example the function name will be 'c' and the alias will be 'b' if the130 *     function is defined as <code>a.b = function c() {};</code>.131 * @param {(string|undefined)} path File path or URL including line number and132 *     optionally column number separated by colons.133 * @constructor134 */135webdriver.stacktrace.Frame = function(context, name, alias, path) {136137  /** @private {string} */138  this.context_ = context || '';139140  /** @private {string} */141  this.name_ = name || '';142143  /** @private {string} */144  this.alias_ = alias || '';145146  /** @private {string} */147  this.path_ = path || '';148149  /** @private {string} */150  this.url_ = this.path_;151152  /** @private {number} */153  this.line_ = -1;154155  /** @private {number} */156  this.column_ = -1;157158  if (path) {159    var match = /:(\d+)(?::(\d+))?$/.exec(path);160    if (match) {161      this.line_ = Number(match[1]);162      this.column = Number(match[2] || -1);163      this.url_ = path.substr(0, match.index);164    }165  }166};167168169/**170 * Constant for an anonymous frame.171 * @private {!webdriver.stacktrace.Frame}172 * @const173 */174webdriver.stacktrace.ANONYMOUS_FRAME_ =175    new webdriver.stacktrace.Frame('', '', '', '');176177178/**179 * @return {string} The function name or empty string if the function is180 *     anonymous and the object field which it's assigned to is unknown.181 */182webdriver.stacktrace.Frame.prototype.getName = function() {183  return this.name_;184};185186187/**188 * @return {string} The url or empty string if it is unknown.189 */190webdriver.stacktrace.Frame.prototype.getUrl = function() {191  return this.url_;192};193194195/**196 * @return {number} The line number if known or -1 if it is unknown.197 */198webdriver.stacktrace.Frame.prototype.getLine = function() {199  return this.line_;200};201202203/**204 * @return {number} The column number if known and -1 if it is unknown.205 */206webdriver.stacktrace.Frame.prototype.getColumn = function() {207  return this.column_;208};209210211/**212 * @return {boolean} Whether the stack frame contains an anonymous function.213 */214webdriver.stacktrace.Frame.prototype.isAnonymous = function() {215  return !this.name_ || this.context_ == '[object Object]';216};217218219/**220 * Converts this frame to its string representation using V8's stack trace221 * format: http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi222 * @return {string} The string representation of this frame.223 * @override224 */225webdriver.stacktrace.Frame.prototype.toString = function() {226  var context = this.context_;227  if (context && context !== 'new ') {228    context += '.';229  }230  context += this.name_;231  context += this.alias_ ? ' [as ' + this.alias_ + ']' : '';232233  var path = this.path_ || '<anonymous>';234  return '    at ' + (context ? context + ' (' + path + ')' : path);235};236237238/**239 * Maximum length of a string that can be matched with a RegExp on240 * Firefox 3x. Exceeding this approximate length will cause string.match241 * to exceed Firefox's stack quota. This situation can be encountered242 * when goog.globalEval is invoked with a long argument; such as243 * when loading a module.244 * @private {number}245 * @const246 */247webdriver.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_ = 500000;248249250/**251 * RegExp pattern for JavaScript identifiers. We don't support Unicode252 * identifiers defined in ECMAScript v3.253 * @private {string}254 * @const255 */256webdriver.stacktrace.IDENTIFIER_PATTERN_ = '[a-zA-Z_$][\\w$]*';257258259/**260 * Pattern for a matching the type on a fully-qualified name. Forms an261 * optional sub-match on the type. For example, in "foo.bar.baz", will match on262 * "foo.bar".263 * @private {string}264 * @const265 */266webdriver.stacktrace.CONTEXT_PATTERN_ =267    '(' + webdriver.stacktrace.IDENTIFIER_PATTERN_ +268    '(?:\\.' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')*)\\.';269270271/**272 * Pattern for matching a fully qualified name. Will create two sub-matches:273 * the type (optional), and the name. For example, in "foo.bar.baz", will274 * match on ["foo.bar", "baz"].275 * @private {string}276 * @const277 */278webdriver.stacktrace.QUALIFIED_NAME_PATTERN_ =279    '(?:' + webdriver.stacktrace.CONTEXT_PATTERN_ + ')?' +280    '(' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')';281282283/**284 * RegExp pattern for function name alias in the V8 stack trace.285 * @private {string}286 * @const287 */288webdriver.stacktrace.V8_ALIAS_PATTERN_ =289    '(?: \\[as (' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')\\])?';290291292/**293 * RegExp pattern for function names and constructor calls in the V8 stack294 * trace.295 * @private {string}296 * @const297 */298webdriver.stacktrace.V8_FUNCTION_NAME_PATTERN_ =299    '(?:' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + '|<anonymous>)';300301302/**303 * RegExp pattern for the context of a function call in V8. Creates two304 * submatches, only one of which will ever match: either the namespace305 * identifier (with optional "new" keyword in the case of a constructor call),306 * or just the "new " phrase for a top level constructor call.307 * @private {string}308 * @const309 */310webdriver.stacktrace.V8_CONTEXT_PATTERN_ =311    '(?:((?:new )?(?:\\[object Object\\]|' +312    webdriver.stacktrace.IDENTIFIER_PATTERN_ +313    '(?:\\.' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')*)' +314    ')\\.|(new ))';315316317/**318 * RegExp pattern for function call in the V8 stack trace.319 * Creates 3 submatches with context object (optional), function name and320 * function alias (optional).321 * @private {string}322 * @const323 */324webdriver.stacktrace.V8_FUNCTION_CALL_PATTERN_ =325    ' (?:' + webdriver.stacktrace.V8_CONTEXT_PATTERN_ + ')?' +326    '(' + webdriver.stacktrace.V8_FUNCTION_NAME_PATTERN_ + ')' +327    webdriver.stacktrace.V8_ALIAS_PATTERN_;328329330/**331 * RegExp pattern for an URL + position inside the file.332 * @private {string}333 * @const334 */335webdriver.stacktrace.URL_PATTERN_ =336    '((?:http|https|file)://[^\\s]+|javascript:.*)';337338339/**340 * RegExp pattern for a location string in a V8 stack frame. Creates two341 * submatches for the location, one for enclosed in parentheticals and on342 * where the location appears alone (which will only occur if the location is343 * the only information in the frame).344 * @private {string}345 * @const346 * @see http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi347 */348webdriver.stacktrace.V8_LOCATION_PATTERN_ = ' (?:\\((.*)\\)|(.*))';349350351/**352 * Regular expression for parsing one stack frame in V8.353 * @private {!RegExp}354 * @const355 */356webdriver.stacktrace.V8_STACK_FRAME_REGEXP_ = new RegExp('^    at' +357    '(?:' + webdriver.stacktrace.V8_FUNCTION_CALL_PATTERN_ + ')?' +358    webdriver.stacktrace.V8_LOCATION_PATTERN_ + '$');359360361/**362 * RegExp pattern for function names in the Firefox stack trace.363 * Firefox has extended identifiers to deal with inner functions and anonymous364 * functions: https://bugzilla.mozilla.org/show_bug.cgi?id=433529#c9365 * @private {string}366 * @const367 */368webdriver.stacktrace.FIREFOX_FUNCTION_NAME_PATTERN_ =369    webdriver.stacktrace.IDENTIFIER_PATTERN_ + '[\\w./<$]*';370371372/**373 * RegExp pattern for function call in the Firefox stack trace.374 * Creates a submatch for the function name.375 * @private {string}376 * @const377 */378webdriver.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ =379    '(' + webdriver.stacktrace.FIREFOX_FUNCTION_NAME_PATTERN_ + ')?' +380    '(?:\\(.*\\))?@';381382383/**384 * Regular expression for parsing one stack frame in Firefox.385 * @private {!RegExp}386 * @const387 */388webdriver.stacktrace.FIREFOX_STACK_FRAME_REGEXP_ = new RegExp('^' +389    webdriver.stacktrace.FIREFOX_FUNCTION_CALL_PATTERN_ +390    '(?::0|' + webdriver.stacktrace.URL_PATTERN_ + ')$');391392393/**394 * RegExp pattern for an anonymous function call in an Opera stack frame.395 * Creates 2 (optional) submatches: the context object and function name.396 * @private {string}397 * @const398 */399webdriver.stacktrace.OPERA_ANONYMOUS_FUNCTION_NAME_PATTERN_ =400    '<anonymous function(?:\\: ' +401    webdriver.stacktrace.QUALIFIED_NAME_PATTERN_ + ')?>';402403404/**405 * RegExp pattern for a function call in an Opera stack frame.406 * Creates 3 (optional) submatches: the function name (if not anonymous),407 * the aliased context object and the function name (if anonymous).408 * @private {string}409 * @const410 */411webdriver.stacktrace.OPERA_FUNCTION_CALL_PATTERN_ =412    '(?:(?:(' + webdriver.stacktrace.IDENTIFIER_PATTERN_ + ')|' +413    webdriver.stacktrace.OPERA_ANONYMOUS_FUNCTION_NAME_PATTERN_ +414    ')(?:\\(.*\\)))?@';415416417/**418 * Regular expression for parsing on stack frame in Opera 11.68+419 * @private {!RegExp}420 * @const421 */422webdriver.stacktrace.OPERA_STACK_FRAME_REGEXP_ = new RegExp('^' +423    webdriver.stacktrace.OPERA_FUNCTION_CALL_PATTERN_ +424    webdriver.stacktrace.URL_PATTERN_ + '?$');425426427/**428 * RegExp pattern for function call in a Chakra (IE) stack trace. This429 * expression allows for identifiers like 'Anonymous function', 'eval code',430 * and 'Global code'.431 * @private {string}432 * @const433 */434webdriver.stacktrace.CHAKRA_FUNCTION_CALL_PATTERN_ = '(' +435    webdriver.stacktrace.IDENTIFIER_PATTERN_ + '(?:\\s+\\w+)*)';436437438/**439 * Regular expression for parsing on stack frame in Chakra (IE).440 * @private {!RegExp}441 * @const442 */443webdriver.stacktrace.CHAKRA_STACK_FRAME_REGEXP_ = new RegExp('^   at ' +444    webdriver.stacktrace.CHAKRA_FUNCTION_CALL_PATTERN_ +445    '\\s*(?:\\((.*)\\))$');446447448/**449 * Placeholder for an unparsable frame in a stack trace generated by450 * {@link goog.testing.stacktrace}.451 * @private {string}452 * @const453 */454webdriver.stacktrace.UNKNOWN_CLOSURE_FRAME_ = '> (unknown)';455456457/**458 * Representation of an anonymous frame in a stack trace generated by459 * {@link goog.testing.stacktrace}.460 * @private {string}461 * @const462 */463webdriver.stacktrace.ANONYMOUS_CLOSURE_FRAME_ = '> anonymous';464465466/**467 * Pattern for a function call in a Closure stack trace. Creates three optional468 * submatches: the context, function name, and alias.469 * @private {string}470 * @const471 */472webdriver.stacktrace.CLOSURE_FUNCTION_CALL_PATTERN_ =473    webdriver.stacktrace.QUALIFIED_NAME_PATTERN_ +474    '(?:\\(.*\\))?' +  // Ignore arguments if present.475    webdriver.stacktrace.V8_ALIAS_PATTERN_;476477478/**479 * Regular expression for parsing a stack frame generated by Closure's480 * {@link goog.testing.stacktrace}.481 * @private {!RegExp}482 * @const483 */484webdriver.stacktrace.CLOSURE_STACK_FRAME_REGEXP_ = new RegExp('^> ' +485    '(?:' + webdriver.stacktrace.CLOSURE_FUNCTION_CALL_PATTERN_ +486    '(?: at )?)?' +487    '(?:(.*:\\d+:\\d+)|' + webdriver.stacktrace.URL_PATTERN_ + ')?$');488489490/**491 * Parses one stack frame.492 * @param {string} frameStr The stack frame as string.493 * @return {webdriver.stacktrace.Frame} Stack frame object or null if the494 *     parsing failed.495 * @private496 */497webdriver.stacktrace.parseStackFrame_ = function(frameStr) {498  var m = frameStr.match(webdriver.stacktrace.V8_STACK_FRAME_REGEXP_);499  if (m) {500    return new webdriver.stacktrace.Frame(501        m[1] || m[2], m[3], m[4], m[5] || m[6]);502  }503504  if (frameStr.length >505      webdriver.stacktrace.MAX_FIREFOX_FRAMESTRING_LENGTH_) {506    return webdriver.stacktrace.parseLongFirefoxFrame_(frameStr);507  }508509  m = frameStr.match(webdriver.stacktrace.FIREFOX_STACK_FRAME_REGEXP_);510  if (m) {511    return new webdriver.stacktrace.Frame('', m[1], '', m[2]);512  }513514  m = frameStr.match(webdriver.stacktrace.OPERA_STACK_FRAME_REGEXP_);515  if (m) {516    return new webdriver.stacktrace.Frame(m[2], m[1] || m[3], '', m[4]);517  }518519  m = frameStr.match(webdriver.stacktrace.CHAKRA_STACK_FRAME_REGEXP_);520  if (m) {521    return new webdriver.stacktrace.Frame('', m[1], '', m[2]);522  }523524  if (frameStr == webdriver.stacktrace.UNKNOWN_CLOSURE_FRAME_ ||525      frameStr == webdriver.stacktrace.ANONYMOUS_CLOSURE_FRAME_) {526    return webdriver.stacktrace.ANONYMOUS_FRAME_;527  }528529  m = frameStr.match(webdriver.stacktrace.CLOSURE_STACK_FRAME_REGEXP_);530  if (m) {531    return new webdriver.stacktrace.Frame(m[1], m[2], m[3], m[4] || m[5]);532  }533534  return null;535};536537538/**539 * Parses a long firefox stack frame.540 * @param {string} frameStr The stack frame as string.541 * @return {!webdriver.stacktrace.Frame} Stack frame object.542 * @private543 */544webdriver.stacktrace.parseLongFirefoxFrame_ = function(frameStr) {545  var firstParen = frameStr.indexOf('(');546  var lastAmpersand = frameStr.lastIndexOf('@');547  var lastColon = frameStr.lastIndexOf(':');548  var functionName = '';549  if ((firstParen >= 0) && (firstParen < lastAmpersand)) {550    functionName = frameStr.substring(0, firstParen);551  }552  var loc = '';553  if ((lastAmpersand >= 0) && (lastAmpersand + 1 < lastColon)) {554    loc = frameStr.substring(lastAmpersand + 1);555  }556  return new webdriver.stacktrace.Frame('', functionName, '', loc);557};558559560/**561 * Get an error's stack trace with the error string trimmed.562 * V8 prepends the string representation of an error to its stack trace.563 * This function trims the string so that the stack trace can be parsed564 * consistently with the other JS engines.565 * @param {(Error|goog.testing.JsUnitException)} error The error.566 * @return {string} The stack trace string.567 * @private568 */569webdriver.stacktrace.getStack_ = function(error) {570  if (!error) {571    return '';572  }573  var stack = error.stack || error.stackTrace || '';574  var errorStr = error + '\n';575  if (goog.string.startsWith(stack, errorStr)) {576    stack = stack.substring(errorStr.length);577  }578  return stack;579};580581582/**583 * Formats an error's stack trace.584 * @param {!(Error|goog.testing.JsUnitException)} error The error to format.585 * @return {!(Error|goog.testing.JsUnitException)} The formatted error.586 */587webdriver.stacktrace.format = function(error) {588  var stack = webdriver.stacktrace.getStack_(error);589  var frames = webdriver.stacktrace.parse_(stack);590591  // Older versions of IE simply return [object Error] for toString(), so592  // only use that as a last resort.593  var errorStr = '';594  if (error.message) {595    errorStr = (error.name ? error.name + ': ' : '') + error.message;596  } else {597    errorStr = error.toString();598  }599600  // Ensure the error is in the V8 style with the error's string representation601  // prepended to the stack.602  error.stack = errorStr + '\n' + frames.join('\n');603  return error;604};605606607/**608 * Parses an Error object's stack trace.609 * @param {string} stack The stack trace.610 * @return {!Array.<!webdriver.stacktrace.Frame>} Stack frames. The611 *     unrecognized frames will be nulled out.612 * @private613 */614webdriver.stacktrace.parse_ = function(stack) {615  if (!stack) {616    return [];617  }618619  var lines = stack.620      replace(/\s*$/, '').621      split('\n');622  var frames = [];623  for (var i = 0; i < lines.length; i++) {624    var frame = webdriver.stacktrace.parseStackFrame_(lines[i]);625    // The first two frames will be:626    //   webdriver.stacktrace.Snapshot()627    //   webdriver.stacktrace.get()628    // In the case of Opera, sometimes an extra frame is injected in the next629    // frame with a reported line number of zero. The next line detects that630    // case and skips that frame.631    if (!(goog.userAgent.OPERA && i == 2 && frame.getLine() == 0)) {632      frames.push(frame || webdriver.stacktrace.ANONYMOUS_FRAME_);633    }634  }635  return frames;636};637638639/**640 * Gets the native stack trace if available otherwise follows the call chain.641 * The generated trace will exclude all frames up to and including the call to642 * this function.643 * @return {!Array.<!webdriver.stacktrace.Frame>} The frames of the stack trace.644 */645webdriver.stacktrace.get = function() {646  return new webdriver.stacktrace.Snapshot(1).getStacktrace();647};