internal/refactor/inline: an inliner for Go source
This change creates at new (internal) package that
implements an inlining algorithm for Go functions,
and an analyzer in the go/analysis framework that
uses it to perform automatic inlining of calls to
specially annotated ("inlineme") functions.
Run this command to invoke the analyzer and
apply any suggested fixes to the source tree:
$ go run ./internal/refactor/inline/analyzer/main.go -fix packages...
The package is intended for use both in interactive tools
such as gopls and batch tools such as the analyzer
just mentioned and the tool proposed in the attached issue.
As noted in the code comments, correct inlining is a
surprisingly tricky problem, so for now we primarily
address the most general case in which a call f(args...)
has the function name f replaced by a literal copy of the
called function (func (...) {...})(args...).
Only in the simplest of special cases is the call
itself eliminated by replacing it with the function body.
There is much further work to do by careful analysis of cases.
The processing of the callee function occurs first,
and results in a serializable summary of the callee
that can be used for a later call to Inline, possibly
from a different process, thus enabling "separate analysis"
pipelines using the analysis.Fact mechanism.
Recommended reviewing order:
- callee.go, inline.go
- inline_test.go, testdata/*txtar
- analyzer/...
Updates golang/go#32816
Change-Id: If28e43a6ba9ab92639276c5b50b5a89a3b0c54c4
Reviewed-on: https://go-review.googlesource.com/c/tools/+/519715
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Alan Donovan <adonovan@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/internal/refactor/inline/analyzer/analyzer.go b/internal/refactor/inline/analyzer/analyzer.go
new file mode 100644
index 0000000..2356fa4
--- /dev/null
+++ b/internal/refactor/inline/analyzer/analyzer.go
@@ -0,0 +1,161 @@
+// 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 analyzer
+
+import (
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "os"
+ "strings"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/inspector"
+ "golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/diff"
+ "golang.org/x/tools/internal/refactor/inline"
+)
+
+const Doc = `inline calls to functions with "inlineme" doc comment`
+
+var Analyzer = &analysis.Analyzer{
+ Name: "inline",
+ Doc: Doc,
+ URL: "https://pkg.go.dev/golang.org/x/tools/internal/refactor/inline/analyzer",
+ Run: run,
+ FactTypes: []analysis.Fact{new(inlineMeFact)},
+ Requires: []*analysis.Analyzer{inspect.Analyzer},
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+ // Memoize repeated calls for same file.
+ // TODO(adonovan): the analysis.Pass should abstract this (#62292)
+ // as the driver may not be reading directly from the file system.
+ fileContent := make(map[string][]byte)
+ readFile := func(node ast.Node) ([]byte, error) {
+ filename := pass.Fset.File(node.Pos()).Name()
+ content, ok := fileContent[filename]
+ if !ok {
+ var err error
+ content, err = os.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ fileContent[filename] = content
+ }
+ return content, nil
+ }
+
+ // Pass 1: find functions annotated with an "inlineme"
+ // comment, and export a fact for each one.
+ inlinable := make(map[*types.Func]*inline.Callee) // memoization of fact import (nil => no fact)
+ for _, file := range pass.Files {
+ for _, decl := range file.Decls {
+ if decl, ok := decl.(*ast.FuncDecl); ok {
+ // TODO(adonovan): this is just a placeholder.
+ // Use the precise go:fix syntax in the proposal.
+ // Beware that //go: comments are treated specially
+ // by (*ast.CommentGroup).Text().
+ // TODO(adonovan): alternatively, consider using
+ // the universal annotation mechanism sketched in
+ // https://go.dev/cl/489835 (which doesn't yet have
+ // a proper proposal).
+ if strings.Contains(decl.Doc.Text(), "inlineme") {
+ content, err := readFile(file)
+ if err != nil {
+ pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: cannot read source file: %v", err)
+ continue
+ }
+ callee, err := inline.AnalyzeCallee(pass.Fset, pass.Pkg, pass.TypesInfo, decl, content)
+ if err != nil {
+ pass.Reportf(decl.Doc.Pos(), "invalid inlining candidate: %v", err)
+ continue
+ }
+ fn := pass.TypesInfo.Defs[decl.Name].(*types.Func)
+ pass.ExportObjectFact(fn, &inlineMeFact{callee})
+ inlinable[fn] = callee
+ }
+ }
+ }
+ }
+
+ // Pass 2. Inline each static call to an inlinable function.
+ inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ nodeFilter := []ast.Node{
+ (*ast.File)(nil),
+ (*ast.CallExpr)(nil),
+ }
+ var currentFile *ast.File
+ inspect.Preorder(nodeFilter, func(n ast.Node) {
+ if file, ok := n.(*ast.File); ok {
+ currentFile = file
+ return
+ }
+ call := n.(*ast.CallExpr)
+ if fn := typeutil.StaticCallee(pass.TypesInfo, call); fn != nil {
+ // Inlinable?
+ callee, ok := inlinable[fn]
+ if !ok {
+ var fact inlineMeFact
+ if pass.ImportObjectFact(fn, &fact) {
+ callee = fact.callee
+ inlinable[fn] = callee
+ }
+ }
+ if callee == nil {
+ return // nope
+ }
+
+ // Inline the call.
+ content, err := readFile(call)
+ if err != nil {
+ pass.Reportf(call.Lparen, "invalid inlining candidate: cannot read source file: %v", err)
+ return
+ }
+ caller := &inline.Caller{
+ Fset: pass.Fset,
+ Types: pass.Pkg,
+ Info: pass.TypesInfo,
+ File: currentFile,
+ Call: call,
+ Content: content,
+ }
+ got, err := inline.Inline(caller, callee)
+ if err != nil {
+ pass.Reportf(call.Lparen, "%v", err)
+ return
+ }
+
+ // Suggest the "fix".
+ var textEdits []analysis.TextEdit
+ for _, edit := range diff.Bytes(content, got) {
+ textEdits = append(textEdits, analysis.TextEdit{
+ Pos: currentFile.FileStart + token.Pos(edit.Start),
+ End: currentFile.FileStart + token.Pos(edit.End),
+ NewText: []byte(edit.New),
+ })
+ }
+ msg := fmt.Sprintf("inline call of %v", callee)
+ pass.Report(analysis.Diagnostic{
+ Pos: call.Pos(),
+ End: call.End(),
+ Message: msg,
+ SuggestedFixes: []analysis.SuggestedFix{{
+ Message: msg,
+ TextEdits: textEdits,
+ }},
+ })
+ }
+ })
+
+ return nil, nil
+}
+
+type inlineMeFact struct{ callee *inline.Callee }
+
+func (f *inlineMeFact) String() string { return "inlineme " + f.callee.String() }
+func (*inlineMeFact) AFact() {}
diff --git a/internal/refactor/inline/analyzer/analyzer_test.go b/internal/refactor/inline/analyzer/analyzer_test.go
new file mode 100644
index 0000000..5ad85cf
--- /dev/null
+++ b/internal/refactor/inline/analyzer/analyzer_test.go
@@ -0,0 +1,16 @@
+// Copyright 2018 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 analyzer_test
+
+import (
+ "testing"
+
+ "golang.org/x/tools/go/analysis/analysistest"
+ inlineanalyzer "golang.org/x/tools/internal/refactor/inline/analyzer"
+)
+
+func TestAnalyzer(t *testing.T) {
+ analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), inlineanalyzer.Analyzer, "a", "b")
+}
diff --git a/internal/refactor/inline/analyzer/main.go b/internal/refactor/inline/analyzer/main.go
new file mode 100644
index 0000000..4be223a
--- /dev/null
+++ b/internal/refactor/inline/analyzer/main.go
@@ -0,0 +1,19 @@
+// 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.
+
+//go:build ignore
+// +build ignore
+
+// The inline command applies the inliner to the specified packages of
+// Go source code. Run with:
+//
+// $ go run ./internal/refactor/inline/analyzer/main.go -fix packages...
+package main
+
+import (
+ "golang.org/x/tools/go/analysis/singlechecker"
+ inlineanalyzer "golang.org/x/tools/internal/refactor/inline/analyzer"
+)
+
+func main() { singlechecker.Main(inlineanalyzer.Analyzer) }
diff --git a/internal/refactor/inline/analyzer/testdata/src/a/a.go b/internal/refactor/inline/analyzer/testdata/src/a/a.go
new file mode 100644
index 0000000..e661515
--- /dev/null
+++ b/internal/refactor/inline/analyzer/testdata/src/a/a.go
@@ -0,0 +1,16 @@
+package a
+
+func f() {
+ One() // want `inline call of a.One`
+ new(T).Two() // want `inline call of \(a.T\).Two`
+}
+
+type T struct{}
+
+// inlineme
+func One() int { return one } // want One:`inlineme a.One`
+
+const one = 1
+
+// inlineme
+func (T) Two() int { return 2 } // want Two:`inlineme \(a.T\).Two`
diff --git a/internal/refactor/inline/analyzer/testdata/src/a/a.go.golden b/internal/refactor/inline/analyzer/testdata/src/a/a.go.golden
new file mode 100644
index 0000000..fe9877b
--- /dev/null
+++ b/internal/refactor/inline/analyzer/testdata/src/a/a.go.golden
@@ -0,0 +1,16 @@
+package a
+
+func f() {
+ _ = one // want `inline call of a.One`
+ func(_ T) int { return 2 }(*new(T)) // want `inline call of \(a.T\).Two`
+}
+
+type T struct{}
+
+// inlineme
+func One() int { return one } // want One:`inlineme a.One`
+
+const one = 1
+
+// inlineme
+func (T) Two() int { return 2 } // want Two:`inlineme \(a.T\).Two`
diff --git a/internal/refactor/inline/analyzer/testdata/src/b/b.go b/internal/refactor/inline/analyzer/testdata/src/b/b.go
new file mode 100644
index 0000000..069e670
--- /dev/null
+++ b/internal/refactor/inline/analyzer/testdata/src/b/b.go
@@ -0,0 +1,9 @@
+package b
+
+import "a"
+
+func f() {
+ a.One() // want `cannot inline call to a.One because body refers to non-exported one`
+
+ new(a.T).Two() // want `inline call of \(a.T\).Two`
+}
diff --git a/internal/refactor/inline/analyzer/testdata/src/b/b.go.golden b/internal/refactor/inline/analyzer/testdata/src/b/b.go.golden
new file mode 100644
index 0000000..61b7bd9
--- /dev/null
+++ b/internal/refactor/inline/analyzer/testdata/src/b/b.go.golden
@@ -0,0 +1,9 @@
+package b
+
+import "a"
+
+func f() {
+ a.One() // want `cannot inline call to a.One because body refers to non-exported one`
+
+ func(_ a.T) int { return 2 }(*new(a.T)) // want `inline call of \(a.T\).Two`
+}
diff --git a/internal/refactor/inline/callee.go b/internal/refactor/inline/callee.go
new file mode 100644
index 0000000..291971c
--- /dev/null
+++ b/internal/refactor/inline/callee.go
@@ -0,0 +1,350 @@
+// 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 inline
+
+// This file defines the analysis of the callee function.
+
+import (
+ "bytes"
+ "encoding/gob"
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "go/types"
+
+ "golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/typeparams"
+)
+
+// A Callee holds information about an inlinable function. Gob-serializable.
+type Callee struct {
+ impl gobCallee
+}
+
+func (callee *Callee) String() string { return callee.impl.Name }
+
+type gobCallee struct {
+ Content []byte // file content, compacted to a single func decl
+
+ // syntax derived from compacted Content (not serialized)
+ fset *token.FileSet
+ decl *ast.FuncDecl
+
+ // results of type analysis (does not reach go/types data structures)
+ PkgPath string // package path of declaring package
+ Name string // user-friendly name for error messages
+ Unexported []string // names of free objects that are unexported
+ FreeRefs []freeRef // locations of references to free objects
+ FreeObjs []object // descriptions of free objects
+ BodyIsReturnExpr bool // function body is "return expr(s)"
+ ValidForCallStmt bool // => bodyIsReturnExpr and sole expr is f() or <-ch
+ NumResults int // number of results (according to type, not ast.FieldList)
+}
+
+// A freeRef records a reference to a free object. Gob-serializable.
+type freeRef struct {
+ Start, End int // Callee.content[start:end] is extent of the reference
+ Object int // index into Callee.freeObjs
+}
+
+// An object abstracts a free types.Object referenced by the callee. Gob-serializable.
+type object struct {
+ Name string // Object.Name()
+ Kind string // one of {var,func,const,type,pkgname,nil,builtin}
+ PkgPath string // pkgpath of object (or of imported package if kind="pkgname")
+ ValidPos bool // Object.Pos().IsValid()
+}
+
+func (callee *gobCallee) offset(pos token.Pos) int { return offsetOf(callee.fset, pos) }
+
+// AnalyzeCallee analyzes a function that is a candidate for inlining
+// and returns a Callee that describes it. The Callee object, which is
+// serializable, can be passed to one or more subsequent calls to
+// Inline, each with a different Caller.
+//
+// This design allows separate analysis of callers and callees in the
+// golang.org/x/tools/go/analysis framework: the inlining information
+// about a callee can be recorded as a "fact".
+func AnalyzeCallee(fset *token.FileSet, pkg *types.Package, info *types.Info, decl *ast.FuncDecl, content []byte) (*Callee, error) {
+
+ // The client is expected to have determined that the callee
+ // is a function with a declaration (not a built-in or var).
+ fn := info.Defs[decl.Name].(*types.Func)
+ sig := fn.Type().(*types.Signature)
+
+ // Create user-friendly name ("pkg.Func" or "(pkg.T).Method")
+ var name string
+ if sig.Recv() == nil {
+ name = fmt.Sprintf("%s.%s", fn.Pkg().Name(), fn.Name())
+ } else {
+ name = fmt.Sprintf("(%s).%s", types.TypeString(sig.Recv().Type(), (*types.Package).Name), fn.Name())
+ }
+
+ if decl.Body == nil {
+ return nil, fmt.Errorf("cannot inline function %s as it has no body", name)
+ }
+
+ // TODO(adonovan): support inlining of instantiated generic
+ // functions by replacing each occurrence of a type parameter
+ // T by its instantiating type argument (e.g. int). We'll need
+ // to wrap the instantiating type in parens when it's not an
+ // ident or qualified ident to prevent "if x == struct{}"
+ // parsing ambiguity, or "T(x)" where T = "*int" or "func()"
+ // from misparsing.
+ if decl.Type.TypeParams != nil {
+ return nil, fmt.Errorf("cannot inline generic function %s: type parameters are not yet supported", name)
+ }
+
+ // Record the location of all free references in the callee body.
+ var (
+ freeObjIndex = make(map[types.Object]int)
+ freeObjs []object
+ freeRefs []freeRef // free refs that may need renaming
+ unexported []string // free refs to unexported objects, for later error checks
+ )
+ var visit func(n ast.Node) bool
+ visit = func(n ast.Node) bool {
+ switch n := n.(type) {
+ case *ast.SelectorExpr:
+ // Check selections of free fields/methods.
+ if sel, ok := info.Selections[n]; ok &&
+ !within(sel.Obj().Pos(), decl) &&
+ !n.Sel.IsExported() {
+ sym := fmt.Sprintf("(%s).%s", info.TypeOf(n.X), n.Sel.Name)
+ unexported = append(unexported, sym)
+ }
+
+ // Don't recur into SelectorExpr.Sel.
+ visit(n.X)
+ return false
+
+ case *ast.CompositeLit:
+ // Check for struct literals that refer to unexported fields,
+ // whether keyed or unkeyed. (Logic assumes well-typedness.)
+ litType := deref(info.TypeOf(n))
+ if s, ok := typeparams.CoreType(litType).(*types.Struct); ok {
+ for i, elt := range n.Elts {
+ var field *types.Var
+ var value ast.Expr
+ if kv, ok := elt.(*ast.KeyValueExpr); ok {
+ field = info.Uses[kv.Key.(*ast.Ident)].(*types.Var)
+ value = kv.Value
+ } else {
+ field = s.Field(i)
+ value = elt
+ }
+ if !within(field.Pos(), decl) && !field.Exported() {
+ sym := fmt.Sprintf("(%s).%s", litType, field.Name())
+ unexported = append(unexported, sym)
+ }
+
+ // Don't recur into KeyValueExpr.Key.
+ visit(value)
+ }
+ return false
+ }
+
+ case *ast.Ident:
+ if obj, ok := info.Uses[n]; ok {
+ // Methods and fields are handled by SelectorExpr and CompositeLit.
+ if isField(obj) || isMethod(obj) {
+ panic(obj)
+ }
+ // Inv: id is a lexical reference.
+
+ // A reference to an unexported package-level declaration
+ // cannot be inlined into another package.
+ if !n.IsExported() &&
+ obj.Pkg() != nil && obj.Parent() == obj.Pkg().Scope() {
+ unexported = append(unexported, n.Name)
+ }
+
+ // Record free reference.
+ if !within(obj.Pos(), decl) {
+ objidx, ok := freeObjIndex[obj]
+ if !ok {
+ objidx = len(freeObjIndex)
+ var pkgpath string
+ if pkgname, ok := obj.(*types.PkgName); ok {
+ pkgpath = pkgname.Imported().Path()
+ } else if obj.Pkg() != nil {
+ pkgpath = obj.Pkg().Path()
+ }
+ freeObjs = append(freeObjs, object{
+ Name: obj.Name(),
+ Kind: objectKind(obj),
+ PkgPath: pkgpath,
+ ValidPos: obj.Pos().IsValid(),
+ })
+ freeObjIndex[obj] = objidx
+ }
+ freeRefs = append(freeRefs, freeRef{
+ Start: offsetOf(fset, n.Pos()),
+ End: offsetOf(fset, n.End()),
+ Object: objidx,
+ })
+ }
+ }
+ }
+ return true
+ }
+ ast.Inspect(decl, visit)
+
+ // Analyze callee body for "return results" form, where
+ // results is one or more expressions or an n-ary call.
+ validForCallStmt := false
+ bodyIsReturnExpr := decl.Type.Results != nil && len(decl.Type.Results.List) > 0 &&
+ len(decl.Body.List) == 1 &&
+ is[*ast.ReturnStmt](decl.Body.List[0]) &&
+ len(decl.Body.List[0].(*ast.ReturnStmt).Results) > 0
+ if bodyIsReturnExpr {
+ ret := decl.Body.List[0].(*ast.ReturnStmt)
+
+ // Ascertain whether the results expression(s)
+ // would be safe to inline as a standalone statement.
+ // (This is true only for a single call or receive expression.)
+ validForCallStmt = func() bool {
+ if len(ret.Results) == 1 {
+ switch expr := astutil.Unparen(ret.Results[0]).(type) {
+ case *ast.CallExpr: // f(x)
+ callee := typeutil.Callee(info, expr)
+ if callee == nil {
+ return false // conversion T(x)
+ }
+
+ // The only non-void built-in functions that may be
+ // called as a statement are copy and recover
+ // (though arguably a call to recover should never
+ // be inlined as that changes its behavior).
+ if builtin, ok := callee.(*types.Builtin); ok {
+ return builtin.Name() == "copy" ||
+ builtin.Name() == "recover"
+ }
+
+ return true // ordinary call f()
+
+ case *ast.UnaryExpr: // <-x
+ return expr.Op == token.ARROW // channel receive <-ch
+ }
+ }
+
+ // No other expressions are valid statements.
+ return false
+ }()
+ }
+
+ // As a space optimization, we don't retain the complete
+ // callee file content; all we need is "package _; func f() { ... }".
+ // This reduces the size of analysis facts.
+ //
+ // The FileSet file/line info is no longer meaningful
+ // and should not be used in error messages.
+ // But the FileSet offsets are valid w.r.t. the content.
+ //
+ // (For ease of debugging we could insert a //line directive after
+ // the package decl but it seems more trouble than it's worth.)
+ {
+ start, end := offsetOf(fset, decl.Pos()), offsetOf(fset, decl.End())
+
+ var compact bytes.Buffer
+ compact.WriteString("package _\n")
+ compact.Write(content[start:end])
+ content = compact.Bytes()
+
+ // Re-parse the compacted content.
+ var err error
+ decl, err = parseCompact(fset, content)
+ if err != nil {
+ return nil, err
+ }
+
+ // (content, decl) are now updated.
+
+ // Adjust the freeRefs offsets.
+ delta := int(offsetOf(fset, decl.Pos()) - start)
+ for i := range freeRefs {
+ freeRefs[i].Start += delta
+ freeRefs[i].End += delta
+ }
+ }
+
+ return &Callee{gobCallee{
+ Content: content,
+ fset: fset,
+ decl: decl,
+ PkgPath: pkg.Path(),
+ Name: name,
+ Unexported: unexported,
+ FreeObjs: freeObjs,
+ FreeRefs: freeRefs,
+ BodyIsReturnExpr: bodyIsReturnExpr,
+ ValidForCallStmt: validForCallStmt,
+ NumResults: sig.Results().Len(),
+ }}, nil
+}
+
+// parseCompact parses a Go source file of the form "package _\n func f() { ... }"
+// and returns the sole function declaration.
+func parseCompact(fset *token.FileSet, content []byte) (*ast.FuncDecl, error) {
+ const mode = parser.ParseComments | parser.SkipObjectResolution | parser.AllErrors
+ f, err := parser.ParseFile(fset, "callee.go", content, mode)
+ if err != nil {
+ return nil, fmt.Errorf("internal error: cannot compact file: %v", err)
+ }
+ return f.Decls[0].(*ast.FuncDecl), nil
+}
+
+// deref removes a pointer type constructor from the core type of t.
+func deref(t types.Type) types.Type {
+ if ptr, ok := typeparams.CoreType(t).(*types.Pointer); ok {
+ return ptr.Elem()
+ }
+ return t
+}
+
+func isField(obj types.Object) bool {
+ if v, ok := obj.(*types.Var); ok && v.IsField() {
+ return true
+ }
+ return false
+}
+
+func isMethod(obj types.Object) bool {
+ if f, ok := obj.(*types.Func); ok && f.Type().(*types.Signature).Recv() != nil {
+ return true
+ }
+ return false
+}
+
+// -- serialization --
+
+var (
+ _ gob.GobEncoder = (*Callee)(nil)
+ _ gob.GobDecoder = (*Callee)(nil)
+)
+
+func (callee *Callee) GobEncode() ([]byte, error) {
+ var out bytes.Buffer
+ if err := gob.NewEncoder(&out).Encode(callee.impl); err != nil {
+ return nil, err
+ }
+ return out.Bytes(), nil
+}
+
+func (callee *Callee) GobDecode(data []byte) error {
+ if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&callee.impl); err != nil {
+ return err
+ }
+ fset := token.NewFileSet()
+ decl, err := parseCompact(fset, callee.impl.Content)
+ if err != nil {
+ return err
+ }
+ callee.impl.fset = fset
+ callee.impl.decl = decl
+ return nil
+}
diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go
new file mode 100644
index 0000000..9167498
--- /dev/null
+++ b/internal/refactor/inline/inline.go
@@ -0,0 +1,688 @@
+// 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 inline implements inlining of Go function calls.
+//
+// The client provides information about the caller and callee,
+// including the source text, syntax tree, and type information, and
+// the inliner returns the modified source file for the caller, or an
+// error if the inlining operation is invalid (for example because the
+// function body refers to names that are inaccessible to the caller).
+//
+// Although this interface demands more information from the client
+// than might seem necessary, it enables smoother integration with
+// existing batch and interactive tools that have their own ways of
+// managing the processes of reading, parsing, and type-checking
+// packages. In particular, this package does not assume that the
+// caller and callee belong to the same token.FileSet or
+// types.Importer realms.
+//
+// In general, inlining consists of modifying a function or method
+// call expression f(a1, ..., an) so that the name of the function f
+// is replaced ("literalized") by a literal copy of the function
+// declaration, with free identifiers suitably modified to use the
+// locally appropriate identifiers or perhaps constant argument
+// values.
+//
+// Inlining must not change the semantics of the call. Semantics
+// preservation is crucial for clients such as codebase maintenance
+// tools that automatically inline all calls to designated functions
+// on a large scale. Such tools must not introduce subtle behavior
+// changes. (Fully inlining a call is dynamically observable using
+// reflection over the call stack, but this exception to the rule is
+// explicitly allowed.)
+//
+// In some special cases it is possible to entirely replace ("reduce")
+// the call by a copy of the function's body in which parameters have
+// been replaced by arguments, but this is surprisingly tricky for a
+// number of reasons, some of which are listed here for illustration:
+//
+// - Any effects of the call argument expressions must be preserved,
+// even if the corresponding parameters are never referenced, or are
+// referenced multiple times, or are referenced in a different order
+// from the arguments.
+//
+// - Even an argument expression as simple as ptr.x may not be
+// referentially transparent, because another argument may have the
+// effect of changing the value of ptr.
+//
+// - Although constants are referentially transparent, as a matter of
+// style we do not wish to duplicate literals that are referenced
+// multiple times in the body because this undoes proper factoring.
+// Also, string literals may be arbitrarily large.
+//
+// - If the function body consists of statements other than just
+// "return expr", in some contexts it may be syntactically
+// impossible to replace the call expression by the body statements.
+// Consider "} else if x := f(); cond { ... }".
+// (Go has no equivalent to Lisp's progn or Rust's blocks.)
+//
+// - Similarly, without the equivalent of Rust-style blocks and
+// first-class tuples, there is no general way to reduce a call
+// to a function such as
+// > func(params)(args)(results) { stmts; return body }
+// to an expression such as
+// > { var params = args; stmts; body }
+// or even a statement such as
+// > results = { var params = args; stmts; body }
+// Consequently the declaration and scope of the result variables,
+// and the assignment and control-flow implications of the return
+// statement, must be dealt with by cases.
+//
+// - A standalone call statement that calls a function whose body is
+// "return expr" cannot be simply replaced by the body expression
+// if it is not itself a call or channel receive expression; it is
+// necessary to explicitly discard the result using "_ = expr".
+//
+// Similarly, if the body is a call expression, only calls to some
+// built-in functions with no result (such as copy or panic) are
+// permitted as statements, whereas others (such as append) return
+// a result that must be used, even if just by discarding.
+//
+// - If a parameter or result variable is updated by an assignment
+// within the function body, it cannot always be safely replaced
+// by a variable in the caller. For example, given
+// > func f(a int) int { a++; return a }
+// The call y = f(x) cannot be replaced by { x++; y = x } because
+// this would change the value of the caller's variable x.
+// Only if the caller is finished with x is this safe.
+//
+// A similar argument applies to parameter or result variables
+// that escape: by eliminating a variable, inlining would change
+// the identity of the variable that escapes.
+//
+// - If the function body uses 'defer' and the inlined call is not a
+// tail-call, inlining may delay the deferred effects.
+//
+// - Each control label that is used by both caller and callee must
+// be α-renamed.
+//
+// - Given
+// > func f() uint8 { return 0 }
+// > var x any = f()
+// reducing the call to var x any = 0 is unsound because it
+// discards the implicit conversion. We may need to make each
+// argument->parameter and return->result assignment conversion
+// implicit if the types differ. Assignments to variadic
+// parameters may need to explicitly construct a slice.
+//
+// More complex callee functions are inlinable with more elaborate and
+// invasive changes to the statements surrounding the call expression.
+//
+// TODO(adonovan): future work:
+//
+// - Handle more of the above special cases by careful analysis,
+// thoughtful factoring of the large design space, and thorough
+// test coverage.
+//
+// - Write a fuzz-like test that selects function calls at
+// random in the corpus, inlines them, and checks that the
+// result is either a sensible error or a valid transformation.
+//
+// - Eliminate parameters that are unreferenced in the callee
+// and whose argument expression is side-effect free.
+//
+// - Afford the client more control such as a limit on the total
+// increase in line count, or a refusal to inline using the
+// general approach (replacing name by function literal). This
+// could be achieved by returning metadata alongside the result
+// and having the client conditionally discard the change.
+//
+// - Is it acceptable to skip effects that are limited to runtime
+// panics? Can we avoid evaluating an argument x.f
+// or a[i] when the corresponding parameter is unused?
+//
+// - When caller syntax permits a block, replace argument-to-parameter
+// assignment by a set of local var decls, e.g. f(1, 2) would
+// become { var x, y = 1, 2; body... }.
+//
+// But even this is complicated: a single var decl initializer
+// cannot declare all the parameters and initialize them to their
+// arguments in one go if they have varied types. Instead,
+// one must use multiple specs such as:
+// > { var x int = 1; var y int32 = 2; body ...}
+// but this means that the initializer expression for y is
+// within the scope of x, so it may require α-renaming.
+//
+// It is tempting to use a short var decl { x, y := 1, 2; body ...}
+// as it permits simultaneous declaration and initialization
+// of many variables of varied type. However, one must take care
+// to convert each argument expression to the correct parameter
+// variable type, perhaps explicitly. (Consider "x := 1 << 64".)
+//
+// Also, as a matter of style, having all parameter declarations
+// and argument expressions in a single statement is potentially
+// unwieldy.
+//
+// - Support inlining of generic functions, replacing type parameters
+// by their instantiations.
+//
+// - Support inlining of calls to function literals such as:
+// > f := func(...) { ...}
+// > f()
+// including recursive ones:
+// > var f func(...)
+// > f = func(...) { ...f...}
+// > f()
+// But note that the existing algorithm makes widespread assumptions
+// that the callee is a package-level function or method.
+//
+// - Eliminate parens inserted conservatively when they are redundant.
+//
+// - Allow non-'go' build systems such as Bazel/Blaze a chance to
+// decide whether an import is accessible using logic other than
+// "/internal/" path segments. This could be achieved by returning
+// the list of added import paths.
+//
+// - Inlining a function from another module may change the
+// effective version of the Go language spec that governs it. We
+// should probably make the client responsible for rejecting
+// attempts to inline from newer callees to older callers, since
+// there's no way for this package to access module versions.
+//
+// - Use an alternative implementation of the import-organizing
+// operation that doesn't require operating on a complete file
+// (and reformatting). Then return the results in a higher-level
+// form as a set of import additions and deletions plus a single
+// diff that encloses the call expression. This interface could
+// perhaps be implemented atop imports.Process by post-processing
+// its result to obtain the abstract import changes and discarding
+// its formatted output.
+package inline
+
+import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "log"
+ pathpkg "path"
+ "reflect"
+ "sort"
+ "strings"
+
+ "golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/imports"
+ "golang.org/x/tools/internal/typeparams"
+)
+
+// A Caller describes the function call and its enclosing context.
+//
+// The client is responsible for populating this struct and passing it to Inline.
+type Caller struct {
+ Fset *token.FileSet
+ Types *types.Package
+ Info *types.Info
+ File *ast.File
+ Call *ast.CallExpr
+ Content []byte
+}
+
+func (caller *Caller) offset(pos token.Pos) int { return offsetOf(caller.Fset, pos) }
+
+// Inline inlines the called function (callee) into the function call (caller)
+// and returns the updated, formatted content of the caller source file.
+func Inline(caller *Caller, callee_ *Callee) ([]byte, error) {
+ callee := &callee_.impl
+
+ // -- check caller --
+
+ // Inlining of dynamic calls is not currently supported,
+ // even for local closure calls.
+ if typeutil.StaticCallee(caller.Info, caller.Call) == nil {
+ // e.g. interface method
+ return nil, fmt.Errorf("cannot inline: not a static function call")
+ }
+
+ // Reject cross-package inlining if callee has
+ // free references to unexported symbols.
+ samePkg := caller.Types.Path() == callee.PkgPath
+ if !samePkg && len(callee.Unexported) > 0 {
+ return nil, fmt.Errorf("cannot inline call to %s because body refers to non-exported %s",
+ callee.Name, callee.Unexported[0])
+ }
+
+ // -- analyze callee's free references in caller context --
+
+ // syntax path enclosing Call, innermost first (Path[0]=Call)
+ callerPath, _ := astutil.PathEnclosingInterval(caller.File, caller.Call.Pos(), caller.Call.End())
+ callerLookup := func(name string, pos token.Pos) types.Object {
+ for _, n := range callerPath {
+ // The function body scope (containing not just params)
+ // is associated with FuncDecl.Type, not FuncDecl.Body.
+ if decl, ok := n.(*ast.FuncDecl); ok {
+ n = decl.Type
+ }
+ if scope := caller.Info.Scopes[n]; scope != nil {
+ if _, obj := scope.LookupParent(name, pos); obj != nil {
+ return obj
+ }
+ }
+ }
+ return nil
+ }
+
+ // Import map, initially populated with caller imports.
+ //
+ // For simplicity we ignore existing dot imports, so that a
+ // qualified identifier (QI) in the callee is always
+ // represented by a QI in the caller, allowing us to treat a
+ // QI like a selection on a package name.
+ importMap := make(map[string]string) // maps package path to local name
+ for _, imp := range caller.File.Imports {
+ if pkgname, ok := importedPkgName(caller.Info, imp); ok && pkgname.Name() != "." {
+ importMap[pkgname.Imported().Path()] = pkgname.Name()
+ }
+ }
+
+ // localImportName returns the local name for a given imported package path.
+ var newImports []string
+ localImportName := func(path string) string {
+ name, ok := importMap[path]
+ if !ok {
+ // import added by callee
+ //
+ // Choose local PkgName based on last segment of
+ // package path plus, if needed, a numeric suffix to
+ // ensure uniqueness.
+ //
+ // TODO(adonovan): preserve the PkgName used
+ // in the original source, or, for a dot import,
+ // use the package's declared name.
+ base := pathpkg.Base(path)
+ name = base
+ for n := 0; callerLookup(name, caller.Call.Pos()) != nil; n++ {
+ name = fmt.Sprintf("%s%d", base, n)
+ }
+
+ // TODO(adonovan): don't use a renaming import
+ // unless the local name differs from either
+ // the package name or the last segment of path.
+ // This requires that we tabulate (path, declared name, local name)
+ // triples for each package referenced by the callee.
+ newImports = append(newImports, fmt.Sprintf("%s %q", name, path))
+ importMap[path] = name
+ }
+ return name
+ }
+
+ // Compute the renaming of the callee's free identifiers.
+ objRenames := make([]string, len(callee.FreeObjs)) // "" => no rename
+ for i, obj := range callee.FreeObjs {
+ // obj is a free object of the callee.
+ //
+ // Possible cases are:
+ // - nil or a builtin
+ // => check not shadowed in caller.
+ // - package-level var/func/const/types
+ // => same package: check not shadowed in caller.
+ // => otherwise: import other package form a qualified identifier.
+ // (Unexported cross-package references were rejected already.)
+ // - type parameter
+ // => not yet supported
+ // - pkgname
+ // => import other package and use its local name.
+ //
+ // There can be no free references to labels, fields, or methods.
+
+ var newName string
+ if obj.Kind == "pkgname" {
+ // Use locally appropriate import, creating as needed.
+ newName = localImportName(obj.PkgPath) // imported package
+
+ } else if !obj.ValidPos {
+ // Built-in function, type, or nil: check not shadowed at caller.
+ found := callerLookup(obj.Name, caller.Call.Pos()) // can't fail
+ if found.Pos().IsValid() {
+ return nil, fmt.Errorf("cannot inline because built-in %q is shadowed in caller by a %s (line %d)",
+ obj.Name, objectKind(found),
+ caller.Fset.Position(found.Pos()).Line)
+ }
+
+ newName = obj.Name
+
+ } else {
+ // Must be reference to package-level var/func/const/type,
+ // since type parameters are not yet supported.
+ newName = obj.Name
+ qualify := false
+ if obj.PkgPath == callee.PkgPath {
+ // reference within callee package
+ if samePkg {
+ // Caller and callee are in same package.
+ // Check caller has not shadowed the decl.
+ found := callerLookup(obj.Name, caller.Call.Pos()) // can't fail
+ if !isPkgLevel(found) {
+ return nil, fmt.Errorf("cannot inline because %q is shadowed in caller by a %s (line %d)",
+ obj.Name, objectKind(found),
+ caller.Fset.Position(found.Pos()).Line)
+ }
+ } else {
+ // Cross-package reference.
+ qualify = true
+ }
+ } else {
+ // Reference to a package-level declaration
+ // in another package, without a qualified identifier:
+ // it must be a dot import.
+ qualify = true
+ }
+
+ // Form a qualified identifier, pkg.Name.
+ if qualify {
+ pkgName := localImportName(obj.PkgPath)
+ newName = pkgName + "." + newName
+ }
+ }
+ objRenames[i] = newName
+ }
+
+ // Compute edits to inlined callee.
+ type edit struct {
+ start, end int // byte offsets wrt callee.content
+ new string
+ }
+ var edits []edit
+
+ // Give explicit blank "_" names to all method parameters
+ // (including receiver) since we will make the receiver a regular
+ // parameter and one cannot mix named and unnamed parameters.
+ // e.g. func (T) f(int, string) -> (_ T, _ int, _ string)
+ if callee.decl.Recv != nil {
+ ensureNamed := func(params *ast.FieldList) {
+ for _, param := range params.List {
+ if param.Names == nil {
+ offset := callee.offset(param.Type.Pos())
+ edits = append(edits, edit{
+ start: offset,
+ end: offset,
+ new: "_ ",
+ })
+ }
+ }
+ }
+ ensureNamed(callee.decl.Recv)
+ ensureNamed(callee.decl.Type.Params)
+ }
+
+ // Generate replacements for each free identifier.
+ for _, ref := range callee.FreeRefs {
+ if repl := objRenames[ref.Object]; repl != "" {
+ edits = append(edits, edit{
+ start: ref.Start,
+ end: ref.End,
+ new: repl,
+ })
+ }
+ }
+
+ // Edits are non-overlapping but insertions and edits may be coincident.
+ // Preserve original order.
+ sort.SliceStable(edits, func(i, j int) bool {
+ return edits[i].start < edits[j].start
+ })
+
+ // Check that all imports (in particular, the new ones) are accessible.
+ // TODO(adonovan): allow customization of the accessibility relation (e.g. for Bazel).
+ for path := range importMap {
+ // TODO(adonovan): better segment hygiene.
+ if i := strings.Index(path, "/internal/"); i >= 0 {
+ if !strings.HasPrefix(caller.Types.Path(), path[:i]) {
+ return nil, fmt.Errorf("can't inline function %v as its body refers to inaccessible package %q", callee.Name, path)
+ }
+ }
+ }
+
+ // The transformation is expressed by splicing substrings of
+ // the two source files, because syntax trees don't preserve
+ // comments faithfully (see #20744).
+ var out bytes.Buffer
+
+ // 'replace' emits to out the specified range of the callee,
+ // applying all edits that fall completely within it.
+ replace := func(start, end int) {
+ off := start
+ for _, edit := range edits {
+ if start <= edit.start && edit.end <= end {
+ out.Write(callee.Content[off:edit.start])
+ out.WriteString(edit.new)
+ off = edit.end
+ }
+ }
+ out.Write(callee.Content[off:end])
+ }
+
+ // Insert new imports after last existing import,
+ // to avoid migration of pre-import comments.
+ // The imports will be organized later.
+ {
+ offset := caller.offset(caller.File.Name.End()) // after package decl
+ if len(caller.File.Imports) > 0 {
+ // It's tempting to insert the new import after the last ImportSpec,
+ // but that may not be at the end of the import decl.
+ // Consider: import ( "a"; "b" ‸ )
+ for _, decl := range caller.File.Decls {
+ if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT {
+ offset = caller.offset(decl.End()) // after import decl
+ }
+ }
+ }
+ out.Write(caller.Content[:offset])
+ out.WriteString("\n")
+ for _, imp := range newImports {
+ fmt.Fprintf(&out, "import %s\n", imp)
+ }
+ out.Write(caller.Content[offset:caller.offset(caller.Call.Pos())])
+ }
+
+ // Special case: a call to a function whose body consists only
+ // of "return expr" may be replaced by the expression, so long as:
+ //
+ // (a) There are no receiver or parameter argument expressions
+ // whose side effects must be considered.
+ // (b) There are no named parameter or named result variables
+ // that could potentially escape.
+ //
+ // TODO(adonovan): expand this special case to cover more scenarios.
+ // Consider each parameter in turn. If:
+ // - the parameter does not escape and is never assigned;
+ // - its argument is pure (no effects or panics--basically just idents and literals)
+ // and referentially transparent (not new(T) or &T{...}) or referenced at most once; and
+ // - the argument and parameter have the same type
+ // then the parameter can be eliminated and each reference
+ // to it replaced by the argument.
+ // If:
+ // - all parameters can be so replaced;
+ // - and the body is just "return expr";
+ // - and the result vars are unnamed or never referenced (and thus cannot escape);
+ // then the call expression can be replaced by its body expression.
+ if callee.BodyIsReturnExpr &&
+ callee.decl.Recv == nil && // no receiver arg effects to consider
+ len(caller.Call.Args) == 0 && // no argument effects to consider
+ !hasNamedVars(callee.decl.Type.Params) && // no param vars escape
+ !hasNamedVars(callee.decl.Type.Results) { // no result vars escape
+
+ // A single return operand inlined to an expression
+ // context may need parens. Otherwise:
+ // func two() int { return 1+1 }
+ // print(-two()) => print(-1+1) // oops!
+ parens := callee.NumResults == 1
+
+ // If the call is a standalone statement, but the
+ // callee body is not suitable as a standalone statement
+ // (f() or <-ch), explicitly discard the results:
+ // _, _ = expr
+ if isCallStmt(callerPath) {
+ parens = false
+
+ if !callee.ValidForCallStmt {
+ for i := 0; i < callee.NumResults; i++ {
+ if i > 0 {
+ out.WriteString(", ")
+ }
+ out.WriteString("_")
+ }
+ out.WriteString(" = ")
+ }
+ }
+
+ // Emit the body expression(s).
+ for i, res := range callee.decl.Body.List[0].(*ast.ReturnStmt).Results {
+ if i > 0 {
+ out.WriteString(", ")
+ }
+ if parens {
+ out.WriteString("(")
+ }
+ replace(callee.offset(res.Pos()), callee.offset(res.End()))
+ if parens {
+ out.WriteString(")")
+ }
+ }
+ goto rest
+ }
+
+ // Emit a function literal in place of the callee name,
+ // with appropriate replacements.
+ out.WriteString("func (")
+ if recv := callee.decl.Recv; recv != nil {
+ // Move method receiver to head of ordinary parameters.
+ replace(callee.offset(recv.Opening+1), callee.offset(recv.Closing))
+ if len(callee.decl.Type.Params.List) > 0 {
+ out.WriteString(", ")
+ }
+ }
+ replace(callee.offset(callee.decl.Type.Params.Opening+1),
+ callee.offset(callee.decl.End()))
+
+ // Emit call arguments.
+ out.WriteString("(")
+ if callee.decl.Recv != nil {
+ // Move receiver argument x.f(...) to argument list f(x, ...).
+ recv := astutil.Unparen(caller.Call.Fun).(*ast.SelectorExpr).X
+
+ // If the receiver argument and parameter have
+ // different pointerness, make the "&" or "*" explicit.
+ argPtr := is[*types.Pointer](typeparams.CoreType(caller.Info.TypeOf(recv)))
+ paramPtr := is[*ast.StarExpr](callee.decl.Recv.List[0].Type)
+ if !argPtr && paramPtr {
+ out.WriteString("&")
+ } else if argPtr && !paramPtr {
+ out.WriteString("*")
+ }
+
+ out.Write(caller.Content[caller.offset(recv.Pos()):caller.offset(recv.End())])
+
+ if len(caller.Call.Args) > 0 {
+ out.WriteString(", ")
+ }
+ }
+ // Append ordinary args, sans initial "(".
+ out.Write(caller.Content[caller.offset(caller.Call.Lparen+1):caller.offset(caller.Call.End())])
+
+ // Append rest of caller file.
+rest:
+ out.Write(caller.Content[caller.offset(caller.Call.End()):])
+
+ // Reformat, and organize imports.
+ //
+ // TODO(adonovan): this looks at the user's cache state.
+ // Replace with a simpler implementation since
+ // all the necessary imports are present but merely untidy.
+ // That will be faster, and also less prone to nondeterminism
+ // if there are bugs in our logic for import maintenance.
+ //
+ // However, golang.org/x/tools/internal/imports.ApplyFixes is
+ // too simple as it requires the caller to have figured out
+ // all the logical edits. In our case, we know all the new
+ // imports that are needed (see newImports), each of which can
+ // be specified as:
+ //
+ // &imports.ImportFix{
+ // StmtInfo: imports.ImportInfo{path, name,
+ // IdentName: name,
+ // FixType: imports.AddImport,
+ // }
+ //
+ // but we don't know which imports are made redundant by the
+ // inlining itself. For example, inlining a call to
+ // fmt.Println may make the "fmt" import redundant.
+ //
+ // Also, both imports.Process and internal/imports.ApplyFixes
+ // reformat the entire file, which is not ideal for clients
+ // such as gopls. (That said, the point of a canonical format
+ // is arguably that any tool can reformat as needed without
+ // this being inconvenient.)
+ res, err := imports.Process("output", out.Bytes(), nil)
+ if err != nil {
+ if false { // debugging
+ log.Printf("cannot reformat: %v <<%s>>", err, &out)
+ }
+ return nil, err // cannot reformat (a bug?)
+ }
+ return res, nil
+}
+
+// -- helpers --
+
+func is[T any](x any) bool {
+ _, ok := x.(T)
+ return ok
+}
+
+func within(pos token.Pos, n ast.Node) bool {
+ return n.Pos() <= pos && pos <= n.End()
+}
+
+func offsetOf(fset *token.FileSet, pos token.Pos) int {
+ return fset.PositionFor(pos, false).Offset
+}
+
+// importedPkgName returns the PkgName object declared by an ImportSpec.
+// TODO(adonovan): make this a method of types.Info (#62037).
+func importedPkgName(info *types.Info, imp *ast.ImportSpec) (*types.PkgName, bool) {
+ var obj types.Object
+ if imp.Name != nil {
+ obj = info.Defs[imp.Name]
+ } else {
+ obj = info.Implicits[imp]
+ }
+ pkgname, ok := obj.(*types.PkgName)
+ return pkgname, ok
+}
+
+func isPkgLevel(obj types.Object) bool {
+ return obj.Pkg().Scope().Lookup(obj.Name()) == obj
+}
+
+// objectKind returns an object's kind (e.g. var, func, const, typename).
+func objectKind(obj types.Object) string {
+ return strings.TrimPrefix(strings.ToLower(reflect.TypeOf(obj).String()), "*types.")
+}
+
+// isCallStmt reports whether the function call (specified
+// as a PathEnclosingInterval) appears within an ExprStmt.
+func isCallStmt(callPath []ast.Node) bool {
+ _ = callPath[0].(*ast.CallExpr)
+ for _, n := range callPath[1:] {
+ switch n.(type) {
+ case *ast.ParenExpr:
+ continue
+ case *ast.ExprStmt:
+ return true
+ }
+ break
+ }
+ return false
+}
+
+// hasNamedVars reports whether a function parameter tuple uses named variables.
+//
+// TODO(adonovan): this is a placeholder for a more complex analysis to detect
+// whether inlining might cause named param/result variables to escape.
+func hasNamedVars(tuple *ast.FieldList) bool {
+ return tuple != nil && len(tuple.List) > 0 && tuple.List[0].Names != nil
+}
diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go
new file mode 100644
index 0000000..0427840
--- /dev/null
+++ b/internal/refactor/inline/inline_test.go
@@ -0,0 +1,322 @@
+// 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 inline_test
+
+import (
+ "bytes"
+ "encoding/gob"
+ "fmt"
+ "go/ast"
+ "go/token"
+ "os"
+ "path/filepath"
+ "regexp"
+ "testing"
+
+ "golang.org/x/tools/go/ast/astutil"
+ "golang.org/x/tools/go/expect"
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/go/types/typeutil"
+ "golang.org/x/tools/internal/diff"
+ "golang.org/x/tools/internal/refactor/inline"
+ "golang.org/x/tools/txtar"
+)
+
+// Test executes test scenarios specified by files in testdata/*.txtar.
+func Test(t *testing.T) {
+ files, err := filepath.Glob("testdata/*.txtar")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, file := range files {
+ file := file
+ t.Run(filepath.Base(file), func(t *testing.T) {
+ t.Parallel()
+
+ // Extract archive to temporary tree.
+ ar, err := txtar.ParseFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ dir := t.TempDir()
+ if err := extractTxtar(ar, dir); err != nil {
+ t.Fatal(err)
+ }
+
+ // Load packages.
+ cfg := &packages.Config{
+ Dir: dir,
+ Mode: packages.LoadAllSyntax,
+ Env: append(os.Environ(),
+ "GO111MODULES=on",
+ "GOPATH=",
+ "GOWORK=off",
+ "GOPROXY=off"),
+ }
+ pkgs, err := packages.Load(cfg, "./...")
+ if err != nil {
+ t.Errorf("Load: %v", err)
+ }
+ // Report parse/type errors; they may be benign.
+ packages.Visit(pkgs, nil, func(pkg *packages.Package) {
+ for _, err := range pkg.Errors {
+ t.Log(err)
+ }
+ })
+
+ // Process @inline notes in comments in initial packages.
+ for _, pkg := range pkgs {
+ for _, file := range pkg.Syntax {
+ // Read file content (for @inline regexp, and inliner).
+ content, err := os.ReadFile(pkg.Fset.File(file.Pos()).Name())
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+
+ // Read and process @inline notes.
+ notes, err := expect.ExtractGo(pkg.Fset, file)
+ if err != nil {
+ t.Errorf("parsing notes in %q: %v", pkg.Fset.File(file.Pos()).Name(), err)
+ continue
+ }
+ for _, note := range notes {
+ posn := pkg.Fset.Position(note.Pos)
+ if note.Name != "inline" {
+ t.Errorf("%s: invalid marker @%s", posn, note.Name)
+ continue
+ }
+ if nargs := len(note.Args); nargs != 2 {
+ t.Errorf("@inline: want 2 args, got %d", nargs)
+ continue
+ }
+ pattern, ok := note.Args[0].(*regexp.Regexp)
+ if !ok {
+ t.Errorf("%s: @inline(rx, want): want regular expression rx", posn)
+ continue
+ }
+
+ // want is a []byte (success) or *Regexp (failure)
+ var want any
+ switch x := note.Args[1].(type) {
+ case string, expect.Identifier:
+ for _, file := range ar.Files {
+ if file.Name == fmt.Sprint(x) {
+ want = file.Data
+ break
+ }
+ }
+ if want == nil {
+ t.Errorf("%s: @inline(rx, want): archive entry %q not found", posn, x)
+ continue
+ }
+ case *regexp.Regexp:
+ want = x
+ default:
+ t.Errorf("%s: @inline(rx, want): want file name (to assert success) or error message regexp (to assert failure)", posn)
+ continue
+ }
+ t.Log("doInlineNote", posn)
+ if err := doInlineNote(pkg, file, content, pattern, posn, want); err != nil {
+ t.Errorf("%s: @inline(%v, %v): %v", posn, note.Args[0], note.Args[1], err)
+ continue
+ }
+ }
+ }
+ }
+ })
+ }
+}
+
+// doInlineNote executes an assertion specified by a single
+// @inline(re"pattern", want) note in a comment. It finds the first
+// match of regular expression 'pattern' on the same line, finds the
+// innermost enclosing CallExpr, and inlines it.
+//
+// Finally it checks that, on success, the transformed file is equal
+// to want (a []byte), or on failure that the error message matches
+// want (a *Regexp).
+func doInlineNote(pkg *packages.Package, file *ast.File, content []byte, pattern *regexp.Regexp, posn token.Position, want any) error {
+ // Find extent of pattern match within commented line.
+ var startPos, endPos token.Pos
+ {
+ tokFile := pkg.Fset.File(file.Pos())
+ lineStartOffset := int(tokFile.LineStart(posn.Line)) - tokFile.Base()
+ line := content[lineStartOffset:]
+ if i := bytes.IndexByte(line, '\n'); i >= 0 {
+ line = line[:i]
+ }
+ matches := pattern.FindSubmatchIndex(line)
+ var start, end int // offsets
+ switch len(matches) {
+ case 2:
+ // no subgroups: return the range of the regexp expression
+ start, end = matches[0], matches[1]
+ case 4:
+ // one subgroup: return its range
+ start, end = matches[2], matches[3]
+ default:
+ return fmt.Errorf("invalid location regexp %q: expect either 0 or 1 subgroups, got %d",
+ pattern, len(matches)/2-1)
+ }
+ startPos = tokFile.Pos(lineStartOffset + start)
+ endPos = tokFile.Pos(lineStartOffset + end)
+ }
+
+ // Find innermost call enclosing the pattern match.
+ var caller *inline.Caller
+ {
+ path, _ := astutil.PathEnclosingInterval(file, startPos, endPos)
+ for _, n := range path {
+ if call, ok := n.(*ast.CallExpr); ok {
+ caller = &inline.Caller{
+ Fset: pkg.Fset,
+ Types: pkg.Types,
+ Info: pkg.TypesInfo,
+ File: file,
+ Call: call,
+ Content: content,
+ }
+ break
+ }
+ }
+ if caller == nil {
+ return fmt.Errorf("no enclosing call")
+ }
+ }
+
+ // Is it a static function call?
+ fn := typeutil.StaticCallee(caller.Info, caller.Call)
+ if fn == nil {
+ return fmt.Errorf("cannot inline: not a static call")
+ }
+
+ // Find callee function.
+ var (
+ calleePkg *packages.Package
+ calleeDecl *ast.FuncDecl
+ )
+ {
+ var same func(*ast.FuncDecl) bool
+ // Is the call within the package?
+ if fn.Pkg() == caller.Types {
+ calleePkg = pkg // same as caller
+ same = func(decl *ast.FuncDecl) bool {
+ return decl.Name.Pos() == fn.Pos()
+ }
+ } else {
+ // Different package. Load it now.
+ // (The primary load loaded all dependencies,
+ // but we choose to load it again, with
+ // a distinct token.FileSet and types.Importer,
+ // to keep the implementation honest.)
+ cfg := &packages.Config{
+ // TODO(adonovan): get the original module root more cleanly
+ Dir: filepath.Dir(filepath.Dir(pkg.GoFiles[0])),
+ Fset: token.NewFileSet(),
+ Mode: packages.LoadSyntax,
+ }
+ roots, err := packages.Load(cfg, fn.Pkg().Path())
+ if err != nil {
+ return fmt.Errorf("loading callee package: %v", err)
+ }
+ if packages.PrintErrors(roots) > 0 {
+ return fmt.Errorf("callee package had errors") // (see log)
+ }
+ calleePkg = roots[0]
+ posn := caller.Fset.Position(fn.Pos()) // callee posn wrt caller package
+ same = func(decl *ast.FuncDecl) bool {
+ // We can't rely on columns in export data:
+ // some variants replace it with 1.
+ // We can't expect file names to have the same prefix.
+ // export data for go1.20 std packages have $GOROOT written in
+ // them, so how are we supposed to find the source? Yuck!
+ // Ugh. need to samefile? Nope $GOROOT just won't work
+ // This is highly client specific anyway.
+ posn2 := calleePkg.Fset.Position(decl.Name.Pos())
+ return posn.Filename == posn2.Filename &&
+ posn.Line == posn2.Line
+ }
+ }
+
+ for _, file := range calleePkg.Syntax {
+ for _, decl := range file.Decls {
+ if decl, ok := decl.(*ast.FuncDecl); ok && same(decl) {
+ calleeDecl = decl
+ goto found
+ }
+ }
+ }
+ return fmt.Errorf("can't find FuncDecl for callee") // can't happen?
+ found:
+ }
+
+ // Do the inlining. For the purposes of the test,
+ // AnalyzeCallee and Inline are a single operation.
+ got, err := func() ([]byte, error) {
+ filename := calleePkg.Fset.File(calleeDecl.Pos()).Name()
+ content, err := os.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+ callee, err := inline.AnalyzeCallee(
+ calleePkg.Fset,
+ calleePkg.Types,
+ calleePkg.TypesInfo,
+ calleeDecl,
+ content)
+ if err != nil {
+ return nil, err
+ }
+
+ // Perform Gob transcoding so that it is exercised by the test.
+ var enc bytes.Buffer
+ if err := gob.NewEncoder(&enc).Encode(callee); err != nil {
+ return nil, fmt.Errorf("internal error: gob encoding failed: %v", err)
+ }
+ *callee = inline.Callee{}
+ if err := gob.NewDecoder(&enc).Decode(callee); err != nil {
+ return nil, fmt.Errorf("internal error: gob decoding failed: %v", err)
+ }
+
+ return inline.Inline(caller, callee)
+ }()
+ if err != nil {
+ if wantRE, ok := want.(*regexp.Regexp); ok {
+ if !wantRE.MatchString(err.Error()) {
+ return fmt.Errorf("Inline failed with wrong error: %v (want error matching %q)", err, want)
+ }
+ return nil // expected error
+ }
+ return fmt.Errorf("Inline failed: %v", err) // success was expected
+ }
+
+ // Inline succeeded.
+ if want, ok := want.([]byte); ok {
+ got = append(bytes.TrimSpace(got), '\n')
+ want = append(bytes.TrimSpace(want), '\n')
+ if diff := diff.Unified("want", "got", string(want), string(got)); diff != "" {
+ return fmt.Errorf("Inline returned wrong output:\n%s\nWant:\n%s\nDiff:\n%s",
+ got, want, diff)
+ }
+ return nil
+ }
+ return fmt.Errorf("Inline succeeded unexpectedly: want error matching %q, got <<%s>>", want, got)
+
+}
+
+// TODO(adonovan): publish this a helper (#61386).
+func extractTxtar(ar *txtar.Archive, dir string) error {
+ for _, file := range ar.Files {
+ name := filepath.Join(dir, file.Name)
+ if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil {
+ return err
+ }
+ if err := os.WriteFile(name, file.Data, 0666); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/internal/refactor/inline/testdata/basic-err.txtar b/internal/refactor/inline/testdata/basic-err.txtar
new file mode 100644
index 0000000..18e0eb7
--- /dev/null
+++ b/internal/refactor/inline/testdata/basic-err.txtar
@@ -0,0 +1,24 @@
+Test of inlining a function that references err.Error,
+which is often a special case because it has no position.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+import "io"
+
+var _ = getError(io.EOF) //@ inline(re"getError", getError)
+
+func getError(err error) string { return err.Error() }
+
+-- getError --
+package a
+
+import "io"
+
+var _ = func(err error) string { return err.Error() }(io.EOF) //@ inline(re"getError", getError)
+
+func getError(err error) string { return err.Error() }
diff --git a/internal/refactor/inline/testdata/basic-literal.txtar b/internal/refactor/inline/testdata/basic-literal.txtar
new file mode 100644
index 0000000..50bac33
--- /dev/null
+++ b/internal/refactor/inline/testdata/basic-literal.txtar
@@ -0,0 +1,19 @@
+Most basic test of inlining by literalization.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+var _ = add(1, 2) //@ inline(re"add", add)
+
+func add(x, y int) int { return x + y }
+
+-- add --
+package a
+
+var _ = func(x, y int) int { return x + y }(1, 2) //@ inline(re"add", add)
+
+func add(x, y int) int { return x + y }
diff --git a/internal/refactor/inline/testdata/basic-reduce.txtar b/internal/refactor/inline/testdata/basic-reduce.txtar
new file mode 100644
index 0000000..9eedbc0
--- /dev/null
+++ b/internal/refactor/inline/testdata/basic-reduce.txtar
@@ -0,0 +1,19 @@
+Most basic test of inlining by reduction.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+var _ = zero() //@ inline(re"zero", zero)
+
+func zero() int { return 0 }
+
+-- zero --
+package a
+
+var _ = (0) //@ inline(re"zero", zero)
+
+func zero() int { return 0 }
diff --git a/internal/refactor/inline/testdata/comments.txtar b/internal/refactor/inline/testdata/comments.txtar
new file mode 100644
index 0000000..0482e91
--- /dev/null
+++ b/internal/refactor/inline/testdata/comments.txtar
@@ -0,0 +1,56 @@
+Inlining, whether by literalization or reduction,
+preserves comments in the callee.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/f.go --
+package a
+
+func _() {
+ f() //@ inline(re"f", f)
+}
+
+func f() {
+ // a
+ /* b */ g() /* c */
+ // d
+}
+
+-- f --
+package a
+
+func _() {
+ func() {
+ // a
+ /* b */
+ g() /* c */
+ // d
+ }() //@ inline(re"f", f)
+}
+
+func f() {
+ // a
+ /* b */
+ g() /* c */
+ // d
+}
+
+-- a/g.go --
+package a
+
+func _() {
+ println(g()) //@ inline(re"g", g)
+}
+
+func g() int { return 1 /*hello*/ + /*there*/ 1 }
+
+-- g --
+package a
+
+func _() {
+ println((1 /*hello*/ + /*there*/ 1)) //@ inline(re"g", g)
+}
+
+func g() int { return 1 /*hello*/ + /*there*/ 1 }
diff --git a/internal/refactor/inline/testdata/crosspkg.txtar b/internal/refactor/inline/testdata/crosspkg.txtar
new file mode 100644
index 0000000..43dc63f
--- /dev/null
+++ b/internal/refactor/inline/testdata/crosspkg.txtar
@@ -0,0 +1,77 @@
+Test of cross-package inlining.
+The first case creates a new import,
+the second reuses an existing one.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+// This comment does not migrate.
+
+import (
+ "fmt"
+ "testdata/b"
+)
+
+// Nor this one.
+
+func A() {
+ fmt.Println()
+ b.B1() //@ inline(re"B1", b1result)
+ b.B2() //@ inline(re"B2", b2result)
+}
+
+-- b/b.go --
+package b
+
+import "testdata/c"
+import "fmt"
+
+func B1() { c.C() }
+func B2() { fmt.Println() }
+
+-- c/c.go --
+package c
+
+func C() {}
+
+-- b1result --
+package a
+
+// This comment does not migrate.
+
+import (
+ "fmt"
+ "testdata/b"
+
+ c "testdata/c"
+)
+
+// Nor this one.
+
+func A() {
+ fmt.Println()
+ func() { c.C() }() //@ inline(re"B1", b1result)
+ b.B2() //@ inline(re"B2", b2result)
+}
+
+-- b2result --
+package a
+
+// This comment does not migrate.
+
+import (
+ "fmt"
+ "testdata/b"
+)
+
+// Nor this one.
+
+func A() {
+ fmt.Println()
+ b.B1() //@ inline(re"B1", b1result)
+ func() { fmt.Println() }() //@ inline(re"B2", b2result)
+}
diff --git a/internal/refactor/inline/testdata/dotimport.txtar b/internal/refactor/inline/testdata/dotimport.txtar
new file mode 100644
index 0000000..7e886af
--- /dev/null
+++ b/internal/refactor/inline/testdata/dotimport.txtar
@@ -0,0 +1,35 @@
+Test of inlining a function that uses a dot import.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+func A() {}
+
+-- b/b.go --
+package b
+
+import . "testdata/a"
+
+func B() { A() }
+
+-- c/c.go --
+package c
+
+import "testdata/b"
+
+func _() {
+ b.B() //@ inline(re"B", result)
+}
+
+-- result --
+package c
+
+import a "testdata/a"
+
+func _() {
+ func() { a.A() }() //@ inline(re"B", result)
+}
diff --git a/internal/refactor/inline/testdata/err-basic.txtar b/internal/refactor/inline/testdata/err-basic.txtar
new file mode 100644
index 0000000..54377c7
--- /dev/null
+++ b/internal/refactor/inline/testdata/err-basic.txtar
@@ -0,0 +1,30 @@
+Basic errors:
+- Inlining of generic functions is not yet supported.
+
+We can't express tests for the error resulting from inlining a
+conversion T(x), a call to a literal func(){}(), a call to a
+func-typed var, or a call to an interface method, since all of these
+cause the test driver to fail to locate the callee, so
+it doesn't even reach the Indent function.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/generic.go --
+package a
+
+func _() {
+ f[int]() //@ inline(re"f", re"type parameters are not yet supported")
+}
+
+func f[T any]() {}
+
+-- a/nobody.go --
+package a
+
+func _() {
+ g() //@ inline(re"g", re"has no body")
+}
+
+func g()
diff --git a/internal/refactor/inline/testdata/err-shadow-builtin.txtar b/internal/refactor/inline/testdata/err-shadow-builtin.txtar
new file mode 100644
index 0000000..543d38f
--- /dev/null
+++ b/internal/refactor/inline/testdata/err-shadow-builtin.txtar
@@ -0,0 +1,36 @@
+Failures to inline because callee references a builtin that
+is shadowed by caller.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/nil.go --
+package a
+
+func _() {
+ const nil = 1
+ _ = f() //@ inline(re"f", re"nil.*shadowed.*by.*const .line 4")
+}
+
+func f() *int { return nil }
+
+-- a/append.go --
+package a
+
+func _() {
+ type append int
+ g(nil) //@ inline(re"g", re"append.*shadowed.*by.*typename .line 4")
+}
+
+func g(x []int) { _ = append(x, x...) }
+
+-- a/type.go --
+package a
+
+func _() {
+ type int uint8
+ _ = h(0) //@ inline(re"h", re"int.*shadowed.*by.*typename .line 4")
+}
+
+func h(x int) int { return x + 1 }
diff --git a/internal/refactor/inline/testdata/err-shadow-pkg.txtar b/internal/refactor/inline/testdata/err-shadow-pkg.txtar
new file mode 100644
index 0000000..4338b8b
--- /dev/null
+++ b/internal/refactor/inline/testdata/err-shadow-pkg.txtar
@@ -0,0 +1,36 @@
+Test of failure to inline because callee references a
+package-level decl that is shadowed by caller.
+
+Observe that the first call to f can be inlined because
+the shadowing has not yet occurred; but the second call
+to f is within the scope of the local constant v.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+func _() {
+ f() //@ inline(re"f", result)
+ const v = 1
+ f() //@ inline(re"f", re"v.*shadowed.*by.*const .line 5")
+}
+
+func f() int { return v }
+
+var v int
+
+-- result --
+package a
+
+func _() {
+ _ = v //@ inline(re"f", result)
+ const v = 1
+ f() //@ inline(re"f", re"v.*shadowed.*by.*const .line 5")
+}
+
+func f() int { return v }
+
+var v int
diff --git a/internal/refactor/inline/testdata/err-unexported.txtar b/internal/refactor/inline/testdata/err-unexported.txtar
new file mode 100644
index 0000000..9ba91e5
--- /dev/null
+++ b/internal/refactor/inline/testdata/err-unexported.txtar
@@ -0,0 +1,31 @@
+Errors from attempting to import a function from another
+package whose body refers to unexported declarations.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+func A1() { b() }
+func b() {}
+
+func A2() { var x T; print(x.f) }
+type T struct { f int }
+
+func A3() { _ = &T{f: 0} }
+
+func A4() { _ = &T{0} }
+
+-- b/b.go --
+package b
+
+import "testdata/a"
+
+func _() {
+ a.A1() //@ inline(re"A1", re`body refers to non-exported b`)
+ a.A2() //@ inline(re"A2", re`body refers to non-exported \(testdata/a.T\).f`)
+ a.A3() //@ inline(re"A3", re`body refers to non-exported \(testdata/a.T\).f`)
+ a.A4() //@ inline(re"A4", re`body refers to non-exported \(testdata/a.T\).f`)
+}
diff --git a/internal/refactor/inline/testdata/exprstmt.txtar b/internal/refactor/inline/testdata/exprstmt.txtar
new file mode 100644
index 0000000..449ce35
--- /dev/null
+++ b/internal/refactor/inline/testdata/exprstmt.txtar
@@ -0,0 +1,99 @@
+Inlining an expression into an ExprStmt.
+Call and receive expressions can be inlined directly
+(though calls to only some builtins can be reduced).
+All other expressions are inlined as "_ = expr".
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/call.go --
+package a
+
+func _() {
+ call() //@ inline(re"call", call)
+}
+
+func call() int { return recv() }
+
+-- call --
+package a
+
+func _() {
+ recv() //@ inline(re"call", call)
+}
+
+func call() int { return recv() }
+
+-- a/recv.go --
+package a
+
+func _() {
+ recv() //@ inline(re"recv", recv)
+}
+
+func recv() int { return <-(chan int)(nil) }
+
+-- recv --
+package a
+
+func _() {
+ <-(chan int)(nil) //@ inline(re"recv", recv)
+}
+
+func recv() int { return <-(chan int)(nil) }
+
+-- a/constant.go --
+package a
+
+func _() {
+ constant() //@ inline(re"constant", constant)
+}
+
+func constant() int { return 0 }
+
+-- constant --
+package a
+
+func _() {
+ _ = 0 //@ inline(re"constant", constant)
+}
+
+func constant() int { return 0 }
+
+-- a/builtin.go --
+package a
+
+func _() {
+ builtin() //@ inline(re"builtin", builtin)
+}
+
+func builtin() int { return len("") }
+
+-- builtin --
+package a
+
+func _() {
+ _ = len("") //@ inline(re"builtin", builtin)
+}
+
+func builtin() int { return len("") }
+
+-- a/copy.go --
+package a
+
+func _() {
+ _copy() //@ inline(re"copy", copy)
+}
+
+func _copy() int { return copy([]int(nil), []int(nil)) }
+
+-- copy --
+package a
+
+func _() {
+ copy([]int(nil), []int(nil)) //@ inline(re"copy", copy)
+}
+
+func _copy() int { return copy([]int(nil), []int(nil)) }
+
diff --git a/internal/refactor/inline/testdata/import-shadow.txtar b/internal/refactor/inline/testdata/import-shadow.txtar
new file mode 100644
index 0000000..913c9cb
--- /dev/null
+++ b/internal/refactor/inline/testdata/import-shadow.txtar
@@ -0,0 +1,41 @@
+Test of heuristic for generating a fresh import PkgName.
+The names c and c0 are taken, so it uses c1.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+import "testdata/b"
+
+func A() {
+ const c = 1
+ type c0 int
+ b.B() //@ inline(re"B", result)
+}
+
+-- b/b.go --
+package b
+
+import "testdata/c"
+
+func B() { c.C() }
+
+-- c/c.go --
+package c
+
+func C() {}
+
+-- result --
+package a
+
+import c1 "testdata/c"
+
+func A() {
+ const c = 1
+ type c0 int
+ func() { c1.C() }() //@ inline(re"B", result)
+}
+
diff --git a/internal/refactor/inline/testdata/internal.txtar b/internal/refactor/inline/testdata/internal.txtar
new file mode 100644
index 0000000..92a0fef
--- /dev/null
+++ b/internal/refactor/inline/testdata/internal.txtar
@@ -0,0 +1,29 @@
+Test of inlining a function that references an
+internal package that is not accessible to the caller.
+
+(c -> b -> b/internal/a)
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- b/internal/a/a.go --
+package a
+
+func A() {}
+
+-- b/b.go --
+package b
+
+import "testdata/b/internal/a"
+
+func B() { a.A() }
+
+-- c/c.go --
+package c
+
+import "testdata/b"
+
+func _() {
+ b.B() //@ inline(re"B", re`body refers to inaccessible package "testdata/b/internal/a"`)
+}
diff --git a/internal/refactor/inline/testdata/method.txtar b/internal/refactor/inline/testdata/method.txtar
new file mode 100644
index 0000000..a4e02d5
--- /dev/null
+++ b/internal/refactor/inline/testdata/method.txtar
@@ -0,0 +1,104 @@
+Test of inlining a method call.
+
+The call to (*T).g0 implicitly takes the address &x.
+
+The f1/g1 methods have parameters, exercising the
+splicing of the receiver into the parameter list.
+Notice that the unnamed parameters become named.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/f0.go --
+package a
+
+type T int
+func (T) f0() {}
+
+func _(x T) {
+ x.f0() //@ inline(re"f0", f0)
+}
+
+-- f0 --
+package a
+
+type T int
+
+func (T) f0() {}
+
+func _(x T) {
+ func(_ T) {}(x) //@ inline(re"f0", f0)
+}
+
+-- a/g0.go --
+package a
+
+func (recv *T) g0() {}
+
+func _(x T) {
+ x.g0() //@ inline(re"g0", g0)
+}
+
+-- g0 --
+package a
+
+func (recv *T) g0() {}
+
+func _(x T) {
+ func(recv *T) {}(&x) //@ inline(re"g0", g0)
+}
+
+-- a/f1.go --
+package a
+
+func (T) f1(int, int) {}
+
+func _(x T) {
+ x.f1(1, 2) //@ inline(re"f1", f1)
+}
+
+-- f1 --
+package a
+
+func (T) f1(int, int) {}
+
+func _(x T) {
+ func(_ T, _ int, _ int) {}(x, 1, 2) //@ inline(re"f1", f1)
+}
+
+-- a/g1.go --
+package a
+
+func (recv *T) g1(int, int) {}
+
+func _(x T) {
+ x.g1(1, 2) //@ inline(re"g1", g1)
+}
+
+-- g1 --
+package a
+
+func (recv *T) g1(int, int) {}
+
+func _(x T) {
+ func(recv *T, _ int, _ int) {}(&x, 1, 2) //@ inline(re"g1", g1)
+}
+
+-- a/h.go --
+package a
+
+func (T) h() int { return 1 }
+
+func _() {
+ new(T).h() //@ inline(re"h", h)
+}
+
+-- h --
+package a
+
+func (T) h() int { return 1 }
+
+func _() {
+ func(_ T) int { return 1 }(*new(T)) //@ inline(re"h", h)
+}
diff --git a/internal/refactor/inline/testdata/n-ary.txtar b/internal/refactor/inline/testdata/n-ary.txtar
new file mode 100644
index 0000000..2de9735
--- /dev/null
+++ b/internal/refactor/inline/testdata/n-ary.txtar
@@ -0,0 +1,79 @@
+Tests of various n-ary result function cases.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+func _() {
+ println(f1()) //@ inline(re"f1", f1)
+}
+
+func f1() (int, int) { return 1, 1 }
+
+-- f1 --
+package a
+
+func _() {
+ println(1, 1) //@ inline(re"f1", f1)
+}
+
+func f1() (int, int) { return 1, 1 }
+
+-- b/b.go --
+package b
+
+func _() {
+ f2() //@ inline(re"f2", f2)
+}
+
+func f2() (int, int) { return 2, 2 }
+
+-- f2 --
+package b
+
+func _() {
+ _, _ = 2, 2 //@ inline(re"f2", f2)
+}
+
+func f2() (int, int) { return 2, 2 }
+
+-- c/c.go --
+package c
+
+func _() {
+ _, _ = f3() //@ inline(re"f3", f3)
+}
+
+func f3() (int, int) { return f3A() }
+func f3A() (x, y int)
+
+-- f3 --
+package c
+
+func _() {
+ _, _ = f3A() //@ inline(re"f3", f3)
+}
+
+func f3() (int, int) { return f3A() }
+func f3A() (x, y int)
+
+-- d/d.go --
+package d
+
+func _() {
+ println(-f4()) //@ inline(re"f4", f4)
+}
+
+func f4() int { return 2 + 2 }
+
+-- f4 --
+package d
+
+func _() {
+ println(-(2 + 2)) //@ inline(re"f4", f4)
+}
+
+func f4() int { return 2 + 2 }
diff --git a/internal/refactor/inline/testdata/revdotimport.txtar b/internal/refactor/inline/testdata/revdotimport.txtar
new file mode 100644
index 0000000..f8b895e
--- /dev/null
+++ b/internal/refactor/inline/testdata/revdotimport.txtar
@@ -0,0 +1,43 @@
+Test of inlining a function into a context that already
+dot-imports the necessary additional import.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a.go --
+package a
+
+func A() {}
+
+-- b/b.go --
+package b
+
+import "testdata/a"
+
+func B() { a.A() }
+
+-- c/c.go --
+package c
+
+import . "testdata/a"
+import "testdata/b"
+
+func _() {
+ A()
+ b.B() //@ inline(re"B", result)
+}
+
+-- result --
+package c
+
+import (
+ . "testdata/a"
+
+ a "testdata/a"
+)
+
+func _() {
+ A()
+ func() { a.A() }() //@ inline(re"B", result)
+}