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