diff --git a/README.md b/README.md index 4e9bea3..4d54791 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ The rational from [Uber Style Guide][1]: * `encoding/xml` support * `gopkg.in/yaml.v3` support * `github.com/BurntSushi/toml` support +* `github.com/mitchellh/mapstructure` support ## 📦 Install @@ -59,7 +60,6 @@ go vet -vettool=$(which musttag) ./... ## 📅 Roadmap -* Support `mapstructure` * Support custom tags via config [1]: https://github.com/uber-go/guide/blob/master/style.md#use-field-tags-in-marshaled-structs diff --git a/go.mod b/go.mod index ed187ae..06d2585 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/BurntSushi/toml v1.2.1 + github.com/mitchellh/mapstructure v1.5.0 golang.org/x/tools v0.3.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 33067ec..a6fe22c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= diff --git a/musttag.go b/musttag.go index 20f959c..4dc9c88 100644 --- a/musttag.go +++ b/musttag.go @@ -14,6 +14,7 @@ import ( // we need these dependencies for the tests in testdata to compile. // `go mod tidy` will remove them from go.mod if we don't import them here. _ "github.com/BurntSushi/toml" + _ "github.com/mitchellh/mapstructure" _ "gopkg.in/yaml.v3" ) @@ -82,10 +83,11 @@ func run(pass *analysis.Pass) (any, error) { // look for and the expression that likely contains the struct to check. func tagAndExpr(pass *analysis.Pass, call *ast.CallExpr) (string, ast.Expr, bool) { const ( - jsonTag = "json" - xmlTag = "xml" - yamlTag = "yaml" - tomlTag = "toml" + jsonTag = "json" + xmlTag = "xml" + yamlTag = "yaml" + tomlTag = "toml" + mapstructureTag = "mapstructure" ) fn := typeutil.StaticCallee(pass.TypesInfo, call) @@ -129,6 +131,12 @@ func tagAndExpr(pass *analysis.Pass, call *ast.CallExpr) (string, ast.Expr, bool case "github.com/BurntSushi/toml.DecodeFS": return tomlTag, call.Args[2], true + case "github.com/mitchellh/mapstructure.Decode", + "github.com/mitchellh/mapstructure.DecodeMetadata", + "github.com/mitchellh/mapstructure.WeakDecode", + "github.com/mitchellh/mapstructure.WeakDecodeMetadata": + return mapstructureTag, call.Args[1], true + default: return "", nil, false } diff --git a/testdata/src/examples/examples.go b/testdata/src/examples/examples.go index ef76520..48e6f7f 100644 --- a/testdata/src/examples/examples.go +++ b/testdata/src/examples/examples.go @@ -5,6 +5,7 @@ import ( "encoding/xml" "github.com/BurntSushi/toml" + "github.com/mitchellh/mapstructure" "gopkg.in/yaml.v3" ) @@ -45,3 +46,12 @@ func exampleTOML() { toml.Decode("", &user) toml.Unmarshal(nil, &user) } + +func exampleMS() { + var user struct { // want `\Qexported fields should be annotated with the "mapstructure" tag` + Name string + Email string `mapstructure:"email"` + } + mapstructure.Decode(nil, &user) + mapstructure.WeakDecode(nil, &user) +} diff --git a/testdata/src/github.com/mitchellh/mapstructure/mapstructure.go b/testdata/src/github.com/mitchellh/mapstructure/mapstructure.go new file mode 100644 index 0000000..f0777c8 --- /dev/null +++ b/testdata/src/github.com/mitchellh/mapstructure/mapstructure.go @@ -0,0 +1,12 @@ +// Package mapstructure provides stubs for github.com/mitchellh/mapstructure. +package mapstructure + +// mapstructure.NewDecoder wants the struct pointer to be passed via +// mapstructure.DecoderConfig, that's why we do not support it for now. + +type Metadata struct{} + +func Decode(_, _ any) error { return nil } +func DecodeMetadata(_, _ any, _ *Metadata) error { return nil } +func WeakDecode(_, _ any) error { return nil } +func WeakDecodeMetadata(_, _ any, _ *Metadata) error { return nil } diff --git a/testdata/src/tests/tests.go b/testdata/src/tests/tests.go index c3a7a87..e179178 100644 --- a/testdata/src/tests/tests.go +++ b/testdata/src/tests/tests.go @@ -5,6 +5,7 @@ import ( "encoding/xml" "github.com/BurntSushi/toml" + "github.com/mitchellh/mapstructure" "gopkg.in/yaml.v3" ) @@ -36,7 +37,12 @@ func namedType() { `\Qtoml.DecodeFS` `\Qtoml.DecodeFile` `\Qtoml.Encoder.Encode` - `\Qtoml.Decoder.Decode` */ + `\Qtoml.Decoder.Decode` + + `\Qmapstructure.Decode` + `\Qmapstructure.DecodeMetadata` + `\Qmapstructure.WeakDecode` + `\Qmapstructure.WeakDecodeMetadata` */ NoTag int } var x X @@ -66,6 +72,11 @@ func namedType() { toml.DecodeFile("", &x) toml.NewEncoder(nil).Encode(X{}) toml.NewDecoder(nil).Decode(&X{}) + + mapstructure.Decode(nil, &x) + mapstructure.DecodeMetadata(nil, &x, nil) + mapstructure.WeakDecode(nil, &X{}) + mapstructure.WeakDecodeMetadata(nil, &X{}, nil) } func nestedNamedType() { @@ -94,11 +105,16 @@ func nestedNamedType() { `\Qtoml.DecodeFS` `\Qtoml.DecodeFile` `\Qtoml.Encoder.Encode` - `\Qtoml.Decoder.Decode` */ + `\Qtoml.Decoder.Decode` + + `\Qmapstructure.Decode` + `\Qmapstructure.DecodeMetadata` + `\Qmapstructure.WeakDecode` + `\Qmapstructure.WeakDecodeMetadata` */ NoTag int } type X struct { - Y Y `json:"y" xml:"y" yaml:"y" toml:"y"` + Y Y `json:"y" xml:"y" yaml:"y" toml:"y" mapstructure:"y"` } var x X @@ -127,6 +143,11 @@ func nestedNamedType() { toml.DecodeFile("", &x) toml.NewEncoder(nil).Encode(X{}) toml.NewDecoder(nil).Decode(&X{}) + + mapstructure.Decode(nil, &x) + mapstructure.DecodeMetadata(nil, &x, nil) + mapstructure.WeakDecode(nil, &X{}) + mapstructure.WeakDecodeMetadata(nil, &X{}, nil) } func anonymousType() { @@ -145,7 +166,10 @@ func anonymousType() { `\Qtoml.Unmarshal` `\Qtoml.Decode` `\Qtoml.DecodeFS` - `\Qtoml.DecodeFile` */ + `\Qtoml.DecodeFile` + + `\Qmapstructure.Decode` + `\Qmapstructure.DecodeMetadata` */ NoTag int } @@ -174,6 +198,11 @@ func anonymousType() { toml.DecodeFile("", &x) toml.NewEncoder(nil).Encode(struct{ NoTag int }{}) // want `\Qtoml.Encoder.Encode` toml.NewDecoder(nil).Decode(&struct{ NoTag int }{}) // want `\Qtoml.Decoder.Decode` + + mapstructure.Decode(nil, &x) + mapstructure.DecodeMetadata(nil, &x, nil) + mapstructure.WeakDecode(nil, &struct{ NoTag int }{}) // want `\Qmapstructure.WeakDecode` + mapstructure.WeakDecodeMetadata(nil, &struct{ NoTag int }{}, nil) // want `\Qmapstructure.WeakDecodeMetadata` } func nestedAnonymousType() { @@ -192,8 +221,11 @@ func nestedAnonymousType() { `\Qtoml.Unmarshal` `\Qtoml.Decode` `\Qtoml.DecodeFS` - `\Qtoml.DecodeFile` */ - Y *struct{ NoTag int } `json:"y" xml:"y" yaml:"y" toml:"y"` + `\Qtoml.DecodeFile` + + `\Qmapstructure.Decode` + `\Qmapstructure.DecodeMetadata` */ + Y *struct{ NoTag int } `json:"y" xml:"y" yaml:"y" toml:"y" mapstructure:"y"` } json.Marshal(x) @@ -221,14 +253,19 @@ func nestedAnonymousType() { toml.DecodeFile("", &x) toml.NewEncoder(nil).Encode(struct{ Y struct{ NoTag int } }{}) // want `\Qtoml.Encoder.Encode` toml.NewDecoder(nil).Decode(&struct{ Y struct{ NoTag int } }{}) // want `\Qtoml.Decoder.Decode` + + mapstructure.Decode(nil, &x) + mapstructure.DecodeMetadata(nil, &x, nil) + mapstructure.WeakDecode(nil, &struct{ Y struct{ NoTag int } }{}) // want `\Qmapstructure.WeakDecode` + mapstructure.WeakDecodeMetadata(nil, &struct{ Y struct{ NoTag int } }{}, nil) // want `\Qmapstructure.WeakDecodeMetadata` } // all good, nothing to report. func typeWithAllTags() { var x struct { - Y int `json:"y" xml:"y" yaml:"y" toml:"y"` - Z int `json:"z" xml:"z" yaml:"z" toml:"z"` - Nested struct{} `json:"nested" xml:"nested" yaml:"nested" toml:"nested"` + Y int `json:"y" xml:"y" yaml:"y" toml:"y" mapstructure:"y"` + Z int `json:"z" xml:"z" yaml:"z" toml:"z" mapstructure:"z"` + Nested struct{} `json:"nested" xml:"nested" yaml:"nested" toml:"nested" mapstructure:"nested"` private int } @@ -257,6 +294,11 @@ func typeWithAllTags() { toml.DecodeFile("", &x) toml.NewEncoder(nil).Encode(x) toml.NewDecoder(nil).Decode(&x) + + mapstructure.Decode(nil, &x) + mapstructure.DecodeMetadata(nil, &x, nil) + mapstructure.WeakDecode(nil, &x) + mapstructure.WeakDecodeMetadata(nil, &x, nil) } // non-static calls should be ignored. @@ -276,6 +318,9 @@ func nonStaticCalls() { unmarshalTOML := toml.Unmarshal unmarshalTOML(nil, &x) + + decodeMS := mapstructure.Decode + decodeMS(nil, &x) } // non-struct argument calls should be ignored. @@ -305,4 +350,9 @@ func nonStructArgument() { toml.DecodeFile("", &[]int{}) toml.NewEncoder(nil).Encode(map[int]int{}) toml.NewDecoder(nil).Decode(&map[int]int{}) + + mapstructure.Decode(nil, &[]int{}) + mapstructure.DecodeMetadata(nil, &[]int{}, nil) + mapstructure.WeakDecode(nil, &map[int]int{}) + mapstructure.WeakDecodeMetadata(nil, &map[int]int{}, nil) }