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