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