1// Copyright 2020 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
5package source
6
7import (
8	"context"
9	"fmt"
10	"go/ast"
11	"go/token"
12	"go/types"
13
14	"golang.org/x/tools/go/analysis"
15	"golang.org/x/tools/internal/lsp/analysis/fillstruct"
16	"golang.org/x/tools/internal/lsp/analysis/undeclaredname"
17	"golang.org/x/tools/internal/lsp/protocol"
18	"golang.org/x/tools/internal/span"
19	errors "golang.org/x/xerrors"
20)
21
22type Command struct {
23	Title string
24	Name  string
25
26	// Async controls whether the command executes asynchronously.
27	Async bool
28
29	// appliesFn is an optional field to indicate whether or not a command can
30	// be applied to the given inputs. If it returns false, we should not
31	// suggest this command for these inputs.
32	appliesFn AppliesFunc
33
34	// suggestedFixFn is an optional field to generate the edits that the
35	// command produces for the given inputs.
36	suggestedFixFn SuggestedFixFunc
37}
38
39// CommandPrefix is the prefix of all command names gopls uses externally.
40const CommandPrefix = "gopls."
41
42// ID adds the CommandPrefix to the command name, in order to avoid
43// collisions with other language servers.
44func (c Command) ID() string {
45	return CommandPrefix + c.Name
46}
47
48type AppliesFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) bool
49
50// SuggestedFixFunc is a function used to get the suggested fixes for a given
51// gopls command, some of which are provided by go/analysis.Analyzers. Some of
52// the analyzers in internal/lsp/analysis are not efficient enough to include
53// suggested fixes with their diagnostics, so we have to compute them
54// separately. Such analyzers should provide a function with a signature of
55// SuggestedFixFunc.
56type SuggestedFixFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error)
57
58// Commands are the commands currently supported by gopls.
59var Commands = []*Command{
60	CommandGenerate,
61	CommandFillStruct,
62	CommandRegenerateCgo,
63	CommandTest,
64	CommandTidy,
65	CommandUpdateGoSum,
66	CommandUndeclaredName,
67	CommandGoGetPackage,
68	CommandAddDependency,
69	CommandUpgradeDependency,
70	CommandRemoveDependency,
71	CommandVendor,
72	CommandExtractVariable,
73	CommandExtractFunction,
74	CommandToggleDetails,
75	CommandGenerateGoplsMod,
76}
77
78var (
79	// CommandTest runs `go test` for a specific test function.
80	CommandTest = &Command{
81		Name:  "test",
82		Title: "Run test(s)",
83		Async: true,
84	}
85
86	// CommandGenerate runs `go generate` for a given directory.
87	CommandGenerate = &Command{
88		Name:  "generate",
89		Title: "Run go generate",
90	}
91
92	// CommandTidy runs `go mod tidy` for a module.
93	CommandTidy = &Command{
94		Name:  "tidy",
95		Title: "Run go mod tidy",
96	}
97
98	// CommandVendor runs `go mod vendor` for a module.
99	CommandVendor = &Command{
100		Name:  "vendor",
101		Title: "Run go mod vendor",
102	}
103
104	// CommandGoGetPackage runs `go get` to fetch a package.
105	CommandGoGetPackage = &Command{
106		Name:  "go_get_package",
107		Title: "go get package",
108	}
109
110	// CommandUpdateGoSum updates the go.sum file for a module.
111	CommandUpdateGoSum = &Command{
112		Name:  "update_go_sum",
113		Title: "Update go.sum",
114	}
115
116	// CommandAddDependency adds a dependency.
117	CommandAddDependency = &Command{
118		Name:  "add_dependency",
119		Title: "Add dependency",
120	}
121
122	// CommandUpgradeDependency upgrades a dependency.
123	CommandUpgradeDependency = &Command{
124		Name:  "upgrade_dependency",
125		Title: "Upgrade dependency",
126	}
127
128	// CommandRemoveDependency removes a dependency.
129	CommandRemoveDependency = &Command{
130		Name:  "remove_dependency",
131		Title: "Remove dependency",
132	}
133
134	// CommandRegenerateCgo regenerates cgo definitions.
135	CommandRegenerateCgo = &Command{
136		Name:  "regenerate_cgo",
137		Title: "Regenerate cgo",
138	}
139
140	// CommandToggleDetails controls calculation of gc annotations.
141	CommandToggleDetails = &Command{
142		Name:  "gc_details",
143		Title: "Toggle gc_details",
144	}
145
146	// CommandFillStruct is a gopls command to fill a struct with default
147	// values.
148	CommandFillStruct = &Command{
149		Name:           "fill_struct",
150		Title:          "Fill struct",
151		suggestedFixFn: fillstruct.SuggestedFix,
152	}
153
154	// CommandUndeclaredName adds a variable declaration for an undeclared
155	// name.
156	CommandUndeclaredName = &Command{
157		Name:           "undeclared_name",
158		Title:          "Undeclared name",
159		suggestedFixFn: undeclaredname.SuggestedFix,
160	}
161
162	// CommandExtractVariable extracts an expression to a variable.
163	CommandExtractVariable = &Command{
164		Name:           "extract_variable",
165		Title:          "Extract to variable",
166		suggestedFixFn: extractVariable,
167		appliesFn: func(_ *token.FileSet, rng span.Range, _ []byte, file *ast.File, _ *types.Package, _ *types.Info) bool {
168			_, _, ok, _ := canExtractVariable(rng, file)
169			return ok
170		},
171	}
172
173	// CommandExtractFunction extracts statements to a function.
174	CommandExtractFunction = &Command{
175		Name:           "extract_function",
176		Title:          "Extract to function",
177		suggestedFixFn: extractFunction,
178		appliesFn: func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) bool {
179			_, ok, _ := canExtractFunction(fset, rng, src, file, info)
180			return ok
181		},
182	}
183
184	// CommandGenerateGoplsMod (re)generates the gopls.mod file.
185	CommandGenerateGoplsMod = &Command{
186		Name:  "generate_gopls_mod",
187		Title: "Generate gopls.mod",
188	}
189)
190
191// Applies reports whether the command c implements a suggested fix that is
192// relevant to the given rng.
193func (c *Command) Applies(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) bool {
194	// If there is no applies function, assume that the command applies.
195	if c.appliesFn == nil {
196		return true
197	}
198	fset, rng, src, file, _, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
199	if err != nil {
200		return false
201	}
202	return c.appliesFn(fset, rng, src, file, pkg, info)
203}
204
205// IsSuggestedFix reports whether the given command is intended to work as a
206// suggested fix. Suggested fix commands are intended to return edits which are
207// then applied to the workspace.
208func (c *Command) IsSuggestedFix() bool {
209	return c.suggestedFixFn != nil
210}
211
212// SuggestedFix applies the command's suggested fix to the given file and
213// range, returning the resulting edits.
214func (c *Command) SuggestedFix(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) {
215	if c.suggestedFixFn == nil {
216		return nil, fmt.Errorf("no suggested fix function for %s", c.Name)
217	}
218	fset, rng, src, file, m, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
219	if err != nil {
220		return nil, err
221	}
222	fix, err := c.suggestedFixFn(fset, rng, src, file, pkg, info)
223	if err != nil {
224		return nil, err
225	}
226	if fix == nil {
227		return nil, nil
228	}
229
230	var edits []protocol.TextDocumentEdit
231	for _, edit := range fix.TextEdits {
232		rng := span.NewRange(fset, edit.Pos, edit.End)
233		spn, err := rng.Span()
234		if err != nil {
235			return nil, err
236		}
237		clRng, err := m.Range(spn)
238		if err != nil {
239			return nil, err
240		}
241		edits = append(edits, protocol.TextDocumentEdit{
242			TextDocument: protocol.VersionedTextDocumentIdentifier{
243				Version: fh.Version(),
244				TextDocumentIdentifier: protocol.TextDocumentIdentifier{
245					URI: protocol.URIFromSpanURI(fh.URI()),
246				},
247			},
248			Edits: []protocol.TextEdit{
249				{
250					Range:   clRng,
251					NewText: string(edit.NewText),
252				},
253			},
254		})
255	}
256	return edits, nil
257}
258
259// getAllSuggestedFixInputs is a helper function to collect all possible needed
260// inputs for an AppliesFunc or SuggestedFixFunc.
261func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, span.Range, []byte, *ast.File, *protocol.ColumnMapper, *types.Package, *types.Info, error) {
262	pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
263	if err != nil {
264		return nil, span.Range{}, nil, nil, nil, nil, nil, errors.Errorf("getting file for Identifier: %w", err)
265	}
266	spn, err := pgf.Mapper.RangeSpan(pRng)
267	if err != nil {
268		return nil, span.Range{}, nil, nil, nil, nil, nil, err
269	}
270	rng, err := spn.Range(pgf.Mapper.Converter)
271	if err != nil {
272		return nil, span.Range{}, nil, nil, nil, nil, nil, err
273	}
274	src, err := fh.Read()
275	if err != nil {
276		return nil, span.Range{}, nil, nil, nil, nil, nil, err
277	}
278	return snapshot.FileSet(), rng, src, pgf.File, pgf.Mapper, pkg.GetTypes(), pkg.GetTypesInfo(), nil
279}
280