blob: a7e0e250a2e9d74a6e82d1f397e6f831794f8d35 [file] [log] [blame]
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Extract example functions from file ASTs.
package doc
import (
"go/ast"
"go/token"
"regexp"
"sort"
"strings"
"unicode"
"unicode/utf8"
)
type Example struct {
Name string // name of the item being exemplified
Doc string // example function doc string
Code ast.Node
Comments []*ast.CommentGroup
Output string // expected output
}
func Examples(files ...*ast.File) []*Example {
var list []*Example
for _, file := range files {
hasTests := false // file contains tests or benchmarks
numDecl := 0 // number of non-import declarations in the file
var flist []*Example
for _, decl := range file.Decls {
if g, ok := decl.(*ast.GenDecl); ok && g.Tok != token.IMPORT {
numDecl++
continue
}
f, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
numDecl++
name := f.Name.Name
if isTest(name, "Test") || isTest(name, "Benchmark") {
hasTests = true
continue
}
if !isTest(name, "Example") {
continue
}
var doc string
if f.Doc != nil {
doc = f.Doc.Text()
}
flist = append(flist, &Example{
Name: name[len("Example"):],
Doc: doc,
Code: f.Body,
Comments: file.Comments,
Output: exampleOutput(f, file.Comments),
})
}
if !hasTests && numDecl > 1 && len(flist) == 1 {
// If this file only has one example function, some
// other top-level declarations, and no tests or
// benchmarks, use the whole file as the example.
flist[0].Code = file
}
list = append(list, flist...)
}
sort.Sort(exampleByName(list))
return list
}
var outputPrefix = regexp.MustCompile(`(?i)^[[:space:]]*output:`)
func exampleOutput(fun *ast.FuncDecl, comments []*ast.CommentGroup) string {
// find the last comment in the function
var last *ast.CommentGroup
for _, cg := range comments {
if cg.Pos() < fun.Pos() {
continue
}
if cg.End() > fun.End() {
break
}
last = cg
}
if last != nil {
// test that it begins with the correct prefix
text := last.Text()
if loc := outputPrefix.FindStringIndex(text); loc != nil {
return strings.TrimSpace(text[loc[1]:])
}
}
return "" // no suitable comment found
}
// isTest tells whether name looks like a test, example, or benchmark.
// It is a Test (say) if there is a character after Test that is not a
// lower-case letter. (We don't want Testiness.)
func isTest(name, prefix string) bool {
if !strings.HasPrefix(name, prefix) {
return false
}
if len(name) == len(prefix) { // "Test" is ok
return true
}
rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
return !unicode.IsLower(rune)
}
type exampleByName []*Example
func (s exampleByName) Len() int { return len(s) }
func (s exampleByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s exampleByName) Less(i, j int) bool { return s[i].Name < s[j].Name }