From ec4687f337465b719efdeef72b357fa0b05879bb Mon Sep 17 00:00:00 2001 From: Michael Matloob Date: Wed, 26 Jan 2022 15:31:20 -0500 Subject: [PATCH] cmd/go: allow users to specify required fields in JSON output For #29666 Change-Id: Ibae3d75bb2c19571c8d473cb47d6c4b3a880bba8 Reviewed-on: https://go-review.googlesource.com/c/go/+/381035 Trust: Michael Matloob Run-TryBot: Michael Matloob Reviewed-by: Bryan Mills TryBot-Result: Gopher Robot --- src/cmd/go/alldocs.go | 5 +- src/cmd/go/internal/list/list.go | 98 +++++++++++++++---- .../go/testdata/script/list_json_fields.txt | 52 ++++++++++ 3 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 src/cmd/go/testdata/script/list_json_fields.txt diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go index 63e7900e02..2bd2fb6fbc 100644 --- a/src/cmd/go/alldocs.go +++ b/src/cmd/go/alldocs.go @@ -869,7 +869,10 @@ // for the go/build package's Context type. // // The -json flag causes the package data to be printed in JSON format -// instead of using the template format. +// instead of using the template format. The JSON flag can optionally be +// provided with a set of comma-separated required field names to be output. +// If so, those required fields will always appear in JSON output, but +// others may be omitted to save work in computing the JSON struct. // // The -compiled flag causes list to set CompiledGoFiles to the Go source // files presented to the compiler. Typically this means that it repeats diff --git a/src/cmd/go/internal/list/list.go b/src/cmd/go/internal/list/list.go index 8be9211935..9cebb934bf 100644 --- a/src/cmd/go/internal/list/list.go +++ b/src/cmd/go/internal/list/list.go @@ -13,7 +13,9 @@ import ( "fmt" "io" "os" + "reflect" "sort" + "strconv" "strings" "text/template" @@ -157,7 +159,10 @@ For more information about the meaning of these fields see the documentation for the go/build package's Context type. The -json flag causes the package data to be printed in JSON format -instead of using the template format. +instead of using the template format. The JSON flag can optionally be +provided with a set of comma-separated required field names to be output. +If so, those required fields will always appear in JSON output, but +others may be omitted to save work in computing the JSON struct. The -compiled flag causes list to set CompiledGoFiles to the Go source files presented to the compiler. Typically this means that it repeats @@ -316,29 +321,79 @@ For more about modules, see https://golang.org/ref/mod. func init() { CmdList.Run = runList // break init cycle work.AddBuildFlags(CmdList, work.DefaultBuildFlags) + CmdList.Flag.Var(&listJsonFields, "json", "") } var ( - listCompiled = CmdList.Flag.Bool("compiled", false, "") - listDeps = CmdList.Flag.Bool("deps", false, "") - listE = CmdList.Flag.Bool("e", false, "") - listExport = CmdList.Flag.Bool("export", false, "") - listFmt = CmdList.Flag.String("f", "", "") - listFind = CmdList.Flag.Bool("find", false, "") - listJson = CmdList.Flag.Bool("json", false, "") - listM = CmdList.Flag.Bool("m", false, "") - listRetracted = CmdList.Flag.Bool("retracted", false, "") - listTest = CmdList.Flag.Bool("test", false, "") - listU = CmdList.Flag.Bool("u", false, "") - listVersions = CmdList.Flag.Bool("versions", false, "") + listCompiled = CmdList.Flag.Bool("compiled", false, "") + listDeps = CmdList.Flag.Bool("deps", false, "") + listE = CmdList.Flag.Bool("e", false, "") + listExport = CmdList.Flag.Bool("export", false, "") + listFmt = CmdList.Flag.String("f", "", "") + listFind = CmdList.Flag.Bool("find", false, "") + listJson bool + listJsonFields jsonFlag // If not empty, only output these fields. + listM = CmdList.Flag.Bool("m", false, "") + listRetracted = CmdList.Flag.Bool("retracted", false, "") + listTest = CmdList.Flag.Bool("test", false, "") + listU = CmdList.Flag.Bool("u", false, "") + listVersions = CmdList.Flag.Bool("versions", false, "") ) +// A StringsFlag is a command-line flag that interprets its argument +// as a space-separated list of possibly-quoted strings. +type jsonFlag map[string]bool + +func (v *jsonFlag) Set(s string) error { + if v, err := strconv.ParseBool(s); err == nil { + listJson = v + return nil + } + listJson = true + if *v == nil { + *v = make(map[string]bool) + } + for _, f := range strings.Split(s, ",") { + (*v)[f] = true + } + return nil +} + +func (v *jsonFlag) String() string { + var fields []string + for f := range *v { + fields = append(fields, f) + } + sort.Strings(fields) + return strings.Join(fields, ",") +} + +func (v *jsonFlag) IsBoolFlag() bool { + return true +} + +func (v *jsonFlag) needAll() bool { + return len(*v) == 0 +} + +func (v *jsonFlag) needAny(fields ...string) bool { + if v.needAll() { + return true + } + for _, f := range fields { + if (*v)[f] { + return true + } + } + return false +} + var nl = []byte{'\n'} func runList(ctx context.Context, cmd *base.Command, args []string) { modload.InitWorkfile() - if *listFmt != "" && *listJson == true { + if *listFmt != "" && listJson == true { base.Fatalf("go list -f cannot be used with -json") } @@ -357,9 +412,18 @@ func runList(ctx context.Context, cmd *base.Command, args []string) { } } - var do func(any) - if *listJson { + var do func(x any) + if listJson { do = func(x any) { + if !listJsonFields.needAll() { + v := reflect.ValueOf(x).Elem() // do is always called with a non-nil pointer. + // Clear all non-requested fields. + for i := 0; i < v.NumField(); i++ { + if !listJsonFields.needAny(v.Type().Field(i).Name) { + v.Field(i).Set(reflect.Zero(v.Type().Field(i).Type)) + } + } + } b, err := json.MarshalIndent(x, "", "\t") if err != nil { out.Flush() @@ -589,7 +653,7 @@ func runList(ctx context.Context, cmd *base.Command, args []string) { } // Do we need to run a build to gather information? - needStale := *listJson || strings.Contains(*listFmt, ".Stale") + needStale := (listJson && listJsonFields.needAny("Stale", "StaleReason")) || strings.Contains(*listFmt, ".Stale") if needStale || *listExport || *listCompiled { var b work.Builder b.Init() diff --git a/src/cmd/go/testdata/script/list_json_fields.txt b/src/cmd/go/testdata/script/list_json_fields.txt new file mode 100644 index 0000000000..58c9efa162 --- /dev/null +++ b/src/cmd/go/testdata/script/list_json_fields.txt @@ -0,0 +1,52 @@ +# Test using -json flag to specify specific fields. + +# Test -json produces "full" output by looking for multiple fields present. +go list -json . +stdout '"Name": "a"' +stdout '"Stale": true' +# Same thing for -json=true +go list -json=true . +stdout '"Name": "a"' +stdout '"Stale": true' + +# Test -json=false produces non-json output. +go list -json=false +cmp stdout want-non-json.txt + +# Test -json= keeps only that field. +go list -json=Name +cmp stdout want-json-name.txt + +# Test -json= with multiple fields. +go list -json=ImportPath,Name,GoFiles,Imports +cmp stdout want-json-multiple.txt + +-- go.mod -- +module example.com/a + +go 1.18 +-- a.go -- +package a + +import "fmt" + +func F() { + fmt.Println("hey there") +} +-- want-non-json.txt -- +example.com/a +-- want-json-name.txt -- +{ + "Name": "a" +} +-- want-json-multiple.txt -- +{ + "ImportPath": "example.com/a", + "Name": "a", + "GoFiles": [ + "a.go" + ], + "Imports": [ + "fmt" + ] +} -- 2.50.0