godoc.go

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

Source Files