Home | History | Annotate | Download | only in doc
      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 }
    356