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 &amp;
+		// 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 &amp;
 		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
-}