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.
5 // Command genapijson generates JSON describing gopls' external-facing API,
6 // including user settings and commands.
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"
29 output = flag.String("output", "", "output file")
34 if err := doMain(); err != nil {
35 fmt.Fprintf(os.Stderr, "Generation failed: %v\n", err)
44 out, err = os.OpenFile(*output, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777)
51 content, err := generate()
55 if _, err := out.Write(content); err != nil {
62 func generate() ([]byte, error) {
63 pkgs, err := packages.Load(
65 Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedDeps,
67 "golang.org/x/tools/internal/lsp/source",
74 api := &source.APIJSON{
75 Options: map[string][]*source.OptionJSON{},
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),
83 opts, err := loadOptions(cat, pkg)
87 catName := strings.TrimSuffix(cat.Type().Name(), "Options")
88 api.Options[catName] = opts
91 api.Commands, err = loadCommands(pkg)
95 api.Lenses = loadLenses(api.Commands)
97 // Transform the internal command name to the external command name.
98 for _, c := range api.Commands {
99 c.Command = source.CommandPrefix + c.Command
102 marshaled, err := json.Marshal(api)
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
111 func 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())
115 return nil, fmt.Errorf("could not find %v in scope %v", category.Type().Name(), pkg.Types.Scope())
118 file, err := fileForPos(pkg, optsType.Pos())
123 enums, err := loadEnums(pkg)
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())
135 return nil, fmt.Errorf("could not find AST node for field %v", typesField)
137 // The AST field gives us the doc.
138 astField, ok := path[1].(*ast.Field)
140 return nil, fmt.Errorf("unexpected AST path %v", path)
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())
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 {
155 defBytes, err := json.Marshal(def)
160 // Nil values format as "null" so print them as hardcoded empty values.
161 switch reflectField.Type().Kind() {
163 if reflectField.IsNil() {
164 defBytes = []byte("{}")
167 if reflectField.IsNil() {
168 defBytes = []byte("[]")
172 typ := typesField.Type().String()
173 if _, ok := enums[typesField.Type()]; ok {
177 opts = append(opts, &source.OptionJSON{
178 Name: lowerFirst(typesField.Name()),
180 Doc: lowerFirst(astField.Doc.Text()),
181 Default: string(defBytes),
182 EnumValues: enums[typesField.Type()],
188 func 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)
196 f, err := fileForPos(pkg, cnst.Pos())
198 return nil, fmt.Errorf("finding file for %q: %v", cnst.Name(), err)
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{
208 enums[obj.Type()] = append(enums[obj.Type()], v)
213 // valueDoc transforms a docstring documenting an constant identifier to a
214 // docstring documenting its value.
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
219 func valueDoc(name, value, doc string) string {
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):])
227 return fmt.Sprintf("`%s`: %s", value, doc)
230 func 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.
237 // Find the Commands slice.
238 typesSlice := pkg.Types.Scope().Lookup("Commands")
239 f, err := fileForPos(pkg, typesSlice.Pos())
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)
252 var commands []*source.CommandJSON
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)
261 var astCommand ast.Expr
262 for i, name := range vspec.Names {
263 if name.Name == typesCommand.Name() {
264 astCommand = vspec.Values[i]
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)
273 k := kv.Key.(*ast.Ident).Name
276 name = strings.Trim(kv.Value.(*ast.BasicLit).Value, `"`)
278 title = strings.Trim(kv.Value.(*ast.BasicLit).Value, `"`)
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)
293 commands = append(commands, &source.CommandJSON{
302 func loadLenses(commands []*source.CommandJSON) []*source.LensJSON {
303 lensNames := map[string]struct{}{}
304 for k := range source.LensFuncs() {
305 lensNames[k] = struct{}{}
307 for k := range mod.LensFuncs() {
308 lensNames[k] = struct{}{}
311 var lenses []*source.LensJSON
313 for _, cmd := range commands {
314 if _, ok := lensNames[cmd.Command]; ok {
315 lenses = append(lenses, &source.LensJSON{
325 func lowerFirst(x string) string {
329 return strings.ToLower(x[:1]) + x[1:]
332 func fileForPos(pkg *packages.Package, pos token.Pos) (*ast.File, error) {
334 for _, f := range pkg.Syntax {
335 if fset.Position(f.Pos()).Filename == fset.Position(pos).Filename {
339 return nil, fmt.Errorf("no file for pos %v", pos)