зеркало из https://github.com/golang/tools.git
gopls/doc: generate JSON too, store as composite literal
Combine the generation of the API json with the generation of the documentation. This cuts out a step from the generation process, and allows us to depend on third-party modules. Use litter to print a composite literal instead of JSON text, which will diff and merge much better. The only real drawback is that you have to "go run" from the gopls module to avoid adding the extra deps to tools. Sorry about the copy and paste; there's relatively little actual code change, just a bit in doMain and rewriteAPI. Change-Id: Iac936d31b7e52651b3b33f27497cfdbd133f1e76 Reviewed-on: https://go-review.googlesource.com/c/tools/+/274373 Trust: Heschi Kreinick <heschi@google.com> Run-TryBot: Heschi Kreinick <heschi@google.com> gopls-CI: kokoro <noreply+kokoro@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
Родитель
fa6651ede5
Коммит
a1a1cbeaa5
|
@ -2,35 +2,49 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Command generate updates settings.md from the UserOptions struct.
|
||||
// Command generate creates API (settings, etc) documentation in JSON and
|
||||
// Markdown for machine and human consumption.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/format"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sanity-io/litter"
|
||||
"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"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if _, err := doMain(".", true); err != nil {
|
||||
if _, err := doMain("..", true); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Generation failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func doMain(baseDir string, write bool) (bool, error) {
|
||||
api := &source.APIJSON{}
|
||||
if err := json.Unmarshal([]byte(source.GeneratedAPIJSON), api); err != nil {
|
||||
api, err := loadAPI()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if ok, err := rewriteFile(filepath.Join(baseDir, "internal/lsp/source/api_json.go"), api, write, rewriteAPI); !ok || err != nil {
|
||||
return ok, err
|
||||
}
|
||||
if ok, err := rewriteFile(filepath.Join(baseDir, "gopls/doc/settings.md"), api, write, rewriteSettings); !ok || err != nil {
|
||||
return ok, err
|
||||
}
|
||||
|
@ -41,28 +55,320 @@ func doMain(baseDir string, write bool) (bool, error) {
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func loadAPI() (*source.APIJSON, 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
|
||||
}
|
||||
return api, 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)
|
||||
}
|
||||
|
||||
func rewriteFile(file string, api *source.APIJSON, write bool, rewrite func([]byte, *source.APIJSON) ([]byte, error)) (bool, error) {
|
||||
doc, err := ioutil.ReadFile(file)
|
||||
old, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
content, err := rewrite(doc, api)
|
||||
new, err := rewrite(old, api)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("rewriting %q: %v", file, err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(doc, content) && !write {
|
||||
return false, nil
|
||||
if !write {
|
||||
return bytes.Equal(old, new), nil
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(file, content, 0); err != nil {
|
||||
if err := ioutil.WriteFile(file, new, 0); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func rewriteAPI(input []byte, api *source.APIJSON) ([]byte, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
apiStr := litter.Options{
|
||||
HomePackage: "source",
|
||||
}.Sdump(api)
|
||||
// Massive hack: filter out redundant types from the composite literal.
|
||||
apiStr = strings.ReplaceAll(apiStr, "&OptionJSON", "")
|
||||
apiStr = strings.ReplaceAll(apiStr, ": []*OptionJSON", ":")
|
||||
apiStr = strings.ReplaceAll(apiStr, "&CommandJSON", "")
|
||||
apiStr = strings.ReplaceAll(apiStr, "&LensJSON", "")
|
||||
apiStr = strings.ReplaceAll(apiStr, " EnumValue{", "{")
|
||||
apiBytes, err := format.Source([]byte(apiStr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Fprintf(buf, "// Code generated by \"golang.org/x/tools/gopls/doc/generate\"; DO NOT EDIT.\n\npackage source\n\nvar GeneratedAPIJSON = %s\n", apiBytes)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
var parBreakRE = regexp.MustCompile("\n{2,}")
|
||||
|
||||
func rewriteSettings(doc []byte, api *source.APIJSON) ([]byte, error) {
|
||||
|
|
|
@ -18,6 +18,6 @@ func TestGenerated(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
t.Error("documentation needs updating. run: `go run gopls/doc/generate.go` from the root of tools.")
|
||||
t.Error("documentation needs updating. run: `go run doc/generate.go` from the gopls module.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ module golang.org/x/tools/gopls
|
|||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/sanity-io/litter v1.3.0
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
golang.org/x/tools v0.0.0-20201021214918-23787c007979
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
@ -12,14 +13,18 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
|||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/sanity-io/litter v1.3.0 h1:5ZO+weUsqdSWMUng5JnpkW/Oz8iTXiIdeumhQr1sSjs=
|
||||
github.com/sanity-io/litter v1.3.0/go.mod h1:5Z71SvaYy5kcGtyglXOC9rrUi3c1E8CamFWjQsazTh0=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
|
|
@ -7,6 +7,7 @@ package cmd
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
@ -94,6 +95,10 @@ func (sj *apiJSON) DetailedHelp(f *flag.FlagSet) {
|
|||
}
|
||||
|
||||
func (sj *apiJSON) Run(ctx context.Context, args ...string) error {
|
||||
fmt.Fprintf(os.Stdout, source.GeneratedAPIJSON)
|
||||
js, err := json.MarshalIndent(source.GeneratedAPIJSON, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprint(os.Stdout, string(js))
|
||||
return nil
|
||||
}
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -1,340 +0,0 @@
|
|||
// 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)
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code.
|
||||
testenv.NeedsGoPackages(t)
|
||||
|
||||
got, err := ioutil.ReadFile("../api_json.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want, err := generate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Error("api_json is out of sync. Run `go generate ./internal/lsp/source` from the root of tools.")
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче