From dca210391016e95d77a81494586b10ab8cb7c898 Mon Sep 17 00:00:00 2001 From: Vincent Serpoul Date: Wed, 21 Apr 2021 08:24:44 +0800 Subject: [PATCH] golangci-lint: marshaler (#516) --- .golangci.toml | 3 + marshaler.go | 214 ++++++++++++++++++++++++++++++---------------- marshaler_test.go | 23 ++++- 3 files changed, 162 insertions(+), 78 deletions(-) diff --git a/.golangci.toml b/.golangci.toml index 60bb5c1..b27872a 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -4,6 +4,9 @@ golangci-lint-version = "1.39.0" [linters-settings.wsl] allow-assign-and-anything = true +[linters-settings.exhaustive] +default-signifies-exhaustive = true + [linters] disable-all = true enable = [ diff --git a/marshaler.go b/marshaler.go index 9019ec0..bbc3130 100644 --- a/marshaler.go +++ b/marshaler.go @@ -18,10 +18,12 @@ import ( func Marshal(v interface{}) ([]byte, error) { var buf bytes.Buffer enc := NewEncoder(&buf) + err := enc.Encode(v) if err != nil { return nil, err } + return buf.Bytes(), nil } @@ -96,7 +98,7 @@ func NewEncoder(w io.Writer) *Encoder { // 5. Intermediate tables are always printed. // // By default, strings are encoded as literal string, unless they contain either -// a newline character or a single quote. In that case they are emited as quoted +// a newline character or a single quote. In that case they are emitted as quoted // strings. // // When encoding structs, fields are encoded in order of definition, with their @@ -107,25 +109,38 @@ func NewEncoder(w io.Writer) *Encoder { // `multiline:"true"`: when the field contains a string, it will be emitted as // a quoted multi-line TOML string. func (enc *Encoder) Encode(v interface{}) error { - var b []byte - var ctx encoderCtx + var ( + b []byte + ctx encoderCtx + ) + b, err := enc.encode(b, ctx, reflect.ValueOf(v)) if err != nil { - return err + return fmt.Errorf("Encode: %w", err) } + _, err = enc.w.Write(b) - return err + if err != nil { + return fmt.Errorf("Encode: %w", err) + } + + return nil } +var errUnsupportedValue = errors.New("unsupported encode value kind") + +//nolint:cyclop func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { + //nolint:gocritic,godox switch i := v.Interface().(type) { case time.Time: // TODO: add TextMarshaler b = i.AppendFormat(b, time.RFC3339) + return b, nil } - // containers switch v.Kind() { + // containers case reflect.Map: return enc.encodeMap(b, ctx, v) case reflect.Struct: @@ -136,19 +151,18 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e if v.IsNil() { return nil, errNilInterface } + return enc.encode(b, ctx, v.Elem()) case reflect.Ptr: if v.IsNil() { return enc.encode(b, ctx, reflect.Zero(v.Type().Elem())) } + return enc.encode(b, ctx, v.Elem()) - } // values - var err error - switch v.Kind() { case reflect.String: - b, err = enc.encodeString(b, v.String(), ctx.options) + b = enc.encodeString(b, v.String(), ctx.options) case reflect.Float32: b = strconv.AppendFloat(b, v.Float(), 'f', -1, 32) case reflect.Float64: @@ -164,10 +178,7 @@ func (enc *Encoder) encode(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, e case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: b = strconv.AppendInt(b, v.Int(), 10) default: - err = fmt.Errorf("unsupported encode value kind: %s", v.Kind()) - } - if err != nil { - return nil, err + return nil, fmt.Errorf("encode(type %s): %w", v.Kind(), errUnsupportedValue) } return b, nil @@ -217,30 +228,31 @@ func (enc *Encoder) encodeKv(b []byte, ctx encoderCtx, options valueOptions, v r const literalQuote = '\'' -func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) ([]byte, error) { +func (enc *Encoder) encodeString(b []byte, v string, options valueOptions) []byte { if needsQuoting(v) { - b = enc.encodeQuotedString(options.multiline, b, v) - } else { - b = enc.encodeLiteralString(b, v) + return enc.encodeQuotedString(options.multiline, b, v) } - return b, nil + + return enc.encodeLiteralString(b, v) } func needsQuoting(v string) bool { return strings.ContainsAny(v, "'\b\f\n\r\t") } -// caller should have checked that the string does not contain new lines or ' +// caller should have checked that the string does not contain new lines or ' . func (enc *Encoder) encodeLiteralString(b []byte, v string) []byte { b = append(b, literalQuote) b = append(b, v...) b = append(b, literalQuote) + return b } +//nolint:cyclop func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byte { - const hextable = "0123456789ABCDEF" stringQuote := `"` + if multiline { stringQuote = `"""` } @@ -250,6 +262,16 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt b = append(b, '\n') } + const ( + hextable = "0123456789ABCDEF" + // U+0000 to U+0008, U+000A to U+001F, U+007F + nul = 0x0 + bs = 0x8 + lf = 0xa + us = 0x1f + del = 0x7f + ) + for _, r := range []byte(v) { switch r { case '\\': @@ -272,7 +294,7 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt b = append(b, `\t`...) default: switch { - case r >= 0x0 && r <= 0x8, r >= 0xA && r <= 0x1F, r == 0x7F: + case r >= nul && r <= bs, r >= lf && r <= us, r == del: b = append(b, `\u00`...) b = append(b, hextable[r>>4]) b = append(b, hextable[r&0x0f]) @@ -280,14 +302,14 @@ func (enc *Encoder) encodeQuotedString(multiline bool, b []byte, v string) []byt b = append(b, r) } } - // U+0000 to U+0008, U+000A to U+001F, U+007F } b = append(b, stringQuote...) + return b } -// called should have checked that the string is in A-Z / a-z / 0-9 / - / _ +// called should have checked that the string is in A-Z / a-z / 0-9 / - / _ . func (enc *Encoder) encodeUnquotedKey(b []byte, v string) []byte { return append(b, v...) } @@ -300,6 +322,7 @@ func (enc *Encoder) encodeTableHeader(b []byte, key []string) ([]byte, error) { b = append(b, '[') var err error + b, err = enc.encodeKey(b, key[0]) if err != nil { return nil, err @@ -307,6 +330,7 @@ func (enc *Encoder) encodeTableHeader(b []byte, key []string) ([]byte, error) { for _, k := range key[1:] { b = append(b, '.') + b, err = enc.encodeKey(b, k) if err != nil { return nil, err @@ -318,6 +342,9 @@ func (enc *Encoder) encodeTableHeader(b []byte, key []string) ([]byte, error) { return b, nil } +var errTomlNoMultiline = errors.New("TOML does not support multiline keys") + +//nolint:cyclop func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) { needsQuotation := false cannotUseLiteral := false @@ -326,32 +353,39 @@ func (enc *Encoder) encodeKey(b []byte, k string) ([]byte, error) { if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' { continue } + if c == '\n' { - return nil, fmt.Errorf("TOML does not support multiline keys") + return nil, errTomlNoMultiline } + if c == literalQuote { cannotUseLiteral = true } + needsQuotation = true } - if cannotUseLiteral { - b = enc.encodeQuotedString(false, b, k) - } else if needsQuotation { - b = enc.encodeLiteralString(b, k) - } else { - b = enc.encodeUnquotedKey(b, k) + switch { + case cannotUseLiteral: + return enc.encodeQuotedString(false, b, k), nil + case needsQuotation: + return enc.encodeLiteralString(b, k), nil + default: + return enc.encodeUnquotedKey(b, k), nil } - - return b, nil } +var errNotSupportedAsMapKey = errors.New("type not supported as map key") + func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { if v.Type().Key().Kind() != reflect.String { - return nil, fmt.Errorf("type '%s' not supported as map key", v.Type().Key().Kind()) + return nil, fmt.Errorf("encodeMap '%s': %w", v.Type().Key().Kind(), errNotSupportedAsMapKey) } - t := table{} + var ( + t table + emptyValueOptions valueOptions + ) iter := v.MapRange() for iter.Next() { @@ -368,9 +402,9 @@ func (enc *Encoder) encodeMap(b []byte, ctx encoderCtx, v reflect.Value) ([]byte } if table { - t.pushTable(k, v, valueOptions{}) + t.pushTable(k, v, emptyValueOptions) } else { - t.pushKV(k, v, valueOptions{}) + t.pushKV(k, v, emptyValueOptions) } } @@ -405,13 +439,10 @@ func (t *table) pushTable(k string, v reflect.Value, options valueOptions) { t.tables = append(t.tables, entry{Key: k, Value: v, Options: options}) } -func (t *table) hasKVs() bool { - return len(t.kvs) > 0 -} - func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { - t := table{} + var t table + //nolint:godox // TODO: cache this? typ := v.Type() for i := 0; i < typ.NumField(); i++ { @@ -443,7 +474,7 @@ func (enc *Encoder) encodeStruct(b []byte, ctx encoderCtx, v reflect.Value) ([]b return nil, err } - options := valueOptions{} + var options valueOptions ml, ok := fieldType.Tag.Lookup("multiline") if ok { @@ -466,38 +497,7 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro ctx.shiftKey() if ctx.insideKv { - b = append(b, '{') - - first := true - for _, kv := range t.kvs { - if first { - first = false - } else { - b = append(b, `, `...) - } - ctx.setKey(kv.Key) - b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value) - if err != nil { - return nil, err - } - } - - for _, table := range t.tables { - if first { - first = false - } else { - b = append(b, `, `...) - } - ctx.setKey(table.Key) - b, err = enc.encode(b, ctx, table.Value) - if err != nil { - return nil, err - } - b = append(b, '\n') - } - - b = append(b, "}\n"...) - return b, nil + return enc.encodeTableInsideKV(b, ctx, t) } if !ctx.skipTableHeader { @@ -510,29 +510,76 @@ func (enc *Encoder) encodeTable(b []byte, ctx encoderCtx, t table) ([]byte, erro for _, kv := range t.kvs { ctx.setKey(kv.Key) + b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value) if err != nil { return nil, err } + b = append(b, '\n') } for _, table := range t.tables { ctx.setKey(table.Key) + b, err = enc.encode(b, ctx, table.Value) if err != nil { return nil, err } + b = append(b, '\n') } return b, nil } +func (enc *Encoder) encodeTableInsideKV(b []byte, ctx encoderCtx, t table) ([]byte, error) { + var err error + + b = append(b, '{') + + first := true + for _, kv := range t.kvs { + if first { + first = false + } else { + b = append(b, `, `...) + } + + ctx.setKey(kv.Key) + + b, err = enc.encodeKv(b, ctx, kv.Options, kv.Value) + if err != nil { + return nil, err + } + } + + for _, table := range t.tables { + if first { + first = false + } else { + b = append(b, `, `...) + } + + ctx.setKey(table.Key) + + b, err = enc.encode(b, ctx, table.Value) + if err != nil { + return nil, err + } + + b = append(b, '\n') + } + + b = append(b, "}\n"...) + + return b, nil +} + var errNilInterface = errors.New("nil interface not supported") -var errNilPointer = errors.New("nil pointer not supported") func willConvertToTable(v reflect.Value) (bool, error) { + //nolint:gocritic,godox switch v.Interface().(type) { case time.Time: // TODO: add TextMarshaler return false, nil @@ -546,11 +593,13 @@ func willConvertToTable(v reflect.Value) (bool, error) { if v.IsNil() { return false, errNilInterface } + return willConvertToTable(v.Elem()) case reflect.Ptr: if v.IsNil() { return false, nil } + return willConvertToTable(v.Elem()) default: return false, nil @@ -564,6 +613,7 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) { if v.IsNil() { return false, errNilInterface } + return willConvertToTableOrArrayTable(v.Elem()) } @@ -572,15 +622,18 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) { // An empty slice should be a kv = []. return false, nil } + for i := 0; i < v.Len(); i++ { t, err := willConvertToTable(v.Index(i)) if err != nil { return false, err } + if !t { return false, nil } } + return true, nil } @@ -590,6 +643,7 @@ func willConvertToTableOrArrayTable(v reflect.Value) (bool, error) { func (enc *Encoder) encodeSlice(b []byte, ctx encoderCtx, v reflect.Value) ([]byte, error) { if v.Len() == 0 { b = append(b, "[]"...) + return b, nil } @@ -617,25 +671,30 @@ func (enc *Encoder) encodeSliceAsArrayTable(b []byte, ctx encoderCtx, v reflect. var err error scratch := make([]byte, 0, 64) scratch = append(scratch, "[["...) + for i, k := range ctx.parentKey { if i > 0 { scratch = append(scratch, '.') } + scratch, err = enc.encodeKey(scratch, k) if err != nil { return nil, err } } + scratch = append(scratch, "]]\n"...) ctx.skipTableHeader = true for i := 0; i < v.Len(); i++ { b = append(b, scratch...) + b, err = enc.encode(b, ctx, v.Index(i)) if err != nil { return nil, err } } + return b, nil } @@ -644,10 +703,12 @@ func (enc *Encoder) encodeSliceAsArray(b []byte, ctx encoderCtx, v reflect.Value var err error first := true + for i := 0; i < v.Len(); i++ { if !first { b = append(b, ", "...) } + first = false b, err = enc.encode(b, ctx, v.Index(i)) @@ -657,5 +718,6 @@ func (enc *Encoder) encodeSliceAsArray(b []byte, ctx encoderCtx, v reflect.Value } b = append(b, ']') + return b, nil } diff --git a/marshaler_test.go b/marshaler_test.go index 29f857d..b144766 100644 --- a/marshaler_test.go +++ b/marshaler_test.go @@ -11,7 +11,10 @@ import ( "github.com/stretchr/testify/require" ) +//nolint:funlen func TestMarshal(t *testing.T) { + t.Parallel() + examples := []struct { desc string v interface{} @@ -65,6 +68,7 @@ hello = 'world'`, a = 'test'`, }, { + //nolint:godox // TODO: this test is flaky because output changes depending on // the map iteration order. desc: "map in map in map and string with values", @@ -89,6 +93,16 @@ a = 'test'`, }, expected: `array = ['one', 'two', 'three']`, }, + { + desc: "empty string array", + v: map[string][]string{}, + expected: ``, + }, + { + desc: "map", + v: map[string][]string{}, + expected: ``, + }, { desc: "nested string arrays", v: map[string][][]string{ @@ -104,7 +118,7 @@ a = 'test'`, expected: `array = ['a string', ['one', 'two'], 'last']`, }, { - desc: "slice of maps", + desc: "array of maps", v: map[string][]map[string]string{ "top": { {"map1.1": "v1.1"}, @@ -157,7 +171,7 @@ K2 = 'v2' `, }, { - desc: "structs in slice with interfaces", + desc: "structs in array with interfaces", v: map[string]interface{}{ "root": map[string]interface{}{ "nested": []interface{}{ @@ -237,7 +251,10 @@ world"""`, } for _, e := range examples { + e := e t.Run(e.desc, func(t *testing.T) { + t.Parallel() + b, err := toml.Marshal(e.v) if e.err { require.Error(t, err) @@ -256,6 +273,8 @@ func equalStringsIgnoreNewlines(t *testing.T, expected string, actual string) { } func TestIssue436(t *testing.T) { + t.Parallel() + data := []byte(`{"a": [ { "b": { "c": "d" } } ]}`) var v interface{}