blob: 3156f4f4e93b232f6020fe638434bfa7d6b1a64b [file] [log] [blame]
// 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)
}