75
88
var outputPrefix = regexp.MustCompile(`(?i)^[[:space:]]*output:`)
77
func exampleOutput(fun *ast.FuncDecl, comments []*ast.CommentGroup) string {
78
// find the last comment in the function
79
var last *ast.CommentGroup
80
for _, cg := range comments {
81
if cg.Pos() < fun.Pos() {
84
if cg.End() > fun.End() {
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 {
90
93
// test that it begins with the correct prefix
91
94
text := last.Text()
92
95
if loc := outputPrefix.FindStringIndex(text); loc != nil {
93
return strings.TrimSpace(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' {
96
return "" // no suitable comment found
105
return "", false // no suitable comment found
99
108
// isTest tells whether name looks like a test, example, or benchmark.
115
124
func (s exampleByName) Len() int { return len(s) }
116
125
func (s exampleByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
117
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 {