# 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.html.SafeHtml');
25 | goog.require('goog.html.SafeUrl');
26 | goog.require('goog.html.uncheckedconversions');
27 | goog.require('goog.string.Const');
28 | goog.require('goog.structs.Set');
29 | goog.require('goog.userAgent');
30 |
31 |
32 | /** @define {boolean} Whether logging should be enabled. */
33 | goog.define('goog.debug.LOGGING_ENABLED', goog.DEBUG);
34 |
35 |
36 | /**
37 | * Catches onerror events fired by windows and similar objects.
38 | * @param {function(Object)} logFunc The function to call with the error
39 | * information.
40 | * @param {boolean=} opt_cancel Whether to stop the error from reaching the
41 | * browser.
42 | * @param {Object=} opt_target Object that fires onerror events.
43 | */
44 | goog.debug.catchErrors = function(logFunc, opt_cancel, opt_target) {
45 | var target = opt_target || goog.global;
46 | var oldErrorHandler = target.onerror;
47 | var retVal = !!opt_cancel;
48 |
49 | // Chrome interprets onerror return value backwards (http://crbug.com/92062)
50 | // until it was fixed in webkit revision r94061 (Webkit 535.3). This
51 | // workaround still needs to be skipped in Safari after the webkit change
52 | // gets pushed out in Safari.
53 | // See https://bugs.webkit.org/show_bug.cgi?id=67119
54 | if (goog.userAgent.WEBKIT &&
55 | !goog.userAgent.isVersionOrHigher('535.3')) {
56 | retVal = !retVal;
57 | }
58 |
59 | /**
60 | * New onerror handler for this target. This onerror handler follows the spec
61 | * according to
62 | * http://www.whatwg.org/specs/web-apps/current-work/#runtime-script-errors
63 | * The spec was changed in August 2013 to support receiving column information
64 | * and an error object for all scripts on the same origin or cross origin
65 | * scripts with the proper headers. See
66 | * https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
67 | *
68 | * @param {string} message The error message. For cross-origin errors, this
69 | * will be scrubbed to just "Script error.". For new browsers that have
70 | * updated to follow the latest spec, errors that come from origins that
71 | * have proper cross origin headers will not be scrubbed.
72 | * @param {string} url The URL of the script that caused the error. The URL
73 | * will be scrubbed to "" for cross origin scripts unless the script has
74 | * proper cross origin headers and the browser has updated to the latest
75 | * spec.
76 | * @param {number} line The line number in the script that the error
77 | * occurred on.
78 | * @param {number=} opt_col The optional column number that the error
79 | * occurred on. Only browsers that have updated to the latest spec will
80 | * include this.
81 | * @param {Error=} opt_error The optional actual error object for this
82 | * error that should include the stack. Only browsers that have updated
83 | * to the latest spec will inlude this parameter.
84 | * @return {boolean} Whether to prevent the error from reaching the browser.
85 | */
86 | target.onerror = function(message, url, line, opt_col, opt_error) {
87 | if (oldErrorHandler) {
88 | oldErrorHandler(message, url, line, opt_col, opt_error);
89 | }
90 | logFunc({
91 | message: message,
92 | fileName: url,
93 | line: line,
94 | col: opt_col,
95 | error: opt_error
96 | });
97 | return retVal;
98 | };
99 | };
100 |
101 |
102 | /**
103 | * Creates a string representing an object and all its properties.
104 | * @param {Object|null|undefined} obj Object to expose.
105 | * @param {boolean=} opt_showFn Show the functions as well as the properties,
106 | * default is false.
107 | * @return {string} The string representation of {@code obj}.
108 | */
109 | goog.debug.expose = function(obj, opt_showFn) {
110 | if (typeof obj == 'undefined') {
111 | return 'undefined';
112 | }
113 | if (obj == null) {
114 | return 'NULL';
115 | }
116 | var str = [];
117 |
118 | for (var x in obj) {
119 | if (!opt_showFn && goog.isFunction(obj[x])) {
120 | continue;
121 | }
122 | var s = x + ' = ';
123 | /** @preserveTry */
124 | try {
125 | s += obj[x];
126 | } catch (e) {
127 | s += '*** ' + e + ' ***';
128 | }
129 | str.push(s);
130 | }
131 | return str.join('\n');
132 | };
133 |
134 |
135 | /**
136 | * Creates a string representing a given primitive or object, and for an
137 | * object, all its properties and nested objects. WARNING: If an object is
138 | * given, it and all its nested objects will be modified. To detect reference
139 | * cycles, this method identifies objects using goog.getUid() which mutates the
140 | * object.
141 | * @param {*} obj Object to expose.
142 | * @param {boolean=} opt_showFn Also show properties that are functions (by
143 | * default, functions are omitted).
144 | * @return {string} A string representation of {@code obj}.
145 | */
146 | goog.debug.deepExpose = function(obj, opt_showFn) {
147 | var str = [];
148 |
149 | var helper = function(obj, space, parentSeen) {
150 | var nestspace = space + ' ';
151 | var seen = new goog.structs.Set(parentSeen);
152 |
153 | var indentMultiline = function(str) {
154 | return str.replace(/\n/g, '\n' + space);
155 | };
156 |
157 | /** @preserveTry */
158 | try {
159 | if (!goog.isDef(obj)) {
160 | str.push('undefined');
161 | } else if (goog.isNull(obj)) {
162 | str.push('NULL');
163 | } else if (goog.isString(obj)) {
164 | str.push('"' + indentMultiline(obj) + '"');
165 | } else if (goog.isFunction(obj)) {
166 | str.push(indentMultiline(String(obj)));
167 | } else if (goog.isObject(obj)) {
168 | if (seen.contains(obj)) {
169 | str.push('*** reference loop detected ***');
170 | } else {
171 | seen.add(obj);
172 | str.push('{');
173 | for (var x in obj) {
174 | if (!opt_showFn && goog.isFunction(obj[x])) {
175 | continue;
176 | }
177 | str.push('\n');
178 | str.push(nestspace);
179 | str.push(x + ' = ');
180 | helper(obj[x], nestspace, seen);
181 | }
182 | str.push('\n' + space + '}');
183 | }
184 | } else {
185 | str.push(obj);
186 | }
187 | } catch (e) {
188 | str.push('*** ' + e + ' ***');
189 | }
190 | };
191 |
192 | helper(obj, '', new goog.structs.Set());
193 | return str.join('');
194 | };
195 |
196 |
197 | /**
198 | * Recursively outputs a nested array as a string.
199 | * @param {Array<?>} arr The array.
200 | * @return {string} String representing nested array.
201 | */
202 | goog.debug.exposeArray = function(arr) {
203 | var str = [];
204 | for (var i = 0; i < arr.length; i++) {
205 | if (goog.isArray(arr[i])) {
206 | str.push(goog.debug.exposeArray(arr[i]));
207 | } else {
208 | str.push(arr[i]);
209 | }
210 | }
211 | return '[ ' + str.join(', ') + ' ]';
212 | };
213 |
214 |
215 | /**
216 | * Exposes an exception that has been caught by a try...catch and outputs the
217 | * error as HTML with a stack trace.
218 | * @param {Object} err Error object or string.
219 | * @param {Function=} opt_fn Optional function to start stack trace from.
220 | * @return {string} Details of exception, as HTML.
221 | */
222 | goog.debug.exposeException = function(err, opt_fn) {
223 | var html = goog.debug.exposeExceptionAsHtml(err, opt_fn);
224 | return goog.html.SafeHtml.unwrap(html);
225 | };
226 |
227 |
228 | /**
229 | * Exposes an exception that has been caught by a try...catch and outputs the
230 | * error with a stack trace.
231 | * @param {Object} err Error object or string.
232 | * @param {Function=} opt_fn Optional function to start stack trace from.
233 | * @return {!goog.html.SafeHtml} Details of exception.
234 | */
235 | goog.debug.exposeExceptionAsHtml = function(err, opt_fn) {
236 | /** @preserveTry */
237 | try {
238 | var e = goog.debug.normalizeErrorObject(err);
239 | // Create the error message
240 | var viewSourceUrl = goog.debug.createViewSourceUrl_(e.fileName);
241 | var error = goog.html.SafeHtml.concat(
242 | goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(
243 | 'Message: ' + e.message + '\nUrl: '),
244 | goog.html.SafeHtml.create('a',
245 | {href: viewSourceUrl, target: '_new'}, e.fileName),
246 | goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(
247 | '\nLine: ' + e.lineNumber + '\n\nBrowser stack:\n' +
248 | e.stack + '-> ' + '[end]\n\nJS stack traversal:\n' +
249 | goog.debug.getStacktrace(opt_fn) + '-> '));
250 | return error;
251 | } catch (e2) {
252 | return goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(
253 | 'Exception trying to expose exception! You win, we lose. ' + e2);
254 | }
255 | };
256 |
257 |
258 | /**
259 | * @param {?string=} opt_fileName
260 | * @return {!goog.html.SafeUrl} SafeUrl with view-source scheme, pointing at
261 | * fileName.
262 | * @private
263 | */
264 | goog.debug.createViewSourceUrl_ = function(opt_fileName) {
265 | if (!goog.isDefAndNotNull(opt_fileName)) {
266 | opt_fileName = '';
267 | }
268 | if (!/^https?:\/\//i.test(opt_fileName)) {
269 | return goog.html.SafeUrl.fromConstant(
270 | goog.string.Const.from('sanitizedviewsrc'));
271 | }
272 | var sanitizedFileName = goog.html.SafeUrl.sanitize(opt_fileName);
273 | return goog.html.uncheckedconversions.
274 | safeUrlFromStringKnownToSatisfyTypeContract(
275 | goog.string.Const.from('view-source scheme plus HTTP/HTTPS URL'),
276 | 'view-source:' + goog.html.SafeUrl.unwrap(sanitizedFileName));
277 | };
278 |
279 |
280 | /**
281 | * Normalizes the error/exception object between browsers.
282 | * @param {Object} err Raw error object.
283 | * @return {!Object} Normalized error object.
284 | */
285 | goog.debug.normalizeErrorObject = function(err) {
286 | var href = goog.getObjectByName('window.location.href');
287 | if (goog.isString(err)) {
288 | return {
289 | 'message': err,
290 | 'name': 'Unknown error',
291 | 'lineNumber': 'Not available',
292 | 'fileName': href,
293 | 'stack': 'Not available'
294 | };
295 | }
296 |
297 | var lineNumber, fileName;
298 | var threwError = false;
299 |
300 | try {
301 | lineNumber = err.lineNumber || err.line || 'Not available';
302 | } catch (e) {
303 | // Firefox 2 sometimes throws an error when accessing 'lineNumber':
304 | // Message: Permission denied to get property UnnamedClass.lineNumber
305 | lineNumber = 'Not available';
306 | threwError = true;
307 | }
308 |
309 | try {
310 | fileName = err.fileName || err.filename || err.sourceURL ||
311 | // $googDebugFname may be set before a call to eval to set the filename
312 | // that the eval is supposed to present.
313 | goog.global['$googDebugFname'] || href;
314 | } catch (e) {
315 | // Firefox 2 may also throw an error when accessing 'filename'.
316 | fileName = 'Not available';
317 | threwError = true;
318 | }
319 |
320 | // The IE Error object contains only the name and the message.
321 | // The Safari Error object uses the line and sourceURL fields.
322 | if (threwError || !err.lineNumber || !err.fileName || !err.stack ||
323 | !err.message || !err.name) {
324 | return {
325 | 'message': err.message || 'Not available',
326 | 'name': err.name || 'UnknownError',
327 | 'lineNumber': lineNumber,
328 | 'fileName': fileName,
329 | 'stack': err.stack || 'Not available'
330 | };
331 | }
332 |
333 | // Standards error object
334 | return err;
335 | };
336 |
337 |
338 | /**
339 | * Converts an object to an Error if it's a String,
340 | * adds a stacktrace if there isn't one,
341 | * and optionally adds an extra message.
342 | * @param {Error|string} err the original thrown object or string.
343 | * @param {string=} opt_message optional additional message to add to the
344 | * error.
345 | * @return {!Error} If err is a string, it is used to create a new Error,
346 | * which is enhanced and returned. Otherwise err itself is enhanced
347 | * and returned.
348 | */
349 | goog.debug.enhanceError = function(err, opt_message) {
350 | var error;
351 | if (typeof err == 'string') {
352 | error = Error(err);
353 | if (Error.captureStackTrace) {
354 | // Trim this function off the call stack, if we can.
355 | Error.captureStackTrace(error, goog.debug.enhanceError);
356 | }
357 | } else {
358 | error = err;
359 | }
360 |
361 | if (!error.stack) {
362 | error.stack = goog.debug.getStacktrace(goog.debug.enhanceError);
363 | }
364 | if (opt_message) {
365 | // find the first unoccupied 'messageX' property
366 | var x = 0;
367 | while (error['message' + x]) {
368 | ++x;
369 | }
370 | error['message' + x] = String(opt_message);
371 | }
372 | return error;
373 | };
374 |
375 |
376 | /**
377 | * Gets the current stack trace. Simple and iterative - doesn't worry about
378 | * catching circular references or getting the args.
379 | * @param {number=} opt_depth Optional maximum depth to trace back to.
380 | * @return {string} A string with the function names of all functions in the
381 | * stack, separated by \n.
382 | * @suppress {es5Strict}
383 | */
384 | goog.debug.getStacktraceSimple = function(opt_depth) {
385 | if (goog.STRICT_MODE_COMPATIBLE) {
386 | var stack = goog.debug.getNativeStackTrace_(goog.debug.getStacktraceSimple);
387 | if (stack) {
388 | return stack;
389 | }
390 | // NOTE: browsers that have strict mode support also have native "stack"
391 | // properties. Fall-through for legacy browser support.
392 | }
393 |
394 | var sb = [];
395 | var fn = arguments.callee.caller;
396 | var depth = 0;
397 |
398 | while (fn && (!opt_depth || depth < opt_depth)) {
399 | sb.push(goog.debug.getFunctionName(fn));
400 | sb.push('()\n');
401 | /** @preserveTry */
402 | try {
403 | fn = fn.caller;
404 | } catch (e) {
405 | sb.push('[exception trying to get caller]\n');
406 | break;
407 | }
408 | depth++;
409 | if (depth >= goog.debug.MAX_STACK_DEPTH) {
410 | sb.push('[...long stack...]');
411 | break;
412 | }
413 | }
414 | if (opt_depth && depth >= opt_depth) {
415 | sb.push('[...reached max depth limit...]');
416 | } else {
417 | sb.push('[end]');
418 | }
419 |
420 | return sb.join('');
421 | };
422 |
423 |
424 | /**
425 | * Max length of stack to try and output
426 | * @type {number}
427 | */
428 | goog.debug.MAX_STACK_DEPTH = 50;
429 |
430 |
431 | /**
432 | * @param {Function} fn The function to start getting the trace from.
433 | * @return {?string}
434 | * @private
435 | */
436 | goog.debug.getNativeStackTrace_ = function(fn) {
437 | var tempErr = new Error();
438 | if (Error.captureStackTrace) {
439 | Error.captureStackTrace(tempErr, fn);
440 | return String(tempErr.stack);
441 | } else {
442 | // IE10, only adds stack traces when an exception is thrown.
443 | try {
444 | throw tempErr;
445 | } catch (e) {
446 | tempErr = e;
447 | }
448 | var stack = tempErr.stack;
449 | if (stack) {
450 | return String(stack);
451 | }
452 | }
453 | return null;
454 | };
455 |
456 |
457 | /**
458 | * Gets the current stack trace, either starting from the caller or starting
459 | * from a specified function that's currently on the call stack.
460 | * @param {Function=} opt_fn Optional function to start getting the trace from.
461 | * If not provided, defaults to the function that called this.
462 | * @return {string} Stack trace.
463 | * @suppress {es5Strict}
464 | */
465 | goog.debug.getStacktrace = function(opt_fn) {
466 | var stack;
467 | if (goog.STRICT_MODE_COMPATIBLE) {
468 | // Try to get the stack trace from the environment if it is available.
469 | var contextFn = opt_fn || goog.debug.getStacktrace;
470 | stack = goog.debug.getNativeStackTrace_(contextFn);
471 | }
472 | if (!stack) {
473 | // NOTE: browsers that have strict mode support also have native "stack"
474 | // properties. This function will throw in strict mode.
475 | stack = goog.debug.getStacktraceHelper_(
476 | opt_fn || arguments.callee.caller, []);
477 | }
478 | return stack;
479 | };
480 |
481 |
482 | /**
483 | * Private helper for getStacktrace().
484 | * @param {Function} fn Function to start getting the trace from.
485 | * @param {Array<!Function>} visited List of functions visited so far.
486 | * @return {string} Stack trace starting from function fn.
487 | * @suppress {es5Strict}
488 | * @private
489 | */
490 | goog.debug.getStacktraceHelper_ = function(fn, visited) {
491 | var sb = [];
492 |
493 | // Circular reference, certain functions like bind seem to cause a recursive
494 | // loop so we need to catch circular references
495 | if (goog.array.contains(visited, fn)) {
496 | sb.push('[...circular reference...]');
497 |
498 | // Traverse the call stack until function not found or max depth is reached
499 | } else if (fn && visited.length < goog.debug.MAX_STACK_DEPTH) {
500 | sb.push(goog.debug.getFunctionName(fn) + '(');
501 | var args = fn.arguments;
502 | // Args may be null for some special functions such as host objects or eval.
503 | for (var i = 0; args && i < args.length; i++) {
504 | if (i > 0) {
505 | sb.push(', ');
506 | }
507 | var argDesc;
508 | var arg = args[i];
509 | switch (typeof arg) {
510 | case 'object':
511 | argDesc = arg ? 'object' : 'null';
512 | break;
513 |
514 | case 'string':
515 | argDesc = arg;
516 | break;
517 |
518 | case 'number':
519 | argDesc = String(arg);
520 | break;
521 |
522 | case 'boolean':
523 | argDesc = arg ? 'true' : 'false';
524 | break;
525 |
526 | case 'function':
527 | argDesc = goog.debug.getFunctionName(arg);
528 | argDesc = argDesc ? argDesc : '[fn]';
529 | break;
530 |
531 | case 'undefined':
532 | default:
533 | argDesc = typeof arg;
534 | break;
535 | }
536 |
537 | if (argDesc.length > 40) {
538 | argDesc = argDesc.substr(0, 40) + '...';
539 | }
540 | sb.push(argDesc);
541 | }
542 | visited.push(fn);
543 | sb.push(')\n');
544 | /** @preserveTry */
545 | try {
546 | sb.push(goog.debug.getStacktraceHelper_(fn.caller, visited));
547 | } catch (e) {
548 | sb.push('[exception trying to get caller]\n');
549 | }
550 |
551 | } else if (fn) {
552 | sb.push('[...long stack...]');
553 | } else {
554 | sb.push('[end]');
555 | }
556 | return sb.join('');
557 | };
558 |
559 |
560 | /**
561 | * Set a custom function name resolver.
562 | * @param {function(Function): string} resolver Resolves functions to their
563 | * names.
564 | */
565 | goog.debug.setFunctionResolver = function(resolver) {
566 | goog.debug.fnNameResolver_ = resolver;
567 | };
568 |
569 |
570 | /**
571 | * Gets a function name
572 | * @param {Function} fn Function to get name of.
573 | * @return {string} Function's name.
574 | */
575 | goog.debug.getFunctionName = function(fn) {
576 | if (goog.debug.fnNameCache_[fn]) {
577 | return goog.debug.fnNameCache_[fn];
578 | }
579 | if (goog.debug.fnNameResolver_) {
580 | var name = goog.debug.fnNameResolver_(fn);
581 | if (name) {
582 | goog.debug.fnNameCache_[fn] = name;
583 | return name;
584 | }
585 | }
586 |
587 | // Heuristically determine function name based on code.
588 | var functionSource = String(fn);
589 | if (!goog.debug.fnNameCache_[functionSource]) {
590 | var matches = /function ([^\(]+)/.exec(functionSource);
591 | if (matches) {
592 | var method = matches[1];
593 | goog.debug.fnNameCache_[functionSource] = method;
594 | } else {
595 | goog.debug.fnNameCache_[functionSource] = '[Anonymous]';
596 | }
597 | }
598 |
599 | return goog.debug.fnNameCache_[functionSource];
600 | };
601 |
602 |
603 | /**
604 | * Makes whitespace visible by replacing it with printable characters.
605 | * This is useful in finding diffrences between the expected and the actual
606 | * output strings of a testcase.
607 | * @param {string} string whose whitespace needs to be made visible.
608 | * @return {string} string whose whitespace is made visible.
609 | */
610 | goog.debug.makeWhitespaceVisible = function(string) {
611 | return string.replace(/ /g, '[_]')
612 | .replace(/\f/g, '[f]')
613 | .replace(/\n/g, '[n]\n')
614 | .replace(/\r/g, '[r]')
615 | .replace(/\t/g, '[t]');
616 | };
617 |
618 |
619 | /**
620 | * Returns the type of a value. If a constructor is passed, and a suitable
621 | * string cannot be found, 'unknown type name' will be returned.
622 | *
623 | * <p>Forked rather than moved from {@link goog.asserts.getType_}
624 | * to avoid adding a dependency to goog.asserts.
625 | * @param {*} value A constructor, object, or primitive.
626 | * @return {string} The best display name for the value, or 'unknown type name'.
627 | */
628 | goog.debug.runtimeType = function(value) {
629 | if (value instanceof Function) {
630 | return value.displayName || value.name || 'unknown type name';
631 | } else if (value instanceof Object) {
632 | return value.constructor.displayName || value.constructor.name ||
633 | Object.prototype.toString.call(value);
634 | } else {
635 | return value === null ? 'null' : typeof value;
636 | }
637 | };
638 |
639 |
640 | /**
641 | * Hash map for storing function names that have already been looked up.
642 | * @type {Object}
643 | * @private
644 | */
645 | goog.debug.fnNameCache_ = {};
646 |
647 |
648 | /**
649 | * Resolves functions to their names. Resolved function names will be cached.
650 | * @type {function(Function):string}
651 | * @private
652 | */
653 | goog.debug.fnNameResolver_;