golang.org/x/tools/gopls@v0.15.3/doc/generate.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 //go:build go1.16 6 // +build go1.16 7 8 // Command generate creates API (settings, etc) documentation in JSON and 9 // Markdown for machine and human consumption. 10 package main 11 12 import ( 13 "bytes" 14 "encoding/json" 15 "fmt" 16 "go/ast" 17 "go/format" 18 "go/token" 19 "go/types" 20 "io" 21 "os" 22 "os/exec" 23 "path/filepath" 24 "reflect" 25 "regexp" 26 "sort" 27 "strconv" 28 "strings" 29 "time" 30 "unicode" 31 32 "github.com/jba/printsrc" 33 "golang.org/x/tools/go/ast/astutil" 34 "golang.org/x/tools/go/packages" 35 "golang.org/x/tools/gopls/internal/golang" 36 "golang.org/x/tools/gopls/internal/mod" 37 "golang.org/x/tools/gopls/internal/protocol/command" 38 "golang.org/x/tools/gopls/internal/protocol/command/commandmeta" 39 "golang.org/x/tools/gopls/internal/settings" 40 "golang.org/x/tools/gopls/internal/util/safetoken" 41 ) 42 43 func main() { 44 if _, err := doMain(true); err != nil { 45 fmt.Fprintf(os.Stderr, "Generation failed: %v\n", err) 46 os.Exit(1) 47 } 48 } 49 50 func doMain(write bool) (bool, error) { 51 api, err := loadAPI() 52 if err != nil { 53 return false, err 54 } 55 56 settingsDir, err := pkgDir("golang.org/x/tools/gopls/internal/settings") 57 if err != nil { 58 return false, err 59 } 60 61 if ok, err := rewriteFile(filepath.Join(settingsDir, "api_json.go"), api, write, rewriteAPI); !ok || err != nil { 62 return ok, err 63 } 64 65 goplsDir, err := pkgDir("golang.org/x/tools/gopls") 66 if err != nil { 67 return false, err 68 } 69 70 if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "settings.md"), api, write, rewriteSettings); !ok || err != nil { 71 return ok, err 72 } 73 if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "commands.md"), api, write, rewriteCommands); !ok || err != nil { 74 return ok, err 75 } 76 if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "analyzers.md"), api, write, rewriteAnalyzers); !ok || err != nil { 77 return ok, err 78 } 79 if ok, err := rewriteFile(filepath.Join(goplsDir, "doc", "inlayHints.md"), api, write, rewriteInlayHints); !ok || err != nil { 80 return ok, err 81 } 82 83 return true, nil 84 } 85 86 // pkgDir returns the directory corresponding to the import path pkgPath. 87 func pkgDir(pkgPath string) (string, error) { 88 cmd := exec.Command("go", "list", "-f", "{{.Dir}}", pkgPath) 89 out, err := cmd.Output() 90 if err != nil { 91 if ee, _ := err.(*exec.ExitError); ee != nil && len(ee.Stderr) > 0 { 92 return "", fmt.Errorf("%v: %w\n%s", cmd, err, ee.Stderr) 93 } 94 return "", fmt.Errorf("%v: %w", cmd, err) 95 } 96 return strings.TrimSpace(string(out)), nil 97 } 98 99 func loadAPI() (*settings.APIJSON, error) { 100 pkgs, err := packages.Load( 101 &packages.Config{ 102 Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedDeps, 103 }, 104 "golang.org/x/tools/gopls/internal/settings", 105 ) 106 if err != nil { 107 return nil, err 108 } 109 pkg := pkgs[0] 110 111 defaults := settings.DefaultOptions() 112 api := &settings.APIJSON{ 113 Options: map[string][]*settings.OptionJSON{}, 114 Analyzers: loadAnalyzers(defaults.DefaultAnalyzers), // no staticcheck analyzers 115 } 116 117 api.Commands, err = loadCommands() 118 if err != nil { 119 return nil, err 120 } 121 api.Lenses = loadLenses(api.Commands) 122 123 // Transform the internal command name to the external command name. 124 for _, c := range api.Commands { 125 c.Command = command.ID(c.Command) 126 } 127 api.Hints = loadHints(golang.AllInlayHints) 128 for _, category := range []reflect.Value{ 129 reflect.ValueOf(defaults.UserOptions), 130 } { 131 // Find the type information and ast.File corresponding to the category. 132 optsType := pkg.Types.Scope().Lookup(category.Type().Name()) 133 if optsType == nil { 134 return nil, fmt.Errorf("could not find %v in scope %v", category.Type().Name(), pkg.Types.Scope()) 135 } 136 opts, err := loadOptions(category, optsType, pkg, "") 137 if err != nil { 138 return nil, err 139 } 140 catName := strings.TrimSuffix(category.Type().Name(), "Options") 141 api.Options[catName] = opts 142 143 // Hardcode the expected values for the analyses and code lenses 144 // settings, since their keys are not enums. 145 for _, opt := range opts { 146 switch opt.Name { 147 case "analyses": 148 for _, a := range api.Analyzers { 149 opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, settings.EnumKey{ 150 Name: fmt.Sprintf("%q", a.Name), 151 Doc: a.Doc, 152 Default: strconv.FormatBool(a.Default), 153 }) 154 } 155 case "codelenses": 156 // Hack: Lenses don't set default values, and we don't want to 157 // pass in the list of expected lenses to loadOptions. Instead, 158 // format the defaults using reflection here. The hackiest part 159 // is reversing lowercasing of the field name. 160 reflectField := category.FieldByName(upperFirst(opt.Name)) 161 for _, l := range api.Lenses { 162 def, err := formatDefaultFromEnumBoolMap(reflectField, l.Lens) 163 if err != nil { 164 return nil, err 165 } 166 opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, settings.EnumKey{ 167 Name: fmt.Sprintf("%q", l.Lens), 168 Doc: l.Doc, 169 Default: def, 170 }) 171 } 172 case "hints": 173 for _, a := range api.Hints { 174 opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, settings.EnumKey{ 175 Name: fmt.Sprintf("%q", a.Name), 176 Doc: a.Doc, 177 Default: strconv.FormatBool(a.Default), 178 }) 179 } 180 } 181 } 182 } 183 return api, nil 184 } 185 186 func loadOptions(category reflect.Value, optsType types.Object, pkg *packages.Package, hierarchy string) ([]*settings.OptionJSON, error) { 187 file, err := fileForPos(pkg, optsType.Pos()) 188 if err != nil { 189 return nil, err 190 } 191 192 enums, err := loadEnums(pkg) 193 if err != nil { 194 return nil, err 195 } 196 197 var opts []*settings.OptionJSON 198 optsStruct := optsType.Type().Underlying().(*types.Struct) 199 for i := 0; i < optsStruct.NumFields(); i++ { 200 // The types field gives us the type. 201 typesField := optsStruct.Field(i) 202 203 // If the field name ends with "Options", assume it is a struct with 204 // additional options and process it recursively. 205 if h := strings.TrimSuffix(typesField.Name(), "Options"); h != typesField.Name() { 206 // Keep track of the parent structs. 207 if hierarchy != "" { 208 h = hierarchy + "." + h 209 } 210 options, err := loadOptions(category, typesField, pkg, strings.ToLower(h)) 211 if err != nil { 212 return nil, err 213 } 214 opts = append(opts, options...) 215 continue 216 } 217 path, _ := astutil.PathEnclosingInterval(file, typesField.Pos(), typesField.Pos()) 218 if len(path) < 2 { 219 return nil, fmt.Errorf("could not find AST node for field %v", typesField) 220 } 221 // The AST field gives us the doc. 222 astField, ok := path[1].(*ast.Field) 223 if !ok { 224 return nil, fmt.Errorf("unexpected AST path %v", path) 225 } 226 227 // The reflect field gives us the default value. 228 reflectField := category.FieldByName(typesField.Name()) 229 if !reflectField.IsValid() { 230 return nil, fmt.Errorf("could not find reflect field for %v", typesField.Name()) 231 } 232 233 def, err := formatDefault(reflectField) 234 if err != nil { 235 return nil, err 236 } 237 238 typ := typesField.Type().String() 239 if _, ok := enums[typesField.Type()]; ok { 240 typ = "enum" 241 } 242 name := lowerFirst(typesField.Name()) 243 244 var enumKeys settings.EnumKeys 245 if m, ok := typesField.Type().Underlying().(*types.Map); ok { 246 e, ok := enums[m.Key()] 247 if ok { 248 typ = strings.Replace(typ, m.Key().String(), m.Key().Underlying().String(), 1) 249 } 250 keys, err := collectEnumKeys(name, m, reflectField, e) 251 if err != nil { 252 return nil, err 253 } 254 if keys != nil { 255 enumKeys = *keys 256 } 257 } 258 259 // Get the status of the field by checking its struct tags. 260 reflectStructField, ok := category.Type().FieldByName(typesField.Name()) 261 if !ok { 262 return nil, fmt.Errorf("no struct field for %s", typesField.Name()) 263 } 264 status := reflectStructField.Tag.Get("status") 265 266 opts = append(opts, &settings.OptionJSON{ 267 Name: name, 268 Type: typ, 269 Doc: lowerFirst(astField.Doc.Text()), 270 Default: def, 271 EnumKeys: enumKeys, 272 EnumValues: enums[typesField.Type()], 273 Status: status, 274 Hierarchy: hierarchy, 275 }) 276 } 277 return opts, nil 278 } 279 280 func loadEnums(pkg *packages.Package) (map[types.Type][]settings.EnumValue, error) { 281 enums := map[types.Type][]settings.EnumValue{} 282 for _, name := range pkg.Types.Scope().Names() { 283 obj := pkg.Types.Scope().Lookup(name) 284 cnst, ok := obj.(*types.Const) 285 if !ok { 286 continue 287 } 288 f, err := fileForPos(pkg, cnst.Pos()) 289 if err != nil { 290 return nil, fmt.Errorf("finding file for %q: %v", cnst.Name(), err) 291 } 292 path, _ := astutil.PathEnclosingInterval(f, cnst.Pos(), cnst.Pos()) 293 spec := path[1].(*ast.ValueSpec) 294 value := cnst.Val().ExactString() 295 doc := valueDoc(cnst.Name(), value, spec.Doc.Text()) 296 v := settings.EnumValue{ 297 Value: value, 298 Doc: doc, 299 } 300 enums[obj.Type()] = append(enums[obj.Type()], v) 301 } 302 return enums, nil 303 } 304 305 func collectEnumKeys(name string, m *types.Map, reflectField reflect.Value, enumValues []settings.EnumValue) (*settings.EnumKeys, error) { 306 // Make sure the value type gets set for analyses and codelenses 307 // too. 308 if len(enumValues) == 0 && !hardcodedEnumKeys(name) { 309 return nil, nil 310 } 311 keys := &settings.EnumKeys{ 312 ValueType: m.Elem().String(), 313 } 314 // We can get default values for enum -> bool maps. 315 var isEnumBoolMap bool 316 if basic, ok := m.Elem().Underlying().(*types.Basic); ok && basic.Kind() == types.Bool { 317 isEnumBoolMap = true 318 } 319 for _, v := range enumValues { 320 var def string 321 if isEnumBoolMap { 322 var err error 323 def, err = formatDefaultFromEnumBoolMap(reflectField, v.Value) 324 if err != nil { 325 return nil, err 326 } 327 } 328 keys.Keys = append(keys.Keys, settings.EnumKey{ 329 Name: v.Value, 330 Doc: v.Doc, 331 Default: def, 332 }) 333 } 334 return keys, nil 335 } 336 337 func formatDefaultFromEnumBoolMap(reflectMap reflect.Value, enumKey string) (string, error) { 338 if reflectMap.Kind() != reflect.Map { 339 return "", nil 340 } 341 name := enumKey 342 if unquoted, err := strconv.Unquote(name); err == nil { 343 name = unquoted 344 } 345 for _, e := range reflectMap.MapKeys() { 346 if e.String() == name { 347 value := reflectMap.MapIndex(e) 348 if value.Type().Kind() == reflect.Bool { 349 return formatDefault(value) 350 } 351 } 352 } 353 // Assume that if the value isn't mentioned in the map, it defaults to 354 // the default value, false. 355 return formatDefault(reflect.ValueOf(false)) 356 } 357 358 // formatDefault formats the default value into a JSON-like string. 359 // VS Code exposes settings as JSON, so showing them as JSON is reasonable. 360 // TODO(rstambler): Reconsider this approach, as the VS Code Go generator now 361 // marshals to JSON. 362 func formatDefault(reflectField reflect.Value) (string, error) { 363 def := reflectField.Interface() 364 365 // Durations marshal as nanoseconds, but we want the stringy versions, 366 // e.g. "100ms". 367 if t, ok := def.(time.Duration); ok { 368 def = t.String() 369 } 370 defBytes, err := json.Marshal(def) 371 if err != nil { 372 return "", err 373 } 374 375 // Nil values format as "null" so print them as hardcoded empty values. 376 switch reflectField.Type().Kind() { 377 case reflect.Map: 378 if reflectField.IsNil() { 379 defBytes = []byte("{}") 380 } 381 case reflect.Slice: 382 if reflectField.IsNil() { 383 defBytes = []byte("[]") 384 } 385 } 386 return string(defBytes), err 387 } 388 389 // valueDoc transforms a docstring documenting an constant identifier to a 390 // docstring documenting its value. 391 // 392 // If doc is of the form "Foo is a bar", it returns '`"fooValue"` is a bar'. If 393 // doc is non-standard ("this value is a bar"), it returns '`"fooValue"`: this 394 // value is a bar'. 395 func valueDoc(name, value, doc string) string { 396 if doc == "" { 397 return "" 398 } 399 if strings.HasPrefix(doc, name) { 400 // docstring in standard form. Replace the subject with value. 401 return fmt.Sprintf("`%s`%s", value, doc[len(name):]) 402 } 403 return fmt.Sprintf("`%s`: %s", value, doc) 404 } 405 406 func loadCommands() ([]*settings.CommandJSON, error) { 407 var commands []*settings.CommandJSON 408 409 _, cmds, err := commandmeta.Load() 410 if err != nil { 411 return nil, err 412 } 413 // Parse the objects it contains. 414 for _, cmd := range cmds { 415 cmdjson := &settings.CommandJSON{ 416 Command: cmd.Name, 417 Title: cmd.Title, 418 Doc: cmd.Doc, 419 ArgDoc: argsDoc(cmd.Args), 420 } 421 if cmd.Result != nil { 422 cmdjson.ResultDoc = typeDoc(cmd.Result, 0) 423 } 424 commands = append(commands, cmdjson) 425 } 426 return commands, nil 427 } 428 429 func argsDoc(args []*commandmeta.Field) string { 430 var b strings.Builder 431 for i, arg := range args { 432 b.WriteString(typeDoc(arg, 0)) 433 if i != len(args)-1 { 434 b.WriteString(",\n") 435 } 436 } 437 return b.String() 438 } 439 440 func typeDoc(arg *commandmeta.Field, level int) string { 441 // Max level to expand struct fields. 442 const maxLevel = 3 443 if len(arg.Fields) > 0 { 444 if level < maxLevel { 445 return arg.FieldMod + structDoc(arg.Fields, level) 446 } 447 return "{ ... }" 448 } 449 under := arg.Type.Underlying() 450 switch u := under.(type) { 451 case *types.Slice: 452 return fmt.Sprintf("[]%s", u.Elem().Underlying().String()) 453 } 454 return types.TypeString(under, nil) 455 } 456 457 func structDoc(fields []*commandmeta.Field, level int) string { 458 var b strings.Builder 459 b.WriteString("{\n") 460 indent := strings.Repeat("\t", level) 461 for _, fld := range fields { 462 if fld.Doc != "" && level == 0 { 463 doclines := strings.Split(fld.Doc, "\n") 464 for _, line := range doclines { 465 text := "" 466 if line != "" { 467 text = " " + line 468 } 469 fmt.Fprintf(&b, "%s\t//%s\n", indent, text) 470 } 471 } 472 tag := strings.Split(fld.JSONTag, ",")[0] 473 if tag == "" { 474 tag = fld.Name 475 } 476 fmt.Fprintf(&b, "%s\t%q: %s,\n", indent, tag, typeDoc(fld, level+1)) 477 } 478 fmt.Fprintf(&b, "%s}", indent) 479 return b.String() 480 } 481 482 func loadLenses(commands []*settings.CommandJSON) []*settings.LensJSON { 483 all := map[command.Command]struct{}{} 484 for k := range golang.LensFuncs() { 485 all[k] = struct{}{} 486 } 487 for k := range mod.LensFuncs() { 488 if _, ok := all[k]; ok { 489 panic(fmt.Sprintf("duplicate lens %q", string(k))) 490 } 491 all[k] = struct{}{} 492 } 493 494 var lenses []*settings.LensJSON 495 496 for _, cmd := range commands { 497 if _, ok := all[command.Command(cmd.Command)]; ok { 498 lenses = append(lenses, &settings.LensJSON{ 499 Lens: cmd.Command, 500 Title: cmd.Title, 501 Doc: cmd.Doc, 502 }) 503 } 504 } 505 return lenses 506 } 507 508 func loadAnalyzers(m map[string]*settings.Analyzer) []*settings.AnalyzerJSON { 509 var sorted []string 510 for _, a := range m { 511 sorted = append(sorted, a.Analyzer.Name) 512 } 513 sort.Strings(sorted) 514 var json []*settings.AnalyzerJSON 515 for _, name := range sorted { 516 a := m[name] 517 json = append(json, &settings.AnalyzerJSON{ 518 Name: a.Analyzer.Name, 519 Doc: a.Analyzer.Doc, 520 URL: a.Analyzer.URL, 521 Default: a.Enabled, 522 }) 523 } 524 return json 525 } 526 527 func loadHints(m map[string]*golang.Hint) []*settings.HintJSON { 528 var sorted []string 529 for _, h := range m { 530 sorted = append(sorted, h.Name) 531 } 532 sort.Strings(sorted) 533 var json []*settings.HintJSON 534 for _, name := range sorted { 535 h := m[name] 536 json = append(json, &settings.HintJSON{ 537 Name: h.Name, 538 Doc: h.Doc, 539 }) 540 } 541 return json 542 } 543 544 func lowerFirst(x string) string { 545 if x == "" { 546 return x 547 } 548 return strings.ToLower(x[:1]) + x[1:] 549 } 550 551 func upperFirst(x string) string { 552 if x == "" { 553 return x 554 } 555 return strings.ToUpper(x[:1]) + x[1:] 556 } 557 558 func fileForPos(pkg *packages.Package, pos token.Pos) (*ast.File, error) { 559 fset := pkg.Fset 560 for _, f := range pkg.Syntax { 561 if safetoken.StartPosition(fset, f.Pos()).Filename == safetoken.StartPosition(fset, pos).Filename { 562 return f, nil 563 } 564 } 565 return nil, fmt.Errorf("no file for pos %v", pos) 566 } 567 568 func rewriteFile(file string, api *settings.APIJSON, write bool, rewrite func([]byte, *settings.APIJSON) ([]byte, error)) (bool, error) { 569 old, err := os.ReadFile(file) 570 if err != nil { 571 return false, err 572 } 573 574 new, err := rewrite(old, api) 575 if err != nil { 576 return false, fmt.Errorf("rewriting %q: %v", file, err) 577 } 578 579 if !write { 580 return bytes.Equal(old, new), nil 581 } 582 583 if err := os.WriteFile(file, new, 0); err != nil { 584 return false, err 585 } 586 587 return true, nil 588 } 589 590 func rewriteAPI(_ []byte, api *settings.APIJSON) ([]byte, error) { 591 var buf bytes.Buffer 592 fmt.Fprintf(&buf, "// Code generated by \"golang.org/x/tools/gopls/doc/generate\"; DO NOT EDIT.\n\npackage settings\n\nvar GeneratedAPIJSON = ") 593 if err := printsrc.NewPrinter("golang.org/x/tools/gopls/internal/settings").Fprint(&buf, api); err != nil { 594 return nil, err 595 } 596 return format.Source(buf.Bytes()) 597 } 598 599 type optionsGroup struct { 600 title string 601 final string 602 level int 603 options []*settings.OptionJSON 604 } 605 606 func rewriteSettings(doc []byte, api *settings.APIJSON) ([]byte, error) { 607 result := doc 608 for category, opts := range api.Options { 609 groups := collectGroups(opts) 610 611 // First, print a table of contents. 612 section := bytes.NewBuffer(nil) 613 fmt.Fprintln(section, "") 614 for _, h := range groups { 615 writeBullet(section, h.final, h.level) 616 } 617 fmt.Fprintln(section, "") 618 619 // Currently, the settings document has a title and a subtitle, so 620 // start at level 3 for a header beginning with "###". 621 baseLevel := 3 622 for _, h := range groups { 623 level := baseLevel + h.level 624 writeTitle(section, h.final, level) 625 for _, opt := range h.options { 626 header := strMultiply("#", level+1) 627 fmt.Fprintf(section, "%s ", header) 628 opt.Write(section) 629 } 630 } 631 var err error 632 result, err = replaceSection(result, category, section.Bytes()) 633 if err != nil { 634 return nil, err 635 } 636 } 637 638 section := bytes.NewBuffer(nil) 639 for _, lens := range api.Lenses { 640 fmt.Fprintf(section, "### **%v**\n\nIdentifier: `%v`\n\n%v\n", lens.Title, lens.Lens, lens.Doc) 641 } 642 return replaceSection(result, "Lenses", section.Bytes()) 643 } 644 645 func collectGroups(opts []*settings.OptionJSON) []optionsGroup { 646 optsByHierarchy := map[string][]*settings.OptionJSON{} 647 for _, opt := range opts { 648 optsByHierarchy[opt.Hierarchy] = append(optsByHierarchy[opt.Hierarchy], opt) 649 } 650 651 // As a hack, assume that uncategorized items are less important to 652 // users and force the empty string to the end of the list. 653 var containsEmpty bool 654 var sorted []string 655 for h := range optsByHierarchy { 656 if h == "" { 657 containsEmpty = true 658 continue 659 } 660 sorted = append(sorted, h) 661 } 662 sort.Strings(sorted) 663 if containsEmpty { 664 sorted = append(sorted, "") 665 } 666 var groups []optionsGroup 667 baseLevel := 0 668 for _, h := range sorted { 669 split := strings.SplitAfter(h, ".") 670 last := split[len(split)-1] 671 // Hack to capitalize all of UI. 672 if last == "ui" { 673 last = "UI" 674 } 675 // A hierarchy may look like "ui.formatting". If "ui" has no 676 // options of its own, it may not be added to the map, but it 677 // still needs a heading. 678 components := strings.Split(h, ".") 679 for i := 1; i < len(components); i++ { 680 parent := strings.Join(components[0:i], ".") 681 if _, ok := optsByHierarchy[parent]; !ok { 682 groups = append(groups, optionsGroup{ 683 title: parent, 684 final: last, 685 level: baseLevel + i, 686 }) 687 } 688 } 689 groups = append(groups, optionsGroup{ 690 title: h, 691 final: last, 692 level: baseLevel + strings.Count(h, "."), 693 options: optsByHierarchy[h], 694 }) 695 } 696 return groups 697 } 698 699 func hardcodedEnumKeys(name string) bool { 700 return name == "analyses" || name == "codelenses" 701 } 702 703 func writeBullet(w io.Writer, title string, level int) { 704 if title == "" { 705 return 706 } 707 // Capitalize the first letter of each title. 708 prefix := strMultiply(" ", level) 709 fmt.Fprintf(w, "%s* [%s](#%s)\n", prefix, capitalize(title), strings.ToLower(title)) 710 } 711 712 func writeTitle(w io.Writer, title string, level int) { 713 if title == "" { 714 return 715 } 716 // Capitalize the first letter of each title. 717 fmt.Fprintf(w, "%s %s\n\n", strMultiply("#", level), capitalize(title)) 718 } 719 720 func capitalize(s string) string { 721 return string(unicode.ToUpper(rune(s[0]))) + s[1:] 722 } 723 724 func strMultiply(str string, count int) string { 725 var result string 726 for i := 0; i < count; i++ { 727 result += str 728 } 729 return result 730 } 731 732 func rewriteCommands(doc []byte, api *settings.APIJSON) ([]byte, error) { 733 section := bytes.NewBuffer(nil) 734 for _, command := range api.Commands { 735 command.Write(section) 736 } 737 return replaceSection(doc, "Commands", section.Bytes()) 738 } 739 740 func rewriteAnalyzers(doc []byte, api *settings.APIJSON) ([]byte, error) { 741 section := bytes.NewBuffer(nil) 742 for _, analyzer := range api.Analyzers { 743 fmt.Fprintf(section, "## **%v**\n\n", analyzer.Name) 744 fmt.Fprintf(section, "%s: %s\n\n", analyzer.Name, analyzer.Doc) 745 if analyzer.URL != "" { 746 fmt.Fprintf(section, "[Full documentation](%s)\n\n", analyzer.URL) 747 } 748 switch analyzer.Default { 749 case true: 750 fmt.Fprintf(section, "**Enabled by default.**\n\n") 751 case false: 752 fmt.Fprintf(section, "**Disabled by default. Enable it by setting `\"analyses\": {\"%s\": true}`.**\n\n", analyzer.Name) 753 } 754 } 755 return replaceSection(doc, "Analyzers", section.Bytes()) 756 } 757 758 func rewriteInlayHints(doc []byte, api *settings.APIJSON) ([]byte, error) { 759 section := bytes.NewBuffer(nil) 760 for _, hint := range api.Hints { 761 fmt.Fprintf(section, "## **%v**\n\n", hint.Name) 762 fmt.Fprintf(section, "%s\n\n", hint.Doc) 763 switch hint.Default { 764 case true: 765 fmt.Fprintf(section, "**Enabled by default.**\n\n") 766 case false: 767 fmt.Fprintf(section, "**Disabled by default. Enable it by setting `\"hints\": {\"%s\": true}`.**\n\n", hint.Name) 768 } 769 } 770 return replaceSection(doc, "Hints", section.Bytes()) 771 } 772 773 func replaceSection(doc []byte, sectionName string, replacement []byte) ([]byte, error) { 774 re := regexp.MustCompile(fmt.Sprintf(`(?s)<!-- BEGIN %v.* -->\n(.*?)<!-- END %v.* -->`, sectionName, sectionName)) 775 idx := re.FindSubmatchIndex(doc) 776 if idx == nil { 777 return nil, fmt.Errorf("could not find section %q", sectionName) 778 } 779 result := append([]byte(nil), doc[:idx[2]]...) 780 result = append(result, replacement...) 781 result = append(result, doc[idx[3]:]...) 782 return result, nil 783 }