diff options
Diffstat (limited to 'gopls/internal/lsp/command/commandmeta/meta.go')
-rw-r--r-- | gopls/internal/lsp/command/commandmeta/meta.go | 259 |
1 files changed, 259 insertions, 0 deletions
diff --git a/gopls/internal/lsp/command/commandmeta/meta.go b/gopls/internal/lsp/command/commandmeta/meta.go new file mode 100644 index 000000000..bf85c4faa --- /dev/null +++ b/gopls/internal/lsp/command/commandmeta/meta.go @@ -0,0 +1,259 @@ +// Copyright 2021 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 commandmeta provides metadata about LSP commands, by analyzing the +// command.Interface type. +package commandmeta + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "reflect" + "strings" + "unicode" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/packages" + "golang.org/x/tools/gopls/internal/lsp/command" +) + +type Command struct { + MethodName string + Name string + // TODO(rFindley): I think Title can actually be eliminated. In all cases + // where we use it, there is probably a more appropriate contextual title. + Title string + Doc string + Args []*Field + Result *Field +} + +func (c *Command) ID() string { + return command.ID(c.Name) +} + +type Field struct { + Name string + Doc string + JSONTag string + Type types.Type + FieldMod string + // In some circumstances, we may want to recursively load additional field + // descriptors for fields of struct types, documenting their internals. + Fields []*Field +} + +func Load() (*packages.Package, []*Command, error) { + pkgs, err := packages.Load( + &packages.Config{ + Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps, + BuildFlags: []string{"-tags=generate"}, + }, + "golang.org/x/tools/gopls/internal/lsp/command", + ) + if err != nil { + return nil, nil, fmt.Errorf("packages.Load: %v", err) + } + pkg := pkgs[0] + if len(pkg.Errors) > 0 { + return pkg, nil, pkg.Errors[0] + } + + // For a bit of type safety, use reflection to get the interface name within + // the package scope. + it := reflect.TypeOf((*command.Interface)(nil)).Elem() + obj := pkg.Types.Scope().Lookup(it.Name()).Type().Underlying().(*types.Interface) + + // Load command metadata corresponding to each interface method. + var commands []*Command + loader := fieldLoader{make(map[types.Object]*Field)} + for i := 0; i < obj.NumMethods(); i++ { + m := obj.Method(i) + c, err := loader.loadMethod(pkg, m) + if err != nil { + return nil, nil, fmt.Errorf("loading %s: %v", m.Name(), err) + } + commands = append(commands, c) + } + return pkg, commands, nil +} + +// fieldLoader loads field information, memoizing results to prevent infinite +// recursion. +type fieldLoader struct { + loaded map[types.Object]*Field +} + +var universeError = types.Universe.Lookup("error").Type() + +func (l *fieldLoader) loadMethod(pkg *packages.Package, m *types.Func) (*Command, error) { + node, err := findField(pkg, m.Pos()) + if err != nil { + return nil, err + } + title, doc := splitDoc(node.Doc.Text()) + c := &Command{ + MethodName: m.Name(), + Name: lspName(m.Name()), + Doc: doc, + Title: title, + } + sig := m.Type().Underlying().(*types.Signature) + rlen := sig.Results().Len() + if rlen > 2 || rlen == 0 { + return nil, fmt.Errorf("must have 1 or 2 returns, got %d", rlen) + } + finalResult := sig.Results().At(rlen - 1) + if !types.Identical(finalResult.Type(), universeError) { + return nil, fmt.Errorf("final return must be error") + } + if rlen == 2 { + obj := sig.Results().At(0) + c.Result, err = l.loadField(pkg, obj, "", "") + if err != nil { + return nil, err + } + } + for i := 0; i < sig.Params().Len(); i++ { + obj := sig.Params().At(i) + fld, err := l.loadField(pkg, obj, "", "") + if err != nil { + return nil, err + } + if i == 0 { + // Lazy check that the first argument is a context. We could relax this, + // but then the generated code gets more complicated. + if named, ok := fld.Type.(*types.Named); !ok || named.Obj().Name() != "Context" || named.Obj().Pkg().Path() != "context" { + return nil, fmt.Errorf("first method parameter must be context.Context") + } + // Skip the context argument, as it is implied. + continue + } + c.Args = append(c.Args, fld) + } + return c, nil +} + +func (l *fieldLoader) loadField(pkg *packages.Package, obj *types.Var, doc, tag string) (*Field, error) { + if existing, ok := l.loaded[obj]; ok { + return existing, nil + } + fld := &Field{ + Name: obj.Name(), + Doc: strings.TrimSpace(doc), + Type: obj.Type(), + JSONTag: reflect.StructTag(tag).Get("json"), + } + under := fld.Type.Underlying() + // Quick-and-dirty handling for various underlying types. + switch p := under.(type) { + case *types.Pointer: + under = p.Elem().Underlying() + case *types.Array: + under = p.Elem().Underlying() + fld.FieldMod = fmt.Sprintf("[%d]", p.Len()) + case *types.Slice: + under = p.Elem().Underlying() + fld.FieldMod = "[]" + } + + if s, ok := under.(*types.Struct); ok { + for i := 0; i < s.NumFields(); i++ { + obj2 := s.Field(i) + pkg2 := pkg + if obj2.Pkg() != pkg2.Types { + pkg2, ok = pkg.Imports[obj2.Pkg().Path()] + if !ok { + return nil, fmt.Errorf("missing import for %q: %q", pkg.ID, obj2.Pkg().Path()) + } + } + node, err := findField(pkg2, obj2.Pos()) + if err != nil { + return nil, err + } + tag := s.Tag(i) + structField, err := l.loadField(pkg2, obj2, node.Doc.Text(), tag) + if err != nil { + return nil, err + } + fld.Fields = append(fld.Fields, structField) + } + } + return fld, nil +} + +// splitDoc parses a command doc string to separate the title from normal +// documentation. +// +// The doc comment should be of the form: "MethodName: Title\nDocumentation" +func splitDoc(text string) (title, doc string) { + docParts := strings.SplitN(text, "\n", 2) + titleParts := strings.SplitN(docParts[0], ":", 2) + if len(titleParts) > 1 { + title = strings.TrimSpace(titleParts[1]) + } + if len(docParts) > 1 { + doc = strings.TrimSpace(docParts[1]) + } + return title, doc +} + +// lspName returns the normalized command name to use in the LSP. +func lspName(methodName string) string { + words := splitCamel(methodName) + for i := range words { + words[i] = strings.ToLower(words[i]) + } + return strings.Join(words, "_") +} + +// splitCamel splits s into words, according to camel-case word boundaries. +// Initialisms are grouped as a single word. +// +// For example: +// +// "RunTests" -> []string{"Run", "Tests"} +// "GCDetails" -> []string{"GC", "Details"} +func splitCamel(s string) []string { + var words []string + for len(s) > 0 { + last := strings.LastIndexFunc(s, unicode.IsUpper) + if last < 0 { + last = 0 + } + if last == len(s)-1 { + // Group initialisms as a single word. + last = 1 + strings.LastIndexFunc(s[:last], func(r rune) bool { return !unicode.IsUpper(r) }) + } + words = append(words, s[last:]) + s = s[:last] + } + for i := 0; i < len(words)/2; i++ { + j := len(words) - i - 1 + words[i], words[j] = words[j], words[i] + } + return words +} + +// findField finds the struct field or interface method positioned at pos, +// within the AST. +func findField(pkg *packages.Package, pos token.Pos) (*ast.Field, error) { + fset := pkg.Fset + var file *ast.File + for _, f := range pkg.Syntax { + if fset.File(f.Pos()).Name() == fset.File(pos).Name() { + file = f + break + } + } + if file == nil { + return nil, fmt.Errorf("no file for pos %v", pos) + } + path, _ := astutil.PathEnclosingInterval(file, pos, pos) + // This is fragile, but in the cases we care about, the field will be in + // path[1]. + return path[1].(*ast.Field), nil +} |