Source file src/go/doc/example.go
1
2
3
4
5
6
7 package doc
8
9 import (
10 "go/ast"
11 "go/token"
12 "internal/lazyregexp"
13 "path"
14 "sort"
15 "strconv"
16 "strings"
17 "unicode"
18 "unicode/utf8"
19 )
20
21
22 type Example struct {
23 Name string
24 Doc string
25 Code ast.Node
26 Play *ast.File
27 Comments []*ast.CommentGroup
28 Output string
29 Unordered bool
30 EmptyOutput bool
31 Order int
32 }
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47 func Examples(files ...*ast.File) []*Example {
48 var list []*Example
49 for _, file := range files {
50 hasTests := false
51 numDecl := 0
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 || f.Recv != nil {
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 if f.Body == nil {
72 continue
73 }
74 var doc string
75 if f.Doc != nil {
76 doc = f.Doc.Text()
77 }
78 output, unordered, hasOutput := exampleOutput(f.Body, file.Comments)
79 flist = append(flist, &Example{
80 Name: name[len("Example"):],
81 Doc: doc,
82 Code: f.Body,
83 Play: playExample(file, f),
84 Comments: file.Comments,
85 Output: output,
86 Unordered: unordered,
87 EmptyOutput: output == "" && hasOutput,
88 Order: len(flist),
89 })
90 }
91 if !hasTests && numDecl > 1 && len(flist) == 1 {
92
93
94
95 flist[0].Code = file
96 flist[0].Play = playExampleFile(file)
97 }
98 list = append(list, flist...)
99 }
100
101 sort.Slice(list, func(i, j int) bool {
102 return list[i].Name < list[j].Name
103 })
104 return list
105 }
106
107 var outputPrefix = lazyregexp.New(`(?i)^[[:space:]]*(unordered )?output:`)
108
109
110 func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) (output string, unordered, ok bool) {
111 if _, last := lastComment(b, comments); last != nil {
112
113 text := last.Text()
114 if loc := outputPrefix.FindStringSubmatchIndex(text); loc != nil {
115 if loc[2] != -1 {
116 unordered = true
117 }
118 text = text[loc[1]:]
119
120 text = strings.TrimLeft(text, " ")
121 if len(text) > 0 && text[0] == '\n' {
122 text = text[1:]
123 }
124 return text, unordered, true
125 }
126 }
127 return "", false, false
128 }
129
130
131
132
133 func isTest(name, prefix string) bool {
134 if !strings.HasPrefix(name, prefix) {
135 return false
136 }
137 if len(name) == len(prefix) {
138 return true
139 }
140 rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
141 return !unicode.IsLower(rune)
142 }
143
144
145
146 func playExample(file *ast.File, f *ast.FuncDecl) *ast.File {
147 body := f.Body
148
149 if !strings.HasSuffix(file.Name.Name, "_test") {
150
151
152 return nil
153 }
154
155
156 topDecls := make(map[*ast.Object]ast.Decl)
157 typMethods := make(map[string][]ast.Decl)
158
159 for _, decl := range file.Decls {
160 switch d := decl.(type) {
161 case *ast.FuncDecl:
162 if d.Recv == nil {
163 topDecls[d.Name.Obj] = d
164 } else {
165 if len(d.Recv.List) == 1 {
166 t := d.Recv.List[0].Type
167 tname, _ := baseTypeName(t)
168 typMethods[tname] = append(typMethods[tname], d)
169 }
170 }
171 case *ast.GenDecl:
172 for _, spec := range d.Specs {
173 switch s := spec.(type) {
174 case *ast.TypeSpec:
175 topDecls[s.Name.Obj] = d
176 case *ast.ValueSpec:
177 for _, name := range s.Names {
178 topDecls[name.Obj] = d
179 }
180 }
181 }
182 }
183 }
184
185
186 unresolved := make(map[string]bool)
187 var depDecls []ast.Decl
188 hasDepDecls := make(map[ast.Decl]bool)
189
190 var inspectFunc func(ast.Node) bool
191 inspectFunc = func(n ast.Node) bool {
192 switch e := n.(type) {
193 case *ast.Ident:
194 if e.Obj == nil && e.Name != "_" {
195 unresolved[e.Name] = true
196 } else if d := topDecls[e.Obj]; d != nil {
197 if !hasDepDecls[d] {
198 hasDepDecls[d] = true
199 depDecls = append(depDecls, d)
200 }
201 }
202 return true
203 case *ast.SelectorExpr:
204
205
206
207 ast.Inspect(e.X, inspectFunc)
208 return false
209 case *ast.KeyValueExpr:
210
211
212
213 ast.Inspect(e.Value, inspectFunc)
214 return false
215 }
216 return true
217 }
218 ast.Inspect(body, inspectFunc)
219 for i := 0; i < len(depDecls); i++ {
220 switch d := depDecls[i].(type) {
221 case *ast.FuncDecl:
222
223 if d.Type.Params != nil {
224 for _, p := range d.Type.Params.List {
225 ast.Inspect(p.Type, inspectFunc)
226 }
227 }
228 if d.Type.Results != nil {
229 for _, r := range d.Type.Results.List {
230 ast.Inspect(r.Type, inspectFunc)
231 }
232 }
233
234 ast.Inspect(d.Body, inspectFunc)
235 case *ast.GenDecl:
236 for _, spec := range d.Specs {
237 switch s := spec.(type) {
238 case *ast.TypeSpec:
239 ast.Inspect(s.Type, inspectFunc)
240
241 depDecls = append(depDecls, typMethods[s.Name.Name]...)
242 case *ast.ValueSpec:
243 if s.Type != nil {
244 ast.Inspect(s.Type, inspectFunc)
245 }
246 for _, val := range s.Values {
247 ast.Inspect(val, inspectFunc)
248 }
249 }
250 }
251 }
252 }
253
254
255 for n := range unresolved {
256 if predeclaredTypes[n] || predeclaredConstants[n] || predeclaredFuncs[n] {
257 delete(unresolved, n)
258 }
259 }
260
261
262
263
264 namedImports := make(map[string]string)
265 var blankImports []ast.Spec
266 for _, s := range file.Imports {
267 p, err := strconv.Unquote(s.Path.Value)
268 if err != nil {
269 continue
270 }
271 if p == "syscall/js" {
272
273
274 return nil
275 }
276 n := path.Base(p)
277 if s.Name != nil {
278 n = s.Name.Name
279 switch n {
280 case "_":
281 blankImports = append(blankImports, s)
282 continue
283 case ".":
284
285 return nil
286 }
287 }
288 if unresolved[n] {
289 namedImports[n] = p
290 delete(unresolved, n)
291 }
292 }
293
294
295
296 if len(unresolved) > 0 {
297 return nil
298 }
299
300
301 var comments []*ast.CommentGroup
302 for _, s := range blankImports {
303 if c := s.(*ast.ImportSpec).Doc; c != nil {
304 comments = append(comments, c)
305 }
306 }
307
308
309 for _, c := range file.Comments {
310 if body.Pos() <= c.Pos() && c.End() <= body.End() {
311 comments = append(comments, c)
312 }
313 }
314
315
316
317 body, comments = stripOutputComment(body, comments)
318
319
320 for _, d := range depDecls {
321 switch d := d.(type) {
322 case *ast.GenDecl:
323 if d.Doc != nil {
324 comments = append(comments, d.Doc)
325 }
326 case *ast.FuncDecl:
327 if d.Doc != nil {
328 comments = append(comments, d.Doc)
329 }
330 }
331 }
332
333
334 importDecl := &ast.GenDecl{
335 Tok: token.IMPORT,
336 Lparen: 1,
337 Rparen: 1,
338 }
339 for n, p := range namedImports {
340 s := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote(p)}}
341 if path.Base(p) != n {
342 s.Name = ast.NewIdent(n)
343 }
344 importDecl.Specs = append(importDecl.Specs, s)
345 }
346 importDecl.Specs = append(importDecl.Specs, blankImports...)
347
348
349 funcDecl := &ast.FuncDecl{
350 Name: ast.NewIdent("main"),
351 Type: f.Type,
352 Body: body,
353 }
354
355 decls := make([]ast.Decl, 0, 2+len(depDecls))
356 decls = append(decls, importDecl)
357 decls = append(decls, depDecls...)
358 decls = append(decls, funcDecl)
359
360 sort.Slice(decls, func(i, j int) bool {
361 return decls[i].Pos() < decls[j].Pos()
362 })
363
364 sort.Slice(comments, func(i, j int) bool {
365 return comments[i].Pos() < comments[j].Pos()
366 })
367
368
369 return &ast.File{
370 Name: ast.NewIdent("main"),
371 Decls: decls,
372 Comments: comments,
373 }
374 }
375
376
377
378 func playExampleFile(file *ast.File) *ast.File {
379
380 comments := file.Comments
381 if len(comments) > 0 && strings.HasPrefix(comments[0].Text(), "Copyright") {
382 comments = comments[1:]
383 }
384
385
386 var decls []ast.Decl
387 for _, d := range file.Decls {
388 if f, ok := d.(*ast.FuncDecl); ok && isTest(f.Name.Name, "Example") {
389
390 newF := *f
391 newF.Name = ast.NewIdent("main")
392 newF.Body, comments = stripOutputComment(f.Body, comments)
393 d = &newF
394 }
395 decls = append(decls, d)
396 }
397
398
399 f := *file
400 f.Name = ast.NewIdent("main")
401 f.Decls = decls
402 f.Comments = comments
403 return &f
404 }
405
406
407
408 func stripOutputComment(body *ast.BlockStmt, comments []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) {
409
410 i, last := lastComment(body, comments)
411 if last == nil || !outputPrefix.MatchString(last.Text()) {
412 return body, comments
413 }
414
415
416 newBody := &ast.BlockStmt{
417 Lbrace: body.Lbrace,
418 List: body.List,
419 Rbrace: last.Pos(),
420 }
421 newComments := make([]*ast.CommentGroup, len(comments)-1)
422 copy(newComments, comments[:i])
423 copy(newComments[i:], comments[i+1:])
424 return newBody, newComments
425 }
426
427
428 func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.CommentGroup) {
429 if b == nil {
430 return
431 }
432 pos, end := b.Pos(), b.End()
433 for j, cg := range c {
434 if cg.Pos() < pos {
435 continue
436 }
437 if cg.End() > end {
438 break
439 }
440 i, last = j, cg
441 }
442 return
443 }
444
View as plain text