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 at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// 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 and
13// limitations under the License.
14
15/**
16 * @fileoverview Logging and debugging utilities.
17 *
18 * @see ../demos/debug.html
19 */
20
21goog.provide('goog.debug');
22
23goog.require('goog.array');
24goog.require('goog.string');
25goog.require('goog.structs.Set');
26goog.require('goog.userAgent');
27
28
29/** @define {boolean} Whether logging should be enabled. */
30goog.define('goog.debug.LOGGING_ENABLED', goog.DEBUG);
31
32
33/**
34 * Catches onerror events fired by windows and similar objects.
35 * @param {function(Object)} logFunc The function to call with the error
36 * information.
37 * @param {boolean=} opt_cancel Whether to stop the error from reaching the
38 * 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;
45
46 // Chrome interprets onerror return value backwards (http://crbug.com/92062)
47 // until it was fixed in webkit revision r94061 (Webkit 535.3). This
48 // workaround still needs to be skipped in Safari after the webkit change
49 // gets pushed out in Safari.
50 // See https://bugs.webkit.org/show_bug.cgi?id=67119
51 if (goog.userAgent.WEBKIT &&
52 !goog.userAgent.isVersionOrHigher('535.3')) {
53 retVal = !retVal;
54 }
55
56 /**
57 * New onerror handler for this target. This onerror handler follows the spec
58 * according to
59 * http://www.whatwg.org/specs/web-apps/current-work/#runtime-script-errors
60 * The spec was changed in August 2013 to support receiving column information
61 * and an error object for all scripts on the same origin or cross origin
62 * scripts with the proper headers. See
63 * https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
64 *
65 * @param {string} message The error message. For cross-origin errors, this
66 * will be scrubbed to just "Script error.". For new browsers that have
67 * updated to follow the latest spec, errors that come from origins that
68 * have proper cross origin headers will not be scrubbed.
69 * @param {string} url The URL of the script that caused the error. The URL
70 * will be scrubbed to "" for cross origin scripts unless the script has
71 * proper cross origin headers and the browser has updated to the latest
72 * spec.
73 * @param {number} line The line number in the script that the error
74 * occurred on.
75 * @param {number=} opt_col The optional column number that the error
76 * occurred on. Only browsers that have updated to the latest spec will
77 * include this.
78 * @param {Error=} opt_error The optional actual error object for this
79 * error that should include the stack. Only browsers that have updated
80 * 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_error
93 });
94 return retVal;
95 };
96};
97
98
99/**
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 = [];
114
115 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};
130
131
132/**
133 * Creates a string representing a given primitive or object, and for an
134 * object, all its properties and nested objects. WARNING: If an object is
135 * given, it and all its nested objects will be modified. To detect reference
136 * cycles, this method identifies objects using goog.getUid() which mutates the
137 * object.
138 * @param {*} obj Object to expose.
139 * @param {boolean=} opt_showFn Also show properties that are functions (by
140 * default, functions are omitted).
141 * @return {string} A string representation of {@code obj}.
142 */
143goog.debug.deepExpose = function(obj, opt_showFn) {
144 var str = [];
145
146 var helper = function(obj, space, parentSeen) {
147 var nestspace = space + ' ';
148 var seen = new goog.structs.Set(parentSeen);
149
150 var indentMultiline = function(str) {
151 return str.replace(/\n/g, '\n' + space);
152 };
153
154 /** @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 };
188
189 helper(obj, '', new goog.structs.Set());
190 return str.join('');
191};
192
193
194/**
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};
210
211
212/**
213 * Exposes an exception that has been caught by a try...catch and outputs the
214 * 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);
223
224 // Create the error message
225 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};
236
237
238/**
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 }
254
255 var lineNumber, fileName;
256 var threwError = false;
257
258 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.lineNumber
263 lineNumber = 'Not available';
264 threwError = true;
265 }
266
267 try {
268 fileName = err.fileName || err.filename || err.sourceURL ||
269 // $googDebugFname may be set before a call to eval to set the filename
270 // 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 }
277
278 // 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 }
290
291 // Standards error object
292 return err;
293};
294
295
296/**
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 the
302 * 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 enhanced
305 * 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 }
318
319 if (!error.stack) {
320 error.stack = goog.debug.getStacktrace(goog.debug.enhanceError);
321 }
322 if (opt_message) {
323 // find the first unoccupied 'messageX' property
324 var x = 0;
325 while (error['message' + x]) {
326 ++x;
327 }
328 error['message' + x] = String(opt_message);
329 }
330 return error;
331};
332
333
334/**
335 * Gets the current stack trace. Simple and iterative - doesn't worry about
336 * 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 the
339 * 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 }
351
352 var sb = [];
353 var fn = arguments.callee.caller;
354 var depth = 0;
355
356 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 }
377
378 return sb.join('');
379};
380
381
382/**
383 * Max length of stack to try and output
384 * @type {number}
385 */
386goog.debug.MAX_STACK_DEPTH = 50;
387
388
389/**
390 * @param {Function} fn The function to start getting the trace from.
391 * @return {?string}
392 * @private
393 */
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};
413
414
415/**
416 * Gets the current stack trace, either starting from the caller or starting
417 * 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};
438
439
440/**
441 * Private helper for getStacktrace().
442 * @param {Function} fn Function to start getting the trace from.
443 * @param {Array<!Function>} visited List of functions visited so far.
444 * @return {string} Stack trace starting from function fn.
445 * @suppress {es5Strict}
446 * @private
447 */
448goog.debug.getStacktraceHelper_ = function(fn, visited) {
449 var sb = [];
450
451 // Circular reference, certain functions like bind seem to cause a recursive
452 // loop so we need to catch circular references
453 if (goog.array.contains(visited, fn)) {
454 sb.push('[...circular reference...]');
455
456 // Traverse the call stack until function not found or max depth is reached
457 } 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;
471
472 case 'string':
473 argDesc = arg;
474 break;
475
476 case 'number':
477 argDesc = String(arg);
478 break;
479
480 case 'boolean':
481 argDesc = arg ? 'true' : 'false';
482 break;
483
484 case 'function':
485 argDesc = goog.debug.getFunctionName(arg);
486 argDesc = argDesc ? argDesc : '[fn]';
487 break;
488
489 case 'undefined':
490 default:
491 argDesc = typeof arg;
492 break;
493 }
494
495 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 }
508
509 } else if (fn) {
510 sb.push('[...long stack...]');
511 } else {
512 sb.push('[end]');
513 }
514 return sb.join('');
515};
516
517
518/**
519 * Set a custom function name resolver.
520 * @param {function(Function): string} resolver Resolves functions to their
521 * names.
522 */
523goog.debug.setFunctionResolver = function(resolver) {
524 goog.debug.fnNameResolver_ = resolver;
525};
526
527
528/**
529 * Gets a function name
530 * @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 }
544
545 // 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 }
556
557 return goog.debug.fnNameCache_[functionSource];
558};
559
560
561/**
562 * Makes whitespace visible by replacing it with printable characters.
563 * This is useful in finding diffrences between the expected and the actual
564 * 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};
575
576
577/**
578 * Hash map for storing function names that have already been looked up.
579 * @type {Object}
580 * @private
581 */
582goog.debug.fnNameCache_ = {};
583
584
585/**
586 * Resolves functions to their names. Resolved function names will be cached.
587 * @type {function(Function):string}
588 * @private
589 */
590goog.debug.fnNameResolver_;