Skip to content

Commit 4faada9

Browse files
committedNov 12, 2019
go/doc: add NewFromFiles with support for classifying examples
This CL is based on work started by Joe Tsai in CL 94855. It's rebased on top of the latest master branch, and addresses various code review comments and findings from attempting to use the original CL in practice. The testing package documents a naming convention for examples so that documentation tools can associate them with: • a package (Example or Example_suffix) • a function F (ExampleF or ExampleF_suffix) • a type T (ExampleT or ExampleT_suffix) • a method T.M (ExampleT_M or ExampleT_M_suffix) This naming convention is in widespread use and enforced via existing go vet checks. This change adds first-class support for classifying examples to go/doc, the package responsible for computing package documentation from Go AST. There isn't a way to supply test files to New that works well. External test files may have a package name with "_test" suffix, so ast.NewPackage may end up using the wrong package name if given test files. A workaround is to add test files to *ast.Package.Files after it is returned from ast.NewPackage: pkg, _ := ast.NewPackage(fset, goFiles, ...) for name, f := range testGoFiles { pkg.Files[name] = f } p := doc.New(pkg, ...) But that is not a good API. After nearly 8 years, a new entry-point is added to the go/doc package, the function NewFromFiles. It accepts a Go package in the form of a list of parsed Go files (including _test.go files) and an import path. The caller is responsible with filtering out files based on build constraints, as was the case before with New. NewFromFiles computes package documentation from .go files, extracts examples from _test.go files and classifies them. Examples fields are added to Package, Type, and Func. They are documented to only be populated with examples found in _test.go files provided to NewFromFiles. The new behavior is: 1. NewFromFiles computes package documentation from provided parsed .go files. It extracts examples from _test.go files. 2. It assigns each Example to corresponding Package, Type, or Func. 3. It sets the Suffix field in each example to the suffix. 4. Malformed examples are skipped. This change implements behavior that matches the current behavior of existing godoc-like tools, and will enable them to rely on the logic in go/doc instead of reimplementing it themselves. Fixes #23864 Change-Id: Iae834f2ff92fbd1c93a9bb7c2bf47d619bee05cf Reviewed-on: https://go-review.googlesource.com/c/go/+/204830 Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Robert Griesemer <gri@golang.org>
1 parent eb68c4a commit 4faada9

File tree

5 files changed

+435
-8
lines changed

5 files changed

+435
-8
lines changed
 

‎src/go/doc/doc.go

+103-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
package doc
77

88
import (
9+
"fmt"
910
"go/ast"
1011
"go/token"
12+
"strings"
1113
)
1214

1315
// Package is the documentation for an entire package.
@@ -28,6 +30,11 @@ type Package struct {
2830
Types []*Type
2931
Vars []*Value
3032
Funcs []*Func
33+
34+
// Examples is a sorted list of examples associated with
35+
// the package. Examples are extracted from _test.go files
36+
// provided to NewFromFiles.
37+
Examples []*Example
3138
}
3239

3340
// Value is the documentation for a (possibly grouped) var or const declaration.
@@ -50,6 +57,11 @@ type Type struct {
5057
Vars []*Value // sorted list of variables of (mostly) this type
5158
Funcs []*Func // sorted list of functions returning this type
5259
Methods []*Func // sorted list of methods (including embedded ones) of this type
60+
61+
// Examples is a sorted list of examples associated with
62+
// this type. Examples are extracted from _test.go files
63+
// provided to NewFromFiles.
64+
Examples []*Example
5365
}
5466

5567
// Func is the documentation for a func declaration.
@@ -63,6 +75,11 @@ type Func struct {
6375
Recv string // actual receiver "T" or "*T"
6476
Orig string // original receiver "T" or "*T"
6577
Level int // embedding level; 0 means not embedded
78+
79+
// Examples is a sorted list of examples associated with this
80+
// function or method. Examples are extracted from _test.go files
81+
// provided to NewFromFiles.
82+
Examples []*Example
6683
}
6784

6885
// A Note represents a marked comment starting with "MARKER(uid): note body".
@@ -75,7 +92,7 @@ type Note struct {
7592
Body string // note body text
7693
}
7794

78-
// Mode values control the operation of New.
95+
// Mode values control the operation of New and NewFromFiles.
7996
type Mode int
8097

