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