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  }