// Copyright 2006 The Closure Library Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS-IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview Logging and debugging utilities. * * @see ../demos/debug.html */ goog.provide('goog.debug'); goog.require('goog.array'); goog.require('goog.userAgent'); /** @define {boolean} Whether logging should be enabled. */ goog.define('goog.debug.LOGGING_ENABLED', goog.DEBUG); /** @define {boolean} Whether to force "sloppy" stack building. */ goog.define('goog.debug.FORCE_SLOPPY_STACKS', false); /** * Catches onerror events fired by windows and similar objects. * @param {function(Object)} logFunc The function to call with the error * information. * @param {boolean=} opt_cancel Whether to stop the error from reaching the * browser. * @param {Object=} opt_target Object that fires onerror events. */ goog.debug.catchErrors = function(logFunc, opt_cancel, opt_target) { var target = opt_target || goog.global; var oldErrorHandler = target.onerror; var retVal = !!opt_cancel; // Chrome interprets onerror return value backwards (http://crbug.com/92062) // until it was fixed in webkit revision r94061 (Webkit 535.3). This // workaround still needs to be skipped in Safari after the webkit change // gets pushed out in Safari. // See https://bugs.webkit.org/show_bug.cgi?id=67119 if (goog.userAgent.WEBKIT && !goog.userAgent.isVersionOrHigher('535.3')) { retVal = !retVal; } /** * New onerror handler for this target. This onerror handler follows the spec * according to * http://www.whatwg.org/specs/web-apps/current-work/#runtime-script-errors * The spec was changed in August 2013 to support receiving column information * and an error object for all scripts on the same origin or cross origin * scripts with the proper headers. See * https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror * * @param {string} message The error message. For cross-origin errors, this * will be scrubbed to just "Script error.". For new browsers that have * updated to follow the latest spec, errors that come from origins that * have proper cross origin headers will not be scrubbed. * @param {string} url The URL of the script that caused the error. The URL * will be scrubbed to "" for cross origin scripts unless the script has * proper cross origin headers and the browser has updated to the latest * spec. * @param {number} line The line number in the script that the error * occurred on. * @param {number=} opt_col The optional column number that the error * occurred on. Only browsers that have updated to the latest spec will * include this. * @param {Error=} opt_error The optional actual error object for this * error that should include the stack. Only browsers that have updated * to the latest spec will inlude this parameter. * @return {boolean} Whether to prevent the error from reaching the browser. */ target.onerror = function(message, url, line, opt_col, opt_error) { if (oldErrorHandler) { oldErrorHandler(message, url, line, opt_col, opt_error); } logFunc({ message: message, fileName: url, line: line, col: opt_col, error: opt_error }); return retVal; }; }; /** * Creates a string representing an object and all its properties. * @param {Object|null|undefined} obj Object to expose. * @param {boolean=} opt_showFn Show the functions as well as the properties, * default is false. * @return {string} The string representation of {@code obj}. */ goog.debug.expose = function(obj, opt_showFn) { if (typeof obj == 'undefined') { return 'undefined'; } if (obj == null) { return 'NULL'; } var str = []; for (var x in obj) { if (!opt_showFn && goog.isFunction(obj[x])) { continue; } var s = x + ' = '; try { s += obj[x]; } catch (e) { s += '*** ' + e + ' ***'; } str.push(s); } return str.join('\n'); }; /** * Creates a string representing a given primitive or object, and for an * object, all its properties and nested objects. NOTE: The output will include * Uids on all objects that were exposed. Any added Uids will be removed before * returning. * @param {*} obj Object to expose. * @param {boolean=} opt_showFn Also show properties that are functions (by * default, functions are omitted). * @return {string} A string representation of {@code obj}. */ goog.debug.deepExpose = function(obj, opt_showFn) { var str = []; // Track any objects where deepExpose added a Uid, so they can be cleaned up // before return. We do this globally, rather than only on ancestors so that // if the same object appears in the output, you can see it. var uidsToCleanup = []; var ancestorUids = {}; var helper = function(obj, space) { var nestspace = space + ' '; var indentMultiline = function(str) { return str.replace(/\n/g, '\n' + space); }; try { if (!goog.isDef(obj)) { str.push('undefined'); } else if (goog.isNull(obj)) { str.push('NULL'); } else if (goog.isString(obj)) { str.push('"' + indentMultiline(obj) + '"'); } else if (goog.isFunction(obj)) { str.push(indentMultiline(String(obj))); } else if (goog.isObject(obj)) { // Add a Uid if needed. The struct calls implicitly adds them. if (!goog.hasUid(obj)) { uidsToCleanup.push(obj); } var uid = goog.getUid(obj); if (ancestorUids[uid]) { str.push('*** reference loop detected (id=' + uid + ') ***'); } else { ancestorUids[uid] = true; str.push('{'); for (var x in obj) { if (!opt_showFn && goog.isFunction(obj[x])) { continue; } str.push('\n'); str.push(nestspace); str.push(x + ' = '); helper(obj[x], nestspace); } str.push('\n' + space + '}'); delete ancestorUids[uid]; } } else { str.push(obj); } } catch (e) { str.push('*** ' + e + ' ***'); } }; helper(obj, ''); // Cleanup any Uids that were added by the deepExpose. for (var i = 0; i < uidsToCleanup.length; i++) { goog.removeUid(uidsToCleanup[i]); } return str.join(''); }; /** * Recursively outputs a nested array as a string. * @param {Array} arr The array. * @return {string} String representing nested array. */ goog.debug.exposeArray = function(arr) { var str = []; for (var i = 0; i < arr.length; i++) { if (goog.isArray(arr[i])) { str.push(goog.debug.exposeArray(arr[i])); } else { str.push(arr[i]); } } return '[ ' + str.join(', ') + ' ]'; }; /** * Normalizes the error/exception object between browsers. * @param {*} err Raw error object. * @return {!{ * message: (?|undefined), * name: (?|undefined), * lineNumber: (?|undefined), * fileName: (?|undefined), * stack: (?|undefined) * }} Normalized error object. */ goog.debug.normalizeErrorObject = function(err) { var href = goog.getObjectByName('window.location.href'); if (goog.isString(err)) { return { 'message': err, 'name': 'Unknown error', 'lineNumber': 'Not available', 'fileName': href, 'stack': 'Not available' }; } var lineNumber, fileName; var threwError = false; try { lineNumber = err.lineNumber || err.line || 'Not available'; } catch (e) { // Firefox 2 sometimes throws an error when accessing 'lineNumber': // Message: Permission denied to get property UnnamedClass.lineNumber lineNumber = 'Not available'; threwError = true; } try { fileName = err.fileName || err.filename || err.sourceURL || // $googDebugFname may be set before a call to eval to set the filename // that the eval is supposed to present. goog.global['$googDebugFname'] || href; } catch (e) { // Firefox 2 may also throw an error when accessing 'filename'. fileName = 'Not available'; threwError = true; } // The IE Error object contains only the name and the message. // The Safari Error object uses the line and sourceURL fields. if (threwError || !err.lineNumber || !err.fileName || !err.stack || !err.message || !err.name) { return { 'message': err.message || 'Not available', 'name': err.name || 'UnknownError', 'lineNumber': lineNumber, 'fileName': fileName, 'stack': err.stack || 'Not available' }; } // Standards error object // Typed !Object. Should be a subtype of the return type, but it's not. return /** @type {?} */ (err); }; /** * Converts an object to an Error using the object's toString if it's not * already an Error, adds a stacktrace if there isn't one, and optionally adds * an extra message. * @param {*} err The original thrown error, object, or string. * @param {string=} opt_message optional additional message to add to the * error. * @return {!Error} If err is an Error, it is enhanced and returned. Otherwise, * it is converted to an Error which is enhanced and returned. */ goog.debug.enhanceError = function(err, opt_message) { var error; if (!(err instanceof Error)) { error = Error(err); if (Error.captureStackTrace) { // Trim this function off the call stack, if we can. Error.captureStackTrace(error, goog.debug.enhanceError); } } else { error = err; } if (!error.stack) { error.stack = goog.debug.getStacktrace(goog.debug.enhanceError); } if (opt_message) { // find the first unoccupied 'messageX' property var x = 0; while (error['message' + x]) { ++x; } error['message' + x] = String(opt_message); } return error; }; /** * Gets the current stack trace. Simple and iterative - doesn't worry about * catching circular references or getting the args. * @param {number=} opt_depth Optional maximum depth to trace back to. * @return {string} A string with the function names of all functions in the * stack, separated by \n. * @suppress {es5Strict} */ goog.debug.getStacktraceSimple = function(opt_depth) { if (!goog.debug.FORCE_SLOPPY_STACKS) { var stack = goog.debug.getNativeStackTrace_(goog.debug.getStacktraceSimple); if (stack) { return stack; } // NOTE: browsers that have strict mode support also have native "stack" // properties. Fall-through for legacy browser support. } var sb = []; var fn = arguments.callee.caller; var depth = 0; while (fn && (!opt_depth || depth < opt_depth)) { sb.push(goog.debug.getFunctionName(fn)); sb.push('()\n'); try { fn = fn.caller; } catch (e) { sb.push('[exception trying to get caller]\n'); break; } depth++; if (depth >= goog.debug.MAX_STACK_DEPTH) { sb.push('[...long stack...]'); break; } } if (opt_depth && depth >= opt_depth) { sb.push('[...reached max depth limit...]'); } else { sb.push('[end]'); } return sb.join(''); }; /** * Max length of stack to try and output * @type {number} */ goog.debug.MAX_STACK_DEPTH = 50; /** * @param {Function} fn The function to start getting the trace from. * @return {?string} * @private */ goog.debug.getNativeStackTrace_ = function(fn) { var tempErr = new Error(); if (Error.captureStackTrace) { Error.captureStackTrace(tempErr, fn); return String(tempErr.stack); } else { // IE10, only adds stack traces when an exception is thrown. try { throw tempErr; } catch (e) { tempErr = e; } var stack = tempErr.stack; if (stack) { return String(stack); } } return null; }; /** * Gets the current stack trace, either starting from the caller or starting * from a specified function that's currently on the call stack. * @param {?Function=} fn If provided, when collecting the stack trace all * frames above the topmost call to this function, including that call, * will be left out of the stack trace. * @return {string} Stack trace. * @suppress {es5Strict} */ goog.debug.getStacktrace = function(fn) { var stack; if (!goog.debug.FORCE_SLOPPY_STACKS) { // Try to get the stack trace from the environment if it is available. var contextFn = fn || goog.debug.getStacktrace; stack = goog.debug.getNativeStackTrace_(contextFn); } if (!stack) { // NOTE: browsers that have strict mode support also have native "stack" // properties. This function will throw in strict mode. stack = goog.debug.getStacktraceHelper_(fn || arguments.callee.caller, []); } return stack; }; /** * Private helper for getStacktrace(). * @param {?Function} fn If provided, when collecting the stack trace all * frames above the topmost call to this function, including that call, * will be left out of the stack trace. * @param {Array} visited List of functions visited so far. * @return {string} Stack trace starting from function fn. * @suppress {es5Strict} * @private */ goog.debug.getStacktraceHelper_ = function(fn, visited) { var sb = []; // Circular reference, certain functions like bind seem to cause a recursive // loop so we need to catch circular references if (goog.array.contains(visited, fn)) { sb.push('[...circular reference...]'); // Traverse the call stack until function not found or max depth is reached } else if (fn && visited.length < goog.debug.MAX_STACK_DEPTH) { sb.push(goog.debug.getFunctionName(fn) + '('); var args = fn.arguments; // Args may be null for some special functions such as host objects or eval. for (var i = 0; args && i < args.length; i++) { if (i > 0) { sb.push(', '); } var argDesc; var arg = args[i]; switch (typeof arg) { case 'object': argDesc = arg ? 'object' : 'null'; break; case 'string': argDesc = arg; break; case 'number': argDesc = String(arg); break; case 'boolean': argDesc = arg ? 'true' : 'false'; break; case 'function': argDesc = goog.debug.getFunctionName(arg); argDesc = argDesc ? argDesc : '[fn]'; break; case 'undefined': default: argDesc = typeof arg; break; } if (argDesc.length > 40) { argDesc = argDesc.substr(0, 40) + '...'; } sb.push(argDesc); } visited.push(fn); sb.push(')\n'); try { sb.push(goog.debug.getStacktraceHelper_(fn.caller, visited)); } catch (e) { sb.push('[exception trying to get caller]\n'); } } else if (fn) { sb.push('[...long stack...]'); } else { sb.push('[end]'); } return sb.join(''); }; /** * Set a custom function name resolver. * @param {function(Function): string} resolver Resolves functions to their * names. */ goog.debug.setFunctionResolver = function(resolver) { goog.debug.fnNameResolver_ = resolver; }; /** * Gets a function name * @param {Function} fn Function to get name of. * @return {string} Function's name. */ goog.debug.getFunctionName = function(fn) { if (goog.debug.fnNameCache_[fn]) { return goog.debug.fnNameCache_[fn]; } if (goog.debug.fnNameResolver_) { var name = goog.debug.fnNameResolver_(fn); if (name) { goog.debug.fnNameCache_[fn] = name; return name; } } // Heuristically determine function name based on code. var functionSource = String(fn); if (!goog.debug.fnNameCache_[functionSource]) { var matches = /function ([^\(]+)/.exec(functionSource); if (matches) { var method = matches[1]; goog.debug.fnNameCache_[functionSource] = method; } else { goog.debug.fnNameCache_[functionSource] = '[Anonymous]'; } } return goog.debug.fnNameCache_[functionSource]; }; /** * Makes whitespace visible by replacing it with printable characters. * This is useful in finding diffrences between the expected and the actual * output strings of a testcase. * @param {string} string whose whitespace needs to be made visible. * @return {string} string whose whitespace is made visible. */ goog.debug.makeWhitespaceVisible = function(string) { return string.replace(/ /g, '[_]') .replace(/\f/g, '[f]') .replace(/\n/g, '[n]\n') .replace(/\r/g, '[r]') .replace(/\t/g, '[t]'); }; /** * Returns the type of a value. If a constructor is passed, and a suitable * string cannot be found, 'unknown type name' will be returned. * *

Forked rather than moved from {@link goog.asserts.getType_} * to avoid adding a dependency to goog.asserts. * @param {*} value A constructor, object, or primitive. * @return {string} The best display name for the value, or 'unknown type name'. */ goog.debug.runtimeType = function(value) { if (value instanceof Function) { return value.displayName || value.name || 'unknown type name'; } else if (value instanceof Object) { return value.constructor.displayName || value.constructor.name || Object.prototype.toString.call(value); } else { return value === null ? 'null' : typeof value; } }; /** * Hash map for storing function names that have already been looked up. * @type {Object} * @private */ goog.debug.fnNameCache_ = {}; /** * Resolves functions to their names. Resolved function names will be cached. * @type {function(Function):string} * @private */ goog.debug.fnNameResolver_;