golang.org/x/tools/gopls@v0.15.3/doc/generate.go (about)

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