--- /dev/null
+// Copyright 2020 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Command genapijson generates JSON describing gopls' external-facing API,
+// including user settings and commands.
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "os"
+ "reflect"
+ "strings"
+ "time"
+
+ "golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/internal/lsp/mod"
+ "golang.org/x/tools/internal/lsp/source"
+)
+
+var (
+ output = flag.String("output", "", "output file")
+)
+
+func main() {
+ flag.Parse()
+ if err := doMain(); err != nil {
+ fmt.Fprintf(os.Stderr, "Generation failed: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+func doMain() error {
+ out := os.Stdout
+ if *output != "" {
+ var err error
+ out, err = os.OpenFile(*output, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+ }
+
+ content, err := generate()
+ if err != nil {
+ return err
+ }
+ if _, err := out.Write(content); err != nil {
+ return err
+ }
+
+ return out.Close()
+}
+
+func generate() ([]byte, error) {
+ pkgs, err := packages.Load(
+ &packages.Config{
+ Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedDeps,
+ },
+ "golang.org/x/tools/internal/lsp/source",
+ )
+ if err != nil {
+ return nil, err
+ }
+ pkg := pkgs[0]
+
+ api := &source.APIJSON{
+ Options: map[string][]*source.OptionJSON{},
+ }
+ defaults := source.DefaultOptions()
+ for _, cat := range []reflect.Value{
+ reflect.ValueOf(defaults.DebuggingOptions),
+ reflect.ValueOf(defaults.UserOptions),
+ reflect.ValueOf(defaults.ExperimentalOptions),
+ } {
+ opts, err := loadOptions(cat, pkg)
+ if err != nil {
+ return nil, err
+ }
+ catName := strings.TrimSuffix(cat.Type().Name(), "Options")
+ api.Options[catName] = opts
+ }
+
+ api.Commands, err = loadCommands(pkg)
+ if err != nil {
+ return nil, err
+ }
+ api.Lenses = loadLenses(api.Commands)
+
+ // Transform the internal command name to the external command name.
+ for _, c := range api.Commands {
+ c.Command = source.CommandPrefix + c.Command
+ }
+
+ marshaled, err := json.Marshal(api)
+ if err != nil {
+ return nil, err
+ }
+ buf := bytes.NewBuffer(nil)
+ fmt.Fprintf(buf, "// Code generated by \"golang.org/x/tools/internal/lsp/source/genapijson\"; DO NOT EDIT.\n\npackage source\n\nconst GeneratedAPIJSON = %q\n", string(marshaled))
+ return buf.Bytes(), nil
+}
+
+func loadOptions(category reflect.Value, pkg *packages.Package) ([]*source.OptionJSON, error) {
+ // Find the type information and ast.File corresponding to the category.
+ optsType := pkg.Types.Scope().Lookup(category.Type().Name())
+ if optsType == nil {
+ return nil, fmt.Errorf("could not find %v in scope %v", category.Type().Name(), pkg.Types.Scope())
+ }
+
+ file, err := fileForPos(pkg, optsType.Pos())
+ if err != nil {
+ return nil, err
+ }
+
+ enums, err := loadEnums(pkg)
+ if err != nil {
+ return nil, err
+ }
+
+ var opts []*source.OptionJSON
+ optsStruct := optsType.Type().Underlying().(*types.Struct)
+ for i := 0; i < optsStruct.NumFields(); i++ {
+ // The types field gives us the type.
+ typesField := optsStruct.Field(i)
+ path, _ := astutil.PathEnclosingInterval(file, typesField.Pos(), typesField.Pos())
+ if len(path) < 2 {
+ return nil, fmt.Errorf("could not find AST node for field %v", typesField)
+ }
+ // The AST field gives us the doc.
+ astField, ok := path[1].(*ast.Field)
+ if !ok {
+ return nil, fmt.Errorf("unexpected AST path %v", path)
+ }
+
+ // The reflect field gives us the default value.
+ reflectField := category.FieldByName(typesField.Name())
+ if !reflectField.IsValid() {
+ return nil, fmt.Errorf("could not find reflect field for %v", typesField.Name())
+ }
+
+ // Format the default value. VSCode exposes settings as JSON, so showing them as JSON is reasonable.
+ def := reflectField.Interface()
+ // Durations marshal as nanoseconds, but we want the stringy versions, e.g. "100ms".
+ if t, ok := def.(time.Duration); ok {
+ def = t.String()
+ }
+ defBytes, err := json.Marshal(def)
+ if err != nil {
+ return nil, err
+ }
+
+ // Nil values format as "null" so print them as hardcoded empty values.
+ switch reflectField.Type().Kind() {
+ case reflect.Map:
+ if reflectField.IsNil() {
+ defBytes = []byte("{}")
+ }
+ case reflect.Slice:
+ if reflectField.IsNil() {
+ defBytes = []byte("[]")
+ }
+ }
+
+ typ := typesField.Type().String()
+ if _, ok := enums[typesField.Type()]; ok {
+ typ = "enum"
+ }
+
+ opts = append(opts, &source.OptionJSON{
+ Name: lowerFirst(typesField.Name()),
+ Type: typ,
+ Doc: lowerFirst(astField.Doc.Text()),
+ Default: string(defBytes),
+ EnumValues: enums[typesField.Type()],
+ })
+ }
+ return opts, nil
+}
+
+func loadEnums(pkg *packages.Package) (map[types.Type][]source.EnumValue, error) {
+ enums := map[types.Type][]source.EnumValue{}
+ for _, name := range pkg.Types.Scope().Names() {
+ obj := pkg.Types.Scope().Lookup(name)
+ cnst, ok := obj.(*types.Const)
+ if !ok {
+ continue
+ }
+ f, err := fileForPos(pkg, cnst.Pos())
+ if err != nil {
+ return nil, fmt.Errorf("finding file for %q: %v", cnst.Name(), err)
+ }
+ path, _ := astutil.PathEnclosingInterval(f, cnst.Pos(), cnst.Pos())
+ spec := path[1].(*ast.ValueSpec)
+ value := cnst.Val().ExactString()
+ doc := valueDoc(cnst.Name(), value, spec.Doc.Text())
+ v := source.EnumValue{
+ Value: value,
+ Doc: doc,
+ }
+ enums[obj.Type()] = append(enums[obj.Type()], v)
+ }
+ return enums, nil
+}
+
+// valueDoc transforms a docstring documenting an constant identifier to a
+// docstring documenting its value.
+//
+// If doc is of the form "Foo is a bar", it returns '`"fooValue"` is a bar'. If
+// doc is non-standard ("this value is a bar"), it returns '`"fooValue"`: this
+// value is a bar'.
+func valueDoc(name, value, doc string) string {
+ if doc == "" {
+ return ""
+ }
+ if strings.HasPrefix(doc, name) {
+ // docstring in standard form. Replace the subject with value.
+ return fmt.Sprintf("`%s`%s", value, doc[len(name):])
+ }
+ return fmt.Sprintf("`%s`: %s", value, doc)
+}
+
+func loadCommands(pkg *packages.Package) ([]*source.CommandJSON, error) {
+ // The code that defines commands is much more complicated than the
+ // code that defines options, so reading comments for the Doc is very
+ // fragile. If this causes problems, we should switch to a dynamic
+ // approach and put the doc in the Commands struct rather than reading
+ // from the source code.
+
+ // Find the Commands slice.
+ typesSlice := pkg.Types.Scope().Lookup("Commands")
+ f, err := fileForPos(pkg, typesSlice.Pos())
+ if err != nil {
+ return nil, err
+ }
+ path, _ := astutil.PathEnclosingInterval(f, typesSlice.Pos(), typesSlice.Pos())
+ vspec := path[1].(*ast.ValueSpec)
+ var astSlice *ast.CompositeLit
+ for i, name := range vspec.Names {
+ if name.Name == "Commands" {
+ astSlice = vspec.Values[i].(*ast.CompositeLit)
+ }
+ }
+
+ var commands []*source.CommandJSON
+
+ // Parse the objects it contains.
+ for _, elt := range astSlice.Elts {
+ // Find the composite literal of the Command.
+ typesCommand := pkg.TypesInfo.ObjectOf(elt.(*ast.Ident))
+ path, _ := astutil.PathEnclosingInterval(f, typesCommand.Pos(), typesCommand.Pos())
+ vspec := path[1].(*ast.ValueSpec)
+
+ var astCommand ast.Expr
+ for i, name := range vspec.Names {
+ if name.Name == typesCommand.Name() {
+ astCommand = vspec.Values[i]
+ }
+ }
+
+ // Read the Name and Title fields of the literal.
+ var name, title string
+ ast.Inspect(astCommand, func(n ast.Node) bool {
+ kv, ok := n.(*ast.KeyValueExpr)
+ if ok {
+ k := kv.Key.(*ast.Ident).Name
+ switch k {
+ case "Name":
+ name = strings.Trim(kv.Value.(*ast.BasicLit).Value, `"`)
+ case "Title":
+ title = strings.Trim(kv.Value.(*ast.BasicLit).Value, `"`)
+ }
+ }
+ return true
+ })
+
+ if title == "" {
+ title = name
+ }
+
+ // Conventionally, the doc starts with the name of the variable.
+ // Replace it with the name of the command.
+ doc := vspec.Doc.Text()
+ doc = strings.Replace(doc, typesCommand.Name(), name, 1)
+
+ commands = append(commands, &source.CommandJSON{
+ Command: name,
+ Title: title,
+ Doc: doc,
+ })
+ }
+ return commands, nil
+}
+
+func loadLenses(commands []*source.CommandJSON) []*source.LensJSON {
+ lensNames := map[string]struct{}{}
+ for k := range source.LensFuncs() {
+ lensNames[k] = struct{}{}
+ }
+ for k := range mod.LensFuncs() {
+ lensNames[k] = struct{}{}
+ }
+
+ var lenses []*source.LensJSON
+
+ for _, cmd := range commands {
+ if _, ok := lensNames[cmd.Command]; ok {
+ lenses = append(lenses, &source.LensJSON{
+ Lens: cmd.Command,
+ Title: cmd.Title,
+ Doc: cmd.Doc,
+ })
+ }
+ }
+ return lenses
+}
+
+func lowerFirst(x string) string {
+ if x == "" {
+ return x
+ }
+ return strings.ToLower(x[:1]) + x[1:]
+}
+
+func fileForPos(pkg *packages.Package, pos token.Pos) (*ast.File, error) {
+ fset := pkg.Fset
+ for _, f := range pkg.Syntax {
+ if fset.Position(f.Pos()).Filename == fset.Position(pos).Filename {
+ return f, nil
+ }
+ }
+ return nil, fmt.Errorf("no file for pos %v", pos)
+}