Skip to content

Commit

Permalink
improve error messages when converting values
Browse files Browse the repository at this point in the history
  • Loading branch information
deoxxa committed Sep 22, 2020
1 parent c382bd3 commit ef014fd
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 49 deletions.
130 changes: 82 additions & 48 deletions runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,14 +328,14 @@ var typeOfJSONRawMessage = reflect.TypeOf(json.RawMessage{})
// convertCallParameter converts request val to type t if possible.
// If the conversion fails due to overflow or type miss-match then it panics.
// If no conversion is known then the original value is returned.
func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Value {
func (self *_runtime) convertCallParameter(v Value, t reflect.Type) (reflect.Value, error) {
if t == typeOfValue {
return reflect.ValueOf(v)
return reflect.ValueOf(v), nil
}

if t == typeOfJSONRawMessage {
if d, err := json.Marshal(v.export()); err == nil {
return reflect.ValueOf(d)
return reflect.ValueOf(d), nil
}
}

Expand All @@ -344,9 +344,9 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu
if gso.value.Type().AssignableTo(t) {
// please see TestDynamicFunctionReturningInterface for why this exists
if t.Kind() == reflect.Interface && gso.value.Type().ConvertibleTo(t) {
return gso.value.Convert(t)
return gso.value.Convert(t), nil
} else {
return gso.value
return gso.value, nil
}
}
}
Expand All @@ -355,59 +355,62 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu
if gao.value.Type().AssignableTo(t) {
// please see TestDynamicFunctionReturningInterface for why this exists
if t.Kind() == reflect.Interface && gao.value.Type().ConvertibleTo(t) {
return gao.value.Convert(t)
return gao.value.Convert(t), nil
} else {
return gao.value
return gao.value, nil
}
}
}
}

if t.Kind() == reflect.Interface {
tk := t.Kind()

if tk == reflect.Interface {
e := v.export()
if e == nil {
return reflect.Zero(t)
return reflect.Zero(t), nil
}
iv := reflect.ValueOf(e)
if iv.Type().AssignableTo(t) {
return iv
return iv, nil
}
}

tk := t.Kind()

if tk == reflect.Ptr {
switch v.kind {
case valueEmpty, valueNull, valueUndefined:
return reflect.Zero(t)
return reflect.Zero(t), nil
default:
var vv reflect.Value
if err := catchPanic(func() { vv = self.convertCallParameter(v, t.Elem()) }); err == nil {
if vv.CanAddr() {
return vv.Addr()
}
vv, err := self.convertCallParameter(v, t.Elem())
if err != nil {
return reflect.Zero(t), fmt.Errorf("can't convert to %s: %s", t, err.Error())
}

pv := reflect.New(vv.Type())
pv.Elem().Set(vv)
return pv
if vv.CanAddr() {
return vv.Addr(), nil
}

pv := reflect.New(vv.Type())
pv.Elem().Set(vv)
return pv, nil
}
}

switch tk {
case reflect.Bool:
return reflect.ValueOf(v.bool())
return reflect.ValueOf(v.bool()), nil
case reflect.String:
switch v.kind {
case valueString:
return reflect.ValueOf(v.value)
return reflect.ValueOf(v.value), nil
case valueNumber:
return reflect.ValueOf(fmt.Sprintf("%v", v.value))
return reflect.ValueOf(fmt.Sprintf("%v", v.value)), nil
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
switch v.kind {
case valueNumber:
return self.convertNumeric(v, t)
return self.convertNumeric(v, t), nil
}
case reflect.Slice:
if o := v._object(); o != nil {
Expand All @@ -430,12 +433,14 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu
continue
}

ev := self.convertCallParameter(e, tt)
ev, err := self.convertCallParameter(e, tt)
if err != nil {
return reflect.Zero(t), fmt.Errorf("couldn't convert element %d of %s: %s", i, t, err.Error())
}

s.Index(int(i)).Set(ev)
}
} else if o.class == "GoArray" {

var gslice bool
switch o.value.(type) {
case *_goSliceObject:
Expand All @@ -460,29 +465,43 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu
continue
}

ev := self.convertCallParameter(e, tt)
ev, err := self.convertCallParameter(e, tt)
if err != nil {
return reflect.Zero(t), fmt.Errorf("couldn't convert element %d of %s: %s", i, t, err.Error())
}

s.Index(int(i)).Set(ev)
}
}

return s
return s, nil
}
}
case reflect.Map:
if o := v._object(); o != nil && t.Key().Kind() == reflect.String {
m := reflect.MakeMap(t)

var err error

o.enumerate(false, func(k string) bool {
m.SetMapIndex(reflect.ValueOf(k), self.convertCallParameter(o.get(k), t.Elem()))
v, verr := self.convertCallParameter(o.get(k), t.Elem())
if verr != nil {
err = fmt.Errorf("couldn't convert property %q of %s: %s", k, t, verr.Error())
return false
}
m.SetMapIndex(reflect.ValueOf(k), v)
return true
})

return m
if err != nil {
return reflect.Zero(t), err
}

return m, nil
}
case reflect.Func:
if t.NumOut() > 1 {
panic(self.panicTypeError("converting JavaScript values to Go functions with more than one return value is currently not supported"))
return reflect.Zero(t), fmt.Errorf("converting JavaScript values to Go functions with more than one return value is currently not supported")
}

if o := v._object(); o != nil && o.class == "Function" {
Expand All @@ -503,8 +522,13 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu
return nil
}

return []reflect.Value{self.convertCallParameter(rv, t.Out(0))}
})
r, err := self.convertCallParameter(rv, t.Out(0))
if err != nil {
panic(self.panicTypeError(err.Error()))
}

