sourcey 3.5.1 → 3.5.3

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.
Files changed (38) hide show
  1. package/README.md +82 -11
  2. package/dist/cli.js +122 -3
  3. package/dist/components/layout/Page.d.ts.map +1 -1
  4. package/dist/components/layout/Page.js +4 -1
  5. package/dist/config.d.ts +65 -3
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +42 -6
  8. package/dist/core/godoc-introspector.d.ts +26 -0
  9. package/dist/core/godoc-introspector.d.ts.map +1 -0
  10. package/dist/core/godoc-introspector.js +144 -0
  11. package/dist/core/godoc-loader.d.ts +34 -0
  12. package/dist/core/godoc-loader.d.ts.map +1 -0
  13. package/dist/core/godoc-loader.js +491 -0
  14. package/dist/core/godoc-types.d.ts +109 -0
  15. package/dist/core/godoc-types.d.ts.map +1 -0
  16. package/dist/core/godoc-types.js +8 -0
  17. package/dist/core/markdown-loader.d.ts +10 -0
  18. package/dist/core/markdown-loader.d.ts.map +1 -1
  19. package/dist/core/navigation.js +2 -2
  20. package/dist/core/search-indexer.d.ts.map +1 -1
  21. package/dist/core/search-indexer.js +13 -9
  22. package/dist/core/sourcey-godoc/cmd/sourcey-godoc/main.go +736 -0
  23. package/dist/core/sourcey-godoc/cmd/sourcey-godoc/site.go +497 -0
  24. package/dist/core/sourcey-godoc/cmd/sourcey-godoc/site_test.go +89 -0
  25. package/dist/core/sourcey-godoc/doc.go +11 -0
  26. package/dist/core/sourcey-godoc/go.mod +3 -0
  27. package/dist/dev-server.d.ts.map +1 -1
  28. package/dist/dev-server.js +52 -5
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +1 -33
  32. package/dist/site-assembly.d.ts +3 -0
  33. package/dist/site-assembly.d.ts.map +1 -1
  34. package/dist/site-assembly.js +42 -11
  35. package/dist/site-url.d.ts.map +1 -1
  36. package/dist/site-url.js +3 -0
  37. package/dist/themes/default/sourcey.css +244 -0
  38. package/package.json +14 -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
+ }