瀏覽代碼

Grab stacktrace at the point of creation an Error. Added non-standard but widely implemented Error.stack property. Closes #337.

Dmitry Panov 3 年之前
父節點
當前提交
45901a5728
共有 6 個文件被更改,包括 227 次插入21 次删除
  1. 122 2
      builtin_error.go
  2. 1 1
      func.go
  3. 35 6
      runtime.go
  4. 21 1
      runtime_test.go
  5. 8 0
      string_unicode.go
  6. 40 11
      vm.go

+ 122 - 2
builtin_error.go

@@ -1,7 +1,127 @@
 package goja
 
+import "github.com/dop251/goja/unistring"
+
+const propNameStack = "stack"
+
+type errorObject struct {
+	baseObject
+	stack          []StackFrame
+	stackPropAdded bool
+}
+
+func (e *errorObject) formatStack() valueString {
+	var b valueStringBuilder
+	if name := e.getStr("name", nil); name != nil {
+		b.WriteString(name.toString())
+		b.WriteRune('\n')
+	} else {
+		b.WriteASCII("Error\n")
+	}
+
+	for _, frame := range e.stack {
+		b.WriteASCII("\tat ")
+		frame.WriteToValueBuilder(&b)
+		b.WriteRune('\n')
+	}
+	return b.String()
+}
+
+func (e *errorObject) addStackProp() Value {
+	if !e.stackPropAdded {
+		res := e._putProp(propNameStack, e.formatStack(), true, false, true)
+		if len(e.propNames) > 1 {
+			// reorder property names to ensure 'stack' is the first one
+			copy(e.propNames[1:], e.propNames)
+			e.propNames[0] = propNameStack
+		}
+		e.stackPropAdded = true
+		return res
+	}
+	return nil
+}
+
+func (e *errorObject) getStr(p unistring.String, receiver Value) Value {
+	return e.getStrWithOwnProp(e.getOwnPropStr(p), p, receiver)
+}
+
+func (e *errorObject) getOwnPropStr(name unistring.String) Value {
+	res := e.baseObject.getOwnPropStr(name)
+	if res == nil && name == propNameStack {
+		return e.addStackProp()
+	}
+
+	return res
+}
+
+func (e *errorObject) setOwnStr(name unistring.String, val Value, throw bool) bool {
+	if name == propNameStack {
+		e.addStackProp()
+	}
+	return e.baseObject.setOwnStr(name, val, throw)
+}
+
+func (e *errorObject) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) {
+	return e._setForeignStr(name, e.getOwnPropStr(name), val, receiver, throw)
+}
+
+func (e *errorObject) deleteStr(name unistring.String, throw bool) bool {
+	if name == propNameStack {
+		e.addStackProp()
+	}
+	return e.baseObject.deleteStr(name, throw)
+}
+
+func (e *errorObject) defineOwnPropertyStr(name unistring.String, desc PropertyDescriptor, throw bool) bool {
+	if name == propNameStack {
+		e.addStackProp()
+	}
+	return e.baseObject.defineOwnPropertyStr(name, desc, throw)
+}
+
+func (e *errorObject) hasOwnPropertyStr(name unistring.String) bool {
+	if e.baseObject.hasOwnPropertyStr(name) {
+		return true
+	}
+
+	return name == propNameStack && !e.stackPropAdded
+}
+
+func (e *errorObject) stringKeys(all bool, accum []Value) []Value {
+	if all && !e.stackPropAdded {
+		accum = append(accum, asciiString(propNameStack))
+	}
+	return e.baseObject.stringKeys(all, accum)
+}
+
+func (e *errorObject) iterateStringKeys() iterNextFunc {
+	e.addStackProp()
+	return e.baseObject.iterateStringKeys()
+}
+
+func (e *errorObject) init() {
+	e.baseObject.init()
+	vm := e.val.runtime.vm
+	e.stack = vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0)
+}
+
+func (r *Runtime) newErrorObject(proto *Object, class string) *errorObject {
+	obj := &Object{runtime: r}
+	o := &errorObject{
+		baseObject: baseObject{
+			class:      class,
+			val:        obj,
+			extensible: true,
+			prototype:  proto,
+		},
+	}
+	obj.self = o
+	o.init()
+	return o
+}
+
 func (r *Runtime) builtin_Error(args []Value, proto *Object) *Object {
-	obj := r.newBaseObject(proto, classError)
+	obj := r.newErrorObject(proto, classError)
 	if len(args) > 0 && args[0] != _undefined {
 		obj._putProp("message", args[0], true, false, true)
 	}
@@ -9,7 +129,7 @@ func (r *Runtime) builtin_Error(args []Value, proto *Object) *Object {
 }
 
 func (r *Runtime) builtin_AggregateError(args []Value, proto *Object) *Object {
-	obj := r.newBaseObject(proto, classAggError)
+	obj := r.newErrorObject(proto, classAggError)
 	if len(args) > 1 && args[1] != nil && args[1] != _undefined {
 		obj._putProp("message", args[1].toString(), true, false, true)
 	}

+ 1 - 1
func.go

@@ -102,7 +102,7 @@ func (f *funcObject) addPrototype() Value {
 }
 
 func (f *funcObject) hasOwnPropertyStr(name unistring.String) bool {
-	if r := f.baseObject.hasOwnPropertyStr(name); r {
+	if f.baseObject.hasOwnPropertyStr(name) {
 		return true
 	}
 

+ 35 - 6
runtime.go

@@ -217,6 +217,40 @@ func (f *StackFrame) Position() file.Position {
 	return f.prg.src.Position(f.prg.sourceOffset(f.pc))
 }
 
+func (f *StackFrame) WriteToValueBuilder(b *valueStringBuilder) {
+	if f.prg != nil {
+		if n := f.prg.funcName; n != "" {
+			b.WriteString(stringValueFromRaw(n))
+			b.WriteASCII(" (")
+		}
+		p := f.Position()
+		if p.Filename != "" {
+			b.WriteASCII(p.Filename)
+		} else {
+			b.WriteASCII("<eval>")
+		}
+		b.WriteRune(':')
+		b.WriteASCII(strconv.Itoa(p.Line))
+		b.WriteRune(':')
+		b.WriteASCII(strconv.Itoa(p.Column))
+		b.WriteRune('(')
+		b.WriteASCII(strconv.Itoa(f.pc))
+		b.WriteRune(')')
+		if f.prg.funcName != "" {
+			b.WriteRune(')')
+		}
+	} else {
+		if f.funcName != "" {
+			b.WriteString(stringValueFromRaw(f.funcName))
+			b.WriteASCII(" (")
+		}
+		b.WriteASCII("native")
+		if f.funcName != "" {
+			b.WriteRune(')')
+		}
+	}
+}
+
 func (f *StackFrame) Write(b *bytes.Buffer) {
 	if f.prg != nil {
 		if n := f.prg.funcName; n != "" {
@@ -257,8 +291,7 @@ type Exception struct {
 }
 
 type uncatchableException struct {
-	stack *[]StackFrame
-	err   error
+	err error
 }
 
 type InterruptedError struct {
@@ -814,10 +847,6 @@ func (r *Runtime) builtin_new(construct *Object, args []Value) *Object {
 	return r.toConstructor(construct)(args, nil)
 }
 
-func (r *Runtime) throw(e Value) {
-	panic(e)
-}
-
 func (r *Runtime) builtin_thrower(call FunctionCall) Value {
 	obj := r.toObject(call.This)
 	strict := true

+ 21 - 1
runtime_test.go

@@ -831,7 +831,7 @@ func ExampleRuntime_ExportTo_funcThrow() {
 	_, err = fn("")
 
 	fmt.Println(err)
-	// Output: Error: testing at f (<eval>:3:9(4))
+	// Output: Error: testing at f (<eval>:3:9(3))
 }
 
 func ExampleRuntime_ExportTo_funcVariadic() {
@@ -2323,6 +2323,26 @@ func TestPromiseExport(t *testing.T) {
 	}
 }
 
+func TestErrorStack(t *testing.T) {
+	const SCRIPT = `
+	const err = new Error("test");
+	if (!("stack" in err)) {
+		throw new Error("in");
+	}
+	if (Reflect.ownKeys(err)[0] !== "stack") {
+		throw new Error("property order");
+	}
+	if (err.stack !== "Error\n\tat test.js:2:14(3)\n") {
+		throw new Error(stack);
+	}
+	delete err.stack;
+	if ("stack" in err) {
+		throw new Error("stack still in err after delete");
+	}
+	`
+	testScript1(SCRIPT, _undefined, t)
+}
+
 /*
 func TestArrayConcatSparse(t *testing.T) {
 function foo(a,b,c)

+ 8 - 0
string_unicode.go

@@ -206,6 +206,14 @@ func (b *valueStringBuilder) WriteString(s valueString) {
 	}
 }
 
+func (b *valueStringBuilder) WriteASCII(s string) {
+	if b.ascii() {
+		b.asciiBuilder.WriteString(s)
+	} else {
+		b.unicodeBuilder.writeASCIIString(s)
+	}
+}
+
 func (b *valueStringBuilder) WriteRune(r rune) {
 	if r < utf8.RuneSelf {
 		if b.ascii() {

+ 40 - 11
vm.go

@@ -420,12 +420,12 @@ func (vm *vm) run() {
 		v := &InterruptedError{
 			iface: vm.interruptVal,
 		}
+		v.stack = vm.captureStack(nil, 0)
 		atomic.StoreUint32(&vm.interrupted, 0)
 		vm.interruptVal = nil
 		vm.interruptLock.Unlock()
 		panic(&uncatchableException{
-			stack: &v.stack,
-			err:   v,
+			err: v,
 		})
 	}
 }
@@ -453,14 +453,15 @@ func (vm *vm) captureStack(stack []StackFrame, ctxOffset int) []StackFrame {
 		stack = append(stack, StackFrame{prg: vm.prg, pc: vm.pc, funcName: funcName})
 	}
 	for i := len(vm.callStack) - 1; i > ctxOffset-1; i-- {
-		if vm.callStack[i].pc != -1 {
+		frame := &vm.callStack[i]
+		if frame.pc != -1 {
 			var funcName unistring.String
-			if prg := vm.callStack[i].prg; prg != nil {
+			if prg := frame.prg; prg != nil {
 				funcName = prg.funcName
 			} else {
-				funcName = vm.callStack[i].funcName
+				funcName = frame.funcName
 			}
-			stack = append(stack, StackFrame{prg: vm.callStack[i].prg, pc: vm.callStack[i].pc - 1, funcName: funcName})
+			stack = append(stack, StackFrame{prg: vm.callStack[i].prg, pc: frame.pc - 1, funcName: funcName})
 		}
 	}
 	return stack
@@ -500,6 +501,13 @@ func (vm *vm) try(f func()) (ex *Exception) {
 				vm.refStack = vm.refStack[:refLen]
 			}()
 			switch x1 := x.(type) {
+			case *Object:
+				ex = &Exception{
+					val: x1,
+				}
+				if er, ok := x1.self.(*errorObject); ok {
+					ex.stack = er.stack
+				}
 			case Value:
 				ex = &Exception{
 					val: x1,
@@ -507,7 +515,6 @@ func (vm *vm) try(f func()) (ex *Exception) {
 			case *Exception:
 				ex = x1
 			case *uncatchableException:
-				*x1.stack = vm.captureStack(*x1.stack, ctxOffset)
 				panic(x1)
 			case typeError:
 				ex = &Exception{
@@ -531,7 +538,9 @@ func (vm *vm) try(f func()) (ex *Exception) {
 				*/
 				panic(x)
 			}
-			ex.stack = vm.captureStack(ex.stack, ctxOffset)
+			if ex.stack == nil {
+				ex.stack = vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0)
+			}
 		}
 	}()
 
@@ -566,9 +575,9 @@ func (vm *vm) saveCtx(ctx *context) {
 func (vm *vm) pushCtx() {
 	if len(vm.callStack) > vm.maxCallStackSize {
 		ex := &StackOverflowError{}
+		ex.stack = vm.captureStack(nil, 0)
 		panic(&uncatchableException{
-			stack: &ex.stack,
-			err:   ex,
+			err: ex,
 		})
 	}
 	vm.callStack = append(vm.callStack, context{})
@@ -3610,7 +3619,27 @@ type _throw struct{}
 var throw _throw
 
 func (_throw) exec(vm *vm) {
-	panic(vm.stack[vm.sp-1])
+	v := vm.stack[vm.sp-1]
+	if o, ok := v.(*Object); ok {
+		if e, ok := o.self.(*errorObject); ok {
+			if len(e.stack) > 0 {
+				frame0 := e.stack[0]
+				// If the Error was created immediately before throwing it (i.e. 'throw new Error(....)')
+				// avoid capturing the stack again by the reusing the stack from the Error.
+				// These stacks would be almost identical and the difference doesn't matter for debugging.
+				if frame0.prg == vm.prg && vm.pc-frame0.pc == 1 {
+					panic(&Exception{
+						val:   v,
+						stack: e.stack,
+					})
+				}
+			}
+		}
+	}
+	panic(&Exception{
+		val:   v,
+		stack: vm.captureStack(make([]StackFrame, 0, len(vm.callStack)+1), 0),
+	})
 }
 
 type _newVariadic struct{}