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.
4
5// Package commandmeta provides metadata about LSP commands, by analyzing the
6// command.Interface type.
7package commandmeta
8
9import (
10	"fmt"
11	"go/ast"
12	"go/token"
13	"go/types"
14	"reflect"
15	"strings"
16	"unicode"
17
18	"golang.org/x/tools/go/ast/astutil"
19	"golang.org/x/tools/go/packages"
20	"golang.org/x/tools/internal/lsp/command"
21)
22
23type Command struct {
24	MethodName string
25	Name       string
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.
28	Title  string
29	Doc    string
30	Args   []*Field
31	Result types.Type
32}
33
34func (c *Command) ID() string {
35	return command.ID(c.Name)
36}
37
38type Field struct {
39	Name    string
40	Doc     string
41	JSONTag string
42	Type    types.Type
43	// In some circumstances, we may want to recursively load additional field
44	// descriptors for fields of struct types, documenting their internals.
45	Fields []*Field
46}
47
48func Load() (*packages.Package, []*Command, error) {
49	pkgs, err := packages.Load(
50		&packages.Config{
51			Mode:       packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps,
52			BuildFlags: []string{"-tags=generate"},
53		},
54		"golang.org/x/tools/internal/lsp/command",
55	)
56	if err != nil {
57		return nil, nil, fmt.Errorf("packages.Load: %v", err)
58	}
59	pkg := pkgs[0]
60	if len(pkg.Errors) > 0 {
61		return pkg, nil, pkg.Errors[0]
62	}
63
64	// For a bit of type safety, use reflection to get the interface name within
65	// the package scope.
66	it := reflect.TypeOf((*command.Interface)(nil)).Elem()
67	obj := pkg.Types.Scope().Lookup(it.Name()).Type().Underlying().(*types.Interface)
68
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++ {
73		m := obj.Method(i)
74		c, err := loader.loadMethod(pkg, m)
75		if err != nil {
76			return nil, nil, fmt.Errorf("loading %s: %v", m.Name(), err)
77		}
78		commands = append(commands, c)
79	}
80	return pkg, commands, nil
81}
82
83// fieldLoader loads field information, memoizing results to prevent infinite
84// recursion.
85type fieldLoader struct {
86	loaded map[types.Object]*Field
87}
88
89var universeError = types.Universe.Lookup("error").Type()
90
91func (l *fieldLoader) loadMethod(pkg *packages.Package, m *types.Func) (*Command, error) {
92	node, err := findField(pkg, m.Pos())
93	if err != nil {
94		return nil, err
95	}
96	title, doc := splitDoc(node.Doc.Text())
97	c := &Command{
98		MethodName: m.Name(),
99		Name:       lspName(m.Name()),
100		Doc:        doc,
101		Title:      title,
102	}
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)
107	}
108	finalResult := sig.Results().At(rlen - 1)
109	if !types.Identical(finalResult.Type(), universeError) {
110		return nil, fmt.Errorf("final return must be error")
111	}
112	if rlen == 2 {
113		c.Result = sig.Results().At(0).Type()
114	}
115	ftype := node.Type.(*ast.FuncType)
116	if sig.Params().Len() != ftype.Params.NumFields() {
117		panic("bug: mismatching method params")
118	}
119	for i, p := range ftype.Params.List {
120		pt := sig.Params().At(i)
121		fld, err := l.loadField(pkg, p, pt, "")
122		if err != nil {
123			return nil, err
124		}
125		if i == 0 {
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")
130			}
131			// Skip the context argument, as it is implied.
132			continue
133		}
134		c.Args = append(c.Args, fld)
135	}
136	return c, nil
137}
138
139func (l *fieldLoader) loadField(pkg *packages.Package, node *ast.Field, obj *types.Var, tag string) (*Field, error) {
140	if existing, ok := l.loaded[obj]; ok {
141		return existing, nil
142	}
143	fld := &Field{
144		Name:    obj.Name(),
145		Doc:     strings.TrimSpace(node.Doc.Text()),
146		Type:    obj.Type(),
147		JSONTag: reflect.StructTag(tag).Get("json"),
148	}
149	under := fld.Type.Underlying()
150	if p, ok := under.(*types.Pointer); ok {
151		under = p.Elem()
152	}
153	if s, ok := under.(*types.Struct); ok {
154		for i := 0; i < s.NumFields(); i++ {
155			obj2 := s.Field(i)
156			pkg2 := pkg
157			if obj2.Pkg() != pkg2.Types {
158				pkg2, ok = pkg.Imports[obj2.Pkg().Path()]
159				if !ok {
160					return nil, fmt.Errorf("missing import for %q: %q", pkg.ID, obj2.Pkg().Path())
161				}
162			}
163			node2, err := findField(pkg2, obj2.Pos())
164			if err != nil {
165				return nil, err
166			}
167			tag := s.Tag(i)
168			structField, err := l.loadField(pkg2, node2, obj2, tag)
169			if err != nil {
170				return nil, err
171			}
172			fld.Fields = append(fld.Fields, structField)
173		}
174	}
175	return fld, nil
176}
177
178// splitDoc parses a command doc string to separate the title from normal
179// documentation.
180//
181// The doc comment should be of the form: "MethodName: Title\nDocumentation"
182func 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])
187	}
188	if len(docParts) > 1 {
189		doc = strings.TrimSpace(docParts[1])
190	}
191	return title, doc
192}
193
194// lspName returns the normalized command name to use in the LSP.
195func lspName(methodName string) string {
196	words := splitCamel(methodName)
197	for i := range words {
198		words[i] = strings.ToLower(words[i])
199	}
200	return strings.Join(words, "_")
201}
202
203// splitCamel splits s into words, according to camel-case word boundaries.
204// Initialisms are grouped as a single word.
205//
206// For example:
207//  "RunTests" -> []string{"Run", "Tests"}
208//  "GCDetails" -> []string{"GC", "Details"}
209func splitCamel(s string) []string {
210	var words []string
211	for len(s) > 0 {
212		last := strings.LastIndexFunc(s, unicode.IsUpper)
213		if last < 0 {
214			last = 0
215		}
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) })
219		}
220		words = append(words, s[last:])
221		s = s[:last]
222	}
223	for i := 0; i < len(words)/2; i++ {
224		j := len(words) - i - 1
225		words[i], words[j] = words[j], words[i]
226	}
227	return words
228}
229
230// findField finds the struct field or interface method positioned at pos,
231// within the AST.
232func findField(pkg *packages.Package, pos token.Pos) (*ast.Field, error) {
233	fset := pkg.Fset
234	var file *ast.File
235	for _, f := range pkg.Syntax {
236		if fset.Position(f.Pos()).Filename == fset.Position(pos).Filename {
237			file = f
238			break
239		}
240	}
241	if file == nil {
242		return nil, fmt.Errorf("no file for pos %v", pos)
243	}
244	path, _ := astutil.PathEnclosingInterval(file, pos, pos)
245	// This is fragile, but in the cases we care about, the field will be in
246	// path[1].
247	return path[1].(*ast.Field), nil
248}
249