Skip to content

Commit 7aa9855

Browse files
committed
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 <[email protected]>
1 parent a474960 commit 7aa9855

File tree

7 files changed

+341
-0
lines changed

7 files changed

+341
-0
lines changed

src/cmd/go/alldocs.go

+58
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cmd/go/internal/modcmd/mod.go

+1
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ See 'go help modules' for an overview of module functionality.
2727
cmdTidy,
2828
cmdVendor,
2929
cmdVerify,
30+
cmdWhy,
3031
},
3132
}

src/cmd/go/internal/modcmd/why.go

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package modcmd
6+
7+
import (
8+
"cmd/go/internal/base"
9+
"cmd/go/internal/modload"
10+
"cmd/go/internal/module"
11+
"fmt"
12+
"strings"
13+
)
14+
15+
var cmdWhy = &base.Command{
16+
UsageLine: "go mod why [-m] [-vendor] packages...",
17+
Short: "explain why packages or modules are needed",
18+
Long: `
19+
Why shows a shortest path in the import graph from the main module to
20+
each of the listed packages. If the -m flag is given, why treats the
21+
arguments as a list of modules and finds a path to any package in each
22+
of the modules.
23+
24+
By default, why queries the graph of packages matched by "go list all",
25+
which includes tests for reachable packages. The -vendor flag causes why
26+
to exclude tests of dependencies.
27+
28+
The output is a sequence of stanzas, one for each package or module
29+
name on the command line, separated by blank lines. Each stanza begins
30+
with a comment line "# package" or "# module" giving the target
31+
package or module. Subsequent lines give a path through the import
32+
graph, one package per line. If the package or module is not
33+
referenced from the main module, the stanza will display a single
34+
parenthesized note indicating that fact.
35+
36+
For example:
37+
38+
$ go mod why golang.org/x/text/language golang.org/x/text/encoding
39+
# golang.org/x/text/language
40+
rsc.io/quote
41+
rsc.io/sampler
42+
golang.org/x/text/language
43+
44+
# golang.org/x/text/encoding
45+
(main module does not need package golang.org/x/text/encoding)
46+
$
47+
`,
48+
}
49+
50+
var (
51+
whyM = cmdWhy.Flag.Bool("m", false, "")
52+
whyVendor = cmdWhy.Flag.Bool("vendor", false, "")
53+
)
54+
55+
func init() {
56+
cmdWhy.Run = runWhy // break init cycle
57+
}
58+
59+
func runWhy(cmd *base.Command, args []string) {
60+
loadALL := modload.LoadALL
61+
if *whyVendor {
62+
loadALL = modload.LoadVendor
63+
}
64+
if *whyM {
65+
listU := false
66+
listVersions := false
67+
for _, arg := range args {
68+
if strings.Contains(arg, "@") {
69+
base.Fatalf("go mod why: module query not allowed")
70+
}
71+
}
72+
mods := modload.ListModules(args, listU, listVersions)
73+
byModule := make(map[module.Version][]string)
74+
for _, path := range loadALL() {
75+
m := modload.PackageModule(path)
76+
if m.Path != "" {
77+
byModule[m] = append(byModule[m], path)
78+
}
79+
}
80+
sep := ""
81+
for _, m := range mods {
82+
best := ""
83+
bestDepth := 1000000000
84+
for _, path := range byModule[module.Version{Path: m.Path, Version: m.Version}] {
85+
d := modload.WhyDepth(path)
86+
if d > 0 && d < bestDepth {
87+
best = path
88+
bestDepth = d
89+
}
90+
}
91+
why := modload.Why(best)
92+
if why == "" {
93+
vendoring := ""
94+
if *whyVendor {
95+
vendoring = " to vendor"
96+
}
97+
why = "(main module does not need" + vendoring + " module " + m.Path + ")\n"
98+
}
99+
fmt.Printf("%s# %s\n%s", sep, m.Path, why)
100+
sep = "\n"
101+
}
102+
} else {
103+
pkgs := modload.ImportPaths(args) // resolve to packages
104+
loadALL() // rebuild graph, from main module (not from named packages)
105+
sep := ""
106+
for _, path := range pkgs {
107+
why := modload.Why(path)
108+
if why == "" {
109+
vendoring := ""
110+
if *whyVendor {
111+
vendoring = " to vendor"
112+
}
113+
why = "(main module does not need" + vendoring + " package " + path + ")\n"
114+
}
115+
fmt.Printf("%s# %s\n%s", sep, path, why)
116+
sep = "\n"
117+
}
118+
}
119+
}

src/cmd/go/internal/modload/load.go

+45
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,51 @@ func (pkg *loadPkg) stackText() string {
671671
return buf.String()
672672
}
673673

674+
// why returns the text to use in "go mod why" output about the given package.
675+
// It is less ornate than the stackText but conatins the same information.
676+
func (pkg *loadPkg) why() string {
677+
var buf strings.Builder
678+
var stack []*loadPkg
679+
for p := pkg; p != nil; p = p.stack {
680+
stack = append(stack, p)
681+
}
682+
683+
for i := len(stack) - 1; i >= 0; i-- {
684+
p := stack[i]
685+
if p.testOf != nil {
686+
fmt.Fprintf(&buf, "%s.test\n", p.testOf.path)
687+
} else {
688+
fmt.Fprintf(&buf, "%s\n", p.path)
689+
}
690+
}
691+
return buf.String()
692+
}
693+
694+
// Why returns the "go mod why" output stanza for the given package,
695+
// without the leading # comment.
696+
// The package graph must have been loaded already, usually by LoadALL.
697+
// If there is no reason for the package to be in the current build,
698+
// Why returns an empty string.
699+
func Why(path string) string {
700+
pkg, ok := loaded.pkgCache.Get(path).(*loadPkg)
701+
if !ok {
702+
return ""
703+
}
704+
return pkg.why()
705+
}
706+
707+
// WhyDepth returns the number of steps in the Why listing.
708+
// If there is no reason for the package to be in the current build,
709+
// WhyDepth returns 0.
710+
func WhyDepth(path string) int {
711+
n := 0
712+
pkg, _ := loaded.pkgCache.Get(path).(*loadPkg)
713+
for p := pkg; p != nil; p = p.stack {
714+
n++
715+
}
716+
return n
717+
}
718+
674719
// Replacement returns the replacement for mod, if any, from go.mod.
675720
// If there is no replacement for mod, Replacement returns
676721
// a module.Version with Path == "".

