github.com/q45/go@v0.0.0-20151101211701-a4fb8c13db3f/src/go/doc/example.go (about) 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. 4 5 // Extract example functions from file ASTs. 6 7 package doc 8 9 import ( 10 "go/ast" 11 "go/token" 12 "path" 13 "regexp" 14 "sort" 15 "strconv" 16 "strings" 17 "unicode" 18 "unicode/utf8" 19 ) 20 21 // An Example represents an example function found in a source files. 22 type Example struct { 23 Name string // name of the item being exemplified 24 Doc string // example function doc string 25 Code ast.Node 26 Play *ast.File // a whole program version of the example 27 Comments []*ast.CommentGroup 28 Output string // expected output 29 EmptyOutput bool // expect empty output 30 Order int // original source code order 31 } 32 33 // Examples returns the examples found in the files, sorted by Name field. 34 // The Order fields record the order in which the examples were encountered. 35 // 36 // Playable Examples must be in a package whose name ends in "_test". 37 // An Example is "playable" (the Play field is non-nil) in either of these 38 // circumstances: 39 // - The example function is self-contained: the function references only 40 // identifiers from other packages (or predeclared identifiers, such as 41 // "int") and the test file does not include a dot import. 42 // - The entire test file is the example: the file contains exactly one 43 // example function, zero test or benchmark functions, and at least one 44 // top-level function, type, variable, or constant declaration other 45 // than the example function. 46 func Examples(files ...*ast.File) []*Example { 47 var list []*Example 48 for _, file := range files { 49 hasTests := false // file contains tests or benchmarks 50 numDecl := 0 // number of non-import declarations in the file 51 var flist []*Example 52 for _, decl := range file.Decls { 53 if g, ok := decl.(*ast.GenDecl); ok && g.Tok != token.IMPORT { 54 numDecl++ 55 continue 56 } 57 f, ok := decl.(*ast.FuncDecl) 58 if !ok { 59 continue 60 } 61 numDecl++ 62 name := f.Name.Name 63 if isTest(name, "Test") || isTest(name, "Benchmark") { 64 hasTests = true 65 continue 66 } 67 if !isTest(name, "Example") { 68 continue 69 } 70 var doc string 71 if f.Doc != nil { 72 doc = f.Doc.Text() 73 } 74 output, hasOutput := exampleOutput(f.Body, file.Comments) 75 flist = append(flist, &Example{ 76 Name: name[len("Example"):], 77 Doc: doc, 78 Code: f.Body, 79 Play: playExample(file, f.Body), 80 Comments: file.Comments, 81 Output: output, 82 EmptyOutput: output == "" && hasOutput, 83 Order: len(flist), 84 }) 85 } 86 if !hasTests && numDecl > 1 && len(flist) == 1 { 87 // If this file only has one example function, some 88 // other top-level declarations, and no tests or 89 // benchmarks, use the whole file as the example. 90 flist[0].Code = file 91 flist[0].Play = playExampleFile(file) 92 } 93 list = append(list, flist...) 94 } 95 sort.Sort(exampleByName(list)) 96 return list 97 } 98 99 var outputPrefix = regexp.MustCompile(`(?i)^[[:space:]]*output:`) 100 101 // Extracts the expected output and whether there was a valid output comment 102 func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) (output string, ok bool) { 103 if _, last := lastComment(b, comments); last != nil { 104 // test that it begins with the correct prefix 105 text := last.Text() 106 if loc := outputPrefix.FindStringIndex(text); loc != nil { 107 text = text[loc[1]:] 108 // Strip zero or more spaces followed by \n or a single space. 109 text = strings.TrimLeft(text, " ") 110 if len(text) > 0 && text[0] == '\n' { 111 text = text[1:] 112 } 113 return text, true 114 } 115 } 116 return "", false // no suitable comment found 117 } 118 119 // isTest tells whether name looks like a test, example, or benchmark. 120 // It is a Test (say) if there is a character after Test that is not a 121 // lower-case letter. (We don't want Testiness.) 122 func isTest(name, prefix string) bool { 123 if !strings.HasPrefix(name, prefix) { 124 return false 125 } 126 if len(name) == len(prefix) { // "Test" is ok 127 return true 128 } 129 rune, _ := utf8.DecodeRuneInString(name[len(prefix):]) 130 return !unicode.IsLower(rune) 131 } 132 133 type exampleByName []*Example 134 135 func (s exampleByName) Len() int { return len(s) } 136 func (s exampleByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 137 func (s exampleByName) Less(i, j int) bool { return s[i].Name < s[j].Name } 138 139 // playExample synthesizes a new *ast.File based on the provided 140 // file with the provided function body as the body of main. 141 func playExample(file *ast.File, body *ast.BlockStmt) *ast.File { 142 if !strings.HasSuffix(file.Name.Name, "_test") { 143 // We don't support examples that are part of the 144 // greater package (yet). 145 return nil 146 } 147 148 // Find top-level declarations in the file. 149 topDecls := make(map[*ast.Object]bool) 150 for _, decl := range file.Decls { 151 switch d := decl.(type) { 152 case *ast.FuncDecl: 153 topDecls[d.Name.Obj] = true 154 case *ast.GenDecl: 155 for _, spec := range d.Specs { 156 switch s := spec.(type) { 157 case *ast.TypeSpec: 158 topDecls[s.Name.Obj] = true 159 case *ast.ValueSpec: 160 for _, id := range s.Names { 161 topDecls[id.Obj] = true 162 } 163 } 164 } 165 } 166 } 167 168 // Find unresolved identifiers and uses of top-level declarations. 169 unresolved := make(map[string]bool) 170 usesTopDecl := false 171 var inspectFunc func(ast.Node) bool 172 inspectFunc = func(n ast.Node) bool { 173 // For selector expressions, only inspect the left hand side. 174 // (For an expression like fmt.Println, only add "fmt" to the 175 // set of unresolved names, not "Println".) 176 if e, ok := n.(*ast.SelectorExpr); ok { 177 ast.Inspect(e.X, inspectFunc) 178 return false 179 } 180 // For key value expressions, only inspect the value 181 // as the key should be resolved by the type of the 182 // composite literal. 183 if e, ok := n.(*ast.KeyValueExpr); ok { 184 ast.Inspect(e.Value, inspectFunc) 185 return false 186 } 187 if id, ok := n.(*ast.Ident); ok { 188 if id.Obj == nil { 189 unresolved[id.Name] = true 190 } else if topDecls[id.Obj] { 191 usesTopDecl = true 192 } 193 } 194 return true 195 } 196 ast.Inspect(body, inspectFunc) 197 if usesTopDecl { 198 // We don't support examples that are not self-contained (yet). 199 return nil 200 } 201 202 // Remove predeclared identifiers from unresolved list. 203 for n := range unresolved { 204 if predeclaredTypes[n] || predeclaredConstants[n] || predeclaredFuncs[n] { 205 delete(unresolved, n) 206 } 207 } 208 209 // Use unresolved identifiers to determine the imports used by this 210 // example. The heuristic assumes package names match base import 211 // paths for imports w/o renames (should be good enough most of the time). 212 namedImports := make(map[string]string) // [name]path 213 var blankImports []ast.Spec // _ imports 214 for _, s := range file.Imports { 215 p, err := strconv.Unquote(s.Path.Value) 216 if err != nil { 217 continue 218 } 219 n := path.Base(p) 220 if s.Name != nil { 221 n = s.Name.Name 222 switch n { 223 case "_": 224 blankImports = append(blankImports, s) 225 continue 226 case ".": 227 // We can't resolve dot imports (yet). 228 return nil 229 } 230 } 231 if unresolved[n] { 232 namedImports[n] = p 233 delete(unresolved, n) 234 } 235 } 236 237 // If there are other unresolved identifiers, give up because this 238 // synthesized file is not going to build. 239 if len(unresolved) > 0 { 240 return nil 241 } 242 243 // Include documentation belonging to blank imports. 244 var comments []*ast.CommentGroup 245 for _, s := range blankImports { 246 if c := s.(*ast.ImportSpec).Doc; c != nil { 247 comments = append(comments, c) 248 } 249 } 250 251 // Include comments that are inside the function body. 252 for _, c := range file.Comments { 253 if body.Pos() <= c.Pos() && c.End() <= body.End() { 254 comments = append(comments, c) 255 } 256 } 257 258 // Strip "Output:" comment and adjust body end position. 259 body, comments = stripOutputComment(body, comments) 260 261 // Synthesize import declaration. 262 importDecl := &ast.GenDecl{ 263 Tok: token.IMPORT, 264 Lparen: 1, // Need non-zero Lparen and Rparen so that printer 265 Rparen: 1, // treats this as a factored import. 266 } 267 for n, p := range namedImports { 268 s := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote(p)}} 269 if path.Base(p) != n { 270 s.Name = ast.NewIdent(n) 271 } 272 importDecl.Specs = append(importDecl.Specs, s) 273 } 274 importDecl.Specs = append(importDecl.Specs, blankImports...) 275 276 // Synthesize main function. 277 funcDecl := &ast.FuncDecl{ 278 Name: ast.NewIdent("main"), 279 Type: &ast.FuncType{Params: &ast.FieldList{}}, // FuncType.Params must be non-nil 280 Body: body, 281 } 282 283 // Synthesize file. 284 return &ast.File{ 285 Name: ast.NewIdent("main"), 286 Decls: []ast.Decl{importDecl, funcDecl}, 287 Comments: comments, 288 } 289 } 290 291 // playExampleFile takes a whole file example and synthesizes a new *ast.File 292 // such that the example is function main in package main. 293 func playExampleFile(file *ast.File) *ast.File { 294 // Strip copyright comment if present. 295 comments := file.Comments 296 if len(comments) > 0 && strings.HasPrefix(comments[0].Text(), "Copyright") { 297 comments = comments[1:] 298 } 299 300 // Copy declaration slice, rewriting the ExampleX function to main. 301 var decls []ast.Decl 302 for _, d := range file.Decls { 303 if f, ok := d.(*ast.FuncDecl); ok && isTest(f.Name.Name, "Example") { 304 // Copy the FuncDecl, as it may be used elsewhere. 305 newF := *f 306 newF.Name = ast.NewIdent("main") 307 newF.Body, comments = stripOutputComment(f.Body, comments) 308 d = &newF 309 } 310 decls = append(decls, d) 311 } 312 313 // Copy the File, as it may be used elsewhere. 314 f := *file 315 f.Name = ast.NewIdent("main") 316 f.Decls = decls 317 f.Comments = comments 318 return &f 319 } 320 321 // stripOutputComment finds and removes an "Output:" comment from body 322 // and comments, and adjusts the body block's end position. 323 func stripOutputComment(body *ast.BlockStmt, comments []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) { 324 // Do nothing if no "Output:" comment found. 325 i, last := lastComment(body, comments) 326 if last == nil || !outputPrefix.MatchString(last.Text()) { 327 return body, comments 328 } 329 330 // Copy body and comments, as the originals may be used elsewhere. 331 newBody := &ast.BlockStmt{ 332 Lbrace: body.Lbrace, 333 List: body.List, 334 Rbrace: last.Pos(), 335 } 336 newComments := make([]*ast.CommentGroup, len(comments)-1) 337 copy(newComments, comments[:i]) 338 copy(newComments[i:], comments[i+1:]) 339 return newBody, newComments 340 } 341 342 // lastComment returns the last comment inside the provided block. 343 func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.CommentGroup) { 344 pos, end := b.Pos(), b.End() 345 for j, cg := range c { 346 if cg.Pos() < pos { 347 continue 348 } 349 if cg.End() > end { 350 break 351 } 352 i, last = j, cg 353 } 354 return 355 }