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 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
|
// Copyright 2023 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/parser"
"go/types"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/refactor/inline"
)
// inlineAllCalls inlines all calls to the original function declaration
// described by callee, returning the resulting modified file content.
//
// inlining everything is currently an expensive operation: it involves re-type
// checking every package that contains a potential call, as reported by
// References. In cases where there are multiple calls per file, inlineAllCalls
// must type check repeatedly for each additional call.
//
// The provided post processing function is applied to the resulting source
// after each transformation. This is necessary because we are using this
// function to inline synthetic wrappers for the purpose of signature
// rewriting. The delegated function has a fake name that doesn't exist in the
// snapshot, and so we can't re-type check until we replace this fake name.
//
// TODO(rfindley): this only works because removing a parameter is a very
// narrow operation. A better solution would be to allow for ad-hoc snapshots
// that expose the full machinery of real snapshots: minimal invalidation,
// batched type checking, etc. Then we could actually rewrite the declaring
// package in this snapshot (and so 'post' would not be necessary), and could
// robustly re-type check for the purpose of iterative inlining, even if the
// inlined code pulls in new imports that weren't present in export data.
//
// The code below notes where are assumptions are made that only hold true in
// the case of parameter removal (annotated with 'Assumption:')
func inlineAllCalls(ctx context.Context, logf func(string, ...any), snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, origDecl *ast.FuncDecl, callee *inline.Callee, post func([]byte) []byte) (map[protocol.DocumentURI][]byte, error) {
// Collect references.
var refs []protocol.Location
{
funcPos, err := pgf.Mapper.PosPosition(pgf.Tok, origDecl.Name.NamePos)
if err != nil {
return nil, err
}
fh, err := snapshot.ReadFile(ctx, pgf.URI)
if err != nil {
return nil, err
}
refs, err = References(ctx, snapshot, fh, funcPos, false)
if err != nil {
return nil, fmt.Errorf("finding references to rewrite: %v", err)
}
}
// Type-check the narrowest package containing each reference.
// TODO(rfindley): we should expose forEachPackage in order to operate in
// parallel and to reduce peak memory for this operation.
var (
pkgForRef = make(map[protocol.Location]PackageID)
pkgs = make(map[PackageID]*cache.Package)
)
{
needPkgs := make(map[PackageID]struct{})
for _, ref := range refs {
md, err := NarrowestMetadataForFile(ctx, snapshot, ref.URI)
if err != nil {
return nil, fmt.Errorf("finding ref metadata: %v", err)
}
pkgForRef[ref] = md.ID
needPkgs[md.ID] = struct{}{}
}
var pkgIDs []PackageID
for id := range needPkgs { // TODO: use maps.Keys once it is available to us
pkgIDs = append(pkgIDs, id)
}
refPkgs, err := snapshot.TypeCheck(ctx, pkgIDs...)
if err != nil {
return nil, fmt.Errorf("type checking reference packages: %v", err)
}
for _, p := range refPkgs {
pkgs[p.Metadata().ID] = p
}
}
// Organize calls by top file declaration. Calls within a single file may
// affect each other, as the inlining edit may affect the surrounding scope
// or imports Therefore, when inlining subsequent calls in the same
// declaration, we must re-type check.
type fileCalls struct {
pkg *cache.Package
pgf *parsego.File
calls []*ast.CallExpr
}
refsByFile := make(map[protocol.DocumentURI]*fileCalls)
for _, ref := range refs {
refpkg := pkgs[pkgForRef[ref]]
pgf, err := refpkg.File(ref.URI)
if err != nil {
return nil, bug.Errorf("finding %s in %s: %v", ref.URI, refpkg.Metadata().ID, err)
}
start, end, err := pgf.RangePos(ref.Range)
if err != nil {
return nil, err // e.g. invalid range
}
// Look for the surrounding call expression.
var (
name *ast.Ident
call *ast.CallExpr
)
path, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
name, _ = path[0].(*ast.Ident)
if _, ok := path[1].(*ast.SelectorExpr); ok {
call, _ = path[2].(*ast.CallExpr)
} else {
call, _ = path[1].(*ast.CallExpr)
}
if name == nil || call == nil {
// TODO(rfindley): handle this case with eta-abstraction:
// a reference to the target function f in a non-call position
// use(f)
// is replaced by
// use(func(...) { f(...) })
return nil, fmt.Errorf("cannot inline: found non-call function reference %v", ref)
}
// Sanity check.
if obj := refpkg.TypesInfo().ObjectOf(name); obj == nil ||
obj.Name() != origDecl.Name.Name ||
obj.Pkg() == nil ||
obj.Pkg().Path() != string(pkg.Metadata().PkgPath) {
return nil, bug.Errorf("cannot inline: corrupted reference %v", ref)
}
callInfo, ok := refsByFile[ref.URI]
if !ok {
callInfo = &fileCalls{
pkg: refpkg,
pgf: pgf,
}
refsByFile[ref.URI] = callInfo
}
callInfo.calls = append(callInfo.calls, call)
}
// Inline each call within the same decl in sequence, re-typechecking after
// each one. If there is only a single call within the decl, we can avoid
// additional type checking.
//
// Assumption: inlining does not affect the package scope, so we can operate
// on separate files independently.
result := make(map[protocol.DocumentURI][]byte)
for uri, callInfo := range refsByFile {
var (
calls = callInfo.calls
fset = callInfo.pkg.FileSet()
tpkg = callInfo.pkg.Types()
tinfo = callInfo.pkg.TypesInfo()
file = callInfo.pgf.File
content = callInfo.pgf.Src
)
// Check for overlapping calls (such as Foo(Foo())). We can't handle these
// because inlining may change the source order of the inner call with
// respect to the inlined outer call, and so the heuristic we use to find
// the next call (counting from top-to-bottom) does not work.
for i := range calls {
if i > 0 && calls[i-1].End() > calls[i].Pos() {
return nil, fmt.Errorf("%s: can't inline overlapping call %s", uri, types.ExprString(calls[i-1]))
}
}
currentCall := 0
for currentCall < len(calls) {
caller := &inline.Caller{
Fset: fset,
Types: tpkg,
Info: tinfo,
File: file,
Call: calls[currentCall],
Content: content,
}
res, err := inline.Inline(caller, callee, &inline.Options{Logf: logf})
if err != nil {
return nil, fmt.Errorf("inlining failed: %v", err)
}
content = res.Content
if post != nil {
content = post(content)
}
if len(calls) <= 1 {
// No need to re-type check, as we've inlined all calls.
break
}
// TODO(rfindley): develop a theory of "trivial" inlining, which are
// inlinings that don't require re-type checking.
//
// In principle, if the inlining only involves replacing one call with
// another, the scope of the caller is unchanged and there is no need to
// type check again before inlining subsequent calls (edits should not
// overlap, and should not affect each other semantically). However, it
// feels sufficiently complicated that, to be safe, this optimization is
// deferred until later.
file, err = parser.ParseFile(fset, uri.Path(), content, parser.ParseComments|parser.SkipObjectResolution)
if err != nil {
return nil, bug.Errorf("inlined file failed to parse: %v", err)
}
// After inlining one call with a removed parameter, the package will
// fail to type check due to "not enough arguments". Therefore, we must
// allow type errors here.
//
// Assumption: the resulting type errors do not affect the correctness of
// subsequent inlining, because invalid arguments to a call do not affect
// anything in the surrounding scope.
//
// TODO(rfindley): improve this.
tpkg, tinfo, err = reTypeCheck(logf, callInfo.pkg, map[protocol.DocumentURI]*ast.File{uri: file}, true)
if err != nil {
return nil, bug.Errorf("type checking after inlining failed: %v", err)
}
// Collect calls to the target function in the modified declaration.
var calls2 []*ast.CallExpr
ast.Inspect(file, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
fn := typeutil.StaticCallee(tinfo, call)
if fn != nil && fn.Pkg().Path() == string(pkg.Metadata().PkgPath) && fn.Name() == origDecl.Name.Name {
calls2 = append(calls2, call)
}
}
return true
})
// If the number of calls has increased, this process will never cease.
// If the number of calls has decreased, assume that inlining removed a
// call.
// If the number of calls didn't change, assume that inlining replaced
// a call, and move on to the next.
//
// Assumption: we're inlining a call that has at most one recursive
// reference (which holds for signature rewrites).
//
// TODO(rfindley): this isn't good enough. We should be able to support
// inlining all existing calls even if they increase calls. How do we
// correlate the before and after syntax?
switch {
case len(calls2) > len(calls):
return nil, fmt.Errorf("inlining increased calls %d->%d, possible recursive call? content:\n%s", len(calls), len(calls2), content)
case len(calls2) < len(calls):
calls = calls2
case len(calls2) == len(calls):
calls = calls2
currentCall++
}
}
result[callInfo.pgf.URI] = content
}
return result, nil
}
|