| 1 | // Domain Public by Eric Wendelin http://eriwen.com/ (2008) |
| 2 | // Luke Smith http://lucassmith.name/ (2008) |
| 3 | // Loic Dachary <loic@dachary.org> (2008) |
| 4 | // Johan Euphrosine <proppy@aminche.com> (2008) |
| 5 | // Oyvind Sean Kinsey http://kinsey.no/blog (2010) |
| 6 | // Victor Homyakov <victor-homyakov@users.sourceforge.net> (2010) |
| 7 | |
| 8 | /** |
| 9 | * Main function giving a function stack trace with a forced or passed in Error |
| 10 | * |
| 11 | * @cfg {Error} e The error to create a stacktrace from (optional) |
| 12 | * @cfg {Boolean} guess If we should try to resolve the names of anonymous functions |
| 13 | * @return {Array} of Strings with functions, lines, files, and arguments where possible |
| 14 | */ |
| 15 | function printStackTrace(options) { |
| 16 | options = options || {guess: true}; |
| 17 | var ex = options.e || null, guess = !!options.guess; |
| 18 | var p = new printStackTrace.implementation(), result = p.run(ex); |
| 19 | return (guess) ? p.guessAnonymousFunctions(result) : result; |
| 20 | } |
| 21 | |
| 22 | printStackTrace.implementation = function() { |
| 23 | }; |
| 24 | |
| 25 | printStackTrace.implementation.prototype = { |
| 26 | /** |
| 27 | * @param {Error} ex The error to create a stacktrace from (optional) |
| 28 | * @param {String} mode Forced mode (optional, mostly for unit tests) |
| 29 | */ |
| 30 | run: function(ex, mode) { |
| 31 | ex = ex || this.createException(); |
| 32 | // examine exception properties w/o debugger |
| 33 | //for (var prop in ex) {alert("Ex['" + prop + "']=" + ex[prop]);} |
| 34 | mode = mode || this.mode(ex); |
| 35 | if (mode === 'other') { |
| 36 | return this.other(arguments.callee); |
| 37 | } else { |
| 38 | return this[mode](ex); |
| 39 | } |
| 40 | }, |
| 41 | |
| 42 | createException: function() { |
| 43 | try { |
| 44 | this.undef(); |
| 45 | } catch (e) { |
| 46 | return e; |
| 47 | } |
| 48 | }, |
| 49 | |
| 50 | /** |
| 51 | * Mode could differ for different exception, e.g. |
| 52 | * exceptions in Chrome may or may not have arguments or stack. |
| 53 | * |
| 54 | * @return {String} mode of operation for the exception |
| 55 | */ |
| 56 | mode: function(e) { |
| 57 | if (e['arguments'] && e.stack) { |
| 58 | return 'chrome'; |
| 59 | } else if (typeof e.message === 'string' && typeof window !== 'undefined' && window.opera) { |
| 60 | // e.message.indexOf("Backtrace:") > -1 -> opera |
| 61 | // !e.stacktrace -> opera |
| 62 | if (!e.stacktrace) { |
| 63 | return 'opera9'; // use e.message |
| 64 | } |
| 65 | // 'opera#sourceloc' in e -> opera9, opera10a |
| 66 | if (e.message.indexOf('\n') > -1 && e.message.split('\n').length > e.stacktrace.split('\n').length) { |
| 67 | return 'opera9'; // use e.message |
| 68 | } |
| 69 | // e.stacktrace && !e.stack -> opera10a |
| 70 | if (!e.stack) { |
| 71 | return 'opera10a'; // use e.stacktrace |
| 72 | } |
| 73 | // e.stacktrace && e.stack -> opera10b |
| 74 | if (e.stacktrace.indexOf("called from line") < 0) { |
| 75 | return 'opera10b'; // use e.stacktrace, format differs from 'opera10a' |
| 76 | } |
| 77 | // e.stacktrace && e.stack -> opera11 |
| 78 | return 'opera11'; // use e.stacktrace, format differs from 'opera10a', 'opera10b' |
| 79 | } else if (e.stack) { |
| 80 | return 'firefox'; |
| 81 | } |
| 82 | return 'other'; |
| 83 | }, |
| 84 | |
| 85 | /** |
| 86 | * Given a context, function name, and callback function, overwrite it so that it calls |
| 87 | * printStackTrace() first with a callback and then runs the rest of the body. |
| 88 | * |
| 89 | * @param {Object} context of execution (e.g. window) |
| 90 | * @param {String} functionName to instrument |
| 91 | * @param {Function} function to call with a stack trace on invocation |
| 92 | */ |
| 93 | instrumentFunction: function(context, functionName, callback) { |
| 94 | context = context || window; |
| 95 | var original = context[functionName]; |
| 96 | context[functionName] = function instrumented() { |
| 97 | callback.call(this, printStackTrace().slice(4)); |
| 98 | return context[functionName]._instrumented.apply(this, arguments); |
| 99 | }; |
| 100 | context[functionName]._instrumented = original; |
| 101 | }, |
| 102 | |
| 103 | /** |
| 104 | * Given a context and function name of a function that has been |
| 105 | * instrumented, revert the function to it's original (non-instrumented) |
| 106 | * state. |
| 107 | * |
| 108 | * @param {Object} context of execution (e.g. window) |
| 109 | * @param {String} functionName to de-instrument |
| 110 | */ |
| 111 | deinstrumentFunction: function(context, functionName) { |
| 112 | if (context[functionName].constructor === Function && |
| 113 | context[functionName]._instrumented && |
| 114 | context[functionName]._instrumented.constructor === Function) { |
| 115 | context[functionName] = context[functionName]._instrumented; |
| 116 | } |
| 117 | }, |
| 118 | |
| 119 | /** |
| 120 | * Given an Error object, return a formatted Array based on Chrome's stack string. |
| 121 | * |
| 122 | * @param e - Error object to inspect |
| 123 | * @return Array<String> of function calls, files and line numbers |
| 124 | */ |
| 125 | chrome: function(e) { |
| 126 | var stack = (e.stack + '\n').replace(/^\S[^\(]+?[\n$]/gm, ''). |
| 127 | replace(/^\s+at\s+/gm, ''). |
| 128 | replace(/^([^\(]+?)([\n$])/gm, '{anonymous}()@$1$2'). |
| 129 | replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}()@$1').split('\n'); |
| 130 | stack.pop(); |
| 131 | return stack; |
| 132 | }, |
| 133 | |
| 134 | /** |
| 135 | * Given an Error object, return a formatted Array based on Firefox's stack string. |
| 136 | * |
| 137 | * @param e - Error object to inspect |
| 138 | * @return Array<String> of function calls, files and line numbers |
| 139 | */ |
| 140 | firefox: function(e) { |
| 141 | return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anonymous}(').split('\n'); |
| 142 | }, |
| 143 | |
| 144 | opera11: function(e) { |
| 145 | // "Error thrown at line 42, column 12 in <anonymous function>() in file://localhost/G:/js/stacktrace.js:\n" |
| 146 | // "Error thrown at line 42, column 12 in <anonymous function: createException>() in file://localhost/G:/js/stacktrace.js:\n" |
| 147 | // "called from line 7, column 4 in bar(n) in file://localhost/G:/js/test/functional/testcase1.html:\n" |
| 148 | // "called from line 15, column 3 in file://localhost/G:/js/test/functional/testcase1.html:\n" |
| 149 | var ANON = '{anonymous}', lineRE = /^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/; |
| 150 | var lines = e.stacktrace.split('\n'), result = []; |
| 151 | |
| 152 | for (var i = 0, len = lines.length; i < len; i += 2) { |
| 153 | var match = lineRE.exec(lines[i]); |
| 154 | if (match) { |
| 155 | var location = match[4] + ':' + match[1] + ':' + match[2]; |
| 156 | var fnName = match[3] || "global code"; |
| 157 | fnName = fnName.replace(/<anonymous function: (\S+)>/, "$1").replace(/<anonymous function>/, ANON); |
| 158 | result.push(fnName + '@' + location + ' -- ' + lines[i + 1].replace(/^\s+/, '')); |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | return result; |
| 163 | }, |
| 164 | |
| 165 | opera10b: function(e) { |
| 166 | // "<anonymous function: run>([arguments not available])@file://localhost/G:/js/stacktrace.js:27\n" + |
| 167 | // "printStackTrace([arguments not available])@file://localhost/G:/js/stacktrace.js:18\n" + |
| 168 | // "@file://localhost/G:/js/test/functional/testcase1.html:15" |
| 169 | var ANON = '{anonymous}', lineRE = /^(.*)@(.+):(\d+)$/; |
| 170 | var lines = e.stacktrace.split('\n'), result = []; |
| 171 | |
| 172 | for (var i = 0, len = lines.length; i < len; i++) { |
| 173 | var match = lineRE.exec(lines[i]); |
| 174 | if (match) { |
| 175 | var fnName = match[1]? (match[1] + '()') : "global code"; |
| 176 | result.push(fnName + '@' + match[2] + ':' + match[3]); |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | return result; |
| 181 | }, |
| 182 | |
| 183 | /** |
| 184 | * Given an Error object, return a formatted Array based on Opera 10's stacktrace string. |
| 185 | * |
| 186 | * @param e - Error object to inspect |
| 187 | * @return Array<String> of function calls, files and line numbers |
| 188 | */ |
| 189 | opera10a: function(e) { |
| 190 | // " Line 27 of linked script file://localhost/G:/js/stacktrace.js\n" |
| 191 | // " Line 11 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html: In function foo\n" |
| 192 | var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; |
| 193 | var lines = e.stacktrace.split('\n'), result = []; |
| 194 | |
| 195 | for (var i = 0, len = lines.length; i < len; i += 2) { |
| 196 | var match = lineRE.exec(lines[i]); |
| 197 | if (match) { |
| 198 | var fnName = match[3] || ANON; |
| 199 | result.push(fnName + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, '')); |
| 200 | } |
| 201 | } |
| 202 | |
| 203 | return result; |
| 204 | }, |
| 205 | |
| 206 | // Opera 7.x-9.2x only! |
| 207 | opera9: function(e) { |
| 208 | // " Line 43 of linked script file://localhost/G:/js/stacktrace.js\n" |
| 209 | // " Line 7 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html\n" |
| 210 | var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)/i; |
| 211 | var lines = e.message.split('\n'), result = []; |
| 212 | |
| 213 | for (var i = 2, len = lines.length; i < len; i += 2) { |
| 214 | var match = lineRE.exec(lines[i]); |
| 215 | if (match) { |
| 216 | result.push(ANON + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, '')); |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | return result; |
| 221 | }, |
| 222 | |
| 223 | // Safari, IE, and others |
| 224 | other: function(curr) { |
| 225 | var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], fn, args, maxStackSize = 10; |
| 226 | while (curr && stack.length < maxStackSize) { |
| 227 | fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON; |
| 228 | args = Array.prototype.slice.call(curr['arguments'] || []); |
| 229 | stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')'; |
| 230 | curr = curr.caller; |
| 231 | } |
| 232 | return stack; |
| 233 | }, |
| 234 | |
| 235 | /** |
| 236 | * Given arguments array as a String, subsituting type names for non-string types. |
| 237 | * |
| 238 | * @param {Arguments} object |
| 239 | * @return {Array} of Strings with stringified arguments |
| 240 | */ |
| 241 | stringifyArguments: function(args) { |
| 242 | var result = []; |
| 243 | var slice = Array.prototype.slice; |
| 244 | for (var i = 0; i < args.length; ++i) { |
| 245 | var arg = args[i]; |
| 246 | if (arg === undefined) { |
| 247 | result[i] = 'undefined'; |
| 248 | } else if (arg === null) { |
| 249 | result[i] = 'null'; |
| 250 | } else if (arg.constructor) { |
| 251 | if (arg.constructor === Array) { |
| 252 | if (arg.length < 3) { |
| 253 | result[i] = '[' + this.stringifyArguments(arg) + ']'; |
| 254 | } else { |
| 255 | result[i] = '[' + this.stringifyArguments(slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(slice.call(arg, -1)) + ']'; |
| 256 | } |
| 257 | } else if (arg.constructor === Object) { |
| 258 | result[i] = '#object'; |
| 259 | } else if (arg.constructor === Function) { |
| 260 | result[i] = '#function'; |
| 261 | } else if (arg.constructor === String) { |
| 262 | result[i] = '"' + arg + '"'; |
| 263 | } else if (arg.constructor === Number) { |
| 264 | result[i] = arg; |
| 265 | } |
| 266 | } |
| 267 | } |
| 268 | return result.join(','); |
| 269 | }, |
| 270 | |
| 271 | sourceCache: {}, |
| 272 | |
| 273 | /** |
| 274 | * @return the text from a given URL |
| 275 | */ |
| 276 | ajax: function(url) { |
| 277 | var req = this.createXMLHTTPObject(); |
| 278 | if (req) { |
| 279 | try { |
| 280 | req.open('GET', url, false); |
| 281 | req.send(null); |
| 282 | return req.responseText; |
| 283 | } catch (e) { |
| 284 | } |
| 285 | } |
| 286 | return ''; |
| 287 | }, |
| 288 | |
| 289 | /** |
| 290 | * Try XHR methods in order and store XHR factory. |
| 291 | * |
| 292 | * @return <Function> XHR function or equivalent |
| 293 | */ |
| 294 | createXMLHTTPObject: function() { |
| 295 | var xmlhttp, XMLHttpFactories = [ |
| 296 | function() { |
| 297 | return new XMLHttpRequest(); |
| 298 | }, function() { |
| 299 | return new ActiveXObject('Msxml2.XMLHTTP'); |
| 300 | }, function() { |
| 301 | return new ActiveXObject('Msxml3.XMLHTTP'); |
| 302 | }, function() { |
| 303 | return new ActiveXObject('Microsoft.XMLHTTP'); |
| 304 | } |
| 305 | ]; |
| 306 | for (var i = 0; i < XMLHttpFactories.length; i++) { |
| 307 | try { |
| 308 | xmlhttp = XMLHttpFactories[i](); |
| 309 | // Use memoization to cache the factory |
| 310 | this.createXMLHTTPObject = XMLHttpFactories[i]; |
| 311 | return xmlhttp; |
| 312 | } catch (e) { |
| 313 | } |
| 314 | } |
| 315 | }, |
| 316 | |
| 317 | /** |
| 318 | * Given a URL, check if it is in the same domain (so we can get the source |
| 319 | * via Ajax). |
| 320 | * |
| 321 | * @param url <String> source url |
| 322 | * @return False if we need a cross-domain request |
| 323 | */ |
| 324 | isSameDomain: function(url) { |
| 325 | return url.indexOf(location.hostname) !== -1; |
| 326 | }, |
| 327 | |
| 328 | /** |
| 329 | * Get source code from given URL if in the same domain. |
| 330 | * |
| 331 | * @param url <String> JS source URL |
| 332 | * @return <Array> Array of source code lines |
| 333 | */ |
| 334 | getSource: function(url) { |
| 335 | // TODO reuse source from script tags? |
| 336 | if (!(url in this.sourceCache)) { |
| 337 | this.sourceCache[url] = this.ajax(url).split('\n'); |
| 338 | } |
| 339 | return this.sourceCache[url]; |
| 340 | }, |
| 341 | |
| 342 | guessAnonymousFunctions: function(stack) { |
| 343 | for (var i = 0; i < stack.length; ++i) { |
| 344 | var reStack = /\{anonymous\}\(.*\)@(.*)/, |
| 345 | reRef = /^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/, |
| 346 | frame = stack[i], ref = reStack.exec(frame); |
| 347 | |
| 348 | if (ref) { |
| 349 | var m = reRef.exec(ref[1]), file = m[1], |
| 350 | lineno = m[2], charno = m[3] || 0; |
| 351 | if (file && this.isSameDomain(file) && lineno) { |
| 352 | var functionName = this.guessAnonymousFunction(file, lineno, charno); |
| 353 | stack[i] = frame.replace('{anonymous}', functionName); |
| 354 | } |
| 355 | } |
| 356 | } |
| 357 | return stack; |
| 358 | }, |
| 359 | |
| 360 | guessAnonymousFunction: function(url, lineNo, charNo) { |
| 361 | var ret; |
| 362 | try { |
| 363 | ret = this.findFunctionName(this.getSource(url), lineNo); |
| 364 | } catch (e) { |
| 365 | ret = 'getSource failed with url: ' + url + ', exception: ' + e.toString(); |
| 366 | } |
| 367 | return ret; |
| 368 | }, |
| 369 | |
| 370 | findFunctionName: function(source, lineNo) { |
| 371 | // FIXME findFunctionName fails for compressed source |
| 372 | // (more than one function on the same line) |
| 373 | // TODO use captured args |
| 374 | // function {name}({args}) m[1]=name m[2]=args |
| 375 | var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/; |
| 376 | // {name} = function ({args}) TODO args capture |
| 377 | // /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function(?:[^(]*)/ |
| 378 | var reFunctionExpression = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function\b/; |
| 379 | // {name} = eval() |
| 380 | var reFunctionEvaluation = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(?:eval|new Function)\b/; |
| 381 | // Walk backwards in the source lines until we find |
| 382 | // the line which matches one of the patterns above |
| 383 | var code = "", line, maxLines = Math.min(lineNo, 20), m, commentPos; |
| 384 | for (var i = 0; i < maxLines; ++i) { |
| 385 | // lineNo is 1-based, source[] is 0-based |
| 386 | line = source[lineNo - i - 1]; |
| 387 | commentPos = line.indexOf('//'); |
| 388 | if (commentPos >= 0) { |
| 389 | line = line.substr(0, commentPos); |
| 390 | } |
| 391 | // TODO check other types of comments? Commented code may lead to false positive |
| 392 | if (line) { |
| 393 | code = line + code; |
| 394 | m = reFunctionExpression.exec(code); |
| 395 | if (m && m[1]) { |
| 396 | return m[1]; |
| 397 | } |
| 398 | m = reFunctionDeclaration.exec(code); |
| 399 | if (m && m[1]) { |
| 400 | //return m[1] + "(" + (m[2] || "") + ")"; |
| 401 | return m[1]; |
| 402 | } |
| 403 | m = reFunctionEvaluation.exec(code); |
| 404 | if (m && m[1]) { |
| 405 | return m[1]; |
| 406 | } |
| 407 | } |
| 408 | } |
| 409 | return '(?)'; |
| 410 | } |
| 411 | }; |