cmd/doc: build cmd/doc directly into the go command

There are a couple of places where our tests expect that 'go doc'
doesn't need to do a build. Invoke the cmd/doc code directly by the go
command instead of starting the doc tool in a separate process so we can
preserve that property.

This change moves most of the doc code into the package
cmd/internal/doc, and exposes a Main function from that function that's
called both by the cmd/doc package, and by go doc.

This change makes couple of additional changes to intergrate doc into
the go command:

The counter.Open call and the increment of invocations counter are only
needed by cmd/doc. The go command will open the counters file and
increment a counter for the doc subcommand.

We add a cmd_go_bootstrap tagged variant of the file that defines go doc
so that we don't end up linking net into the bootstrap version of the go
command. We don't need doc in that version of the command.

We create a new flagSet rather than using flag.CommandLine because when
running as part of the go command, the flags to "go doc" won't be the top
level flags.

We change TestGoListTest in go_test.go to use gofmt instead of doc as an
example of a main package in cmd with an in-package test.

For #71867

Change-Id: I3e3df83e5fa266559606fdc086b461165e09f037
Reviewed-on: https://go-review.googlesource.com/c/go/+/677775
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Reviewed-by: Michael Matloob <matloob@google.com>
This commit is contained in:
Michael Matloob 2025-05-30 18:20:05 -04:00 committed by Michael Matloob
parent 74b70eead7
commit 94764d0938
16 changed files with 97 additions and 76 deletions

55
src/cmd/doc/doc.go Normal file
View file

@ -0,0 +1,55 @@
// Copyright 2015 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.
// Doc (usually run as go doc) accepts zero, one or two arguments.
//
// Zero arguments:
//
// go doc
//
// Show the documentation for the package in the current directory.
//
// One argument:
//
// go doc <pkg>
// go doc <sym>[.<methodOrField>]
// go doc [<pkg>.]<sym>[.<methodOrField>]
// go doc [<pkg>.][<sym>.]<methodOrField>
//
// The first item in this list that succeeds is the one whose documentation
// is printed. If there is a symbol but no package, the package in the current
// directory is chosen. However, if the argument begins with a capital
// letter it is always assumed to be a symbol in the current directory.
//
// Two arguments:
//
// go doc <pkg> <sym>[.<methodOrField>]
//
// Show the documentation for the package, symbol, and method or field. The
// first argument must be a full package path. This is similar to the
// command-line usage for the godoc command.
//
// For commands, unless the -cmd flag is present "go doc command"
// shows only the package-level docs for the package.
//
// The -src flag causes doc to print the full source code for the symbol, such
// as the body of a struct, function or method.
//
// The -all flag causes doc to print all documentation for the package and
// all its visible symbols. The argument must identify a package.
//
// For complete documentation, run "go help doc".
package main
import (
"cmd/internal/doc"
"cmd/internal/telemetry/counter"
"os"
)
func main() {
counter.Open()
counter.Inc("doc/invocations")
doc.Main(os.Args[1:])
}

View file

