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