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