Skip to content

Commit

Permalink
Custom output patterns for stack items (fixes #6)
Browse files Browse the repository at this point in the history
  • Loading branch information
RealyUniqueName committed Mar 6, 2017
1 parent dc858f0 commit 0fa7fc3
Show file tree
Hide file tree
Showing 7 changed files with 361 additions and 25 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ haxelib install jstack
## Usage
Just add JStack to compilation with `-lib jstack` compiler flag.

## Clickable positions in stack traces.

If your IDE supports clickable file links in app output, you can specify a pattern for call stack entries:
```haxe
-D JSTACK_FORMAT=%symbol% at %file%:%line%
//or predefined pattern for VSCode
-D JSTACK_FORMAT=vscode
//or predefined pattern for IntelliJ IDEA
-D JSTACK_FORMAT=idea
```
![](http://i.imgur.com/OgRnQOI.gif)

## Custom entry point

If you don't have `-main` in your build config, then you need to specify entry point like this:
```
-D JSTACK_MAIN=my.SomeClass.entryPoint
Expand Down
2 changes: 1 addition & 1 deletion extraParams.hxml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
--macro include('jstack.JStack')
--macro keep('jstack.JStack')
--macro jstack.Tools.addInjectMetaToEntryPoint()
--macro jstack.Tools.initialize()
102 changes: 102 additions & 0 deletions format/js/haxe/CallStack.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package haxe;

enum StackItem {
CFunction;
Module( m : String );
FilePos( s : Null<StackItem>, file : String, line : Int );
Method( classname : String, method : String );
LocalFunction( ?v : Int );
}

/**
Get information about the call stack.
**/
class CallStack {
static var lastException:js.Error;

static function getStack(e:js.Error):Array<StackItem> {
if (e == null) return [];
// https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
var oldValue = (untyped Error).prepareStackTrace;
(untyped Error).prepareStackTrace = function (error, callsites :Array<Dynamic>) {
var stack = [];
for (site in callsites) {
if (wrapCallSite != null) site = wrapCallSite(site);
var method = null;
var fullName :String = site.getFunctionName();
if (fullName != null) {
var idx = fullName.lastIndexOf(".");
if (idx >= 0) {
var className = fullName.substr(0, idx);
var methodName = fullName.substr(idx+1);
method = Method(className, methodName);
}
}
stack.push(FilePos(method, site.getFileName(), site.getLineNumber()));
}
return stack;
}
var a = makeStack(e.stack);
(untyped Error).prepareStackTrace = oldValue;
return a;
}

// support for source-map-support module
@:noCompletion
public static var wrapCallSite:Dynamic->Dynamic;

/**
Return the call stack elements, or an empty array if not available.
**/
public static function callStack() : Array<StackItem> {
try {
throw new js.Error();
} catch( e : Dynamic ) {
var a = getStack(e);
a.shift(); // remove Stack.callStack()
return a;
}
}

/**
Return the exception stack : this is the stack elements between
the place the last exception was thrown and the place it was
caught, or an empty array if not available.
**/
public static function exceptionStack() : Array<StackItem> {
return untyped __define_feature__("haxe.CallStack.exceptionStack", getStack(lastException));
}

/**
Returns a representation of the stack as a printable string.
**/
public static function toString( stack : Array<StackItem> ) {
return jstack.Format.toString(stack);
}

private static function makeStack(s #if cs : cs.system.diagnostics.StackTrace #elseif hl : hl.NativeArray<hl.Bytes> #end) {
if (s == null) {
return [];
} else if ((untyped __js__("typeof"))(s) == "string") {
// Return the raw lines in browsers that don't support prepareStackTrace
var stack : Array<String> = s.split("\n");
if( stack[0] == "Error" ) stack.shift();
var m = [];
var rie10 = ~/^ at ([A-Za-z0-9_. ]+) \(([^)]+):([0-9]+):([0-9]+)\)$/;
for( line in stack ) {
if( rie10.match(line) ) {
var path = rie10.matched(1).split(".");
var meth = path.pop();
var file = rie10.matched(2);
var line = Std.parseInt(rie10.matched(3));
m.push(FilePos( meth == "Anonymous function" ? LocalFunction() : meth == "Global code" ? null : Method(path.join("."),meth), file, line ));
} else
m.push(Module(StringTools.trim(line))); // A little weird, but better than nothing
}
return m;
} else {
return cast s;
}
}

}
125 changes: 125 additions & 0 deletions format/php7/haxe/CallStack.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package haxe;

import php.*;

private typedef NativeTrace = NativeIndexedArray<NativeAssocArray<Dynamic>>;

/**
Elements return by `CallStack` methods.
**/
enum StackItem {
CFunction;
Module( m : String );
FilePos( s : Null<StackItem>, file : String, line : Int );
Method( classname : String, method : String );
LocalFunction( ?v : Int );
}

class CallStack {
/**
If defined this function will be used to transform call stack entries.
@param String - generated php file name.
@param Int - Line number in generated file.
*/
static public var mapPosition : String->Int->Null<{?source:String, ?originalLine:Int}>;

@:ifFeature("haxe.CallStack.exceptionStack")
static var lastExceptionTrace : NativeTrace;

/**
Return the call stack elements, or an empty array if not available.
**/
public static function callStack() : Array<StackItem> {
return makeStack(Global.debug_backtrace(Const.DEBUG_BACKTRACE_IGNORE_ARGS));
}

/**
Return the exception stack : this is the stack elements between
the place the last exception was thrown and the place it was
caught, or an empty array if not available.
**/
public static function exceptionStack() : Array<StackItem> {
return makeStack(lastExceptionTrace == null ? new NativeIndexedArray() : lastExceptionTrace);
}

/**
Returns a representation of the stack as a printable string.
**/
public static function toString( stack : Array<StackItem> ) {
return jstack.Format.toString(stack);
}

@:ifFeature("haxe.CallStack.exceptionStack")
static function saveExceptionTrace( e:Throwable ) : Void {
lastExceptionTrace = e.getTrace();

//Reduce exception stack to the place where exception was caught
var currentTrace = Global.debug_backtrace(Const.DEBUG_BACKTRACE_IGNORE_ARGS);
var count = Global.count(currentTrace);

for (i in -(count - 1)...1) {
var exceptionEntry:NativeAssocArray<Dynamic> = Global.end(lastExceptionTrace);

if(!Global.isset(exceptionEntry['file']) || !Global.isset(currentTrace[-i]['file'])) {
Global.array_pop(lastExceptionTrace);
} else if (currentTrace[-i]['file'] == exceptionEntry['file'] && currentTrace[-i]['line'] == exceptionEntry['line']) {
Global.array_pop(lastExceptionTrace);
} else {
break;
}
}

//Remove arguments from trace to avoid blocking some objects from GC
var count = Global.count(lastExceptionTrace);
for (i in 0...count) {
lastExceptionTrace[i]['args'] = new NativeArray();
}

var thrownAt = new NativeAssocArray<Dynamic>();
thrownAt['function'] = '';
thrownAt['line'] = e.getLine();
thrownAt['file'] = e.getFile();
thrownAt['class'] = '';
thrownAt['args'] = new NativeArray();
Global.array_unshift(lastExceptionTrace, thrownAt);
}

static function makeStack (native:NativeTrace) : Array<StackItem> {
var result = [];
var count = Global.count(native);

for (i in 0...count) {
var entry = native[i];
var item = null;

if (i + 1 < count) {
var next = native[i + 1];

if(!Global.isset(next['function'])) next['function'] = '';
if(!Global.isset(next['class'])) next['class'] = '';

if ((next['function']:String).indexOf('{closure}') >= 0) {
item = LocalFunction();
} else if ((next['class']:String).length > 0 && (next['function']:String).length > 0) {
var cls = Boot.getClassName(next['class']);
item = Method(cls, next['function']);
}
}
if (Global.isset(entry['file'])) {
if (mapPosition != null) {
var pos = mapPosition(entry['file'], entry['line']);
if (pos != null && pos.source != null && pos.originalLine != null) {
entry['file'] = pos.source;
entry['line'] = pos.originalLine;
}
}
result.push(FilePos(item, entry['file'], entry['line']));
} else if (item != null) {
result.push(item);
}
}

return result;
}

}
4 changes: 2 additions & 2 deletions haxelib.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"license" : "MIT",
"tags" : ["js", "php7", "stack", "callstack", "stacktrace"],
"description" : "Friendly stack traces for JS and PHP7 targets. Makes them point to haxe sources.",
"version" : "2.2.1",
"releasenote" : "Try harder to map stack of uncaught exceptions on nodejs.",
"version" : "2.3.0",
"releasenote" : "-D JSTACK_FORMAT for IDE-friendly stack traces (see Readme).",
"classPath" : "src",
"contributors" : ["RealyUniqueName"],
"dependencies" : {
Expand Down
70 changes: 70 additions & 0 deletions src/jstack/Format.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package jstack;

import haxe.CallStack.StackItem;

using StringTools;

/**
Call stack formatting utils.
**/
class Format {
public static function toString (stack:Array<StackItem>) : String {
var format = Tools.getFormat();
var buf = new StringBuf();
for (item in stack) {
if(format == null) {
buf.add('\nCalled from ');
itemToString(buf, item);
} else {
buf.add('\n');
buf.add(itemToFormat(format, item));
}
}
return buf.toString();
}

static function itemToString (buf:StringBuf, item:StackItem) {
switch (item) {
case CFunction:
buf.add('a C function');
case Module(m):
buf.add('module ');
buf.add(m);
case FilePos(item, file, line):
if( item != null ) {
itemToString(buf, item);
buf.add(' (');
}
buf.add(file);
buf.add(' line ');
buf.add(line);
if (item != null) buf.add(')');
case Method(cname,meth):
buf.add(cname);
buf.add('.');
buf.add(meth);
case LocalFunction(n):
buf.add('local function #');
buf.add(n);
}
}

static function itemToFormat (format:String, item:StackItem) : String {
switch (item) {
case CFunction:
return 'a C function';
case Module(m):
return 'module $m';
case FilePos(s,file,line):
if(file.substr(0, 'file://'.length) == 'file://') {
file = file.substr('file://'.length);
}
var symbol = (s == null ? '' : itemToFormat(format, s));
return format.replace('%file%', file).replace('%line%', '$line').replace('%symbol%', symbol);
case Method(cname,meth):
return '$cname.$meth';
case LocalFunction(n):
return 'local function #$n';
}
}
}
Loading

0 comments on commit 0fa7fc3

Please sign in to comment.