golang.org/x/tools/gopls@v0.15.3/internal/golang/fix.go (about) 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 5 package golang 6 7 import ( 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/gopls/internal/analysis/embeddirective" 16 "golang.org/x/tools/gopls/internal/analysis/fillstruct" 17 "golang.org/x/tools/gopls/internal/analysis/stubmethods" 18 "golang.org/x/tools/gopls/internal/analysis/undeclaredname" 19 "golang.org/x/tools/gopls/internal/analysis/unusedparams" 20 "golang.org/x/tools/gopls/internal/cache" 21 "golang.org/x/tools/gopls/internal/cache/parsego" 22 "golang.org/x/tools/gopls/internal/file" 23 "golang.org/x/tools/gopls/internal/protocol" 24 "golang.org/x/tools/gopls/internal/util/bug" 25 "golang.org/x/tools/internal/imports" 26 ) 27 28 // A fixer is a function that suggests a fix for a diagnostic produced 29 // by the analysis framework. This is done outside of the analyzer Run 30 // function so that the construction of expensive fixes can be 31 // deferred until they are requested by the user. 32 // 33 // The actual diagnostic is not provided; only its position, as the 34 // triple (pgf, start, end); the resulting SuggestedFix implicitly 35 // relates to that file. 36 // 37 // The supplied token positions (start, end) must belong to 38 // pkg.FileSet(), and the returned positions 39 // (SuggestedFix.TextEdits[*].{Pos,End}) must belong to the returned 40 // FileSet. 41 // 42 // A fixer may return (nil, nil) if no fix is available. 43 type fixer func(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) 44 45 // A singleFileFixer is a Fixer that inspects only a single file, 46 // and does not depend on data types from the cache package. 47 // 48 // TODO(adonovan): move fillstruct and undeclaredname into this 49 // package, so we can remove the import restriction and push 50 // the singleFile wrapper down into each singleFileFixer? 51 type singleFileFixer func(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) 52 53 // singleFile adapts a single-file fixer to a Fixer. 54 func singleFile(fixer1 singleFileFixer) fixer { 55 return func(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { 56 return fixer1(pkg.FileSet(), start, end, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()) 57 } 58 } 59 60 // Names of ApplyFix.Fix created directly by the CodeAction handler. 61 const ( 62 fixExtractVariable = "extract_variable" 63 fixExtractFunction = "extract_function" 64 fixExtractMethod = "extract_method" 65 fixInlineCall = "inline_call" 66 fixInvertIfCondition = "invert_if_condition" 67 ) 68 69 // ApplyFix applies the specified kind of suggested fix to the given 70 // file and range, returning the resulting edits. 71 // 72 // A fix kind is either the Category of an analysis.Diagnostic that 73 // had a SuggestedFix with no edits; or the name of a fix agreed upon 74 // by [CodeActions] and this function. 75 // Fix kinds identify fixes in the command protocol. 76 // 77 // TODO(adonovan): come up with a better mechanism for registering the 78 // connection between analyzers, code actions, and fixers. A flaw of 79 // the current approach is that the same Category could in theory 80 // apply to a Diagnostic with several lazy fixes, making them 81 // impossible to distinguish. It would more precise if there was a 82 // SuggestedFix.Category field, or some other way to squirrel metadata 83 // in the fix. 84 func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) ([]protocol.TextDocumentEdit, error) { 85 // This can't be expressed as an entry in the fixer table below 86 // because it operates in the protocol (not go/{token,ast}) domain. 87 // (Sigh; perhaps it was a mistake to factor out the 88 // NarrowestPackageForFile/RangePos/suggestedFixToEdits 89 // steps.) 90 if fix == unusedparams.FixCategory { 91 changes, err := RemoveUnusedParameter(ctx, fh, rng, snapshot) 92 if err != nil { 93 return nil, err 94 } 95 // Unwrap TextDocumentEdits again! 96 var edits []protocol.TextDocumentEdit 97 for _, change := range changes { 98 edits = append(edits, *change.TextDocumentEdit) 99 } 100 return edits, nil 101 } 102 103 fixers := map[string]fixer{ 104 // Fixes for analyzer-provided diagnostics. 105 // These match the Diagnostic.Category. 106 embeddirective.FixCategory: addEmbedImport, 107 fillstruct.FixCategory: singleFile(fillstruct.SuggestedFix), 108 stubmethods.FixCategory: stubMethodsFixer, 109 undeclaredname.FixCategory: singleFile(undeclaredname.SuggestedFix), 110 111 // Ad-hoc fixers: these are used when the command is 112 // constructed directly by logic in server/code_action. 113 fixExtractFunction: singleFile(extractFunction), 114 fixExtractMethod: singleFile(extractMethod), 115 fixExtractVariable: singleFile(extractVariable), 116 fixInlineCall: inlineCall, 117 fixInvertIfCondition: singleFile(invertIfCondition), 118 } 119 fixer, ok := fixers[fix] 120 if !ok { 121 return nil, fmt.Errorf("no suggested fix function for %s", fix) 122 } 123 pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) 124 if err != nil { 125 return nil, err 126 } 127 start, end, err := pgf.RangePos(rng) 128 if err != nil { 129 return nil, err 130 } 131 fixFset, suggestion, err := fixer(ctx, snapshot, pkg, pgf, start, end) 132 if err != nil { 133 return nil, err 134 } 135 if suggestion == nil { 136 return nil, nil 137 } 138 return suggestedFixToEdits(ctx, snapshot, fixFset, suggestion) 139 } 140 141 // suggestedFixToEdits converts the suggestion's edits from analysis form into protocol form. 142 func suggestedFixToEdits(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSet, suggestion *analysis.SuggestedFix) ([]protocol.TextDocumentEdit, error) { 143 editsPerFile := map[protocol.DocumentURI]*protocol.TextDocumentEdit{} 144 for _, edit := range suggestion.TextEdits { 145 tokFile := fset.File(edit.Pos) 146 if tokFile == nil { 147 return nil, bug.Errorf("no file for edit position") 148 } 149 end := edit.End 150 if !end.IsValid() { 151 end = edit.Pos 152 } 153 fh, err := snapshot.ReadFile(ctx, protocol.URIFromPath(tokFile.Name())) 154 if err != nil { 155 return nil, err 156 } 157 te, ok := editsPerFile[fh.URI()] 158 if !ok { 159 te = &protocol.TextDocumentEdit{ 160 TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ 161 Version: fh.Version(), 162 TextDocumentIdentifier: protocol.TextDocumentIdentifier{ 163 URI: fh.URI(), 164 }, 165 }, 166 } 167 editsPerFile[fh.URI()] = te 168 } 169 content, err := fh.Content() 170 if err != nil { 171 return nil, err 172 } 173 m := protocol.NewMapper(fh.URI(), content) // TODO(adonovan): opt: memoize in map 174 rng, err := m.PosRange(tokFile, edit.Pos, end) 175 if err != nil { 176 return nil, err 177 } 178 te.Edits = append(te.Edits, protocol.Or_TextDocumentEdit_edits_Elem{ 179 Value: protocol.TextEdit{ 180 Range: rng, 181 NewText: string(edit.NewText), 182 }, 183 }) 184 } 185 var edits []protocol.TextDocumentEdit 186 for _, edit := range editsPerFile { 187 edits = append(edits, *edit) 188 } 189 return edits, nil 190 } 191 192 // addEmbedImport adds a missing embed "embed" import with blank name. 193 func addEmbedImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, _, _ token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { 194 // Like golang.AddImport, but with _ as Name and using our pgf. 195 protoEdits, err := ComputeOneImportFixEdits(snapshot, pgf, &imports.ImportFix{ 196 StmtInfo: imports.ImportInfo{ 197 ImportPath: "embed", 198 Name: "_", 199 }, 200 FixType: imports.AddImport, 201 }) 202 if err != nil { 203 return nil, nil, fmt.Errorf("compute edits: %w", err) 204 } 205 206 var edits []analysis.TextEdit 207 for _, e := range protoEdits { 208 start, end, err := pgf.RangePos(e.Range) 209 if err != nil { 210 return nil, nil, err // e.g. invalid range 211 } 212 edits = append(edits, analysis.TextEdit{ 213 Pos: start, 214 End: end, 215 NewText: []byte(e.NewText), 216 }) 217 } 218 219 return pkg.FileSet(), &analysis.SuggestedFix{ 220 Message: "Add embed import", 221 TextEdits: edits, 222 }, nil 223 }