From 7aa9855704a766a1a16fbed79f9f6e34fc83bdf0 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Tue, 7 Aug 2018 15:52:04 -0400 Subject: [PATCH] cmd/go: add go mod why A very common question is "why is this package or module being kept by go mod vendor or go mod tidy?" go mod why answers that question. Fixes #26620. Change-Id: Iac3b6bbdf703b4784f5eed8e0f69d41325bc6d7f Reviewed-on: https://go-review.googlesource.com/128359 Reviewed-by: Bryan C. Mills --- src/cmd/go/alldocs.go | 58 +++++++++ src/cmd/go/internal/modcmd/mod.go | 1 + src/cmd/go/internal/modcmd/why.go | 119 ++++++++++++++++++ src/cmd/go/internal/modload/load.go | 45 +++++++ ...ext_v0.0.0-20170915032832-14c0d48ead0c.txt | 2 + .../testdata/mod/golang.org_x_text_v0.3.0.txt | 2 + src/cmd/go/testdata/script/mod_why.txt | 114 +++++++++++++++++ 7 files changed, 341 insertions(+) create mode 100644 src/cmd/go/internal/modcmd/why.go create mode 100644 src/cmd/go/testdata/script/mod_why.txt diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go index a4a66efcf5..aea77175e8 100644 --- a/src/cmd/go/alldocs.go +++ b/src/cmd/go/alldocs.go @@ -871,6 +871,7 @@ // // The commands are: // +// download download modules to local cache // edit edit go.mod from tools or scripts // fix make go.mod semantically consistent // graph print module requirement graph @@ -878,9 +879,42 @@ // tidy add missing and remove unused modules // vendor make vendored copy of dependencies // verify verify dependencies have expected content +// why explain why packages or modules are needed // // Use "go help mod " for more information about a command. // +// Download modules to local cache +// +// Usage: +// +// go mod download [-dir] [-json] [modules] +// +// Download downloads the named modules, which can be module patterns selecting +// dependencies of the main module or module queries of the form path@version. +// With no arguments, download applies to all dependencies of the main module. +// +// The go command will automatically download modules as needed during ordinary +// execution. The "go mod download" command is useful mainly for pre-filling +// the local cache or to compute the answers for a Go module proxy. +// +// By default, download reports errors to standard error but is otherwise silent. +// The -json flag causes download to print a sequence of JSON objects +// to standard output, describing each downloaded module (or failure), +// corresponding to this Go struct: +// +// type Module struct { +// Path string // module path +// Version string // module version +// Error string // error loading module +// Info string // absolute path to cached .info file +// GoMod string // absolute path to cached .mod file +// Zip string // absolute path to cached .zip file +// Dir string // absolute path to cached source root directory +// } +// +// See 'go help module' for more about module queries. +// +// // Edit go.mod from tools or scripts // // Usage: @@ -1079,6 +1113,30 @@ // non-zero status. // // +// Explain why packages or modules are needed +// +// Usage: +// +// go mod why [-m] [-vendor] packages... +// +// Why shows a shortest path in the import graph from the main module to +// each of the listed packages. If the -m flag is given, why treats the +// arguments as a list of modules and finds a path to any package in each +// of the modules. +// +// By default, why queries the graph of packages matched by "go list all", +// which includes tests for reachable packages. The -vendor flag causes why +// to exclude tests of dependencies. +// +// The output is a sequence of stanzas, one for each package or module +// name on the command line, separated by blank lines. Each stanza begins +// with a comment line "# package" or "# module" giving the target +// package or module. Subsequent lines give a path through the import +// graph, one package per line. If the package or module is not +// referenced from the main module the stanza will be empty except for +// the comment line. +// +// // Compile and run Go program // // Usage: diff --git a/src/cmd/go/internal/modcmd/mod.go b/src/cmd/go/internal/modcmd/mod.go index 0f78cc3b41..c1d0c13a10 100644 --- a/src/cmd/go/internal/modcmd/mod.go +++ b/src/cmd/go/internal/modcmd/mod.go @@ -27,5 +27,6 @@ See 'go help modules' for an overview of module functionality. cmdTidy, cmdVendor, cmdVerify, + cmdWhy, }, } diff --git a/src/cmd/go/internal/modcmd/why.go b/src/cmd/go/internal/modcmd/why.go new file mode 100644 index 0000000000..6923685599 --- /dev/null +++ b/src/cmd/go/internal/modcmd/why.go @@ -0,0 +1,119 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package modcmd + +import ( + "cmd/go/internal/base" + "cmd/go/internal/modload" + "cmd/go/internal/module" + "fmt" + "strings" +) + +var cmdWhy = &base.Command{ + UsageLine: "go mod why [-m] [-vendor] packages...", + Short: "explain why packages or modules are needed", + Long: ` +Why shows a shortest path in the import graph from the main module to +each of the listed packages. If the -m flag is given, why treats the +arguments as a list of modules and finds a path to any package in each +of the modules. + +By default, why queries the graph of packages matched by "go list all", +which includes tests for reachable packages. The -vendor flag causes why +to exclude tests of dependencies. + +The output is a sequence of stanzas, one for each package or module +name on the command line, separated by blank lines. Each stanza begins +with a comment line "# package" or "# module" giving the target +package or module. Subsequent lines give a path through the import +graph, one package per line. If the package or module is not +referenced from the main module, the stanza will display a single +parenthesized note indicating that fact. + +For example: + + $ go mod why golang.org/x/text/language golang.org/x/text/encoding + # golang.org/x/text/language + rsc.io/quote + rsc.io/sampler + golang.org/x/text/language + + # golang.org/x/text/encoding + (main module does not need package golang.org/x/text/encoding) + $ + `, +} + +var ( + whyM = cmdWhy.Flag.Bool("m", false, "") + whyVendor = cmdWhy.Flag.Bool("vendor", false, "") +) + +func init() { + cmdWhy.Run = runWhy // break init cycle +} + +func runWhy(cmd *base.Command, args []string) { + loadALL := modload.LoadALL + if *whyVendor { + loadALL = modload.LoadVendor + } + if *whyM { + listU := false + listVersions := false + for _, arg := range args { + if strings.Contains(arg, "@") { + base.Fatalf("go mod why: module query not allowed") + } + } + mods := modload.ListModules(args, listU, listVersions) + byModule := make(map[module.Version][]string) + for _, path := range loadALL() { + m := modload.PackageModule(path) + if m.Path != "" { + byModule[m] = append(byModule[m], path) + } + } + sep := "" + for _, m := range mods { + best := "" + bestDepth := 1000000000 + for _, path := range byModule[module.Version{Path: m.Path, Version: m.Version}] { + d := modload.WhyDepth(path) + if d > 0 && d < bestDepth { + best = path + bestDepth = d + } + } + why := modload.Why(best) + if why == "" { + vendoring := "" + if *whyVendor { + vendoring = " to vendor" + } + why = "(main module does not need" + vendoring + " module " + m.Path + ")\n" + } + fmt.Printf("%s# %s\n%s", sep, m.Path, why) + sep = "\n" + } + } else { + pkgs := modload.ImportPaths(args) // resolve to packages + loadALL() // rebuild graph, from main module (not from named packages) + sep := "" + for _, path := range pkgs { + why := modload.Why(path) + if why == "" { + vendoring := "" + if *whyVendor { + vendoring = " to vendor" + } + why = "(main module does not need" + vendoring + " package " + path + ")\n" + } + fmt.Printf("%s# %s\n%s", sep, path, why) + sep = "\n" + } + } +} diff --git a/src/cmd/go/internal/modload/load.go b/src/cmd/go/internal/modload/load.go index d15832bdea..b42d0d2e50 100644 --- a/src/cmd/go/internal/modload/load.go +++ b/src/cmd/go/internal/modload/load.go @@ -671,6 +671,51 @@ func (pkg *loadPkg) stackText() string { return buf.String() } +// why returns the text to use in "go mod why" output about the given package. +// It is less ornate than the stackText but conatins the same information. +func (pkg *loadPkg) why() string { + var buf strings.Builder + var stack []*loadPkg + for p := pkg; p != nil; p = p.stack { + stack = append(stack, p) + } + + for i := len(stack) - 1; i >= 0; i-- { + p := stack[i] + if p.testOf != nil { + fmt.Fprintf(&buf, "%s.test\n", p.testOf.path) + } else { + fmt.Fprintf(&buf, "%s\n", p.path) + } + } + return buf.String() +} + +// Why returns the "go mod why" output stanza for the given package, +// without the leading # comment. +// The package graph must have been loaded already, usually by LoadALL. +// If there is no reason for the package to be in the current build, +// Why returns an empty string. +func Why(path string) string { + pkg, ok := loaded.pkgCache.Get(path).(*loadPkg) + if !ok { + return "" + } + return pkg.why() +} + +// WhyDepth returns the number of steps in the Why listing. +// If there is no reason for the package to be in the current build, +// WhyDepth returns 0. +func WhyDepth(path string) int { + n := 0 + pkg, _ := loaded.pkgCache.Get(path).(*loadPkg) + for p := pkg; p != nil; p = p.stack { + n++ + } + return n +} + // Replacement returns the replacement for mod, if any, from go.mod. // If there is no replacement for mod, Replacement returns // a module.Version with Path == "". diff --git a/src/cmd/go/testdata/mod/golang.org_x_text_v0.0.0-20170915032832-14c0d48ead0c.txt b/src/cmd/go/testdata/mod/golang.org_x_text_v0.0.0-20170915032832-14c0d48ead0c.txt index e03b3ce081..f4f50cdedb 100644 --- a/src/cmd/go/testdata/mod/golang.org_x_text_v0.0.0-20170915032832-14c0d48ead0c.txt +++ b/src/cmd/go/testdata/mod/golang.org_x_text_v0.0.0-20170915032832-14c0d48ead0c.txt @@ -6,6 +6,8 @@ module golang.org/x/text {"Version":"v0.0.0-20170915032832-14c0d48ead0c","Name":"v0.0.0-20170915032832-14c0d48ead0c","Short":"14c0d48ead0c","Time":"2017-09-15T03:28:32Z"} -- go.mod -- module golang.org/x/text +-- unused/unused.go -- +package unused -- language/lang.go -- // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style diff --git a/src/cmd/go/testdata/mod/golang.org_x_text_v0.3.0.txt b/src/cmd/go/testdata/mod/golang.org_x_text_v0.3.0.txt index 6932642cd6..5561afae8e 100644 --- a/src/cmd/go/testdata/mod/golang.org_x_text_v0.3.0.txt +++ b/src/cmd/go/testdata/mod/golang.org_x_text_v0.3.0.txt @@ -6,6 +6,8 @@ module golang.org/x/text {"Version":"v0.3.0","Name":"","Short":"","Time":"2017-09-16T03:28:32Z"} -- go.mod -- module golang.org/x/text +-- unused/unused.go -- +package unused -- language/lang.go -- // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style diff --git a/src/cmd/go/testdata/script/mod_why.txt b/src/cmd/go/testdata/script/mod_why.txt new file mode 100644 index 0000000000..4d556fc73f --- /dev/null +++ b/src/cmd/go/testdata/script/mod_why.txt @@ -0,0 +1,114 @@ +env GO111MODULE=on + +go list -test all +stdout rsc.io/quote +stdout golang.org/x/text/language + +# why a package? +go mod why golang.org/x/text/language +cmp stdout why-language.txt + +# why a module? +go mod why -m golang.org... +cmp stdout why-text-module.txt + +# why a package used only in tests? +go mod why rsc.io/testonly +cmp stdout why-testonly.txt + +# why a module used only in tests? +go mod why -m rsc.io/testonly +cmp stdout why-testonly.txt + +# test package not needed +go mod why golang.org/x/text/unused +cmp stdout why-unused.txt + +# vendor doesn't use packages used only in tests. +go mod why -vendor rsc.io/testonly +cmp stdout why-vendor.txt + +# vendor doesn't use modules used only in tests. +go mod why -vendor -m rsc.io/testonly +cmp stdout why-vendor-module.txt + +# test multiple packages +go mod why golang.org/x/text/language golang.org/x/text/unused +cmp stdout why-both.txt + +# test multiple modules +go mod why -m rsc.io/quote rsc.io/sampler +cmp stdout why-both-module.txt + +-- go.mod -- +module mymodule +require rsc.io/quote v1.5.2 + +-- x/x.go -- +package x +import _ "mymodule/z" + +-- y/y.go -- +package y + +-- y/y_test.go -- +package y +import _ "rsc.io/quote" + +-- z/z.go -- +package z +import _ "mymodule/y" + + +-- why-language.txt -- +# golang.org/x/text/language +mymodule/y +mymodule/y.test +rsc.io/quote +rsc.io/sampler +golang.org/x/text/language +-- why-unused.txt -- +# golang.org/x/text/unused +(main module does not need package golang.org/x/text/unused) +-- why-text-module.txt -- +# golang.org/x/text +mymodule/y +mymodule/y.test +rsc.io/quote +rsc.io/sampler +golang.org/x/text/language +-- why-testonly.txt -- +# rsc.io/testonly +mymodule/y +mymodule/y.test +rsc.io/quote +rsc.io/sampler +rsc.io/sampler.test +rsc.io/testonly +-- why-vendor.txt -- +# rsc.io/testonly +(main module does not need to vendor package rsc.io/testonly) +-- why-vendor-module.txt -- +# rsc.io/testonly +(main module does not need to vendor module rsc.io/testonly) +-- why-both.txt -- +# golang.org/x/text/language +mymodule/y +mymodule/y.test +rsc.io/quote +rsc.io/sampler +golang.org/x/text/language + +# golang.org/x/text/unused +(main module does not need package golang.org/x/text/unused) +-- why-both-module.txt -- +# rsc.io/quote +mymodule/y +mymodule/y.test +rsc.io/quote + +# rsc.io/sampler +mymodule/y +mymodule/y.test +rsc.io/quote +rsc.io/sampler