# lib/goog/debug/debug.js

    1// Copyright 2006 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 Logging and debugging utilities.17 *18 * @see ../demos/debug.html19 */2021goog.provide('goog.debug');2223goog.require('goog.array');24goog.require('goog.string');25goog.require('goog.structs.Set');26goog.require('goog.userAgent');272829/** @define {boolean} Whether logging should be enabled. */30goog.define('goog.debug.LOGGING_ENABLED', goog.DEBUG);313233/**34 * Catches onerror events fired by windows and similar objects.35 * @param {function(Object)} logFunc The function to call with the error36 *    information.37 * @param {boolean=} opt_cancel Whether to stop the error from reaching the38 *    browser.39 * @param {Object=} opt_target Object that fires onerror events.40 */41goog.debug.catchErrors = function(logFunc, opt_cancel, opt_target) {42  var target = opt_target || goog.global;43  var oldErrorHandler = target.onerror;44  var retVal = !!opt_cancel;4546  // Chrome interprets onerror return value backwards (http://crbug.com/92062)47  // until it was fixed in webkit revision r94061 (Webkit 535.3). This48  // workaround still needs to be skipped in Safari after the webkit change49  // gets pushed out in Safari.50  // See https://bugs.webkit.org/show_bug.cgi?id=6711951  if (goog.userAgent.WEBKIT &&52      !goog.userAgent.isVersionOrHigher('535.3')) {53    retVal = !retVal;54  }5556  /**57   * New onerror handler for this target. This onerror handler follows the spec58   * according to59   * http://www.whatwg.org/specs/web-apps/current-work/#runtime-script-errors60   * The spec was changed in August 2013 to support receiving column information61   * and an error object for all scripts on the same origin or cross origin62   * scripts with the proper headers. See63   * https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror64   *65   * @param {string} message The error message. For cross-origin errors, this66   *     will be scrubbed to just "Script error.". For new browsers that have67   *     updated to follow the latest spec, errors that come from origins that68   *     have proper cross origin headers will not be scrubbed.69   * @param {string} url The URL of the script that caused the error. The URL70   *     will be scrubbed to "" for cross origin scripts unless the script has71   *     proper cross origin headers and the browser has updated to the latest72   *     spec.73   * @param {number} line The line number in the script that the error74   *     occurred on.75   * @param {number=} opt_col The optional column number that the error76   *     occurred on. Only browsers that have updated to the latest spec will77   *     include this.78   * @param {Error=} opt_error The optional actual error object for this79   *     error that should include the stack. Only browsers that have updated80   *     to the latest spec will inlude this parameter.81   * @return {boolean} Whether to prevent the error from reaching the browser.82   */83  target.onerror = function(message, url, line, opt_col, opt_error) {84    if (oldErrorHandler) {85      oldErrorHandler(message, url, line, opt_col, opt_error);86    }87    logFunc({88      message: message,89      fileName: url,90      line: line,91      col: opt_col,92      error: opt_error93    });94    return retVal;95  };96};979899/**100 * Creates a string representing an object and all its properties.101 * @param {Object|null|undefined} obj Object to expose.102 * @param {boolean=} opt_showFn Show the functions as well as the properties,103 *     default is false.104 * @return {string} The string representation of {@code obj}.105 */106goog.debug.expose = function(obj, opt_showFn) {107  if (typeof obj == 'undefined') {108    return 'undefined';109  }110  if (obj == null) {111    return 'NULL';112  }113  var str = [];114115  for (var x in obj) {116    if (!opt_showFn && goog.isFunction(obj[x])) {117      continue;118    }119    var s = x + ' = ';120    /** @preserveTry */121    try {122      s += obj[x];123    } catch (e) {124      s += '*** ' + e + ' ***';125    }126    str.push(s);127  }128  return str.join('\n');129};130131132/**133 * Creates a string representing a given primitive or object, and for an134 * object, all its properties and nested objects.  WARNING: If an object is135 * given, it and all its nested objects will be modified.  To detect reference136 * cycles, this method identifies objects using goog.getUid() which mutates the137 * object.138 * @param {*} obj Object to expose.139 * @param {boolean=} opt_showFn Also show properties that are functions (by140 *     default, functions are omitted).141 * @return {string} A string representation of {@code obj}.142 */143goog.debug.deepExpose = function(obj, opt_showFn) {144  var str = [];145146  var helper = function(obj, space, parentSeen) {147    var nestspace = space + '  ';148    var seen = new goog.structs.Set(parentSeen);149150    var indentMultiline = function(str) {151      return str.replace(/\n/g, '\n' + space);152    };153154    /** @preserveTry */155    try {156      if (!goog.isDef(obj)) {157        str.push('undefined');158      } else if (goog.isNull(obj)) {159        str.push('NULL');160      } else if (goog.isString(obj)) {161        str.push('"' + indentMultiline(obj) + '"');162      } else if (goog.isFunction(obj)) {163        str.push(indentMultiline(String(obj)));164      } else if (goog.isObject(obj)) {165        if (seen.contains(obj)) {166          str.push('*** reference loop detected ***');167        } else {168          seen.add(obj);169          str.push('{');170          for (var x in obj) {171            if (!opt_showFn && goog.isFunction(obj[x])) {172              continue;173            }174            str.push('\n');175            str.push(nestspace);176            str.push(x + ' = ');177            helper(obj[x], nestspace, seen);178          }179          str.push('\n' + space + '}');180        }181      } else {182        str.push(obj);183      }184    } catch (e) {185      str.push('*** ' + e + ' ***');186    }187  };188189  helper(obj, '', new goog.structs.Set());190  return str.join('');191};192193194/**195 * Recursively outputs a nested array as a string.196 * @param {Array} arr The array.197 * @return {string} String representing nested array.198 */199goog.debug.exposeArray = function(arr) {200  var str = [];201  for (var i = 0; i < arr.length; i++) {202    if (goog.isArray(arr[i])) {203      str.push(goog.debug.exposeArray(arr[i]));204    } else {205      str.push(arr[i]);206    }207  }208  return '[ ' + str.join(', ') + ' ]';209};210211212/**213 * Exposes an exception that has been caught by a try...catch and outputs the214 * error with a stack trace.215 * @param {Object} err Error object or string.216 * @param {Function=} opt_fn Optional function to start stack trace from.217 * @return {string} Details of exception.218 */219goog.debug.exposeException = function(err, opt_fn) {220  /** @preserveTry */221  try {222    var e = goog.debug.normalizeErrorObject(err);223224    // Create the error message225    var error = 'Message: ' + goog.string.htmlEscape(e.message) +226        '\nUrl: <a href="view-source:' + e.fileName + '" target="_new">' +227        e.fileName + '</a>\nLine: ' + e.lineNumber + '\n\nBrowser stack:\n' +228        goog.string.htmlEscape(e.stack + '-> ') +229        '[end]\n\nJS stack traversal:\n' + goog.string.htmlEscape(230            goog.debug.getStacktrace(opt_fn) + '-> ');231    return error;232  } catch (e2) {233    return 'Exception trying to expose exception! You win, we lose. ' + e2;234  }235};236237238/**239 * Normalizes the error/exception object between browsers.240 * @param {Object} err Raw error object.241 * @return {!Object} Normalized error object.242 */243goog.debug.normalizeErrorObject = function(err) {244  var href = goog.getObjectByName('window.location.href');245  if (goog.isString(err)) {246    return {247      'message': err,248      'name': 'Unknown error',249      'lineNumber': 'Not available',250      'fileName': href,251      'stack': 'Not available'252    };253  }254255  var lineNumber, fileName;256  var threwError = false;257258  try {259    lineNumber = err.lineNumber || err.line || 'Not available';260  } catch (e) {261    // Firefox 2 sometimes throws an error when accessing 'lineNumber':262    // Message: Permission denied to get property UnnamedClass.lineNumber263    lineNumber = 'Not available';264    threwError = true;265  }266267  try {268    fileName = err.fileName || err.filename || err.sourceURL ||269        // $googDebugFname may be set before a call to eval to set the filename270        // that the eval is supposed to present.271        goog.global['$googDebugFname'] || href;272  } catch (e) {273    // Firefox 2 may also throw an error when accessing 'filename'.274    fileName = 'Not available';275    threwError = true;276  }277278  // The IE Error object contains only the name and the message.279  // The Safari Error object uses the line and sourceURL fields.280  if (threwError || !err.lineNumber || !err.fileName || !err.stack ||281      !err.message || !err.name) {282    return {283      'message': err.message || 'Not available',284      'name': err.name || 'UnknownError',285      'lineNumber': lineNumber,286      'fileName': fileName,287      'stack': err.stack || 'Not available'288    };289  }290291  // Standards error object292  return err;293};294295296/**297 * Converts an object to an Error if it's a String,298 * adds a stacktrace if there isn't one,299 * and optionally adds an extra message.300 * @param {Error|string} err  the original thrown object or string.301 * @param {string=} opt_message  optional additional message to add to the302 *     error.303 * @return {!Error} If err is a string, it is used to create a new Error,304 *     which is enhanced and returned.  Otherwise err itself is enhanced305 *     and returned.306 */307goog.debug.enhanceError = function(err, opt_message) {308  var error;309  if (typeof err == 'string') {310    error = Error(err);311    if (Error.captureStackTrace) {312      // Trim this function off the call stack, if we can.313      Error.captureStackTrace(error, goog.debug.enhanceError);314    }315  } else {316    error = err;317  }318319  if (!error.stack) {320    error.stack = goog.debug.getStacktrace(goog.debug.enhanceError);321  }322  if (opt_message) {323    // find the first unoccupied 'messageX' property324    var x = 0;325    while (error['message' + x]) {326      ++x;327    }328    error['message' + x] = String(opt_message);329  }330  return error;331};332333334/**335 * Gets the current stack trace. Simple and iterative - doesn't worry about336 * catching circular references or getting the args.337 * @param {number=} opt_depth Optional maximum depth to trace back to.338 * @return {string} A string with the function names of all functions in the339 *     stack, separated by \n.340 * @suppress {es5Strict}341 */342goog.debug.getStacktraceSimple = function(opt_depth) {343  if (goog.STRICT_MODE_COMPATIBLE) {344    var stack = goog.debug.getNativeStackTrace_(goog.debug.getStacktraceSimple);345    if (stack) {346      return stack;347    }348    // NOTE: browsers that have strict mode support also have native "stack"349    // properties.  Fall-through for legacy browser support.350  }351352  var sb = [];353  var fn = arguments.callee.caller;354  var depth = 0;355356  while (fn && (!opt_depth || depth < opt_depth)) {357    sb.push(goog.debug.getFunctionName(fn));358    sb.push('()\n');359    /** @preserveTry */360    try {361      fn = fn.caller;362    } catch (e) {363      sb.push('[exception trying to get caller]\n');364      break;365    }366    depth++;367    if (depth >= goog.debug.MAX_STACK_DEPTH) {368      sb.push('[...long stack...]');369      break;370    }371  }372  if (opt_depth && depth >= opt_depth) {373    sb.push('[...reached max depth limit...]');374  } else {375    sb.push('[end]');376  }377378  return sb.join('');379};380381382/**383 * Max length of stack to try and output384 * @type {number}385 */386goog.debug.MAX_STACK_DEPTH = 50;387388389/**390 * @param {Function} fn The function to start getting the trace from.391 * @return {?string}392 * @private393 */394goog.debug.getNativeStackTrace_ = function(fn) {395  var tempErr = new Error();396  if (Error.captureStackTrace) {397    Error.captureStackTrace(tempErr, fn);398    return String(tempErr.stack);399  } else {400    // IE10, only adds stack traces when an exception is thrown.401    try {402      throw tempErr;403    } catch (e) {404      tempErr = e;405    }406    var stack = tempErr.stack;407    if (stack) {408      return String(stack);409    }410  }411  return null;412};413414415/**416 * Gets the current stack trace, either starting from the caller or starting417 * from a specified function that's currently on the call stack.418 * @param {Function=} opt_fn Optional function to start getting the trace from.419 *     If not provided, defaults to the function that called this.420 * @return {string} Stack trace.421 * @suppress {es5Strict}422 */423goog.debug.getStacktrace = function(opt_fn) {424  var stack;425  if (goog.STRICT_MODE_COMPATIBLE) {426    // Try to get the stack trace from the environment if it is available.427    var contextFn = opt_fn || goog.debug.getStacktrace;428    stack = goog.debug.getNativeStackTrace_(contextFn);429  }430  if (!stack) {431    // NOTE: browsers that have strict mode support also have native "stack"432    // properties. This function will throw in strict mode.433    stack = goog.debug.getStacktraceHelper_(434        opt_fn || arguments.callee.caller, []);435  }436  return stack;437};438439440/**441 * Private helper for getStacktrace().442 * @param {Function} fn Function to start getting the trace from.443 * @param {Array} visited List of functions visited so far.444 * @return {string} Stack trace starting from function fn.445 * @suppress {es5Strict}446 * @private447 */448goog.debug.getStacktraceHelper_ = function(fn, visited) {449  var sb = [];450451  // Circular reference, certain functions like bind seem to cause a recursive452  // loop so we need to catch circular references453  if (goog.array.contains(visited, fn)) {454    sb.push('[...circular reference...]');455456  // Traverse the call stack until function not found or max depth is reached457  } else if (fn && visited.length < goog.debug.MAX_STACK_DEPTH) {458    sb.push(goog.debug.getFunctionName(fn) + '(');459    var args = fn.arguments;460    // Args may be null for some special functions such as host objects or eval.461    for (var i = 0; args && i < args.length; i++) {462      if (i > 0) {463        sb.push(', ');464      }465      var argDesc;466      var arg = args[i];467      switch (typeof arg) {468        case 'object':469          argDesc = arg ? 'object' : 'null';470          break;471472        case 'string':473          argDesc = arg;474          break;475476        case 'number':477          argDesc = String(arg);478          break;479480        case 'boolean':481          argDesc = arg ? 'true' : 'false';482          break;483484        case 'function':485          argDesc = goog.debug.getFunctionName(arg);486          argDesc = argDesc ? argDesc : '[fn]';487          break;488489        case 'undefined':490        default:491          argDesc = typeof arg;492          break;493      }494495      if (argDesc.length > 40) {496        argDesc = argDesc.substr(0, 40) + '...';497      }498      sb.push(argDesc);499    }500    visited.push(fn);501    sb.push(')\n');502    /** @preserveTry */503    try {504      sb.push(goog.debug.getStacktraceHelper_(fn.caller, visited));505    } catch (e) {506      sb.push('[exception trying to get caller]\n');507    }508509  } else if (fn) {510    sb.push('[...long stack...]');511  } else {512    sb.push('[end]');513  }514  return sb.join('');515};516517518/**519 * Set a custom function name resolver.520 * @param {function(Function): string} resolver Resolves functions to their521 *     names.522 */523goog.debug.setFunctionResolver = function(resolver) {524  goog.debug.fnNameResolver_ = resolver;525};526527528/**529 * Gets a function name530 * @param {Function} fn Function to get name of.531 * @return {string} Function's name.532 */533goog.debug.getFunctionName = function(fn) {534  if (goog.debug.fnNameCache_[fn]) {535    return goog.debug.fnNameCache_[fn];536  }537  if (goog.debug.fnNameResolver_) {538    var name = goog.debug.fnNameResolver_(fn);539    if (name) {540      goog.debug.fnNameCache_[fn] = name;541      return name;542    }543  }544545  // Heuristically determine function name based on code.546  var functionSource = String(fn);547  if (!goog.debug.fnNameCache_[functionSource]) {548    var matches = /function ([^\(]+)/.exec(functionSource);549    if (matches) {550      var method = matches[1];551      goog.debug.fnNameCache_[functionSource] = method;552    } else {553      goog.debug.fnNameCache_[functionSource] = '[Anonymous]';554    }555  }556557  return goog.debug.fnNameCache_[functionSource];558};559560561/**562 * Makes whitespace visible by replacing it with printable characters.563 * This is useful in finding diffrences between the expected and the actual564 * output strings of a testcase.565 * @param {string} string whose whitespace needs to be made visible.566 * @return {string} string whose whitespace is made visible.567 */568goog.debug.makeWhitespaceVisible = function(string) {569  return string.replace(/ /g, '[_]')570      .replace(/\f/g, '[f]')571      .replace(/\n/g, '[n]\n')572      .replace(/\r/g, '[r]')573      .replace(/\t/g, '[t]');574};575576577/**578 * Hash map for storing function names that have already been looked up.579 * @type {Object}580 * @private581 */582goog.debug.fnNameCache_ = {};583584585/**586 * Resolves functions to their names.  Resolved function names will be cached.587 * @type {function(Function):string}588 * @private589 */590goog.debug.fnNameResolver_;