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