Source file
src/go/doc/example.go
Documentation: go/doc
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 depDecls, unresolved := findDeclsAndUnresolved(body, topDecls, typMethods)
193
194
195 for n := range unresolved {
196 if predeclaredTypes[n] || predeclaredConstants[n] || predeclaredFuncs[n] {
197 delete(unresolved, n)
198 }
199 }
200
201
202
203
204 var namedImports []ast.Spec
205 var blankImports []ast.Spec
206
207
208
209
210 groupStarts := findImportGroupStarts(file.Imports)
211 groupStart := func(s *ast.ImportSpec) token.Pos {
212 for i, start := range groupStarts {
213 if s.Path.ValuePos < start {
214 return groupStarts[i-1]
215 }
216 }
217 return groupStarts[len(groupStarts)-1]
218 }
219
220 for _, s := range file.Imports {
221 p, err := strconv.Unquote(s.Path.Value)
222 if err != nil {
223 continue
224 }
225 if p == "syscall/js" {
226
227
228 return nil
229 }
230 n := path.Base(p)
231 if s.Name != nil {
232 n = s.Name.Name
233 switch n {
234 case "_":
235 blankImports = append(blankImports, s)
236 continue
237 case ".":
238
239 return nil
240 }
241 }
242 if unresolved[n] {
243
244 spec := *s
245 path := *s.Path
246 spec.Path = &path
247 spec.Path.ValuePos = groupStart(&spec)
248 namedImports = append(namedImports, &spec)
249 delete(unresolved, n)
250 }
251 }
252
253
254
255 if len(unresolved) > 0 {
256 return nil
257 }
258
259
260 var comments []*ast.CommentGroup
261 for _, s := range blankImports {
262 if c := s.(*ast.ImportSpec).Doc; c != nil {
263 comments = append(comments, c)
264 }
265 }
266
267
268 for _, c := range file.Comments {
269 if body.Pos() <= c.Pos() && c.End() <= body.End() {
270 comments = append(comments, c)
271 }
272 }
273
274
275
276 body, comments = stripOutputComment(body, comments)
277
278
279 for _, d := range depDecls {
280 switch d := d.(type) {
281 case *ast.GenDecl:
282 if d.Doc != nil {
283 comments = append(comments, d.Doc)
284 }
285 case *ast.FuncDecl:
286 if d.Doc != nil {
287 comments = append(comments, d.Doc)
288 }
289 }
290 }
291
292
293 importDecl := &ast.GenDecl{
294 Tok: token.IMPORT,
295 Lparen: 1,
296 Rparen: 1,
297 }
298 importDecl.Specs = append(namedImports, blankImports...)
299
300
301 funcDecl := &ast.FuncDecl{
302 Name: ast.NewIdent("main"),
303 Type: f.Type,
304 Body: body,
305 }
306
307 decls := make([]ast.Decl, 0, 2+len(depDecls))
308 decls = append(decls, importDecl)
309 decls = append(decls, depDecls...)
310 decls = append(decls, funcDecl)
311
312 sort.Slice(decls, func(i, j int) bool {
313 return decls[i].Pos() < decls[j].Pos()
314 })
315 sort.Slice(comments, func(i, j int) bool {
316 return comments[i].Pos() < comments[j].Pos()
317 })
318
319
320 return &ast.File{
321 Name: ast.NewIdent("main"),
322 Decls: decls,
323 Comments: comments,
324 }
325 }
326
327
328
329
330
331
332
333
334 func findDeclsAndUnresolved(body ast.Node, topDecls map[*ast.Object]ast.Decl, typMethods map[string][]ast.Decl) ([]ast.Decl, map[string]bool) {
335
336
337
338
339
340 unresolved := make(map[string]bool)
341 var depDecls []ast.Decl
342 usedDecls := make(map[ast.Decl]bool)
343 usedObjs := make(map[*ast.Object]bool)
344
345 var inspectFunc func(ast.Node) bool
346 inspectFunc = func(n ast.Node) bool {
347 switch e := n.(type) {
348 case *ast.Ident:
349 if e.Obj == nil && e.Name != "_" {
350 unresolved[e.Name] = true
351 } else if d := topDecls[e.Obj]; d != nil {
352
353 usedObjs[e.Obj] = true
354 if !usedDecls[d] {
355 usedDecls[d] = true
356 depDecls = append(depDecls, d)
357 }
358 }
359 return true
360 case *ast.SelectorExpr:
361
362
363
364 ast.Inspect(e.X, inspectFunc)
365 return false
366 case *ast.KeyValueExpr:
367
368
369
370 ast.Inspect(e.Value, inspectFunc)
371 return false
372 }
373 return true
374 }
375
376 inspectFieldList := func(fl *ast.FieldList) {
377 if fl != nil {
378 for _, f := range fl.List {
379 ast.Inspect(f.Type, inspectFunc)
380 }
381 }
382 }
383
384
385 ast.Inspect(body, inspectFunc)
386
387
388 for i := 0; i < len(depDecls); i++ {
389 switch d := depDecls[i].(type) {
390 case *ast.FuncDecl:
391
392 inspectFieldList(d.Type.TypeParams)
393
394 inspectFieldList(d.Type.Params)
395 inspectFieldList(d.Type.Results)
396
397
398 if d.Body != nil {
399 ast.Inspect(d.Body, inspectFunc)
400 }
401 case *ast.GenDecl:
402 for _, spec := range d.Specs {
403 switch s := spec.(type) {
404 case *ast.TypeSpec:
405 inspectFieldList(s.TypeParams)
406 ast.Inspect(s.Type, inspectFunc)
407 depDecls = append(depDecls, typMethods[s.Name.Name]...)
408 case *ast.ValueSpec:
409 if s.Type != nil {
410 ast.Inspect(s.Type, inspectFunc)
411 }
412 for _, val := range s.Values {
413 ast.Inspect(val, inspectFunc)
414 }
415 }
416 }
417 }
418 }
419
420
421
422
423
424
425
426
427
428 var ds []ast.Decl
429 for _, d := range depDecls {
430 switch d := d.(type) {
431 case *ast.FuncDecl:
432 ds = append(ds, d)
433 case *ast.GenDecl:
434 containsIota := false
435
436 var specs []ast.Spec
437 for _, s := range d.Specs {
438 switch s := s.(type) {
439 case *ast.TypeSpec:
440 if usedObjs[s.Name.Obj] {
441 specs = append(specs, s)
442 }
443 case *ast.ValueSpec:
444 if !containsIota {
445 containsIota = hasIota(s)
446 }
447
448
449
450
451
452 if len(s.Names) > 1 && len(s.Values) == 1 {
453 specs = append(specs, s)
454 continue
455 }
456 ns := *s
457 ns.Names = nil
458 ns.Values = nil
459 for i, n := range s.Names {
460 if usedObjs[n.Obj] {
461 ns.Names = append(ns.Names, n)
462 if s.Values != nil {
463 ns.Values = append(ns.Values, s.Values[i])
464 }
465 }
466 }
467 if len(ns.Names) > 0 {
468 specs = append(specs, &ns)
469 }
470 }
471 }
472 if len(specs) > 0 {
473
474 if d.Tok == token.CONST && containsIota {
475 ds = append(ds, d)
476 } else {
477
478 nd := *d
479 nd.Specs = specs
480 if len(specs) == 1 {
481
482 nd.Lparen = 0
483 }
484 ds = append(ds, &nd)
485 }
486 }
487 }
488 }
489 return ds, unresolved
490 }
491
492 func hasIota(s ast.Spec) bool {
493 has := false
494 ast.Inspect(s, func(n ast.Node) bool {
495
496
497 if id, ok := n.(*ast.Ident); ok && id.Name == "iota" && id.Obj == nil {
498 has = true
499 return false
500 }
501 return true
502 })
503 return has
504 }
505
506
507
508 func findImportGroupStarts(imps []*ast.ImportSpec) []token.Pos {
509 startImps := findImportGroupStarts1(imps)
510 groupStarts := make([]token.Pos, len(startImps))
511 for i, imp := range startImps {
512 groupStarts[i] = imp.Pos()
513 }
514 return groupStarts
515 }
516
517
518 func findImportGroupStarts1(origImps []*ast.ImportSpec) []*ast.ImportSpec {
519
520 imps := make([]*ast.ImportSpec, len(origImps))
521 copy(imps, origImps)
522
523 sort.Slice(imps, func(i, j int) bool { return imps[i].Pos() < imps[j].Pos() })
524
525
526 var groupStarts []*ast.ImportSpec
527 prevEnd := token.Pos(-2)
528 for _, imp := range imps {
529 if imp.Pos()-prevEnd > 2 {
530 groupStarts = append(groupStarts, imp)
531 }
532 prevEnd = imp.End()
533
534 if imp.Comment != nil {
535 prevEnd = imp.Comment.End()
536 }
537 }
538 return groupStarts
539 }
540
541
542
543 func playExampleFile(file *ast.File) *ast.File {
544
545 comments := file.Comments
546 if len(comments) > 0 && strings.HasPrefix(comments[0].Text(), "Copyright") {
547 comments = comments[1:]
548 }
549
550
551 var decls []ast.Decl
552 for _, d := range file.Decls {
553 if f, ok := d.(*ast.FuncDecl); ok && isTest(f.Name.Name, "Example") {
554
555 newF := *f
556 newF.Name = ast.NewIdent("main")
557 newF.Body, comments = stripOutputComment(f.Body, comments)
558 d = &newF
559 }
560 decls = append(decls, d)
561 }
562
563
564 f := *file
565 f.Name = ast.NewIdent("main")
566 f.Decls = decls
567 f.Comments = comments
568 return &f
569 }
570
571
572
573 func stripOutputComment(body *ast.BlockStmt, comments []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) {
574
575 i, last := lastComment(body, comments)
576 if last == nil || !outputPrefix.MatchString(last.Text()) {
577 return body, comments
578 }
579
580
581 newBody := &ast.BlockStmt{
582 Lbrace: body.Lbrace,
583 List: body.List,
584 Rbrace: last.Pos(),
585 }
586 newComments := make([]*ast.CommentGroup, len(comments)-1)
587 copy(newComments, comments[:i])
588 copy(newComments[i:], comments[i+1:])
589 return newBody, newComments
590 }
591
592
593 func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.CommentGroup) {
594 if b == nil {
595 return
596 }
597 pos, end := b.Pos(), b.End()
598 for j, cg := range c {
599 if cg.Pos() < pos {
600 continue
601 }
602 if cg.End() > end {
603 break
604 }
605 i, last = j, cg
606 }
607 return
608 }
609
610
611
612
613
614
615
616
617
618
619
620
621 func classifyExamples(p *Package, examples []*Example) {
622 if len(examples) == 0 {
623 return
624 }
625
626 ids := make(map[string]*[]*Example)
627 ids[""] = &p.Examples
628 for _, f := range p.Funcs {
629 if !token.IsExported(f.Name) {
630 continue
631 }
632 ids[f.Name] = &f.Examples
633 }
634 for _, t := range p.Types {
635 if !token.IsExported(t.Name) {
636 continue
637 }
638 ids[t.Name] = &t.Examples
639 for _, f := range t.Funcs {
640 if !token.IsExported(f.Name) {
641 continue
642 }
643 ids[f.Name] = &f.Examples
644 }
645 for _, m := range t.Methods {
646 if !token.IsExported(m.Name) {
647 continue
648 }
649 ids[strings.TrimPrefix(nameWithoutInst(m.Recv), "*")+"_"+m.Name] = &m.Examples
650 }
651 }
652
653
654 for _, ex := range examples {
655
656
657
658
659
660
661 for i := len(ex.Name); i >= 0; i = strings.LastIndexByte(ex.Name[:i], '_') {
662 prefix, suffix, ok := splitExampleName(ex.Name, i)
663 if !ok {
664 continue
665 }
666 exs, ok := ids[prefix]
667 if !ok {
668 continue
669 }
670 ex.Suffix = suffix
671 *exs = append(*exs, ex)
672 break
673 }
674 }
675
676
677 for _, exs := range ids {
678 sort.Slice((*exs), func(i, j int) bool {
679 return (*exs)[i].Suffix < (*exs)[j].Suffix
680 })
681 }
682 }
683
684
685
686
687
688
689 func nameWithoutInst(name string) string {
690 start := strings.Index(name, "[")
691 if start < 0 {
692 return name
693 }
694 end := strings.LastIndex(name, "]")
695 if end < 0 {
696
697 return name
698 }
699 return name[0:start] + name[end+1:]
700 }
701
702
703
704
705
706
707
708 func splitExampleName(s string, i int) (prefix, suffix string, ok bool) {
709 if i == len(s) {
710 return s, "", true
711 }
712 if i == len(s)-1 {
713 return "", "", false
714 }
715 prefix, suffix = s[:i], s[i+1:]
716 return prefix, suffix, isExampleSuffix(suffix)
717 }
718
719 func isExampleSuffix(s string) bool {
720 r, size := utf8.DecodeRuneInString(s)
721 return size > 0 && unicode.IsLower(r)
722 }
723
View as plain text