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