godoc.go

v1.3.5
Doc Versions Source
1
package godoc
2
3
import (
4
	"go/ast"
5
	"go/doc"
6
	"go/doc/comment"
7
	"go/parser"
8
	"go/token"
9
	"path/filepath"
10
	"sort"
11
	"strings"
12
13
	"go.bigb.es/curator/internal/git"
14
)
15
16
// PackageDoc holds parsed documentation for a Go package.
17
type PackageDoc struct {
18
	ImportPath string
19
	Name       string
20
	Synopsis   string
21
	Doc        string // HTML-rendered doc comment
22
	Consts     []ValueDoc
23
	Vars       []ValueDoc
24
	Funcs      []FuncDoc
25
	Types      []TypeDoc
26
	Files      []string
27
28
	// Module-level info (populated by caller).
29
	Module      string
30
	Version     string
31
	Versions    []string
32
	SubPackages []SubPkgSummary
33
}
34
35
// ValueDoc represents a const or var group.
36
type ValueDoc struct {
37
	Names []string
38
	Doc   string // HTML
39
	Decl  string // source declaration
40
}
41
42
// FuncDoc represents a function.
43
type FuncDoc struct {
44
	Name       string
45
	Doc        string // HTML
46
	Decl       string // signature
47
	Recv       string // receiver type, empty for top-level functions
48
	ShortDecl  string // one-line signature for index
49
	SourceFile string // file containing the declaration
50
	SourceLine int    // line number
51
}
52
53
// TypeDoc represents a type with its methods and associated functions.
54
type TypeDoc struct {
55
	Name       string
56
	Doc        string // HTML
57
	Decl       string // type declaration source
58
	Consts     []ValueDoc
59
	Vars       []ValueDoc
60
	Funcs      []FuncDoc // associated constructors
61
	Methods    []FuncDoc
62
	SourceFile string // file containing the declaration
63
	SourceLine int    // line number
64
}
65
66
// SubPkgSummary describes a sub-package for the module overview.
67
type SubPkgSummary struct {
68
	RelPath    string // relative path from module root
69
	ImportPath string
70
	Synopsis   string
71
}
72
73
// ParsePackage reads Go source files from a git revision and produces documentation.
74
func ParsePackage(gitCache *git.Cache, repoPath, rev, pkgDir, importPath string) (*PackageDoc, error) {
75
	files, err := git.ReadDir(repoPath, rev, pkgDir)
76
	if err != nil {
77
		return nil, err
78
	}
79
80
	if len(files) == 0 {
81
		return nil, nil
82
	}
83
84
	fset := token.NewFileSet()
85
	var astFiles []*ast.File
86
	var fileNames []string
87
88
	for name, src := range files {
89
		fileNames = append(fileNames, name)
90
		f, err := parser.ParseFile(fset, name, src, parser.ParseComments)
91
		if err != nil {
92
			continue // skip files with parse errors
93
		}
94
		astFiles = append(astFiles, f)
95
	}
96
97
	if len(astFiles) == 0 {
98
		return nil, nil
99
	}
100
101
	sort.Strings(fileNames)
102
103
	pkg, err := doc.NewFromFiles(fset, astFiles, importPath)
104
	if err != nil {
105
		return nil, err
106
	}
107
108
	printer := pkg.Printer()
109
110
	result := &PackageDoc{
111
		ImportPath: importPath,
112
		Name:       pkg.Name,
113
		Synopsis:   doc.Synopsis(pkg.Doc),
114
		Doc:        renderDocHTML(printer, pkg.Parser().Parse(pkg.Doc)),
115
		Files:      fileNames,
116
	}
117
118
	// Constants.
119
	for _, v := range pkg.Consts {
120
		result.Consts = append(result.Consts, valueDoc(fset, printer, pkg.Parser(), v))
121
	}
122
123
	// Variables.
124
	for _, v := range pkg.Vars {
125
		result.Vars = append(result.Vars, valueDoc(fset, printer, pkg.Parser(), v))
126
	}
127
128
	// Functions.
129
	for _, f := range pkg.Funcs {
130
		result.Funcs = append(result.Funcs, funcDoc(fset, printer, pkg.Parser(), f))
131
	}
132
133
	// Types.
134
	for _, t := range pkg.Types {
135
		td := TypeDoc{
136
			Name: t.Name,
137
			Doc:  renderDocHTML(printer, pkg.Parser().Parse(t.Doc)),
138
			Decl: formatNode(fset, t.Decl),
139
		}
140
		if t.Decl != nil {
141
			pos := fset.Position(t.Decl.Pos())
142
			td.SourceFile = filepath.Base(pos.Filename)
143
			td.SourceLine = pos.Line
144
		}
145
		for _, v := range t.Consts {
146
			td.Consts = append(td.Consts, valueDoc(fset, printer, pkg.Parser(), v))
147
		}
148
		for _, v := range t.Vars {
149
			td.Vars = append(td.Vars, valueDoc(fset, printer, pkg.Parser(), v))
150
		}
151
		for _, f := range t.Funcs {
152
			td.Funcs = append(td.Funcs, funcDoc(fset, printer, pkg.Parser(), f))
153
		}
154
		for _, m := range t.Methods {
155
			td.Methods = append(td.Methods, funcDoc(fset, printer, pkg.Parser(), m))
156
		}
157
		result.Types = append(result.Types, td)
158
	}
159
160
	return result, nil
161
}
162
163
// ListSubPackages discovers sub-packages in a module.
164
func ListSubPackages(gitCache *git.Cache, repoPath, rev, modulePath string) ([]SubPkgSummary, error) {
165
	dirs, err := git.ListSubDirs(repoPath, rev, "")
166
	if err != nil {
167
		return nil, err
168
	}
169
170
	var subs []SubPkgSummary
171
	fset := token.NewFileSet()
172
173
	for _, dir := range dirs {
174
		files, err := git.ReadDir(repoPath, rev, dir)
175
		if err != nil {
176
			continue
177
		}
178
179
		// Parse just enough to get package name and synopsis.
180
		var synopsis string
181
		for _, src := range files {
182
			f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
183
			if err != nil {
184
				continue
185
			}
186
			if f.Doc != nil {
187
				synopsis = doc.Synopsis(f.Doc.Text())
188
				break
189
			}
190
		}
191
192
		subs = append(subs, SubPkgSummary{
193
			RelPath:    dir,
194
			ImportPath: modulePath + "/" + dir,
195
			Synopsis:   synopsis,
196
		})
197
	}
198
199
	return subs, nil
200
}
201
202
func valueDoc(fset *token.FileSet, printer *comment.Printer, docParser *comment.Parser, v *doc.Value) ValueDoc {
203
	return ValueDoc{
204
		Names: v.Names,
205
		Doc:   renderDocHTML(printer, docParser.Parse(v.Doc)),
206
		Decl:  formatNode(fset, v.Decl),
207
	}
208
}
209
210
func funcDoc(fset *token.FileSet, printer *comment.Printer, docParser *comment.Parser, f *doc.Func) FuncDoc {
211
	recv := ""
212
	if f.Recv != "" {
213
		recv = f.Recv
214
	}
215
	fd := FuncDoc{
216
		Name:      f.Name,
217
		Doc:       renderDocHTML(printer, docParser.Parse(f.Doc)),
218
		Decl:      formatNode(fset, f.Decl),
219
		Recv:      recv,
220
		ShortDecl: shortFuncDecl(f),
221
	}
222
	if f.Decl != nil {
223
		pos := fset.Position(f.Decl.Pos())
224
		fd.SourceFile = filepath.Base(pos.Filename)
225
		fd.SourceLine = pos.Line
226
	}
227
	return fd
228
}
229
230
func renderDocHTML(printer *comment.Printer, d *comment.Doc) string {
231
	raw := string(printer.HTML(d))
232
	// Post-process: highlight code blocks inside <pre> tags.
233
	return highlightDocPreBlocks(raw)
234
}
235
236
// highlightDocPreBlocks finds <pre>...</pre> blocks in doc HTML and
237
// applies Go syntax highlighting to their contents.
238
func highlightDocPreBlocks(html string) string {
239
	var b strings.Builder
240
	for {
241
		preStart := strings.Index(html, "<pre>")
242
		if preStart < 0 {
243
			b.WriteString(html)
244
			break
245
		}
246
		preEnd := strings.Index(html[preStart:], "</pre>")
247
		if preEnd < 0 {
248
			b.WriteString(html)
249
			break
250
		}
251
		preEnd += preStart
252
253
		// Write everything before <pre>.
254
		b.WriteString(html[:preStart])
255
256
		// Extract content between <pre> and </pre>.
257
		content := html[preStart+5 : preEnd]
258
259
		// Unescape HTML entities for the highlighter (the printer HTML-escapes content).
260
		content = strings.ReplaceAll(content, "&amp;", "&")
261
		content = strings.ReplaceAll(content, "&lt;", "<")
262
		content = strings.ReplaceAll(content, "&gt;", ">")
263
		content = strings.ReplaceAll(content, "&#34;", "\"")
264
		content = strings.ReplaceAll(content, "&quot;", "\"")
265
266
		highlighted := HighlightDecl(content)
267
268
		b.WriteString("<pre>")
269
		b.WriteString(highlighted)
270
		b.WriteString("</pre>")
271
272
		html = html[preEnd+6:] // skip past </pre>
273
	}
274
	return b.String()
275
}
276
277
func shortFuncDecl(f *doc.Func) string {
278
	s := "func "
279
	if f.Recv != "" {
280
		s += "(" + f.Recv + ") "
281
	}
282
	s += f.Name + formatFuncSignature(f)
283
	return s
284
}
285
286
func formatFuncSignature(f *doc.Func) string {
287
	if f.Decl == nil || f.Decl.Type == nil {
288
		return "()"
289
	}
290
291
	var b strings.Builder
292
	b.WriteString("(")
293
294
	if f.Decl.Type.Params != nil {
295
		for i, p := range f.Decl.Type.Params.List {
296
			if i > 0 {
297
				b.WriteString(", ")
298
			}
299
			for j, name := range p.Names {
300
				if j > 0 {
301
					b.WriteString(", ")
302
				}
303
				b.WriteString(name.Name)
304
			}
305
			if len(p.Names) > 0 {
306
				b.WriteString(" ")
307
			}
308
			b.WriteString(formatExpr(p.Type))
309
		}
310
	}
311
	b.WriteString(")")
312
313
	if f.Decl.Type.Results != nil && len(f.Decl.Type.Results.List) > 0 {
314
		results := f.Decl.Type.Results.List
315
		if len(results) == 1 && len(results[0].Names) == 0 {
316
			b.WriteString(" " + formatExpr(results[0].Type))
317
		} else {
318
			b.WriteString(" (")
319
			for i, r := range results {
320
				if i > 0 {
321
					b.WriteString(", ")
322
				}
323
				for j, name := range r.Names {
324
					if j > 0 {
325
						b.WriteString(", ")
326
					}
327
					b.WriteString(name.Name)
328
				}
329
				if len(r.Names) > 0 {
330
					b.WriteString(" ")
331
				}
332
				b.WriteString(formatExpr(r.Type))
333
			}
334
			b.WriteString(")")
335
		}
336
	}
337
338
	return b.String()
339
}
340

Source Files