sourcey 3.5.1 → 3.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -10
- package/dist/cli.js +122 -3
- package/dist/components/layout/Page.d.ts.map +1 -1
- package/dist/components/layout/Page.js +4 -1
- package/dist/config.d.ts +60 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +28 -3
- package/dist/core/godoc-introspector.d.ts +26 -0
- package/dist/core/godoc-introspector.d.ts.map +1 -0
- package/dist/core/godoc-introspector.js +144 -0
- package/dist/core/godoc-loader.d.ts +34 -0
- package/dist/core/godoc-loader.d.ts.map +1 -0
- package/dist/core/godoc-loader.js +491 -0
- package/dist/core/godoc-types.d.ts +109 -0
- package/dist/core/godoc-types.d.ts.map +1 -0
- package/dist/core/godoc-types.js +8 -0
- package/dist/core/markdown-loader.d.ts +10 -0
- package/dist/core/markdown-loader.d.ts.map +1 -1
- package/dist/core/search-indexer.d.ts.map +1 -1
- package/dist/core/search-indexer.js +9 -0
- package/dist/core/sourcey-godoc/cmd/sourcey-godoc/main.go +736 -0
- package/dist/core/sourcey-godoc/cmd/sourcey-godoc/site.go +497 -0
- package/dist/core/sourcey-godoc/cmd/sourcey-godoc/site_test.go +89 -0
- package/dist/core/sourcey-godoc/doc.go +11 -0
- package/dist/core/sourcey-godoc/go.mod +3 -0
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +42 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/site-assembly.d.ts +3 -0
- package/dist/site-assembly.d.ts.map +1 -1
- package/dist/site-assembly.js +30 -1
- package/dist/themes/default/sourcey.css +244 -0
- package/package.json +13 -4
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
// Package main is Sourcey's native Go documentation introspector.
|
|
2
|
+
//
|
|
3
|
+
// It reads a Go module via the toolchain (`go list -json`) and emits a
|
|
4
|
+
// `GodocSnapshot` JSON document on stdout (or `--out`). The output mirrors
|
|
5
|
+
// the TypeScript types in `src/core/godoc-types.ts`.
|
|
6
|
+
//
|
|
7
|
+
// The helper depends only on the Go standard library, builds against the
|
|
8
|
+
// host toolchain via `go run`, and never shells out to `go doc`.
|
|
9
|
+
package main
|
|
10
|
+
|
|
11
|
+
import (
|
|
12
|
+
"bytes"
|
|
13
|
+
"encoding/json"
|
|
14
|
+
"errors"
|
|
15
|
+
"flag"
|
|
16
|
+
"fmt"
|
|
17
|
+
"go/ast"
|
|
18
|
+
"go/doc"
|
|
19
|
+
"go/parser"
|
|
20
|
+
"go/printer"
|
|
21
|
+
"go/token"
|
|
22
|
+
"io"
|
|
23
|
+
"os"
|
|
24
|
+
"os/exec"
|
|
25
|
+
"path/filepath"
|
|
26
|
+
"runtime/debug"
|
|
27
|
+
"sort"
|
|
28
|
+
"strings"
|
|
29
|
+
"time"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const (
|
|
33
|
+
schemaVersion = 1
|
|
34
|
+
source = "sourcey-godoc"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
var version = "dev"
|
|
38
|
+
|
|
39
|
+
type snapshot struct {
|
|
40
|
+
SchemaVersion int `json:"schema_version"`
|
|
41
|
+
Source string `json:"source"`
|
|
42
|
+
ModulePath string `json:"module_path"`
|
|
43
|
+
GeneratedAt string `json:"generated_at,omitempty"`
|
|
44
|
+
Packages []pkgOut `json:"packages"`
|
|
45
|
+
Diagnostics []diagnostic `json:"diagnostics,omitempty"`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type pkgOut struct {
|
|
49
|
+
ImportPath string `json:"importPath"`
|
|
50
|
+
Name string `json:"name"`
|
|
51
|
+
Synopsis string `json:"synopsis"`
|
|
52
|
+
Doc string `json:"doc"`
|
|
53
|
+
Dir string `json:"dir"`
|
|
54
|
+
Files []string `json:"files"`
|
|
55
|
+
Consts []valueOut `json:"consts"`
|
|
56
|
+
Vars []valueOut `json:"vars"`
|
|
57
|
+
Funcs []funcOut `json:"funcs"`
|
|
58
|
+
Types []typeOut `json:"types"`
|
|
59
|
+
Examples []exOut `json:"examples"`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type valueOut struct {
|
|
63
|
+
Name string `json:"name"`
|
|
64
|
+
Doc string `json:"doc"`
|
|
65
|
+
Declaration string `json:"declaration"`
|
|
66
|
+
Position *positionOut `json:"position,omitempty"`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type funcOut struct {
|
|
70
|
+
Name string `json:"name"`
|
|
71
|
+
Doc string `json:"doc"`
|
|
72
|
+
Signature string `json:"signature"`
|
|
73
|
+
Position *positionOut `json:"position,omitempty"`
|
|
74
|
+
Examples []exOut `json:"examples"`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type typeOut struct {
|
|
78
|
+
Name string `json:"name"`
|
|
79
|
+
Doc string `json:"doc"`
|
|
80
|
+
Declaration string `json:"declaration"`
|
|
81
|
+
Kind string `json:"kind"`
|
|
82
|
+
Position *positionOut `json:"position,omitempty"`
|
|
83
|
+
Fields []fieldOut `json:"fields"`
|
|
84
|
+
Methods []funcOut `json:"methods"`
|
|
85
|
+
Examples []exOut `json:"examples"`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type fieldOut struct {
|
|
89
|
+
Name string `json:"name"`
|
|
90
|
+
Doc string `json:"doc"`
|
|
91
|
+
Type string `json:"type"`
|
|
92
|
+
Tag string `json:"tag,omitempty"`
|
|
93
|
+
Embedded bool `json:"embedded,omitempty"`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type exOut struct {
|
|
97
|
+
Name string `json:"name"`
|
|
98
|
+
Suffix string `json:"suffix"`
|
|
99
|
+
Doc string `json:"doc"`
|
|
100
|
+
Code string `json:"code"`
|
|
101
|
+
Output string `json:"output,omitempty"`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type positionOut struct {
|
|
105
|
+
File string `json:"file"`
|
|
106
|
+
Line int `json:"line"`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type diagnostic struct {
|
|
110
|
+
Severity string `json:"severity"`
|
|
111
|
+
Code string `json:"code"`
|
|
112
|
+
Message string `json:"message"`
|
|
113
|
+
Package string `json:"package,omitempty"`
|
|
114
|
+
File string `json:"file,omitempty"`
|
|
115
|
+
Line int `json:"line,omitempty"`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type stringSliceFlag []string
|
|
119
|
+
|
|
120
|
+
func (s *stringSliceFlag) String() string { return strings.Join(*s, ",") }
|
|
121
|
+
func (s *stringSliceFlag) Set(v string) error {
|
|
122
|
+
for _, part := range strings.Split(v, ",") {
|
|
123
|
+
if part = strings.TrimSpace(part); part != "" {
|
|
124
|
+
*s = append(*s, part)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return nil
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type goListPackage struct {
|
|
131
|
+
Dir string
|
|
132
|
+
ImportPath string
|
|
133
|
+
Name string
|
|
134
|
+
Doc string
|
|
135
|
+
Module *struct{ Path string }
|
|
136
|
+
GoFiles []string
|
|
137
|
+
TestGoFiles []string
|
|
138
|
+
XTestGoFiles []string
|
|
139
|
+
IgnoredGoFiles []string
|
|
140
|
+
Error *struct {
|
|
141
|
+
Err string
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type config struct {
|
|
146
|
+
command string
|
|
147
|
+
moduleDir string
|
|
148
|
+
patterns []string
|
|
149
|
+
excludes []string
|
|
150
|
+
includeTests bool
|
|
151
|
+
includeUnexported bool
|
|
152
|
+
out string
|
|
153
|
+
title string
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func main() {
|
|
157
|
+
cfg, err := parseFlags()
|
|
158
|
+
if err != nil {
|
|
159
|
+
fail(err)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
snap, err := build(cfg)
|
|
163
|
+
if err != nil {
|
|
164
|
+
fail(err)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if cfg.command == "generate" {
|
|
168
|
+
if err := writeSite(cfg, snap); err != nil {
|
|
169
|
+
fail(err)
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
if err := writeSnapshot(cfg.out, snap); err != nil {
|
|
173
|
+
fail(err)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if hasErrorDiagnostic(snap.Diagnostics) {
|
|
178
|
+
os.Exit(1)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
func fail(err error) {
|
|
183
|
+
fmt.Fprintln(os.Stderr, "sourcey-godoc:", err)
|
|
184
|
+
os.Exit(2)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func parseFlags() (*config, error) {
|
|
188
|
+
args := os.Args[1:]
|
|
189
|
+
command := "snapshot"
|
|
190
|
+
if len(args) > 0 {
|
|
191
|
+
switch args[0] {
|
|
192
|
+
case "snapshot", "generate":
|
|
193
|
+
command = args[0]
|
|
194
|
+
args = args[1:]
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
defaultOut := ""
|
|
199
|
+
if command == "generate" {
|
|
200
|
+
defaultOut = "site"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fs := flag.NewFlagSet("sourcey-godoc "+command, flag.ContinueOnError)
|
|
204
|
+
module := fs.String("module", ".", "Go module directory")
|
|
205
|
+
includeTests := fs.Bool("include-tests", true, "include examples from *_test.go")
|
|
206
|
+
includeUnexported := fs.Bool("include-unexported", false, "include unexported symbols")
|
|
207
|
+
out := fs.String("out", defaultOut, "output file for snapshot mode; output directory for generate mode")
|
|
208
|
+
title := fs.String("title", "", "site title for generate mode")
|
|
209
|
+
showVersion := fs.Bool("version", false, "print version and exit")
|
|
210
|
+
var patterns stringSliceFlag
|
|
211
|
+
var excludes stringSliceFlag
|
|
212
|
+
fs.Var(&patterns, "packages", "package patterns (repeatable; comma-separated allowed)")
|
|
213
|
+
fs.Var(&excludes, "exclude", "package import-path prefixes to exclude (repeatable)")
|
|
214
|
+
if err := fs.Parse(args); err != nil {
|
|
215
|
+
return nil, err
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if *showVersion {
|
|
219
|
+
fmt.Println(versionString())
|
|
220
|
+
os.Exit(0)
|
|
221
|
+
}
|
|
222
|
+
if fs.NArg() > 0 {
|
|
223
|
+
return nil, fmt.Errorf("unexpected arguments: %s", strings.Join(fs.Args(), " "))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
abs, err := filepath.Abs(*module)
|
|
227
|
+
if err != nil {
|
|
228
|
+
return nil, fmt.Errorf("resolve --module: %w", err)
|
|
229
|
+
}
|
|
230
|
+
if info, err := os.Stat(abs); err != nil || !info.IsDir() {
|
|
231
|
+
return nil, fmt.Errorf("--module %q is not a directory", abs)
|
|
232
|
+
}
|
|
233
|
+
if len(patterns) == 0 {
|
|
234
|
+
patterns = []string{"./..."}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return &config{
|
|
238
|
+
command: command,
|
|
239
|
+
moduleDir: abs,
|
|
240
|
+
patterns: patterns,
|
|
241
|
+
excludes: excludes,
|
|
242
|
+
includeTests: *includeTests,
|
|
243
|
+
includeUnexported: *includeUnexported,
|
|
244
|
+
out: *out,
|
|
245
|
+
title: *title,
|
|
246
|
+
}, nil
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
func writeSnapshot(out string, snap *snapshot) error {
|
|
250
|
+
payload, err := json.MarshalIndent(snap, "", " ")
|
|
251
|
+
if err != nil {
|
|
252
|
+
return err
|
|
253
|
+
}
|
|
254
|
+
payload = append(payload, '\n')
|
|
255
|
+
if out == "" {
|
|
256
|
+
_, err = os.Stdout.Write(payload)
|
|
257
|
+
return err
|
|
258
|
+
}
|
|
259
|
+
if err := os.MkdirAll(filepath.Dir(out), 0o755); err != nil {
|
|
260
|
+
return err
|
|
261
|
+
}
|
|
262
|
+
return os.WriteFile(out, payload, 0o644)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
func versionString() string {
|
|
266
|
+
if version != "" && version != "dev" {
|
|
267
|
+
return version
|
|
268
|
+
}
|
|
269
|
+
info, ok := debug.ReadBuildInfo()
|
|
270
|
+
if !ok {
|
|
271
|
+
return version
|
|
272
|
+
}
|
|
273
|
+
if info.Main.Version == "" || info.Main.Version == "(devel)" {
|
|
274
|
+
return version
|
|
275
|
+
}
|
|
276
|
+
return strings.TrimPrefix(info.Main.Version, "v")
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
func build(cfg *config) (*snapshot, error) {
|
|
280
|
+
pkgs, err := goList(cfg.moduleDir, cfg.patterns)
|
|
281
|
+
if err != nil {
|
|
282
|
+
return nil, err
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
modulePath := ""
|
|
286
|
+
if len(pkgs) > 0 && pkgs[0].Module != nil {
|
|
287
|
+
modulePath = pkgs[0].Module.Path
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
snap := &snapshot{
|
|
291
|
+
SchemaVersion: schemaVersion,
|
|
292
|
+
Source: source,
|
|
293
|
+
ModulePath: modulePath,
|
|
294
|
+
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
|
295
|
+
Packages: []pkgOut{},
|
|
296
|
+
Diagnostics: []diagnostic{},
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
matched := 0
|
|
300
|
+
for _, p := range pkgs {
|
|
301
|
+
if p.Error != nil {
|
|
302
|
+
snap.Diagnostics = append(snap.Diagnostics, diagnostic{
|
|
303
|
+
Severity: "error",
|
|
304
|
+
Code: "GODOC_PACKAGE_LIST_FAILED",
|
|
305
|
+
Message: p.Error.Err,
|
|
306
|
+
Package: p.ImportPath,
|
|
307
|
+
})
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
if isExcluded(p.ImportPath, cfg.excludes) {
|
|
311
|
+
continue
|
|
312
|
+
}
|
|
313
|
+
matched++
|
|
314
|
+
out, perPkgDiags := parsePackage(p, cfg)
|
|
315
|
+
snap.Diagnostics = append(snap.Diagnostics, perPkgDiags...)
|
|
316
|
+
if out != nil {
|
|
317
|
+
snap.Packages = append(snap.Packages, *out)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if matched == 0 {
|
|
322
|
+
snap.Diagnostics = append(snap.Diagnostics, diagnostic{
|
|
323
|
+
Severity: "warning",
|
|
324
|
+
Code: "GODOC_NO_PACKAGES_MATCHED",
|
|
325
|
+
Message: fmt.Sprintf("no packages matched patterns %v", cfg.patterns),
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
sort.Slice(snap.Packages, func(i, j int) bool {
|
|
330
|
+
return snap.Packages[i].ImportPath < snap.Packages[j].ImportPath
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
return snap, nil
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
func goList(moduleDir string, patterns []string) ([]goListPackage, error) {
|
|
337
|
+
args := append([]string{"list", "-json", "-e"}, patterns...)
|
|
338
|
+
cmd := exec.Command("go", args...)
|
|
339
|
+
cmd.Dir = moduleDir
|
|
340
|
+
var stdout, stderr bytes.Buffer
|
|
341
|
+
cmd.Stdout = &stdout
|
|
342
|
+
cmd.Stderr = &stderr
|
|
343
|
+
if err := cmd.Run(); err != nil {
|
|
344
|
+
return nil, fmt.Errorf("go list failed: %v\n%s", err, strings.TrimSpace(stderr.String()))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
var pkgs []goListPackage
|
|
348
|
+
dec := json.NewDecoder(&stdout)
|
|
349
|
+
for {
|
|
350
|
+
var p goListPackage
|
|
351
|
+
if err := dec.Decode(&p); err != nil {
|
|
352
|
+
if errors.Is(err, io.EOF) {
|
|
353
|
+
break
|
|
354
|
+
}
|
|
355
|
+
return nil, fmt.Errorf("decode go list output: %w", err)
|
|
356
|
+
}
|
|
357
|
+
pkgs = append(pkgs, p)
|
|
358
|
+
}
|
|
359
|
+
return pkgs, nil
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
func isExcluded(importPath string, excludes []string) bool {
|
|
363
|
+
for _, e := range excludes {
|
|
364
|
+
e = strings.TrimSuffix(strings.TrimSuffix(e, "/..."), "/")
|
|
365
|
+
if e == "" {
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
if importPath == e || strings.HasPrefix(importPath, e+"/") {
|
|
369
|
+
return true
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
func parsePackage(p goListPackage, cfg *config) (*pkgOut, []diagnostic) {
|
|
376
|
+
if len(p.GoFiles) == 0 && len(p.TestGoFiles) == 0 {
|
|
377
|
+
return nil, []diagnostic{{
|
|
378
|
+
Severity: "info",
|
|
379
|
+
Code: "GODOC_PACKAGE_EMPTY",
|
|
380
|
+
Message: "package has no Go files",
|
|
381
|
+
Package: p.ImportPath,
|
|
382
|
+
}}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
fset := token.NewFileSet()
|
|
386
|
+
parseErrors := []diagnostic{}
|
|
387
|
+
packageFiles := []*ast.File{}
|
|
388
|
+
exampleFiles := []*ast.File{}
|
|
389
|
+
|
|
390
|
+
collect := func(files []string, severity string, code string, target *[]*ast.File) {
|
|
391
|
+
for _, name := range files {
|
|
392
|
+
path := filepath.Join(p.Dir, name)
|
|
393
|
+
file, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
|
|
394
|
+
if err != nil {
|
|
395
|
+
parseErrors = append(parseErrors, diagnostic{
|
|
396
|
+
Severity: severity,
|
|
397
|
+
Code: code,
|
|
398
|
+
Message: err.Error(),
|
|
399
|
+
Package: p.ImportPath,
|
|
400
|
+
File: name,
|
|
401
|
+
})
|
|
402
|
+
continue
|
|
403
|
+
}
|
|
404
|
+
*target = append(*target, file)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
collect(p.GoFiles, "error", "GODOC_PACKAGE_PARSE_FAILED", &packageFiles)
|
|
409
|
+
if cfg.includeTests {
|
|
410
|
+
collect(p.TestGoFiles, "warning", "GODOC_TEST_PARSE_FAILED", &exampleFiles)
|
|
411
|
+
collect(p.XTestGoFiles, "warning", "GODOC_TEST_PARSE_FAILED", &exampleFiles)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if len(packageFiles) == 0 {
|
|
415
|
+
return nil, parseErrors
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
docMode := doc.Mode(0)
|
|
419
|
+
if cfg.includeUnexported {
|
|
420
|
+
docMode |= doc.AllDecls
|
|
421
|
+
}
|
|
422
|
+
d, err := doc.NewFromFiles(fset, packageFiles, p.ImportPath, docMode)
|
|
423
|
+
if err != nil {
|
|
424
|
+
parseErrors = append(parseErrors, diagnostic{
|
|
425
|
+
Severity: "error",
|
|
426
|
+
Code: "GODOC_PACKAGE_DOC_FAILED",
|
|
427
|
+
Message: err.Error(),
|
|
428
|
+
Package: p.ImportPath,
|
|
429
|
+
})
|
|
430
|
+
return nil, parseErrors
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
moduleDir := cfg.moduleDir
|
|
434
|
+
files := relPaths(moduleDir, p.GoFiles, p.Dir)
|
|
435
|
+
|
|
436
|
+
out := &pkgOut{
|
|
437
|
+
ImportPath: p.ImportPath,
|
|
438
|
+
Name: d.Name,
|
|
439
|
+
Doc: strings.TrimSpace(d.Doc),
|
|
440
|
+
Synopsis: doc.Synopsis(d.Doc),
|
|
441
|
+
Dir: relPath(moduleDir, p.Dir),
|
|
442
|
+
Files: files,
|
|
443
|
+
Consts: []valueOut{},
|
|
444
|
+
Vars: []valueOut{},
|
|
445
|
+
Funcs: []funcOut{},
|
|
446
|
+
Types: []typeOut{},
|
|
447
|
+
Examples: []exOut{},
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
allConsts := append([]*doc.Value{}, d.Consts...)
|
|
451
|
+
allVars := append([]*doc.Value{}, d.Vars...)
|
|
452
|
+
for _, t := range d.Types {
|
|
453
|
+
allConsts = append(allConsts, t.Consts...)
|
|
454
|
+
allVars = append(allVars, t.Vars...)
|
|
455
|
+
}
|
|
456
|
+
for _, c := range allConsts {
|
|
457
|
+
out.Consts = append(out.Consts, extractValues(fset, moduleDir, c)...)
|
|
458
|
+
}
|
|
459
|
+
for _, v := range allVars {
|
|
460
|
+
out.Vars = append(out.Vars, extractValues(fset, moduleDir, v)...)
|
|
461
|
+
}
|
|
462
|
+
for _, f := range d.Funcs {
|
|
463
|
+
out.Funcs = append(out.Funcs, extractFunc(fset, moduleDir, f))
|
|
464
|
+
}
|
|
465
|
+
for _, t := range d.Types {
|
|
466
|
+
out.Types = append(out.Types, extractType(fset, moduleDir, t))
|
|
467
|
+
// Factory functions returning t (e.g. `func New() *Widget`) live
|
|
468
|
+
// under the type in go/doc; hoist them to package-level so
|
|
469
|
+
// renderers can list them in the Functions section.
|
|
470
|
+
for _, f := range t.Funcs {
|
|
471
|
+
out.Funcs = append(out.Funcs, extractFunc(fset, moduleDir, f))
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
sort.Slice(out.Funcs, func(i, j int) bool { return out.Funcs[i].Name < out.Funcs[j].Name })
|
|
475
|
+
|
|
476
|
+
examples := doc.Examples(exampleFiles...)
|
|
477
|
+
out.Examples = convertExamples(fset, packageExamples(examples))
|
|
478
|
+
for i := range out.Funcs {
|
|
479
|
+
if attached := functionExamples(examples, out.Funcs[i].Name); len(attached) > 0 {
|
|
480
|
+
out.Funcs[i].Examples = convertExamples(fset, attached)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
for i := range out.Types {
|
|
484
|
+
out.Types[i].Examples = convertExamples(fset, typeExamples(examples, out.Types[i].Name))
|
|
485
|
+
for j := range out.Types[i].Methods {
|
|
486
|
+
out.Types[i].Methods[j].Examples = convertExamples(fset, methodExamples(examples, out.Types[i].Name, out.Types[i].Methods[j].Name))
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return out, parseErrors
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
func convertExamples(fset *token.FileSet, examples []*doc.Example) []exOut {
|
|
494
|
+
out := make([]exOut, 0, len(examples))
|
|
495
|
+
for _, ex := range examples {
|
|
496
|
+
out = append(out, exOut{
|
|
497
|
+
Name: ex.Name,
|
|
498
|
+
Suffix: ex.Suffix,
|
|
499
|
+
Doc: strings.TrimSpace(ex.Doc),
|
|
500
|
+
Code: printNode(fset, ex.Code),
|
|
501
|
+
Output: strings.TrimSpace(ex.Output),
|
|
502
|
+
})
|
|
503
|
+
}
|
|
504
|
+
return out
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
func packageExamples(examples []*doc.Example) []*doc.Example {
|
|
508
|
+
return matchingExamples(examples, func(ex *doc.Example) bool { return ex.Name == "" })
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
func functionExamples(examples []*doc.Example, name string) []*doc.Example {
|
|
512
|
+
return matchingExamples(examples, func(ex *doc.Example) bool { return ex.Name == name })
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
func typeExamples(examples []*doc.Example, typeName string) []*doc.Example {
|
|
516
|
+
return matchingExamples(examples, func(ex *doc.Example) bool { return ex.Name == typeName })
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
func methodExamples(examples []*doc.Example, typeName string, methodName string) []*doc.Example {
|
|
520
|
+
want := typeName + "_" + methodName
|
|
521
|
+
return matchingExamples(examples, func(ex *doc.Example) bool { return ex.Name == want })
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
func matchingExamples(examples []*doc.Example, keep func(*doc.Example) bool) []*doc.Example {
|
|
525
|
+
out := []*doc.Example{}
|
|
526
|
+
for _, ex := range examples {
|
|
527
|
+
if keep(ex) {
|
|
528
|
+
out = append(out, ex)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return out
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
func extractValues(fset *token.FileSet, moduleDir string, v *doc.Value) []valueOut {
|
|
535
|
+
out := []valueOut{}
|
|
536
|
+
groupDecl := printNode(fset, v.Decl)
|
|
537
|
+
groupDoc := strings.TrimSpace(v.Doc)
|
|
538
|
+
for _, spec := range v.Decl.Specs {
|
|
539
|
+
vs, ok := spec.(*ast.ValueSpec)
|
|
540
|
+
if !ok {
|
|
541
|
+
continue
|
|
542
|
+
}
|
|
543
|
+
for _, name := range vs.Names {
|
|
544
|
+
out = append(out, valueOut{
|
|
545
|
+
Name: name.Name,
|
|
546
|
+
Doc: groupDoc,
|
|
547
|
+
Declaration: groupDecl,
|
|
548
|
+
Position: relPosition(fset, moduleDir, name.Pos()),
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return out
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
func extractFunc(fset *token.FileSet, moduleDir string, f *doc.Func) funcOut {
|
|
556
|
+
signature := "func " + f.Name + signatureSuffix(fset, f.Decl)
|
|
557
|
+
if f.Recv != "" {
|
|
558
|
+
recv := strings.TrimSpace(printNode(fset, f.Decl.Recv))
|
|
559
|
+
signature = "func " + recv + " " + f.Name + signatureSuffix(fset, f.Decl)
|
|
560
|
+
}
|
|
561
|
+
return funcOut{
|
|
562
|
+
Name: f.Name,
|
|
563
|
+
Doc: strings.TrimSpace(f.Doc),
|
|
564
|
+
Signature: signature,
|
|
565
|
+
Position: relPosition(fset, moduleDir, f.Decl.Pos()),
|
|
566
|
+
Examples: []exOut{},
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
func signatureSuffix(fset *token.FileSet, decl *ast.FuncDecl) string {
|
|
571
|
+
clone := *decl
|
|
572
|
+
clone.Body = nil
|
|
573
|
+
clone.Doc = nil
|
|
574
|
+
rendered := printNode(fset, &clone)
|
|
575
|
+
if i := strings.Index(rendered, decl.Name.Name); i >= 0 {
|
|
576
|
+
rendered = rendered[i+len(decl.Name.Name):]
|
|
577
|
+
}
|
|
578
|
+
return rendered
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
func extractType(fset *token.FileSet, moduleDir string, t *doc.Type) typeOut {
|
|
582
|
+
declaration := printNode(fset, t.Decl)
|
|
583
|
+
kind := "defined"
|
|
584
|
+
fields := []fieldOut{}
|
|
585
|
+
|
|
586
|
+
for _, spec := range t.Decl.Specs {
|
|
587
|
+
ts, ok := spec.(*ast.TypeSpec)
|
|
588
|
+
if !ok {
|
|
589
|
+
continue
|
|
590
|
+
}
|
|
591
|
+
switch typed := ts.Type.(type) {
|
|
592
|
+
case *ast.StructType:
|
|
593
|
+
kind = "struct"
|
|
594
|
+
fields = extractFields(fset, typed.Fields)
|
|
595
|
+
case *ast.InterfaceType:
|
|
596
|
+
kind = "interface"
|
|
597
|
+
fields = extractInterfaceMembers(fset, typed)
|
|
598
|
+
default:
|
|
599
|
+
if ts.Assign.IsValid() {
|
|
600
|
+
kind = "alias"
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
out := typeOut{
|
|
606
|
+
Name: t.Name,
|
|
607
|
+
Doc: strings.TrimSpace(t.Doc),
|
|
608
|
+
Declaration: declaration,
|
|
609
|
+
Kind: kind,
|
|
610
|
+
Position: relPosition(fset, moduleDir, t.Decl.Pos()),
|
|
611
|
+
Fields: fields,
|
|
612
|
+
Methods: []funcOut{},
|
|
613
|
+
Examples: []exOut{},
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
for _, m := range t.Methods {
|
|
617
|
+
out.Methods = append(out.Methods, extractFunc(fset, moduleDir, m))
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return out
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
func extractFields(fset *token.FileSet, list *ast.FieldList) []fieldOut {
|
|
624
|
+
out := []fieldOut{}
|
|
625
|
+
if list == nil {
|
|
626
|
+
return out
|
|
627
|
+
}
|
|
628
|
+
for _, field := range list.List {
|
|
629
|
+
fieldType := strings.TrimSpace(printNode(fset, field.Type))
|
|
630
|
+
tag := ""
|
|
631
|
+
if field.Tag != nil {
|
|
632
|
+
tag = strings.Trim(field.Tag.Value, "`")
|
|
633
|
+
}
|
|
634
|
+
docComment := strings.TrimSpace(commentText(field.Doc, field.Comment))
|
|
635
|
+
if len(field.Names) == 0 {
|
|
636
|
+
out = append(out, fieldOut{
|
|
637
|
+
Name: fieldType,
|
|
638
|
+
Doc: docComment,
|
|
639
|
+
Type: fieldType,
|
|
640
|
+
Tag: tag,
|
|
641
|
+
Embedded: true,
|
|
642
|
+
})
|
|
643
|
+
continue
|
|
644
|
+
}
|
|
645
|
+
for _, name := range field.Names {
|
|
646
|
+
out = append(out, fieldOut{
|
|
647
|
+
Name: name.Name,
|
|
648
|
+
Doc: docComment,
|
|
649
|
+
Type: fieldType,
|
|
650
|
+
Tag: tag,
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return out
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
func extractInterfaceMembers(fset *token.FileSet, iface *ast.InterfaceType) []fieldOut {
|
|
658
|
+
out := []fieldOut{}
|
|
659
|
+
if iface.Methods == nil {
|
|
660
|
+
return out
|
|
661
|
+
}
|
|
662
|
+
for _, method := range iface.Methods.List {
|
|
663
|
+
typeStr := strings.TrimSpace(printNode(fset, method.Type))
|
|
664
|
+
docComment := strings.TrimSpace(commentText(method.Doc, method.Comment))
|
|
665
|
+
if len(method.Names) == 0 {
|
|
666
|
+
out = append(out, fieldOut{
|
|
667
|
+
Name: typeStr,
|
|
668
|
+
Doc: docComment,
|
|
669
|
+
Type: typeStr,
|
|
670
|
+
Embedded: true,
|
|
671
|
+
})
|
|
672
|
+
continue
|
|
673
|
+
}
|
|
674
|
+
for _, name := range method.Names {
|
|
675
|
+
out = append(out, fieldOut{
|
|
676
|
+
Name: name.Name,
|
|
677
|
+
Doc: docComment,
|
|
678
|
+
Type: typeStr,
|
|
679
|
+
})
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return out
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
func printNode(fset *token.FileSet, node any) string {
|
|
686
|
+
var buf bytes.Buffer
|
|
687
|
+
cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 4}
|
|
688
|
+
if err := cfg.Fprint(&buf, fset, node); err != nil {
|
|
689
|
+
return ""
|
|
690
|
+
}
|
|
691
|
+
return buf.String()
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
func relPath(moduleDir, target string) string {
|
|
695
|
+
if rel, err := filepath.Rel(moduleDir, target); err == nil {
|
|
696
|
+
return filepath.ToSlash(rel)
|
|
697
|
+
}
|
|
698
|
+
return filepath.ToSlash(target)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
func relPaths(moduleDir string, names []string, baseDir string) []string {
|
|
702
|
+
out := make([]string, 0, len(names))
|
|
703
|
+
for _, n := range names {
|
|
704
|
+
out = append(out, relPath(moduleDir, filepath.Join(baseDir, n)))
|
|
705
|
+
}
|
|
706
|
+
sort.Strings(out)
|
|
707
|
+
return out
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
func relPosition(fset *token.FileSet, moduleDir string, p token.Pos) *positionOut {
|
|
711
|
+
if !p.IsValid() {
|
|
712
|
+
return nil
|
|
713
|
+
}
|
|
714
|
+
pos := fset.Position(p)
|
|
715
|
+
return &positionOut{File: relPath(moduleDir, pos.Filename), Line: pos.Line}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
func commentText(groups ...*ast.CommentGroup) string {
|
|
719
|
+
parts := []string{}
|
|
720
|
+
for _, g := range groups {
|
|
721
|
+
if g == nil {
|
|
722
|
+
continue
|
|
723
|
+
}
|
|
724
|
+
parts = append(parts, g.Text())
|
|
725
|
+
}
|
|
726
|
+
return strings.Join(parts, "\n")
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
func hasErrorDiagnostic(diags []diagnostic) bool {
|
|
730
|
+
for _, d := range diags {
|
|
731
|
+
if d.Severity == "error" {
|
|
732
|
+
return true
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return false
|
|
736
|
+
}
|