Commit 7aa98557 authored by Russ Cox's avatar Russ Cox

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/128359Reviewed-by: default avatarBryan C. Mills <bcmills@google.com>
parent a4749604
......@@ -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 <command>" 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:
......
......@@ -27,5 +27,6 @@ See 'go help modules' for an overview of module functionality.
cmdTidy,
cmdVendor,
cmdVerify,
cmdWhy,
},
}
// 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"
}
}
}
......@@ -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 == "".
......
......@@ -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
......
......@@ -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
......
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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment