Skip to content

Commit

Permalink
Add Context method to aid debugging
Browse files Browse the repository at this point in the history
This change introduces a Context method to otto that allows developers to get
information about the current execution context. The method returns a Context
struct that contains information such as the filename, line and column of the
current execution, the current value of this, the stacktrace and the available
symbols at the current context.
  • Loading branch information
darkliquid committed Dec 5, 2015
1 parent cf4c264 commit 45c7a8d
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 1 deletion.
69 changes: 69 additions & 0 deletions otto.go
Expand Up @@ -363,6 +363,75 @@ func (self Otto) SetDebuggerHandler(fn func(vm *Otto)) {
self.runtime.debugger = fn
}

// Context is a structure that contains information about the current execution
// context.
type Context struct {
Filename string
Line int
Column int
Callee string
Symbols map[string]Value
This Value
Stacktrace []string
}

// Context returns the current execution context of the vm
func (self Otto) Context() (ctx Context) {
// Ensure we are operating in a scope
if self.runtime.scope == nil {
self.runtime.enterGlobalScope()
defer self.runtime.leaveScope()
}

scope := self.runtime.scope
frame := scope.frame

// Get location information
ctx.Filename = "<unknown>"
ctx.Callee = frame.callee
if frame.file != nil {
ctx.Filename = frame.file.Name()
if ctx.Filename == "" {
ctx.Filename = "<anonymous>"
}
ctx.Line, ctx.Column = _position(frame.file, frame.offset)
}

// Get the current scope this Value
ctx.This = toValue_object(scope.this)

// Build stacktrace (up to 10 levels deep)
limit := 10
ctx.Symbols = make(map[string]Value)
ctx.Stacktrace = append(ctx.Stacktrace, frame.location())
for limit > 0 {
// Get variables
stash := scope.lexical
for {
for _, name := range getStashProperties(stash) {
if _, ok := ctx.Symbols[name]; !ok {
ctx.Symbols[name] = stash.getBinding(name, true)
}
}
stash = stash.outer()
if stash == nil || stash.outer() == nil {
break
}
}

scope = scope.outer
if scope == nil {
break
}
if scope.frame.offset >= 0 {
ctx.Stacktrace = append(ctx.Stacktrace, scope.frame.location())
}
limit--
}

return
}

// Call the given JavaScript with a given this and arguments.
//
// If this is nil, then some special handling takes place to determine the proper
Expand Down
129 changes: 128 additions & 1 deletion otto_test.go
Expand Up @@ -360,7 +360,7 @@ func TestTryFinally(t *testing.T) {
finally {
def = 1;
continue;
}
}
def -= 1;
}
while (abc < 2)
Expand Down Expand Up @@ -1473,6 +1473,133 @@ func TestOttoEval(t *testing.T) {
})
}

func TestOttoContext(t *testing.T) {
// These are all the builtin global scope symbols
builtins := []string{
"escape",
"URIError",
"RegExp",
"ReferenceError",
"parseFloat",
"parseInt",
"SyntaxError",
"decodeURIComponent",
"encodeURIComponent",
"Infinity",
"JSON",
"isNaN",
"unescape",
"decodeURI",
"Object",
"Function",
"RangeError",
"Error",
"get_context",
"eval",
"Number",
"Math",
"NaN",
"Date",
"Boolean",
"console",
"encodeURI",
"EvalError",
"Array",
"TypeError",
"String",
"isFinite",
"undefined",
}

tt(t, func() {
vm := New()

vm.Set("get_context", func(c FunctionCall) Value {
ctx := c.Otto.Context()
is(ctx.Callee, "f1")
is(ctx.Filename, "<anonymous>")
is(ctx.Line, 8)
is(ctx.Column, 5)
is(ctx.Stacktrace, []string{
"f1 (<anonymous>:8:5)",
"f2 (<anonymous>:15:5)",
"f3 (<anonymous>:19:5)",
"t (<anonymous>:22:4)",
})
is(len(ctx.Symbols), 9+len(builtins))
is(ctx.Symbols["a"], 1)
is(ctx.Symbols["b"], "hello")
is(ctx.Symbols["c"], true)
is(ctx.Symbols["j"], 2)
is(ctx.Symbols["f1"].IsFunction(), true)
is(ctx.Symbols["f2"].IsFunction(), true)
is(ctx.Symbols["f3"].IsFunction(), true)
is(ctx.Symbols["t"].IsFunction(), true)
callee, _ := ctx.Symbols["arguments"].Object().Get("callee")
is(callee.IsDefined(), true)

return Value{}
})

_, err := vm.Run(`(function t() {
var a = 1;
var b = 'hello';
var c = true;
function f1() {
var j = 2;
get_context();
(function() {
var d = 4;
})()
}
function f2() {
f1();
}
function f3() {
f2();
}
f3();
a = 2;
b = 'goodbye';
c = false;
}())`)

is(err, nil)
})

// this test makes sure that `Context` works on global scope by default, if
// there is not a current scope.
tt(t, func() {
vm := New()

vm.Set("get_context", func(c FunctionCall) Value {
ctx := c.Otto.Context()
is(ctx.Callee, "")
is(ctx.Filename, "<anonymous>")
is(ctx.Line, 3)
is(ctx.Column, 4)
is(ctx.Stacktrace, []string{"<anonymous>:3:4"})
is(len(ctx.Symbols), 2+len(builtins))
is(ctx.Symbols["a"], 1)
is(ctx.Symbols["b"], UndefinedValue())

return Value{}
})

_, err := vm.Run(`
var a = 1;
get_context()
var b = 2;
`)
is(err, nil)
})
}

func Test_objectLength(t *testing.T) {
tt(t, func() {
_, vm := test()
Expand Down
21 changes: 21 additions & 0 deletions stash.go
Expand Up @@ -273,3 +273,24 @@ func (in *_fnStash) clone(clone *_clone) _stash {
}
return out
}

func getStashProperties(stash _stash) (keys []string) {
switch vars := stash.(type) {
case *_dclStash:
for k := range vars.property {
keys = append(keys, k)
}
case *_fnStash:
for k := range vars.property {
keys = append(keys, k)
}
case *_objectStash:
for k := range vars.object.property {
keys = append(keys, k)
}
default:
panic("unknown stash type")
}

return
}

0 comments on commit 45c7a8d

Please sign in to comment.