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 "slices"
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 results := f.Type.Results; results != nil && len(results.List) != 0 {
78 continue
79 }
80 if f.Body == nil {
81 continue
82 }
83 var doc string
84 if f.Doc != nil {
85 doc = f.Doc.Text()
86 }
87 output, unordered, hasOutput := exampleOutput(f.Body, file.Comments)
88 flist = append(flist, &Example{
89 Name: name[len("Example"):],
90 Doc: doc,
91 Code: f.Body,
92 Play: playExample(file, f),
93 Comments: file.Comments,
94 Output: output,
95 Unordered: unordered,
96 EmptyOutput: output == "" && hasOutput,
97 Order: len(flist),
98 })
99 }
100 if !hasTests && numDecl > 1 && len(flist) == 1 {
101
102
103
104 flist[0].Code = file
105 flist[0].Play = playExampleFile(file)
106 }
107 list = append(list, flist...)
108 }
109
110 slices.SortFunc(list, func(a, b *Example) int {
111 return cmp.Compare(a.Name, b.Name)
112 })
113 return list
114 }
115
116 var outputPrefix = lazyregexp.New(`(?i)^[[:space:]]*(unordered )?output:`)
117
118
119 func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) (output string, unordered, ok bool) {
120 if _, last := lastComment(b, comments); last != nil {
121
122 text := last.Text()
123 if loc := outputPrefix.FindStringSubmatchIndex(text); loc != nil {
124 if loc[2] != -1 {
125 unordered = true
126 }
127 text = text[loc[1]:]
128
129 text = strings.TrimLeft(text, " ")
130 if len(text) > 0 && text[0] == '\n' {
131 text = text[1:]
132 }
133 return text, unordered, true
134 }
135 }
136 return "", false, false
137 }
138
139
140
141
142 func isTest(name, prefix string) bool {
143 if !strings.HasPrefix(name, prefix) {
144 return false
145 }
146 if len(name) == len(prefix) {
147 return true
148 }
149 rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
150 return !unicode.IsLower(rune)
151 }
152
153
154
155 func playExample(file *ast.File, f *ast.FuncDecl) *ast.File {
156 body := f.Body
157
158 if !strings.HasSuffix(file.Name.Name, "_test") {
159
160
161 return nil
162 }
163
164
165 topDecls := make(map[*ast.Object]ast.Decl)
166 typMethods := make(map[string][]ast.Decl)
167
168 for _, decl := range file.Decls {
169 switch d := decl.(type) {
170 case *ast.FuncDecl:
171 if d.Recv == nil {
172 topDecls[d.Name.Obj] = d
173 } else {
174 if len(d.Recv.List) == 1 {
175 t := d.Recv.List[0].Type
176 tname, _ := baseTypeName(t)
177 typMethods[tname] = append(typMethods[tname], d)
178 }
179 }
180 case *ast.GenDecl:
181 for _, spec := range d.Specs {
182 switch s := spec.(type) {
183 case *ast.TypeSpec:
184 topDecls[s.Name.Obj] = d
185 case *ast.ValueSpec:
186 for _, name := range s.Names {
187 topDecls[name.Obj] = d
188 }
189 }
190 }
191 }
192 }
193
194
195 depDecls, unresolved := findDeclsAndUnresolved(body, topDecls, typMethods)
196
197
198
199
200 var namedImports []ast.Spec
201 var blankImports []ast.Spec
202
203
204
205
206 groupStarts := findImportGroupStarts(file.Imports)
207 groupStart := func(s *ast.ImportSpec) token.Pos {
208 for i, start := range groupStarts {
209 if s.Path.ValuePos < start {
210 return groupStarts[i-1]
211 }
212 }
213 return groupStarts[len(groupStarts)-1]
214 }
215
216 for _, s := range file.Imports {
217 p, err := strconv.Unquote(s.Path.Value)
218 if err != nil {
219 continue
220 }
221 if p == "syscall/js" {
222
223
224 return nil
225 }
226 n := assumedPackageName(p)
227 if s.Name != nil {
228 n = s.Name.Name
229 switch n {
230 case "_":
231 blankImports = append(blankImports, s)
232 continue
233 case ".":
234
235 return nil
236 }
237 }
238 if unresolved[n] {
239
240 spec := *s
241 path := *s.Path
242 spec.Path = &path
243 updateBasicLitPos(spec.Path, groupStart(&spec))
244 namedImports = append(namedImports, &spec)
245 delete(unresolved, n)
246 }
247 }
248
249
250 for n := range unresolved {
251 if predeclaredTypes[n] || predeclaredConstants[n] || predeclaredFuncs[n] {
252 delete(unresolved, n)
253 }
254 }
255
256
257
258 if len(unresolved) > 0 {
259 return nil
260 }
261
262
263 var comments []*ast.CommentGroup
264 for _, s := range blankImports {
265 if c := s.(*ast.ImportSpec).Doc; c != nil {
266 comments = append(comments, c)
267 }
268 }
269
270
271 for _, c := range file.Comments {
272 if body.Pos() <= c.Pos() && c.End() <= body.End() {
273 comments = append(comments, c)
274 }
275 }
276
277
278
279 body, comments = stripOutputComment(body, comments)
280
281
282 for _, d := range depDecls {
283 switch d := d.(type) {
284 case *ast.GenDecl:
285 if d.Doc != nil {
286 comments = append(comments, d.Doc)
287 }
288 case *ast.FuncDecl:
289 if d.Doc != nil {
290 comments = append(comments, d.Doc)
291 }
292 }
293 }
294
295
296 importDecl := &ast.GenDecl{
297 Tok: token.IMPORT,
298 Lparen: 1,
299 Rparen: 1,
300 }
301 importDecl.Specs = append(namedImports, blankImports...)
302
303
304 funcDecl := &ast.FuncDecl{
305 Name: ast.NewIdent("main"),
306 Type: f.Type,
307 Body: body,
308 }
309
310 decls := make([]ast.Decl, 0, 2+len(depDecls))
311 decls = append(decls, importDecl)
312 decls = append(decls, depDecls...)
313 decls = append(decls, funcDecl)
314
315 slices.SortFunc(decls, func(a, b ast.Decl) int {
316 return cmp.Compare(a.Pos(), b.Pos())
317 })
318 slices.SortFunc(comments, func(a, b *ast.CommentGroup) int {
319 return cmp.Compare(a.Pos(), b.Pos())
320 })
321
322
323 return &ast.File{
324 Name: ast.NewIdent("main"),
325 Decls: decls,
326 Comments: comments,
327 }
328 }
329
330
331
332
333
334
335
336
337 func findDeclsAndUnresolved(body ast.Node, topDecls map[*ast.Object]ast.Decl, typMethods map[string][]ast.Decl) ([]ast.Decl, map[string]bool) {
338
339
340
341
342
343 unresolved := make(map[string]bool)
344 var depDecls []ast.Decl
345 usedDecls := make(map[ast.Decl]bool)
346 usedObjs := make(map[*ast.Object]bool)
347
348 var inspectFunc func(ast.Node) bool
349 inspectFunc = func(n ast.Node) bool {
350 switch e := n.(type) {
351 case *ast.Ident:
352 if e.Obj == nil && e.Name != "_" {
353 unresolved[e.Name] = true
354 } else if d := topDecls[e.Obj]; d != nil {
355
356 usedObjs[e.Obj] = true
357 if !usedDecls[d] {
358 usedDecls[d] = true
359 depDecls = append(depDecls, d)
360 }
361 }
362 return true
363 case *ast.SelectorExpr:
364
365
366
367 ast.Inspect(e.X, inspectFunc)
368 return false
369 case *ast.KeyValueExpr:
370
371
372
373 ast.Inspect(e.Value, inspectFunc)
374 return false
375 }
376 return true
377 }
378
379 inspectFieldList := func(fl *ast.FieldList) {
380 if fl != nil {
381 for _, f := range fl.List {
382 ast.Inspect(f.Type, inspectFunc)
383 }
384 }
385 }
386
387
388 ast.Inspect(body, inspectFunc)
389
390
391 for i := 0; i < len(depDecls); i++ {
392 switch d := depDecls[i].(type) {
393 case *ast.FuncDecl:
394
395 inspectFieldList(d.Type.TypeParams)
396
397 inspectFieldList(d.Type.Params)
398 inspectFieldList(d.Type.Results)
399
400
401 if d.Body != nil {
402 ast.Inspect(d.Body, inspectFunc)
403 }
404 case *ast.GenDecl:
405 for _, spec := range d.Specs {
406 switch s := spec.(type) {
407 case *ast.TypeSpec:
408 inspectFieldList(s.TypeParams)
409 ast.Inspect(s.Type, inspectFunc)
410 depDecls = append(depDecls, typMethods[s.Name.Name]...)
411 case *ast.ValueSpec:
412 if s.Type != nil {
413 ast.Inspect(s.Type, inspectFunc)
414 }
415 for _, val := range s.Values {
416 ast.Inspect(val, inspectFunc)
417 }
418 }
419 }
420 }
421 }
422
423
424
425
426
427
428
429
430
431 var ds []ast.Decl
432 for _, d := range depDecls {
433 switch d := d.(type) {
434 case *ast.FuncDecl:
435 ds = append(ds, d)
436 case *ast.GenDecl:
437 containsIota := false
438
439 var specs []ast.Spec
440 for _, s := range d.Specs {
441 switch s := s.(type) {
442 case *ast.TypeSpec:
443 if usedObjs[s.Name.Obj] {
444 specs = append(specs, s)
445 }
446 case *ast.ValueSpec:
447 if !containsIota {
448 containsIota = hasIota(s)
449 }
450
451
452
453
454
455 if len(s.Names) > 1 && len(s.Values) == 1 {
456 specs = append(specs, s)
457 continue
458 }
459 ns := *s
460 ns.Names = nil
461 ns.Values = nil
462 for i, n := range s.Names {
463 if usedObjs[n.Obj] {
464 ns.Names = append(ns.Names, n)
465 if s.Values != nil {
466 ns.Values = append(ns.Values, s.Values[i])
467 }
468 }
469 }
470 if len(ns.Names) > 0 {
471 specs = append(specs, &ns)
472 }
473 }
474 }
475 if len(specs) > 0 {
476
477 if d.Tok == token.CONST && containsIota {
478 ds = append(ds, d)
479 } else {
480
481 nd := *d
482 nd.Specs = specs
483 if len(specs) == 1 {
484
485 nd.Lparen = 0
486 }
487 ds = append(ds, &nd)
488 }
489 }
490 }
491 }
492 return ds, unresolved
493 }
494
495 func hasIota(s ast.Spec) bool {
496 for n := range ast.Preorder(s) {
497
498
499 if id, ok := n.(*ast.Ident); ok && id.Name == "iota" && id.Obj == nil {
500 return true
501 }
502 }
503 return false
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 slices.SortFunc(imps, func(a, b *ast.ImportSpec) int {
524 return cmp.Compare(a.Pos(), b.Pos())
525 })
526
527
528 var groupStarts []*ast.ImportSpec
529 prevEnd := token.Pos(-2)
530 for _, imp := range imps {
531 if imp.Pos()-prevEnd > 2 {
532 groupStarts = append(groupStarts, imp)
533 }
534 prevEnd = imp.End()
535
536 if imp.Comment != nil {
537 prevEnd = imp.Comment.End()
538 }
539 }
540 return groupStarts
541 }
542
543
544
545 func playExampleFile(file *ast.File) *ast.File {
546
547 comments := file.Comments
548 if len(comments) > 0 && strings.HasPrefix(comments[0].Text(), "Copyright") {
549 comments = comments[1:]
550 }
551
552
553 var decls []ast.Decl
554 for _, d := range file.Decls {
555 if f, ok := d.(*ast.FuncDecl); ok && isTest(f.Name.Name, "Example") {
556
557 newF := *f
558 newF.Name = ast.NewIdent("main")
559 newF.Body, comments = stripOutputComment(f.Body, comments)
560 d = &newF
561 }
562 decls = append(decls, d)
563 }
564
565
566 f := *file
567 f.Name = ast.NewIdent("main")
568 f.Decls = decls
569 f.Comments = comments
570 return &f
571 }
572
573
574
575 func stripOutputComment(body *ast.BlockStmt, comments []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) {
576
577 i, last := lastComment(body, comments)
578 if last == nil || !outputPrefix.MatchString(last.Text()) {
579 return body, comments
580 }
581
582
583 newBody := &ast.BlockStmt{
584 Lbrace: body.Lbrace,
585 List: body.List,
586 Rbrace: last.Pos(),
587 }
588 newComments := make([]*ast.CommentGroup, len(comments)-1)
589 copy(newComments, comments[:i])
590 copy(newComments[i:], comments[i+1:])
591 return newBody, newComments
592 }
593
594
595 func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.CommentGroup) {
596 if b == nil {
597 return
598 }
599 pos, end := b.Pos(), b.End()
600 for j, cg := range c {
601 if cg.Pos() < pos {
602 continue
603 }
604 if cg.End() > end {
605 break
606 }
607 i, last = j, cg
608 }
609 return
610 }
611
612
613
614
615
616
617
618
619
620
621
622
623 func classifyExamples(p *Package, examples []*Example) {
624 if len(examples) == 0 {
625 return
626 }
627
628 ids := make(map[string]*[]*Example)
629 ids[""] = &p.Examples
630 for _, f := range p.Funcs {
631 if !token.IsExported(f.Name) {
632 continue
633 }
634 ids[f.Name] = &f.Examples
635 }
636 for _, t := range p.Types {
637 if !token.IsExported(t.Name) {
638 continue
639 }
640 ids[t.Name] = &t.Examples
641 for _, f := range t.Funcs {
642 if !token.IsExported(f.Name) {
643 continue
644 }
645 ids[f.Name] = &f.Examples
646 }
647 for _, m := range t.Methods {
648 if !token.IsExported(m.Name) {
649 continue
650 }
651 ids[strings.TrimPrefix(nameWithoutInst(m.Recv), "*")+"_"+m.Name] = &m.Examples
652 }
653 }
654
655
656 for _, ex := range examples {
657
658
659
660
661
662
663 for i := len(ex.Name); i >= 0; i = strings.LastIndexByte(ex.Name[:i], '_') {
664 prefix, suffix, ok := splitExampleName(ex.Name, i)
665 if !ok {
666 continue
667 }
668 exs, ok := ids[prefix]
669 if !ok {
670 continue
671 }
672 ex.Suffix = suffix
673 *exs = append(*exs, ex)
674 break
675 }
676 }
677
678
679 for _, exs := range ids {
680 slices.SortFunc(*exs, func(a, b *Example) int {
681 return cmp.Compare(a.Suffix, b.Suffix)
682 })
683 }
684 }
685
686
687
688
689
690
691 func nameWithoutInst(name string) string {
692 start := strings.Index(name, "[")
693 if start < 0 {
694 return name
695 }
696 end := strings.LastIndex(name, "]")
697 if end < 0 {
698
699 return name
700 }
701 return name[0:start] + name[end+1:]
702 }
703
704
705
706
707
708
709
710 func splitExampleName(s string, i int) (prefix, suffix string, ok bool) {
711 if i == len(s) {
712 return s, "", true
713 }
714 if i == len(s)-1 {
715 return "", "", false
716 }
717 prefix, suffix = s[:i], s[i+1:]
718 return prefix, suffix, isExampleSuffix(suffix)
719 }
720
721 func isExampleSuffix(s string) bool {
722 r, size := utf8.DecodeRuneInString(s)
723 return size > 0 && unicode.IsLower(r)
724 }
725
726
727
728
729 func updateBasicLitPos(lit *ast.BasicLit, pos token.Pos) {
730 len := lit.End() - lit.Pos()
731 lit.ValuePos = pos
732 if lit.ValueEnd.IsValid() {
733 lit.ValueEnd = pos + len
734 }
735 }
736
View as plain text