Commit | Line | Data |
---|---|---|
00639fab DV |
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 | }; |