1// Copyright 2020 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Command genapijson generates JSON describing gopls' external-facing API,
6// including user settings and commands.
7package main
8
9import (
10	"bytes"
11	"encoding/json"
12	"flag"
13	"fmt"
14	"go/ast"
15	"go/token"
16	"go/types"
17	"os"
18	"reflect"
19	"strings"
20	"time"
21
22	"golang.org/x/tools/go/ast/astutil"
23	"golang.org/x/tools/go/packages"
24	"golang.org/x/tools/internal/lsp/mod"
25	"golang.org/x/tools/internal/lsp/source"
26)
27
28var (
29	output = flag.String("output", "", "output file")
30)
31
32func main() {
33	flag.Parse()
34	if err := doMain(); err != nil {
35		fmt.Fprintf(os.Stderr, "Generation failed: %v\n", err)
36		os.Exit(1)
37	}
38}
39
40func doMain() error {
41	out := os.Stdout
42	if *output != "" {
43		var err error
44		out, err = os.OpenFile(*output, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777)
45		if err != nil {
46			return err
47		}
48		defer out.Close()
49	}
50
51	content, err := generate()
52	if err != nil {
53		return err
54	}
55	if _, err := out.Write(content); err != nil {
56		return err
57	}
58
59	return out.Close()
60}
61
62func generate() ([]byte, error) {
63	pkgs, err := packages.Load(
64		&packages.Config{
65			Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedDeps,
66		},
67		"golang.org/x/tools/internal/lsp/source",
68	)
69	if err != nil {
70		return nil, err
71	}
72	pkg := pkgs[0]
73
74	api := &source.APIJSON{
75		Options: map[string][]*source.OptionJSON{},
76	}
77	defaults := source.DefaultOptions()
78	for _, cat := range []reflect.Value{
79		reflect.ValueOf(defaults.DebuggingOptions),
80		reflect.ValueOf(defaults.UserOptions),
81		reflect.ValueOf(defaults.ExperimentalOptions),
82	} {
83		opts, err := loadOptions(cat, pkg)
84		if err != nil {
85			return nil, err
86		}
87		catName := strings.TrimSuffix(cat.Type().Name(), "Options")
88		api.Options[catName] = opts
89	}
90
91	api.Commands, err = loadCommands(pkg)
92	if err != nil {
93		return nil, err
94	}
95	api.Lenses = loadLenses(api.Commands)
96	// Command names need to be prefixed with "gopls_".
97	// TODO: figure out a better way.
98	for _, c := range api.Commands {
99		c.Command = "gopls_" + c.Command
100	}
101
102	marshaled, err := json.Marshal(api)
103	if err != nil {
104		return nil, err
105	}
106	buf := bytes.NewBuffer(nil)
107	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))
108	return buf.Bytes(), nil
109}
110
111func loadOptions(category reflect.Value, pkg *packages.Package) ([]*source.OptionJSON, error) {
112	// Find the type information and ast.File corresponding to the category.
113	optsType := pkg.Types.Scope().Lookup(category.Type().Name())
114	if optsType == nil {
115		return nil, fmt.Errorf("could not find %v in scope %v", category.Type().Name(), pkg.Types.Scope())
116	}
117
118	file, err := fileForPos(pkg, optsType.Pos())
119	if err != nil {
120		return nil, err
121	}
122
123	enums, err := loadEnums(pkg)
124	if err != nil {
125		return nil, err
126	}
127
128	var opts []*source.OptionJSON
129	optsStruct := optsType.Type().Underlying().(*types.Struct)
130	for i := 0; i < optsStruct.NumFields(); i++ {
131		// The types field gives us the type.
132		typesField := optsStruct.Field(i)
133		path, _ := astutil.PathEnclosingInterval(file, typesField.Pos(), typesField.Pos())
134		if len(path) < 2 {
135			return nil, fmt.Errorf("could not find AST node for field %v", typesField)
136		}
137		// The AST field gives us the doc.
138		astField, ok := path[1].(*ast.Field)
139		if !ok {
140			return nil, fmt.Errorf("unexpected AST path %v", path)
141		}
142
143		// The reflect field gives us the default value.
144		reflectField := category.FieldByName(typesField.Name())
145		if !reflectField.IsValid() {
146			return nil, fmt.Errorf("could not find reflect field for %v", typesField.Name())
147		}
148
149		// Format the default value. VSCode exposes settings as JSON, so showing them as JSON is reasonable.
150		def := reflectField.Interface()
151		// Durations marshal as nanoseconds, but we want the stringy versions, e.g. "100ms".
152		if t, ok := def.(time.Duration); ok {
153			def = t.String()
154		}
155		defBytes, err := json.Marshal(def)
156		if err != nil {
157			return nil, err
158		}
159
160		// Nil values format as "null" so print them as hardcoded empty values.
161		switch reflectField.Type().Kind() {
162		case reflect.Map:
163			if reflectField.IsNil() {
164				defBytes = []byte("{}")
165			}
166		case reflect.Slice:
167			if reflectField.IsNil() {
168				defBytes = []byte("[]")
169			}
170		}
171
172		typ := typesField.Type().String()
173		if _, ok := enums[typesField.Type()]; ok {
174			typ = "enum"
175		}
176
177		opts = append(opts, &source.OptionJSON{
178			Name:       lowerFirst(typesField.Name()),
179			Type:       typ,
180			Doc:        lowerFirst(astField.Doc.Text()),
181			Default:    string(defBytes),
182			EnumValues: enums[typesField.Type()],
183		})
184	}
185	return opts, nil
186}
187
188func loadEnums(pkg *packages.Package) (map[types.Type][]source.EnumValue, error) {
189	enums := map[types.Type][]source.EnumValue{}
190	for _, name := range pkg.Types.Scope().Names() {
191		obj := pkg.Types.Scope().Lookup(name)
192		cnst, ok := obj.(*types.Const)
193		if !ok {
194			continue
195		}
196		f, err := fileForPos(pkg, cnst.Pos())
197		if err != nil {
198			return nil, fmt.Errorf("finding file for %q: %v", cnst.Name(), err)
199		}
200		path, _ := astutil.PathEnclosingInterval(f, cnst.Pos(), cnst.Pos())
201		spec := path[1].(*ast.ValueSpec)
202		value := cnst.Val().ExactString()
203		doc := valueDoc(cnst.Name(), value, spec.Doc.Text())
204		v := source.EnumValue{
205			Value: value,
206			Doc:   doc,
207		}
208		enums[obj.Type()] = append(enums[obj.Type()], v)
209	}
210	return enums, nil
211}
212
213// valueDoc transforms a docstring documenting an constant identifier to a
214// docstring documenting its value.
215//
216// If doc is of the form "Foo is a bar", it returns '`"fooValue"` is a bar'. If
217// doc is non-standard ("this value is a bar"), it returns '`"fooValue"`: this
218// value is a bar'.
219func valueDoc(name, value, doc string) string {
220	if doc == "" {
221		return ""
222	}
223	if strings.HasPrefix(doc, name) {
224		// docstring in standard form. Replace the subject with value.
225		return fmt.Sprintf("`%s`%s", value, doc[len(name):])
226	}
227	return fmt.Sprintf("`%s`: %s", value, doc)
228}
229
230func loadCommands(pkg *packages.Package) ([]*source.CommandJSON, error) {
231	// The code that defines commands is much more complicated than the
232	// code that defines options, so reading comments for the Doc is very
233	// fragile. If this causes problems, we should switch to a dynamic
234	// approach and put the doc in the Commands struct rather than reading
235	// from the source code.
236
237	// Find the Commands slice.
238	typesSlice := pkg.Types.Scope().Lookup("Commands")
239	f, err := fileForPos(pkg, typesSlice.Pos())
240	if err != nil {
241		return nil, err
242	}
243	path, _ := astutil.PathEnclosingInterval(f, typesSlice.Pos(), typesSlice.Pos())
244	vspec := path[1].(*ast.ValueSpec)
245	var astSlice *ast.CompositeLit
246	for i, name := range vspec.Names {
247		if name.Name == "Commands" {
248			astSlice = vspec.Values[i].(*ast.CompositeLit)
249		}
250	}
251
252	var commands []*source.CommandJSON
253
254	// Parse the objects it contains.
255	for _, elt := range astSlice.Elts {
256		// Find the composite literal of the Command.
257		typesCommand := pkg.TypesInfo.ObjectOf(elt.(*ast.Ident))
258		path, _ := astutil.PathEnclosingInterval(f, typesCommand.Pos(), typesCommand.Pos())
259		vspec := path[1].(*ast.ValueSpec)
260
261		var astCommand ast.Expr
262		for i, name := range vspec.Names {
263			if name.Name == typesCommand.Name() {
264				astCommand = vspec.Values[i]
265			}
266		}
267
268		// Read the Name and Title fields of the literal.
269		var name, title string
270		ast.Inspect(astCommand, func(n ast.Node) bool {
271			kv, ok := n.(*ast.KeyValueExpr)
272			if ok {
273				k := kv.Key.(*ast.Ident).Name
274				switch k {
275				case "Name":
276					name = strings.Trim(kv.Value.(*ast.BasicLit).Value, `"`)
277				case "Title":
278					title = strings.Trim(kv.Value.(*ast.BasicLit).Value, `"`)
279				}
280			}
281			return true
282		})
283
284		if title == "" {
285			title = name
286		}
287
288		// Conventionally, the doc starts with the name of the variable.
289		// Replace it with the name of the command.
290		doc := vspec.Doc.Text()
291		doc = strings.Replace(doc, typesCommand.Name(), name, 1)
292
293		commands = append(commands, &source.CommandJSON{
294			Command: name,
295			Title:   title,
296			Doc:     doc,
297		})
298	}
299	return commands, nil
300}
301
302func loadLenses(commands []*source.CommandJSON) []*source.LensJSON {
303	lensNames := map[string]struct{}{}
304	for k := range source.LensFuncs() {
305		lensNames[k] = struct{}{}
306	}
307	for k := range mod.LensFuncs() {
308		lensNames[k] = struct{}{}
309	}
310
311	var lenses []*source.LensJSON
312
313	for _, cmd := range commands {
314		if _, ok := lensNames[cmd.Command]; ok {
315			lenses = append(lenses, &source.LensJSON{
316				Lens:  cmd.Command,
317				Title: cmd.Title,
318				Doc:   cmd.Doc,
319			})
320		}
321	}
322	return lenses
323}
324
325func lowerFirst(x string) string {
326	if x == "" {
327		return x
328	}
329	return strings.ToLower(x[:1]) + x[1:]
330}
331
332func fileForPos(pkg *packages.Package, pos token.Pos) (*ast.File, error) {
333	fset := pkg.Fset
334	for _, f := range pkg.Syntax {
335		if fset.Position(f.Pos()).Filename == fset.Position(pos).Filename {
336			return f, nil
337		}
338	}
339	return nil, fmt.Errorf("no file for pos %v", pos)
340}
341