// 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) }