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:
Heschi Kreinick 2020-12-01 12:19:44 -05:00
Родитель fa6651ede5
Коммит a1a1cbeaa5
8 изменённых файлов: 706 добавлений и 383 удалений

Просмотреть файл

@ -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.")
}
}