mirror of https://github.com/golang/go.git
341 lines
8.8 KiB
Go
341 lines
8.8 KiB
Go
// 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)
|
|
}
|