| // 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" |
| |
| "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) |
| 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 := loadEnums(pkg) |
| |
| 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. |
| // Nil values format as "null" so print them as hardcoded empty values. |
| def := reflectField.Interface() |
| defBytes, err := json.Marshal(def) |
| if err != nil { |
| return nil, err |
| } |
| |
| 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][]string { |
| enums := map[types.Type][]string{} |
| for _, name := range pkg.Types.Scope().Names() { |
| obj := pkg.Types.Scope().Lookup(name) |
| cnst, ok := obj.(*types.Const) |
| if !ok { |
| continue |
| } |
| enums[obj.Type()] = append(enums[obj.Type()], cnst.Val().ExactString()) |
| } |
| return enums |
| } |
| |
| 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) |
| } |