...
Run Format

Source file src/go/doc/example.go

     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	}
   362	

View as plain text