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