godoc.go

v1.0.1
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
	"sort"
10
	"strings"
11
12
	"example.com/curator/internal/git"
13
)
14
15
// PackageDoc holds parsed documentation for a Go package.
16
type PackageDoc struct {
17
	ImportPath string
18
	Name       string
19
	Synopsis   string
20
	Doc        string // HTML-rendered doc comment
21
	Consts     []ValueDoc
22
	Vars       []ValueDoc
23
	Funcs      []FuncDoc
24
	Types      []TypeDoc
25
	Files      []string
26
27
	// Module-level info (populated by caller).
28
	Module      string
29
	Version     string
30
	Versions    []string
31
	SubPackages []SubPkgSummary
32
}
33
34
// ValueDoc represents a const or var group.
35
type ValueDoc struct {
36
	Names []string
37
	Doc   string // HTML
38
	Decl  string // source declaration
39
}
40
41
// FuncDoc represents a function.
42
type FuncDoc struct {
43
	Name      string
44
	Doc       string // HTML
45
	Decl      string // signature
46
	Recv      string // receiver type, empty for top-level functions
47
	ShortDecl string // one-line signature for index
48
}
49
50
// TypeDoc represents a type with its methods and associated functions.
51
type TypeDoc struct {
52
	Name    string
53
	Doc     string // HTML
54
	Decl    string // type declaration source
55
	Consts  []ValueDoc
56
	Vars    []ValueDoc
57
	Funcs   []FuncDoc // associated constructors
58
	Methods []FuncDoc
59
}
60
61
// SubPkgSummary describes a sub-package for the module overview.
62
type SubPkgSummary struct {
63
	RelPath    string // relative path from module root
64
	ImportPath string
65
	Synopsis   string
66
}
67
68
// ParsePackage reads Go source files from a git revision and produces documentation.
69
func ParsePackage(gitCache *git.Cache, repoPath, rev, pkgDir, importPath string) (*PackageDoc, error) {
70
	files, err := git.ReadDir(repoPath, rev, pkgDir)
71
	if err != nil {
72
		return nil, err
73
	}
74
75
	if len(files) == 0 {
76
		return nil, nil
77
	}
78
79
	fset := token.NewFileSet()
80
	var astFiles []*ast.File
81
	var fileNames []string
82
83
	for name, src := range files {
84
		fileNames = append(fileNames, name)
85
		f, err := parser.ParseFile(fset, name, src, parser.ParseComments)
86
		if err != nil {
87
			continue // skip files with parse errors
88
		}
89
		astFiles = append(astFiles, f)
90
	}
91
92
	if len(astFiles) == 0 {
93
		return nil, nil
94
	}
95
96
	sort.Strings(fileNames)
97
98
	pkg, err := doc.NewFromFiles(fset, astFiles, importPath)
99
	if err != nil {
100
		return nil, err
101
	}
102
103
	printer := pkg.Printer()
104
105
	result := &PackageDoc{
106
		ImportPath: importPath,
107
		Name:       pkg.Name,
108
		Synopsis:   doc.Synopsis(pkg.Doc),
109
		Doc:        renderDocHTML(printer, pkg.Parser().Parse(pkg.Doc)),
110
		Files:      fileNames,
111
	}
112
113
	// Constants.
114
	for _, v := range pkg.Consts {
115
		result.Consts = append(result.Consts, valueDoc(fset, printer, pkg.Parser(), v))
116
	}
117
118
	// Variables.
119
	for _, v := range pkg.Vars {
120
		result.Vars = append(result.Vars, valueDoc(fset, printer, pkg.Parser(), v))
121
	}
122
123
	// Functions.
124
	for _, f := range pkg.Funcs {
125
		result.Funcs = append(result.Funcs, funcDoc(fset, printer, pkg.Parser(), f))
126
	}
127
128
	// Types.
129
	for _, t := range pkg.Types {
130
		td := TypeDoc{
131
			Name: t.Name,
132
			Doc:  renderDocHTML(printer, pkg.Parser().Parse(t.Doc)),
133
			Decl: formatNode(fset, t.Decl),
134
		}
135
		for _, v := range t.Consts {
136
			td.Consts = append(td.Consts, valueDoc(fset, printer, pkg.Parser(), v))
137
		}
138
		for _, v := range t.Vars {
139
			td.Vars = append(td.Vars, valueDoc(fset, printer, pkg.Parser(), v))
140
		}
141
		for _, f := range t.Funcs {
142
			td.Funcs = append(td.Funcs, funcDoc(fset, printer, pkg.Parser(), f))
143
		}
144
		for _, m := range t.Methods {
145
			td.Methods = append(td.Methods, funcDoc(fset, printer, pkg.Parser(), m))
146
		}
147
		result.Types = append(result.Types, td)
148
	}
149
150
	return result, nil
151
}
152
153
// ListSubPackages discovers sub-packages in a module.
154
func ListSubPackages(gitCache *git.Cache, repoPath, rev, modulePath string) ([]SubPkgSummary, error) {
155
	dirs, err := git.ListSubDirs(repoPath, rev, "")
156
	if err != nil {
157
		return nil, err
158
	}
159
160
	var subs []SubPkgSummary
161
	fset := token.NewFileSet()
162
163
	for _, dir := range dirs {
164
		files, err := git.ReadDir(repoPath, rev, dir)
165
		if err != nil {
166
			continue
167
		}
168
169
		// Parse just enough to get package name and synopsis.
170
		var synopsis string
171
		for _, src := range files {
172
			f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
173
			if err != nil {
174
				continue
175
			}
176
			if f.Doc != nil {
177
				synopsis = doc.Synopsis(f.Doc.Text())
178
				break
179
			}
180
		}
181
182
		subs = append(subs, SubPkgSummary{
183
			RelPath:    dir,
184
			ImportPath: modulePath + "/" + dir,
185
			Synopsis:   synopsis,
186
		})
187
	}
188
189
	return subs, nil
190
}
191
192
func valueDoc(fset *token.FileSet, printer *comment.Printer, docParser *comment.Parser, v *doc.Value) ValueDoc {
193
	return ValueDoc{
194
		Names: v.Names,
195
		Doc:   renderDocHTML(printer, docParser.Parse(v.Doc)),
196
		Decl:  formatNode(fset, v.Decl),
197
	}
198
}
199
200
func funcDoc(fset *token.FileSet, printer *comment.Printer, docParser *comment.Parser, f *doc.Func) FuncDoc {
201
	recv := ""
202
	if f.Recv != "" {
203
		recv = f.Recv
204
	}
205
	return FuncDoc{
206
		Name:      f.Name,
207
		Doc:       renderDocHTML(printer, docParser.Parse(f.Doc)),
208
		Decl:      formatNode(fset, f.Decl),
209
		Recv:      recv,
210
		ShortDecl: shortFuncDecl(f),
211
	}
212
}
213
214
func renderDocHTML(printer *comment.Printer, d *comment.Doc) string {
215
	b := printer.HTML(d)
216
	return string(b)
217
}
218
219
func shortFuncDecl(f *doc.Func) string {
220
	s := "func "
221
	if f.Recv != "" {
222
		s += "(" + f.Recv + ") "
223
	}
224
	s += f.Name + formatFuncSignature(f)
225
	return s
226
}
227
228
func formatFuncSignature(f *doc.Func) string {
229
	if f.Decl == nil || f.Decl.Type == nil {
230
		return "()"
231
	}
232
233
	var b strings.Builder
234
	b.WriteString("(")
235
236
	if f.Decl.Type.Params != nil {
237
		for i, p := range f.Decl.Type.Params.List {
238
			if i > 0 {
239
				b.WriteString(", ")
240
			}
241
			for j, name := range p.Names {
242
				if j > 0 {
243
					b.WriteString(", ")
244
				}
245
				b.WriteString(name.Name)
246
			}
247
			if len(p.Names) > 0 {
248
				b.WriteString(" ")
249
			}
250
			b.WriteString(formatExpr(p.Type))
251
		}
252
	}
253
	b.WriteString(")")
254
255
	if f.Decl.Type.Results != nil && len(f.Decl.Type.Results.List) > 0 {
256
		results := f.Decl.Type.Results.List
257
		if len(results) == 1 && len(results[0].Names) == 0 {
258
			b.WriteString(" " + formatExpr(results[0].Type))
259
		} else {
260
			b.WriteString(" (")
261
			for i, r := range results {
262
				if i > 0 {
263
					b.WriteString(", ")
264
				}
265
				for j, name := range r.Names {
266
					if j > 0 {
267
						b.WriteString(", ")
268
					}
269
					b.WriteString(name.Name)
270
				}
271
				if len(r.Names) > 0 {
272
					b.WriteString(" ")
273
				}
274
				b.WriteString(formatExpr(r.Type))
275
			}
276
			b.WriteString(")")
277
		}
278
	}
279
280
	return b.String()
281
}
282

Source Files