Skip to content

Commit 0fa7fc3

Browse files
Custom output patterns for stack items (fixes #6)
1 parent dc858f0 commit 0fa7fc3

File tree

7 files changed

+361
-25
lines changed

7 files changed

+361
-25
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ haxelib install jstack
1414
## Usage
1515
Just add JStack to compilation with `-lib jstack` compiler flag.
1616

17+
## Clickable positions in stack traces.
18+
19+
If your IDE supports clickable file links in app output, you can specify a pattern for call stack entries:
20+
```haxe
21+
-D JSTACK_FORMAT=%symbol% at %file%:%line%
22+
//or predefined pattern for VSCode
23+
-D JSTACK_FORMAT=vscode
24+
//or predefined pattern for IntelliJ IDEA
25+
-D JSTACK_FORMAT=idea
26+
```
27+
![](http://i.imgur.com/OgRnQOI.gif)
28+
29+
## Custom entry point
30+
1731
If you don't have `-main` in your build config, then you need to specify entry point like this:
1832
```
1933
-D JSTACK_MAIN=my.SomeClass.entryPoint

extraParams.hxml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
--macro include('jstack.JStack')
22
--macro keep('jstack.JStack')
3-
--macro jstack.Tools.addInjectMetaToEntryPoint()
3+
--macro jstack.Tools.initialize()

format/js/haxe/CallStack.hx

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package haxe;
2+
3+
enum StackItem {
4+
CFunction;
5+
Module( m : String );
6+
FilePos( s : Null<StackItem>, file : String, line : Int );
7+
Method( classname : String, method : String );
8+
LocalFunction( ?v : Int );
9+
}
10+
11+
/**
12+
Get information about the call stack.
13+
**/
14+
class CallStack {
15+
static var lastException:js.Error;
16+
17+
static function getStack(e:js.Error):Array<StackItem> {
18+
if (e == null) return [];
19+
// https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
20+
var oldValue = (untyped Error).prepareStackTrace;
21+
(untyped Error).prepareStackTrace = function (error, callsites :Array<Dynamic>) {
22+
var stack = [];
23+
for (site in callsites) {
24+
if (wrapCallSite != null) site = wrapCallSite(site);
25+
var method = null;
26+
var fullName :String = site.getFunctionName();
27+
if (fullName != null) {
28+
var idx = fullName.lastIndexOf(".");
29+
if (idx >= 0) {
30+
var className = fullName.substr(0, idx);
31+
var methodName = fullName.substr(idx+1);
32+
method = Method(className, methodName);
33+
}
34+
}
35+
stack.push(FilePos(method, site.getFileName(), site.getLineNumber()));
36+
}
37+
return stack;
38+
}
39+
var a = makeStack(e.stack);
40+
(untyped Error).prepareStackTrace = oldValue;
41+
return a;
42+
}
43+
44+
// support for source-map-support module
45+
@:noCompletion
46+
public static var wrapCallSite:Dynamic->Dynamic;
47+
48+
/**
49+
Return the call stack elements, or an empty array if not available.
50+
**/
51+
public static function callStack() : Array<StackItem> {
52+
try {
53+
throw new js.Error();
54+
} catch( e : Dynamic ) {
55+
var a = getStack(e);
56+
a.shift(); // remove Stack.callStack()
57+
return a;
58+
}
59+
}
60+
61+
/**
62+
Return the exception stack : this is the stack elements between
63+
the place the last exception was thrown and the place it was
64+
caught, or an empty array if not available.
65+
**/
66+
public static function exceptionStack() : Array<StackItem> {
67+
return untyped __define_feature__("haxe.CallStack.exceptionStack", getStack(lastException));
68+
}
69+
70+
/**
71+
Returns a representation of the stack as a printable string.
72+
**/
73+
public static function toString( stack : Array<StackItem> ) {
74+
return jstack.Format.toString(stack);
75+
}
76+
77+
private static function makeStack(s #if cs : cs.system.diagnostics.StackTrace #elseif hl : hl.NativeArray<hl.Bytes> #end) {
78+
if (s == null) {
79+
return [];
80+
} else if ((untyped __js__("typeof"))(s) == "string") {
81+
// Return the raw lines in browsers that don't support prepareStackTrace
82+
var stack : Array<String> = s.split("\n");
83+
if( stack[0] == "Error" ) stack.shift();
84+
var m = [];
85+
var rie10 = ~/^ at ([A-Za-z0-9_. ]+) \(([^)]+):([0-9]+):([0-9]+)\)$/;
86+
for( line in stack ) {
87+
if( rie10.match(line) ) {
88+
var path = rie10.matched(1).split(".");
89+
var meth = path.pop();
90+
var file = rie10.matched(2);
91+
var line = Std.parseInt(rie10.matched(3));
92+
m.push(FilePos( meth == "Anonymous function" ? LocalFunction() : meth == "Global code" ? null : Method(path.join("."),meth), file, line ));
93+
} else
94+
m.push(Module(StringTools.trim(line))); // A little weird, but better than nothing
95+
}
96+
return m;
97+
} else {
98+
return cast s;
99+
}
100+
}
101+
102+
}

format/php7/haxe/CallStack.hx

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package haxe;
2+
3+
import php.*;
4+
5+
private typedef NativeTrace = NativeIndexedArray<NativeAssocArray<Dynamic>>;
6+
7+
/**
8+
Elements return by `CallStack` methods.
9+
**/
10+
enum StackItem {
11+
CFunction;
12+
Module( m : String );
13+
FilePos( s : Null<StackItem>, file : String, line : Int );
14+
Method( classname : String, method : String );
15+
LocalFunction( ?v : Int );
16+
}
17+
18+
class CallStack {
19+
/**
20+
If defined this function will be used to transform call stack entries.
21+
@param String - generated php file name.
22+
@param Int - Line number in generated file.
23+
*/
24+
static public var mapPosition : String->Int->Null<{?source:String, ?originalLine:Int}>;
25+
26+
@:ifFeature("haxe.CallStack.exceptionStack")
27+
static var lastExceptionTrace : NativeTrace;
28+
29+
/**
30+
Return the call stack elements, or an empty array if not available.
31+
**/
32+
public static function callStack() : Array<StackItem> {
33+
return makeStack(Global.debug_backtrace(Const.DEBUG_BACKTRACE_IGNORE_ARGS));
34+
}
35+
36+
/**
37+
Return the exception stack : this is the stack elements between
38+
the place the last exception was thrown and the place it was
39+
caught, or an empty array if not available.
40+
**/
41+
public static function exceptionStack() : Array<StackItem> {
42+
return makeStack(lastExceptionTrace == null ? new NativeIndexedArray() : lastExceptionTrace);
43+
}
44+
45+
/**
46+
Returns a representation of the stack as a printable string.
47+
**/
48+
public static function toString( stack : Array<StackItem> ) {
49+
return jstack.Format.toString(stack);
50+
}
51+
52+
@:ifFeature("haxe.CallStack.exceptionStack")
53+
static function saveExceptionTrace( e:Throwable ) : Void {
54+
lastExceptionTrace = e.getTrace();
55+
56+
//Reduce exception stack to the place where exception was caught
57+
var currentTrace = Global.debug_backtrace(Const.DEBUG_BACKTRACE_IGNORE_ARGS);
58+
var count = Global.count(currentTrace);
59+
60+
for (i in -(count - 1)...1) {
61+
var exceptionEntry:NativeAssocArray<Dynamic> = Global.end(lastExceptionTrace);
62+
63+
if(!Global.isset(exceptionEntry['file']) || !Global.isset(currentTrace[-i]['file'])) {
64+
Global.array_pop(lastExceptionTrace);
65+
} else if (currentTrace[-i]['file'] == exceptionEntry['file'] && currentTrace[-i]['line'] == exceptionEntry['line']) {
66+
Global.array_pop(lastExceptionTrace);
67+
} else {
68+
break;
69+
}
70+
}
71+
72+
//Remove arguments from trace to avoid blocking some objects from GC
73+
var count = Global.count(lastExceptionTrace);
74+
for (i in 0...count) {
75+
lastExceptionTrace[i]['args'] = new NativeArray();
76+
}
77+
78+
var thrownAt = new NativeAssocArray<Dynamic>();
79+
thrownAt['function'] = '';
80+
thrownAt['line'] = e.getLine();
81+
thrownAt['file'] = e.getFile();
82+
thrownAt['class'] = '';
83+
thrownAt['args'] = new NativeArray();
84+
Global.array_unshift(lastExceptionTrace, thrownAt);
85+
}
86+
87+
static function makeStack (native:NativeTrace) : Array<StackItem> {
88+
var result = [];
89+
var count = Global.count(native);
90+
91+
for (i in 0...count) {
92+
var entry = native[i];
93+
var item = null;
94+
95+
if (i + 1 < count) {
96+
var next = native[i + 1];
97+
98+
if(!Global.isset(next['function'])) next['function'] = '';
99+
if(!Global.isset(next['class'])) next['class'] = '';
100+
101+
if ((next['function']:String).indexOf('{closure}') >= 0) {
102+
item = LocalFunction();
103+
} else if ((next['class']:String).length > 0 && (next['function']:String).length > 0) {
104+
var cls = Boot.getClassName(next['class']);
105+
item = Method(cls, next['function']);
106+
}
107+
}
108+
if (Global.isset(entry['file'])) {
109+
if (mapPosition != null) {
110+
var pos = mapPosition(entry['file'], entry['line']);
111+
if (pos != null && pos.source != null && pos.originalLine != null) {
112+
entry['file'] = pos.source;
113+
entry['line'] = pos.originalLine;
114+
}
115+
}
116+
result.push(FilePos(item, entry['file'], entry['line']));
117+
} else if (item != null) {
118+
result.push(item);
119+
}
120+
}
121+
122+
return result;
123+
}
124+
125+
}

haxelib.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
"license" : "MIT",
55
"tags" : ["js", "php7", "stack", "callstack", "stacktrace"],
66
"description" : "Friendly stack traces for JS and PHP7 targets. Makes them point to haxe sources.",
7-
"version" : "2.2.1",
8-
"releasenote" : "Try harder to map stack of uncaught exceptions on nodejs.",
7+
"version" : "2.3.0",
8+
"releasenote" : "-D JSTACK_FORMAT for IDE-friendly stack traces (see Readme).",
99
"classPath" : "src",
1010
"contributors" : ["RealyUniqueName"],
1111
"dependencies" : {

src/jstack/Format.hx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package jstack;
2+
3+
import haxe.CallStack.StackItem;
4+
5+
using StringTools;
6+
7+
/**
8+
Call stack formatting utils.
9+
**/
10+
class Format {
11+
public static function toString (stack:Array<StackItem>) : String {
12+
var format = Tools.getFormat();
13+
var buf = new StringBuf();
14+
for (item in stack) {
15+
if(format == null) {
16+
buf.add('\nCalled from ');
17+
itemToString(buf, item);
18+
} else {
19+
buf.add('\n');
20+
buf.add(itemToFormat(format, item));
21+
}
22+
}
23+
return buf.toString();
24+
}
25+
26+
static function itemToString (buf:StringBuf, item:StackItem) {
27+
switch (item) {
28+
case CFunction:
29+
buf.add('a C function');
30+
case Module(m):
31+
buf.add('module ');
32+
buf.add(m);
33+
case FilePos(item, file, line):
34+
if( item != null ) {
35+
itemToString(buf, item);
36+
buf.add(' (');
37+
}
38+
buf.add(file);
39+
buf.add(' line ');
40+
buf.add(line);
41+
if (item != null) buf.add(')');
42+
case Method(cname,meth):
43+
buf.add(cname);
44+
buf.add('.');
45+
buf.add(meth);
46+
case LocalFunction(n):
47+
buf.add('local function #');
48+
buf.add(n);
49+
}
50+
}
51+
52+
static function itemToFormat (format:String, item:StackItem) : String {
53+
switch (item) {
54+
case CFunction:
55+
return 'a C function';
56+
case Module(m):
57+
return 'module $m';
58+
case FilePos(s,file,line):
59+
if(file.substr(0, 'file://'.length) == 'file://') {
60+
file = file.substr('file://'.length);
61+
}
62+
var symbol = (s == null ? '' : itemToFormat(format, s));
63+
return format.replace('%file%', file).replace('%line%', '$line').replace('%symbol%', symbol);
64+
case Method(cname,meth):
65+
return '$cname.$meth';
66+
case LocalFunction(n):
67+
return 'local function #$n';
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)