@ -1093,10 +1093,10 @@ func TestGoListTest(t *testing.T) {
tg.grepStdoutNot(`^testing \[bytes.test\]$`, "unexpected test copy of testing") tg.grepStdoutNot(`^testing \[bytes.test\]$`, "unexpected test copy of testing")
tg.grepStdoutNot(`^testing$`, "unexpected real copy of testing") tg.grepStdoutNot(`^testing$`, "unexpected real copy of testing")
tg.run("list", "-test", "cmd/buildid", "cmd/doc") tg.run("list", "-test", "cmd/buildid", "cmd/gofmt")
tg.grepStdout(`^cmd/buildid$`, "missing cmd/buildid") tg.grepStdout(`^cmd/buildid$`, "missing cmd/buildid")
tg.grepStdout(`^cmd/doc$`, "missing cmd/doc") tg.grepStdout(`^cmd/gofmt$`, "missing cmd/gofmt")
tg.grepStdout(`^cmd/doc\.test$`, "missing cmd/doc test") tg.grepStdout(`^cmd/gofmt\.test$`, "missing cmd/gofmt test")
tg.grepStdoutNot(`^cmd/buildid\.test$`, "unexpected cmd/buildid test") tg.grepStdoutNot(`^cmd/buildid\.test$`, "unexpected cmd/buildid test")
tg.grepStdoutNot(`^testing`, "unexpected testing") tg.grepStdoutNot(`^testing`, "unexpected testing")

View file

@ -2,17 +2,15 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !cmd_go_bootstrap
// Package doc implements the “go doc” command. // Package doc implements the “go doc” command.
package doc package doc
import ( import (
"cmd/go/internal/base" "cmd/go/internal/base"
"cmd/go/internal/cfg" "cmd/internal/doc"
"context" "context"
"errors"
"os"
"os/exec"
"path/filepath"
) )
var CmdDoc = &base.Command{ var CmdDoc = &base.Command{
@ -134,13 +132,5 @@ Flags:
} }
func runDoc(ctx context.Context, cmd *base.Command, args []string) { func runDoc(ctx context.Context, cmd *base.Command, args []string) {
base.StartSigHandlers() doc.Main(args)
err := base.RunErr(cfg.BuildToolexec, filepath.Join(cfg.GOROOTbin, "go"), "tool", "doc", args)
if err != nil {
var ee *exec.ExitError
if errors.As(err, &ee) {
os.Exit(ee.ExitCode())
}
base.Error(err)
}
} }

View file

@ -0,0 +1,13 @@
// Copyright 2025 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.
//go:build cmd_go_bootstrap
// Don't build cmd/doc into go_bootstrap because it depends on net.
package doc
import "cmd/go/internal/base"
var CmdDoc = &base.Command{}

View file

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package main package doc
import ( import (
"bytes" "bytes"

View file

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package main package doc
import ( import (
"bytes" "bytes"
@ -90,7 +90,7 @@ type test struct {
no []string // Regular expressions that should not match. no []string // Regular expressions that should not match.
} }
const p = "cmd/doc/testdata" const p = "cmd/internal/doc/testdata"
var tests = []test{ var tests = []test{
// Sanity check. // Sanity check.
@ -105,7 +105,7 @@ var tests = []test{
{ {
"package clause", "package clause",
[]string{p}, []string{p},
[]string{`package pkg.*cmd/doc/testdata`}, []string{`package pkg.*cmd/internal/doc/testdata`},
nil, nil,
}, },

View file

@ -2,45 +2,8 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// Doc (usually run as go doc) accepts zero, one or two arguments. // Package doc provides the implementation of the "go doc" subcommand and cmd/doc.
// package doc
// Zero arguments:
//
// go doc
//
// Show the documentation for the package in the current directory.
//
// One argument:
//
// go doc <pkg>
// go doc <sym>[.<methodOrField>]
// go doc [<pkg>.]<sym>[.<methodOrField>]
// go doc [<pkg>.][<sym>.]<methodOrField>
//
// The first item in this list that succeeds is the one whose documentation
// is printed. If there is a symbol but no package, the package in the current
// directory is chosen. However, if the argument begins with a capital
// letter it is always assumed to be a symbol in the current directory.
//
// Two arguments:
//
// go doc <pkg> <sym>[.<methodOrField>]
//
// Show the documentation for the package, symbol, and method or field. The
// first argument must be a full package path. This is similar to the
// command-line usage for the godoc command.
//
// For commands, unless the -cmd flag is present "go doc command"
// shows only the package-level docs for the package.
//
// The -src flag causes doc to print the full source code for the symbol, such
// as the body of a struct, function or method.
//
// The -all flag causes doc to print all documentation for the package and
// all its visible symbols. The argument must identify a package.
//
// For complete documentation, run "go help doc".
package main
import ( import (
"bytes" "bytes"
@ -74,7 +37,7 @@ var (
) )
// usage is a replacement usage function for the flags package. // usage is a replacement usage function for the flags package.
func usage() { func usage(flagSet *flag.FlagSet) {
fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n") fmt.Fprintf(os.Stderr, "Usage of [go] doc:\n")
fmt.Fprintf(os.Stderr, "\tgo doc\n") fmt.Fprintf(os.Stderr, "\tgo doc\n")
fmt.Fprintf(os.Stderr, "\tgo doc <pkg>\n") fmt.Fprintf(os.Stderr, "\tgo doc <pkg>\n")
@ -85,16 +48,17 @@ func usage() {
fmt.Fprintf(os.Stderr, "For more information run\n") fmt.Fprintf(os.Stderr, "For more information run\n")
fmt.Fprintf(os.Stderr, "\tgo help doc\n\n") fmt.Fprintf(os.Stderr, "\tgo help doc\n\n")
fmt.Fprintf(os.Stderr, "Flags:\n") fmt.Fprintf(os.Stderr, "Flags:\n")
flag.PrintDefaults() flagSet.PrintDefaults()
os.Exit(2) os.Exit(2)
} }
func main() { // Main is the entry point, invoked both by go doc and cmd/doc.
func Main(args []string) {
log.SetFlags(0) log.SetFlags(0)
log.SetPrefix("doc: ") log.SetPrefix("doc: ")
counter.Open()
dirsInit() dirsInit()
err := do(os.Stdout, flag.CommandLine, os.Args[1:]) var flagSet flag.FlagSet
err := do(os.Stdout, &flagSet, args)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -102,7 +66,7 @@ func main() {
// do is the workhorse, broken out of main to make testing easier. // do is the workhorse, broken out of main to make testing easier.
func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) { func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
flagSet.Usage = usage flagSet.Usage = func() { usage(flagSet) }
unexported = false unexported = false
matchCase = false matchCase = false
flagSet.StringVar(&chdir, "C", "", "change to `dir` before running command") flagSet.StringVar(&chdir, "C", "", "change to `dir` before running command")
@ -114,7 +78,6 @@ func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol") flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol")
flagSet.BoolVar(&serveHTTP, "http", false, "serve HTML docs over HTTP") flagSet.BoolVar(&serveHTTP, "http", false, "serve HTML docs over HTTP")
flagSet.Parse(args) flagSet.Parse(args)
counter.Inc("doc/invocations")
counter.CountFlags("doc/flag:", *flag.CommandLine) counter.CountFlags("doc/flag:", *flag.CommandLine)
if chdir != "" { if chdir != "" {
if err := os.Chdir(chdir); err != nil { if err := os.Chdir(chdir); err != nil {
@ -151,7 +114,7 @@ func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
// Loop until something is printed. // Loop until something is printed.
dirs.Reset() dirs.Reset()
for i := 0; ; i++ { for i := 0; ; i++ {
buildPackage, userPath, sym, more := parseArgs(flagSet.Args()) buildPackage, userPath, sym, more := parseArgs(flagSet, flagSet.Args())
if i > 0 && !more { // Ignore the "more" bit on the first iteration. if i > 0 && !more { // Ignore the "more" bit on the first iteration.
return failMessage(paths, symbol, method) return failMessage(paths, symbol, method)
} }
@ -165,7 +128,7 @@ func do(writer io.Writer, flagSet *flag.FlagSet, args []string) (err error) {
unexported = true unexported = true
} }
symbol, method = parseSymbol(sym) symbol, method = parseSymbol(flagSet, sym)
pkg := parsePackage(writer, buildPackage, userPath) pkg := parsePackage(writer, buildPackage, userPath)
paths = append(paths, pkg.prettyPath()) paths = append(paths, pkg.prettyPath())
@ -338,7 +301,7 @@ func failMessage(paths []string, symbol, method string) error {
// and there may be more matches. For example, if the argument // and there may be more matches. For example, if the argument
// is rand.Float64, we must scan both crypto/rand and math/rand // 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. // to find the symbol, and the first call will return crypto/rand, true.
func parseArgs(args []string) (pkg *build.Package, path, symbol string, more bool) { func parseArgs(flagSet *flag.FlagSet, args []string) (pkg *build.Package, path, symbol string, more bool) {
wd, err := os.Getwd() wd, err := os.Getwd()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -356,7 +319,7 @@ func parseArgs(args []string) (pkg *build.Package, path, symbol string, more boo
} }
switch len(args) { switch len(args) {
default: default:
usage() usage(flagSet)
case 1: case 1:
// Done below. // Done below.
case 2: case 2:
@ -499,7 +462,7 @@ func importDir(dir string) *build.Package {
// parseSymbol breaks str apart into a symbol and method. // parseSymbol breaks str apart into a symbol and method.
// Both may be missing or the method may be missing. // Both may be missing or the method may be missing.
// If present, each must be a valid Go identifier. // If present, each must be a valid Go identifier.
func parseSymbol(str string) (symbol, method string) { func parseSymbol(flagSet *flag.FlagSet, str string) (symbol, method string) {
if str == "" { if str == "" {
return return
} }
@ -510,7 +473,7 @@ func parseSymbol(str string) (symbol, method string) {
method = elem[1] method = elem[1]
default: default:
log.Printf("too many periods in symbol specification") log.Printf("too many periods in symbol specification")
usage() usage(flagSet)
} }
symbol = elem[0] symbol = elem[0]
return return

View file

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package main package doc
import ( import (
"bufio" "bufio"

View file

@ -4,7 +4,7 @@
//go:build plan9 || windows //go:build plan9 || windows
package main package doc
import ( import (
"os" "os"

View file

@ -4,7 +4,7 @@
//go:build unix || js || wasip1 //go:build unix || js || wasip1
package main package doc
import ( import (
"os" "os"