| // 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 genopts generates JSON describing gopls' user options. |
| package main |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "go/ast" |
| "go/types" |
| "os" |
| "reflect" |
| "strings" |
| |
| "golang.org/x/tools/go/ast/astutil" |
| "golang.org/x/tools/go/packages" |
| "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.NeedSyntax | packages.NeedDeps, |
| }, |
| "golang.org/x/tools/internal/lsp/source", |
| ) |
| if err != nil { |
| return nil, err |
| } |
| pkg := pkgs[0] |
| |
| defaults := source.DefaultOptions() |
| categories := map[string][]option{} |
| 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") |
| categories[catName] = opts |
| } |
| |
| marshaled, err := json.Marshal(categories) |
| if err != nil { |
| return nil, err |
| } |
| |
| buf := bytes.NewBuffer(nil) |
| fmt.Fprintf(buf, "// Code generated by \"golang.org/x/tools/internal/lsp/source/genopts\"; DO NOT EDIT.\n\npackage source\n\nconst OptionsJson = %q\n", string(marshaled)) |
| return buf.Bytes(), nil |
| } |
| |
| type option struct { |
| Name string |
| Type string |
| Doc string |
| Default string |
| } |
| |
| func loadOptions(category reflect.Value, pkg *packages.Package) ([]option, 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()) |
| } |
| |
| fset := pkg.Fset |
| var file *ast.File |
| for _, f := range pkg.Syntax { |
| if fset.Position(f.Pos()).Filename == fset.Position(optsType.Pos()).Filename { |
| file = f |
| } |
| } |
| if file == nil { |
| return nil, fmt.Errorf("no file for opts type %v", optsType) |
| } |
| |
| var opts []option |
| 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) < 1 { |
| 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. String values should look like strings. Other stuff should look like JSON literals. |
| var defString string |
| switch def := reflectField.Interface().(type) { |
| case fmt.Stringer: |
| defString = `"` + def.String() + `"` |
| case string: |
| defString = `"` + def + `"` |
| default: |
| defString = fmt.Sprint(def) |
| } |
| if reflectField.Kind() == reflect.Map { |
| b, err := json.Marshal(reflectField.Interface()) |
| if err != nil { |
| return nil, err |
| } |
| defString = string(b) |
| } |
| |
| opts = append(opts, option{ |
| Name: lowerFirst(typesField.Name()), |
| Type: typesField.Type().String(), |
| Doc: lowerFirst(astField.Doc.Text()), |
| Default: defString, |
| }) |
| } |
| return opts, nil |
| } |
| |
| func lowerFirst(x string) string { |
| if x == "" { |
| return x |
| } |
| return strings.ToLower(x[:1]) + x[1:] |
| } |