8198
const (
@@ -95,6 +112,8 @@ const (
95112

96113
// New computes the package documentation for the given package AST.
97114
// New takes ownership of the AST pkg and may edit or overwrite it.
115+
// To have the Examples fields populated, use NewFromFiles and include
116+
// the package's _test.go files.
98117
//
99118
func New(pkg *ast.Package, importPath string, mode Mode) *Package {
100119
var r reader
@@ -115,3 +134,86 @@ func New(pkg *ast.Package, importPath string, mode Mode) *Package {
115134
Funcs: sortedFuncs(r.funcs, true),
116135
}
117136
}
137+
138+
// NewFromFiles computes documentation for a package.
139+
//
140+
// The package is specified by a list of *ast.Files and corresponding
141+
// file set, which must not be nil. NewFromFiles does not skip files
142+
// based on build constraints, so it is the caller's responsibility to
143+
// provide only the files that are matched by the build context.
144+
// The import path of the package is specified by importPath.
145+
//
146+
// Examples found in _test.go files are associated with the corresponding
147+
// type, function, method, or the package, based on their name.
148+
// If the example has a suffix in its name, it is set in the
149+
// Example.Suffix field. Examples with malformed names are skipped.
150+
//
151+
// Optionally, a single extra argument of type Mode can be provided to
152+
// control low-level aspects of the documentation extraction behavior.
153+
//
154+
// NewFromFiles takes ownership of the AST files and may edit them,
155+
// unless the PreserveAST Mode bit is on.
156+
//
157+
func NewFromFiles(fset *token.FileSet, files []*ast.File, importPath string, opts ...interface{}) (*Package, error) {
158+
// Check for invalid API usage.
159+
if fset == nil {
160+
panic(fmt.Errorf("doc.NewFromFiles: no token.FileSet provided (fset == nil)"))
161+
}
162+
var mode Mode
163+
switch len(opts) { // There can only be 0 or 1 options, so a simple switch works for now.
164+
case 0:
165+
// Nothing to do.
166+
case 1:
167+
m, ok := opts[0].(Mode)
168+
if !ok {
169+
panic(fmt.Errorf("doc.NewFromFiles: option argument type must be doc.Mode"))
170+
}
171+
mode = m
172+
default:
173+
panic(fmt.Errorf("doc.NewFromFiles: there must not be more than 1 option argument"))
174+
}
175+
176+
// Collect .go and _test.go files.
177+
var (
178+
goFiles = make(map[string]*ast.File)
179+
testGoFiles []*ast.File
180+
)
181+
for i := range files {
182+
f := fset.File(files[i].Pos())
183+
if f == nil {
184+
return nil, fmt.Errorf("file files[%d] is not found in the provided file set", i)
185+
}
186+
switch name := f.Name(); {
187+
case strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go"):
188+
goFiles[name] = files[i]
189+
case strings.HasSuffix(name, "_test.go"):
190+
testGoFiles = append(testGoFiles, files[i])
191+
default:
192+
return nil, fmt.Errorf("file files[%d] filename %q does not have a .go extension", i, name)
193+
}
194+
}
195+
196+
// TODO(dmitshur,gri): A relatively high level call to ast.NewPackage with a simpleImporter
197+
// ast.Importer implementation is made below. It might be possible to short-circuit and simplify.
198+
199+
// Compute package documentation.
200+
pkg, _ := ast.NewPackage(fset, goFiles, simpleImporter, nil) // Ignore errors that can happen due to unresolved identifiers.
201+
p := New(pkg, importPath, mode)
202+
classifyExamples(p, Examples(testGoFiles...))
203+
return p, nil
204+
}
205+
206+
// simpleImporter returns a (dummy) package object named by the last path
207+
// component of the provided package path (as is the convention for packages).
208+
// This is sufficient to resolve package identifiers without doing an actual
209+
// import. It never returns an error.
210+
func simpleImporter(imports map[string]*ast.Object, path string) (*ast.Object, error) {
211+
pkg := imports[path]
212+
if pkg == nil {
213+
// note that strings.LastIndex returns -1 if there is no "/"
214+
pkg = ast.NewObj(ast.Pkg, path[strings.LastIndex(path, "/")+1:])
215+
pkg.Data = ast.NewScope(nil) // required by ast.NewPackage for dot-import
216+
imports[path] = pkg
217+
}
218+
return pkg, nil
219+
}

‎src/go/doc/doc_test.go

+11-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"bytes"
99
"flag"
1010
"fmt"
11+
"go/ast"
1112
"go/parser"
1213
"go/printer"
1314
"go/token"
@@ -99,8 +100,16 @@ func test(t *testing.T, mode Mode) {
99100

100101
// test packages
101102
for _, pkg := range pkgs {
102-
importpath := dataDir + "/" + pkg.Name
103-
doc := New(pkg, importpath, mode)
103+
importPath := dataDir + "/" + pkg.Name
104+
var files []*ast.File
105+
for _, f := range pkg.Files {
106+
files = append(files, f)
107+
}
108+
doc, err := NewFromFiles(fset, files, importPath, mode)
109+
if err != nil {
110+
t.Error(err)
111+
continue
112+
}
104113

105114
// golden files always use / in filenames - canonicalize them
106115
for i, filename := range doc.Filenames {

‎src/go/doc/example.go

+106-5
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ import (
1818
"unicode/utf8"
1919
)
2020

21-
// An Example represents an example function found in a source files.
21+
// An Example represents an example function found in a test source file.
2222
type Example struct {
23-
Name string // name of the item being exemplified
23+
Name string // name of the item being exemplified (including optional suffix)
24+
Suffix string // example suffix, without leading '_' (only populated by NewFromFiles)
2425
Doc string // example function doc string
2526
Code ast.Node
2627
Play *ast.File // a whole program version of the example
@@ -31,8 +32,10 @@ type Example struct {
3132
Order int // original source code order
3233
}
3334

34-
// Examples returns the examples found in the files, sorted by Name field.
35+
// Examples returns the examples found in testFiles, sorted by Name field.
3536
// The Order fields record the order in which the examples were encountered.
37+
// The Suffix field is not populated when Examples is called directly, it is
38+
// only populated by NewFromFiles for examples it finds in _test.go files.
3639
//
3740
// Playable Examples must be in a package whose name ends in "_test".
3841
// An Example is "playable" (the Play field is non-nil) in either of these
@@ -44,9 +47,9 @@ type Example struct {
4447
// example function, zero test or benchmark functions, and at least one
4548
// top-level function, type, variable, or constant declaration other
4649
// than the example function.
47-
func Examples(files ...*ast.File) []*Example {
50+
func Examples(testFiles ...*ast.File) []*Example {
4851
var list []*Example
49-
for _, file := range files {
52+
for _, file := range testFiles {
5053
hasTests := false // file contains tests or benchmarks
5154
numDecl := 0 // number of non-import declarations in the file
5255
var flist []*Example
@@ -441,3 +444,101 @@ func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.Comm
441444
}
442445
return
443446
}
447+
448+
// classifyExamples classifies examples and assigns them to the Examples field
449+
// of the relevant Func, Type, or Package that the example is associated with.
450+
//
451+
// The classification process is ambiguous in some cases:
452+
//
453+
// - ExampleFoo_Bar matches a type named Foo_Bar
454+
// or a method named Foo.Bar.
455+
// - ExampleFoo_bar matches a type named Foo_bar
456+
// or Foo (with a "bar" suffix).
457+
//
458+
// Examples with malformed names are not associated with anything.
459+
//
460+
func classifyExamples(p *Package, examples []*Example) {
461+
if len(examples) == 0 {
462+
return
463+
}
464+
465+
// Mapping of names for funcs, types, and methods to the example listing.
466+
ids := make(map[string]*[]*Example)
467+
ids[""] = &p.Examples // package-level examples have an empty name
468+
for _, f := range p.Funcs {
469+
if !token.IsExported(f.Name) {
470+
continue
471+
}
472+
ids[f.Name] = &f.Examples
473+
}
474+
for _, t := range p.Types {
475+
if !token.IsExported(t.Name) {
476+
continue
477+
}
478+
ids[t.Name] = &t.Examples
479+
for _, f := range t.Funcs {
480+
if !token.IsExported(f.Name) {
481+
continue
482+
}
483+
ids[f.Name] = &f.Examples
484+
}
485+
for _, m := range t.Methods {
486+
if !token.IsExported(m.Name) || m.Level != 0 { // avoid forwarded methods from embedding
487+
continue
488+
}
489+
ids[strings.TrimPrefix(m.Recv, "*")+"_"+m.Name] = &m.Examples
490+
}
491+
}
492+
493+
// Group each example with the associated func, type, or method.
494+
for _, ex := range examples {
495+
// Consider all possible split points for the suffix
496+
// by starting at the end of string (no suffix case),
497+
// then trying all positions that contain a '_' character.
498+
//
499+
// An association is made on the first successful match.
500+
// Examples with malformed names that match nothing are skipped.
501+
for i := len(ex.Name); i >= 0; i = strings.LastIndexByte(ex.Name[:i], '_') {
502+
prefix, suffix, ok := splitExampleName(ex.Name, i)
503+
if !ok {
504+
continue
505+
}
506+
exs, ok := ids[prefix]
507+
if !ok {
508+
continue
509+
}
510+
ex.Suffix = suffix
511+
*exs = append(*exs, ex)
512+
break
513+
}
514+
}
515+
516+
// Sort list of example according to the user-specified suffix name.
517+
for _, exs := range ids {
518+
sort.Slice((*exs), func(i, j int) bool {
519+
return (*exs)[i].Suffix < (*exs)[j].Suffix
520+
})
521+
}
522+
}
523+
524+
// splitExampleName attempts to split example name s at index i,
525+
// and reports if that produces a valid split. The suffix may be
526+
// absent. Otherwise, it must start with a lower-case letter and
527+
// be preceded by '_'.
528+
//
529+
// One of i == len(s) or s[i] == '_' must be true.
530+
func splitExampleName(s string, i int) (prefix, suffix string, ok bool) {
531+
if i == len(s) {
532+
return s, "", true
533+
}
534+
if i == len(s)-1 {
535+
return "", "", false
536+
}
537+
prefix, suffix = s[:i], s[i+1:]
538+
return prefix, suffix, isExampleSuffix(suffix)
539+
}
540+
541+
func isExampleSuffix(s string) bool {
542+
r, size := utf8.DecodeRuneInString(s)
543+
return size > 0 && unicode.IsLower(r)
544+
}

0 commit comments

Comments
 (0)
Please sign in to comment.