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  }