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} 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_; |