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  }