diff options
Diffstat (limited to 'src/pkg/go/doc/example.go')
-rw-r--r-- | src/pkg/go/doc/example.go | 270 |
1 files changed, 245 insertions, 25 deletions
diff --git a/src/pkg/go/doc/example.go b/src/pkg/go/doc/example.go index a7e0e250a..693ad5b94 100644 --- a/src/pkg/go/doc/example.go +++ b/src/pkg/go/doc/example.go @@ -9,21 +9,29 @@ package doc import ( "go/ast" "go/token" + "path" "regexp" "sort" + "strconv" "strings" "unicode" "unicode/utf8" ) +// An Example represents an example function found in a source files. type Example struct { - Name string // name of the item being exemplified - Doc string // example function doc string - Code ast.Node - Comments []*ast.CommentGroup - Output string // expected output + Name string // name of the item being exemplified + Doc string // example function doc string + Code ast.Node + Play *ast.File // a whole program version of the example + Comments []*ast.CommentGroup + Output string // expected output + EmptyOutput bool // expect empty output + Order int // original source code order } +// Examples returns the examples found in the files, sorted by Name field. +// The Order fields record the order in which the examples were encountered. func Examples(files ...*ast.File) []*Example { var list []*Example for _, file := range files { @@ -52,12 +60,16 @@ func Examples(files ...*ast.File) []*Example { if f.Doc != nil { doc = f.Doc.Text() } + output, hasOutput := exampleOutput(f.Body, file.Comments) flist = append(flist, &Example{ - Name: name[len("Example"):], - Doc: doc, - Code: f.Body, - Comments: file.Comments, - Output: exampleOutput(f, file.Comments), + Name: name[len("Example"):], + Doc: doc, + Code: f.Body, + Play: playExample(file, f.Body), + Comments: file.Comments, + Output: output, + EmptyOutput: output == "" && hasOutput, + Order: len(flist), }) } if !hasTests && numDecl > 1 && len(flist) == 1 { @@ -65,6 +77,7 @@ func Examples(files ...*ast.File) []*Example { // 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...) } @@ -74,26 +87,22 @@ func Examples(files ...*ast.File) []*Example { var outputPrefix = regexp.MustCompile(`(?i)^[[:space:]]*output:`) -func exampleOutput(fun *ast.FuncDecl, comments []*ast.CommentGroup) string { - // find the last comment in the function - var last *ast.CommentGroup - for _, cg := range comments { - if cg.Pos() < fun.Pos() { - continue - } - if cg.End() > fun.End() { - break - } - last = cg - } - if last != nil { +// Extracts the expected output and whether there was a valid output comment +func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) (output string, ok bool) { + if _, last := lastComment(b, comments); last != nil { // test that it begins with the correct prefix text := last.Text() if loc := outputPrefix.FindStringIndex(text); loc != nil { - return strings.TrimSpace(text[loc[1]:]) + text = text[loc[1]:] + // Strip zero or more spaces followed by \n or a single space. + text = strings.TrimLeft(text, " ") + if len(text) > 0 && text[0] == '\n' { + text = text[1:] + } + return text, true } } - return "" // no suitable comment found + return "", false // no suitable comment found } // isTest tells whether name looks like a test, example, or benchmark. @@ -115,3 +124,214 @@ type exampleByName []*Example func (s exampleByName) Len() int { return len(s) } func (s exampleByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s exampleByName) Less(i, j int) bool { return s[i].Name < s[j].Name } + +// playExample synthesizes a new *ast.File based on the provided +// file with the provided function body as the body of main. +func playExample(file *ast.File, body *ast.BlockStmt) *ast.File { + if !strings.HasSuffix(file.Name.Name, "_test") { + // We don't support examples that are part of the + // greater package (yet). + return nil + } + + // Find top-level declarations in the file. + topDecls := make(map[*ast.Object]bool) + for _, decl := range file.Decls { + switch d := decl.(type) { + case *ast.FuncDecl: + topDecls[d.Name.Obj] = true + case *ast.GenDecl: + for _, spec := range d.Specs { + switch s := spec.(type) { + case *ast.TypeSpec: + topDecls[s.Name.Obj] = true + case *ast.ValueSpec: + for _, id := range s.Names { + topDecls[id.Obj] = true + } + } + } + } + } + + // Find unresolved identifiers and uses of top-level declarations. + unresolved := make(map[string]bool) + usesTopDecl := false + var inspectFunc func(ast.Node) bool + inspectFunc = func(n ast.Node) bool { + // 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".) + if e, ok := n.(*ast.SelectorExpr); ok { + ast.Inspect(e.X, inspectFunc) + return false + } + if id, ok := n.(*ast.Ident); ok { + if id.Obj == nil { + unresolved[id.Name] = true + } else if topDecls[id.Obj] { + usesTopDecl = true + } + } + return true + } + ast.Inspect(body, inspectFunc) + if usesTopDecl { + // We don't support examples that are not self-contained (yet). + return nil + } + + // 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 + } + 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 "Output:" commment and adjust body end position. + body, comments = stripOutputComment(body, comments) + + // 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{ + Name: ast.NewIdent("main"), + Type: &ast.FuncType{}, + Body: body, + } + + // Synthesize file. + return &ast.File{ + Name: ast.NewIdent("main"), + Decls: []ast.Decl{importDecl, funcDecl}, + Comments: comments, + } +} + +// 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 { + // Strip copyright comment if present. + comments := file.Comments + if len(comments) > 0 && strings.HasPrefix(comments[0].Text(), "Copyright") { + comments = comments[1:] + } + + // Copy declaration slice, rewriting the ExampleX function to main. + var decls []ast.Decl + for _, d := range file.Decls { + if f, ok := d.(*ast.FuncDecl); ok && isTest(f.Name.Name, "Example") { + // Copy the FuncDecl, as it may be used elsewhere. + newF := *f + newF.Name = ast.NewIdent("main") + newF.Body, comments = stripOutputComment(f.Body, comments) + d = &newF + } + decls = append(decls, d) + } + + // Copy the File, as it may be used elsewhere. + f := *file + f.Name = ast.NewIdent("main") + f.Decls = decls + f.Comments = comments + return &f +} + +// stripOutputComment finds and removes an "Output:" commment from body +// and comments, and adjusts the body block's end position. +func stripOutputComment(body *ast.BlockStmt, comments []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) { + // Do nothing if no "Output:" comment found. + i, last := lastComment(body, comments) + if last == nil || !outputPrefix.MatchString(last.Text()) { + return body, comments + } + + // Copy body and comments, as the originals may be used elsewhere. + newBody := &ast.BlockStmt{ + Lbrace: body.Lbrace, + List: body.List, + Rbrace: last.Pos(), + } + newComments := make([]*ast.CommentGroup, len(comments)-1) + copy(newComments, comments[:i]) + copy(newComments[i:], comments[i+1:]) + return newBody, newComments +} + +// lastComment returns the last comment inside the provided block. +func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.CommentGroup) { + pos, end := b.Pos(), b.End() + for j, cg := range c { + if cg.Pos() < pos { + continue + } + if cg.End() > end { + break + } + i, last = j, cg + } + return +} |