src/cmd/go/testdata/mod/golang.org_x_text_v0.0.0-20170915032832-14c0d48ead0c.txt

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ module golang.org/x/text
66
{"Version":"v0.0.0-20170915032832-14c0d48ead0c","Name":"v0.0.0-20170915032832-14c0d48ead0c","Short":"14c0d48ead0c","Time":"2017-09-15T03:28:32Z"}
77
-- go.mod --
88
module golang.org/x/text
9+
-- unused/unused.go --
10+
package unused
911
-- language/lang.go --
1012
// Copyright 2018 The Go Authors. All rights reserved.
1113
// Use of this source code is governed by a BSD-style

src/cmd/go/testdata/mod/golang.org_x_text_v0.3.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ module golang.org/x/text
66
{"Version":"v0.3.0","Name":"","Short":"","Time":"2017-09-16T03:28:32Z"}
77
-- go.mod --
88
module golang.org/x/text
9+
-- unused/unused.go --
10+
package unused
911
-- language/lang.go --
1012
// Copyright 2018 The Go Authors. All rights reserved.
1113
// Use of this source code is governed by a BSD-style
+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
env GO111MODULE=on
2+
3+
go list -test all
4+
stdout rsc.io/quote
5+
stdout golang.org/x/text/language
6+
7+
# why a package?
8+
go mod why golang.org/x/text/language
9+
cmp stdout why-language.txt
10+
11+
# why a module?
12+
go mod why -m golang.org...
13+
cmp stdout why-text-module.txt
14+
15+
# why a package used only in tests?
16+
go mod why rsc.io/testonly
17+
cmp stdout why-testonly.txt
18+
19+
# why a module used only in tests?
20+
go mod why -m rsc.io/testonly
21+
cmp stdout why-testonly.txt
22+
23+
# test package not needed
24+
go mod why golang.org/x/text/unused
25+
cmp stdout why-unused.txt
26+
27+
# vendor doesn't use packages used only in tests.
28+
go mod why -vendor rsc.io/testonly
29+
cmp stdout why-vendor.txt
30+
31+
# vendor doesn't use modules used only in tests.
32+
go mod why -vendor -m rsc.io/testonly
33+
cmp stdout why-vendor-module.txt
34+
35+
# test multiple packages
36+
go mod why golang.org/x/text/language golang.org/x/text/unused
37+
cmp stdout why-both.txt
38+
39+
# test multiple modules
40+
go mod why -m rsc.io/quote rsc.io/sampler
41+
cmp stdout why-both-module.txt
42+
43+
-- go.mod --
44+
module mymodule
45+
require rsc.io/quote v1.5.2
46+
47+
-- x/x.go --
48+
package x
49+
import _ "mymodule/z"
50+
51+
-- y/y.go --
52+
package y
53+
54+
-- y/y_test.go --
55+
package y
56+
import _ "rsc.io/quote"
57+
58+
-- z/z.go --
59+
package z
60+
import _ "mymodule/y"
61+
62+
63+
-- why-language.txt --
64+
# golang.org/x/text/language
65+
mymodule/y
66+
mymodule/y.test
67+
rsc.io/quote
68+
rsc.io/sampler
69+
golang.org/x/text/language
70+
-- why-unused.txt --
71+
# golang.org/x/text/unused
72+
(main module does not need package golang.org/x/text/unused)
73+
-- why-text-module.txt --
74+
# golang.org/x/text
75+
mymodule/y
76+
mymodule/y.test
77+
rsc.io/quote
78+
rsc.io/sampler
79+
golang.org/x/text/language
80+
-- why-testonly.txt --
81+
# rsc.io/testonly
82+
mymodule/y
83+
mymodule/y.test
84+
rsc.io/quote
85+
rsc.io/sampler
86+
rsc.io/sampler.test
87+
rsc.io/testonly
88+
-- why-vendor.txt --
89+
# rsc.io/testonly
90+
(main module does not need to vendor package rsc.io/testonly)
91+
-- why-vendor-module.txt --
92+
# rsc.io/testonly
93+
(main module does not need to vendor module rsc.io/testonly)
94+
-- why-both.txt --
95+
# golang.org/x/text/language
96+
mymodule/y
97+
mymodule/y.test
98+
rsc.io/quote
99+
rsc.io/sampler
100+
golang.org/x/text/language
101+
102+
# golang.org/x/text/unused
103+
(main module does not need package golang.org/x/text/unused)
104+
-- why-both-module.txt --
105+
# rsc.io/quote
106+
mymodule/y
107+
mymodule/y.test
108+
rsc.io/quote
109+
110+
# rsc.io/sampler
111+
mymodule/y
112+
mymodule/y.test
113+
rsc.io/quote
114+
rsc.io/sampler

0 commit comments

Comments
 (0)