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