golang.org/x/tools/gopls@v0.15.3/internal/golang/inline_all.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 import ( 8 "context" 9 "fmt" 10 "go/ast" 11 "go/parser" 12 "go/types" 13 14 "golang.org/x/tools/go/ast/astutil" 15 "golang.org/x/tools/go/types/typeutil" 16 "golang.org/x/tools/gopls/internal/cache" 17 "golang.org/x/tools/gopls/internal/protocol" 18 "golang.org/x/tools/gopls/internal/util/bug" 19 "golang.org/x/tools/internal/refactor/inline" 20 ) 21 22 // inlineAllCalls inlines all calls to the original function declaration 23 // described by callee, returning the resulting modified file content. 24 // 25 // inlining everything is currently an expensive operation: it involves re-type 26 // checking every package that contains a potential call, as reported by 27 // References. In cases where there are multiple calls per file, inlineAllCalls 28 // must type check repeatedly for each additional call. 29 // 30 // The provided post processing function is applied to the resulting source 31 // after each transformation. This is necessary because we are using this 32 // function to inline synthetic wrappers for the purpose of signature 33 // rewriting. The delegated function has a fake name that doesn't exist in the 34 // snapshot, and so we can't re-type check until we replace this fake name. 35 // 36 // TODO(rfindley): this only works because removing a parameter is a very 37 // narrow operation. A better solution would be to allow for ad-hoc snapshots 38 // that expose the full machinery of real snapshots: minimal invalidation, 39 // batched type checking, etc. Then we could actually rewrite the declaring 40 // package in this snapshot (and so 'post' would not be necessary), and could 41 // robustly re-type check for the purpose of iterative inlining, even if the 42 // inlined code pulls in new imports that weren't present in export data. 43 // 44 // The code below notes where are assumptions are made that only hold true in 45 // the case of parameter removal (annotated with 'Assumption:') 46 func inlineAllCalls(ctx context.Context, logf func(string, ...any), snapshot *cache.Snapshot, pkg *cache.Package, pgf *ParsedGoFile, origDecl *ast.FuncDecl, callee *inline.Callee, post func([]byte) []byte) (map[protocol.DocumentURI][]byte, error) { 47 // Collect references. 48 var refs []protocol.Location 49 { 50 funcPos, err := pgf.Mapper.PosPosition(pgf.Tok, origDecl.Name.NamePos) 51 if err != nil { 52 return nil, err 53 } 54 fh, err := snapshot.ReadFile(ctx, pgf.URI) 55 if err != nil { 56 return nil, err 57 } 58 refs, err = References(ctx, snapshot, fh, funcPos, false) 59 if err != nil { 60 return nil, fmt.Errorf("finding references to rewrite: %v", err) 61 } 62 } 63 64 // Type-check the narrowest package containing each reference. 65 // TODO(rfindley): we should expose forEachPackage in order to operate in 66 // parallel and to reduce peak memory for this operation. 67 var ( 68 pkgForRef = make(map[protocol.Location]PackageID) 69 pkgs = make(map[PackageID]*cache.Package) 70 ) 71 { 72 needPkgs := make(map[PackageID]struct{}) 73 for _, ref := range refs { 74 md, err := NarrowestMetadataForFile(ctx, snapshot, ref.URI) 75 if err != nil { 76 return nil, fmt.Errorf("finding ref metadata: %v", err) 77 } 78 pkgForRef[ref] = md.ID 79 needPkgs[md.ID] = struct{}{} 80 } 81 var pkgIDs []PackageID 82 for id := range needPkgs { // TODO: use maps.Keys once it is available to us 83 pkgIDs = append(pkgIDs, id) 84 } 85 86 refPkgs, err := snapshot.TypeCheck(ctx, pkgIDs...) 87 if err != nil { 88 return nil, fmt.Errorf("type checking reference packages: %v", err) 89 } 90 91 for _, p := range refPkgs { 92 pkgs[p.Metadata().ID] = p 93 } 94 } 95 96 // Organize calls by top file declaration. Calls within a single file may 97 // affect each other, as the inlining edit may affect the surrounding scope 98 // or imports Therefore, when inlining subsequent calls in the same 99 // declaration, we must re-type check. 100 101 type fileCalls struct { 102 pkg *cache.Package 103 pgf *ParsedGoFile 104 calls []*ast.CallExpr 105 } 106 107 refsByFile := make(map[protocol.DocumentURI]*fileCalls) 108 for _, ref := range refs { 109 refpkg := pkgs[pkgForRef[ref]] 110 pgf, err := refpkg.File(ref.URI) 111 if err != nil { 112 return nil, bug.Errorf("finding %s in %s: %v", ref.URI, refpkg.Metadata().ID, err) 113 } 114 start, end, err := pgf.RangePos(ref.Range) 115 if err != nil { 116 return nil, err // e.g. invalid range 117 } 118 119 // Look for the surrounding call expression. 120 var ( 121 name *ast.Ident 122 call *ast.CallExpr 123 ) 124 path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) 125 name, _ = path[0].(*ast.Ident) 126 if _, ok := path[1].(*ast.SelectorExpr); ok { 127 call, _ = path[2].(*ast.CallExpr) 128 } else { 129 call, _ = path[1].(*ast.CallExpr) 130 } 131 if name == nil || call == nil { 132 // TODO(rfindley): handle this case with eta-abstraction: 133 // a reference to the target function f in a non-call position 134 // use(f) 135 // is replaced by 136 // use(func(...) { f(...) }) 137 return nil, fmt.Errorf("cannot inline: found non-call function reference %v", ref) 138 } 139 // Sanity check. 140 if obj := refpkg.GetTypesInfo().ObjectOf(name); obj == nil || 141 obj.Name() != origDecl.Name.Name || 142 obj.Pkg() == nil || 143 obj.Pkg().Path() != string(pkg.Metadata().PkgPath) { 144 return nil, bug.Errorf("cannot inline: corrupted reference %v", ref) 145 } 146 147 callInfo, ok := refsByFile[ref.URI] 148 if !ok { 149 callInfo = &fileCalls{ 150 pkg: refpkg, 151 pgf: pgf, 152 } 153 refsByFile[ref.URI] = callInfo 154 } 155 callInfo.calls = append(callInfo.calls, call) 156 } 157 158 // Inline each call within the same decl in sequence, re-typechecking after 159 // each one. If there is only a single call within the decl, we can avoid 160 // additional type checking. 161 // 162 // Assumption: inlining does not affect the package scope, so we can operate 163 // on separate files independently. 164 result := make(map[protocol.DocumentURI][]byte) 165 for uri, callInfo := range refsByFile { 166 var ( 167 calls = callInfo.calls 168 fset = callInfo.pkg.FileSet() 169 tpkg = callInfo.pkg.GetTypes() 170 tinfo = callInfo.pkg.GetTypesInfo() 171 file = callInfo.pgf.File 172 content = callInfo.pgf.Src 173 ) 174 175 // Check for overlapping calls (such as Foo(Foo())). We can't handle these 176 // because inlining may change the source order of the inner call with 177 // respect to the inlined outer call, and so the heuristic we use to find 178 // the next call (counting from top-to-bottom) does not work. 179 for i := range calls { 180 if i > 0 && calls[i-1].End() > calls[i].Pos() { 181 return nil, fmt.Errorf("%s: can't inline overlapping call %s", uri, types.ExprString(calls[i-1])) 182 } 183 } 184 185 currentCall := 0 186 for currentCall < len(calls) { 187 caller := &inline.Caller{ 188 Fset: fset, 189 Types: tpkg, 190 Info: tinfo, 191 File: file, 192 Call: calls[currentCall], 193 Content: content, 194 } 195 var err error 196 content, err = inline.Inline(logf, caller, callee) 197 if err != nil { 198 return nil, fmt.Errorf("inlining failed: %v", err) 199 } 200 if post != nil { 201 content = post(content) 202 } 203 if len(calls) <= 1 { 204 // No need to re-type check, as we've inlined all calls. 205 break 206 } 207 208 // TODO(rfindley): develop a theory of "trivial" inlining, which are 209 // inlinings that don't require re-type checking. 210 // 211 // In principle, if the inlining only involves replacing one call with 212 // another, the scope of the caller is unchanged and there is no need to 213 // type check again before inlining subsequent calls (edits should not 214 // overlap, and should not affect each other semantically). However, it 215 // feels sufficiently complicated that, to be safe, this optimization is 216 // deferred until later. 217 218 file, err = parser.ParseFile(fset, uri.Path(), content, parser.ParseComments|parser.SkipObjectResolution) 219 if err != nil { 220 return nil, bug.Errorf("inlined file failed to parse: %v", err) 221 } 222 223 // After inlining one call with a removed parameter, the package will 224 // fail to type check due to "not enough arguments". Therefore, we must 225 // allow type errors here. 226 // 227 // Assumption: the resulting type errors do not affect the correctness of 228 // subsequent inlining, because invalid arguments to a call do not affect 229 // anything in the surrounding scope. 230 // 231 // TODO(rfindley): improve this. 232 tpkg, tinfo, err = reTypeCheck(logf, callInfo.pkg, map[protocol.DocumentURI]*ast.File{uri: file}, true) 233 if err != nil { 234 return nil, bug.Errorf("type checking after inlining failed: %v", err) 235 } 236 237 // Collect calls to the target function in the modified declaration. 238 var calls2 []*ast.CallExpr 239 ast.Inspect(file, func(n ast.Node) bool { 240 if call, ok := n.(*ast.CallExpr); ok { 241 fn := typeutil.StaticCallee(tinfo, call) 242 if fn != nil && fn.Pkg().Path() == string(pkg.Metadata().PkgPath) && fn.Name() == origDecl.Name.Name { 243 calls2 = append(calls2, call) 244 } 245 } 246 return true 247 }) 248 249 // If the number of calls has increased, this process will never cease. 250 // If the number of calls has decreased, assume that inlining removed a 251 // call. 252 // If the number of calls didn't change, assume that inlining replaced 253 // a call, and move on to the next. 254 // 255 // Assumption: we're inlining a call that has at most one recursive 256 // reference (which holds for signature rewrites). 257 // 258 // TODO(rfindley): this isn't good enough. We should be able to support 259 // inlining all existing calls even if they increase calls. How do we 260 // correlate the before and after syntax? 261 switch { 262 case len(calls2) > len(calls): 263 return nil, fmt.Errorf("inlining increased calls %d->%d, possible recursive call? content:\n%s", len(calls), len(calls2), content) 264 case len(calls2) < len(calls): 265 calls = calls2 266 case len(calls2) == len(calls): 267 calls = calls2 268 currentCall++ 269 } 270 } 271 272 result[callInfo.pgf.URI] = content 273 } 274 return result, nil 275 }