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)
20
21type Command struct {
22	Name, Title string
23
24	// appliesFn is an optional field to indicate whether or not a command can
25	// be applied to the given inputs. If it returns false, we should not
26	// suggest this command for these inputs.
27	appliesFn AppliesFunc
28
29	// suggestedFixFn is an optional field to generate the edits that the
30	// command produces for the given inputs.
31	suggestedFixFn SuggestedFixFunc
32}
33
34type AppliesFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) bool
35
36// SuggestedFixFunc is a function used to get the suggested fixes for a given
37// gopls command, some of which are provided by go/analysis.Analyzers. Some of
38// the analyzers in internal/lsp/analysis are not efficient enough to include
39// suggested fixes with their diagnostics, so we have to compute them
40// separately. Such analyzers should provide a function with a signature of
41// SuggestedFixFunc.
42type SuggestedFixFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error)
43
44// Commands are the commands currently supported by gopls.
45var Commands = []*Command{
46	CommandGenerate,
47	CommandFillStruct,
48	CommandRegenerateCgo,
49	CommandTest,
50	CommandTidy,
51	CommandUndeclaredName,
52	CommandUpgradeDependency,
53	CommandVendor,
54	CommandExtractVariable,
55	CommandExtractFunction,
56	CommandToggleDetails,
57}
58
59var (
60	// CommandTest runs `go test` for a specific test function.
61	CommandTest = &Command{
62		Name: "test",
63	}
64
65	// CommandGenerate runs `go generate` for a given directory.
66	CommandGenerate = &Command{
67		Name: "generate",
68	}
69
70	// CommandTidy runs `go mod tidy` for a module.
71	CommandTidy = &Command{
72		Name: "tidy",
73	}
74
75	// CommandVendor runs `go mod vendor` for a module.
76	CommandVendor = &Command{
77		Name: "vendor",
78	}
79
80	// CommandUpgradeDependency upgrades a dependency.
81	CommandUpgradeDependency = &Command{
82		Name: "upgrade_dependency",
83	}
84
85	// CommandRegenerateCgo regenerates cgo definitions.
86	CommandRegenerateCgo = &Command{
87		Name: "regenerate_cgo",
88	}
89
90	// CommandToggleDetails controls calculation of gc annotations.
91	CommandToggleDetails = &Command{
92		Name: "gc_details",
93	}
94
95	// CommandFillStruct is a gopls command to fill a struct with default
96	// values.
97	CommandFillStruct = &Command{
98		Name:           "fill_struct",
99		suggestedFixFn: fillstruct.SuggestedFix,
100	}
101
102	// CommandUndeclaredName adds a variable declaration for an undeclared
103	// name.
104	CommandUndeclaredName = &Command{
105		Name:           "undeclared_name",
106		suggestedFixFn: undeclaredname.SuggestedFix,
107	}
108
109	// CommandExtractVariable extracts an expression to a variable.
110	CommandExtractVariable = &Command{
111		Name:           "extract_variable",
112		Title:          "Extract to variable",
113		suggestedFixFn: extractVariable,
114		appliesFn: func(_ *token.FileSet, rng span.Range, _ []byte, file *ast.File, _ *types.Package, _ *types.Info) bool {
115			_, _, ok, _ := canExtractVariable(rng, file)
116			return ok
117		},
118	}
119
120	// CommandExtractFunction extracts statements to a function.
121	CommandExtractFunction = &Command{
122		Name:           "extract_function",
123		Title:          "Extract to function",
124		suggestedFixFn: extractFunction,
125		appliesFn: func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) bool {
126			_, _, _, _, _, ok, _ := canExtractFunction(fset, rng, src, file, info)
127			return ok
128		},
129	}
130)
131
132// Applies reports whether the command c implements a suggested fix that is
133// relevant to the given rng.
134func (c *Command) Applies(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) bool {
135	// If there is no applies function, assume that the command applies.
136	if c.appliesFn == nil {
137		return true
138	}
139	fset, rng, src, file, _, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
140	if err != nil {
141		return false
142	}
143	return c.appliesFn(fset, rng, src, file, pkg, info)
144}
145
146// IsSuggestedFix reports whether the given command is intended to work as a
147// suggested fix. Suggested fix commands are intended to return edits which are
148// then applied to the workspace.
149func (c *Command) IsSuggestedFix() bool {
150	return c.suggestedFixFn != nil
151}
152
153// SuggestedFix applies the command's suggested fix to the given file and
154// range, returning the resulting edits.
155func (c *Command) SuggestedFix(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) {
156	if c.suggestedFixFn == nil {
157		return nil, fmt.Errorf("no suggested fix function for %s", c.Name)
158	}
159	fset, rng, src, file, m, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
160	if err != nil {
161		return nil, err
162	}
163	fix, err := c.suggestedFixFn(fset, rng, src, file, pkg, info)
164	if err != nil {
165		return nil, err
166	}
167	var edits []protocol.TextDocumentEdit
168	for _, edit := range fix.TextEdits {
169		rng := span.NewRange(fset, edit.Pos, edit.End)
170		spn, err := rng.Span()
171		if err != nil {
172			return nil, err
173		}
174		clRng, err := m.Range(spn)
175		if err != nil {
176			return nil, err
177		}
178		edits = append(edits, protocol.TextDocumentEdit{
179			TextDocument: protocol.VersionedTextDocumentIdentifier{
180				Version: fh.Version(),
181				TextDocumentIdentifier: protocol.TextDocumentIdentifier{
182					URI: protocol.URIFromSpanURI(fh.URI()),
183				},
184			},
185			Edits: []protocol.TextEdit{
186				{
187					Range:   clRng,
188					NewText: string(edit.NewText),
189				},
190			},
191		})
192	}
193	return edits, nil
194}
195
196// getAllSuggestedFixInputs is a helper function to collect all possible needed
197// inputs for an AppliesFunc or SuggestedFixFunc.
198func 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) {
199	pkg, pgf, err := getParsedFile(ctx, snapshot, fh, NarrowestPackage)
200	if err != nil {
201		return nil, span.Range{}, nil, nil, nil, nil, nil, fmt.Errorf("getting file for Identifier: %w", err)
202	}
203	spn, err := pgf.Mapper.RangeSpan(pRng)
204	if err != nil {
205		return nil, span.Range{}, nil, nil, nil, nil, nil, err
206	}
207	rng, err := spn.Range(pgf.Mapper.Converter)
208	if err != nil {
209		return nil, span.Range{}, nil, nil, nil, nil, nil, err
210	}
211	src, err := fh.Read()
212	if err != nil {
213		return nil, span.Range{}, nil, nil, nil, nil, nil, err
214	}
215	return snapshot.FileSet(), rng, src, pgf.File, pgf.Mapper, pkg.GetTypes(), pkg.GetTypesInfo(), nil
216}
217