1 // Copyright 2011 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
5 // Extract example functions from file ASTs.
22 Name string // name of the item being exemplified
23 Doc string // example function doc string
25 Play *ast.File // a whole program version of the example
26 Comments []*ast.CommentGroup
27 Output string // expected output
28 EmptyOutput bool // expect empty output
31 func Examples(files ...*ast.File) []*Example {
33 for _, file := range files {
34 hasTests := false // file contains tests or benchmarks
35 numDecl := 0 // number of non-import declarations in the file
37 for _, decl := range file.Decls {
38 if g, ok := decl.(*ast.GenDecl); ok && g.Tok != token.IMPORT {
42 f, ok := decl.(*ast.FuncDecl)
48 if isTest(name, "Test") || isTest(name, "Benchmark") {
52 if !isTest(name, "Example") {
59 output, hasOutput := exampleOutput(f.Body, file.Comments)
60 flist = append(flist, &Example{
61 Name: name[len("Example"):],
64 Play: playExample(file, f.Body),
65 Comments: file.Comments,
67 EmptyOutput: output == "" && hasOutput,
70 if !hasTests && numDecl > 1 && len(flist) == 1 {
71 // If this file only has one example function, some
72 // other top-level declarations, and no tests or
73 // benchmarks, use the whole file as the example.
75 flist[0].Play = playExampleFile(file)
77 list = append(list, flist...)
79 sort.Sort(exampleByName(list))
83 var outputPrefix = regexp.MustCompile(`(?i)^[[:space:]]*output:`)
85 // Extracts the expected output and whether there was a valid output comment
86 func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) (output string, ok bool) {
87 if _, last := lastComment(b, comments); last != nil {
88 // test that it begins with the correct prefix
90 if loc := outputPrefix.FindStringIndex(text); loc != nil {
92 // Strip zero or more spaces followed by \n or a single space.
93 text = strings.TrimLeft(text, " ")
94 if len(text) > 0 && text[0] == '\n' {
100 return "", false // no suitable comment found
103 // isTest tells whether name looks like a test, example, or benchmark.
104 // It is a Test (say) if there is a character after Test that is not a
105 // lower-case letter. (We don't want Testiness.)
106 func isTest(name, prefix string) bool {
107 if !strings.HasPrefix(name, prefix) {
110 if len(name) == len(prefix) { // "Test" is ok
113 rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
114 return !unicode.IsLower(rune)
117 type exampleByName []*Example
119 func (s exampleByName) Len() int { return len(s) }
120 func (s exampleByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
121 func (s exampleByName) Less(i, j int) bool { return s[i].Name < s[j].Name }
123 // playExample synthesizes a new *ast.File based on the provided
124 // file with the provided function body as the body of main.
125 func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
126 if !strings.HasSuffix(file.Name.Name, "_test") {
127 // We don't support examples that are part of the
128 // greater package (yet).
132 // Find top-level declarations in the file.
133 topDecls := make(map[*ast.Object]bool)
134 for _, decl := range file.Decls {
135 switch d := decl.(type) {
137 topDecls[d.Name.Obj] = true
139 for _, spec := range d.Specs {
140 switch s := spec.(type) {
142 topDecls[s.Name.Obj] = true
144 for _, id := range s.Names {
145 topDecls[id.Obj] = true
152 // Find unresolved identifiers and uses of top-level declarations.
153 unresolved := make(map[string]bool)
155 var inspectFunc func(ast.Node) bool
156 inspectFunc = func(n ast.Node) bool {
157 // For selector expressions, only inspect the left hand side.
158 // (For an expression like fmt.Println, only add "fmt" to the
159 // set of unresolved names, not "Println".)
160 if e, ok := n.(*ast.SelectorExpr); ok {
161 ast.Inspect(e.X, inspectFunc)
164 if id, ok := n.(*ast.Ident); ok {
166 unresolved[id.Name] = true
167 } else if topDecls[id.Obj] {
173 ast.Inspect(body, inspectFunc)
175 // We don't support examples that are not self-contained (yet).
179 // Remove predeclared identifiers from unresolved list.
180 for n := range unresolved {
181 if predeclaredTypes[n] || predeclaredConstants[n] || predeclaredFuncs[n] {
182 delete(unresolved, n)
186 // Use unresolved identifiers to determine the imports used by this
187 // example. The heuristic assumes package names match base import
188 // paths for imports w/o renames (should be good enough most of the time).
189 namedImports := make(map[string]string) // [name]path
190 var blankImports []ast.Spec // _ imports
191 for _, s := range file.Imports {
192 p, err := strconv.Unquote(s.Path.Value)
201 blankImports = append(blankImports, s)
204 // We can't resolve dot imports (yet).
210 delete(unresolved, n)
214 // If there are other unresolved identifiers, give up because this
215 // synthesized file is not going to build.
216 if len(unresolved) > 0 {
220 // Include documentation belonging to blank imports.
221 var comments []*ast.CommentGroup
222 for _, s := range blankImports {
223 if c := s.(*ast.ImportSpec).Doc; c != nil {
224 comments = append(comments, c)
228 // Include comments that are inside the function body.
229 for _, c := range file.Comments {
230 if body.Pos() <= c.Pos() && c.End() <= body.End() {
231 comments = append(comments, c)
235 // Strip "Output:" commment and adjust body end position.
236 body, comments = stripOutputComment(body, comments)
238 // Synthesize import declaration.
239 importDecl := &ast.GenDecl{
241 Lparen: 1, // Need non-zero Lparen and Rparen so that printer
242 Rparen: 1, // treats this as a factored import.
244 for n, p := range namedImports {
245 s := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote(p)}}
246 if path.Base(p) != n {
247 s.Name = ast.NewIdent(n)
249 importDecl.Specs = append(importDecl.Specs, s)
251 importDecl.Specs = append(importDecl.Specs, blankImports...)
253 // Synthesize main function.
254 funcDecl := &ast.FuncDecl{
255 Name: ast.NewIdent("main"),
256 Type: &ast.FuncType{},
262 Name: ast.NewIdent("main"),
263 Decls: []ast.Decl{importDecl, funcDecl},
268 // playExampleFile takes a whole file example and synthesizes a new *ast.File
269 // such that the example is function main in package main.
270 func playExampleFile(file *ast.File) *ast.File {
271 // Strip copyright comment if present.
272 comments := file.Comments
273 if len(comments) > 0 && strings.HasPrefix(comments[0].Text(), "Copyright") {
274 comments = comments[1:]
277 // Copy declaration slice, rewriting the ExampleX function to main.
279 for _, d := range file.Decls {
280 if f, ok := d.(*ast.FuncDecl); ok && isTest(f.Name.Name, "Example") {
281 // Copy the FuncDecl, as it may be used elsewhere.
283 newF.Name = ast.NewIdent("main")
284 newF.Body, comments = stripOutputComment(f.Body, comments)
287 decls = append(decls, d)
290 // Copy the File, as it may be used elsewhere.
292 f.Name = ast.NewIdent("main")
294 f.Comments = comments
298 // stripOutputComment finds and removes an "Output:" commment from body
299 // and comments, and adjusts the body block's end position.
300 func stripOutputComment(body *ast.BlockStmt, comments []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) {
301 // Do nothing if no "Output:" comment found.
302 i, last := lastComment(body, comments)
303 if last == nil || !outputPrefix.MatchString(last.Text()) {
304 return body, comments
307 // Copy body and comments, as the originals may be used elsewhere.
308 newBody := &ast.BlockStmt{
313 newComments := make([]*ast.CommentGroup, len(comments)-1)
314 copy(newComments, comments[:i])
315 copy(newComments[i:], comments[i+1:])
316 return newBody, newComments
319 // lastComment returns the last comment inside the provided block.
320 func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.CommentGroup) {
321 pos, end := b.Pos(), b.End()
322 for j, cg := range c {