From dd567e70aec2f270687098899ad97108194b6409 Mon Sep 17 00:00:00 2001 From: Dmitry Panov Date: Mon, 24 Jan 2022 12:56:25 +0000 Subject: [PATCH] Added support for go arrays (both to ToValue and ExportTo). Nom-addressable structs and arrays are copied in ToValue() so they remain writable in ES. Subsequent Export() returns the value including any changes made. --- array.go | 12 +- array_sparse.go | 12 +- builtin_map_test.go | 17 ++ builtin_set.go | 15 +- builtin_set_test.go | 19 +++ destruct.go | 4 +- object.go | 24 ++- object_dynamic.go | 4 +- object_goarray_reflect.go | 288 +++++++++++++++++++++++++++++++++ object_goarray_reflect_test.go | 49 ++++++ object_goreflect.go | 6 + object_goreflect_test.go | 54 +++---- object_goslice_reflect.go | 273 ++----------------------------- object_goslice_reflect_test.go | 78 +++++++++ object_lazy.go | 4 +- object_test.go | 56 +++++++ runtime.go | 39 ++++- 17 files changed, 629 insertions(+), 325 deletions(-) create mode 100644 object_goarray_reflect.go create mode 100644 object_goarray_reflect_test.go diff --git a/array.go b/array.go index bec0fab0..6cff33fe 100644 --- a/array.go +++ b/array.go @@ -507,12 +507,16 @@ func (a *arrayObject) exportType() reflect.Type { return reflectTypeArray } -func (a *arrayObject) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { +func (a *arrayObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { r := a.val.runtime if iter := a.getSym(SymIterator, nil); iter == r.global.arrayValues || iter == nil { l := toIntStrict(int64(a.length)) - if dst.IsNil() || dst.Len() != l { - dst.Set(reflect.MakeSlice(typ, l, l)) + if dst.Len() != l { + if typ.Kind() == reflect.Array { + return fmt.Errorf("cannot convert an Array into an array, lengths mismatch (have %d, need %d)", l, dst.Len()) + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } } ctx.putTyped(a.val, typ, dst.Interface()) for i := 0; i < l; i++ { @@ -530,7 +534,7 @@ func (a *arrayObject) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *ob } return nil } - return a.baseObject.exportToSlice(dst, typ, ctx) + return a.baseObject.exportToArrayOrSlice(dst, typ, ctx) } func (a *arrayObject) setValuesFromSparse(items []sparseArrayItem, newMaxIdx int) { diff --git a/array_sparse.go b/array_sparse.go index 018f7403..0c0917a8 100644 --- a/array_sparse.go +++ b/array_sparse.go @@ -459,12 +459,16 @@ func (a *sparseArrayObject) exportType() reflect.Type { return reflectTypeArray } -func (a *sparseArrayObject) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { +func (a *sparseArrayObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { r := a.val.runtime if iter := a.getSym(SymIterator, nil); iter == r.global.arrayValues || iter == nil { l := toIntStrict(int64(a.length)) - if dst.IsNil() || dst.Len() != l { - dst.Set(reflect.MakeSlice(typ, l, l)) + if dst.Len() != l { + if typ.Kind() == reflect.Array { + return fmt.Errorf("cannot convert an Array into an array, lengths mismatch (have %d, need %d)", l, dst.Len()) + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } } ctx.putTyped(a.val, typ, dst.Interface()) for _, item := range a.items { @@ -483,5 +487,5 @@ func (a *sparseArrayObject) exportToSlice(dst reflect.Value, typ reflect.Type, c } return nil } - return a.baseObject.exportToSlice(dst, typ, ctx) + return a.baseObject.exportToArrayOrSlice(dst, typ, ctx) } diff --git a/builtin_map_test.go b/builtin_map_test.go index 8e853ea3..78c76952 100644 --- a/builtin_map_test.go +++ b/builtin_map_test.go @@ -109,6 +109,23 @@ func ExampleRuntime_ExportTo_mapToSlice() { // Output: [[1 true] [2 false]] } +func ExampleRuntime_ExportTo_mapToTypedSlice() { + vm := New() + m, err := vm.RunString(` + new Map([[1, true], [2, false]]); + `) + if err != nil { + panic(err) + } + exp := make([][2]interface{}, 0) + err = vm.ExportTo(m, &exp) + if err != nil { + panic(err) + } + fmt.Println(exp) + // Output: [[1 true] [2 false]] +} + func BenchmarkMapDelete(b *testing.B) { var key1 Value = asciiString("a") var key2 Value = asciiString("b") diff --git a/builtin_set.go b/builtin_set.go index a76d5eca..72e9015f 100644 --- a/builtin_set.go +++ b/builtin_set.go @@ -1,6 +1,9 @@ package goja -import "reflect" +import ( + "fmt" + "reflect" +) var setExportType = reflectTypeArray @@ -60,10 +63,14 @@ func (so *setObject) export(ctx *objectExportCtx) interface{} { return a } -func (so *setObject) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { +func (so *setObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { l := so.m.size - if dst.IsNil() || dst.Len() != l { - dst.Set(reflect.MakeSlice(typ, l, l)) + if dst.Len() != l { + if typ.Kind() == reflect.Array { + return fmt.Errorf("cannot convert a Set into an array, lengths mismatch: have %d, need %d)", l, dst.Len()) + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } } ctx.putTyped(so.val, typ, dst.Interface()) iter := so.m.newIter() diff --git a/builtin_set_test.go b/builtin_set_test.go index 6ec80857..9daa3489 100644 --- a/builtin_set_test.go +++ b/builtin_set_test.go @@ -2,6 +2,7 @@ package goja import ( "fmt" + "strings" "testing" ) @@ -81,3 +82,21 @@ func TestSetExportToSliceCircular(t *testing.T) { t.Fatalf("a: %v", a) } } + +func TestSetExportToArrayMismatchedLengths(t *testing.T) { + vm := New() + s, err := vm.RunString(` + new Set([1, 2]) + `) + if err != nil { + panic(err) + } + var s1 [3]int + err = vm.ExportTo(s, &s1) + if err == nil { + t.Fatal("expected error") + } + if msg := err.Error(); !strings.Contains(msg, "lengths mismatch") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/destruct.go b/destruct.go index 1cb507f4..a9ef1186 100644 --- a/destruct.go +++ b/destruct.go @@ -245,8 +245,8 @@ func (d *destructKeyedSource) exportToMap(dst reflect.Value, typ reflect.Type, c return d.w().exportToMap(dst, typ, ctx) } -func (d *destructKeyedSource) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { - return d.w().exportToSlice(dst, typ, ctx) +func (d *destructKeyedSource) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return d.w().exportToArrayOrSlice(dst, typ, ctx) } func (d *destructKeyedSource) equal(impl objectImpl) bool { diff --git a/object.go b/object.go index 18acda53..4c5b92f0 100644 --- a/object.go +++ b/object.go @@ -194,7 +194,7 @@ type objectImpl interface { export(ctx *objectExportCtx) interface{} exportType() reflect.Type exportToMap(m reflect.Value, typ reflect.Type, ctx *objectExportCtx) error - exportToSlice(s reflect.Value, typ reflect.Type, ctx *objectExportCtx) error + exportToArrayOrSlice(s reflect.Value, typ reflect.Type, ctx *objectExportCtx) error equal(objectImpl) bool iterateStringKeys() iterNextFunc @@ -1014,7 +1014,7 @@ func (o *baseObject) exportToMap(m reflect.Value, typ reflect.Type, ctx *objectE return genericExportToMap(o.val, m, typ, ctx) } -func genericExportToSlice(o *Object, dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) (err error) { +func genericExportToArrayOrSlice(o *Object, dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) (err error) { r := o.runtime if method := toMethod(r.getV(o, SymIterator)); method != nil { @@ -1028,8 +1028,12 @@ func genericExportToSlice(o *Object, dst reflect.Value, typ reflect.Type, ctx *o if ex != nil { return ex } - if dst.IsNil() || dst.Len() != len(values) { - dst.Set(reflect.MakeSlice(typ, len(values), len(values))) + if dst.Len() != len(values) { + if typ.Kind() == reflect.Array { + return fmt.Errorf("cannot convert an iterable into an array, lengths mismatch (have %d, need %d)", len(values), dst.Len()) + } else { + dst.Set(reflect.MakeSlice(typ, len(values), len(values))) + } } ctx.putTyped(o, typ, dst.Interface()) for i, val := range values { @@ -1041,8 +1045,12 @@ func genericExportToSlice(o *Object, dst reflect.Value, typ reflect.Type, ctx *o } else { // array-like l := toIntStrict(toLength(o.self.getStr("length", nil))) - if dst.IsNil() || dst.Len() != l { - dst.Set(reflect.MakeSlice(typ, l, l)) + if dst.Len() != l { + if typ.Kind() == reflect.Array { + return fmt.Errorf("cannot convert an array-like object into an array, lengths mismatch (have %d, need %d)", l, dst.Len()) + } else { + dst.Set(reflect.MakeSlice(typ, l, l)) + } } ctx.putTyped(o, typ, dst.Interface()) for i := 0; i < l; i++ { @@ -1057,8 +1065,8 @@ func genericExportToSlice(o *Object, dst reflect.Value, typ reflect.Type, ctx *o return } -func (o *baseObject) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { - return genericExportToSlice(o.val, dst, typ, ctx) +func (o *baseObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return genericExportToArrayOrSlice(o.val, dst, typ, ctx) } type enumerableFlag int diff --git a/object_dynamic.go b/object_dynamic.go index 7d42e8c4..e1ef97d5 100644 --- a/object_dynamic.go +++ b/object_dynamic.go @@ -487,8 +487,8 @@ func (o *baseDynamicObject) exportToMap(dst reflect.Value, typ reflect.Type, ctx return genericExportToMap(o.val, dst, typ, ctx) } -func (o *baseDynamicObject) exportToSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { - return genericExportToSlice(o.val, dst, typ, ctx) +func (o *baseDynamicObject) exportToArrayOrSlice(dst reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { + return genericExportToArrayOrSlice(o.val, dst, typ, ctx) } func (o *dynamicObject) equal(impl objectImpl) bool { diff --git a/object_goarray_reflect.go b/object_goarray_reflect.go new file mode 100644 index 00000000..9b6998af --- /dev/null +++ b/object_goarray_reflect.go @@ -0,0 +1,288 @@ +package goja + +import ( + "reflect" + "strconv" + + "github.com/dop251/goja/unistring" +) + +type objectGoArrayReflect struct { + objectGoReflect + lengthProp valueProperty + + putIdx func(idx int, v Value, throw bool) bool +} + +func (o *objectGoArrayReflect) _init() { + o.objectGoReflect.init() + o.class = classArray + o.prototype = o.val.runtime.global.ArrayPrototype + o.updateLen() + o.baseObject._put("length", &o.lengthProp) +} + +func (o *objectGoArrayReflect) init() { + o._init() + o.putIdx = o._putIdx +} + +func (o *objectGoArrayReflect) updateLen() { + o.lengthProp.value = intToValue(int64(o.value.Len())) +} + +func (o *objectGoArrayReflect) _hasIdx(idx valueInt) bool { + if idx := int64(idx); idx >= 0 && idx < int64(o.value.Len()) { + return true + } + return false +} + +func (o *objectGoArrayReflect) _hasStr(name unistring.String) bool { + if idx := strToIdx64(name); idx >= 0 && idx < int64(o.value.Len()) { + return true + } + return false +} + +func (o *objectGoArrayReflect) _getIdx(idx int) Value { + v := o.value.Index(idx) + if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() { + return _null + } + return o.val.runtime.ToValue(v.Interface()) +} + +func (o *objectGoArrayReflect) getIdx(idx valueInt, receiver Value) Value { + if idx := toIntStrict(int64(idx)); idx >= 0 && idx < o.value.Len() { + return o._getIdx(idx) + } + return o.objectGoReflect.getStr(idx.string(), receiver) +} + +func (o *objectGoArrayReflect) getStr(name unistring.String, receiver Value) Value { + var ownProp Value + if idx := strToGoIdx(name); idx >= 0 && idx < o.value.Len() { + ownProp = o._getIdx(idx) + } else if name == "length" { + ownProp = &o.lengthProp + } else { + ownProp = o.objectGoReflect.getOwnPropStr(name) + } + return o.getStrWithOwnProp(ownProp, name, receiver) +} + +func (o *objectGoArrayReflect) getOwnPropStr(name unistring.String) Value { + if idx := strToGoIdx(name); idx >= 0 { + if idx < o.value.Len() { + return &valueProperty{ + value: o._getIdx(idx), + writable: true, + enumerable: true, + } + } + return nil + } + if name == "length" { + return &o.lengthProp + } + return o.objectGoReflect.getOwnPropStr(name) +} + +func (o *objectGoArrayReflect) getOwnPropIdx(idx valueInt) Value { + if idx := toIntStrict(int64(idx)); idx >= 0 && idx < o.value.Len() { + return &valueProperty{ + value: o._getIdx(idx), + writable: true, + enumerable: true, + } + } + return nil +} + +func (o *objectGoArrayReflect) _putIdx(idx int, v Value, throw bool) bool { + err := o.val.runtime.toReflectValue(v, o.value.Index(idx), &objectExportCtx{}) + if err != nil { + o.val.runtime.typeErrorResult(throw, "Go type conversion error: %v", err) + return false + } + return true +} + +func (o *objectGoArrayReflect) setOwnIdx(idx valueInt, val Value, throw bool) bool { + if i := toIntStrict(int64(idx)); i >= 0 { + if i >= o.value.Len() { + if res, ok := o._setForeignIdx(idx, nil, val, o.val, throw); ok { + return res + } + } + return o.putIdx(i, val, throw) + } else { + name := idx.string() + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } + } +} + +func (o *objectGoArrayReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if idx >= o.value.Len() { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); ok { + return res + } + } + return o.putIdx(idx, val, throw) + } else { + if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { + o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) + return false + } else { + return res + } + } +} + +func (o *objectGoArrayReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignIdx(idx, trueValIfPresent(o._hasIdx(idx)), val, receiver, throw) +} + +func (o *objectGoArrayReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { + return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) +} + +func (o *objectGoArrayReflect) hasOwnPropertyIdx(idx valueInt) bool { + return o._hasIdx(idx) +} + +func (o *objectGoArrayReflect) hasOwnPropertyStr(name unistring.String) bool { + if o._hasStr(name) || name == "length" { + return true + } + return o.objectGoReflect._has(name.String()) +} + +func (o *objectGoArrayReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { + if i := toIntStrict(int64(idx)); i >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + return o.putIdx(i, val, throw) + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%d' on a Go slice", idx) + return false +} + +func (o *objectGoArrayReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { + return false + } + val := descr.Value + if val == nil { + val = _undefined + } + return o.putIdx(idx, val, throw) + } + o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a Go slice", name) + return false +} + +func (o *objectGoArrayReflect) toPrimitiveNumber() Value { + return o.toPrimitiveString() +} + +func (o *objectGoArrayReflect) toPrimitiveString() Value { + return o.val.runtime.arrayproto_join(FunctionCall{ + This: o.val, + }) +} + +func (o *objectGoArrayReflect) toPrimitive() Value { + return o.toPrimitiveString() +} + +func (o *objectGoArrayReflect) _deleteIdx(idx int) { + if idx < o.value.Len() { + o.value.Index(idx).Set(reflect.Zero(o.value.Type().Elem())) + } +} + +func (o *objectGoArrayReflect) deleteStr(name unistring.String, throw bool) bool { + if idx := strToGoIdx(name); idx >= 0 { + o._deleteIdx(idx) + return true + } + + return o.objectGoReflect.deleteStr(name, throw) +} + +func (o *objectGoArrayReflect) deleteIdx(i valueInt, throw bool) bool { + idx := toIntStrict(int64(i)) + if idx >= 0 { + o._deleteIdx(idx) + } + return true +} + +type goArrayReflectPropIter struct { + o *objectGoArrayReflect + idx, limit int +} + +func (i *goArrayReflectPropIter) next() (propIterItem, iterNextFunc) { + if i.idx < i.limit && i.idx < i.o.value.Len() { + name := strconv.Itoa(i.idx) + i.idx++ + return propIterItem{name: asciiString(name), enumerable: _ENUM_TRUE}, i.next + } + + return i.o.objectGoReflect.iterateStringKeys()() +} + +func (o *objectGoArrayReflect) stringKeys(all bool, accum []Value) []Value { + for i := 0; i < o.value.Len(); i++ { + accum = append(accum, asciiString(strconv.Itoa(i))) + } + + return o.objectGoReflect.stringKeys(all, accum) +} + +func (o *objectGoArrayReflect) iterateStringKeys() iterNextFunc { + return (&goArrayReflectPropIter{ + o: o, + limit: o.value.Len(), + }).next +} + +func (o *objectGoArrayReflect) equal(other objectImpl) bool { + if other, ok := other.(*objectGoArrayReflect); ok { + return o.value.Interface() == other.value.Interface() + } + return false +} + +func (o *objectGoArrayReflect) sortLen() int64 { + return int64(o.value.Len()) +} + +func (o *objectGoArrayReflect) sortGet(i int64) Value { + return o.getIdx(valueInt(i), nil) +} + +func (o *objectGoArrayReflect) swap(i, j int64) { + ii := toIntStrict(i) + jj := toIntStrict(j) + x := o._getIdx(ii) + y := o._getIdx(jj) + + o._putIdx(ii, y, false) + o._putIdx(jj, x, false) +} diff --git a/object_goarray_reflect_test.go b/object_goarray_reflect_test.go new file mode 100644 index 00000000..ff795fef --- /dev/null +++ b/object_goarray_reflect_test.go @@ -0,0 +1,49 @@ +package goja + +import "testing" + +func TestGoReflectArray(t *testing.T) { + vm := New() + vm.Set("a", [...]int{1, 2, 3}) + _, err := vm.RunString(` + if (!Array.isArray(a)) { + throw new Error("isArray() returned false"); + } + if (a[0] !== 1 || a[1] !== 2 || a[2] !== 3) { + throw new Error("Array contents is incorrect"); + } + if (!a.hasOwnProperty("length")) { + throw new Error("hasOwnProperty() returned false"); + } + let desc = Object.getOwnPropertyDescriptor(a, "length"); + if (desc.value !== 3 || desc.writable || desc.enumerable || desc.configurable) { + throw new Error("incorrect property descriptor: " + JSON.stringify(desc)); + } + `) + if err != nil { + t.Fatal(err) + } +} + +func TestGoReflectArraySort(t *testing.T) { + vm := New() + vm.Set("a", [...]int{3, 1, 2}) + v, err := vm.RunString(` + a.sort(); + if (a[0] !== 1 || a[1] !== 2 || a[2] !== 3) { + throw new Error(a.toString()); + } + a; + `) + if err != nil { + t.Fatal(err) + } + res := v.Export() + if a, ok := res.([3]int); ok { + if a[0] != 1 || a[1] != 2 || a[2] != 3 { + t.Fatal(a) + } + } else { + t.Fatalf("Wrong type: %T", res) + } +} diff --git a/object_goreflect.go b/object_goreflect.go index 7d06b2c6..86e8fb54 100644 --- a/object_goreflect.go +++ b/object_goreflect.go @@ -103,6 +103,12 @@ func (o *objectGoReflect) init() { default: o.class = classObject o.prototype = o.val.runtime.global.ObjectPrototype + if !o.value.CanAddr() { + value := reflect.Indirect(reflect.New(o.value.Type())) + value.Set(o.value) + o.origValue = value + o.value = value + } } o.extensible = true diff --git a/object_goreflect_test.go b/object_goreflect_test.go index 979095fd..24050922 100644 --- a/object_goreflect_test.go +++ b/object_goreflect_test.go @@ -636,25 +636,17 @@ func TestStructNonAddressable(t *testing.T) { const SCRIPT = ` "use strict"; - if (Object.getOwnPropertyDescriptor(s, "Field").writable) { - throw new Error("Field is writable"); + if (!Object.getOwnPropertyDescriptor(s, "Field").writable) { + throw new Error("s.Field is non-writable"); } if (!Object.getOwnPropertyDescriptor(s1, "Field").writable) { - throw new Error("Field is non-writable"); + throw new Error("s1.Field is non-writable"); } s1.Field = 42; - - var result; - try { - s.Field = 42; - result = false; - } catch (e) { - result = e instanceof TypeError; - } - - result; + s.Field = 43; + s; ` var s S @@ -665,8 +657,13 @@ func TestStructNonAddressable(t *testing.T) { if err != nil { t.Fatal(err) } - if !v.StrictEquals(valueTrue) { - t.Fatalf("Unexpected result: %v", v) + exp := v.Export() + if s1, ok := exp.(S); ok { + if s1.Field != 43 { + t.Fatal(s1) + } + } else { + t.Fatalf("Wrong type: %T", exp) } if s.Field != 42 { t.Fatalf("Unexpected s.Field value: %d", s.Field) @@ -877,20 +874,11 @@ func TestNestedStructSet(t *testing.T) { throw new Error("a1.B.Field = " + a1.B.Field); } var d = Object.getOwnPropertyDescriptor(a1.B, "Field"); - if (d.writable) { - throw new Error("a1.B is writable"); - } - var thrown = false; - try { - a1.B.Field = 42; - } catch (e) { - if (e instanceof TypeError) { - thrown = true; - } - } - if (!thrown) { - throw new Error("TypeError was not thrown"); + if (!d.writable) { + throw new Error("a1.B is not writable"); } + a1.B.Field = 42; + a1; ` a := A{ B: B{ @@ -900,10 +888,18 @@ func TestNestedStructSet(t *testing.T) { vm := New() vm.Set("a", &a) vm.Set("a1", a) - _, err := vm.RunString(SCRIPT) + v, err := vm.RunString(SCRIPT) if err != nil { t.Fatal(err) } + exp := v.Export() + if v, ok := exp.(A); ok { + if v.B.Field != 42 { + t.Fatal(v) + } + } else { + t.Fatalf("Wrong type: %T", exp) + } if v := a.B.Field; v != 2 { t.Fatalf("Unexpected a.B.Field: %d", v) diff --git a/object_goslice_reflect.go b/object_goslice_reflect.go index 94284806..2da5f8dc 100644 --- a/object_goslice_reflect.go +++ b/object_goslice_reflect.go @@ -4,113 +4,25 @@ import ( "math" "math/bits" "reflect" - "strconv" "github.com/dop251/goja/unistring" ) type objectGoSliceReflect struct { - objectGoReflect - lengthProp valueProperty + objectGoArrayReflect } func (o *objectGoSliceReflect) init() { - o.objectGoReflect.init() - o.class = classArray - o.prototype = o.val.runtime.global.ArrayPrototype - if !o.value.CanSet() { - value := reflect.Indirect(reflect.New(o.value.Type())) - value.Set(o.value) - o.value = value - } + o.objectGoArrayReflect._init() o.lengthProp.writable = true - o.updateLen() - o.baseObject._put("length", &o.lengthProp) -} - -func (o *objectGoSliceReflect) updateLen() { - o.lengthProp.value = intToValue(int64(o.value.Len())) -} - -func (o *objectGoSliceReflect) _hasIdx(idx valueInt) bool { - if idx := int64(idx); idx >= 0 && idx < int64(o.value.Len()) { - return true - } - return false -} - -func (o *objectGoSliceReflect) _hasStr(name unistring.String) bool { - if idx := strToIdx64(name); idx >= 0 && idx < int64(o.value.Len()) { - return true - } - return false -} - -func (o *objectGoSliceReflect) _getIdx(idx int) Value { - v := o.value.Index(idx) - if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() { - return _null - } - return o.val.runtime.ToValue(v.Interface()) + o.putIdx = o._putIdx } -func (o *objectGoSliceReflect) getIdx(idx valueInt, receiver Value) Value { - if idx := toIntStrict(int64(idx)); idx >= 0 && idx < o.value.Len() { - return o._getIdx(idx) - } - return o.objectGoReflect.getStr(idx.string(), receiver) -} - -func (o *objectGoSliceReflect) getStr(name unistring.String, receiver Value) Value { - var ownProp Value - if idx := strToGoIdx(name); idx >= 0 && idx < o.value.Len() { - ownProp = o._getIdx(idx) - } else if name == "length" { - ownProp = &o.lengthProp - } else { - ownProp = o.objectGoReflect.getOwnPropStr(name) - } - return o.getStrWithOwnProp(ownProp, name, receiver) -} - -func (o *objectGoSliceReflect) getOwnPropStr(name unistring.String) Value { - if idx := strToGoIdx(name); idx >= 0 { - if idx < o.value.Len() { - return &valueProperty{ - value: o._getIdx(idx), - writable: true, - enumerable: true, - } - } - return nil - } - if name == "length" { - return &o.lengthProp - } - return o.objectGoReflect.getOwnPropStr(name) -} - -func (o *objectGoSliceReflect) getOwnPropIdx(idx valueInt) Value { - if idx := toIntStrict(int64(idx)); idx >= 0 && idx < o.value.Len() { - return &valueProperty{ - value: o._getIdx(idx), - writable: true, - enumerable: true, - } - } - return nil -} - -func (o *objectGoSliceReflect) putIdx(idx int, v Value, throw bool) bool { +func (o *objectGoSliceReflect) _putIdx(idx int, v Value, throw bool) bool { if idx >= o.value.Len() { o.grow(idx + 1) } - err := o.val.runtime.toReflectValue(v, o.value.Index(idx), &objectExportCtx{}) - if err != nil { - o.val.runtime.typeErrorResult(throw, "Go type conversion error: %v", err) - return false - } - return true + return o.objectGoArrayReflect._putIdx(idx, v, throw) } func (o *objectGoSliceReflect) grow(size int) { @@ -154,167 +66,18 @@ func (o *objectGoSliceReflect) putLength(v uint32, throw bool) bool { return true } -func (o *objectGoSliceReflect) setOwnIdx(idx valueInt, val Value, throw bool) bool { - if i := toIntStrict(int64(idx)); i >= 0 { - if i >= o.value.Len() { - if res, ok := o._setForeignIdx(idx, nil, val, o.val, throw); ok { - return res - } - } - o.putIdx(i, val, throw) - } else { - name := idx.string() - if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { - o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) - return false - } else { - return res - } - } - return true -} - func (o *objectGoSliceReflect) setOwnStr(name unistring.String, val Value, throw bool) bool { - if idx := strToGoIdx(name); idx >= 0 { - if idx >= o.value.Len() { - if res, ok := o._setForeignStr(name, nil, val, o.val, throw); ok { - return res - } - } - o.putIdx(idx, val, throw) - } else { - if name == "length" { - return o.putLength(o.val.runtime.toLengthUint32(val), throw) - } - if res, ok := o._setForeignStr(name, nil, val, o.val, throw); !ok { - o.val.runtime.typeErrorResult(throw, "Can't set property '%s' on Go slice", name) - return false - } else { - return res - } - } - return true -} - -func (o *objectGoSliceReflect) setForeignIdx(idx valueInt, val, receiver Value, throw bool) (bool, bool) { - return o._setForeignIdx(idx, trueValIfPresent(o._hasIdx(idx)), val, receiver, throw) -} - -func (o *objectGoSliceReflect) setForeignStr(name unistring.String, val, receiver Value, throw bool) (bool, bool) { - return o._setForeignStr(name, trueValIfPresent(o.hasOwnPropertyStr(name)), val, receiver, throw) -} - -func (o *objectGoSliceReflect) hasOwnPropertyIdx(idx valueInt) bool { - return o._hasIdx(idx) -} - -func (o *objectGoSliceReflect) hasOwnPropertyStr(name unistring.String) bool { - if o._hasStr(name) || name == "length" { - return true - } - return o.objectGoReflect._has(name.String()) -} - -func (o *objectGoSliceReflect) defineOwnPropertyIdx(idx valueInt, descr PropertyDescriptor, throw bool) bool { - if i := toIntStrict(int64(idx)); i >= 0 { - if !o.val.runtime.checkHostObjectPropertyDescr(idx.string(), descr, throw) { - return false - } - val := descr.Value - if val == nil { - val = _undefined - } - o.putIdx(i, val, throw) - return true + if name == "length" { + return o.putLength(o.val.runtime.toLengthUint32(val), throw) } - o.val.runtime.typeErrorResult(throw, "Cannot define property '%d' on a Go slice", idx) - return false + return o.objectGoArrayReflect.setOwnStr(name, val, throw) } func (o *objectGoSliceReflect) defineOwnPropertyStr(name unistring.String, descr PropertyDescriptor, throw bool) bool { - if idx := strToGoIdx(name); idx >= 0 { - if !o.val.runtime.checkHostObjectPropertyDescr(name, descr, throw) { - return false - } - val := descr.Value - if val == nil { - val = _undefined - } - o.putIdx(idx, val, throw) - return true - } if name == "length" { return o.val.runtime.defineArrayLength(&o.lengthProp, descr, o.putLength, throw) } - o.val.runtime.typeErrorResult(throw, "Cannot define property '%s' on a Go slice", name) - return false -} - -func (o *objectGoSliceReflect) toPrimitiveNumber() Value { - return o.toPrimitiveString() -} - -func (o *objectGoSliceReflect) toPrimitiveString() Value { - return o.val.runtime.arrayproto_join(FunctionCall{ - This: o.val, - }) -} - -func (o *objectGoSliceReflect) toPrimitive() Value { - return o.toPrimitiveString() -} - -func (o *objectGoSliceReflect) _deleteIdx(idx int) { - if idx < o.value.Len() { - o.value.Index(idx).Set(reflect.Zero(o.value.Type().Elem())) - } -} - -func (o *objectGoSliceReflect) deleteStr(name unistring.String, throw bool) bool { - if idx := strToGoIdx(name); idx >= 0 { - o._deleteIdx(idx) - return true - } - - return o.objectGoReflect.deleteStr(name, throw) -} - -func (o *objectGoSliceReflect) deleteIdx(i valueInt, throw bool) bool { - idx := toIntStrict(int64(i)) - if idx >= 0 { - o._deleteIdx(idx) - } - return true -} - -type gosliceReflectPropIter struct { - o *objectGoSliceReflect - idx, limit int -} - -func (i *gosliceReflectPropIter) next() (propIterItem, iterNextFunc) { - if i.idx < i.limit && i.idx < i.o.value.Len() { - name := strconv.Itoa(i.idx) - i.idx++ - return propIterItem{name: asciiString(name), enumerable: _ENUM_TRUE}, i.next - } - - return i.o.objectGoReflect.iterateStringKeys()() -} - -func (o *objectGoSliceReflect) stringKeys(all bool, accum []Value) []Value { - for i := 0; i < o.value.Len(); i++ { - accum = append(accum, asciiString(strconv.Itoa(i))) - } - - return o.objectGoReflect.stringKeys(all, accum) -} - -func (o *objectGoSliceReflect) iterateStringKeys() iterNextFunc { - return (&gosliceReflectPropIter{ - o: o, - limit: o.value.Len(), - }).next + return o.objectGoArrayReflect.defineOwnPropertyStr(name, descr, throw) } func (o *objectGoSliceReflect) equal(other objectImpl) bool { @@ -323,21 +86,3 @@ func (o *objectGoSliceReflect) equal(other objectImpl) bool { } return false } - -func (o *objectGoSliceReflect) sortLen() int64 { - return int64(o.value.Len()) -} - -func (o *objectGoSliceReflect) sortGet(i int64) Value { - return o.getIdx(valueInt(i), nil) -} - -func (o *objectGoSliceReflect) swap(i, j int64) { - ii := valueInt(i) - jj := valueInt(j) - x := o.getIdx(ii, nil) - y := o.getIdx(jj, nil) - - o.setOwnIdx(ii, y, false) - o.setOwnIdx(jj, x, false) -} diff --git a/object_goslice_reflect_test.go b/object_goslice_reflect_test.go index ec2c77fc..150347ec 100644 --- a/object_goslice_reflect_test.go +++ b/object_goslice_reflect_test.go @@ -111,6 +111,54 @@ func TestGoSliceReflectPush(t *testing.T) { } +func TestGoSliceReflectStructField(t *testing.T) { + vm := New() + var s struct { + A []int + B *[]int + } + vm.Set("s", &s) + _, err := vm.RunString(` + 'use strict'; + s.A.push(1); + if (s.B !== null) { + throw new Error("s.B is not null: " + s.B); + } + s.B = [2]; + `) + if err != nil { + t.Fatal(err) + } + if len(s.A) != 1 || s.A[0] != 1 { + t.Fatalf("s.A: %v", s.A) + } + if len(*s.B) != 1 || (*s.B)[0] != 2 { + t.Fatalf("s.B: %v", *s.B) + } +} + +func TestGoSliceReflectExportToStructField(t *testing.T) { + vm := New() + v, err := vm.RunString(`({A: [1], B: [2]})`) + if err != nil { + t.Fatal(err) + } + var s struct { + A []int + B *[]int + } + err = vm.ExportTo(v, &s) + if err != nil { + t.Fatal(err) + } + if len(s.A) != 1 || s.A[0] != 1 { + t.Fatalf("s.A: %v", s.A) + } + if len(*s.B) != 1 || (*s.B)[0] != 2 { + t.Fatalf("s.B: %v", *s.B) + } +} + func TestGoSliceReflectProtoMethod(t *testing.T) { const SCRIPT = ` a.join(",") @@ -370,3 +418,33 @@ func TestGoSliceReflectMethods(t *testing.T) { t.Fatal(err) } } + +func TestGoSliceReflectExportAfterGrow(t *testing.T) { + vm := New() + vm.Set("a", []int{1}) + v, err := vm.RunString(` + a.push(2); + a; + `) + if err != nil { + t.Fatal(err) + } + exp := v.Export() + if a, ok := exp.([]int); ok { + if len(a) != 2 || a[0] != 1 || a[1] != 2 { + t.Fatal(a) + } + } else { + t.Fatalf("Wrong type: %T", exp) + } +} + +func BenchmarkGoSliceReflectSet(b *testing.B) { + vm := New() + a := vm.ToValue([]int{1}).(*Object) + b.ResetTimer() + v := intToValue(0) + for i := 0; i < b.N; i++ { + a.Set("0", v) + } +} diff --git a/object_lazy.go b/object_lazy.go index 8db64f83..e8de40e2 100644 --- a/object_lazy.go +++ b/object_lazy.go @@ -259,10 +259,10 @@ func (o *lazyObject) exportToMap(m reflect.Value, typ reflect.Type, ctx *objectE return obj.exportToMap(m, typ, ctx) } -func (o *lazyObject) exportToSlice(s reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { +func (o *lazyObject) exportToArrayOrSlice(s reflect.Value, typ reflect.Type, ctx *objectExportCtx) error { obj := o.create(o.val) o.val.self = obj - return obj.exportToSlice(s, typ, ctx) + return obj.exportToArrayOrSlice(s, typ, ctx) } func (o *lazyObject) equal(other objectImpl) bool { diff --git a/object_test.go b/object_test.go index 3f221d5e..66ecf66a 100644 --- a/object_test.go +++ b/object_test.go @@ -3,6 +3,7 @@ package goja import ( "fmt" "reflect" + "strings" "testing" ) @@ -390,6 +391,61 @@ func ExampleRuntime_ExportTo_arrayLikeToSlice() { // Output: [1 2 3] } +func TestExportArrayToArrayMismatchedLengths(t *testing.T) { + vm := New() + a := vm.NewArray(1, 2) + var a1 [3]int + err := vm.ExportTo(a, &a1) + if err == nil { + t.Fatal("expected error") + } + if msg := err.Error(); !strings.Contains(msg, "lengths mismatch") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestExportIterableToArrayMismatchedLengths(t *testing.T) { + vm := New() + a, err := vm.RunString(` + new Map([[1, true], [2, true]]); + `) + if err != nil { + t.Fatal(err) + } + + var a1 [3]interface{} + err = vm.ExportTo(a, &a1) + if err == nil { + t.Fatal("expected error") + } + if msg := err.Error(); !strings.Contains(msg, "lengths mismatch") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestExportArrayLikeToArrayMismatchedLengths(t *testing.T) { + vm := New() + a, err := vm.RunString(` + ({ + length: 2, + 0: true, + 1: true + }); + `) + if err != nil { + t.Fatal(err) + } + + var a1 [3]interface{} + err = vm.ExportTo(a, &a1) + if err == nil { + t.Fatal("expected error") + } + if msg := err.Error(); !strings.Contains(msg, "lengths mismatch") { + t.Fatalf("unexpected error: %v", err) + } +} + func TestSetForeignReturnValue(t *testing.T) { const SCRIPT = ` var array = [1, 2, 3]; diff --git a/runtime.go b/runtime.go index eee25a42..ec157928 100644 --- a/runtime.go +++ b/runtime.go @@ -1463,6 +1463,8 @@ Export()'ed and therefore copied. This may result in an unexpected behaviour in `) fmt.Println(m["obj"].(map[string]interface{})["test"]) // prints "false" +Non-addressable structs, slices and arrays get copied (as if they were passed as a function parameter, by value). + Notes on individual types: Primitive types @@ -1539,8 +1541,7 @@ Structs Structs are converted to Object-like values. Fields and methods are available as properties, their values are results of this method (ToValue()) applied to the corresponding Go value. -Field properties are writable (if the struct is addressable) and non-configurable. -Method properties are non-writable and non-configurable. +Field properties are writable and non-configurable. Method properties are non-writable and non-configurable. Attempt to define a new property or delete an existing property will fail (throw in strict mode) unless it's a Symbol property. Symbol properties only exist in the wrapper and do not affect the underlying Go value. @@ -1611,6 +1612,11 @@ an index < length will set it to a zero value (but the property will remain). Ni `null`. Accessing an element beyond `length` returns `undefined`. Also see the warning above about passing slices as values (as opposed to pointers). +Arrays + +Arrays are converted similarly to slices, except the resulting Arrays are not resizable (and therefore the 'length' +property is non-writable). + Any other type is converted to a generic reflect based host object. Depending on the underlying type it behaves similar to a Number, String, Boolean or Object. @@ -1767,9 +1773,9 @@ func (r *Runtime) ToValue(i interface{}) Value { return obj } } - case reflect.Slice: + case reflect.Array: obj := &Object{runtime: r} - a := &objectGoSliceReflect{ + a := &objectGoArrayReflect{ objectGoReflect: objectGoReflect{ baseObject: baseObject{ val: obj, @@ -1781,6 +1787,22 @@ func (r *Runtime) ToValue(i interface{}) Value { a.init() obj.self = a return obj + case reflect.Slice: + obj := &Object{runtime: r} + a := &objectGoSliceReflect{ + objectGoArrayReflect: objectGoArrayReflect{ + objectGoReflect: objectGoReflect{ + baseObject: baseObject{ + val: obj, + }, + origValue: origValue, + value: value, + }, + }, + } + a.init() + obj.self = a + return obj case reflect.Func: name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()) return r.newNativeFunc(r.wrapReflectFunc(value), nil, name, nil, value.Type().NumIn()) @@ -2012,13 +2034,13 @@ func (r *Runtime) toReflectValue(v Value, dst reflect.Value, ctx *objectExportCt case reflect.Float32: dst.Set(reflect.ValueOf(toFloat32(v)).Convert(typ)) return nil - case reflect.Slice: + case reflect.Slice, reflect.Array: if o, ok := v.(*Object); ok { if v, exists := ctx.getTyped(o, typ); exists { dst.Set(reflect.ValueOf(v)) return nil } - return o.self.exportToSlice(dst, typ, ctx) + return o.self.exportToArrayOrSlice(dst, typ, ctx) } case reflect.Map: if o, ok := v.(*Object); ok { @@ -2175,6 +2197,11 @@ func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect. // // Any other Object is treated as an array-like object with zero length and results in an empty slice. // +// Array types +// +// Anything that can be exported to a slice type can also be exported to an array type, as long as the lengths +// match. If they do not, an error is returned. +// // Proxy // // Proxy objects are treated the same way as if they were accessed from ES code in regard to their properties