internal/godoc/internal/doc: reconcile with master
Adopt minor changes from go/doc at the current go tip
(be0d049a42ee4b07bfb71acb5e8f7c3d2735049a).
After this CL, the only major difference between this package and
go/doc is example processing.
To clarify the differences, move the pkgsite-specific example
processing code and tests to separate files.
Change-Id: Ifc781c7a9ccb219813b5a23ed3dbe454c8d5a5b1
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/384337
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Jamal Carvalho <jamal@golang.org>
diff --git a/internal/godoc/internal/doc/comment.go b/internal/godoc/internal/doc/comment.go
index 9dbd6b8..75b3726 100644
--- a/internal/godoc/internal/doc/comment.go
+++ b/internal/godoc/internal/doc/comment.go
@@ -38,10 +38,9 @@
text = convertQuotes(text)
var buf bytes.Buffer
template.HTMLEscape(&buf, []byte(text))
- // Now we convert the unicode quotes to their HTML escaped entities to
- // maintain old behavior. We need to use a temp buffer to read the
- // string back and do the conversion, otherwise HTMLEscape will escape &
- // to &
+ // Now we convert the unicode quotes to their HTML escaped entities to maintain old behavior.
+ // We need to use a temp buffer to read the string back and do the conversion,
+ // otherwise HTMLEscape will escape & to &
htmlQuoteReplacer.WriteString(w, buf.String())
return
}
@@ -300,9 +299,8 @@
// in the words map, the link is taken from the map (if the corresponding map
// value is the empty string, the URL is not converted into a link).
//
-// A pair of (consecutive) backticks (`) is converted to a unicode left quote
-// (“), and a pair of (consecutive) single quotes (') is converted to a unicode
-// right quote (”).
+// A pair of (consecutive) backticks (`) is converted to a unicode left quote (“), and a pair of (consecutive)
+// single quotes (') is converted to a unicode right quote (”).
//
// Go identifiers that appear in the words map are italicized; if the corresponding
// map value is not the empty string, it is considered a URL and the word is converted
@@ -422,9 +420,8 @@
// and then prefixes each line with the indent. In preformatted sections
// (such as program text), it prefixes each non-blank line with preIndent.
//
-// A pair of (consecutive) backticks (`) is converted to a unicode left quote
-// (“), and a pair of (consecutive) single quotes (') is converted to a unicode
-// right quote (”).
+// A pair of (consecutive) backticks (`) is converted to a unicode left quote (“), and a pair of (consecutive)
+// single quotes (') is converted to a unicode right quote (”).
func ToText(w io.Writer, text string, indent, preIndent string, width int) {
l := lineWrapper{
out: w,
diff --git a/internal/godoc/internal/doc/doc.go b/internal/godoc/internal/doc/doc.go
index 21ae510..8f684ee 100644
--- a/internal/godoc/internal/doc/doc.go
+++ b/internal/godoc/internal/doc/doc.go
@@ -10,8 +10,6 @@
"go/ast"
"go/token"
"strings"
-
- "golang.org/x/mod/module"
)
// Package is the documentation for an entire package.
@@ -146,6 +144,7 @@
// match the desired build context. "go/build".Context.MatchFile can
// be used for determining whether a file matches a build context with
// the desired GOOS and GOARCH values, and other build constraints.
+// The import path of the package is specified by importPath.
//
// Examples found in _test.go files are associated with the corresponding
// type, function, method, or the package, based on their name.
@@ -203,7 +202,7 @@
// Compute package documentation.
pkg, _ := ast.NewPackage(fset, goFiles, simpleImporter, nil) // Ignore errors that can happen due to unresolved identifiers.
p := New(pkg, importPath, mode)
- classifyExamples(p, Examples(fset, testGoFiles...))
+ classifyExamples(p, Examples2(fset, testGoFiles...))
return p, nil
}
@@ -214,17 +213,10 @@
func simpleImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
pkg := imports[path]
if pkg == nil {
- pkg = ast.NewObj(ast.Pkg, packageName(path))
+ // note that strings.LastIndex returns -1 if there is no "/"
+ pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
imports[path] = pkg
}
return pkg, nil
}
-
-// packageName returns the last path component of the provided package,
-// stripping the major version component, if any.
-func packageName(path string) string {
- pathPrefix, _, _ := module.SplitPathVersion(path)
- // Note that strings.LastIndex returns -1 if there is no "/".
- return pathPrefix[strings.LastIndex(pathPrefix, "/")+1:]
-}
diff --git a/internal/godoc/internal/doc/doc_test.go b/internal/godoc/internal/doc/doc_test.go
index 1dfa87d..5a5fbd8 100644
--- a/internal/godoc/internal/doc/doc_test.go
+++ b/internal/godoc/internal/doc/doc_test.go
@@ -100,93 +100,56 @@
// test packages
for _, pkg := range pkgs {
- importPath := dataDir + "/" + pkg.Name
- var files []*ast.File
- for _, f := range pkg.Files {
- files = append(files, f)
- }
- doc, err := NewFromFiles(fset, files, importPath, mode)
- if err != nil {
- t.Error(err)
- continue
- }
-
- // golden files always use / in filenames - canonicalize them
- for i, filename := range doc.Filenames {
- doc.Filenames[i] = filepath.ToSlash(filename)
- }
-
- // print documentation
- var buf bytes.Buffer
- if err := templateTxt.Execute(&buf, bundle{doc, fset}); err != nil {
- t.Error(err)
- continue
- }
- got := buf.Bytes()
-
- // update golden file if necessary
- golden := filepath.Join(dataDir, fmt.Sprintf("%s.%d.golden", pkg.Name, mode))
- if *update {
- err := os.WriteFile(golden, got, 0644)
- if err != nil {
- t.Error(err)
+ t.Run(pkg.Name, func(t *testing.T) {
+ importPath := dataDir + "/" + pkg.Name
+ var files []*ast.File
+ for _, f := range pkg.Files {
+ files = append(files, f)
}
- continue
- }
+ doc, err := NewFromFiles(fset, files, importPath, mode)
+ if err != nil {
+ t.Fatal(err)
+ }
- // get golden file
- want, err := os.ReadFile(golden)
- if err != nil {
- t.Error(err)
- continue
- }
+ // golden files always use / in filenames - canonicalize them
+ for i, filename := range doc.Filenames {
+ doc.Filenames[i] = filepath.ToSlash(filename)
+ }
- // compare
- if !bytes.Equal(got, want) {
- t.Errorf("package %s\n\tgot:\n%s\n\twant:\n%s", pkg.Name, got, want)
- }
+ // print documentation
+ var buf bytes.Buffer
+ if err := templateTxt.Execute(&buf, bundle{doc, fset}); err != nil {
+ t.Fatal(err)
+ }
+ got := buf.Bytes()
+
+ // update golden file if necessary
+ golden := filepath.Join(dataDir, fmt.Sprintf("%s.%d.golden", pkg.Name, mode))
+ if *update {
+ err := os.WriteFile(golden, got, 0644)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ // get golden file
+ want, err := os.ReadFile(golden)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // compare
+ if !bytes.Equal(got, want) {
+ t.Errorf("package %s\n\tgot:\n%s\n\twant:\n%s", pkg.Name, got, want)
+ }
+ })
}
}
func Test(t *testing.T) {
- test(t, 0)
- test(t, AllDecls)
- test(t, AllMethods)
-}
-
-func TestPackageName(t *testing.T) {
- for _, test := range []struct {
- path string
- want string
- }{
- {
- path: "net/http",
- want: "http",
- },
- {
- path: "github.com/golang/pkgsite",
- want: "pkgsite",
- },
- {
- path: "k8s.io/client-go/listers/apps/v1",
- want: "v1",
- },
- {
- path: "github.com/googleapis/gax-go/v2",
- want: "gax-go",
- },
- {
- path: "google.golang.org/api/drive/v3",
- want: "drive",
- },
- } {
- t.Run(test.path, func(t *testing.T) {
- got := packageName(test.path)
- if got != test.want {
- t.Errorf("packageName(%q) = %q, want %q", test.path, got, test.want)
- }
- })
- }
+ t.Run("default", func(t *testing.T) { test(t, 0) })
+ t.Run("AllDecls", func(t *testing.T) { test(t, AllDecls) })
+ t.Run("AllMethods", func(t *testing.T) { test(t, AllMethods) })
}
func TestAnchorID(t *testing.T) {
@@ -197,3 +160,142 @@
t.Errorf("anchorID(%q) = %q; want %q", in, got, want)
}
}
+
+func TestFuncs(t *testing.T) {
+ fset := token.NewFileSet()
+ file, err := parser.ParseFile(fset, "funcs.go", strings.NewReader(funcsTestFile), parser.ParseComments)
+ if err != nil {
+ t.Fatal(err)
+ }
+ doc, err := NewFromFiles(fset, []*ast.File{file}, "importPath", Mode(0))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, f := range doc.Funcs {
+ f.Decl = nil
+ }
+ for _, ty := range doc.Types {
+ for _, f := range ty.Funcs {
+ f.Decl = nil
+ }
+ for _, m := range ty.Methods {
+ m.Decl = nil
+ }
+ }
+
+ compareFuncs := func(t *testing.T, msg string, got, want *Func) {
+ // ignore Decl and Examples
+ got.Decl = nil
+ got.Examples = nil
+ if !(got.Doc == want.Doc &&
+ got.Name == want.Name &&
+ got.Recv == want.Recv &&
+ got.Orig == want.Orig &&
+ got.Level == want.Level) {
+ t.Errorf("%s:\ngot %+v\nwant %+v", msg, got, want)
+ }
+ }
+
+ compareSlices(t, "Funcs", doc.Funcs, funcsPackage.Funcs, compareFuncs)
+ compareSlices(t, "Types", doc.Types, funcsPackage.Types, func(t *testing.T, msg string, got, want *Type) {
+ if got.Name != want.Name {
+ t.Errorf("%s.Name: got %q, want %q", msg, got.Name, want.Name)
+ } else {
+ compareSlices(t, got.Name+".Funcs", got.Funcs, want.Funcs, compareFuncs)
+ compareSlices(t, got.Name+".Methods", got.Methods, want.Methods, compareFuncs)
+ }
+ })
+}
+
+func compareSlices[E any](t *testing.T, name string, got, want []E, compareElem func(*testing.T, string, E, E)) {
+ if len(got) != len(want) {
+ t.Errorf("%s: got %d, want %d", name, len(got), len(want))
+ }
+ for i := 0; i < len(got) && i < len(want); i++ {
+ compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i])
+ }
+}
+
+const funcsTestFile = `
+package funcs
+
+func F() {}
+
+type S1 struct {
+ S2 // embedded, exported
+ s3 // embedded, unexported
+}
+
+func NewS1() S1 {return S1{} }
+func NewS1p() *S1 { return &S1{} }
+
+func (S1) M1() {}
+func (r S1) M2() {}
+func(S1) m3() {} // unexported not shown
+func (*S1) P1() {} // pointer receiver
+
+type S2 int
+func (S2) M3() {} // shown on S2
+
+type s3 int
+func (s3) M4() {} // shown on S1
+
+type G1[T any] struct {
+ *s3
+}
+
+func NewG1[T any]() G1[T] { return G1[T]{} }
+
+func (G1[T]) MG1() {}
+func (*G1[U]) MG2() {}
+
+type G2[T, U any] struct {}
+
+func NewG2[T, U any]() G2[T, U] { return G2[T, U]{} }
+
+func (G2[T, U]) MG3() {}
+func (*G2[A, B]) MG4() {}
+
+
+`
+
+var funcsPackage = &Package{
+ Funcs: []*Func{{Name: "F"}},
+ Types: []*Type{
+ {
+ Name: "G1",
+ Funcs: []*Func{{Name: "NewG1"}},
+ Methods: []*Func{
+ {Name: "M4", Recv: "G1", // TODO: synthesize a param for G1?
+ Orig: "s3", Level: 1},
+ {Name: "MG1", Recv: "G1[T]", Orig: "G1[T]", Level: 0},
+ {Name: "MG2", Recv: "*G1[U]", Orig: "*G1[U]", Level: 0},
+ },
+ },
+ {
+ Name: "G2",
+ Funcs: []*Func{{Name: "NewG2"}},
+ Methods: []*Func{
+ {Name: "MG3", Recv: "G2[T, U]", Orig: "G2[T, U]", Level: 0},
+ {Name: "MG4", Recv: "*G2[A, B]", Orig: "*G2[A, B]", Level: 0},
+ },
+ },
+ {
+ Name: "S1",
+ Funcs: []*Func{{Name: "NewS1"}, {Name: "NewS1p"}},
+ Methods: []*Func{
+ {Name: "M1", Recv: "S1", Orig: "S1", Level: 0},
+ {Name: "M2", Recv: "S1", Orig: "S1", Level: 0},
+ {Name: "M4", Recv: "S1", Orig: "s3", Level: 1},
+ {Name: "P1", Recv: "*S1", Orig: "*S1", Level: 0},
+ },
+ },
+ {
+ Name: "S2",
+ Methods: []*Func{
+ {Name: "M3", Recv: "S2", Orig: "S2", Level: 0},
+ },
+ },
+ },
+}
diff --git a/internal/godoc/internal/doc/example.go b/internal/godoc/internal/doc/example.go
index 6dc1a53..792ba56 100644
--- a/internal/godoc/internal/doc/example.go
+++ b/internal/godoc/internal/doc/example.go
@@ -48,7 +48,7 @@
// example function, zero test, fuzz test, or benchmark function, and at
// least one top-level function, type, variable, or constant declaration
// other than the example function.
-func Examples(fset *token.FileSet, testFiles ...*ast.File) []*Example {
+func Examples(testFiles ...*ast.File) []*Example {
var list []*Example
for _, file := range testFiles {
hasTests := false // file contains tests, fuzz test, or benchmarks
@@ -87,7 +87,7 @@
Name: name[len("Example"):],
Doc: doc,
Code: f.Body,
- Play: playExample(fset, file, f),
+ Play: playExample(file, f),
Comments: file.Comments,
Output: output,
Unordered: unordered,
@@ -150,9 +150,9 @@
// playExample synthesizes a new *ast.File based on the provided
// file with the provided function body as the body of main.
-func playExample(fset *token.FileSet, file *ast.File, f *ast.FuncDecl) *ast.File {
+func playExample(file *ast.File, f *ast.FuncDecl) *ast.File {
body := f.Body
- tokenFile := fset.File(file.Package)
+
if !strings.HasSuffix(file.Name.Name, "_test") {
// We don't support examples that are part of the
// greater package (yet).
@@ -190,7 +190,76 @@
}
// Find unresolved identifiers and uses of top-level declarations.
- depDecls, unresolved := findDeclsAndUnresolved(body, topDecls, typMethods)
+ unresolved := make(map[string]bool)
+ var depDecls []ast.Decl
+ hasDepDecls := make(map[ast.Decl]bool)
+
+ var inspectFunc func(ast.Node) bool
+ inspectFunc = func(n ast.Node) bool {
+ switch e := n.(type) {
+ case *ast.Ident:
+ if e.Obj == nil && e.Name != "_" {
+ unresolved[e.Name] = true
+ } else if d := topDecls[e.Obj]; d != nil {
+ if !hasDepDecls[d] {
+ hasDepDecls[d] = true
+ depDecls = append(depDecls, d)
+ }
+ }
+ return true
+ case *ast.SelectorExpr:
+ // For selector expressions, only inspect the left hand side.
+ // (For an expression like fmt.Println, only add "fmt" to the
+ // set of unresolved names, not "Println".)
+ ast.Inspect(e.X, inspectFunc)
+ return false
+ case *ast.KeyValueExpr:
+ // For key value expressions, only inspect the value
+ // as the key should be resolved by the type of the
+ // composite literal.
+ ast.Inspect(e.Value, inspectFunc)
+ return false
+ }
+ return true
+ }
+ ast.Inspect(body, inspectFunc)
+ for i := 0; i < len(depDecls); i++ {
+ switch d := depDecls[i].(type) {
+ case *ast.FuncDecl:
+ // Inspect types of parameters and results. See #28492.
+ if d.Type.Params != nil {
+ for _, p := range d.Type.Params.List {
+ ast.Inspect(p.Type, inspectFunc)
+ }
+ }
+ if d.Type.Results != nil {
+ for _, r := range d.Type.Results.List {
+ ast.Inspect(r.Type, inspectFunc)
+ }
+ }
+
+ // Functions might not have a body. See #42706.
+ if d.Body != nil {
+ ast.Inspect(d.Body, inspectFunc)
+ }
+ case *ast.GenDecl:
+ for _, spec := range d.Specs {
+ switch s := spec.(type) {
+ case *ast.TypeSpec:
+ ast.Inspect(s.Type, inspectFunc)
+
+ depDecls = append(depDecls, typMethods[s.Name.Name]...)
+ case *ast.ValueSpec:
+ if s.Type != nil {
+ ast.Inspect(s.Type, inspectFunc)
+ }
+ for _, val := range s.Values {
+ ast.Inspect(val, inspectFunc)
+ }
+ }
+ }
+ }
+ }
// Remove predeclared identifiers from unresolved list.
for n := range unresolved {
@@ -271,7 +340,20 @@
}
}
- importDecl := synthesizeImportDecl(namedImports, blankImports, tokenFile)
+ // Synthesize import declaration.
+ importDecl := &ast.GenDecl{
+ Tok: token.IMPORT,
+ Lparen: 1, // Need non-zero Lparen and Rparen so that printer
+ Rparen: 1, // treats this as a factored import.
+ }
+ for n, p := range namedImports {
+ s := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote(p)}}
+ if path.Base(p) != n {
+ s.Name = ast.NewIdent(n)
+ }
+ importDecl.Specs = append(importDecl.Specs, s)
+ }
+ importDecl.Specs = append(importDecl.Specs, blankImports...)
// Synthesize main function.
funcDecl := &ast.FuncDecl{
@@ -301,202 +383,6 @@
}
}
-func findDeclsAndUnresolved(body ast.Node, topDecls map[*ast.Object]ast.Decl, typMethods map[string][]ast.Decl) ([]ast.Decl, map[string]bool) {
- var depDecls []ast.Decl
- unresolved := make(map[string]bool)
- hasDepDecls := make(map[ast.Decl]bool)
- objs := map[*ast.Object]bool{}
-
- var inspectFunc func(ast.Node) bool
- inspectFunc = func(n ast.Node) bool {
- switch e := n.(type) {
- case *ast.Ident:
- if e.Obj == nil && e.Name != "_" {
- unresolved[e.Name] = true
- } else if d := topDecls[e.Obj]; d != nil {
- objs[e.Obj] = true
- if !hasDepDecls[d] {
- hasDepDecls[d] = true
- depDecls = append(depDecls, d)
- }
- }
- return true
- case *ast.SelectorExpr:
- // For selector expressions, only inspect the left hand side.
- // (For an expression like fmt.Println, only add "fmt" to the
- // set of unresolved names, not "Println".)
- ast.Inspect(e.X, inspectFunc)
- return false
- case *ast.KeyValueExpr:
- // For key value expressions, only inspect the value
- // as the key should be resolved by the type of the
- // composite literal.
- ast.Inspect(e.Value, inspectFunc)
- return false
- }
- return true
- }
- ast.Inspect(body, inspectFunc)
- for i := 0; i < len(depDecls); i++ {
- switch d := depDecls[i].(type) {
- case *ast.FuncDecl:
- // Inspect types of parameters and results. See #28492.
- if d.Type.Params != nil {
- for _, p := range d.Type.Params.List {
- ast.Inspect(p.Type, inspectFunc)
- }
- }
- if d.Type.Results != nil {
- for _, r := range d.Type.Results.List {
- ast.Inspect(r.Type, inspectFunc)
- }
- }
-
- ast.Inspect(d.Body, inspectFunc)
- case *ast.GenDecl:
- for _, spec := range d.Specs {
- switch s := spec.(type) {
- case *ast.TypeSpec:
- ast.Inspect(s.Type, inspectFunc)
- depDecls = append(depDecls, typMethods[s.Name.Name]...)
- case *ast.ValueSpec:
- if s.Type != nil {
- ast.Inspect(s.Type, inspectFunc)
- }
- for _, val := range s.Values {
- ast.Inspect(val, inspectFunc)
- }
- }
- }
- }
- }
- // Some decls include multiple specs, such as a variable declaration with
- // multiple variables on the same line, or a parenthesized declaration. Trim
- // the declarations to include only the specs that are actually mentioned.
- // However, if there is a constant group with iota, leave it all: later
- // constant declarations in the group may have no value and so cannot stand
- // on their own, and furthermore, removing any constant from the group could
- // change the values of subsequent ones.
- // See testdata/examples/iota.go for a minimal example.
- ds := depDecls[:0]
- for _, d := range depDecls {
- switch d := d.(type) {
- case *ast.FuncDecl:
- ds = append(ds, d)
- case *ast.GenDecl:
- // Collect all Specs that were mentioned in the example.
- var specs []ast.Spec
- for _, s := range d.Specs {
- switch s := s.(type) {
- case *ast.TypeSpec:
- if objs[s.Name.Obj] {
- specs = append(specs, s)
- }
- case *ast.ValueSpec:
- // A ValueSpec may have multiple names (e.g. "var a, b int").
- // Keep only the names that were mentioned in the example.
- // Exception: the multiple names have a single initializer (which
- // would be a function call with multiple return values). In that
- // case, keep everything.
- if len(s.Names) > 1 && len(s.Values) == 1 {
- specs = append(specs, s)
- continue
- }
- ns := *s
- ns.Names = nil
- ns.Values = nil
- for i, n := range s.Names {
- if objs[n.Obj] {
- ns.Names = append(ns.Names, n)
- if s.Values != nil {
- ns.Values = append(ns.Values, s.Values[i])
- }
- }
- }
- if len(ns.Names) > 0 {
- specs = append(specs, &ns)
- }
- }
- }
- if len(specs) > 0 {
- // Constant with iota? Keep it all.
- if d.Tok == token.CONST && hasIota(d.Specs[0]) {
- ds = append(ds, d)
- } else {
- // Synthesize a GenDecl with just the Specs we need.
- nd := *d // copy the GenDecl
- nd.Specs = specs
- if len(specs) == 1 {
- // Remove grouping parens if there is only one spec.
- nd.Lparen = 0
- }
- ds = append(ds, &nd)
- }
- }
- }
- }
- return ds, unresolved
-}
-
-func hasIota(s ast.Spec) bool {
- has := false
- ast.Inspect(s, func(n ast.Node) bool {
- if id, ok := n.(*ast.Ident); ok && id.Name == "iota" {
- has = true
- return false
- }
- return true
- })
- return has
-}
-
-// synthesizeImportDecl creates the imports for the example. We want the imports
-// divided into two groups, one for the standard library and one for all others.
-// To get ast.SortImports (called by the formatter) to do that, we must assign
-// file positions to the import specs so that there is a blank line between the
-// two groups. The exact positions don't matter, and they don't have to be
-// distinct within a group; ast.SortImports just looks for a gap of more than
-// one line between specs.
-func synthesizeImportDecl(namedImports map[string]string, blankImports []ast.Spec, tfile *token.File) *ast.GenDecl {
- importDecl := &ast.GenDecl{
- Tok: token.IMPORT,
- Lparen: 1, // Need non-zero Lparen and Rparen so that printer
- Rparen: 1, // treats this as a factored import.
- }
- var stds, others []ast.Spec
- var stdPos, otherPos token.Pos
- if tfile.LineCount() >= 3 {
- stdPos = tfile.LineStart(1)
- otherPos = tfile.LineStart(3)
- }
- for n, p := range namedImports {
- var (
- pos token.Pos
- specs *[]ast.Spec
- )
- if !strings.ContainsRune(p, '.') {
- pos = stdPos
- specs = &stds
- } else {
- pos = otherPos
- specs = &others
- }
- s := &ast.ImportSpec{
- Path: &ast.BasicLit{Value: strconv.Quote(p), Kind: token.STRING, ValuePos: pos},
- EndPos: pos,
- }
- if path.Base(p) != n {
- s.Name = ast.NewIdent(n)
- s.Name.NamePos = pos
- }
- *specs = append(*specs, s)
- }
- importDecl.Specs = append(stds, others...)
- importDecl.Specs = append(importDecl.Specs, blankImports...)
-
- return importDecl
-}
-
// playExampleFile takes a whole file example and synthesizes a new *ast.File
// such that the example is function main in package main.
func playExampleFile(file *ast.File) *ast.File {
@@ -604,7 +490,7 @@
ids[f.Name] = &f.Examples
}
for _, m := range t.Methods {
- if !token.IsExported(m.Name) { // avoid unexported methods
+ if !token.IsExported(m.Name) {
continue
}
ids[strings.TrimPrefix(m.Recv, "*")+"_"+m.Name] = &m.Examples
diff --git a/internal/godoc/internal/doc/example_pkgsite.go b/internal/godoc/internal/doc/example_pkgsite.go
new file mode 100644
index 0000000..b2575e7
--- /dev/null
+++ b/internal/godoc/internal/doc/example_pkgsite.go
@@ -0,0 +1,443 @@
+// Copyright 2011 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.
+
+// Extract example functions from file ASTs.
+
+package doc
+
+import (
+ "go/ast"
+ "go/token"
+ "path"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// Examples returns the examples found in testFiles, sorted by Name field.
+// The Order fields record the order in which the examples were encountered.
+// The Suffix field is not populated when Examples is called directly, it is
+// only populated by NewFromFiles for examples it finds in _test.go files.
+//
+// Playable Examples must be in a package whose name ends in "_test".
+// An Example is "playable" (the Play field is non-nil) in either of these
+// circumstances:
+// - The example function is self-contained: the function references only
+// identifiers from other packages (or predeclared identifiers, such as
+// "int") and the test file does not include a dot import.
+// - The entire test file is the example: the file contains exactly one
+// example function, zero test, fuzz test, or benchmark function, and at
+// least one top-level function, type, variable, or constant declaration
+// other than the example function.
+func Examples2(fset *token.FileSet, testFiles ...*ast.File) []*Example {
+ var list []*Example
+ for _, file := range testFiles {
+ hasTests := false // file contains tests, fuzz test, or benchmarks
+ numDecl := 0 // number of non-import declarations in the file
+ var flist []*Example
+ for _, decl := range file.Decls {
+ if g, ok := decl.(*ast.GenDecl); ok && g.Tok != token.IMPORT {
+ numDecl++
+ continue
+ }
+ f, ok := decl.(*ast.FuncDecl)
+ if !ok || f.Recv != nil {
+ continue
+ }
+ numDecl++
+ name := f.Name.Name
+ if isTest(name, "Test") || isTest(name, "Benchmark") || isTest(name, "Fuzz") {
+ hasTests = true
+ continue
+ }
+ if !isTest(name, "Example") {
+ continue
+ }
+ if params := f.Type.Params; len(params.List) != 0 {
+ continue // function has params; not a valid example
+ }
+ if f.Body == nil { // ast.File.Body nil dereference (see issue 28044)
+ continue
+ }
+ var doc string
+ if f.Doc != nil {
+ doc = f.Doc.Text()
+ }
+ output, unordered, hasOutput := exampleOutput(f.Body, file.Comments)
+ flist = append(flist, &Example{
+ Name: name[len("Example"):],
+ Doc: doc,
+ Code: f.Body,
+ Play: playExample2(fset, file, f),
+ Comments: file.Comments,
+ Output: output,
+ Unordered: unordered,
+ EmptyOutput: output == "" && hasOutput,
+ Order: len(flist),
+ })
+ }
+ if !hasTests && numDecl > 1 && len(flist) == 1 {
+ // If this file only has one example function, some
+ // other top-level declarations, and no tests or
+ // benchmarks, use the whole file as the example.
+ flist[0].Code = file
+ flist[0].Play = playExampleFile(file)
+ }
+ list = append(list, flist...)
+ }
+ // sort by name
+ sort.Slice(list, func(i, j int) bool {
+ return list[i].Name < list[j].Name
+ })
+ return list
+}
+
+// playExample synthesizes a new *ast.File based on the provided
+// file with the provided function body as the body of main.
+func playExample2(fset *token.FileSet, file *ast.File, f *ast.FuncDecl) *ast.File {
+ body := f.Body
+ tokenFile := fset.File(file.Package)
+ if !strings.HasSuffix(file.Name.Name, "_test") {
+ // We don't support examples that are part of the
+ // greater package (yet).
+ return nil
+ }
+
+ // Collect top-level declarations in the file.
+ topDecls := make(map[*ast.Object]ast.Decl)
+ typMethods := make(map[string][]ast.Decl)
+
+ for _, decl := range file.Decls {
+ switch d := decl.(type) {
+ case *ast.FuncDecl:
+ if d.Recv == nil {
+ topDecls[d.Name.Obj] = d
+ } else {
+ if len(d.Recv.List) == 1 {
+ t := d.Recv.List[0].Type
+ tname, _ := baseTypeName(t)
+ typMethods[tname] = append(typMethods[tname], d)
+ }
+ }
+ case *ast.GenDecl:
+ for _, spec := range d.Specs {
+ switch s := spec.(type) {
+ case *ast.TypeSpec:
+ topDecls[s.Name.Obj] = d
+ case *ast.ValueSpec:
+ for _, name := range s.Names {
+ topDecls[name.Obj] = d
+ }
+ }
+ }
+ }
+ }
+
+ // Find unresolved identifiers and uses of top-level declarations.
+ depDecls, unresolved := findDeclsAndUnresolved(body, topDecls, typMethods)
+
+ // Remove predeclared identifiers from unresolved list.
+ for n := range unresolved {
+ if predeclaredTypes[n] || predeclaredConstants[n] || predeclaredFuncs[n] {
+ delete(unresolved, n)
+ }
+ }
+
+ // Use unresolved identifiers to determine the imports used by this
+ // example. The heuristic assumes package names match base import
+ // paths for imports w/o renames (should be good enough most of the time).
+ namedImports := make(map[string]string) // [name]path
+ var blankImports []ast.Spec // _ imports
+ for _, s := range file.Imports {
+ p, err := strconv.Unquote(s.Path.Value)
+ if err != nil {
+ continue
+ }
+ if p == "syscall/js" {
+ // We don't support examples that import syscall/js,
+ // because the package syscall/js is not available in the playground.
+ return nil
+ }
+ n := path.Base(p)
+ if s.Name != nil {
+ n = s.Name.Name
+ switch n {
+ case "_":
+ blankImports = append(blankImports, s)
+ continue
+ case ".":
+ // We can't resolve dot imports (yet).
+ return nil
+ }
+ }
+ if unresolved[n] {
+ namedImports[n] = p
+ delete(unresolved, n)
+ }
+ }
+
+ // If there are other unresolved identifiers, give up because this
+ // synthesized file is not going to build.
+ if len(unresolved) > 0 {
+ return nil
+ }
+
+ // Include documentation belonging to blank imports.
+ var comments []*ast.CommentGroup
+ for _, s := range blankImports {
+ if c := s.(*ast.ImportSpec).Doc; c != nil {
+ comments = append(comments, c)
+ }
+ }
+
+ // Include comments that are inside the function body.
+ for _, c := range file.Comments {
+ if body.Pos() <= c.Pos() && c.End() <= body.End() {
+ comments = append(comments, c)
+ }
+ }
+
+ // Strip the "Output:" or "Unordered output:" comment and adjust body
+ // end position.
+ body, comments = stripOutputComment(body, comments)
+
+ // Include documentation belonging to dependent declarations.
+ for _, d := range depDecls {
+ switch d := d.(type) {
+ case *ast.GenDecl:
+ if d.Doc != nil {
+ comments = append(comments, d.Doc)
+ }
+ case *ast.FuncDecl:
+ if d.Doc != nil {
+ comments = append(comments, d.Doc)
+ }
+ }
+ }
+
+ importDecl := synthesizeImportDecl(namedImports, blankImports, tokenFile)
+
+ // Synthesize main function.
+ funcDecl := &ast.FuncDecl{
+ Name: ast.NewIdent("main"),
+ Type: f.Type,
+ Body: body,
+ }
+
+ decls := make([]ast.Decl, 0, 2+len(depDecls))
+ decls = append(decls, importDecl)
+ decls = append(decls, depDecls...)
+ decls = append(decls, funcDecl)
+
+ sort.Slice(decls, func(i, j int) bool {
+ return decls[i].Pos() < decls[j].Pos()
+ })
+
+ sort.Slice(comments, func(i, j int) bool {
+ return comments[i].Pos() < comments[j].Pos()
+ })
+
+ // Synthesize file.
+ return &ast.File{
+ Name: ast.NewIdent("main"),
+ Decls: decls,
+ Comments: comments,
+ }
+}
+
+func findDeclsAndUnresolved(body ast.Node, topDecls map[*ast.Object]ast.Decl, typMethods map[string][]ast.Decl) ([]ast.Decl, map[string]bool) {
+ var depDecls []ast.Decl
+ unresolved := make(map[string]bool)
+ hasDepDecls := make(map[ast.Decl]bool)
+ objs := map[*ast.Object]bool{}
+
+ var inspectFunc func(ast.Node) bool
+ inspectFunc = func(n ast.Node) bool {
+ switch e := n.(type) {
+ case *ast.Ident:
+ if e.Obj == nil && e.Name != "_" {
+ unresolved[e.Name] = true
+ } else if d := topDecls[e.Obj]; d != nil {
+ objs[e.Obj] = true
+ if !hasDepDecls[d] {
+ hasDepDecls[d] = true
+ depDecls = append(depDecls, d)
+ }
+ }
+ return true
+ case *ast.SelectorExpr:
+ // For selector expressions, only inspect the left hand side.
+ // (For an expression like fmt.Println, only add "fmt" to the
+ // set of unresolved names, not "Println".)
+ ast.Inspect(e.X, inspectFunc)
+ return false
+ case *ast.KeyValueExpr:
+ // For key value expressions, only inspect the value
+ // as the key should be resolved by the type of the
+ // composite literal.
+ ast.Inspect(e.Value, inspectFunc)
+ return false
+ }
+ return true
+ }
+ ast.Inspect(body, inspectFunc)
+ for i := 0; i < len(depDecls); i++ {
+ switch d := depDecls[i].(type) {
+ case *ast.FuncDecl:
+ // Inspect types of parameters and results. See #28492.
+ if d.Type.Params != nil {
+ for _, p := range d.Type.Params.List {
+ ast.Inspect(p.Type, inspectFunc)
+ }
+ }
+ if d.Type.Results != nil {
+ for _, r := range d.Type.Results.List {
+ ast.Inspect(r.Type, inspectFunc)
+ }
+ }
+
+ ast.Inspect(d.Body, inspectFunc)
+ case *ast.GenDecl:
+ for _, spec := range d.Specs {
+ switch s := spec.(type) {
+ case *ast.TypeSpec:
+ ast.Inspect(s.Type, inspectFunc)
+ depDecls = append(depDecls, typMethods[s.Name.Name]...)
+ case *ast.ValueSpec:
+ if s.Type != nil {
+ ast.Inspect(s.Type, inspectFunc)
+ }
+ for _, val := range s.Values {
+ ast.Inspect(val, inspectFunc)
+ }
+ }
+ }
+ }
+ }
+ // Some decls include multiple specs, such as a variable declaration with
+ // multiple variables on the same line, or a parenthesized declaration. Trim
+ // the declarations to include only the specs that are actually mentioned.
+ // However, if there is a constant group with iota, leave it all: later
+ // constant declarations in the group may have no value and so cannot stand
+ // on their own, and furthermore, removing any constant from the group could
+ // change the values of subsequent ones.
+ // See testdata/examples/iota.go for a minimal example.
+ ds := depDecls[:0]
+ for _, d := range depDecls {
+ switch d := d.(type) {
+ case *ast.FuncDecl:
+ ds = append(ds, d)
+ case *ast.GenDecl:
+ // Collect all Specs that were mentioned in the example.
+ var specs []ast.Spec
+ for _, s := range d.Specs {
+ switch s := s.(type) {
+ case *ast.TypeSpec:
+ if objs[s.Name.Obj] {
+ specs = append(specs, s)
+ }
+ case *ast.ValueSpec:
+ // A ValueSpec may have multiple names (e.g. "var a, b int").
+ // Keep only the names that were mentioned in the example.
+ // Exception: the multiple names have a single initializer (which
+ // would be a function call with multiple return values). In that
+ // case, keep everything.
+ if len(s.Names) > 1 && len(s.Values) == 1 {
+ specs = append(specs, s)
+ continue
+ }
+ ns := *s
+ ns.Names = nil
+ ns.Values = nil
+ for i, n := range s.Names {
+ if objs[n.Obj] {
+ ns.Names = append(ns.Names, n)
+ if s.Values != nil {
+ ns.Values = append(ns.Values, s.Values[i])
+ }
+ }
+ }
+ if len(ns.Names) > 0 {
+ specs = append(specs, &ns)
+ }
+ }
+ }
+ if len(specs) > 0 {
+ // Constant with iota? Keep it all.
+ if d.Tok == token.CONST && hasIota(d.Specs[0]) {
+ ds = append(ds, d)
+ } else {
+ // Synthesize a GenDecl with just the Specs we need.
+ nd := *d // copy the GenDecl
+ nd.Specs = specs
+ if len(specs) == 1 {
+ // Remove grouping parens if there is only one spec.
+ nd.Lparen = 0
+ }
+ ds = append(ds, &nd)
+ }
+ }
+ }
+ }
+ return ds, unresolved
+}
+
+func hasIota(s ast.Spec) bool {
+ has := false
+ ast.Inspect(s, func(n ast.Node) bool {
+ if id, ok := n.(*ast.Ident); ok && id.Name == "iota" {
+ has = true
+ return false
+ }
+ return true
+ })
+ return has
+}
+
+// synthesizeImportDecl creates the imports for the example. We want the imports
+// divided into two groups, one for the standard library and one for all others.
+// To get ast.SortImports (called by the formatter) to do that, we must assign
+// file positions to the import specs so that there is a blank line between the
+// two groups. The exact positions don't matter, and they don't have to be
+// distinct within a group; ast.SortImports just looks for a gap of more than
+// one line between specs.
+func synthesizeImportDecl(namedImports map[string]string, blankImports []ast.Spec, tfile *token.File) *ast.GenDecl {
+ importDecl := &ast.GenDecl{
+ Tok: token.IMPORT,
+ Lparen: 1, // Need non-zero Lparen and Rparen so that printer
+ Rparen: 1, // treats this as a factored import.
+ }
+ var stds, others []ast.Spec
+ var stdPos, otherPos token.Pos
+ if tfile.LineCount() >= 3 {
+ stdPos = tfile.LineStart(1)
+ otherPos = tfile.LineStart(3)
+ }
+ for n, p := range namedImports {
+ var (
+ pos token.Pos
+ specs *[]ast.Spec
+ )
+ if !strings.ContainsRune(p, '.') {
+ pos = stdPos
+ specs = &stds
+ } else {
+ pos = otherPos
+ specs = &others
+ }
+ s := &ast.ImportSpec{
+ Path: &ast.BasicLit{Value: strconv.Quote(p), Kind: token.STRING, ValuePos: pos},
+ EndPos: pos,
+ }
+ if path.Base(p) != n {
+ s.Name = ast.NewIdent(n)
+ s.Name.NamePos = pos
+ }
+ *specs = append(*specs, s)
+ }
+ importDecl.Specs = append(stds, others...)
+ importDecl.Specs = append(importDecl.Specs, blankImports...)
+
+ return importDecl
+}
diff --git a/internal/godoc/internal/doc/example_pkgsite_test.go b/internal/godoc/internal/doc/example_pkgsite_test.go
new file mode 100644
index 0000000..e7d051a
--- /dev/null
+++ b/internal/godoc/internal/doc/example_pkgsite_test.go
@@ -0,0 +1,91 @@
+// Copyright 2013 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_test
+
+import (
+ "go/parser"
+ "go/token"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/pkgsite/internal/godoc/internal/doc"
+ "golang.org/x/tools/txtar"
+)
+
+func TestExamples2(t *testing.T) {
+ dir := filepath.Join("testdata", "examples")
+ filenames, err := filepath.Glob(filepath.Join(dir, "*.go"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, filename := range filenames {
+ t.Run(strings.TrimSuffix(filepath.Base(filename), ".go"), func(t *testing.T) {
+ fset := token.NewFileSet()
+ astFile, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
+ if err != nil {
+ t.Fatal(err)
+ }
+ goldenFilename := strings.TrimSuffix(filename, ".go") + ".golden"
+ golden, err := readSectionFile(goldenFilename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ examples := map[string]*doc.Example{}
+ unseen := map[string]bool{} // examples we haven't seen yet
+ for _, e := range doc.Examples2(fset, astFile) {
+ examples[e.Name] = e
+ unseen[e.Name] = true
+ }
+ for section, want := range golden {
+ words := strings.Split(section, ".")
+ if len(words) != 2 {
+ t.Fatalf("bad section name %q", section)
+ }
+ name, kind := words[0], words[1]
+ ex := examples[name]
+ if ex == nil {
+ t.Fatalf("no example named %q", name)
+ }
+ switch kind {
+ case "Play":
+ got := strings.TrimSpace(formatFile(t, fset, ex.Play))
+ if diff := cmp.Diff(want, got); diff != "" {
+ t.Errorf("%s Play: mismatch (-want, +got):\n%s", name, diff)
+ }
+ delete(unseen, name)
+ case "Output":
+ got := strings.TrimSpace(ex.Output)
+ if got != want {
+ t.Errorf("%s Output: got\n%q\n---- want ----\n%q", ex.Name, got, want)
+ }
+ default:
+ t.Fatalf("bad section kind %q", kind)
+ }
+ }
+ for name := range unseen {
+ t.Errorf("no Play golden for example %q", name)
+ }
+ })
+ }
+}
+
+// readSectionFile reads a file that is divided into sections, and returns
+// a map from section name to contents.
+//
+// We use the txtar format for the file. See https://pkg.go.dev/golang.org/x/tools/txtar.
+// Although the format talks about filenames as the keys, they can be arbitrary strings.
+func readSectionFile(filename string) (map[string]string, error) {
+ archive, err := txtar.ParseFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ m := map[string]string{}
+ for _, f := range archive.Files {
+ m[f.Name] = strings.TrimSpace(string(f.Data))
+ }
+ return m, nil
+}
diff --git a/internal/godoc/internal/doc/example_test.go b/internal/godoc/internal/doc/example_test.go
index 2a860ac..21b7129 100644
--- a/internal/godoc/internal/doc/example_test.go
+++ b/internal/godoc/internal/doc/example_test.go
@@ -8,78 +8,520 @@
"bytes"
"fmt"
"go/ast"
+ "go/doc"
"go/format"
"go/parser"
"go/token"
- "path/filepath"
"reflect"
"strings"
"testing"
-
- "github.com/google/go-cmp/cmp"
- "golang.org/x/pkgsite/internal/godoc/internal/doc"
- "golang.org/x/tools/txtar"
)
+const exampleTestFile = `
+package foo_test
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "sort"
+ "os/exec"
+)
+
+func ExampleHello() {
+ fmt.Println("Hello, world!")
+ // Output: Hello, world!
+}
+
+func ExampleImport() {
+ out, err := exec.Command("date").Output()
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("The date is %s\n", out)
+}
+
+func ExampleKeyValue() {
+ v := struct {
+ a string
+ b int
+ }{
+ a: "A",
+ b: 1,
+ }
+ fmt.Print(v)
+ // Output: a: "A", b: 1
+}
+
+func ExampleKeyValueImport() {
+ f := flag.Flag{
+ Name: "play",
+ }
+ fmt.Print(f)
+ // Output: Name: "play"
+}
+
+var keyValueTopDecl = struct {
+ a string
+ b int
+}{
+ a: "B",
+ b: 2,
+}
+
+func ExampleKeyValueTopDecl() {
+ fmt.Print(keyValueTopDecl)
+ // Output: a: "B", b: 2
+}
+
+// Person represents a person by name and age.
+type Person struct {
+ Name string
+ Age int
+}
+
+// String returns a string representation of the Person.
+func (p Person) String() string {
+ return fmt.Sprintf("%s: %d", p.Name, p.Age)
+}
+
+// ByAge implements sort.Interface for []Person based on
+// the Age field.
+type ByAge []Person
+
+// Len returns the number of elements in ByAge.
+func (a (ByAge)) Len() int { return len(a) }
+
+// Swap swaps the elements in ByAge.
+func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
+
+// people is the array of Person
+var people = []Person{
+ {"Bob", 31},
+ {"John", 42},
+ {"Michael", 17},
+ {"Jenny", 26},
+}
+
+func ExampleSort() {
+ fmt.Println(people)
+ sort.Sort(ByAge(people))
+ fmt.Println(people)
+ // Output:
+ // [Bob: 31 John: 42 Michael: 17 Jenny: 26]
+ // [Michael: 17 Jenny: 26 Bob: 31 John: 42]
+}
+`
+
+var exampleTestCases = []struct {
+ Name, Play, Output string
+}{
+ {
+ Name: "Hello",
+ Play: exampleHelloPlay,
+ Output: "Hello, world!\n",
+ },
+ {
+ Name: "Import",
+ Play: exampleImportPlay,
+ },
+ {
+ Name: "KeyValue",
+ Play: exampleKeyValuePlay,
+ Output: "a: \"A\", b: 1\n",
+ },
+ {
+ Name: "KeyValueImport",
+ Play: exampleKeyValueImportPlay,
+ Output: "Name: \"play\"\n",
+ },
+ {
+ Name: "KeyValueTopDecl",
+ Play: exampleKeyValueTopDeclPlay,
+ Output: "a: \"B\", b: 2\n",
+ },
+ {
+ Name: "Sort",
+ Play: exampleSortPlay,
+ Output: "[Bob: 31 John: 42 Michael: 17 Jenny: 26]\n[Michael: 17 Jenny: 26 Bob: 31 John: 42]\n",
+ },
+}
+
+const exampleHelloPlay = `package main
+
+import (
+ "fmt"
+)
+
+func main() {
+ fmt.Println("Hello, world!")
+}
+`
+const exampleImportPlay = `package main
+
+import (
+ "fmt"
+ "log"
+ "os/exec"
+)
+
+func main() {
+ out, err := exec.Command("date").Output()
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("The date is %s\n", out)
+}
+`
+
+const exampleKeyValuePlay = `package main
+
+import (
+ "fmt"
+)
+
+func main() {
+ v := struct {
+ a string
+ b int
+ }{
+ a: "A",
+ b: 1,
+ }
+ fmt.Print(v)
+}
+`
+
+const exampleKeyValueImportPlay = `package main
+
+import (
+ "flag"
+ "fmt"
+)
+
+func main() {
+ f := flag.Flag{
+ Name: "play",
+ }
+ fmt.Print(f)
+}
+`
+
+const exampleKeyValueTopDeclPlay = `package main
+
+import (
+ "fmt"
+)
+
+var keyValueTopDecl = struct {
+ a string
+ b int
+}{
+ a: "B",
+ b: 2,
+}
+
+func main() {
+ fmt.Print(keyValueTopDecl)
+}
+`
+
+const exampleSortPlay = `package main
+
+import (
+ "fmt"
+ "sort"
+)
+
+// Person represents a person by name and age.
+type Person struct {
+ Name string
+ Age int
+}
+
+// String returns a string representation of the Person.
+func (p Person) String() string {
+ return fmt.Sprintf("%s: %d", p.Name, p.Age)
+}
+
+// ByAge implements sort.Interface for []Person based on
+// the Age field.
+type ByAge []Person
+
+// Len returns the number of elements in ByAge.
+func (a ByAge) Len() int { return len(a) }
+
+// Swap swaps the elements in ByAge.
+func (a ByAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
+
+// people is the array of Person
+var people = []Person{
+ {"Bob", 31},
+ {"John", 42},
+ {"Michael", 17},
+ {"Jenny", 26},
+}
+
+func main() {
+ fmt.Println(people)
+ sort.Sort(ByAge(people))
+ fmt.Println(people)
+}
+`
+
func TestExamples(t *testing.T) {
- dir := filepath.Join("testdata", "examples")
- filenames, err := filepath.Glob(filepath.Join(dir, "*.go"))
+ fset := token.NewFileSet()
+ file, err := parser.ParseFile(fset, "test.go", strings.NewReader(exampleTestFile), parser.ParseComments)
if err != nil {
t.Fatal(err)
}
- for _, filename := range filenames {
- t.Run(strings.TrimSuffix(filepath.Base(filename), ".go"), func(t *testing.T) {
- fset := token.NewFileSet()
- astFile, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
- if err != nil {
- t.Fatal(err)
+ for i, e := range doc.Examples(file) {
+ c := exampleTestCases[i]
+ if e.Name != c.Name {
+ t.Errorf("got Name == %q, want %q", e.Name, c.Name)
+ }
+ if w := c.Play; w != "" {
+ g := formatFile(t, fset, e.Play)
+ if g != w {
+ t.Errorf("%s: got Play == %q, want %q", c.Name, g, w)
}
- goldenFilename := strings.TrimSuffix(filename, ".go") + ".golden"
- golden, err := readSectionFile(goldenFilename)
- if err != nil {
- t.Fatal(err)
- }
- examples := map[string]*doc.Example{}
- unseen := map[string]bool{} // examples we haven't seen yet
- for _, e := range doc.Examples(fset, astFile) {
- examples[e.Name] = e
- unseen[e.Name] = true
- }
- for section, want := range golden {
- words := strings.Split(section, ".")
- if len(words) != 2 {
- t.Fatalf("bad section name %q", section)
- }
- name, kind := words[0], words[1]
- ex := examples[name]
- if ex == nil {
- t.Fatalf("no example named %q", name)
- }
- switch kind {
- case "Play":
- got := strings.TrimSpace(formatFile(t, fset, ex.Play))
- if diff := cmp.Diff(want, got); diff != "" {
- t.Errorf("%s Play: mismatch (-want, +got):\n%s", name, diff)
- }
- delete(unseen, name)
- case "Output":
- got := strings.TrimSpace(ex.Output)
- if got != want {
- t.Errorf("%s Output: got\n%q\n---- want ----\n%q", ex.Name, got, want)
- }
- default:
- t.Fatalf("bad section kind %q", kind)
- }
- }
- for name := range unseen {
- t.Errorf("no Play golden for example %q", name)
- }
- })
+ }
+ if g, w := e.Output, c.Output; g != w {
+ t.Errorf("%s: got Output == %q, want %q", c.Name, g, w)
+ }
+ }
+}
+
+const exampleWholeFile = `package foo_test
+
+type X int
+
+func (X) Foo() {
+}
+
+func (X) TestBlah() {
+}
+
+func (X) BenchmarkFoo() {
+}
+
+func (X) FuzzFoo() {
+}
+
+func Example() {
+ fmt.Println("Hello, world!")
+ // Output: Hello, world!
+}
+`
+
+const exampleWholeFileOutput = `package main
+
+type X int
+
+func (X) Foo() {
+}
+
+func (X) TestBlah() {
+}
+
+func (X) BenchmarkFoo() {
+}
+
+func (X) FuzzFoo() {
+}
+
+func main() {
+ fmt.Println("Hello, world!")
+}
+`
+
+const exampleWholeFileFunction = `package foo_test
+
+func Foo(x int) {
+}
+
+func Example() {
+ fmt.Println("Hello, world!")
+ // Output: Hello, world!
+}
+`
+
+const exampleWholeFileFunctionOutput = `package main
+
+func Foo(x int) {
+}
+
+func main() {
+ fmt.Println("Hello, world!")
+}
+`
+
+const exampleWholeFileExternalFunction = `package foo_test
+
+func foo(int)
+
+func Example() {
+ foo(42)
+ // Output:
+}
+`
+
+const exampleWholeFileExternalFunctionOutput = `package main
+
+func foo(int)
+
+func main() {
+ foo(42)
+}
+`
+
+var exampleWholeFileTestCases = []struct {
+ Title, Source, Play, Output string
+}{
+ {
+ "Methods",
+ exampleWholeFile,
+ exampleWholeFileOutput,
+ "Hello, world!\n",
+ },
+ {
+ "Function",
+ exampleWholeFileFunction,
+ exampleWholeFileFunctionOutput,
+ "Hello, world!\n",
+ },
+ {
+ "ExternalFunction",
+ exampleWholeFileExternalFunction,
+ exampleWholeFileExternalFunctionOutput,
+ "",
+ },
+}
+
+func TestExamplesWholeFile(t *testing.T) {
+ for _, c := range exampleWholeFileTestCases {
+ fset := token.NewFileSet()
+ file, err := parser.ParseFile(fset, "test.go", strings.NewReader(c.Source), parser.ParseComments)
+ if err != nil {
+ t.Fatal(err)
+ }
+ es := doc.Examples(file)
+ if len(es) != 1 {
+ t.Fatalf("%s: wrong number of examples; got %d want 1", c.Title, len(es))
+ }
+ e := es[0]
+ if e.Name != "" {
+ t.Errorf("%s: got Name == %q, want %q", c.Title, e.Name, "")
+ }
+ if g, w := formatFile(t, fset, e.Play), c.Play; g != w {
+ t.Errorf("%s: got Play == %q, want %q", c.Title, g, w)
+ }
+ if g, w := e.Output, c.Output; g != w {
+ t.Errorf("%s: got Output == %q, want %q", c.Title, g, w)
+ }
+ }
+}
+
+const exampleInspectSignature = `package foo_test
+
+import (
+ "bytes"
+ "io"
+)
+
+func getReader() io.Reader { return nil }
+
+func do(b bytes.Reader) {}
+
+func Example() {
+ getReader()
+ do()
+ // Output:
+}
+
+func ExampleIgnored() {
+}
+`
+
+const exampleInspectSignatureOutput = `package main
+
+import (
+ "bytes"
+ "io"
+)
+
+func getReader() io.Reader { return nil }
+
+func do(b bytes.Reader) {}
+
+func main() {
+ getReader()
+ do()
+}
+`
+
+func TestExampleInspectSignature(t *testing.T) {
+ // Verify that "bytes" and "io" are imported. See issue #28492.
+ fset := token.NewFileSet()
+ file, err := parser.ParseFile(fset, "test.go", strings.NewReader(exampleInspectSignature), parser.ParseComments)
+ if err != nil {
+ t.Fatal(err)
+ }
+ es := doc.Examples(file)
+ if len(es) != 2 {
+ t.Fatalf("wrong number of examples; got %d want 2", len(es))
+ }
+ // We are interested in the first example only.
+ e := es[0]
+ if e.Name != "" {
+ t.Errorf("got Name == %q, want %q", e.Name, "")
+ }
+ if g, w := formatFile(t, fset, e.Play), exampleInspectSignatureOutput; g != w {
+ t.Errorf("got Play == %q, want %q", g, w)
+ }
+ if g, w := e.Output, ""; g != w {
+ t.Errorf("got Output == %q, want %q", g, w)
+ }
+}
+
+const exampleEmpty = `
+package p
+func Example() {}
+func Example_a()
+`
+
+const exampleEmptyOutput = `package main
+
+func main() {}
+func main()
+`
+
+func TestExampleEmpty(t *testing.T) {
+ fset := token.NewFileSet()
+ file, err := parser.ParseFile(fset, "test.go", strings.NewReader(exampleEmpty), parser.ParseComments)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ es := doc.Examples(file)
+ if len(es) != 1 {
+ t.Fatalf("wrong number of examples; got %d want 1", len(es))
+ }
+ e := es[0]
+ if e.Name != "" {
+ t.Errorf("got Name == %q, want %q", e.Name, "")
+ }
+ if g, w := formatFile(t, fset, e.Play), exampleEmptyOutput; g != w {
+ t.Errorf("got Play == %q, want %q", g, w)
+ }
+ if g, w := e.Output, ""; g != w {
+ t.Errorf("got Output == %q, want %q", g, w)
}
}
func formatFile(t *testing.T, fset *token.FileSet, n *ast.File) string {
- t.Helper()
if n == nil {
return "<nil>"
}
@@ -305,20 +747,3 @@
}
return f
}
-
-// readSectionFile reads a file that is divided into sections, and returns
-// a map from section name to contents.
-//
-// We use the txtar format for the file. See https://pkg.go.dev/golang.org/x/tools/txtar.
-// Although the format talks about filenames as the keys, they can be arbitrary strings.
-func readSectionFile(filename string) (map[string]string, error) {
- archive, err := txtar.ParseFile(filename)
- if err != nil {
- return nil, err
- }
- m := map[string]string{}
- for _, f := range archive.Files {
- m[f.Name] = strings.TrimSpace(string(f.Data))
- }
- return m, nil
-}