| // 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 |
| } |