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.
5 // Extract example functions from file ASTs.
21 // An Example represents an example function found in a source files.
23 Name
string // name of the item being exemplified
24 Doc
string // example function doc string
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
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
{
37 for _
, file
:= range files
{
38 hasTests
:= false // file contains tests or benchmarks
39 numDecl
:= 0 // number of non-import declarations in the file
41 for _
, decl
:= range file
.Decls
{
42 if g
, ok
:= decl
.(*ast
.GenDecl
); ok
&& g
.Tok
!= token
.IMPORT
{
46 f
, ok
:= decl
.(*ast
.FuncDecl
)
52 if isTest(name
, "Test") ||
isTest(name
, "Benchmark") {
56 if !isTest(name
, "Example") {
63 output
, hasOutput
:= exampleOutput(f
.Body
, file
.Comments
)
64 flist
= append(flist
, &Example
{
65 Name
: name
[len("Example"):],
68 Play
: playExample(file
, f
.Body
),
69 Comments
: file
.Comments
,
71 EmptyOutput
: output
== "" && hasOutput
,
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.
80 flist
[0].Play
= playExampleFile(file
)
82 list
= append(list
, flist
...)
84 sort
.Sort(exampleByName(list
))
88 var outputPrefix
= regexp
.MustCompile(`(?i)^[[:space:]]*output:`)
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
95 if loc
:= outputPrefix
.FindStringIndex(text
); loc
!= nil {
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' {
105 return "", false // no suitable comment found
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
) {
115 if len(name
) == len(prefix
) { // "Test" is ok
118 rune
, _
:= utf8
.DecodeRuneInString(name
[len(prefix
):])
119 return !unicode
.IsLower(rune
)
122 type exampleByName
[]*Example
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
}
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).
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) {
142 topDecls
[d
.Name
.Obj
] = true
144 for _
, spec
:= range d
.Specs
{
145 switch s
:= spec
.(type) {
147 topDecls
[s
.Name
.Obj
] = true
149 for _
, id
:= range s
.Names
{
150 topDecls
[id
.Obj
] = true
157 // Find unresolved identifiers and uses of top-level declarations.
158 unresolved
:= make(map[string]bool)
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
)
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
)
176 if id
, ok
:= n
.(*ast
.Ident
); ok
{
178 unresolved
[id
.Name
] = true
179 } else if topDecls
[id
.Obj
] {
185 ast
.Inspect(body
, inspectFunc
)
187 // We don't support examples that are not self-contained (yet).
191 // Remove predeclared identifiers from unresolved list.
192 for n
:= range unresolved
{
193 if predeclaredTypes
[n
] || predeclaredConstants
[n
] || predeclaredFuncs
[n
] {
194 delete(unresolved
, n
)
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
)
213 blankImports
= append(blankImports
, s
)
216 // We can't resolve dot imports (yet).
222 delete(unresolved
, n
)
226 // If there are other unresolved identifiers, give up because this
227 // synthesized file is not going to build.
228 if len(unresolved
) > 0 {
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
)
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
)
247 // Strip "Output:" commment and adjust body end position.
248 body
, comments
= stripOutputComment(body
, comments
)
250 // Synthesize import declaration.
251 importDecl
:= &ast
.GenDecl
{
253 Lparen
: 1, // Need non-zero Lparen and Rparen so that printer
254 Rparen
: 1, // treats this as a factored import.
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
)
261 importDecl
.Specs
= append(importDecl
.Specs
, s
)
263 importDecl
.Specs
= append(importDecl
.Specs
, blankImports
...)
265 // Synthesize main function.
266 funcDecl
:= &ast
.FuncDecl
{
267 Name
: ast
.NewIdent("main"),
268 Type
: &ast
.FuncType
{},
274 Name
: ast
.NewIdent("main"),
275 Decls
: []ast
.Decl
{importDecl
, funcDecl
},
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:]
289 // Copy declaration slice, rewriting the ExampleX function to main.
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.
295 newF
.Name
= ast
.NewIdent("main")
296 newF
.Body
, comments
= stripOutputComment(f
.Body
, comments
)
299 decls
= append(decls
, d
)
302 // Copy the File, as it may be used elsewhere.
304 f
.Name
= ast
.NewIdent("main")
306 f
.Comments
= comments
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
319 // Copy body and comments, as the originals may be used elsewhere.
320 newBody
:= &ast
.BlockStmt
{
325 newComments
:= make([]*ast
.CommentGroup
, len(comments
)-1)
326 copy(newComments
, comments
[:i
])
327 copy(newComments
[i
:], comments
[i
+1:])
328 return newBody
, newComments
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
{