diff options
| author | Nick White <git@njw.name> | 2026-02-10 08:26:25 +0000 |
|---|---|---|
| committer | Michael Matloob <matloob@golang.org> | 2026-03-27 07:58:59 -0700 |
| commit | 7f2e2fd71f0b25fde6031b8317f525f1a2739e9b (patch) | |
| tree | 52c3edfccbc6400fd26f7447823ea7163269e7d1 /src | |
| parent | 31a839621976f968a997fd8578b6c5fd074bbc0c (diff) | |
| download | go-7f2e2fd71f0b25fde6031b8317f525f1a2739e9b.tar.xz | |
cmd/go: add example support to go doc output
The go doc command now includes a -ex flag to list executable examples.
It will also print the code and expected output of an example when
passed its name.
Fixes #26715
Change-Id: I34b09403cc3cb45655939bd4fe27accec0e141f5
Reviewed-on: https://go-review.googlesource.com/c/go/+/445116
Reviewed-by: Rob Pike <r@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Matloob <matloob@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Michael Matloob <matloob@google.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/cmd/go/alldocs.go | 2 | ||||
| -rw-r--r-- | src/cmd/go/internal/doc/doc.go | 4 | ||||
| -rw-r--r-- | src/cmd/go/internal/doc/doc_test.go | 137 | ||||
| -rw-r--r-- | src/cmd/go/internal/doc/pkg.go | 124 | ||||
| -rw-r--r-- | src/cmd/go/internal/doc/testdata/pkg_test.go | 47 | ||||
| -rw-r--r-- | src/cmd/go/internal/doc/testdata/play_test.go | 13 |
6 files changed, 312 insertions, 15 deletions
diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go index 4be78b33bb..e394ef836a 100644 --- a/src/cmd/go/alldocs.go +++ b/src/cmd/go/alldocs.go @@ -452,6 +452,8 @@ // Treat a command (package main) like a regular package. // Otherwise package main's exported symbols are hidden // when showing the package's top-level documentation. +// -ex +// Include executable examples. // -http // Serve HTML docs over HTTP. // -short diff --git a/src/cmd/go/internal/doc/doc.go b/src/cmd/go/internal/doc/doc.go index a68c8cb3a3..fd6d663375 100644 --- a/src/cmd/go/internal/doc/doc.go +++ b/src/cmd/go/internal/doc/doc.go @@ -129,6 +129,8 @@ Flags: Treat a command (package main) like a regular package. Otherwise package main's exported symbols are hidden when showing the package's top-level documentation. + -ex + Include executable examples. -http Serve HTML docs over HTTP. -short @@ -163,6 +165,7 @@ var ( chdir string // -C flag showAll bool // -all flag showCmd bool // -cmd flag + showEx bool // -ex flag showSrc bool // -src flag short bool // -short flag serveHTTP bool // -http flag @@ -191,6 +194,7 @@ func do(ctx context.Context, writer io.Writer, flagSet *flag.FlagSet, args []str flagSet.BoolVar(&unexported, "u", false, "show unexported symbols as well as exported") flagSet.BoolVar(&matchCase, "c", false, "symbol matching honors case (paths not affected)") flagSet.BoolVar(&showAll, "all", false, "show all documentation for package") + flagSet.BoolVar(&showEx, "ex", false, "show executable examples for symbol or package") flagSet.BoolVar(&showCmd, "cmd", false, "show symbols with package docs even if package is a command") flagSet.BoolVar(&showSrc, "src", false, "show source code for symbol") flagSet.BoolVar(&short, "short", false, "one-line representation for each symbol") diff --git a/src/cmd/go/internal/doc/doc_test.go b/src/cmd/go/internal/doc/doc_test.go index bb415f6991..3eeaffc25b 100644 --- a/src/cmd/go/internal/doc/doc_test.go +++ b/src/cmd/go/internal/doc/doc_test.go @@ -750,6 +750,143 @@ var tests = []test{ []string{`other fields elided`}, }, + // Package with -ex. + { + "package with -ex", + []string{`-ex`, p}, + []string{ + `func ExampleExportedFunc\(\)`, + `func ExampleExportedType\(\)`, + `func Example\(\)`, + `func Example_multiline\(\)`, + `func Example_playable\(\)`, + }, + nil, + }, + + // Type with -ex. + { + "type with -ex", + []string{`-ex`, p, `ExportedType`}, + []string{ + `func ExampleExportedType\(\)`, + `func ExampleExportedType_ExportedMethod\(\)`, + }, + nil, + }, + + // Function with -ex. + { + "function with -ex", + []string{`-ex`, p, `ExportedFunc`}, + []string{ + `func ExampleExportedFunc\(\)`, + `Function example.`, + `func ExampleExportedFunc_two\(\)`, + `Function example two.`, + }, + nil, + }, + + // Method with -ex. + { + "method with -ex", + []string{`-ex`, p, `ExportedType.ExportedMethod`}, + []string{ + `func ExampleExportedType_ExportedMethod\(\)`, + `Method example.`, + }, + nil, + }, + + // Package example. + { + "package example", + []string{p, `Example`}, + []string{ + `fmt.Println\("Package example output"\)`, + `Output: Package example output`, + }, + []string{`func Example\(\)`}, + }, + + // Multiline output example. + { + "multiline output example", + []string{p, `Example_multiline`}, + []string{ + `fmt.Println\("Multiline\\nexample\\noutput"\)`, + "Output: \nMultiline\nexample\noutput", + }, + []string{`Output: Multiline example output`}, + }, + + // Type example. + { + "type example", + []string{p, `ExampleExportedType`}, + []string{ + `fmt.Println\("Type example output"\)`, + `Output: Type example output`, + }, + []string{`func ExampleExportedType\(\)`}, + }, + + // Function example. + { + "function example", + []string{p, `ExampleExportedFunc`}, + []string{ + `fmt.Println\("Function example output"\)`, + `Output: Function example output`, + }, + []string{ + `func ExampleExportedFunc\(\)`, + `fmt.Println\("Function example two output"\)`, + `Output: Function example two output`, + }, + }, + + // Function example two. + { + "function example", + []string{p, `ExampleExportedFunc_two`}, + []string{ + `fmt.Println\("Function example two output"\)`, + `Output: Function example two output`, + }, + []string{ + `func ExampleExportedFunc_two\(\)`, + `fmt.Println\("Function example output"\)`, + `Output: Function example output`, + }, + }, + + // Method example. + { + "method example", + []string{p, `ExampleExportedType_ExportedMethod`}, + []string{ + `fmt.Println\("Method example output"\)`, + `Output: Method example output`, + }, + []string{`func ExampleExportedType_ExportedMethod\(\)`}, + }, + + // Playable example. + { + "playable example", + []string{p, `Example_playable`}, + []string{ + `package main`, + `func main\(\) {`, + `fmt.Println\("Playable example output"\)`, + `}`, + `Output: Playable example output`, + }, + []string{`func Example_playable\(\)`}, + }, + // Case matching off. { "case matching off", diff --git a/src/cmd/go/internal/doc/pkg.go b/src/cmd/go/internal/doc/pkg.go index 3c36d0e05c..c531595f86 100644 --- a/src/cmd/go/internal/doc/pkg.go +++ b/src/cmd/go/internal/doc/pkg.go @@ -139,17 +139,16 @@ func (pkg *Package) Fatalf(format string, args ...any) { // we can then use to generate documentation. func parsePackage(writer io.Writer, pkg *build.Package, userPath string) *Package { // include tells parser.ParseDir which files to include. - // That means the file must be in the build package's GoFiles or CgoFiles - // list only (no tag-ignored files, tests, swig or other non-Go files). + // That means the file must be in the build package's GoFiles, CgoFiles, + // TestGoFiles or XTestGoFiles list only (no tag-ignored files, swig or + // other non-Go files). include := func(info fs.FileInfo) bool { - for _, name := range pkg.GoFiles { - if name == info.Name() { - return true - } - } - for _, name := range pkg.CgoFiles { - if name == info.Name() { - return true + files := [][]string{pkg.GoFiles, pkg.CgoFiles, pkg.TestGoFiles, pkg.XTestGoFiles} + for _, f := range files { + for _, name := range f { + if name == info.Name() { + return true + } } } return false @@ -159,13 +158,9 @@ func parsePackage(writer io.Writer, pkg *build.Package, userPath string) *Packag if err != nil { log.Fatal(err) } - // Make sure they are all in one package. if len(pkgs) == 0 { log.Fatalf("no source-code package in directory %s", pkg.Dir) } - if len(pkgs) > 1 { - log.Fatalf("multiple packages in directory %s", pkg.Dir) - } astPkg := pkgs[pkg.Name] // TODO: go/doc does not include typed constants in the constants @@ -180,7 +175,16 @@ func parsePackage(writer io.Writer, pkg *build.Package, userPath string) *Packag if showSrc { mode |= doc.PreserveAST // See comment for Package.emit. } - docPkg := doc.New(astPkg, pkg.ImportPath, mode) + var allGoFiles []*ast.File + for _, p := range pkgs { + for _, f := range p.Files { + allGoFiles = append(allGoFiles, f) + } + } + docPkg, err := doc.NewFromFiles(fset, allGoFiles, pkg.ImportPath, mode) + if err != nil { + log.Fatal(err) + } typedValue := make(map[*doc.Value]bool) constructor := make(map[*doc.Func]bool) for _, typ := range docPkg.Types { @@ -568,6 +572,7 @@ func (pkg *Package) packageDoc() { pkg.valueSummary(pkg.doc.Vars, false) pkg.funcSummary(pkg.doc.Funcs, false) pkg.typeSummary() + pkg.exampleSummary(pkg.doc.Examples, false) } if !short { @@ -650,11 +655,27 @@ func (pkg *Package) funcSummary(funcs []*doc.Func, showConstructors bool) { if isExported(fun.Name) { if showConstructors || !pkg.constructor[fun] { pkg.Printf("%s\n", pkg.oneLineNode(fun.Decl)) + if showEx { + pkg.exampleSummary(fun.Examples, false) + } } } } } +// exampleSummary prints a one-line summary for each example. +func (pkg *Package) exampleSummary(exs []*doc.Example, showDoc bool) { + if !showEx { + return + } + for _, ex := range exs { + pkg.Printf(indent+"func Example%s()\n", ex.Name) + if showDoc && ex.Doc != "" { + pkg.ToText(&pkg.buf, ex.Doc, indent+indent, indent+indent) + } + } +} + // typeSummary prints a one-line summary for each type, followed by its constructors. func (pkg *Package) typeSummary() { for _, typ := range pkg.doc.Types { @@ -676,8 +697,10 @@ func (pkg *Package) typeSummary() { for _, constructor := range typ.Funcs { if isExported(constructor.Name) { pkg.Printf(indent+"%s\n", pkg.oneLineNode(constructor.Decl)) + pkg.exampleSummary(constructor.Examples, false) } } + pkg.exampleSummary(typ.Examples, false) } } } @@ -728,6 +751,40 @@ func (pkg *Package) findTypes(symbol string) (types []*doc.Type) { return } +// findExamples finds any examples that describe the symbol. +func (pkg *Package) findExamples(symbol string) (examples []*doc.Example) { + if !strings.HasPrefix(symbol, "Example") { + return + } + symbol = strings.TrimPrefix(symbol, "Example") + var all []*doc.Example + all = append(all, pkg.doc.Examples...) + for _, typ := range pkg.doc.Types { + all = append(all, typ.Examples...) + for _, fun := range typ.Funcs { + all = append(all, fun.Examples...) + } + for _, fun := range typ.Methods { + all = append(all, fun.Examples...) + } + } + for _, fun := range pkg.doc.Funcs { + all = append(all, fun.Examples...) + } + + // always include unexported in below match(), so Example_one + // is still matched despite trimming the Example_ part. + u := unexported + unexported = true + for _, ex := range all { + if match(symbol, ex.Name) { + examples = append(examples, ex) + } + } + unexported = u + return +} + // findTypeSpec returns the ast.TypeSpec within the declaration that defines the symbol. // The name must match exactly. func (pkg *Package) findTypeSpec(decl *ast.GenDecl, symbol string) *ast.TypeSpec { @@ -750,6 +807,11 @@ func (pkg *Package) symbolDoc(symbol string) bool { // Symbol is a function. decl := fun.Decl pkg.emit(fun.Doc, decl) + pkg.exampleSummary(fun.Examples, true) + found = true + } + for _, ex := range pkg.findExamples(symbol) { + pkg.emitExample(ex) found = true } // Constants and variables behave the same. @@ -866,6 +928,7 @@ func (pkg *Package) typeDoc(typ *doc.Type) { pkg.valueSummary(typ.Consts, true) pkg.valueSummary(typ.Vars, true) pkg.funcSummary(typ.Funcs, true) + pkg.exampleSummary(typ.Examples, false) pkg.funcSummary(typ.Methods, true) } } @@ -1025,6 +1088,7 @@ func (pkg *Package) printMethodDoc(symbol, method string) bool { if match(method, meth.Name) { decl := meth.Decl pkg.emit(meth.Doc, decl) + pkg.exampleSummary(meth.Examples, true) found = true } } @@ -1166,3 +1230,33 @@ func simpleFold(r rune) rune { r = r1 } } + +// emitExample prints an example and its output. +func (pkg *Package) emitExample(ex *doc.Example) { + pkg.buf.printed = true // omit the package clause + var err error + if ex.Play != nil { + err = format.Node(&pkg.buf, pkg.fs, ex.Play) + } else { + // If code is an *ast.BlockStmt, trim the braces and indentation + // by just printing the enclosed List of ast.[]Stmt. + b, ok := ex.Code.(*ast.BlockStmt) + if !ok { + err = format.Node(&pkg.buf, pkg.fs, ex.Code) + } else { + err = format.Node(&pkg.buf, pkg.fs, b.List) + } + } + if err != nil { + log.Fatal(err) + } + if ex.Output != "" { + pkg.newlines(2) + pkg.Printf("Output: ") + if strings.Count(ex.Output, "\n") > 1 { + pkg.newlines(1) + } + pkg.Printf("%s", ex.Output) + } + pkg.newlines(1) +} diff --git a/src/cmd/go/internal/doc/testdata/pkg_test.go b/src/cmd/go/internal/doc/testdata/pkg_test.go new file mode 100644 index 0000000000..0b4e62e008 --- /dev/null +++ b/src/cmd/go/internal/doc/testdata/pkg_test.go @@ -0,0 +1,47 @@ +// Copyright 2026 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 pkg + +import "fmt" + +// Package example. +func Example() { + fmt.Println("Package example output") + // Output: Package example output +} + +// Function example. +func ExampleExportedFunc() { + fmt.Println("Function example output") + // Output: Function example output +} + +// Function example two. +func ExampleExportedFunc_two() { + fmt.Println("Function example two output") + // Output: Function example two output +} + +// Type example. +func ExampleExportedType() { + fmt.Println("Type example output") + // Output: Type example output +} + +// Method example. +func ExampleExportedType_ExportedMethod() { + fmt.Println("Method example output") + // Output: Method example output +} + +// Multiline example. +func Example_multiline() { + fmt.Println("Multiline\nexample\noutput") + // Output: + // Multiline + // example + // output +} + diff --git a/src/cmd/go/internal/doc/testdata/play_test.go b/src/cmd/go/internal/doc/testdata/play_test.go new file mode 100644 index 0000000000..6b51089503 --- /dev/null +++ b/src/cmd/go/internal/doc/testdata/play_test.go @@ -0,0 +1,13 @@ +// Copyright 2026 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 pkg_test + +import "fmt" + +// Playable example. +func Example_playable() { + fmt.Println("Playable example output") + // Output: Playable example output +} |
