// 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/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 types.Type } func (c *Command) ID() string { return command.ID(c.Name) } type Field struct { Name string Doc string JSONTag string Type types.Type // 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/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 { c.Result = sig.Results().At(0).Type() } ftype := node.Type.(*ast.FuncType) if sig.Params().Len() != ftype.Params.NumFields() { panic("bug: mismatching method params") } for i, p := range ftype.Params.List { pt := sig.Params().At(i) fld, err := l.loadField(pkg, p, pt, "") 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, node *ast.Field, obj *types.Var, tag string) (*Field, error) { if existing, ok := l.loaded[obj]; ok { return existing, nil } fld := &Field{ Name: obj.Name(), Doc: strings.TrimSpace(node.Doc.Text()), Type: obj.Type(), JSONTag: reflect.StructTag(tag).Get("json"), } under := fld.Type.Underlying() if p, ok := under.(*types.Pointer); ok { under = p.Elem() } 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()) } } node2, err := findField(pkg2, obj2.Pos()) if err != nil { return nil, err } tag := s.Tag(i) structField, err := l.loadField(pkg2, node2, obj2, 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.Position(f.Pos()).Filename == fset.Position(pos).Filename { 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 }