blob: 1519ef85d80fee8194fff7b7669e8d140438d81c [file] [log] [blame]
// 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 source
// This file defines the refactor.inline code action.
import (
"context"
"fmt"
"go/ast"
"go/token"
"go/types"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/gopls/internal/lsp/cache"
"golang.org/x/tools/gopls/internal/lsp/cache/parsego"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/util/safetoken"
"golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/refactor/inline"
)
// EnclosingStaticCall returns the innermost function call enclosing
// the selected range, along with the callee.
func EnclosingStaticCall(pkg *cache.Package, pgf *ParsedGoFile, start, end token.Pos) (*ast.CallExpr, *types.Func, error) {
path, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
var call *ast.CallExpr
loop:
for _, n := range path {
switch n := n.(type) {
case *ast.FuncLit:
break loop
case *ast.CallExpr:
call = n
break loop
}
}
if call == nil {
return nil, nil, fmt.Errorf("no enclosing call")
}
if safetoken.Line(pgf.Tok, call.Lparen) != safetoken.Line(pgf.Tok, start) {
return nil, nil, fmt.Errorf("enclosing call is not on this line")
}
fn := typeutil.StaticCallee(pkg.GetTypesInfo(), call)
if fn == nil {
return nil, nil, fmt.Errorf("not a static call to a Go function")
}
return call, fn, nil
}
func inlineCall(ctx context.Context, snapshot *cache.Snapshot, callerPkg *cache.Package, callerPGF *parsego.File, start, end token.Pos) (_ *token.FileSet, _ *analysis.SuggestedFix, err error) {
// Find enclosing static call.
call, fn, err := EnclosingStaticCall(callerPkg, callerPGF, start, end)
if err != nil {
return nil, nil, err
}
// Locate callee by file/line and analyze it.
calleePosn := safetoken.StartPosition(callerPkg.FileSet(), fn.Pos())
calleePkg, calleePGF, err := NarrowestPackageForFile(ctx, snapshot, protocol.URIFromPath(calleePosn.Filename))
if err != nil {
return nil, nil, err
}
var calleeDecl *ast.FuncDecl
for _, decl := range calleePGF.File.Decls {
if decl, ok := decl.(*ast.FuncDecl); ok {
posn := safetoken.StartPosition(calleePkg.FileSet(), decl.Name.Pos())
if posn.Line == calleePosn.Line && posn.Column == calleePosn.Column {
calleeDecl = decl
break
}
}
}
if calleeDecl == nil {
return nil, nil, fmt.Errorf("can't find callee")
}
// The inliner assumes that input is well-typed,
// but that is frequently not the case within gopls.
// Until we are able to harden the inliner,
// report panics as errors to avoid crashing the server.
bad := func(p *cache.Package) bool { return len(p.GetParseErrors())+len(p.GetTypeErrors()) > 0 }
if bad(calleePkg) || bad(callerPkg) {
defer func() {
if x := recover(); x != nil {
err = fmt.Errorf("inlining failed (%q), likely because inputs were ill-typed", x)
}
}()
}
// Users can consult the gopls event log to see
// why a particular inlining strategy was chosen.
logf := logger(ctx, "inliner", snapshot.Options().VerboseOutput)
callee, err := inline.AnalyzeCallee(logf, calleePkg.FileSet(), calleePkg.GetTypes(), calleePkg.GetTypesInfo(), calleeDecl, calleePGF.Src)
if err != nil {
return nil, nil, err
}
// Inline the call.
caller := &inline.Caller{
Fset: callerPkg.FileSet(),
Types: callerPkg.GetTypes(),
Info: callerPkg.GetTypesInfo(),
File: callerPGF.File,
Call: call,
Content: callerPGF.Src,
}
got, err := inline.Inline(logf, caller, callee)
if err != nil {
return nil, nil, err
}
return callerPkg.FileSet(), &analysis.SuggestedFix{
Message: fmt.Sprintf("inline call of %v", callee),
TextEdits: diffToTextEdits(callerPGF.Tok, diff.Bytes(callerPGF.Src, got)),
}, nil
}
// TODO(adonovan): change the inliner to instead accept an io.Writer.
func logger(ctx context.Context, name string, verbose bool) func(format string, args ...any) {
if verbose {
return func(format string, args ...any) {
event.Log(ctx, name+": "+fmt.Sprintf(format, args...))
}
} else {
return func(string, ...any) {}
}
}