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 Suffix string
25 Doc string
26 Code ast.Node
27 Play *ast.File
28 Comments []*ast.CommentGroup
29 Output string
30 Unordered bool
31 EmptyOutput bool
32 Order int
33 }
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 func Examples(testFiles ...*ast.File) []*Example {
51 var list []*Example
52 for _, file := range testFiles {
53 hasTests := false
54 numDecl := 0
55 var flist []*Example
56 for _, decl := range file.Decls {
57 if g, ok := decl.(*ast.GenDecl); ok && g.Tok != token.IMPORT {
58 numDecl++
59 continue
60 }
61 f, ok := decl.(*ast.FuncDecl)
62 if !ok || f.Recv != nil {
63 continue
64 }
65 numDecl++
66 name := f.Name.Name
67 if isTest(name, "Test") || isTest(name, "Benchmark") || isTest(name, "Fuzz") {
68 hasTests = true
69 continue
70 }
71 if !isTest(name, "Example") {
72 continue
73 }
74 if params := f.Type.Params; len(params.List) != 0 {
75 continue
76 }
77 if f.Body == nil {
78 continue
79 }
80 var doc string
81 if f.Doc != nil {
82 doc = f.Doc.Text()
83 }
84 output, unordered, hasOutput := exampleOutput(f.Body, file.Comments)
85 flist = append(flist, &Example{
86 Name: name[len("Example"):],
87 Doc: doc,
88 Code: f.Body,
89 Play: playExample(file, f),
90 Comments: file.Comments,
91 Output: output,
92 Unordered: unordered,
93 EmptyOutput: output == "" && hasOutput,
94 Order: len(flist),
95 })
96 }
97 if !hasTests && numDecl > 1 && len(flist) == 1 {
98
99
100
101 flist[0].Code = file
102 flist[0].Play = playExampleFile(file)
103 }
104 list = append(list, flist...)
105 }
106
107 sort.Slice(list, func(i, j int) bool {
108 return list[i].Name < list[j].Name
109 })
110 return list
111 }
112
113 var outputPrefix = lazyregexp.New(`(?i)^[[:space:]]*(unordered )?output:`)
114
115
116 func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) (output string, unordered, ok bool) {
117 if _, last := lastComment(b, comments); last != nil {
118
119 text := last.Text()
120 if loc := outputPrefix.FindStringSubmatchIndex(text); loc != nil {
121 if loc[2] != -1 {
122 unordered = true
123 }
124 text = text[loc[1]:]
125
126 text = strings.TrimLeft(text, " ")
127 if len(text) > 0 && text[0] == '\n' {
128 text = text[1:]
129 }
130 return text, unordered, true
131 }
132 }
133 return "", false, false
134 }
135
136
137
138
139 func isTest(name, prefix string) bool {
140 if !strings.HasPrefix(name, prefix) {
141 return false
142 }
143 if len(name) == len(prefix) {
144 return true
145 }
146 rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
147 return !unicode.IsLower(rune)
148 }
149
150
151
152 func playExample(file *ast.File, f *ast.FuncDecl) *ast.File {
153 body := f.Body
154
155 if !strings.HasSuffix(file.Name.Name, "_test") {
156
157
158 return nil
159 }
160
161
162 topDecls := make(map[*ast.Object]ast.Decl)
163 typMethods := make(map[string][]ast.Decl)
164
165 for _, decl := range file.Decls {
166 switch d := decl.(type) {
167 case *ast.FuncDecl:
168 if d.Recv == nil {
169 topDecls[d.Name.Obj] = d
170 } else {
171 if len(d.Recv.List) == 1 {
172 t := d.Recv.List[0].Type
173 tname, _ := baseTypeName(t)
174 typMethods[tname] = append(typMethods[tname], d)
175 }
176 }
177 case *ast.GenDecl:
178 for _, spec := range d.Specs {
179 switch s := spec.(type) {
180 case *ast.TypeSpec:
181 topDecls[s.Name.Obj] = d
182 case *ast.ValueSpec:
183 for _, name := range s.Names {
184 topDecls[name.Obj] = d
185 }
186 }
187 }
188 }
189 }
190
191
192 unresolved := make(map[string]bool)
193 var depDecls []ast.Decl
194 hasDepDecls := make(map[ast.Decl]bool)
195
196 var inspectFunc func(ast.Node) bool
197 inspectFunc = func(n ast.Node) bool {
198 switch e := n.(type) {
199 case *ast.Ident:
200 if e.Obj == nil && e.Name != "_" {
201 unresolved[e.Name] = true
202 } else if d := topDecls[e.Obj]; d != nil {
203 if !hasDepDecls[d] {
204 hasDepDecls[d] = true
205 depDecls = append(depDecls, d)
206 }
207 }
208 return true
209 case *ast.SelectorExpr:
210
211
212
213 ast.Inspect(e.X, inspectFunc)
214 return false
215 case *ast.KeyValueExpr:
216
217
218
219 ast.Inspect(e.Value, inspectFunc)
220 return false
221 }
222 return true
223 }
224 ast.Inspect(body, inspectFunc)
225 for i := 0; i < len(depDecls); i++ {
226 switch d := depDecls[i].(type) {
227 case *ast.FuncDecl:
228
229 if d.Type.Params != nil {
230 for _, p := range d.Type.Params.List {
231 ast.Inspect(p.Type, inspectFunc)
232 }
233 }
234 if d.Type.Results != nil {
235 for _, r := range d.Type.Results.List {
236 ast.Inspect(r.Type, inspectFunc)
237 }
238 }
239
240
241 if d.Body != nil {
242 ast.Inspect(d.Body, inspectFunc)
243 }
244 case *ast.GenDecl:
245 for _, spec := range d.Specs {
246 switch s := spec.(type) {
247 case *ast.TypeSpec:
248 ast.Inspect(s.Type, inspectFunc)
249
250 depDecls = append(depDecls, typMethods[s.Name.Name]...)
251 case *ast.ValueSpec:
252 if s.Type != nil {
253 ast.Inspect(s.Type, inspectFunc)
254 }
255 for _, val := range s.Values {
256 ast.Inspect(val, inspectFunc)
257 }
258 }
259 }
260 }
261 }
262
263
264 for n := range unresolved {
265 if predeclaredTypes[n] || predeclaredConstants[n] || predeclaredFuncs[n] {
266 delete(unresolved, n)
267 }
268 }
269
270
271
272
273 namedImports := make(map[string]string)
274 var blankImports []ast.Spec
275 for _, s := range file.Imports {
276 p, err := strconv.Unquote(s.Path.Value)
277 if err != nil {
278 continue
279 }
280 if p == "syscall/js" {
281
282
283 return nil
284 }
285 n := path.Base(p)
286 if s.Name != nil {
287 n = s.Name.Name
288 switch n {
289 case "_":
290 blankImports = append(blankImports, s)
291 continue
292 case ".":
293
294 return nil
295 }
296 }
297 if unresolved[n] {
298 namedImports[n] = p
299 delete(unresolved, n)
300 }
301 }
302
303
304
305 if len(unresolved) > 0 {
306 return nil
307 }
308
309
310 var comments []*ast.CommentGroup
311 for _, s := range blankImports {
312 if c := s.(*ast.ImportSpec).Doc; c != nil {
313 comments = append(comments, c)
314 }
315 }
316
317
318 for _, c := range file.Comments {
319 if body.Pos() <= c.Pos() && c.End() <= body.End() {
320 comments = append(comments, c)
321 }
322 }
323
324
325
326 body, comments = stripOutputComment(body, comments)
327
328
329 for _, d := range depDecls {
330 switch d := d.(type) {
331 case *ast.GenDecl:
332 if d.Doc != nil {
333 comments = append(comments, d.Doc)
334 }
335 case *ast.FuncDecl:
336 if d.Doc != nil {
337 comments = append(comments, d.Doc)
338 }
339 }
340 }
341
342
343 importDecl := &ast.GenDecl{
344 Tok: token.IMPORT,
345 Lparen: 1,
346 Rparen: 1,
347 }
348 for n, p := range namedImports {
349 s := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote(p)}}
350 if path.Base(p) != n {
351 s.Name = ast.NewIdent(n)
352 }
353 importDecl.Specs = append(importDecl.Specs, s)
354 }
355 importDecl.Specs = append(importDecl.Specs, blankImports...)
356
357
358 funcDecl := &ast.FuncDecl{
359 Name: ast.NewIdent("main"),
360 Type: f.Type,
361 Body: body,
362 }
363
364 decls := make([]ast.Decl, 0, 2+len(depDecls))
365 decls = append(decls, importDecl)
366 decls = append(decls, depDecls...)
367 decls = append(decls, funcDecl)
368
369 sort.Slice(decls, func(i, j int) bool {
370 return decls[i].Pos() < decls[j].Pos()
371 })
372
373 sort.Slice(comments, func(i, j int) bool {
374 return comments[i].Pos() < comments[j].Pos()
375 })
376
377
378 return &ast.File{
379 Name: ast.NewIdent("main"),
380 Decls: decls,
381 Comments: comments,
382 }
383 }
384
385
386
387 func playExampleFile(file *ast.File) *ast.File {
388
389 comments := file.Comments
390 if len(comments) > 0 && strings.HasPrefix(comments[0].Text(), "Copyright") {
391 comments = comments[1:]
392 }
393
394
395 var decls []ast.Decl
396 for _, d := range file.Decls {
397 if f, ok := d.(*ast.FuncDecl); ok && isTest(f.Name.Name, "Example") {
398
399 newF := *f
400 newF.Name = ast.NewIdent("main")
401 newF.Body, comments = stripOutputComment(f.Body, comments)
402 d = &newF
403 }
404 decls = append(decls, d)
405 }
406
407
408 f := *file
409 f.Name = ast.NewIdent("main")
410 f.Decls = decls
411 f.Comments = comments
412 return &f
413 }
414
415
416
417 func stripOutputComment(body *ast.BlockStmt, comments []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) {
418
419 i, last := lastComment(body, comments)
420 if last == nil || !outputPrefix.MatchString(last.Text()) {
421 return body, comments
422 }
423
424
425 newBody := &ast.BlockStmt{
426 Lbrace: body.Lbrace,
427 List: body.List,
428 Rbrace: last.Pos(),
429 }
430 newComments := make([]*ast.CommentGroup, len(comments)-1)
431 copy(newComments, comments[:i])
432 copy(newComments[i:], comments[i+1:])
433 return newBody, newComments
434 }
435
436
437 func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.CommentGroup) {
438 if b == nil {
439 return
440 }
441 pos, end := b.Pos(), b.End()
442 for j, cg := range c {
443 if cg.Pos() < pos {
444 continue
445 }
446 if cg.End() > end {
447 break
448 }
449 i, last = j, cg
450 }
451 return
452 }
453
454
455
456
457
458
459
460
461
462
463
464
465
466 func classifyExamples(p *Package, examples []*Example) {
467 if len(examples) == 0 {
468 return
469 }
470
471
472 ids := make(map[string]*[]*Example)
473 ids[""] = &p.Examples
474 for _, f := range p.Funcs {
475 if !token.IsExported(f.Name) {
476 continue
477 }
478 ids[f.Name] = &f.Examples
479 }
480 for _, t := range p.Types {
481 if !token.IsExported(t.Name) {
482 continue
483 }
484 ids[t.Name] = &t.Examples
485 for _, f := range t.Funcs {
486 if !token.IsExported(f.Name) {
487 continue
488 }
489 ids[f.Name] = &f.Examples
490 }
491 for _, m := range t.Methods {
492 if !token.IsExported(m.Name) {
493 continue
494 }
495 ids[strings.TrimPrefix(m.Recv, "*")+"_"+m.Name] = &m.Examples
496 }
497 }
498
499
500 for _, ex := range examples {
501
502
503
504
505
506
507 for i := len(ex.Name); i >= 0; i = strings.LastIndexByte(ex.Name[:i], '_') {
508 prefix, suffix, ok := splitExampleName(ex.Name, i)
509 if !ok {
510 continue
511 }
512 exs, ok := ids[prefix]
513 if !ok {
514 continue
515 }
516 ex.Suffix = suffix
517 *exs = append(*exs, ex)
518 break
519 }
520 }
521
522
523 for _, exs := range ids {
524 sort.Slice((*exs), func(i, j int) bool {
525 return (*exs)[i].Suffix < (*exs)[j].Suffix
526 })
527 }
528 }
529
530
531
532
533
534
535
536 func splitExampleName(s string, i int) (prefix, suffix string, ok bool) {
537 if i == len(s) {
538 return s, "", true
539 }
540 if i == len(s)-1 {
541 return "", "", false
542 }
543 prefix, suffix = s[:i], s[i+1:]
544 return prefix, suffix, isExampleSuffix(suffix)
545 }
546
547 func isExampleSuffix(s string) bool {
548 r, size := utf8.DecodeRuneInString(s)
549 return size > 0 && unicode.IsLower(r)
550 }
551
View as plain text