golang.org/x/tools/gopls@v0.15.3/internal/golang/inline.go (about) 1 // Copyright 2023 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 // This file defines the refactor.inline code action. 8 9 import ( 10 "context" 11 "fmt" 12 "go/ast" 13 "go/token" 14 "go/types" 15 16 "golang.org/x/tools/go/analysis" 17 "golang.org/x/tools/go/ast/astutil" 18 "golang.org/x/tools/go/types/typeutil" 19 "golang.org/x/tools/gopls/internal/cache" 20 "golang.org/x/tools/gopls/internal/cache/parsego" 21 "golang.org/x/tools/gopls/internal/protocol" 22 "golang.org/x/tools/gopls/internal/util/safetoken" 23 "golang.org/x/tools/internal/diff" 24 "golang.org/x/tools/internal/event" 25 "golang.org/x/tools/internal/refactor/inline" 26 ) 27 28 // EnclosingStaticCall returns the innermost function call enclosing 29 // the selected range, along with the callee. 30 func EnclosingStaticCall(pkg *cache.Package, pgf *ParsedGoFile, start, end token.Pos) (*ast.CallExpr, *types.Func, error) { 31 path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) 32 33 var call *ast.CallExpr 34 loop: 35 for _, n := range path { 36 switch n := n.(type) { 37 case *ast.FuncLit: 38 break loop 39 case *ast.CallExpr: 40 call = n 41 break loop 42 } 43 } 44 if call == nil { 45 return nil, nil, fmt.Errorf("no enclosing call") 46 } 47 if safetoken.Line(pgf.Tok, call.Lparen) != safetoken.Line(pgf.Tok, start) { 48 return nil, nil, fmt.Errorf("enclosing call is not on this line") 49 } 50 fn := typeutil.StaticCallee(pkg.GetTypesInfo(), call) 51 if fn == nil { 52 return nil, nil, fmt.Errorf("not a static call to a Go function") 53 } 54 return call, fn, nil 55 } 56 57 func inlineCall(ctx context.Context, snapshot *cache.Snapshot, callerPkg *cache.Package, callerPGF *parsego.File, start, end token.Pos) (_ *token.FileSet, _ *analysis.SuggestedFix, err error) { 58 // Find enclosing static call. 59 call, fn, err := EnclosingStaticCall(callerPkg, callerPGF, start, end) 60 if err != nil { 61 return nil, nil, err 62 } 63 64 // Locate callee by file/line and analyze it. 65 calleePosn := safetoken.StartPosition(callerPkg.FileSet(), fn.Pos()) 66 calleePkg, calleePGF, err := NarrowestPackageForFile(ctx, snapshot, protocol.URIFromPath(calleePosn.Filename)) 67 if err != nil { 68 return nil, nil, err 69 } 70 var calleeDecl *ast.FuncDecl 71 for _, decl := range calleePGF.File.Decls { 72 if decl, ok := decl.(*ast.FuncDecl); ok { 73 posn := safetoken.StartPosition(calleePkg.FileSet(), decl.Name.Pos()) 74 if posn.Line == calleePosn.Line && posn.Column == calleePosn.Column { 75 calleeDecl = decl 76 break 77 } 78 } 79 } 80 if calleeDecl == nil { 81 return nil, nil, fmt.Errorf("can't find callee") 82 } 83 84 // The inliner assumes that input is well-typed, 85 // but that is frequently not the case within gopls. 86 // Until we are able to harden the inliner, 87 // report panics as errors to avoid crashing the server. 88 bad := func(p *cache.Package) bool { return len(p.GetParseErrors())+len(p.GetTypeErrors()) > 0 } 89 if bad(calleePkg) || bad(callerPkg) { 90 defer func() { 91 if x := recover(); x != nil { 92 err = fmt.Errorf("inlining failed (%q), likely because inputs were ill-typed", x) 93 } 94 }() 95 } 96 97 // Users can consult the gopls event log to see 98 // why a particular inlining strategy was chosen. 99 logf := logger(ctx, "inliner", snapshot.Options().VerboseOutput) 100 101 callee, err := inline.AnalyzeCallee(logf, calleePkg.FileSet(), calleePkg.GetTypes(), calleePkg.GetTypesInfo(), calleeDecl, calleePGF.Src) 102 if err != nil { 103 return nil, nil, err 104 } 105 106 // Inline the call. 107 caller := &inline.Caller{ 108 Fset: callerPkg.FileSet(), 109 Types: callerPkg.GetTypes(), 110 Info: callerPkg.GetTypesInfo(), 111 File: callerPGF.File, 112 Call: call, 113 Content: callerPGF.Src, 114 } 115 116 got, err := inline.Inline(logf, caller, callee) 117 if err != nil { 118 return nil, nil, err 119 } 120 121 return callerPkg.FileSet(), &analysis.SuggestedFix{ 122 Message: fmt.Sprintf("inline call of %v", callee), 123 TextEdits: diffToTextEdits(callerPGF.Tok, diff.Bytes(callerPGF.Src, got)), 124 }, nil 125 } 126 127 // TODO(adonovan): change the inliner to instead accept an io.Writer. 128 func logger(ctx context.Context, name string, verbose bool) func(format string, args ...any) { 129 if verbose { 130 return func(format string, args ...any) { 131 event.Log(ctx, name+": "+fmt.Sprintf(format, args...)) 132 } 133 } else { 134 return func(string, ...any) {} 135 } 136 }