diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 0000000..85f4841 --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,119 @@ +name: Tests + +on: + pull_request: + branches: + - master + types: [opened, synchronize] + paths-ignore: + - '*.md' + workflow_dispatch: + +env: + GO111MODULE: "on" + +jobs: + test_cover: + name: Coverage + runs-on: ubuntu-latest + + env: + CGO_ENABLED: 0 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.26 + + - name: Write coverage profile + run: go test -v ./... -coverprofile=./coverage.txt -covermode=atomic + + - name: Upload coverage results to Codecov + uses: codecov/codecov-action@v2 + with: + fail_ci_if_error: false + path_to_write_report: ./coverage.txt + verbose: true + + tests: + name: Go + runs-on: ${{ matrix.os }} + strategy: + matrix: + go_versions: [ '1.25', '1.26' ] + os: [ubuntu-latest, windows-latest, macos-latest] + exclude: + # Only latest Go version for Windows and MacOS. + - os: windows-latest + go_versions: '1.25' + - os: macos-latest + go_versions: '1.25' + # Exclude latest Go version for Ubuntu as Coverage uses it. + - os: ubuntu-latest + go_versions: '1.26' + fail-fast: false + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '${{ matrix.go_versions }}' + + - name: Run tests + run: go test -v -race ./... + + lint: + name: Linter + uses: nspcc-dev/.github/.github/workflows/go-linter.yml@master + + codeql: + name: CodeQL + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/README.md b/README.md index c0b78e7..77b2460 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,18 @@ -go-ordered-json -=============== +.NET-compatible JSON library +============================ -There are some legacy/stupid applications[1] that you need to interoperate with, -and they for whatever reason require that the JSON you're using is ordered in -a particular way (contrary to the JSON specifications). +It's made for 100% compatibility with the JSON variation used by +[Neo blockchain](https://github.com/neo-project/). There are three problems +there: + * it's ordered (that's why it's a fork of [go-ordered-json](https://github.com/virtuald/go-ordered-json)) + * it has different conventions regarding control and "special" symbols + * it has different conventions wrt incorrect UTF-8 -Unfortunately, the golang authors are not willing to support such a broken use -case, so on [their advice](https://groups.google.com/forum/#!topic/golang-dev/zBQwhm3VfvU) -this is a fork of the golang encoding/json package, with the ordered JSON -support originating with a patch from -[Peter Waldschmidt](https://go-review.googlesource.com/c/7930/). +The primary user of this library is [NeoGo](https://github.com/nspcc-dev/neo-go/), +it has to be 100% compatible with C# implementation to correctly process +transactions, that's why we're maintaining this library and solving any +inconsistencies with .NET libraries if found. **If you can, you should avoid using this package**. However, if you can't avoid it, then you are welcome to. Provided under the MIT license, just like golang. - -Known broken applications -------------------------- - -* [1][Windows Communication Foundation Json __type ordering](https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/stand-alone-json-serialization#type-hint-position-in-json-objects) \ No newline at end of file diff --git a/bench_test.go b/bench_test.go index 85d7ae0..8d51375 100644 --- a/bench_test.go +++ b/bench_test.go @@ -13,7 +13,7 @@ package json import ( "bytes" "compress/gzip" - "io/ioutil" + "io" "os" "strings" "testing" @@ -47,7 +47,7 @@ func codeInit() { if err != nil { panic(err) } - data, err := ioutil.ReadAll(gz) + data, err := io.ReadAll(gz) if err != nil { panic(err) } @@ -83,7 +83,7 @@ func BenchmarkCodeEncoder(b *testing.B) { b.StartTimer() } b.RunParallel(func(pb *testing.PB) { - enc := NewEncoder(ioutil.Discard) + enc := NewEncoder(io.Discard) for pb.Next() { if err := enc.Encode(&codeStruct); err != nil { b.Fatal("Encode:", err) @@ -138,13 +138,13 @@ func BenchmarkDecoderStream(b *testing.B) { var buf bytes.Buffer dec := NewDecoder(&buf) buf.WriteString(`"` + strings.Repeat("x", 1000000) + `"` + "\n\n\n") - var x interface{} + var x any if err := dec.Decode(&x); err != nil { b.Fatal("Decode:", err) } ones := strings.Repeat(" 1\n", 300000) + "\n\n\n" b.StartTimer() - for i := 0; i < b.N; i++ { + for i := range b.N { if i%300000 == 0 { buf.WriteString(ones) } diff --git a/decode.go b/decode.go index 6dd5657..ac9bd4d 100644 --- a/decode.go +++ b/decode.go @@ -53,8 +53,8 @@ import ( // bool, for JSON booleans // float64, for JSON numbers // string, for JSON strings -// []interface{}, for JSON arrays -// map[string]interface{}, for JSON objects +// []any, for JSON arrays +// map[string]any, for JSON objects // nil for JSON null // // To unmarshal a JSON array into a slice, Unmarshal resets the slice length @@ -85,15 +85,14 @@ import ( // // The JSON null value unmarshals into an interface, map, pointer, or slice // by setting that Go value to nil. Because null is often used in JSON to mean -// ``not present,'' unmarshaling a JSON null into any other Go type has no effect +// “not present,” unmarshaling a JSON null into any other Go type has no effect // on the value and produces no error. // // When unmarshaling quoted strings, invalid UTF-8 or // invalid UTF-16 surrogate pairs are not treated as an error. // Instead, they are replaced by the Unicode replacement // character U+FFFD. -// -func Unmarshal(data []byte, v interface{}) error { +func Unmarshal(data []byte, v any) error { // Check for well-formedness. // Avoids filling out half a data structure // before discovering a JSON syntax error. @@ -166,7 +165,7 @@ func (e *InvalidUnmarshalError) Error() string { return "json: Unmarshal(nil " + e.Type.String() + ")" } -func (d *decodeState) unmarshal(v interface{}) (err error) { +func (d *decodeState) unmarshal(v any) (err error) { defer func() { if r := recover(); r != nil { if _, ok := r.(runtime.Error); ok { @@ -274,8 +273,8 @@ type decodeState struct { Struct string Field string } - savedError error - useNumber bool + savedError error + useNumber bool useOrderedObject bool } @@ -306,14 +305,14 @@ func (d *decodeState) saveError(err error) { } } -// addErrorContext returns a new error enhanced with information from d.errorContext +// addErrorContext returns a new error enhanced with information from d.errorContext. func (d *decodeState) addErrorContext(err error) error { if d.errorContext.Struct != "" || d.errorContext.Field != "" { - switch err := err.(type) { - case *UnmarshalTypeError: - err.Struct = d.errorContext.Struct - err.Field = d.errorContext.Field - return err + var e *UnmarshalTypeError + if errors.As(err, &e) { + e.Struct = d.errorContext.Struct + e.Field = d.errorContext.Field + return e } } return err @@ -375,7 +374,7 @@ func (d *decodeState) scanWhile(op int) int { // by different goroutines unmarshalling skipped fields. var ( discardObject = reflect.ValueOf(struct{}{}) - discardArray = reflect.ValueOf([0]interface{}{}) + discardArray = reflect.ValueOf([0]any{}) ) // value decodes a JSON value from d.data[d.off:] into the value. @@ -409,7 +408,7 @@ type unquotedValue struct{} // quoted string literal or literal null into an interface value. // If it finds anything other than a quoted string literal or null, // valueQuoted returns unquotedValue{}. -func (d *decodeState) valueQuoted() interface{} { +func (d *decodeState) valueQuoted() any { switch op := d.scanWhile(scanSkipSpace); op { default: d.error(errPhase) @@ -462,11 +461,11 @@ func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler, v.Set(reflect.New(v.Type().Elem())) } if v.Type().NumMethod() > 0 { - if u, ok := v.Interface().(Unmarshaler); ok { + if u, ok := reflect.TypeAssert[Unmarshaler](v); ok { return u, nil, reflect.Value{} } if !decodingNull { - if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { + if u, ok := reflect.TypeAssert[encoding.TextUnmarshaler](v); ok { return nil, u, reflect.Value{} } } @@ -533,10 +532,7 @@ func (d *decodeState) array(v reflect.Value) { if v.Kind() == reflect.Slice { // Grow slice if necessary if i >= v.Cap() { - newcap := v.Cap() + v.Cap()/2 - if newcap < 4 { - newcap = 4 - } + newcap := max(v.Cap()+v.Cap()/2, 4) newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) reflect.Copy(newv, v) v.Set(newv) @@ -582,7 +578,7 @@ func (d *decodeState) array(v reflect.Value) { } var nullLiteral = []byte("null") -var textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem() +var textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]() // object consumes an object from d.data[d.off-1:], decoding into the value v. // the first byte ('{') of the object has been read already. @@ -629,7 +625,7 @@ func (d *decodeState) object(v reflect.Value) { reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: default: - if !reflect.PtrTo(t.Key()).Implements(textUnmarshalerType) { + if !reflect.PointerTo(t.Key()).Implements(textUnmarshalerType) { d.saveError(&UnmarshalTypeError{Value: "object", Type: v.Type(), Offset: int64(d.off)}) d.off-- d.next() // skip over { } in input @@ -741,7 +737,7 @@ func (d *decodeState) object(v reflect.Value) { switch { case kt.Kind() == reflect.String: kv = reflect.ValueOf(key).Convert(kt) - case reflect.PtrTo(kt).Implements(textUnmarshalerType): + case reflect.PointerTo(kt).Implements(textUnmarshalerType): kv = reflect.New(v.Type().Key()) d.literalStore(item, kv, true) kv = kv.Elem() @@ -803,18 +799,18 @@ func (d *decodeState) literal(v reflect.Value) { // convertNumber converts the number literal s to a float64 or a Number // depending on the setting of d.useNumber. -func (d *decodeState) convertNumber(s string) (interface{}, error) { +func (d *decodeState) convertNumber(s string) (any, error) { if d.useNumber { return Number(s), nil } f, err := strconv.ParseFloat(s, 64) if err != nil { - return nil, &UnmarshalTypeError{Value: "number " + s, Type: reflect.TypeOf(0.0), Offset: int64(d.off)} + return nil, &UnmarshalTypeError{Value: "number " + s, Type: reflect.TypeFor[float64](), Offset: int64(d.off)} } return f, nil } -var numberType = reflect.TypeOf(Number("")) +var numberType = reflect.TypeFor[Number]() // literalStore decodes a literal stored in item into v. // @@ -884,6 +880,7 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: v.Set(reflect.Zero(v.Type())) // otherwise, ignore null for primitives/string + default: } case 't', 'f': // true, false value := item[0] == 't' @@ -1010,8 +1007,8 @@ func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool // in an empty interface. They are not strictly necessary, // but they avoid the weight of reflection in this common case. -// valueInterface is like value but returns interface{} -func (d *decodeState) valueInterface() interface{} { +// valueInterface is like value but returns any. +func (d *decodeState) valueInterface() any { switch d.scanWhile(scanSkipSpace) { default: d.error(errPhase) @@ -1025,9 +1022,9 @@ func (d *decodeState) valueInterface() interface{} { } } -// arrayInterface is like array but returns []interface{}. -func (d *decodeState) arrayInterface() []interface{} { - var v = make([]interface{}, 0) +// arrayInterface is like array but returns []any. +func (d *decodeState) arrayInterface() []any { + var v = make([]any, 0) for { // Look ahead for ] - can only happen on first iteration. op := d.scanWhile(scanSkipSpace) @@ -1053,9 +1050,9 @@ func (d *decodeState) arrayInterface() []interface{} { return v } -// objectInterface is like object but returns map[string]interface{} or []OrderedObject. -func (d *decodeState) objectInterface(forceOrderedObject bool) interface{} { - m := make(map[string]interface{}) +// objectInterface is like object but returns map[string]any or []OrderedObject. +func (d *decodeState) objectInterface(forceOrderedObject bool) any { + m := make(map[string]any) v := make(OrderedObject, 0) for { // Read opening " of string key or closing }. @@ -1104,13 +1101,12 @@ func (d *decodeState) objectInterface(forceOrderedObject bool) interface{} { if d.useOrderedObject || forceOrderedObject { return v - } else { - return m } + return m } // literalInterface is like literal but returns an interface value. -func (d *decodeState) literalInterface() interface{} { +func (d *decodeState) literalInterface() any { // All bytes inside literal return scanContinue op code. start := d.off - 1 op := d.scanWhile(scanContinue) diff --git a/decode_test.go b/decode_test.go index 67ab32a..f18eb55 100644 --- a/decode_test.go +++ b/decode_test.go @@ -31,7 +31,7 @@ type U struct { } type V struct { - F1 interface{} + F1 any F2 int32 F3 Number F4 *VOuter @@ -42,23 +42,23 @@ type VOuter struct { } // ifaceNumAsFloat64/ifaceNumAsNumber are used to test unmarshaling with and -// without UseNumber -var ifaceNumAsFloat64 = map[string]interface{}{ +// without UseNumber. +var ifaceNumAsFloat64 = map[string]any{ "k1": float64(1), "k2": "s", - "k3": []interface{}{float64(1), float64(2.0), float64(3e-3)}, - "k4": map[string]interface{}{"kk1": "s", "kk2": float64(2)}, + "k3": []any{float64(1), float64(2.0), float64(3e-3)}, + "k4": map[string]any{"kk1": "s", "kk2": float64(2)}, } -var ifaceNumAsNumber = map[string]interface{}{ +var ifaceNumAsNumber = map[string]any{ "k1": Number("1"), "k2": "s", - "k3": []interface{}{Number("1"), Number("2.0"), Number("3e-3")}, - "k4": map[string]interface{}{"kk1": "s", "kk2": Number("2")}, + "k3": []any{Number("1"), Number("2.0"), Number("3e-3")}, + "k4": map[string]any{"kk1": "s", "kk2": Number("2")}, } type tx struct { - x int + x int //nolint:unused // Used via reflection. } type u8 uint8 @@ -82,17 +82,17 @@ type unmarshalerText struct { A, B string } -// needed for re-marshaling tests +// MarshalText is needed for re-marshaling tests. func (u unmarshalerText) MarshalText() ([]byte, error) { return []byte(u.A + ":" + u.B), nil } func (u *unmarshalerText) UnmarshalText(b []byte) error { - pos := bytes.Index(b, []byte(":")) - if pos == -1 { + before, after, ok := bytes.Cut(b, []byte(":")) + if !ok { return errors.New("missing separator") } - u.A, u.B = string(b[:pos]), string(b[pos+1:]) + u.A, u.B = string(before), string(after) return nil } @@ -106,7 +106,7 @@ type ustructText struct { type u8marshal uint8 func (u8 u8marshal) MarshalText() ([]byte, error) { - return []byte(fmt.Sprintf("u%d", u8)), nil + return fmt.Appendf(nil, "u%d", u8), nil } var errMissingU8Prefix = errors.New("missing 'u' prefix") @@ -142,7 +142,7 @@ var ( umstructXY = ustructText{unmarshalerText{"x", "y"}} ummapType = map[unmarshalerText]bool{} - ummapXY = map[unmarshalerText]bool{unmarshalerText{"x", "y"}: true} + ummapXY = map[unmarshalerText]bool{{"x", "y"}: true} ) // Test data structures for anonymous fields. @@ -251,9 +251,9 @@ type Ambig struct { } type XYZ struct { - X interface{} - Y interface{} - Z interface{} + X any + Y any + Z any } func sliceAddr(x []int) *[]int { return &x } @@ -262,7 +262,7 @@ func mapAddr(x map[string]int) *map[string]int { return &x } type byteWithMarshalJSON byte func (b byteWithMarshalJSON) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`"Z%.2x"`, byte(b))), nil + return fmt.Appendf(nil, `"Z%.2x"`, byte(b)), nil } func (b *byteWithMarshalJSON) UnmarshalJSON(data []byte) error { @@ -290,7 +290,7 @@ func (b *byteWithPtrMarshalJSON) UnmarshalJSON(data []byte) error { type byteWithMarshalText byte func (b byteWithMarshalText) MarshalText() ([]byte, error) { - return []byte(fmt.Sprintf(`Z%.2x`, byte(b))), nil + return fmt.Appendf(nil, `Z%.2x`, byte(b)), nil } func (b *byteWithMarshalText) UnmarshalText(data []byte) error { @@ -318,7 +318,7 @@ func (b *byteWithPtrMarshalText) UnmarshalText(data []byte) error { type intWithMarshalJSON int func (b intWithMarshalJSON) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`"Z%.2x"`, int(b))), nil + return fmt.Appendf(nil, `"Z%.2x"`, int(b)), nil } func (b *intWithMarshalJSON) UnmarshalJSON(data []byte) error { @@ -346,7 +346,7 @@ func (b *intWithPtrMarshalJSON) UnmarshalJSON(data []byte) error { type intWithMarshalText int func (b intWithMarshalText) MarshalText() ([]byte, error) { - return []byte(fmt.Sprintf(`Z%.2x`, int(b))), nil + return fmt.Appendf(nil, `Z%.2x`, int(b)), nil } func (b *intWithMarshalText) UnmarshalText(data []byte) error { @@ -372,12 +372,12 @@ func (b *intWithPtrMarshalText) UnmarshalText(data []byte) error { } type unmarshalTest struct { - in string - ptr interface{} - out interface{} - err error - useNumber bool - golden bool + in string + ptr any + out any + err error + useNumber bool + golden bool useOrderedObject bool } @@ -393,19 +393,19 @@ var unmarshalTests = []unmarshalTest{ {in: `-5`, ptr: new(int16), out: int16(-5)}, {in: `2`, ptr: new(Number), out: Number("2"), useNumber: true}, {in: `2`, ptr: new(Number), out: Number("2")}, - {in: `2`, ptr: new(interface{}), out: float64(2.0)}, - {in: `2`, ptr: new(interface{}), out: Number("2"), useNumber: true}, + {in: `2`, ptr: new(any), out: float64(2.0)}, + {in: `2`, ptr: new(any), out: Number("2"), useNumber: true}, {in: `"a\u1234"`, ptr: new(string), out: "a\u1234"}, {in: `"http:\/\/"`, ptr: new(string), out: "http://"}, {in: `"g-clef: \uD834\uDD1E"`, ptr: new(string), out: "g-clef: \U0001D11E"}, {in: `"invalid: \uD834x\uDD1E"`, ptr: new(string), out: "invalid: \uFFFDx\uFFFD"}, - {in: "null", ptr: new(interface{}), out: nil}, - {in: `{"X": [1,2,3], "Y": 4}`, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{"array", reflect.TypeOf(""), 7, "T", "X"}}, + {in: "null", ptr: new(any), out: nil}, + {in: `{"X": [1,2,3], "Y": 4}`, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{"array", reflect.TypeFor[string](), 7, "T", "X"}}, {in: `{"x": 1}`, ptr: new(tx), out: tx{}}, {in: `{"F1":1,"F2":2,"F3":3}`, ptr: new(V), out: V{F1: float64(1), F2: int32(2), F3: Number("3")}}, {in: `{"F1":1,"F2":2,"F3":3}`, ptr: new(V), out: V{F1: Number("1"), F2: int32(2), F3: Number("3")}, useNumber: true}, - {in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(interface{}), out: ifaceNumAsFloat64}, - {in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(interface{}), out: ifaceNumAsNumber, useNumber: true}, + {in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(any), out: ifaceNumAsFloat64}, + {in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(any), out: ifaceNumAsNumber, useNumber: true}, // raw values with whitespace {in: "\n true ", ptr: new(bool), out: true}, @@ -442,10 +442,10 @@ var unmarshalTests = []unmarshalTest{ {in: `[1, 2, 3]`, ptr: new([5]int), out: [5]int{1, 2, 3, 0, 0}}, // empty array to interface test - {in: `[]`, ptr: new([]interface{}), out: []interface{}{}}, - {in: `null`, ptr: new([]interface{}), out: []interface{}(nil)}, - {in: `{"T":[]}`, ptr: new(map[string]interface{}), out: map[string]interface{}{"T": []interface{}{}}}, - {in: `{"T":null}`, ptr: new(map[string]interface{}), out: map[string]interface{}{"T": interface{}(nil)}}, + {in: `[]`, ptr: new([]any), out: []any{}}, + {in: `null`, ptr: new([]any), out: []any(nil)}, + {in: `{"T":[]}`, ptr: new(map[string]any), out: map[string]any{"T": []any{}}}, + {in: `{"T":null}`, ptr: new(map[string]any), out: map[string]any{"T": any(nil)}}, // composite tests {in: allValueIndent, ptr: new(All), out: allValue}, @@ -515,22 +515,22 @@ var unmarshalTests = []unmarshalTest{ { in: `{"abc":"abc"}`, ptr: new(map[int]string), - err: &UnmarshalTypeError{Value: "number abc", Type: reflect.TypeOf(0), Offset: 2}, + err: &UnmarshalTypeError{Value: "number abc", Type: reflect.TypeFor[int](), Offset: 2}, }, { in: `{"256":"abc"}`, ptr: new(map[uint8]string), - err: &UnmarshalTypeError{Value: "number 256", Type: reflect.TypeOf(uint8(0)), Offset: 2}, + err: &UnmarshalTypeError{Value: "number 256", Type: reflect.TypeFor[uint8](), Offset: 2}, }, { in: `{"128":"abc"}`, ptr: new(map[int8]string), - err: &UnmarshalTypeError{Value: "number 128", Type: reflect.TypeOf(int8(0)), Offset: 2}, + err: &UnmarshalTypeError{Value: "number 128", Type: reflect.TypeFor[int8](), Offset: 2}, }, { in: `{"-1":"abc"}`, ptr: new(map[uint8]string), - err: &UnmarshalTypeError{Value: "number -1", Type: reflect.TypeOf(uint8(0)), Offset: 2}, + err: &UnmarshalTypeError{Value: "number -1", Type: reflect.TypeFor[uint8](), Offset: 2}, }, // Map keys can be encoding.TextUnmarshalers. @@ -664,12 +664,12 @@ var unmarshalTests = []unmarshalTest{ { in: `{"2009-11-10T23:00:00Z": "hello world"}`, ptr: &map[Point]string{}, - err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeOf(map[Point]string{}), Offset: 1}, + err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeFor[map[Point]string](), Offset: 1}, }, { in: `{"asdf": "hello world"}`, ptr: &map[unmarshaler]string{}, - err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeOf(map[unmarshaler]string{}), Offset: 1}, + err: &UnmarshalTypeError{Value: "object", Type: reflect.TypeFor[map[unmarshaler]string](), Offset: 1}, }, // related to issue 13783. @@ -769,7 +769,7 @@ var unmarshalTests = []unmarshalTest{ Value: "string", Struct: "V", Field: "F2", - Type: reflect.TypeOf(int32(0)), + Type: reflect.TypeFor[int32](), Offset: 20, }, }, @@ -780,7 +780,7 @@ var unmarshalTests = []unmarshalTest{ Value: "string", Struct: "V", Field: "F2", - Type: reflect.TypeOf(int32(0)), + Type: reflect.TypeFor[int32](), Offset: 30, }, }, @@ -795,27 +795,27 @@ var unmarshalTests = []unmarshalTest{ {in: `{"B": "null"}`, ptr: new(B), out: B{false}}, {in: `{"B": "nul"}`, ptr: new(B), err: errors.New(`json: invalid use of ,string struct tag, trying to unmarshal "nul" into bool`)}, - // OrderedObject tests - into interface{} - {in: `{"a":"1","b":2,"c":3}`, ptr: new(interface{}), useOrderedObject: true, + // OrderedObject tests - into any + {in: `{"a":"1","b":2,"c":3}`, ptr: new(any), useOrderedObject: true, out: OrderedObject{{"a", "1"}, {"b", float64(2)}, {"c", float64(3)}}}, - {in: `[{"a":"1","b":2}]`, ptr: new(interface{}), useOrderedObject: true, - out: []interface{}{OrderedObject{{"a", "1"}, {"b", float64(2)}}}}, - {in: `{"a":null,"b": {"c":true} }`, ptr: new(interface{}), useOrderedObject: true, + {in: `[{"a":"1","b":2}]`, ptr: new(any), useOrderedObject: true, + out: []any{OrderedObject{{"a", "1"}, {"b", float64(2)}}}}, + {in: `{"a":null,"b": {"c":true} }`, ptr: new(any), useOrderedObject: true, out: OrderedObject{{"a", nil}, {"b", OrderedObject{{"c", true}}}}}, - {in: `{"T":[]}`, ptr: new(interface{}), useOrderedObject: true, - out: OrderedObject{{Key: "T", Value: []interface{}{}}}}, - {in: `{"T":null}`, ptr: new(interface{}), useOrderedObject: true, + {in: `{"T":[]}`, ptr: new(any), useOrderedObject: true, + out: OrderedObject{{Key: "T", Value: []any{}}}}, + {in: `{"T":null}`, ptr: new(any), useOrderedObject: true, out: OrderedObject{{Key: "T", Value: nil}}}, - // OrderedObject tests - into map[string]interface{} - {in: `{"a":"1","b":2,"c":3}`, ptr: new(map[string]interface{}), useOrderedObject: true, - out: map[string]interface{}{"a": "1", "b": float64(2), "c": float64(3)}}, - {in: `{"a":null,"b": {"c":true} }`, ptr: new(map[string]interface{}), useOrderedObject: true, - out: map[string]interface{}{"a": nil, "b": OrderedObject{{"c", true}}}}, - {in: `{"T":[]}`, ptr: new(map[string]interface{}), useOrderedObject: true, - out: map[string]interface{}{"T": []interface{}{}}}, - {in: `{"T":null}`, ptr: new(map[string]interface{}), useOrderedObject: true, - out: map[string]interface{}{"T": nil}}, + // OrderedObject tests - into map[string]any + {in: `{"a":"1","b":2,"c":3}`, ptr: new(map[string]any), useOrderedObject: true, + out: map[string]any{"a": "1", "b": float64(2), "c": float64(3)}}, + {in: `{"a":null,"b": {"c":true} }`, ptr: new(map[string]any), useOrderedObject: true, + out: map[string]any{"a": nil, "b": OrderedObject{{"c", true}}}}, + {in: `{"T":[]}`, ptr: new(map[string]any), useOrderedObject: true, + out: map[string]any{"T": []any{}}}, + {in: `{"T":null}`, ptr: new(map[string]any), useOrderedObject: true, + out: map[string]any{"T": nil}}, // OrderedObject tests - into OrderedObject {in: `{"a":"1","b":2,"c":3}`, ptr: new(OrderedObject), useOrderedObject: true, @@ -826,7 +826,7 @@ var unmarshalTests = []unmarshalTest{ {in: `{"a":null,"b": {"c":true} }`, ptr: new(OrderedObject), useOrderedObject: true, out: OrderedObject{{"a", nil}, {"b", OrderedObject{{"c", true}}}}}, {in: `{"a":null,"b": {"c":true} }`, ptr: new(OrderedObject), - out: OrderedObject{{"a", nil}, {"b", map[string]interface{}{"c": true}}}}, + out: OrderedObject{{"a", nil}, {"b", map[string]any{"c": true}}}}, // OrderedObject tests -into []OrderedObject {in: `[{"a":"1","b":2},{"c":3}]`, ptr: &[]OrderedObject{}, @@ -858,12 +858,12 @@ func TestMarshal(t *testing.T) { var badUTF8 = []struct { in, out string }{ - {"hello\xffworld", `"hello\ufffdworld"`}, + {"hello\xffworld", `"hello\u00FFworld"`}, {"", `""`}, - {"\xff", `"\ufffd"`}, - {"\xff\xff", `"\ufffd\ufffd"`}, - {"a\xffb", `"a\ufffdb"`}, - {"\xe6\x97\xa5\xe6\x9c\xac\xff\xaa\x9e", `"日本\ufffd\ufffd\ufffd"`}, + {"\xff", `"\u00FF"`}, + {"\xff\xff", `"\u00FF\u00FF"`}, + {"a\xffb", `"a\u00FFb"`}, + {"\xe6\x97\xa5\xe6\x9c\xac\xff\xaa\x9e", `"\u65E5\u672C\u00FF\u00AA\u009E"`}, } func TestMarshalBadUTF8(t *testing.T) { @@ -1001,7 +1001,7 @@ func TestUnmarshal(t *testing.T) { func TestUnmarshalMarshal(t *testing.T) { initBig() - var v interface{} + var v any if err := Unmarshal(jsonBig, &v); err != nil { t.Fatalf("Unmarshal: %v", err) } @@ -1028,7 +1028,7 @@ var numberTests = []struct { {in: "1e1000", intErr: "strconv.ParseInt: parsing \"1e1000\": invalid syntax", floatErr: "strconv.ParseFloat: parsing \"1e1000\": value out of range"}, } -// Independent of Decode, basic coverage of the accessors in Number +// Independent of Decode, basic coverage of the accessors in Number. func TestNumberAccessors(t *testing.T) { for _, tt := range numberTests { n := Number(tt.in) @@ -1073,7 +1073,7 @@ type Xint struct { func TestUnmarshalInterface(t *testing.T) { var xint Xint - var i interface{} = &xint + var i any = &xint if err := Unmarshal([]byte(`{"X":1}`), &i); err != nil { t.Fatalf("Unmarshal: %v", err) } @@ -1095,7 +1095,7 @@ func TestUnmarshalPtrPtr(t *testing.T) { func TestEscape(t *testing.T) { const input = `"foobar"` + " [\u2028 \u2029]" - const expected = `"\"foobar\"\u003chtml\u003e [\u2028 \u2029]"` + const expected = `"\u0022foobar\u0022\u003Chtml\u003E [\u2028 \u2029]"` b, err := Marshal(input) if err != nil { t.Fatalf("Marshal error: %v", err) @@ -1159,7 +1159,7 @@ type All struct { Float64 float64 Foo string `json:"bar"` - Foo2 string `json:"bar2,dummyopt"` + Foo2 string `json:"bar2,dummyopt"` //nolint:staticcheck // It's intentionally wrong. IntStr int64 `json:",string"` @@ -1204,10 +1204,10 @@ type All struct { PSmall *Small PPSmall **Small - Interface interface{} - PInterface *interface{} + Interface any + PInterface *any - unexported int + unexported int //nolint:unused // Not really used, but needed for test. } type Small struct { @@ -1483,7 +1483,7 @@ func TestRefUnmarshal(t *testing.T) { } // Test that the empty string doesn't panic decoding when ,string is specified -// Issue 3450 +// Issue 3450. func TestEmptyString(t *testing.T) { type T2 struct { Number1 int `json:",string"` @@ -1536,9 +1536,9 @@ func intpp(x *int) **int { } var interfaceSetTests = []struct { - pre interface{} + pre any json string - post interface{} + post any }{ {"foo", `"bar"`, "bar"}, {"foo", `2`, 2.0}, @@ -1557,7 +1557,7 @@ var interfaceSetTests = []struct { func TestInterfaceSet(t *testing.T) { for _, tt := range interfaceSetTests { - b := struct{ X interface{} }{tt.pre} + b := struct{ X any }{tt.pre} blob := `{"X":` + tt.json + `}` if err := Unmarshal([]byte(blob), &b); err != nil { t.Errorf("Unmarshal %#q: %v", blob, err) @@ -1587,7 +1587,7 @@ type NullTest struct { PBool *bool Map map[string]string Slice []string - Interface interface{} + Interface any PRaw *RawMessage PTime *time.Time @@ -1620,27 +1620,27 @@ type NullTestStrings struct { Float64 float64 `json:",string"` String string `json:",string"` PBool *bool `json:",string"` - Map map[string]string `json:",string"` - Slice []string `json:",string"` - Interface interface{} `json:",string"` + Map map[string]string `json:",string"` //nolint:staticcheck // It's intentionally wrong. + Slice []string `json:",string"` //nolint:staticcheck // It's intentionally wrong. + Interface any `json:",string"` //nolint:staticcheck // It's intentionally wrong. - PRaw *RawMessage `json:",string"` - PTime *time.Time `json:",string"` - PBigInt *big.Int `json:",string"` - PText *MustNotUnmarshalText `json:",string"` - PBuffer *bytes.Buffer `json:",string"` - PStruct *struct{} `json:",string"` + PRaw *RawMessage `json:",string"` //nolint:staticcheck // It's intentionally wrong. + PTime *time.Time `json:",string"` //nolint:staticcheck // It's intentionally wrong. + PBigInt *big.Int `json:",string"` //nolint:staticcheck // It's intentionally wrong. + PText *MustNotUnmarshalText `json:",string"` //nolint:staticcheck // It's intentionally wrong. + PBuffer *bytes.Buffer `json:",string"` //nolint:staticcheck // It's intentionally wrong. + PStruct *struct{} `json:",string"` //nolint:staticcheck // It's intentionally wrong. - Raw RawMessage `json:",string"` - Time time.Time `json:",string"` - BigInt big.Int `json:",string"` - Text MustNotUnmarshalText `json:",string"` - Buffer bytes.Buffer `json:",string"` - Struct struct{} `json:",string"` + Raw RawMessage `json:",string"` //nolint:staticcheck // It's intentionally wrong. + Time time.Time `json:",string"` //nolint:staticcheck // It's intentionally wrong. + BigInt big.Int `json:",string"` //nolint:staticcheck // It's intentionally wrong. + Text MustNotUnmarshalText `json:",string"` //nolint:staticcheck // It's intentionally wrong. + Buffer bytes.Buffer `json:",string"` //nolint:staticcheck // It's intentionally wrong. + Struct struct{} `json:",string"` //nolint:staticcheck // It's intentionally wrong. } // JSON null values should be ignored for primitives and string values instead of resulting in an error. -// Issue 2540 +// Issue 2540. func TestUnmarshalNulls(t *testing.T) { // Unmarshal docs: // The JSON null value unmarshals into an interface, map, pointer, or slice @@ -1843,7 +1843,7 @@ func TestSliceOfCustomByte(t *testing.T) { } var decodeTypeErrorTests = []struct { - dest interface{} + dest any src string }{ {new(string), `{"user": "name"}`}, // issue 4628. @@ -1857,7 +1857,7 @@ var decodeTypeErrorTests = []struct { func TestUnmarshalTypeError(t *testing.T) { for _, item := range decodeTypeErrorTests { err := Unmarshal([]byte(item.src), item.dest) - if _, ok := err.(*UnmarshalTypeError); !ok { + if _, ok := err.(*UnmarshalTypeError); !ok { //nolint:errorlint // It must match exactly, it's a test. t.Errorf("expected type error for Unmarshal(%q, type %T): got %T", item.src, item.dest, err) } @@ -1876,21 +1876,21 @@ var unmarshalSyntaxTests = []string{ } func TestUnmarshalSyntax(t *testing.T) { - var x interface{} + var x any for _, src := range unmarshalSyntaxTests { err := Unmarshal([]byte(src), &x) - if _, ok := err.(*SyntaxError); !ok { + if _, ok := err.(*SyntaxError); !ok { //nolint:errorlint // It must match exactly, it's a test. t.Errorf("expected syntax error for Unmarshal(%q): got %T", src, err) } } } // Test handling of unexported fields that should be ignored. -// Issue 4660 +// Issue 4660. type unexportedFields struct { Name string - m map[string]interface{} `json:"-"` - m2 map[string]interface{} `json:"abcd"` + m map[string]any `json:"-"` //nolint:unused // Not really used, but important for test. + m2 map[string]any `json:"abcd"` //nolint:unused,govet,staticcheck // Not really used and wrong, but important for test. } func TestUnmarshalUnexported(t *testing.T) { @@ -1936,10 +1936,10 @@ func TestUnmarshalJSONLiteralError(t *testing.T) { // Test that extra object elements in an array do not result in a // "data changing underfoot" error. -// Issue 3717 +// Issue 3717. func TestSkipArrayObjects(t *testing.T) { json := `[{}]` - var dest [0]interface{} + var dest [0]any err := Unmarshal([]byte(json), &dest) if err != nil { @@ -1950,13 +1950,13 @@ func TestSkipArrayObjects(t *testing.T) { // Test semantics of pre-filled struct fields and pre-filled map fields. // Issue 4900. func TestPrefilled(t *testing.T) { - ptrToMap := func(m map[string]interface{}) *map[string]interface{} { return &m } + ptrToMap := func(m map[string]any) *map[string]any { return &m } // Values here change, cannot reuse table across runs. var prefillTests = []struct { in string - ptr interface{} - out interface{} + ptr any + out any }{ { in: `{"X": 1, "Y": 2}`, @@ -1965,8 +1965,8 @@ func TestPrefilled(t *testing.T) { }, { in: `{"X": 1, "Y": 2}`, - ptr: ptrToMap(map[string]interface{}{"X": float32(3), "Y": int16(4), "Z": 1.5}), - out: ptrToMap(map[string]interface{}{"X": float64(1), "Y": float64(2), "Z": 1.5}), + ptr: ptrToMap(map[string]any{"X": float32(3), "Y": int16(4), "Z": 1.5}), + out: ptrToMap(map[string]any{"X": float64(1), "Y": float64(2), "Z": 1.5}), }, } @@ -1983,7 +1983,7 @@ func TestPrefilled(t *testing.T) { } var invalidUnmarshalTests = []struct { - v interface{} + v any want string }{ {nil, "json: Unmarshal(nil)"}, @@ -2006,7 +2006,7 @@ func TestInvalidUnmarshal(t *testing.T) { } var invalidUnmarshalTextTests = []struct { - v interface{} + v any want string }{ {nil, "json: Unmarshal(nil)"}, @@ -2034,12 +2034,12 @@ func TestInvalidUnmarshalText(t *testing.T) { func TestInvalidStringOption(t *testing.T) { num := 0 item := struct { - T time.Time `json:",string"` - M map[string]string `json:",string"` - S []string `json:",string"` - A [1]string `json:",string"` - I interface{} `json:",string"` - P *int `json:",string"` + T time.Time `json:",string"` //nolint:staticcheck // It's intentionally wrong. + M map[string]string `json:",string"` //nolint:staticcheck // It's intentionally wrong. + S []string `json:",string"` //nolint:staticcheck // It's intentionally wrong. + A [1]string `json:",string"` //nolint:staticcheck // It's intentionally wrong. + I any `json:",string"` //nolint:staticcheck // It's intentionally wrong. + P *int `json:",string"` //nolint:staticcheck // It's intentionally wrong. }{M: make(map[string]string), S: make([]string, 0), I: num, P: &num} data, err := Marshal(item) diff --git a/encode.go b/encode.go index f3e86aa..c2f4c28 100644 --- a/encode.go +++ b/encode.go @@ -15,18 +15,19 @@ import ( "encoding" "encoding/base64" "fmt" + "maps" "math" "reflect" "runtime" + "slices" "sort" "strconv" "strings" "sync" "sync/atomic" "unicode" + "unicode/utf16" "unicode/utf8" - // new in golang 1.9 - "golang.org/x/sync/syncmap" ) // Marshal returns the JSON encoding of v. @@ -80,31 +81,31 @@ import ( // // Examples of struct field tags and their meanings: // -// // Field appears in JSON as key "myName". -// Field int `json:"myName"` +// // Field appears in JSON as key "myName". +// Field int `json:"myName"` // -// // Field appears in JSON as key "myName" and -// // the field is omitted from the object if its value is empty, -// // as defined above. -// Field int `json:"myName,omitempty"` +// // Field appears in JSON as key "myName" and +// // the field is omitted from the object if its value is empty, +// // as defined above. +// Field int `json:"myName,omitempty"` // -// // Field appears in JSON as key "Field" (the default), but -// // the field is skipped if empty. -// // Note the leading comma. -// Field int `json:",omitempty"` +// // Field appears in JSON as key "Field" (the default), but +// // the field is skipped if empty. +// // Note the leading comma. +// Field int `json:",omitempty"` // -// // Field is ignored by this package. -// Field int `json:"-"` +// // Field is ignored by this package. +// Field int `json:"-"` // -// // Field appears in JSON as key "-". -// Field int `json:"-,"` +// // Field appears in JSON as key "-". +// Field int `json:"-,"` // // The "string" option signals that a field is stored as JSON inside a // JSON-encoded string. It applies only to fields of string, floating point, // integer, or boolean types. This extra level of encoding is sometimes used // when communicating with JavaScript programs: // -// Int64String int64 `json:",string"` +// Int64String int64 `json:",string"` // // The key name will be used if it's a non-empty string consisting of // only Unicode letters, digits, and ASCII punctuation except quotation @@ -160,8 +161,7 @@ import ( // JSON cannot represent cyclic data structures and Marshal does not // handle them. Passing cyclic structures to Marshal will result in // an infinite recursion. -// -func Marshal(v interface{}) ([]byte, error) { +func Marshal(v any) ([]byte, error) { e := &encodeState{} err := e.marshal(v, encOpts{escapeHTML: true}) if err != nil { @@ -171,7 +171,7 @@ func Marshal(v interface{}) ([]byte, error) { } // MarshalIndent is like Marshal but applies Indent to format the output. -func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { +func MarshalIndent(v any, prefix, indent string) ([]byte, error) { b, err := Marshal(v) if err != nil { return nil, err @@ -267,7 +267,7 @@ func (e *MarshalerError) Error() string { return "json: error calling MarshalJSON for type " + e.Type.String() + ": " + e.Err.Error() } -var hex = "0123456789abcdef" +var hex = "0123456789ABCDEF" // An encodeState encodes JSON into a bytes.Buffer. type encodeState struct { @@ -286,7 +286,7 @@ func newEncodeState() *encodeState { return new(encodeState) } -func (e *encodeState) marshal(v interface{}, opts encOpts) (err error) { +func (e *encodeState) marshal(v any, opts encOpts) (err error) { defer func() { if r := recover(); r != nil { if _, ok := r.(runtime.Error); ok { @@ -320,6 +320,7 @@ func isEmptyValue(v reflect.Value) bool { return v.Float() == 0 case reflect.Interface, reflect.Ptr: return v.IsNil() + default: } return false } @@ -337,7 +338,7 @@ type encOpts struct { type encoderFunc func(e *encodeState, v reflect.Value, opts encOpts) -var encoderCache syncmap.Map // map[reflect.Type]encoderFunc +var encoderCache sync.Map // map[reflect.Type]encoderFunc func valueEncoder(v reflect.Value) encoderFunc { if !v.IsValid() { @@ -376,9 +377,9 @@ func typeEncoder(t reflect.Type) encoderFunc { } var ( - marshalerType = reflect.TypeOf(new(Marshaler)).Elem() - textMarshalerType = reflect.TypeOf(new(encoding.TextMarshaler)).Elem() - orderedObjectType = reflect.TypeOf(new(OrderedObject)).Elem() + marshalerType = reflect.TypeFor[Marshaler]() + textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]() + orderedObjectType = reflect.TypeFor[OrderedObject]() ) // newTypeEncoder constructs an encoderFunc for a type. @@ -388,7 +389,7 @@ func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc { return marshalerEncoder } if t.Kind() != reflect.Ptr && allowAddr { - if reflect.PtrTo(t).Implements(marshalerType) { + if reflect.PointerTo(t).Implements(marshalerType) { return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false)) } } @@ -397,7 +398,7 @@ func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc { return textMarshalerEncoder } if t.Kind() != reflect.Ptr && allowAddr { - if reflect.PtrTo(t).Implements(textMarshalerType) { + if reflect.PointerTo(t).Implements(textMarshalerType) { return newCondAddrEncoder(addrTextMarshalerEncoder, newTypeEncoder(t, false)) } } @@ -445,7 +446,7 @@ func marshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { e.WriteString("null") return } - m, ok := v.Interface().(Marshaler) + m, ok := reflect.TypeAssert[Marshaler](v) if !ok { e.WriteString("null") return @@ -466,7 +467,7 @@ func addrMarshalerEncoder(e *encodeState, v reflect.Value, _ encOpts) { e.WriteString("null") return } - m := va.Interface().(Marshaler) + m, _ := reflect.TypeAssert[Marshaler](va) b, err := m.MarshalJSON() if err == nil { // copy JSON into buffer, checking validity. @@ -482,7 +483,7 @@ func textMarshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { e.WriteString("null") return } - m := v.Interface().(encoding.TextMarshaler) + m, _ := reflect.TypeAssert[encoding.TextMarshaler](v) b, err := m.MarshalText() if err != nil { e.error(&MarshalerError{v.Type(), err}) @@ -496,7 +497,7 @@ func addrTextMarshalerEncoder(e *encodeState, v reflect.Value, opts encOpts) { e.WriteString("null") return } - m := va.Interface().(encoding.TextMarshaler) + m, _ := reflect.TypeAssert[encoding.TextMarshaler](va) b, err := m.MarshalText() if err != nil { e.error(&MarshalerError{v.Type(), err}) @@ -714,7 +715,7 @@ func orderedObjectEncoder(e *encodeState, v reflect.Value, opts encOpts) { return } e.WriteByte('{') - var ov OrderedObject = v.Interface().(OrderedObject) + var ov, _ = reflect.TypeAssert[OrderedObject](v) for i, o := range ov { if i > 0 { e.WriteByte(',') @@ -742,7 +743,10 @@ func encodeByteSlice(e *encodeState, v reflect.Value, _ encOpts) { // for large buffers, avoid unnecessary extra temporary // buffer space. enc := base64.NewEncoder(base64.StdEncoding, e) - enc.Write(s) + _, err := enc.Write(s) + if err != nil { + panic(err) + } enc.Close() } e.WriteByte('"') @@ -764,7 +768,7 @@ func (se *sliceEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) { func newSliceEncoder(t reflect.Type) encoderFunc { // Byte slices get special treatment; arrays don't. if t.Elem().Kind() == reflect.Uint8 { - p := reflect.PtrTo(t.Elem()) + p := reflect.PointerTo(t.Elem()) if !p.Implements(marshalerType) && !p.Implements(textMarshalerType) { return encodeByteSlice } @@ -780,7 +784,7 @@ type arrayEncoder struct { func (ae *arrayEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) { e.WriteByte('[') n := v.Len() - for i := 0; i < n; i++ { + for i := range n { if i > 0 { e.WriteByte(',') } @@ -882,7 +886,7 @@ func (w *reflectWithString) resolve() error { w.s = w.v.String() return nil } - if tm, ok := w.v.Interface().(encoding.TextMarshaler); ok { + if tm, ok := reflect.TypeAssert[encoding.TextMarshaler](w.v); ok { buf, err := tm.MarshalText() w.s = string(buf) return err @@ -894,6 +898,7 @@ func (w *reflectWithString) resolve() error { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: w.s = strconv.FormatUint(w.v.Uint(), 10) return nil + default: } panic("unexpected map key type") } @@ -913,12 +918,18 @@ func (e *encodeState) string(s string, escapeHTML bool) int { e.WriteString(s[start:i]) } switch b { - case '\\', '"': + case '\\': e.WriteByte('\\') e.WriteByte(b) + case 0x08: + e.WriteByte('\\') + e.WriteByte('b') case '\n': e.WriteByte('\\') e.WriteByte('n') + case 0x0c: + e.WriteByte('\\') + e.WriteByte('f') case '\r': e.WriteByte('\\') e.WriteByte('r') @@ -926,7 +937,7 @@ func (e *encodeState) string(s string, escapeHTML bool) int { e.WriteByte('\\') e.WriteByte('t') default: - // This encodes bytes < 0x20 except for \t, \n and \r. + // This encodes bytes < 0x20 and " except for \t, \n and \r. // If escapeHTML is set, it also escapes <, >, and & // because they can lead to security holes when // user-controlled strings are rendered into JSON @@ -944,7 +955,9 @@ func (e *encodeState) string(s string, escapeHTML bool) int { if start < i { e.WriteString(s[start:i]) } - e.WriteString(`\ufffd`) + e.WriteString(`\u00`) + e.WriteByte(hex[s[i]>>4]) + e.WriteByte(hex[s[i]&0xF]) i += size start = i continue @@ -966,7 +979,17 @@ func (e *encodeState) string(s string, escapeHTML bool) int { start = i continue } + if start < i { + e.WriteString(s[start:i]) + } + if c < 0x10000 { + fmt.Fprintf(e, `\u%04X`, c) + } else { + r1, r2 := utf16.EncodeRune(c) + fmt.Fprintf(e, `\u%04X\u%04X`, r1, r2) + } i += size + start = i } if start < len(s) { e.WriteString(s[start:]) @@ -990,12 +1013,18 @@ func (e *encodeState) stringBytes(s []byte, escapeHTML bool) int { e.Write(s[start:i]) } switch b { - case '\\', '"': + case '\\': e.WriteByte('\\') e.WriteByte(b) + case 0x08: + e.WriteByte('\\') + e.WriteByte('b') case '\n': e.WriteByte('\\') e.WriteByte('n') + case 0x0c: + e.WriteByte('\\') + e.WriteByte('f') case '\r': e.WriteByte('\\') e.WriteByte('r') @@ -1003,7 +1032,7 @@ func (e *encodeState) stringBytes(s []byte, escapeHTML bool) int { e.WriteByte('\\') e.WriteByte('t') default: - // This encodes bytes < 0x20 except for \t, \n and \r. + // This encodes bytes < 0x20 and " except for \t, \n and \r. // If escapeHTML is set, it also escapes <, >, and & // because they can lead to security holes when // user-controlled strings are rendered into JSON @@ -1021,7 +1050,9 @@ func (e *encodeState) stringBytes(s []byte, escapeHTML bool) int { if start < i { e.Write(s[start:i]) } - e.WriteString(`\ufffd`) + e.WriteString(`\u00`) + e.WriteByte(hex[s[i]>>4]) + e.WriteByte(hex[s[i]&0xF]) i += size start = i continue @@ -1043,7 +1074,17 @@ func (e *encodeState) stringBytes(s []byte, escapeHTML bool) int { start = i continue } + if start < i { + e.Write(s[start:i]) + } + if c < 0x10000 { + fmt.Fprintf(e, `\u%04X`, c) + } else { + r1, r2 := utf16.EncodeRune(c) + fmt.Fprintf(e, `\u%04X\u%04X`, r1, r2) + } i += size + start = i } if start < len(s) { e.Write(s[start:]) @@ -1071,23 +1112,16 @@ func fillField(f field) field { return f } -// byIndex sorts field by index sequence. -type byIndex []field - -func (x byIndex) Len() int { return len(x) } - -func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] } - -func (x byIndex) Less(i, j int) bool { - for k, xik := range x[i].index { - if k >= len(x[j].index) { - return false +func cmpFieldsByIndex(a, b field) int { + for k, ak := range a.index { + if k >= len(b.index) { + return 1 } - if xik != x[j].index[k] { - return xik < x[j].index[k] + if ak != b.index[k] { + return ak - b.index[k] } } - return len(x[i].index) < len(x[j].index) + return len(a.index) - len(b.index) } // typeFields returns a list of fields that JSON should recognize for the given type. @@ -1099,7 +1133,7 @@ func typeFields(t reflect.Type) []field { next := []field{{typ: t}} // Count of queued names for current level and the next. - count := map[reflect.Type]int{} + var count map[reflect.Type]int nextCount := map[reflect.Type]int{} // Types already visited at an earlier level. @@ -1119,7 +1153,7 @@ func typeFields(t reflect.Type) []field { visited[f.typ] = true // Scan f.typ for fields to include. - for i := 0; i < f.typ.NumField(); i++ { + for i := range f.typ.NumField() { sf := f.typ.Field(i) if sf.PkgPath != "" && (!sf.Anonymous || sf.Type.Kind() != reflect.Struct) { // unexported continue @@ -1152,6 +1186,7 @@ func typeFields(t reflect.Type) []field { reflect.Float32, reflect.Float64, reflect.String: quoted = true + default: } } @@ -1202,7 +1237,7 @@ func typeFields(t reflect.Type) []field { if x[i].tag != x[j].tag { return x[i].tag } - return byIndex(x).Less(i, j) + return cmpFieldsByIndex(x[i], x[j]) < 0 }) // Delete all fields that are hidden by the Go rules for embedded fields, @@ -1234,7 +1269,7 @@ func typeFields(t reflect.Type) []field { } fields = out - sort.Sort(byIndex(fields)) + slices.SortFunc(fields, cmpFieldsByIndex) return fields } @@ -1300,9 +1335,7 @@ func cachedTypeFields(t reflect.Type) []field { fieldCache.mu.Lock() m, _ = fieldCache.value.Load().(map[reflect.Type][]field) newM := make(map[reflect.Type][]field, len(m)+1) - for k, v := range m { - newM[k] = v - } + maps.Copy(newM, m) newM[t] = f fieldCache.value.Store(newM) fieldCache.mu.Unlock() diff --git a/encode_test.go b/encode_test.go index 39152b0..6e2aead 100644 --- a/encode_test.go +++ b/encode_test.go @@ -24,11 +24,11 @@ type Optionals struct { Ir int `json:"omitempty"` // actually named omitempty, not an option Io int `json:"io,omitempty"` - Slr []string `json:"slr,random"` + Slr []string `json:"slr,random"` //nolint:staticcheck // It's intentionally wrong. Slo []string `json:"slo,omitempty"` - Mr map[string]interface{} `json:"mr"` - Mo map[string]interface{} `json:",omitempty"` + Mr map[string]any `json:"mr"` + Mo map[string]any `json:",omitempty"` Fr float64 `json:"fr"` Fo float64 `json:"fo,omitempty"` @@ -40,7 +40,7 @@ type Optionals struct { Uo uint `json:"uo,omitempty"` Str struct{} `json:"str"` - Sto struct{} `json:"sto,omitempty"` + Sto struct{} `json:"sto"` } var optionalsExpected = `{ @@ -58,8 +58,8 @@ var optionalsExpected = `{ func TestOmitEmpty(t *testing.T) { var o Optionals o.Sw = "something" - o.Mr = map[string]interface{}{} - o.Mo = map[string]interface{}{} + o.Mr = map[string]any{} + o.Mo = map[string]any{} got, err := MarshalIndent(&o, "", " ") if err != nil { @@ -79,7 +79,7 @@ type StringTag struct { var stringTagExpected = `{ "BoolStr": "true", "IntStr": "42", - "StrStr": "\"xzbit\"" + "StrStr": "\u0022xzbit\u0022" }` func TestStringTag(t *testing.T) { @@ -131,7 +131,7 @@ func TestEncodeRenamedByteSlice(t *testing.T) { } } -var unsupportedValues = []interface{}{ +var unsupportedValues = []any{ math.NaN(), math.Inf(-1), math.Inf(1), @@ -140,7 +140,7 @@ var unsupportedValues = []interface{}{ func TestUnsupportedValues(t *testing.T) { for _, v := range unsupportedValues { if _, err := Marshal(v); err != nil { - if _, ok := err.(*UnsupportedValueError); !ok { + if _, ok := err.(*UnsupportedValueError); !ok { //nolint:errorlint // It must match exactly, it's a test. t.Errorf("for %v, got %T want UnsupportedValueError", v, err) } } else { @@ -207,7 +207,7 @@ func TestRefValMarshal(t *testing.T) { V2: 15, V3: new(ValText), } - const want = `{"R0":"ref","R1":"ref","R2":"\"ref\"","R3":"\"ref\"","V0":"val","V1":"val","V2":"\"val\"","V3":"\"val\""}` + const want = `{"R0":"ref","R1":"ref","R2":"\u0022ref\u0022","R3":"\u0022ref\u0022","V0":"val","V1":"val","V2":"\u0022val\u0022","V3":"\u0022val\u0022"}` b, err := Marshal(&s) if err != nil { t.Fatalf("Marshal: %v", err) @@ -231,9 +231,21 @@ func (CText) MarshalText() ([]byte, error) { return []byte(`"<&>"`), nil } +func TestMarshaler_NeoGo_PR2174(t *testing.T) { + source := "IOU(欠条币):一种支持负数的NEP-17(非严格意义上的)资产,合约无存储区,账户由区块链浏览器统计" + b, err := Marshal(source) + if err != nil { + t.Fatalf("Marshal(c): %v", err) + } + want := `"` + `IOU\uFF08\u6B20\u6761\u5E01\uFF09\uFF1A\u4E00\u79CD\u652F\u6301\u8D1F\u6570\u7684NEP-17\uFF08\u975E\u4E25\u683C\u610F\u4E49\u4E0A\u7684\uFF09\u8D44\u4EA7\uFF0C\u5408\u7EA6\u65E0\u5B58\u50A8\u533A\uFF0C\u8D26\u6237\u7531\u533A\u5757\u94FE\u6D4F\u89C8\u5668\u7EDF\u8BA1` + `"` + if got := string(b); got != want { + t.Errorf("Marshal(c) = %#q, want %#q", got, want) + } +} + func TestMarshalerEscaping(t *testing.T) { var c C - want := `"\u003c\u0026\u003e"` + want := `"\u003C\u0026\u003E"` b, err := Marshal(c) if err != nil { t.Fatalf("Marshal(c): %v", err) @@ -243,7 +255,7 @@ func TestMarshalerEscaping(t *testing.T) { } var ct CText - want = `"\"\u003c\u0026\u003e\""` + want = `"\u0022\u003C\u0026\u003E\u0022"` b, err = Marshal(ct) if err != nil { t.Fatalf("Marshal(ct): %v", err) @@ -360,19 +372,19 @@ func (nm *nilMarshaler) MarshalJSON() ([]byte, error) { // Issue 16042. func TestNilMarshal(t *testing.T) { testCases := []struct { - v interface{} + v any want string }{ {v: nil, want: `null`}, {v: new(float64), want: `0`}, - {v: []interface{}(nil), want: `null`}, + {v: []any(nil), want: `null`}, {v: []string(nil), want: `null`}, {v: map[string]string(nil), want: `null`}, {v: []byte(nil), want: `null`}, {v: struct{ M string }{"gopher"}, want: `{"M":"gopher"}`}, {v: struct{ M Marshaler }{}, want: `{"M":null}`}, {v: struct{ M Marshaler }{(*nilMarshaler)(nil)}, want: `{"M":"0zenil0"}`}, - {v: struct{ M interface{} }{(*nilMarshaler)(nil)}, want: `{"M":null}`}, + {v: struct{ M any }{(*nilMarshaler)(nil)}, want: `{"M":null}`}, } for _, tt := range testCases { @@ -484,8 +496,8 @@ func TestStringBytes(t *testing.T) { esBytes := &encodeState{} esBytes.stringBytes([]byte(s), escapeHTML) - enc := es.Buffer.String() - encBytes := esBytes.Buffer.String() + enc := es.String() + encBytes := esBytes.String() if enc != encBytes { i := 0 for i < len(enc) && i < len(encBytes) && enc[i] == encBytes[i] { @@ -528,14 +540,14 @@ func TestIssue10281(t *testing.T) { func TestHTMLEscape(t *testing.T) { var b, want bytes.Buffer m := `{"M":"foo &` + "\xe2\x80\xa8 \xe2\x80\xa9" + `"}` - want.Write([]byte(`{"M":"\u003chtml\u003efoo \u0026\u2028 \u2029\u003c/html\u003e"}`)) + want.Write([]byte(`{"M":"\u003Chtml\u003Efoo \u0026\u2028 \u2029\u003C/html\u003E"}`)) HTMLEscape(&b, []byte(m)) if !bytes.Equal(b.Bytes(), want.Bytes()) { t.Errorf("HTMLEscape(&b, []byte(m)) = %s; want %s", b.Bytes(), want.Bytes()) } } -// golang.org/issue/8582 +// golang.org/issue/8582. func TestEncodePointerString(t *testing.T) { type stringPointer struct { N *int64 `json:"n,string"` @@ -565,38 +577,16 @@ var encodeStringTests = []struct { in string out string }{ - {"\x00", `"\u0000"`}, - {"\x01", `"\u0001"`}, - {"\x02", `"\u0002"`}, - {"\x03", `"\u0003"`}, - {"\x04", `"\u0004"`}, - {"\x05", `"\u0005"`}, - {"\x06", `"\u0006"`}, - {"\x07", `"\u0007"`}, - {"\x08", `"\u0008"`}, - {"\x09", `"\t"`}, - {"\x0a", `"\n"`}, - {"\x0b", `"\u000b"`}, - {"\x0c", `"\u000c"`}, - {"\x0d", `"\r"`}, - {"\x0e", `"\u000e"`}, - {"\x0f", `"\u000f"`}, - {"\x10", `"\u0010"`}, - {"\x11", `"\u0011"`}, - {"\x12", `"\u0012"`}, - {"\x13", `"\u0013"`}, - {"\x14", `"\u0014"`}, - {"\x15", `"\u0015"`}, - {"\x16", `"\u0016"`}, - {"\x17", `"\u0017"`}, - {"\x18", `"\u0018"`}, - {"\x19", `"\u0019"`}, - {"\x1a", `"\u001a"`}, - {"\x1b", `"\u001b"`}, - {"\x1c", `"\u001c"`}, - {"\x1d", `"\u001d"`}, - {"\x1e", `"\u001e"`}, - {"\x1f", `"\u001f"`}, + {"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f", `"\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000B\f\r\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F"`}, + {"\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f", `" !\u0022#$%\u0026\u0027()*\u002B,-./0123456789:;\u003C=\u003E?"`}, + {"\x40\x41\x44\x45\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x54\x55\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f", `"@ADEDEFGHIJKLMNOPQTUTUVWXYZ[\\]^_"`}, + {"\x60\x61\x66\x67\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x76\x77\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f", `"\u0060afgdefghijklmnopqvwtuvwxyz{|}~\u007F"`}, + {"\x80\x81\x88\x89\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x98\x99\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f", `"\u0080\u0081\u0088\u0089\u0084\u0085\u0086\u0087\u0088\u0089\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0098\u0099\u0094\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E\u009F"`}, + {"\xa0\xa1\xaa\xab\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xba\xbb\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf", `"\u00A0\u00A1\u00AA\u00AB\u00A4\u00A5\u00A6\u00A7\u00A8\u00A9\u00AA\u00AB\u00AC\u00AD\u00AE\u00AF\u00B0\u00B1\u00BA\u00BB\u00B4\u00B5\u00B6\u00B7\u00B8\u00B9\u00BA\u00BB\u00BC\u00BD\u00BE\u00BF"`}, + {"\xc0\xc1\xcc\xcd\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xdc\xdd\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf", `"\u00C0\u00C1\u00CC\u00CD\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00DC\u00DD\u00D4\u00D5\u00D6\u00D7\u00D8\u00D9\u00DA\u00DB\u00DC\u00DD\u00DE\u00DF"`}, + {"\xe0\xe1\xee\xef\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xfe\xff\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff", `"\u00E0\u00E1\u00EE\u00EF\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00FE\u00FF\u00F4\u00F5\u00F6\u00F7\u00F8\u00F9\u00FA\u00FB\u00FC\u00FD\u00FE\u00FF"`}, + {"\xff\xd0", `"\u00FF\u00D0"`}, + {"测试", `"\u6D4B\u8BD5"`}, } func TestEncodeString(t *testing.T) { @@ -629,16 +619,16 @@ type textint int func (i textint) MarshalText() ([]byte, error) { return tenc(`TI:%d`, i) } -func tenc(format string, a ...interface{}) ([]byte, error) { +func tenc(format string, a ...any) ([]byte, error) { var buf bytes.Buffer fmt.Fprintf(&buf, format, a...) return buf.Bytes(), nil } -// Issue 13783 +// Issue 13783. func TestEncodeBytekind(t *testing.T) { testdata := []struct { - data interface{} + data any want string }{ {byte(7), "7"}, @@ -711,7 +701,7 @@ func TestMarshalFloat(t *testing.T) { t.Parallel() nfail := 0 test := func(f float64, bits int) { - vf := interface{}(f) + vf := any(f) if bits == 32 { f = float64(float32(f)) // round vf = float32(f) @@ -804,25 +794,25 @@ func TestMarshalRawMessageValue(t *testing.T) { ) tests := []struct { - in interface{} + in any want string ok bool }{ // Test with nil RawMessage. {rawNil, "null", true}, {&rawNil, "null", true}, - {[]interface{}{rawNil}, "[null]", true}, - {&[]interface{}{rawNil}, "[null]", true}, - {[]interface{}{&rawNil}, "[null]", true}, - {&[]interface{}{&rawNil}, "[null]", true}, + {[]any{rawNil}, "[null]", true}, + {&[]any{rawNil}, "[null]", true}, + {[]any{&rawNil}, "[null]", true}, + {&[]any{&rawNil}, "[null]", true}, {struct{ M RawMessage }{rawNil}, `{"M":null}`, true}, {&struct{ M RawMessage }{rawNil}, `{"M":null}`, true}, {struct{ M *RawMessage }{&rawNil}, `{"M":null}`, true}, {&struct{ M *RawMessage }{&rawNil}, `{"M":null}`, true}, - {map[string]interface{}{"M": rawNil}, `{"M":null}`, true}, - {&map[string]interface{}{"M": rawNil}, `{"M":null}`, true}, - {map[string]interface{}{"M": &rawNil}, `{"M":null}`, true}, - {&map[string]interface{}{"M": &rawNil}, `{"M":null}`, true}, + {map[string]any{"M": rawNil}, `{"M":null}`, true}, + {&map[string]any{"M": rawNil}, `{"M":null}`, true}, + {map[string]any{"M": &rawNil}, `{"M":null}`, true}, + {&map[string]any{"M": &rawNil}, `{"M":null}`, true}, {T1{rawNil}, "{}", true}, {T2{&rawNil}, `{"M":null}`, true}, {&T1{rawNil}, "{}", true}, @@ -831,18 +821,18 @@ func TestMarshalRawMessageValue(t *testing.T) { // Test with empty, but non-nil, RawMessage. {rawEmpty, "", false}, {&rawEmpty, "", false}, - {[]interface{}{rawEmpty}, "", false}, - {&[]interface{}{rawEmpty}, "", false}, - {[]interface{}{&rawEmpty}, "", false}, - {&[]interface{}{&rawEmpty}, "", false}, + {[]any{rawEmpty}, "", false}, + {&[]any{rawEmpty}, "", false}, + {[]any{&rawEmpty}, "", false}, + {&[]any{&rawEmpty}, "", false}, {struct{ X RawMessage }{rawEmpty}, "", false}, {&struct{ X RawMessage }{rawEmpty}, "", false}, {struct{ X *RawMessage }{&rawEmpty}, "", false}, {&struct{ X *RawMessage }{&rawEmpty}, "", false}, - {map[string]interface{}{"nil": rawEmpty}, "", false}, - {&map[string]interface{}{"nil": rawEmpty}, "", false}, - {map[string]interface{}{"nil": &rawEmpty}, "", false}, - {&map[string]interface{}{"nil": &rawEmpty}, "", false}, + {map[string]any{"nil": rawEmpty}, "", false}, + {&map[string]any{"nil": rawEmpty}, "", false}, + {map[string]any{"nil": &rawEmpty}, "", false}, + {&map[string]any{"nil": &rawEmpty}, "", false}, {T1{rawEmpty}, "{}", true}, {T2{&rawEmpty}, "", false}, {&T1{rawEmpty}, "{}", true}, @@ -855,18 +845,18 @@ func TestMarshalRawMessageValue(t *testing.T) { // See https://github.com/golang/go/issues/14493#issuecomment-255857318 {rawText, `"foo"`, true}, // Issue6458 {&rawText, `"foo"`, true}, - {[]interface{}{rawText}, `["foo"]`, true}, // Issue6458 - {&[]interface{}{rawText}, `["foo"]`, true}, // Issue6458 - {[]interface{}{&rawText}, `["foo"]`, true}, - {&[]interface{}{&rawText}, `["foo"]`, true}, + {[]any{rawText}, `["foo"]`, true}, // Issue6458 + {&[]any{rawText}, `["foo"]`, true}, // Issue6458 + {[]any{&rawText}, `["foo"]`, true}, + {&[]any{&rawText}, `["foo"]`, true}, {struct{ M RawMessage }{rawText}, `{"M":"foo"}`, true}, // Issue6458 {&struct{ M RawMessage }{rawText}, `{"M":"foo"}`, true}, {struct{ M *RawMessage }{&rawText}, `{"M":"foo"}`, true}, {&struct{ M *RawMessage }{&rawText}, `{"M":"foo"}`, true}, - {map[string]interface{}{"M": rawText}, `{"M":"foo"}`, true}, // Issue6458 - {&map[string]interface{}{"M": rawText}, `{"M":"foo"}`, true}, // Issue6458 - {map[string]interface{}{"M": &rawText}, `{"M":"foo"}`, true}, - {&map[string]interface{}{"M": &rawText}, `{"M":"foo"}`, true}, + {map[string]any{"M": rawText}, `{"M":"foo"}`, true}, // Issue6458 + {&map[string]any{"M": rawText}, `{"M":"foo"}`, true}, // Issue6458 + {map[string]any{"M": &rawText}, `{"M":"foo"}`, true}, + {&map[string]any{"M": &rawText}, `{"M":"foo"}`, true}, {T1{rawText}, `{"M":"foo"}`, true}, // Issue6458 {T2{&rawText}, `{"M":"foo"}`, true}, {&T1{rawText}, `{"M":"foo"}`, true}, @@ -891,11 +881,11 @@ func TestMarshalRawMessageValue(t *testing.T) { type ObjEnc struct { A int B OrderedObject - C interface{} + C any } var orderedObjectTests = []struct { - in interface{} + in any out string }{ {OrderedObject{}, `{}`}, @@ -904,7 +894,7 @@ var orderedObjectTests = []struct { {ObjEnc{A: 234, B: OrderedObject{}, C: OrderedObject{{"A", 0}}}, `{"A":234,"B":{},"C":{"A":0}}`}, {[]OrderedObject{{{"A", "Ay"}, {"B", "Bee"}}, {{"A", "Nay"}}}, `[{"A":"Ay","B":"Bee"},{"A":"Nay"}]`}, {map[string]OrderedObject{"A": {{"A", "Ay"}, {"B", "Bee"}}}, `{"A":{"A":"Ay","B":"Bee"}}`}, - {map[string]interface{}{"A": OrderedObject{{"A", "Ay"}, {"B", "Bee"}}}, `{"A":{"A":"Ay","B":"Bee"}}`}, + {map[string]any{"A": OrderedObject{{"A", "Ay"}, {"B", "Bee"}}}, `{"A":{"A":"Ay","B":"Bee"}}`}, } func TestEncodeOrderedObject(t *testing.T) { diff --git a/example_marshaling_test.go b/example_marshaling_test.go index 7f15c74..0b1125b 100644 --- a/example_marshaling_test.go +++ b/example_marshaling_test.go @@ -59,7 +59,7 @@ func Example_customMarshalJSON() { census := make(map[Animal]int) for _, animal := range zoo { - census[animal] += 1 + census[animal]++ } fmt.Printf("Zoo Census:\n* Gophers: %d\n* Zebras: %d\n* Unknown: %d\n", diff --git a/example_test.go b/example_test.go index 611fe3c..ec33c3a 100644 --- a/example_test.go +++ b/example_test.go @@ -6,12 +6,14 @@ package json_test import ( "bytes" - "github.com/virtuald/go-ordered-json" + "errors" "fmt" "io" "log" "os" "strings" + + json "github.com/nspcc-dev/go-ordered-json" ) func ExampleMarshal() { @@ -68,7 +70,7 @@ func ExampleDecoder() { dec := json.NewDecoder(strings.NewReader(jsonStream)) for { var m Message - if err := dec.Decode(&m); err == io.EOF { + if err := dec.Decode(&m); errors.Is(err, io.EOF) { break } else if err != nil { log.Fatal(err) @@ -91,7 +93,7 @@ func ExampleDecoder_Token() { dec := json.NewDecoder(strings.NewReader(jsonStream)) for { t, err := dec.Token() - if err == io.EOF { + if errors.Is(err, io.EOF) { break } if err != nil { @@ -170,7 +172,6 @@ func ExampleDecoder_Decode_stream() { // Sam: Go fmt who? // Ed: Go fmt yourself! // json.Delim: ] - } // This example uses RawMessage to delay parsing part of a JSON message. @@ -201,7 +202,7 @@ func ExampleRawMessage_unmarshal() { } for _, c := range colors { - var dst interface{} + var dst any switch c.Space { case "RGB": dst = new(RGB) @@ -259,8 +260,8 @@ func ExampleIndent() { } var out bytes.Buffer - json.Indent(&out, b, "=", "\t") - out.WriteTo(os.Stdout) + _ = json.Indent(&out, b, "=", "\t") + _, _ = out.WriteTo(os.Stdout) // Output: // [ // = { @@ -311,5 +312,5 @@ func ExampleOrderedObject() { // name=Issac Newton born=1643 died=1727 // name=André-Marie Ampère born=1777 died=1836 // Encoded: - // {"name":"Hans Christian Ørsted","born":1777,"died":1851,"nationality":"Danish"} -} \ No newline at end of file + // {"name":"Hans Christian \u00D8rsted","born":1777,"died":1851,"nationality":"Danish"} +} diff --git a/fold.go b/fold.go index 9e17012..520b5a8 100644 --- a/fold.go +++ b/fold.go @@ -24,8 +24,9 @@ const ( // 4) simpleLetterEqualFold, no specials, no non-letters. // // The letters S and K are special because they map to 3 runes, not just 2: -// * S maps to s and to U+017F 'ſ' Latin small letter long s -// * k maps to K and to U+212A 'K' Kelvin sign +// - S maps to s and to U+017F 'ſ' Latin small letter long s +// - k maps to K and to U+212A 'K' Kelvin sign +// // See https://play.golang.org/p/tTxjOc0OGo // // The returned function is specialized for matching against s and @@ -94,12 +95,8 @@ func equalFoldRight(s, t []byte) bool { return false } t = t[size:] - } - if len(t) > 0 { - return false - } - return true + return len(t) <= 0 } // asciiEqualFold is a specialization of bytes.EqualFold for use when diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4723659 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/nspcc-dev/go-ordered-json + +go 1.25 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/indent.go b/indent.go index fba1954..aea0a4b 100644 --- a/indent.go +++ b/indent.go @@ -60,7 +60,7 @@ func compact(dst *bytes.Buffer, src []byte, escape bool) error { func newline(dst *bytes.Buffer, prefix, indent string, depth int) { dst.WriteByte('\n') dst.WriteString(prefix) - for i := 0; i < depth; i++ { + for range depth { dst.WriteString(indent) } } diff --git a/number_test.go b/number_test.go index 4b86999..5d57bb0 100644 --- a/number_test.go +++ b/number_test.go @@ -119,7 +119,7 @@ func TestNumberIsValid(t *testing.T) { func BenchmarkNumberIsValid(b *testing.B) { s := "-61657.61667E+61673" - for i := 0; i < b.N; i++ { + for b.Loop() { isValidNumber(s) } } @@ -127,7 +127,7 @@ func BenchmarkNumberIsValid(b *testing.B) { func BenchmarkNumberIsValidRegexp(b *testing.B) { var jsonNumberRegexp = regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`) s := "-61657.61667E+61673" - for i := 0; i < b.N; i++ { + for b.Loop() { jsonNumberRegexp.MatchString(s) } } diff --git a/scanner.go b/scanner.go index ae34418..f7cfda0 100644 --- a/scanner.go +++ b/scanner.go @@ -593,7 +593,7 @@ func (s *scanner) error(c byte, context string) int { return scanError } -// quoteChar formats c as a quoted character literal +// quoteChar formats c as a quoted character literal. func quoteChar(c byte) string { // special cases - different from quoted strings if c == '\'' { diff --git a/scanner_test.go b/scanner_test.go index 0d4518a..5ae9913 100644 --- a/scanner_test.go +++ b/scanner_test.go @@ -7,7 +7,7 @@ package json import ( "bytes" "math" - "math/rand" + "math/rand/v2" "reflect" "testing" ) @@ -230,9 +230,8 @@ var benchScan scanner func BenchmarkSkipValue(b *testing.B) { initBig() - b.ResetTimer() - for i := 0; i < b.N; i++ { - nextValue(jsonBig, &benchScan) + for b.Loop() { + _, _, _ = nextValue(jsonBig, &benchScan) } b.SetBytes(int64(len(jsonBig))) } @@ -240,10 +239,7 @@ func BenchmarkSkipValue(b *testing.B) { func diff(t *testing.T, a, b []byte) { for i := 0; ; i++ { if i >= len(a) || i >= len(b) || a[i] != b[i] { - j := i - 10 - if j < 0 { - j = 0 - } + j := max(i-10, 0) t.Errorf("diverge at %d: «%s» vs «%s»", i, trim(a[j:]), trim(b[j:])) return } @@ -273,18 +269,18 @@ func initBig() { jsonBig = b } -func genValue(n int) interface{} { +func genValue(n int) any { if n > 1 { - switch rand.Intn(2) { + switch rand.IntN(2) { case 0: return genArray(n) case 1: return genMap(n) } } - switch rand.Intn(3) { + switch rand.IntN(3) { case 0: - return rand.Intn(2) == 0 + return rand.IntN(2) == 0 case 1: return rand.NormFloat64() case 2: @@ -306,31 +302,25 @@ func genString(stddev float64) string { return string(c) } -func genArray(n int) []interface{} { - f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2))) - if f > n { - f = n - } +func genArray(n int) []any { + f := min(int(math.Abs(rand.NormFloat64())*math.Min(10, float64(n/2))), n) if f < 1 { f = 1 } - x := make([]interface{}, f) + x := make([]any, f) for i := range x { x[i] = genValue(((i+1)*n)/f - (i*n)/f) } return x } -func genMap(n int) map[string]interface{} { - f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2))) - if f > n { - f = n - } +func genMap(n int) map[string]any { + f := min(int(math.Abs(rand.NormFloat64())*math.Min(10, float64(n/2))), n) if n > 0 && f == 0 { f = 1 } - x := make(map[string]interface{}) - for i := 0; i < f; i++ { + x := make(map[string]any) + for i := range f { x[genString(10)] = genValue(((i+1)*n)/f - (i*n)/f) } return x diff --git a/stream.go b/stream.go index 744dcd5..3c69a14 100644 --- a/stream.go +++ b/stream.go @@ -31,12 +31,12 @@ func NewDecoder(r io.Reader) *Decoder { return &Decoder{r: r} } -// UseNumber causes the Decoder to unmarshal a number into an interface{} as a +// UseNumber causes the Decoder to unmarshal a number into an any as a // Number instead of as a float64. func (dec *Decoder) UseNumber() { dec.d.useNumber = true } -// UseOrderedObject causes the Decoder to unmarshal an object into an interface{} -// as a OrderedObject instead of as a map[string]interface{}. +// UseOrderedObject causes the Decoder to unmarshal an object into an any +// as a OrderedObject instead of as a map[string]any. func (dec *Decoder) UseOrderedObject() { dec.d.useOrderedObject = true } // Decode reads the next JSON-encoded value from its @@ -44,7 +44,7 @@ func (dec *Decoder) UseOrderedObject() { dec.d.useOrderedObject = true } // // See the documentation for Unmarshal for details about // the conversion of JSON into a Go value. -func (dec *Decoder) Decode(v interface{}) error { +func (dec *Decoder) Decode(v any) error { if dec.err != nil { return dec.err } @@ -116,7 +116,7 @@ Input: // Did the last read have an error? // Delayed until now to allow buffer scan. if err != nil { - if err == io.EOF { + if errors.Is(err, io.EOF) { if dec.scan.step(&dec.scan, ' ') == scanEnd { break Input } @@ -189,7 +189,7 @@ func NewEncoder(w io.Writer) *Encoder { // // See the documentation for Marshal for details about the // conversion of Go values to JSON. -func (enc *Encoder) Encode(v interface{}) error { +func (enc *Encoder) Encode(v any) error { if enc.err != nil { return enc.err } @@ -273,7 +273,7 @@ var _ Unmarshaler = (*RawMessage)(nil) // Member is used to store key/value pairs in an OrderedObject. type Member struct { Key string - Value interface{} + Value any } // OrderedObject stores the key/value pairs of a JSON object in the order @@ -282,41 +282,41 @@ type Member struct { // OrderedObject is used to enable decoding of arbitrary JSON objects while preserving // the order of the keys. Unmarshal and Decoder.Decode are supported. // -// var o OrderedObject -// Unmarshal(json, &o) // decode JSON object, while preserving key order +// var o OrderedObject +// Unmarshal(json, &o) // decode JSON object, while preserving key order // -// var oa []OrderedObject -// Unmarshal(json, &oa) // decode an array of JSON objects, while preserving key order +// var oa []OrderedObject +// Unmarshal(json, &oa) // decode an array of JSON objects, while preserving key order // -// var v interface{} -// d := new Decoder(json) -// d.UseOrderedObject() // decode all JSON objects as OrderedObject rather than map[string]interface{} -// d.Decode(&v) +// var v any +// d := new Decoder(json) +// d.UseOrderedObject() // decode all JSON objects as OrderedObject rather than map[string]any +// d.Decode(&v) // -// type A struct { -// B bool -// Inner OrderedObject -// I int -// } -// var a A -// Unmarshal(&a) // decode A as a JSON object with Inner as a nested object, preserving key order +// type A struct { +// B bool +// Inner OrderedObject +// I int +// } +// var a A +// Unmarshal(&a) // decode A as a JSON object with Inner as a nested object, preserving key order // // OrderedObject can also be used to encode a JSON object in // a specified order. Marshal and Encoder.Encode are supported. // -// var o OrderedObject -// Marshal(o) // encode JSON object, each with keys in OrderedObject order +// var o OrderedObject +// Marshal(o) // encode JSON object, each with keys in OrderedObject order // -// var oa []OrderedObject -// Marshal(oa) // encode an array of JSON objects, with keys in OrderedObject order +// var oa []OrderedObject +// Marshal(oa) // encode an array of JSON objects, with keys in OrderedObject order // -// type A struct { -// B bool -// Inner OrderedObject -// I int -// } -// var a A = createA() -// Marshal(a) // encode A as a JSON object with Inner as a nested object +// type A struct { +// B bool +// Inner OrderedObject +// I int +// } +// var a A = createA() +// Marshal(a) // encode A as a JSON object with Inner as a nested object type OrderedObject []Member // A Token holds a value of one of these types: @@ -327,8 +327,7 @@ type OrderedObject []Member // Number, for JSON numbers // string, for JSON string literals // nil, for JSON null -// -type Token interface{} +type Token any const ( tokenTopValue = iota @@ -342,7 +341,7 @@ const ( tokenObjectComma ) -// advance tokenstate from a separator state to a value state +// advance tokenstate from a separator state to a value state. func (dec *Decoder) tokenPrepareForDecode() error { // Note: Not calling peek before switch, to avoid // putting peek into the standard Decode path. @@ -493,7 +492,7 @@ func (dec *Decoder) Token() (Token, error) { if !dec.tokenValueAllowed() { return dec.tokenError(c) } - var x interface{} + var x any if err := dec.Decode(&x); err != nil { clearOffset(err) return nil, err @@ -504,7 +503,8 @@ func (dec *Decoder) Token() (Token, error) { } func clearOffset(err error) { - if s, ok := err.(*SyntaxError); ok { + var s *SyntaxError + if errors.As(err, &s) { s.Offset = 0 } } diff --git a/stream_test.go b/stream_test.go index d0b3ffb..2d90968 100644 --- a/stream_test.go +++ b/stream_test.go @@ -6,8 +6,8 @@ package json import ( "bytes" + "errors" "io" - "io/ioutil" "log" "net" "net/http" @@ -19,14 +19,14 @@ import ( // Test values for the stream test. // One of each JSON kind. -var streamTest = []interface{}{ +var streamTest = []any{ 0.1, "hello", nil, true, false, - []interface{}{"a", "b", "c"}, - map[string]interface{}{"K": "Kelvin", "ß": "long s"}, + []any{"a", "b", "c"}, + map[string]any{"K": "Kelvin", "ß": "long s"}, 3.14, // another value to make sure something can follow map } @@ -36,7 +36,7 @@ null true false ["a","b","c"] -{"ß":"long s","K":"Kelvin"} +{"\u00DF":"long s","\u212A":"Kelvin"} 3.14 ` @@ -71,8 +71,8 @@ false >."c" >] { ->."ß": "long s", ->."K": "Kelvin" +>."\u00DF": "long s", +>."\u212A": "Kelvin" >} 3.14 ` @@ -82,7 +82,10 @@ func TestEncoderIndent(t *testing.T) { enc := NewEncoder(&buf) enc.SetIndent(">", ".") for _, v := range streamTest { - enc.Encode(v) + err := enc.Encode(v) + if err != nil { + t.Error(err) + } } if have, want := buf.String(), streamEncodedIndent; have != want { t.Error("indented encoding mismatch") @@ -95,13 +98,13 @@ func TestEncoderSetEscapeHTML(t *testing.T) { var ct CText for _, tt := range []struct { name string - v interface{} + v any wantEscape string want string }{ - {"c", c, `"\u003c\u0026\u003e"`, `"<&>"`}, - {"ct", ct, `"\"\u003c\u0026\u003e\""`, `"\"<&>\""`}, - {`"<&>"`, "<&>", `"\u003c\u0026\u003e"`, `"<&>"`}, + {"c", c, `"\u003C\u0026\u003E"`, `"<&>"`}, + {"ct", ct, `"\u0022\u003C\u0026\u003E\u0022"`, `"\u0022<\u0026>\u0022"`}, + {`"<&>"`, "<&>", `"\u003C\u0026\u003E"`, `"<\u0026>"`}, } { var buf bytes.Buffer enc := NewEncoder(&buf) @@ -136,7 +139,7 @@ func TestDecoder(t *testing.T) { buf.WriteRune(c) } } - out := make([]interface{}, i) + out := make([]any, i) dec := NewDecoder(&buf) for j := range out { if err := dec.Decode(&out[j]); err != nil { @@ -168,7 +171,7 @@ func TestDecoderBuffered(t *testing.T) { if m.Name != "Gopher" { t.Errorf("Name = %q; want Gopher", m.Name) } - rest, err := ioutil.ReadAll(d.Buffered()) + rest, err := io.ReadAll(d.Buffered()) if err != nil { t.Fatal(err) } @@ -195,7 +198,7 @@ func TestRawMessage(t *testing.T) { // TODO(rsc): Should not need the * in *RawMessage var data struct { X float64 - Id *RawMessage + Id *RawMessage //nolint:revive // It's "Id" in JSON and it's a test. Y float32 } const raw = `["\u0056",null]` @@ -220,7 +223,7 @@ func TestNullRawMessage(t *testing.T) { // TODO(rsc): Should not need the * in *RawMessage var data struct { X float64 - Id *RawMessage + Id *RawMessage //nolint:revive // It's "Id" in JSON and it's a test. Y float32 } data.Id = new(RawMessage) @@ -249,8 +252,13 @@ var blockingTests = []string{ func TestBlocking(t *testing.T) { for _, enc := range blockingTests { r, w := net.Pipe() - go w.Write([]byte(enc)) - var val interface{} + go func() { + _, err := w.Write([]byte(enc)) + if err != nil { + t.Error(err) + } + }() + var val any // If Decode reads beyond what w.Write writes above, // it will block, and the test will deadlock. @@ -270,7 +278,7 @@ func BenchmarkEncoderEncode(b *testing.B) { v := &T{"foo", "bar"} b.RunParallel(func(pb *testing.PB) { for pb.Next() { - if err := NewEncoder(ioutil.Discard).Encode(v); err != nil { + if err := NewEncoder(io.Discard).Encode(v); err != nil { b.Fatal(err) } } @@ -279,85 +287,82 @@ func BenchmarkEncoderEncode(b *testing.B) { type tokenStreamCase struct { json string - expTokens []interface{} + expTokens []any } type decodeThis struct { - v interface{} + v any } -var tokenStreamCases []tokenStreamCase = []tokenStreamCase{ +var tokenStreamCases = []tokenStreamCase{ // streaming token cases - {json: `10`, expTokens: []interface{}{float64(10)}}, - {json: ` [10] `, expTokens: []interface{}{ + {json: `10`, expTokens: []any{float64(10)}}, + {json: ` [10] `, expTokens: []any{ Delim('['), float64(10), Delim(']')}}, - {json: ` [false,10,"b"] `, expTokens: []interface{}{ + {json: ` [false,10,"b"] `, expTokens: []any{ Delim('['), false, float64(10), "b", Delim(']')}}, - {json: `{ "a": 1 }`, expTokens: []interface{}{ + {json: `{ "a": 1 }`, expTokens: []any{ Delim('{'), "a", float64(1), Delim('}')}}, - {json: `{"a": 1, "b":"3"}`, expTokens: []interface{}{ + {json: `{"a": 1, "b":"3"}`, expTokens: []any{ Delim('{'), "a", float64(1), "b", "3", Delim('}')}}, - {json: ` [{"a": 1},{"a": 2}] `, expTokens: []interface{}{ + {json: ` [{"a": 1},{"a": 2}] `, expTokens: []any{ Delim('['), Delim('{'), "a", float64(1), Delim('}'), Delim('{'), "a", float64(2), Delim('}'), Delim(']')}}, - {json: `{"obj": {"a": 1}}`, expTokens: []interface{}{ + {json: `{"obj": {"a": 1}}`, expTokens: []any{ Delim('{'), "obj", Delim('{'), "a", float64(1), Delim('}'), Delim('}')}}, - {json: `{"obj": [{"a": 1}]}`, expTokens: []interface{}{ + {json: `{"obj": [{"a": 1}]}`, expTokens: []any{ Delim('{'), "obj", Delim('['), Delim('{'), "a", float64(1), Delim('}'), Delim(']'), Delim('}')}}, // streaming tokens with intermittent Decode() - {json: `{ "a": 1 }`, expTokens: []interface{}{ + {json: `{ "a": 1 }`, expTokens: []any{ Delim('{'), "a", decodeThis{float64(1)}, Delim('}')}}, - {json: ` [ { "a" : 1 } ] `, expTokens: []interface{}{ + {json: ` [ { "a" : 1 } ] `, expTokens: []any{ Delim('['), - decodeThis{map[string]interface{}{"a": float64(1)}}, + decodeThis{map[string]any{"a": float64(1)}}, Delim(']')}}, - {json: ` [{"a": 1},{"a": 2}] `, expTokens: []interface{}{ + {json: ` [{"a": 1},{"a": 2}] `, expTokens: []any{ Delim('['), - decodeThis{map[string]interface{}{"a": float64(1)}}, - decodeThis{map[string]interface{}{"a": float64(2)}}, + decodeThis{map[string]any{"a": float64(1)}}, + decodeThis{map[string]any{"a": float64(2)}}, Delim(']')}}, - {json: `{ "obj" : [ { "a" : 1 } ] }`, expTokens: []interface{}{ + {json: `{ "obj" : [ { "a" : 1 } ] }`, expTokens: []any{ Delim('{'), "obj", Delim('['), - decodeThis{map[string]interface{}{"a": float64(1)}}, + decodeThis{map[string]any{"a": float64(1)}}, Delim(']'), Delim('}')}}, - {json: `{"obj": {"a": 1}}`, expTokens: []interface{}{ + {json: `{"obj": {"a": 1}}`, expTokens: []any{ Delim('{'), "obj", - decodeThis{map[string]interface{}{"a": float64(1)}}, + decodeThis{map[string]any{"a": float64(1)}}, Delim('}')}}, - {json: `{"obj": [{"a": 1}]}`, expTokens: []interface{}{ + {json: `{"obj": [{"a": 1}]}`, expTokens: []any{ Delim('{'), "obj", - decodeThis{[]interface{}{ - map[string]interface{}{"a": float64(1)}, + decodeThis{[]any{ + map[string]any{"a": float64(1)}, }}, Delim('}')}}, - {json: ` [{"a": 1} {"a": 2}] `, expTokens: []interface{}{ + {json: ` [{"a": 1} {"a": 2}] `, expTokens: []any{ Delim('['), - decodeThis{map[string]interface{}{"a": float64(1)}}, + decodeThis{map[string]any{"a": float64(1)}}, decodeThis{&SyntaxError{"expected comma after array element", 0}}, }}, - {json: `{ "a" 1 }`, expTokens: []interface{}{ + {json: `{ "a" 1 }`, expTokens: []any{ Delim('{'), "a", decodeThis{&SyntaxError{"expected colon after object key", 0}}, }}, } func TestDecodeInStream(t *testing.T) { - for ci, tcase := range tokenStreamCases { - dec := NewDecoder(strings.NewReader(tcase.json)) for i, etk := range tcase.expTokens { - - var tk interface{} + var tk any var err error if dt, ok := etk.(decodeThis); ok { @@ -371,7 +376,7 @@ func TestDecodeInStream(t *testing.T) { t.Errorf("case %v: Expected error %v in %q, but was %v", ci, experr, tcase.json, err) } break - } else if err == io.EOF { + } else if errors.Is(err, io.EOF) { t.Errorf("case %v: Unexpected EOF in %q", ci, tcase.json) break } else if err != nil { @@ -384,15 +389,17 @@ func TestDecodeInStream(t *testing.T) { } } } - } -// Test from golang.org/issue/11893 +// Test from golang.org/issue/11893. func TestHTTPDecoding(t *testing.T) { const raw = `{ "foo": "bar" }` ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(raw)) + _, err := w.Write([]byte(raw)) + if err != nil { + t.Error(err) + } })) defer ts.Close() res, err := http.Get(ts.URL) @@ -416,7 +423,7 @@ func TestHTTPDecoding(t *testing.T) { // make sure we get the EOF the second time err = d.Decode(&foo) - if err != io.EOF { + if !errors.Is(err, io.EOF) { t.Errorf("err = %v; want io.EOF", err) } } diff --git a/tables.go b/tables.go index 10acdc1..4ac200d 100644 --- a/tables.go +++ b/tables.go @@ -19,12 +19,12 @@ var safeSet = [utf8.RuneSelf]bool{ '#': true, '$': true, '%': true, - '&': true, - '\'': true, + '&': false, + '\'': false, '(': true, ')': true, '*': true, - '+': true, + '+': false, ',': true, '-': true, '.': true, @@ -77,7 +77,7 @@ var safeSet = [utf8.RuneSelf]bool{ ']': true, '^': true, '_': true, - '`': true, + '`': false, 'a': true, 'b': true, 'c': true, @@ -108,7 +108,7 @@ var safeSet = [utf8.RuneSelf]bool{ '|': true, '}': true, '~': true, - '\u007f': true, + '\u007f': false, } // htmlSafeSet holds the value true if the ASCII character with the given @@ -126,11 +126,11 @@ var htmlSafeSet = [utf8.RuneSelf]bool{ '$': true, '%': true, '&': false, - '\'': true, + '\'': false, '(': true, ')': true, '*': true, - '+': true, + '+': false, ',': true, '-': true, '.': true, @@ -183,7 +183,7 @@ var htmlSafeSet = [utf8.RuneSelf]bool{ ']': true, '^': true, '_': true, - '`': true, + '`': false, 'a': true, 'b': true, 'c': true, @@ -214,5 +214,5 @@ var htmlSafeSet = [utf8.RuneSelf]bool{ '|': true, '}': true, '~': true, - '\u007f': true, + '\u007f': false, } diff --git a/tagkey_test.go b/tagkey_test.go index f77c49c..b388844 100644 --- a/tagkey_test.go +++ b/tagkey_test.go @@ -45,7 +45,7 @@ type punctuationTag struct { } type dashTag struct { - V string `json:"-,"` + V string `json:"-,"` // nolint:staticcheck // The test is written this way } type emptyTag struct { @@ -57,11 +57,11 @@ type misnamedTag struct { } type badFormatTag struct { - Y string `:"BadFormat"` + Y string `:"BadFormat"` //nolint:govet // It's intentionally wrong. } type badCodeTag struct { - Z string `json:" !\"#&'()*+,."` + Z string `json:" !\"#&'()*+,."` //nolint:staticcheck // It's intentionally wrong. } type spaceTag struct { @@ -73,7 +73,7 @@ type unicodeTag struct { } var structTagObjectKeyTests = []struct { - raw interface{} + raw any value string key string }{ @@ -101,12 +101,12 @@ func TestStructTagObjectKey(t *testing.T) { if err != nil { t.Fatalf("Marshal(%#q) failed: %v", tt.raw, err) } - var f interface{} + var f any err = Unmarshal(b, &f) if err != nil { t.Fatalf("Unmarshal(%#q) failed: %v", b, err) } - for i, v := range f.(map[string]interface{}) { + for i, v := range f.(map[string]any) { switch i { case tt.key: if s, ok := v.(string); !ok || s != tt.value { diff --git a/tags.go b/tags.go index c38fd51..1fc0817 100644 --- a/tags.go +++ b/tags.go @@ -15,8 +15,8 @@ type tagOptions string // parseTag splits a struct field's json tag into its name and // comma-separated options. func parseTag(tag string) (string, tagOptions) { - if idx := strings.Index(tag, ","); idx != -1 { - return tag[:idx], tagOptions(tag[idx+1:]) + if before, after, ok := strings.Cut(tag, ","); ok { + return before, tagOptions(after) } return tag, tagOptions("") } @@ -28,17 +28,10 @@ func (o tagOptions) Contains(optionName string) bool { if len(o) == 0 { return false } - s := string(o) - for s != "" { - var next string - i := strings.Index(s, ",") - if i >= 0 { - s, next = s[:i], s[i+1:] - } + for s := range strings.FieldsFuncSeq(string(o), func(c rune) bool { return c == ',' }) { if s == optionName { return true } - s = next } return false }