mirror of
https://github.com/golang/go.git
synced 2026-04-03 09:49:56 +09:00
cmd/go/internal/doc: support @version suffix on first argument
This change allows `go doc` to display documentation for packages outside the workspace by explicitly providing a version (e.g., `go doc pkg@version`) or by inferring it from an installed command. Therefore, all of the following are now valid: ``` go doc txtar@v0.13.0 go doc txtar@v0.13.0 FS go doc txtar.FS@v0.13.0 go doc golang.org/x/tools/txtar@v0.13.0 go doc golang.org/x/tools/txtar@v0.13.0 FS go doc golang.org/x/tools/txtar.FS@v0.13.0 ``` Fixes #63696. Change-Id: I22fb68e29c7f62bbe0bb240b82e5b789edc653f2 Reviewed-on: https://go-review.googlesource.com/c/go/+/747380 Reviewed-by: Michael Matloob <matloob@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Michael Matloob <matloob@google.com>
This commit is contained in:
@@ -151,7 +151,7 @@ func runDoc(ctx context.Context, cmd *base.Command, args []string) {
|
||||
log.SetPrefix("doc: ")
|
||||
dirsInit()
|
||||
var flagSet flag.FlagSet
|
||||
err := do(os.Stdout, &flagSet, args)
|
||||
err := do(ctx, os.Stdout, &flagSet, args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func usage(flagSet *flag.FlagSet) {
|
||||
}
|
||||
|
||||
// do is the workhorse, broken out of runDoc to make testing easier.
|
||||
func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
|
||||
func do(ctx context.Context, writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
|
||||
flagSet.Usage = func() { usage(flagSet) }
|
||||
unexported = false
|
||||
matchCase = false
|
||||
@@ -234,7 +234,7 @@ func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
|
||||
// Loop until something is printed.
|
||||
dirs.Reset()
|
||||
for i := 0; ; i++ {
|
||||
buildPackage, userPath, sym, more := parseArgs(flagSet, flagSet.Args())
|
||||
buildPackage, userPath, sym, more := parseArgs(ctx, flagSet, flagSet.Args())
|
||||
if i > 0 && !more { // Ignore the "more" bit on the first iteration.
|
||||
return failMessage(paths, symbol, method)
|
||||
}
|
||||
@@ -356,7 +356,7 @@ func failMessage(paths []string, symbol, method string) error {
|
||||
// and there may be more matches. For example, if the argument
|
||||
// is rand.Float64, we must scan both crypto/rand and math/rand
|
||||
// to find the symbol, and the first call will return crypto/rand, true.
|
||||
func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path, symbol string, more bool) {
|
||||
func parseArgs(ctx context.Context, flagSet *flag.FlagSet, args []string) (pkg *build.Package, path, symbol string, more bool) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -366,12 +366,29 @@ func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path,
|
||||
return importDir(wd), "", "", false
|
||||
}
|
||||
arg := args[0]
|
||||
|
||||
var version string
|
||||
if i := strings.Index(arg, "@"); i >= 0 {
|
||||
arg, version = arg[:i], arg[i+1:]
|
||||
}
|
||||
|
||||
// We have an argument. If it is a directory name beginning with . or ..,
|
||||
// use the absolute path name. This discriminates "./errors" from "errors"
|
||||
// if the current directory contains a non-standard errors package.
|
||||
if isDotSlash(arg) {
|
||||
arg = filepath.Join(wd, arg)
|
||||
}
|
||||
if version != "" && (build.IsLocalImport(filepath.ToSlash(arg)) || filepath.IsAbs(arg)) {
|
||||
log.Fatal("cannot use @version with local or absolute paths")
|
||||
}
|
||||
|
||||
importPkg := func(p string) (*build.Package, error) {
|
||||
if version != "" {
|
||||
return loadVersioned(ctx, p, version)
|
||||
}
|
||||
return build.Import(p, wd, build.ImportComment)
|
||||
}
|
||||
|
||||
switch len(args) {
|
||||
default:
|
||||
usage(flagSet)
|
||||
@@ -379,20 +396,29 @@ func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path,
|
||||
// Done below.
|
||||
case 2:
|
||||
// Package must be findable and importable.
|
||||
pkg, err := build.Import(args[0], wd, build.ImportComment)
|
||||
pkg, err := importPkg(arg)
|
||||
if err == nil {
|
||||
return pkg, args[0], args[1], false
|
||||
return pkg, arg, args[1], false
|
||||
}
|
||||
for {
|
||||
packagePath, ok := findNextPackage(arg)
|
||||
dir, importPath, ok := findNextPackage(arg)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if pkg, err := build.ImportDir(packagePath, build.ImportComment); err == nil {
|
||||
return pkg, arg, args[1], true
|
||||
if version != "" {
|
||||
if pkg, err = loadVersioned(ctx, importPath, version); err == nil {
|
||||
return pkg, arg, args[1], true
|
||||
}
|
||||
} else {
|
||||
if pkg, err = build.ImportDir(dir, build.ImportComment); err == nil {
|
||||
return pkg, arg, args[1], true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, args[0], args[1], false
|
||||
if version != "" {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return nil, arg, args[1], false
|
||||
}
|
||||
// Usual case: one argument.
|
||||
// If it contains slashes, it begins with either a package path
|
||||
@@ -407,7 +433,7 @@ func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path,
|
||||
return pkg, arg, "", false
|
||||
}
|
||||
} else {
|
||||
pkg, importErr = build.Import(arg, wd, build.ImportComment)
|
||||
pkg, importErr = importPkg(arg)
|
||||
if importErr == nil {
|
||||
return pkg, arg, "", false
|
||||
}
|
||||
@@ -443,7 +469,7 @@ func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path,
|
||||
symbol = arg[period+1:]
|
||||
}
|
||||
// Have we identified a package already?
|
||||
pkg, err := build.Import(arg[0:period], wd, build.ImportComment)
|
||||
pkg, err := importPkg(arg[0:period])
|
||||
if err == nil {
|
||||
return pkg, arg[0:period], symbol, false
|
||||
}
|
||||
@@ -451,16 +477,43 @@ func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path,
|
||||
// or ivy/value for robpike.io/ivy/value.
|
||||
pkgName := arg[:period]
|
||||
for {
|
||||
path, ok := findNextPackage(pkgName)
|
||||
dir, importPath, ok := findNextPackage(pkgName)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if pkg, err = build.ImportDir(path, build.ImportComment); err == nil {
|
||||
return pkg, arg[0:period], symbol, true
|
||||
if version != "" {
|
||||
if pkg, err = loadVersioned(ctx, importPath, version); err == nil {
|
||||
return pkg, arg[0:period], symbol, true
|
||||
}
|
||||
} else {
|
||||
if pkg, err = build.ImportDir(dir, build.ImportComment); err == nil {
|
||||
return pkg, arg[0:period], symbol, true
|
||||
}
|
||||
}
|
||||
}
|
||||
dirs.Reset() // Next iteration of for loop must scan all the directories again.
|
||||
}
|
||||
|
||||
// Try inference from $PATH before giving up.
|
||||
if slash < 0 && !isDotSlash(arg) && !filepath.IsAbs(arg) {
|
||||
if pkgPath, v, ok := inferVersion(arg); ok {
|
||||
if version == "" {
|
||||
version = v
|
||||
}
|
||||
pkg, err := loadVersioned(ctx, pkgPath, version)
|
||||
if err == nil {
|
||||
return pkg, pkgPath, "", false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if version != "" {
|
||||
if importErr != nil {
|
||||
log.Fatal(importErr)
|
||||
}
|
||||
log.Fatalf("no such package %q at version %q", arg, version)
|
||||
}
|
||||
|
||||
// If it has a slash, we've failed.
|
||||
if slash >= 0 {
|
||||
// build.Import should always include the path in its error message,
|
||||
@@ -541,28 +594,29 @@ func isExported(name string) bool {
|
||||
return unexported || token.IsExported(name)
|
||||
}
|
||||
|
||||
// findNextPackage returns the next full file name path that matches the
|
||||
// (perhaps partial) package path pkg. The boolean reports if any match was found.
|
||||
func findNextPackage(pkg string) (string, bool) {
|
||||
// findNextPackage returns the next full file name path and import path that
|
||||
// matches the (perhaps partial) package path pkg. The boolean reports if
|
||||
// any match was found.
|
||||
func findNextPackage(pkg string) (string, string, bool) {
|
||||
if filepath.IsAbs(pkg) {
|
||||
if dirs.offset == 0 {
|
||||
dirs.offset = -1
|
||||
return pkg, true
|
||||
return pkg, "", true
|
||||
}
|
||||
return "", false
|
||||
return "", "", false
|
||||
}
|
||||
if pkg == "" || token.IsExported(pkg) { // Upper case symbol cannot be a package name.
|
||||
return "", false
|
||||
return "", "", false
|
||||
}
|
||||
pkg = path.Clean(pkg)
|
||||
pkgSuffix := "/" + pkg
|
||||
for {
|
||||
d, ok := dirs.Next()
|
||||
if !ok {
|
||||
return "", false
|
||||
return "", "", false
|
||||
}
|
||||
if d.importPath == pkg || strings.HasSuffix(d.importPath, pkgSuffix) {
|
||||
return d.dir, true
|
||||
return d.dir, d.importPath, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -910,7 +910,7 @@ func TestDoc(t *testing.T) {
|
||||
var flagSet flag.FlagSet
|
||||
var logbuf bytes.Buffer
|
||||
log.SetOutput(&logbuf)
|
||||
err := do(&b, &flagSet, test.args)
|
||||
err := do(t.Context(), &b, &flagSet, test.args)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %v: %s\n", test.name, test.args, err)
|
||||
}
|
||||
@@ -963,7 +963,7 @@ func TestMultiplePackages(t *testing.T) {
|
||||
// Make sure crypto/rand does not have the symbol.
|
||||
{
|
||||
var flagSet flag.FlagSet
|
||||
err := do(&b, &flagSet, []string{"crypto/rand.float64"})
|
||||
err := do(t.Context(), &b, &flagSet, []string{"crypto/rand.float64"})
|
||||
if err == nil {
|
||||
t.Errorf("expected error from crypto/rand.float64")
|
||||
} else if !strings.Contains(err.Error(), "no symbol float64") {
|
||||
@@ -973,7 +973,7 @@ func TestMultiplePackages(t *testing.T) {
|
||||
// Make sure math/rand does have the symbol.
|
||||
{
|
||||
var flagSet flag.FlagSet
|
||||
err := do(&b, &flagSet, []string{"math/rand.float64"})
|
||||
err := do(t.Context(), &b, &flagSet, []string{"math/rand.float64"})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %q from math/rand.float64", err)
|
||||
}
|
||||
@@ -981,7 +981,7 @@ func TestMultiplePackages(t *testing.T) {
|
||||
// Try the shorthand.
|
||||
{
|
||||
var flagSet flag.FlagSet
|
||||
err := do(&b, &flagSet, []string{"rand.float64"})
|
||||
err := do(t.Context(), &b, &flagSet, []string{"rand.float64"})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %q from rand.float64", err)
|
||||
}
|
||||
@@ -989,7 +989,7 @@ func TestMultiplePackages(t *testing.T) {
|
||||
// Now try a missing symbol. We should see both packages in the error.
|
||||
{
|
||||
var flagSet flag.FlagSet
|
||||
err := do(&b, &flagSet, []string{"rand.doesnotexit"})
|
||||
err := do(t.Context(), &b, &flagSet, []string{"rand.doesnotexit"})
|
||||
if err == nil {
|
||||
t.Errorf("expected error from rand.doesnotexit")
|
||||
} else {
|
||||
@@ -1027,21 +1027,21 @@ func TestTwoArgLookup(t *testing.T) {
|
||||
var b bytes.Buffer // We don't care about the output.
|
||||
{
|
||||
var flagSet flag.FlagSet
|
||||
err := do(&b, &flagSet, []string{"binary", "BigEndian"})
|
||||
err := do(t.Context(), &b, &flagSet, []string{"binary", "BigEndian"})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %q from binary BigEndian", err)
|
||||
}
|
||||
}
|
||||
{
|
||||
var flagSet flag.FlagSet
|
||||
err := do(&b, &flagSet, []string{"rand", "Float64"})
|
||||
err := do(t.Context(), &b, &flagSet, []string{"rand", "Float64"})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %q from rand Float64", err)
|
||||
}
|
||||
}
|
||||
{
|
||||
var flagSet flag.FlagSet
|
||||
err := do(&b, &flagSet, []string{"bytes", "Foo"})
|
||||
err := do(t.Context(), &b, &flagSet, []string{"bytes", "Foo"})
|
||||
if err == nil {
|
||||
t.Errorf("expected error from bytes Foo")
|
||||
} else if !strings.Contains(err.Error(), "no symbol Foo") {
|
||||
@@ -1050,7 +1050,7 @@ func TestTwoArgLookup(t *testing.T) {
|
||||
}
|
||||
{
|
||||
var flagSet flag.FlagSet
|
||||
err := do(&b, &flagSet, []string{"nosuchpackage", "Foo"})
|
||||
err := do(t.Context(), &b, &flagSet, []string{"nosuchpackage", "Foo"})
|
||||
if err == nil {
|
||||
// actually present in the user's filesystem
|
||||
} else if !strings.Contains(err.Error(), "no such package") {
|
||||
@@ -1071,7 +1071,7 @@ func TestDotSlashLookup(t *testing.T) {
|
||||
|
||||
var b strings.Builder
|
||||
var flagSet flag.FlagSet
|
||||
err := do(&b, &flagSet, []string{"./template"})
|
||||
err := do(t.Context(), &b, &flagSet, []string{"./template"})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %q from ./template", err)
|
||||
}
|
||||
@@ -1089,7 +1089,7 @@ func TestNoPackageClauseWhenNoMatch(t *testing.T) {
|
||||
maybeSkip(t)
|
||||
var b strings.Builder
|
||||
var flagSet flag.FlagSet
|
||||
err := do(&b, &flagSet, []string{"template.ZZZ"})
|
||||
err := do(t.Context(), &b, &flagSet, []string{"template.ZZZ"})
|
||||
// Expect an error.
|
||||
if err == nil {
|
||||
t.Error("expect an error for template.zzz")
|
||||
|
||||
54
src/cmd/go/internal/doc/mod.go
Normal file
54
src/cmd/go/internal/doc/mod.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright 2024 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 doc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"debug/buildinfo"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"os/exec"
|
||||
|
||||
"cmd/go/internal/load"
|
||||
"cmd/go/internal/modload"
|
||||
)
|
||||
|
||||
// loadVersioned loads a package at a specific version.
|
||||
func loadVersioned(ctx context.Context, pkgPath, version string) (*build.Package, error) {
|
||||
loaderState := modload.NewState()
|
||||
loaderState.ForceUseModules = true
|
||||
loaderState.RootMode = modload.NoRoot
|
||||
modload.Init(loaderState)
|
||||
|
||||
var opts load.PackageOpts
|
||||
args := []string{
|
||||
fmt.Sprintf("%s@%s", pkgPath, version),
|
||||
}
|
||||
pkgs, err := load.PackagesAndErrorsOutsideModule(loaderState, ctx, opts, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pkgs) != 1 {
|
||||
return nil, fmt.Errorf("incorrect number of packages: want 1, got %d", len(pkgs))
|
||||
}
|
||||
return pkgs[0].Internal.Build, nil
|
||||
}
|
||||
|
||||
// inferVersion checks if the argument matches a command on $PATH and returns its module path and version.
|
||||
func inferVersion(arg string) (pkgPath, version string, ok bool) {
|
||||
path, err := exec.LookPath(arg)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
bi, err := buildinfo.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", "", false
|
||||
}
|
||||
if bi.Main.Path == "" || bi.Main.Version == "" {
|
||||
return "", "", false
|
||||
}
|
||||
// bi.Path is the package path for the main package.
|
||||
return bi.Path, bi.Main.Version, true
|
||||
}
|
||||
95
src/cmd/go/testdata/script/issue_63696.txt
vendored
Normal file
95
src/cmd/go/testdata/script/issue_63696.txt
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
go mod init m
|
||||
go get rsc.io/quote@latest
|
||||
go doc rsc.io/quote
|
||||
cmp stdout want-latest.txt
|
||||
go doc rsc.io/quote@latest
|
||||
cmp stdout want-latest.txt
|
||||
go doc rsc.io/quote@v1.0.0
|
||||
cmp stdout want-1.0.0.txt
|
||||
go doc rsc.io/quote@v1.1.0
|
||||
cmp stdout want-1.1.0.txt
|
||||
go doc rsc.io/quote@v1.2.0
|
||||
cmp stdout want-1.2.0.txt
|
||||
# Test short format
|
||||
go doc quote@latest
|
||||
cmp stdout want-latest.txt
|
||||
go doc quote@v1.0.0
|
||||
cmp stdout want-1.0.0.txt
|
||||
go doc quote@v1.1.0
|
||||
cmp stdout want-1.1.0.txt
|
||||
go doc quote@v1.2.0
|
||||
cmp stdout want-1.2.0.txt
|
||||
go doc quote
|
||||
cmp stdout want-latest.txt
|
||||
go doc quote@v1.0.0 Hello
|
||||
cmp stdout hello-1.0.0.txt
|
||||
go doc quote@v1.1.0 Glass
|
||||
cmp stdout glass-1.1.0.txt
|
||||
go doc quote@v1.2.0 Go
|
||||
cmp stdout go-1.2.0.txt
|
||||
go doc quote@v1.3.0 Opt
|
||||
cmp stdout opt-1.3.0.txt
|
||||
-- main.go --
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"rsc.io/quote"
|
||||
)
|
||||
func main() {
|
||||
fmt.Println(quote.Hello())
|
||||
}
|
||||
-- want-latest.txt --
|
||||
package quote // import "rsc.io/quote"
|
||||
|
||||
Package quote collects pithy sayings.
|
||||
|
||||
func Glass() string
|
||||
func Go() string
|
||||
func Hello() string
|
||||
func Opt() string
|
||||
-- want-1.0.0.txt --
|
||||
package quote // import "rsc.io/quote"
|
||||
|
||||
Package quote collects pithy sayings.
|
||||
|
||||
func Hello() string
|
||||
-- want-1.1.0.txt --
|
||||
package quote // import "rsc.io/quote"
|
||||
|
||||
Package quote collects pithy sayings.
|
||||
|
||||
func Glass() string
|
||||
func Hello() string
|
||||
-- want-1.2.0.txt --
|
||||
package quote // import "rsc.io/quote"
|
||||
|
||||
Package quote collects pithy sayings.
|
||||
|
||||
func Glass() string
|
||||
func Go() string
|
||||
func Hello() string
|
||||
-- hello-1.0.0.txt --
|
||||
package quote // import "rsc.io/quote"
|
||||
|
||||
func Hello() string
|
||||
Hello returns a greeting.
|
||||
|
||||
-- glass-1.1.0.txt --
|
||||
package quote // import "rsc.io/quote"
|
||||
|
||||
func Glass() string
|
||||
Glass returns a useful phrase for world travelers.
|
||||
|
||||
-- go-1.2.0.txt --
|
||||
package quote // import "rsc.io/quote"
|
||||
|
||||
func Go() string
|
||||
Go returns a Go proverb.
|
||||
|
||||
-- opt-1.3.0.txt --
|
||||
package quote // import "rsc.io/quote"
|
||||
|
||||
func Opt() string
|
||||
Opt returns an optimization truth.
|
||||
|
||||
Reference in New Issue
Block a user