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 "path"
13 "regexp"
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 {
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 var doc string
72 if f.Doc != nil {
73 doc = f.Doc.Text()
74 }
75 output, unordered, hasOutput := exampleOutput(f.Body, file.Comments)
76 flist = append(flist, &Example{
77 Name: name[len("Example"):],
78 Doc: doc,
79 Code: f.Body,
80 Play: playExample(file, f.Body),
81 Comments: file.Comments,
82 Output: output,
83 Unordered: unordered,
84 EmptyOutput: output == "" && hasOutput,
85 Order: len(flist),
86 })
87 }
88 if !hasTests && numDecl > 1 && len(flist) == 1 {
89
90
91
92 flist[0].Code = file
93 flist[0].Play = playExampleFile(file)
94 }
95 list = append(list, flist...)
96 }
97
98 sort.Slice(list, func(i, j int) bool {
99 return list[i].Name < list[j].Name
100 })
101 return list
102 }
103
104 var outputPrefix = regexp.MustCompile(`(?i)^[[:space:]]*(unordered )?output:`)
105
106
107 func exampleOutput(b *ast.BlockStmt, comments []*ast.CommentGroup) (output string, unordered, ok bool) {
108 if _, last := lastComment(b, comments); last != nil {
109
110 text := last.Text()
111 if loc := outputPrefix.FindStringSubmatchIndex(text); loc != nil {
112 if loc[2] != -1 {
113 unordered = true
114 }
115 text = text[loc[1]:]
116
117 text = strings.TrimLeft(text, " ")
118 if len(text) > 0 && text[0] == '\n' {
119 text = text[1:]
120 }
121 return text, unordered, true
122 }
123 }
124 return "", false, false
125 }
126
127
128
129
130 func isTest(name, prefix string) bool {
131 if !strings.HasPrefix(name, prefix) {
132 return false
133 }
134 if len(name) == len(prefix) {
135 return true
136 }
137 rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
138 return !unicode.IsLower(rune)
139 }
140
141
142
143 func playExample(file *ast.File, body *ast.BlockStmt) *ast.File {
144 if !strings.HasSuffix(file.Name.Name, "_test") {
145
146
147 return nil
148 }
149
150
151 topDecls := make(map[*ast.Object]bool)
152 for _, decl := range file.Decls {
153 switch d := decl.(type) {
154 case *ast.FuncDecl:
155 topDecls[d.Name.Obj] = true
156 case *ast.GenDecl:
157 for _, spec := range d.Specs {
158 switch s := spec.(type) {
159 case *ast.TypeSpec:
160 topDecls[s.Name.Obj] = true
161 case *ast.ValueSpec:
162 for _, id := range s.Names {
163 topDecls[id.Obj] = true
164 }
165 }
166 }
167 }
168 }
169
170
171 unresolved := make(map[string]bool)
172 usesTopDecl := false
173 var inspectFunc func(ast.Node) bool
174 inspectFunc = func(n ast.Node) bool {
175
176
177
178 if e, ok := n.(*ast.SelectorExpr); ok {
179 ast.Inspect(e.X, inspectFunc)
180 return false
181 }
182
183
184
185 if e, ok := n.(*ast.KeyValueExpr); ok {
186 ast.Inspect(e.Value, inspectFunc)
187 return false
188 }
189 if id, ok := n.(*ast.Ident); ok {
190 if id.Obj == nil {
191 unresolved[id.Name] = true
192 } else if topDecls[id.Obj] {
193 usesTopDecl = true
194 }
195 }
196 return true
197 }
198 ast.Inspect(body, inspectFunc)
199 if usesTopDecl {
200
201 return nil
202 }
203
204
205 for n := range unresolved {
206 if predeclaredTypes[n] || predeclaredConstants[n] || predeclaredFuncs[n] {
207 delete(unresolved, n)
208 }
209 }
210
211
212
213
214 namedImports := make(map[string]string)
215 var blankImports []ast.Spec
216 for _, s := range file.Imports {
217 p, err := strconv.Unquote(s.Path.Value)
218 if err != nil {
219 continue
220 }
221 n := path.Base(p)
222 if s.Name != nil {
223 n = s.Name.Name
224 switch n {
225 case "_":
226 blankImports = append(blankImports, s)
227 continue
228 case ".":
229
230 return nil
231 }
232 }
233 if unresolved[n] {
234 namedImports[n] = p
235 delete(unresolved, n)
236 }
237 }
238
239
240
241 if len(unresolved) > 0 {
242 return nil
243 }
244
245
246 var comments []*ast.CommentGroup
247 for _, s := range blankImports {
248 if c := s.(*ast.ImportSpec).Doc; c != nil {
249 comments = append(comments, c)
250 }
251 }
252
253
254 for _, c := range file.Comments {
255 if body.Pos() <= c.Pos() && c.End() <= body.End() {
256 comments = append(comments, c)
257 }
258 }
259
260
261
262 body, comments = stripOutputComment(body, comments)
263
264
265 importDecl := &ast.GenDecl{
266 Tok: token.IMPORT,
267 Lparen: 1,
268 Rparen: 1,
269 }
270 for n, p := range namedImports {
271 s := &ast.ImportSpec{Path: &ast.BasicLit{Value: strconv.Quote(p)}}
272 if path.Base(p) != n {
273 s.Name = ast.NewIdent(n)
274 }
275 importDecl.Specs = append(importDecl.Specs, s)
276 }
277 importDecl.Specs = append(importDecl.Specs, blankImports...)
278
279
280 funcDecl := &ast.FuncDecl{
281 Name: ast.NewIdent("main"),
282 Type: &ast.FuncType{Params: &ast.FieldList{}},
283 Body: body,
284 }
285
286
287 return &ast.File{
288 Name: ast.NewIdent("main"),
289 Decls: []ast.Decl{importDecl, funcDecl},
290 Comments: comments,
291 }
292 }
293
294
295
296 func playExampleFile(file *ast.File) *ast.File {
297
298 comments := file.Comments
299 if len(comments) > 0 && strings.HasPrefix(comments[0].Text(), "Copyright") {
300 comments = comments[1:]
301 }
302
303
304 var decls []ast.Decl
305 for _, d := range file.Decls {
306 if f, ok := d.(*ast.FuncDecl); ok && isTest(f.Name.Name, "Example") {
307
308 newF := *f
309 newF.Name = ast.NewIdent("main")
310 newF.Body, comments = stripOutputComment(f.Body, comments)
311 d = &newF
312 }
313 decls = append(decls, d)
314 }
315
316
317 f := *file
318 f.Name = ast.NewIdent("main")
319 f.Decls = decls
320 f.Comments = comments
321 return &f
322 }
323
324
325
326 func stripOutputComment(body *ast.BlockStmt, comments []*ast.CommentGroup) (*ast.BlockStmt, []*ast.CommentGroup) {
327
328 i, last := lastComment(body, comments)
329 if last == nil || !outputPrefix.MatchString(last.Text()) {
330 return body, comments
331 }
332
333
334 newBody := &ast.BlockStmt{
335 Lbrace: body.Lbrace,
336 List: body.List,
337 Rbrace: last.Pos(),
338 }
339 newComments := make([]*ast.CommentGroup, len(comments)-1)
340 copy(newComments, comments[:i])
341 copy(newComments[i:], comments[i+1:])
342 return newBody, newComments
343 }
344
345
346 func lastComment(b *ast.BlockStmt, c []*ast.CommentGroup) (i int, last *ast.CommentGroup) {
347 pos, end := b.Pos(), b.End()
348 for j, cg := range c {
349 if cg.Pos() < pos {
350 continue
351 }
352 if cg.End() > end {
353 break
354 }
355 i, last = j, cg
356 }
357 return
358 }
359
View as plain text