1 // Copyright 2021 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 // Package commandmeta provides metadata about LSP commands, by analyzing the
6 // command.Interface type.
18 "golang.org/x/tools/go/ast/astutil"
19 "golang.org/x/tools/go/packages"
20 "golang.org/x/tools/internal/lsp/command"
26 // TODO(rFindley): I think Title can actually be eliminated. In all cases
27 // where we use it, there is probably a more appropriate contextual title.
34 func (c *Command) ID() string {
35 return command.ID(c.Name)
43 // In some circumstances, we may want to recursively load additional field
44 // descriptors for fields of struct types, documenting their internals.
48 func Load() (*packages.Package, []*Command, error) {
49 pkgs, err := packages.Load(
51 Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps,
52 BuildFlags: []string{"-tags=generate"},
54 "golang.org/x/tools/internal/lsp/command",
57 return nil, nil, fmt.Errorf("packages.Load: %v", err)
60 if len(pkg.Errors) > 0 {
61 return pkg, nil, pkg.Errors[0]
64 // For a bit of type safety, use reflection to get the interface name within
66 it := reflect.TypeOf((*command.Interface)(nil)).Elem()
67 obj := pkg.Types.Scope().Lookup(it.Name()).Type().Underlying().(*types.Interface)
69 // Load command metadata corresponding to each interface method.
70 var commands []*Command
71 loader := fieldLoader{make(map[types.Object]*Field)}
72 for i := 0; i < obj.NumMethods(); i++ {
74 c, err := loader.loadMethod(pkg, m)
76 return nil, nil, fmt.Errorf("loading %s: %v", m.Name(), err)
78 commands = append(commands, c)
80 return pkg, commands, nil
83 // fieldLoader loads field information, memoizing results to prevent infinite
85 type fieldLoader struct {
86 loaded map[types.Object]*Field
89 var universeError = types.Universe.Lookup("error").Type()
91 func (l *fieldLoader) loadMethod(pkg *packages.Package, m *types.Func) (*Command, error) {
92 node, err := findField(pkg, m.Pos())
96 title, doc := splitDoc(node.Doc.Text())
99 Name: lspName(m.Name()),
103 sig := m.Type().Underlying().(*types.Signature)
104 rlen := sig.Results().Len()
105 if rlen > 2 || rlen == 0 {
106 return nil, fmt.Errorf("must have 1 or 2 returns, got %d", rlen)
108 finalResult := sig.Results().At(rlen - 1)
109 if !types.Identical(finalResult.Type(), universeError) {
110 return nil, fmt.Errorf("final return must be error")
113 c.Result = sig.Results().At(0).Type()
115 ftype := node.Type.(*ast.FuncType)
116 if sig.Params().Len() != ftype.Params.NumFields() {
117 panic("bug: mismatching method params")
119 for i, p := range ftype.Params.List {
120 pt := sig.Params().At(i)
121 fld, err := l.loadField(pkg, p, pt, "")
126 // Lazy check that the first argument is a context. We could relax this,
127 // but then the generated code gets more complicated.
128 if named, ok := fld.Type.(*types.Named); !ok || named.Obj().Name() != "Context" || named.Obj().Pkg().Path() != "context" {
129 return nil, fmt.Errorf("first method parameter must be context.Context")
131 // Skip the context argument, as it is implied.
134 c.Args = append(c.Args, fld)
139 func (l *fieldLoader) loadField(pkg *packages.Package, node *ast.Field, obj *types.Var, tag string) (*Field, error) {
140 if existing, ok := l.loaded[obj]; ok {
145 Doc: strings.TrimSpace(node.Doc.Text()),
147 JSONTag: reflect.StructTag(tag).Get("json"),
149 under := fld.Type.Underlying()
150 if p, ok := under.(*types.Pointer); ok {
153 if s, ok := under.(*types.Struct); ok {
154 for i := 0; i < s.NumFields(); i++ {
157 if obj2.Pkg() != pkg2.Types {
158 pkg2, ok = pkg.Imports[obj2.Pkg().Path()]
160 return nil, fmt.Errorf("missing import for %q: %q", pkg.ID, obj2.Pkg().Path())
163 node2, err := findField(pkg2, obj2.Pos())
168 structField, err := l.loadField(pkg2, node2, obj2, tag)
172 fld.Fields = append(fld.Fields, structField)
178 // splitDoc parses a command doc string to separate the title from normal
181 // The doc comment should be of the form: "MethodName: Title\nDocumentation"
182 func splitDoc(text string) (title, doc string) {
183 docParts := strings.SplitN(text, "\n", 2)
184 titleParts := strings.SplitN(docParts[0], ":", 2)
185 if len(titleParts) > 1 {
186 title = strings.TrimSpace(titleParts[1])
188 if len(docParts) > 1 {
189 doc = strings.TrimSpace(docParts[1])
194 // lspName returns the normalized command name to use in the LSP.
195 func lspName(methodName string) string {
196 words := splitCamel(methodName)
197 for i := range words {
198 words[i] = strings.ToLower(words[i])
200 return strings.Join(words, "_")
203 // splitCamel splits s into words, according to camel-case word boundaries.
204 // Initialisms are grouped as a single word.
207 // "RunTests" -> []string{"Run", "Tests"}
208 // "GCDetails" -> []string{"GC", "Details"}
209 func splitCamel(s string) []string {
212 last := strings.LastIndexFunc(s, unicode.IsUpper)
216 if last == len(s)-1 {
217 // Group initialisms as a single word.
218 last = 1 + strings.LastIndexFunc(s[:last], func(r rune) bool { return !unicode.IsUpper(r) })
220 words = append(words, s[last:])
223 for i := 0; i < len(words)/2; i++ {
224 j := len(words) - i - 1
225 words[i], words[j] = words[j], words[i]
230 // findField finds the struct field or interface method positioned at pos,
232 func findField(pkg *packages.Package, pos token.Pos) (*ast.Field, error) {
235 for _, f := range pkg.Syntax {
236 if fset.Position(f.Pos()).Filename == fset.Position(pos).Filename {
242 return nil, fmt.Errorf("no file for pos %v", pos)
244 path, _ := astutil.PathEnclosingInterval(file, pos, pos)
245 // This is fragile, but in the cases we care about, the field will be in
247 return path[1].(*ast.Field), nil