diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f97e4be..501be13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: ['1.22.x', '1.23.x', '1.24.x'] + go: ['1.22.x', '1.23.x', '1.24.x', '1.25.x'] platform: [ubuntu-latest, windows-latest, macos-latest, macos-14] steps: - uses: actions/checkout@v5 diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..c21ed61 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,25 @@ +project_name: qjs +dist: releases + +version: 1 + +before: + hooks: + - go mod tidy + +builds: + - skip: true + +archives: + - id: build_main + format: zip + +snapshot: + name_template: "{{ incpatch .Version }}-next" + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" diff --git a/common.go b/common.go index 88b5ab0..8609a5f 100644 --- a/common.go +++ b/common.go @@ -13,19 +13,19 @@ import ( ) const ( - // The minimum number of arguments for Map forEach callback (value, key). + // MinMapForeachArgs is the minimum number of arguments for Map forEach callback (value, key). MinMapForeachArgs = 2 - // The conversion factor from nanoseconds to milliseconds. + // NanosToMillis is the conversion factor from nanoseconds to milliseconds. NanosToMillis = 1e6 - // The size of a uint64 value in bytes. + // Uint64ByteSize is the size of a uint64 value in bytes. Uint64ByteSize = 8 - // The bit position of the sign bit in a 64-bit unsigned integer. + // Uint64SignBitPosition is the bit position of the sign bit in a 64-bit unsigned integer. Uint64SignBitPosition = 63 - // The size in bytes of a packed pointer structure. + // PackedPtrSize is the size in bytes of a packed pointer structure. PackedPtrSize = 8 // NullPtr represents a null pointer value. NullPtr = uint32(0) - // The null terminator byte for C-style strings. + // StringTerminator is the null terminator byte for C-style strings. StringTerminator = byte(0) ) @@ -870,7 +870,7 @@ func ParseTimezone(tz string) *time.Location { return time.UTC } -// Check if the platform is 32-bit by comparing the size of uintptr. +// Is32BitPlatform check if the platform is 32-bit by comparing the size of uintptr. func Is32BitPlatform() bool { return strconv.IntSize == 32 } diff --git a/context.go b/context.go index 2750259..6c4a226 100644 --- a/context.go +++ b/context.go @@ -356,7 +356,7 @@ func (c *Context) Function(fn Function, isAsyncs ...bool) *Value { fnID := c.runtime.registry.Register(fn) ctxID := c.runtime.registry.Register(c) - proxyFuncVal := c.Call("QJS_CreateProxyFunction", c.Raw(), fnID, ctxID, isAsync) + proxyFuncVal := c.Call("QJS_CreateFunctionProxy", c.Raw(), fnID, ctxID, isAsync) // Registry: Store IDs for cleanup and callback identification proxyFuncVal.SetPropertyStr("__fnID", c.NewInt64(int64(fnID))) diff --git a/eval_test.go b/eval_test.go index a0e3c65..e282163 100644 --- a/eval_test.go +++ b/eval_test.go @@ -325,7 +325,7 @@ func TestModuleLoading(t *testing.T) { options: []qjs.EvalOptionFunc{qjs.TypeModule()}, expectErr: func(t *testing.T, err error) { assert.Error(t, err) - assert.Contains(t, err.Error(), "could not load file") + assert.Contains(t, err.Error(), "No such file or directory: testdata/00_loader/not_found.js") }, }, { diff --git a/functojs.go b/functojs.go index 8e8bce4..abcfa4a 100644 --- a/functojs.go +++ b/functojs.go @@ -1,6 +1,7 @@ package qjs import ( + "context" "fmt" "reflect" ) @@ -125,13 +126,69 @@ func handlePointerArgument(jsArg *Value, argType reflect.Type) (reflect.Value, e return ptrVal, nil } +// CreateNonNilSample creates appropriate non-nil samples for types that have nil zero values. +func CreateNonNilSample(argType reflect.Type) any { + switch argType.Kind() { + case reflect.Interface: + // Special handling for context.Context + if argType.Implements(reflect.TypeOf((*context.Context)(nil)).Elem()) { + return context.Background() + } + + // For other interfaces, return nil to let the conversion + // use the default logic which can handle dynamic type inference + return nil + + case reflect.Ptr: + elemType := argType.Elem() + elemZero := reflect.Zero(elemType) + ptr := reflect.New(elemType) + ptr.Elem().Set(elemZero) + + return ptr.Interface() + + case reflect.Array: + return reflect.New(argType).Elem().Interface() + + case reflect.Slice: + return reflect.MakeSlice(argType, 0, 0).Interface() + + case reflect.Map: + return reflect.MakeMap(argType).Interface() + + case reflect.Chan: + return reflect.MakeChan(argType, 1).Interface() // size 1 buffer to avoid blocking + + case reflect.Func: + return createDummyFunction(argType) + + default: + // For other types (shouldn't happen), return zero value + return reflect.Zero(argType).Interface() + } +} + +// createDummyFunction creates a dummy function with the specified signature for type inference. +func createDummyFunction(funcType reflect.Type) any { + fn := reflect.MakeFunc(funcType, func(_ []reflect.Value) []reflect.Value { + results := make([]reflect.Value, funcType.NumOut()) + for i := range funcType.NumOut() { + results[i] = reflect.Zero(funcType.Out(i)) + } + + return results + }) + + return fn.Interface() +} + // JsArgToGo converts a single JS argument to a Go value with enhanced type handling. func JsArgToGo(jsArg *Value, argType reflect.Type) (reflect.Value, error) { if argType.Kind() == reflect.Ptr { return handlePointerArgument(jsArg, argType) } - goZeroVal := reflect.Zero(argType).Interface() + goZeroVal := CreateNonNilSample(argType) goVal, err := JsValueToGo(jsArg, goZeroVal) if err != nil { diff --git a/functojs_test.go b/functojs_test.go index d3fd763..1b1179c 100644 --- a/functojs_test.go +++ b/functojs_test.go @@ -1,8 +1,10 @@ package qjs_test import ( + "context" "errors" "fmt" + "reflect" "testing" "unsafe" @@ -641,3 +643,76 @@ func TestMethodBinding(t *testing.T) { }) }) } + +func TestCreateNonNilSample(t *testing.T) { + tests := []struct { + name string + input any + expectedType any + shouldBeNotNil bool + testFuncExecution bool + }{ + { + name: "Context type", + input: context.Background(), + expectedType: context.Background(), + shouldBeNotNil: true, + }, + { + name: "Pointer type", + input: (*int)(nil), + expectedType: (*int)(nil), + shouldBeNotNil: false, // Pointer itself can be nil but sample won't be + }, + { + name: "Array type", + input: [3]string{}, + expectedType: [3]string{}, + shouldBeNotNil: true, + }, + { + name: "Slice type", + input: []string{}, + expectedType: []string{}, + shouldBeNotNil: true, + }, + { + name: "Map type", + input: map[string]int{}, + expectedType: map[string]int{}, + shouldBeNotNil: true, + }, + { + name: "Chan type", + input: (chan int)(nil), + expectedType: (chan int)(nil), + shouldBeNotNil: true, + }, + { + name: "Func type", + input: (func(int) int)(nil), + expectedType: (func(int) int)(nil), + shouldBeNotNil: true, + testFuncExecution: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sample := qjs.CreateNonNilSample(reflect.TypeOf(tt.input)) + + assert.IsType(t, tt.expectedType, sample) + + if tt.shouldBeNotNil { + assert.NotNil(t, sample) + } + + // Special test for function execution + if tt.testFuncExecution { + dummyFunc := sample.(func(int) int) + result := dummyFunc(42) + assert.Equal(t, 0, result) // Should return zero value + } + }) + } +} diff --git a/gotojs.go b/gotojs.go index 8c4c2cc..555d636 100644 --- a/gotojs.go +++ b/gotojs.go @@ -42,6 +42,8 @@ func (tracker *Tracker[T]) StructToJSObjectValue( rval reflect.Value, ) (*Value, error) { return withJSObject(c, func(obj *Value) error { + // obj.SetPropertyStr("__go_type", c.NewString(GetGoTypeName(rtype))) + // Determine the struct type and value for field processing: // - pointer to struct: dereference for field processing. // - direct struct: use as-is. @@ -50,7 +52,7 @@ func (tracker *Tracker[T]) StructToJSObjectValue( structVal reflect.Value ) - if rtype.Kind() == reflect.Ptr { + if rtype.Kind() == reflect.Pointer { structType = rtype.Elem() structVal = rval.Elem() } else { @@ -360,8 +362,9 @@ func (tracker *Tracker[T]) processEmbeddedFields( ) error { for i := range rtype.NumField() { field := rtype.Field(i) + jsonIgnore := field.Tag.Get("json") == "-" - if !field.IsExported() || !field.Anonymous { + if !field.IsExported() || !field.Anonymous || jsonIgnore { continue } @@ -434,6 +437,7 @@ func (tracker *Tracker[T]) processRegularFields( ) error { for i := range rtype.NumField() { field := rtype.Field(i) + if !field.IsExported() || field.Anonymous { continue } diff --git a/handle.go b/handle.go index a9c3c83..fce78d4 100644 --- a/handle.go +++ b/handle.go @@ -142,7 +142,6 @@ func ConvertToUnsigned[T Unsigned](h *Handle) T { return result } -// Integer conversion methods using the generic functions. func (h *Handle) Int() int { return ConvertToSigned[int](h) } func (h *Handle) Int8() int8 { return ConvertToSigned[int8](h) } func (h *Handle) Int16() int16 { return ConvertToSigned[int16](h) } diff --git a/jstogo.go b/jstogo.go index 8ec2d74..abff5c9 100644 --- a/jstogo.go +++ b/jstogo.go @@ -210,7 +210,7 @@ func jsObjectOrMapToGoMap[T any]( temp, obj, sample, targetType := createGoObjectTarget(input, samples...) defer func() { - v, err = processTempValue("JsObjectOrMapToGo", temp, err, sample) + v, err = processTempValue("JsObjectOrMapToGoMap", temp, err, sample) }() var ct CircularTracker[uint64] @@ -270,7 +270,7 @@ func jsObjectOrMapToGoStruct[T any]( temp, obj, sample, targetType := createGoObjectTarget(input, samples...) defer func() { - v, err = processTempValue("JsObjectOrMapToGo", temp, err, sample) + v, err = processTempValue("JsObjectOrMapToGoStruct", temp, err, sample) }() var ct CircularTracker[uint64] diff --git a/options.go b/options.go index 1231da7..6b4a9a2 100644 --- a/options.go +++ b/options.go @@ -22,9 +22,9 @@ const ( JsEvalFlagStrict = (1 << 3) // JsEvalFlagUnUsed is reserved for future use. JsEvalFlagUnUsed = (1 << 4) - // Compile-only; returns a JS bytecode/module for JS_EvalFunction().. + // JsEvalFlagCompileOnly returns a JS bytecode/module for JS_EvalFunction(). JsEvalFlagCompileOnly = (1 << 5) - // Don't include the stack frames before this eval in the Error() backtraces. + // JsEvalFlagBackTraceBarrier prevents the stack frames before this eval in the Error() backtraces. JsEvalFlagBackTraceBarrier = (1 << 6) // JsEvalFlagAsync enables top-level await (global scope only). JsEvalFlagAsync = (1 << 7) diff --git a/qjsextra.wasm b/qjsextra.wasm index 3b0f19a..ff496cf 100755 Binary files a/qjsextra.wasm and b/qjsextra.wasm differ diff --git a/qjsextra/eval.c b/qjsextra/eval.c index a3c3480..d6d8a47 100644 --- a/qjsextra/eval.c +++ b/qjsextra/eval.c @@ -1,4 +1,5 @@ #include "qjs.h" +#include #include #include #include @@ -45,10 +46,14 @@ static uint8_t *detect_buf(JSContext *ctx, QJSEvalOptions *opts, size_t *buf_len /* Returns a thrown exception for an empty buffer. * If buf is non-NULL, this function returns JS_NULL. */ -static JSValue buf_empty_error(JSContext *ctx, uint8_t *buf, bool is_file, QJSEvalOptions *opts) +static JSValue buf_empty_error(JSContext *ctx, bool is_file, QJSEvalOptions *opts) { - if (buf != NULL) - return JS_NULL; // No error. + if (JS_HasException(ctx)) + return JS_Throw(ctx, JS_GetException(ctx)); + + if (errno != 0) + return JS_ThrowReferenceError(ctx, "%s: %s", strerror(errno), opts->filename); + if (is_file) { if (!opts->filename || opts->filename[0] == '\0') @@ -56,6 +61,7 @@ static JSValue buf_empty_error(JSContext *ctx, uint8_t *buf, bool is_file, QJSEv else return JS_ThrowReferenceError(ctx, "could not load file: %s", opts->filename); } + return JS_ThrowReferenceError(ctx, "in-memory buffer/bytecode is required for evaluation"); } @@ -65,20 +71,49 @@ static JSValue load_buf(JSContext *ctx, QJSEvalOptions opts, int flags, bool eva size_t buf_len = 0; uint8_t *buf = detect_buf(ctx, &opts, &buf_len, is_file); if (buf == NULL) - return buf_empty_error(ctx, buf, is_file, &opts); + return buf_empty_error(ctx, is_file, &opts); JSValue module_val; if (opts.bytecode_buf != NULL) if (!eval) + { module_val = JS_ReadObject(ctx, opts.bytecode_buf, opts.bytecode_len, JS_READ_OBJ_BYTECODE); + if (JS_IsException(module_val)) + { + if (is_file) + js_free(ctx, buf); + return JS_Throw(ctx, JS_GetException(ctx)); + } + } else { JSValue obj = JS_ReadObject(ctx, opts.bytecode_buf, opts.bytecode_len, JS_READ_OBJ_BYTECODE); + if (JS_IsException(obj)) + { + if (is_file) + js_free(ctx, buf); + return JS_Throw(ctx, JS_GetException(ctx)); + } module_val = JS_EvalFunction(ctx, obj); + if (JS_IsException(module_val)) + { + JS_FreeValue(ctx, obj); + if (is_file) + js_free(ctx, buf); + return JS_Throw(ctx, JS_GetException(ctx)); + } } else + { module_val = JS_Eval(ctx, (const char *)buf, buf_len, opts.filename, flags); + if (JS_IsException(module_val)) + { + if (is_file) + js_free(ctx, buf); + return JS_Throw(ctx, JS_GetException(ctx)); + } + } if (is_file) js_free(ctx, buf); @@ -131,6 +166,13 @@ static JSModuleDef *js_module_loader_json(JSContext *ctx, const char *module_nam /* Create the module source */ size_t source_len = strlen(module_source_template) + strlen(json_string) + 1; char *module_source = malloc(source_len); + if (!module_source) + { + JS_FreeCString(ctx, json_string); + JS_FreeValue(ctx, json_str); + JS_FreeValue(ctx, json_val); + return NULL; + } snprintf(module_source, source_len, module_source_template, json_string); /* Compile the module */ @@ -222,6 +264,9 @@ static JSValue qjs_eval_module(JSContext *ctx, QJSEvalOptions opts) result = js_std_await(ctx, result); js_std_loop(ctx); + if (JS_IsException(result)) + return JS_Throw(ctx, JS_GetException(ctx)); + JSModuleDef *module_def = JS_VALUE_GET_PTR(module_val); JSValue ns = JS_GetModuleNamespace(ctx, module_def); JSAtom default_atom = JS_NewAtom(ctx, "default"); @@ -355,7 +400,7 @@ QJSEvalOptions *QJS_CreateEvalOption(void *buf, uint8_t *bytecode_buf, size_t by QJSEvalOptions *opts = (QJSEvalOptions *)malloc(sizeof(QJSEvalOptions)); if (opts == NULL) { - printf("Error allocating memory for QJSEvalOptions\n"); + // Memory allocation failed - return NULL so caller can handle the error return NULL; } diff --git a/qjsextra/function.c b/qjsextra/function.c index c1882ef..19d67c8 100644 --- a/qjsextra/function.c +++ b/qjsextra/function.c @@ -22,7 +22,7 @@ JSValue InvokeFunctionProxy(JSContext *ctx, JSValueConst this_val, int argc, JSV #endif } -JSValue QJS_CreateProxyFunction(JSContext *ctx, uint64_t func_id, uint64_t ctx_id, uint64_t is_async) +JSValue QJS_CreateFunctionProxy(JSContext *ctx, uint64_t func_id, uint64_t ctx_id, uint64_t is_async) { // Create the C function binding that will be our proxy handler JSValue proxy = JS_NewCFunction( @@ -47,14 +47,14 @@ JSValue QJS_CreateProxyFunction(JSContext *ctx, uint64_t func_id, uint64_t ctx_i if (is_async == 0) { - proxy_content = "(proxy, fnHandler, ctx, is_async) => function(...args) { " + proxy_content = "(proxy, fnHandler, ctx, is_async) => function QJS_FunctionProxy (...args) { " " if (typeof proxy !== 'function') throw new TypeError('proxy is not a function'); " " return proxy.call(this, fnHandler, ctx, is_async, undefined, ...args); " "}"; } else { - proxy_content = "(proxy, fnHandler, ctx, is_async) => async function(...args) {" + proxy_content = "(proxy, fnHandler, ctx, is_async) => async function QJS_AsyncFunctionProxy (...args) {" " if (typeof proxy !== 'function') throw new TypeError('proxy is not a function'); " " let resolve, reject;" " const promise = new Promise((resolve_, reject_) => {" diff --git a/qjsextra/qjs.h b/qjsextra/qjs.h index 9ac537d..1978f7d 100644 --- a/qjsextra/qjs.h +++ b/qjsextra/qjs.h @@ -1,6 +1,6 @@ -#include "../quickjs/quickjs.h" -#include "../quickjs/quickjs-libc.h" -#include "../quickjs/cutils.h" +#include "./quickjs/quickjs.h" +#include "./quickjs/quickjs-libc.h" +#include "./quickjs/cutils.h" #include JSValue JS_NewNull(); @@ -117,7 +117,7 @@ bool QJS_IsArray(JSValue v); bool QJS_IsConstructor(JSContext *ctx, JSValue v); bool QJS_IsInstanceOf(JSContext *ctx, JSValue v, JSValue obj); JSValue QJS_NewString(JSContext *ctx, const char *str); -JSValue QJS_CreateProxyFunction(JSContext *ctx, uint64_t handle_id, uint64_t ctx_id, uint64_t is_async); +JSValue QJS_CreateFunctionProxy(JSContext *ctx, uint64_t handle_id, uint64_t ctx_id, uint64_t is_async); JSValue QJS_NewInt64(JSContext *ctx, int64_t val); JSValue QJS_NewUint32(JSContext *ctx, uint32_t val); JSValue QJS_NewInt32(JSContext *ctx, int32_t val); diff --git a/qjsextra/qjsextra.cmake b/qjsextra/qjsextra.cmake index 25fde58..dbaf008 100644 --- a/qjsextra/qjsextra.cmake +++ b/qjsextra/qjsextra.cmake @@ -75,7 +75,7 @@ target_link_options(qjsextra PRIVATE "LINKER:--export=JS_SetPropertyStr" # "LINKER:--export=JS_Call" "LINKER:--export=JS_CallConstructor" - "LINKER:--export=QJS_CreateProxyFunction" + "LINKER:--export=QJS_CreateFunctionProxy" "LINKER:--export=QJS_NewInt64" "LINKER:--export=QJS_NewUint32" "LINKER:--export=QJS_NewInt32" diff --git a/runtime.go b/runtime.go index eb4fb00..1513a92 100644 --- a/runtime.go +++ b/runtime.go @@ -87,10 +87,7 @@ func New(options ...*Option) (runtime *Runtime, err error) { proxyRegistry := NewProxyRegistry() - option, err := getRuntimeOption( - proxyRegistry, - options..., - ) + option, err := getRuntimeOption(proxyRegistry, options...) if err != nil { return nil, fmt.Errorf("failed to get runtime options: %w", err) } diff --git a/utils.go b/utils.go index 116f3cf..44892c4 100644 --- a/utils.go +++ b/utils.go @@ -19,7 +19,7 @@ func IsImplementError(rtype reflect.Type) bool { return rtype.Implements(reflect.TypeOf((*error)(nil)).Elem()) } -// Checks if a type implements json.Unmarshaler. +// IsImplementsJSONUnmarshaler checks if a type implements json.Unmarshaler. func IsImplementsJSONUnmarshaler(t reflect.Type) bool { unmarshalerType := reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() diff --git a/value.go b/value.go index 6f2d1a8..79569d0 100644 --- a/value.go +++ b/value.go @@ -33,8 +33,10 @@ type JSPropertyEnum struct { atom JSAtom } -// Object property names and some strings are stored as Atoms (unique strings) -// to save memory and allow fast comparison. +const jsPropertyEnumSize = uint32(unsafe.Sizeof(JSPropertyEnum{})) + +// Atom represents a JavaScript atom: +// Object property names and some strings are stored as Atoms (unique strings) to save memory and allow fast comparison. type Atom struct { *Value @@ -189,7 +191,15 @@ func (v *Value) Type() string { // Check Constructor before Function. if v.IsConstructor() { - return "Constructor" + name := v.GetPropertyStr("name") + defer name.Free() + + constructorName := "" + if name.IsString() && name.String() != "" { + constructorName = " " + name.String() + } + + return "Constructor" + constructorName } if v.IsFunction() { @@ -221,17 +231,26 @@ func (v *Value) GetOwnPropertyNames() (_ []string, err error) { } func (v *Value) GetOwnProperties() []OwnProperty { - ptr, size := v.context.CallUnPack("QJS_GetOwnPropertyNames", v.Ctx(), v.Raw()) - if size == 0 { + ptr, entriesCount := v.context.CallUnPack( + "QJS_GetOwnPropertyNames", + v.Ctx(), + v.Raw(), + ) + if entriesCount == 0 { return []OwnProperty{} } // Block size: number of entries * sizeof(JSPropertyEnum) - blockSize := size * uint32(unsafe.Sizeof(JSPropertyEnum{})) + blockSize := entriesCount * jsPropertyEnumSize bytes := v.context.MemRead(ptr, uint64(blockSize)) - // Convert the memory block into a slice of JSPropertyEnum. - entries := unsafe.Slice((*JSPropertyEnum)(unsafe.Pointer(&bytes[0])), size) + // SAFETY: This converts C memory layout to Go structs. + // The memory comes from QJS C code and matches JSPropertyEnum layout. + // This is safe because: + // 1. Memory size is validated (size * jsPropertyEnumSize) + // 2. JSPropertyEnum layout matches C struct layout + // 3. Memory lifetime is managed by context.FreeHandle() + entries := unsafe.Slice((*JSPropertyEnum)(unsafe.Pointer(&bytes[0])), entriesCount) property := make([]OwnProperty, len(entries)) @@ -578,7 +597,7 @@ func (v *Value) String() string { return result.handle.String() } -// JSONString returns the JSON string representation of the value. +// JSONStringify returns the JSON string representation of the value. func (v *Value) JSONStringify() (_ string, err error) { defer func() { r := AnyToError(recover()) @@ -593,7 +612,7 @@ func (v *Value) JSONStringify() (_ string, err error) { return result.handle.String(), nil } -// Date returns the date value of the value. +// DateTime returns the date value of the value. func (v *Value) DateTime(tzs ...string) *time.Time { var loc *time.Location if len(tzs) > 0 && tzs[0] != "" { @@ -628,8 +647,8 @@ func (v *Value) Bool() bool { return v.Call("JS_ToBool", v.Ctx(), v.Raw()).handle.Bool() } -// in c int is 32 bit, but in go it is depends on the architecture // Int32 returns the int32 value of the value. +// in c int is 32 bit, but in go it is depends on the architecture. func (v *Value) Int32() int32 { return v.Call("QJS_ToInt32", v.Ctx(), v.Raw()).handle.Int32() } @@ -686,7 +705,7 @@ func (v *Value) ToSet() *Set { return NewSet(v) } -// Call Class Constructor. +// New creates a new instance of the value as a constructor with the given arguments. func (v *Value) New(args ...*Value) *Value { return v.CallConstructor(args...) } diff --git a/value_test.go b/value_test.go index 859b4bd..cbcd1f0 100644 --- a/value_test.go +++ b/value_test.go @@ -3,6 +3,7 @@ package qjs_test import ( "errors" "math/big" + "slices" "testing" "unsafe" @@ -732,7 +733,7 @@ func TestValueType(t *testing.T) { {"Array", "[]", nil, "Array", nil}, {"Map", "new Map()", nil, "Map", nil}, {"Set", "new Set()", nil, "Set", nil}, - {"Constructor", "Array", nil, "Constructor", []string{"Function"}}, + {"Constructor", "Array", nil, "Constructor", []string{"Constructor Array"}}, {"Function", "(() => 42)", nil, "Function", []string{"Constructor"}}, {"ArrayBuffer", "new ArrayBuffer(8)", nil, "ArrayBuffer", nil}, @@ -756,13 +757,7 @@ func TestValueType(t *testing.T) { actualType := val.Type() validTypes := append([]string{tc.expectedType}, tc.allowedTypes...) - matched := false - for _, validType := range validTypes { - if actualType == validType { - matched = true - break - } - } + matched := slices.Contains(validTypes, actualType) assert.True(t, matched, "Code '%s' expected type in %v but got '%s'",