return []reflect.Value{r}
}), nil
}
case reflect.Struct:
if o := v._object(); o != nil && o.class == "Object" {
Expand All @@ -514,7 +538,7 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu
idx := fieldIndexByName(t, k)

if idx == nil {
panic(self.panicTypeError("can't convert object; field %q was supplied but does not exist on target %v", k, t))
return reflect.Zero(t), fmt.Errorf("can't convert property %q of %s: field does not exist", k, t)
}

ss := s
Expand All @@ -523,7 +547,7 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu
if ss.Kind() == reflect.Ptr {
if ss.IsNil() {
if !ss.CanSet() {
panic(self.panicTypeError("can't set embedded pointer to unexported struct: %v", ss.Type().Elem()))
return reflect.Zero(t), fmt.Errorf("can't convert property %q of %s: %s is unexported", k, t, ss.Type().Elem())
}

ss.Set(reflect.New(ss.Type().Elem()))
Expand All @@ -535,10 +559,15 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu
ss = ss.Field(i)
}

ss.Set(self.convertCallParameter(o.get(k), ss.Type()))
v, err := self.convertCallParameter(o.get(k), ss.Type())
if err != nil {
return reflect.Zero(t), fmt.Errorf("couldn't convert property %q of %s: %s", k, t, err.Error())
}

ss.Set(v)
}

return s.Elem()
return s.Elem(), nil
}
}

Expand All @@ -547,17 +576,18 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu
if fn := o.get("toString"); fn.IsFunction() {
sv, err := fn.Call(v)
if err != nil {
panic(err)
return reflect.Zero(t), fmt.Errorf("couldn't call toString: %s", err.Error())
}

var r reflect.Value
if err := catchPanic(func() { r = self.convertCallParameter(sv, t) }); err == nil {
return r
r, err := self.convertCallParameter(sv, t)
if err != nil {
return reflect.Zero(t), fmt.Errorf("couldn't convert toString result: %s", err.Error())
}
return r, nil
}
}

return reflect.ValueOf(v.String())
return reflect.ValueOf(v.String()), nil
}

if v.kind == valueString {
Expand All @@ -567,10 +597,10 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu
r := reflect.New(t)

if err := r.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(v.string())); err != nil {
panic(self.panicSyntaxError("can't convert to %s: %s", t.String(), err.Error()))
return reflect.Zero(t), fmt.Errorf("can't convert to %s as TextUnmarshaller: %s", t.String(), err.Error())
}

return r.Elem()
return r.Elem(), nil
}
}

Expand All @@ -590,7 +620,7 @@ func (self *_runtime) convertCallParameter(v Value, t reflect.Type) reflect.Valu
s = v.Class()
}

panic(self.panicTypeError("can't convert from %q to %q", s, t))
return reflect.Zero(t), fmt.Errorf("can't convert from %s to %s", s, t)
}

func (self *_runtime) toValue(value interface{}) Value {
Expand Down Expand Up @@ -695,15 +725,19 @@ func (self *_runtime) toValue(value interface{}) Value {
// actual set of variadic Go arguments. if that succeeds, break
// out of the loop.
if typ.IsVariadic() && len(c.ArgumentList) == nargs && i == nargs-1 {
var v reflect.Value
if err := catchPanic(func() { v = self.convertCallParameter(a, typ.In(n)) }); err == nil {
if v, err := self.convertCallParameter(a, typ.In(n)); err == nil {
in[i] = v
callSlice = true
break
}
}

in[i] = self.convertCallParameter(a, t)
v, err := self.convertCallParameter(a, t)
if err != nil {
panic(self.panicTypeError(err.Error()))
}

in[i] = v
}

var out []reflect.Value
Expand Down
6 changes: 5 additions & 1 deletion type_go_struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ func (self _goStructObject) setValue(rt *_runtime, name string, value Value) boo
}

fieldValue := self.getValue(name)
fieldValue.Set(rt.convertCallParameter(value, fieldValue.Type()))
converted, err := rt.convertCallParameter(value, fieldValue.Type())
if err != nil {
panic(rt.panicTypeError(err.Error()))
}
fieldValue.Set(converted)

return true
}
Expand Down
45 changes: 45 additions & 0 deletions type_go_struct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,48 @@ func TestGoStructEmbeddedFields(t *testing.T) {
test(`[v.a1,v.a2,v.a3,v.b1]`, "a1,a2,a3,b1")
})
}

func TestGoStructNilBoolPointerField(t *testing.T) {
type S struct {
A int `json:"a"`
B *bool `json:"b"`
C interface{} `json:"c"`
}

tt(t, func() {
test, vm := test()
vm.Set("s", S{A: 1, B: nil, C: nil})
test(`'a' in s`, true)
test(`typeof s.a`, "number")
test(`'b' in s`, true)
test(`typeof s.b`, "undefined")
test(`'c' in s`, true)
test(`typeof s.c`, "undefined")
})
}

func TestGoStructError(t *testing.T) {
type S1 struct {
A string `json:"a"`
B string `json:"b"`
}

type S2 struct {
A []S1 `json:"a"`
B S1 `json:"b"`
}

type S3 struct {
A []S2 `json:"a"`
B S2 `json:"b"`
}

tt(t, func() {
test, vm := test()
vm.Set("fn", func(s *S3) string { return "cool" })
test(
`(function() { try { fn({a:[{a:[{c:"x"}]}]}) } catch (ex) { return ex } })()`,
`TypeError: can't convert to *otto.S3: couldn't convert property "a" of otto.S3: couldn't convert element 0 of []otto.S2: couldn't convert property "a" of otto.S2: couldn't convert element 0 of []otto.S1: can't convert property "c" of otto.S1: field does not exist`,
)
})
}

0 comments on commit ef014fd

Please sign in to comment.