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