# 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 |
21 | goog.provide('goog.debug');
22 |
23 | goog.require('goog.array');
24 | goog.require('goog.string');
25 | goog.require('goog.structs.Set');
26 | goog.require('goog.userAgent');
27 |
28 |
29 | /** @define {boolean} Whether logging should be enabled. */
30 | goog.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 | */
41 | goog.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 | */
106 | goog.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 | */
143 | goog.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 | */
199 | goog.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 | */
219 | goog.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 | */
243 | goog.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 | */
307 | goog.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 | */
342 | goog.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 | */
386 | goog.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 | */
394 | goog.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 | */
423 | goog.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 | */
448 | goog.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 | */
523 | goog.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 | */
533 | goog.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 | */
568 | goog.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 | */
582 | goog.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 | */
590 | goog.debug.fnNameResolver_;