Skip to content

Commit ad827af

Browse files
committed
internal/refactor/inline: add table-driven test
This change adds a table-driven test of "one liner" callers, callees, and expected results/errors. The various strategies need much more extensive testing of minor variants than is practical with the .txtar approach. Also: - add assertions that types.Info is correctly populated; - opt: skip computation of unreferenced imports if there were none to begin with; - add fix and test for a bug in the "infallible" literalization strategy (spread calls to methods). - Clone syntax trees before splicing, otherwise we end up putting caller trees into the callee and then clearing their positions. - Move TODO comment re: var decl approach to parameter assignment. Change-Id: I44a9825ab87f12700423fb00aa7df97f81d12e81 Reviewed-on: https://go-review.googlesource.com/c/tools/+/527195 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]>
1 parent 8d6ad46 commit ad827af

File tree

4 files changed

+372
-53
lines changed

4 files changed

+372
-53
lines changed

internal/refactor/inline/callee.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ type object struct {
7070
// golang.org/x/tools/go/analysis framework: the inlining information
7171
// about a callee can be recorded as a "fact".
7272
func AnalyzeCallee(fset *token.FileSet, pkg *types.Package, info *types.Info, decl *ast.FuncDecl, content []byte) (*Callee, error) {
73+
checkInfoFields(info)
7374

7475
// The client is expected to have determined that the callee
7576
// is a function with a declaration (not a built-in or var).

internal/refactor/inline/inline.go

Lines changed: 121 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ func Inline(logf func(string, ...any), caller *Caller, callee *Callee) ([]byte,
310310
logf = func(string, ...any) {} // discard
311311
}
312312
logf("inline %s @ %v",
313-
formatNode(caller.Fset, caller.Call),
313+
debugFormatNode(caller.Fset, caller.Call),
314314
caller.Fset.Position(caller.Call.Lparen))
315315

316316
res, err := inline(logf, caller, &callee.impl)
@@ -331,17 +331,19 @@ func Inline(logf func(string, ...any), caller *Caller, callee *Callee) ([]byte,
331331
{
332332
start := offsetOf(caller.Fset, res.old.Pos())
333333
end := offsetOf(caller.Fset, res.old.End())
334+
var out bytes.Buffer
335+
out.Write(caller.Content[:start])
334336
// TODO(adonovan): might it make more sense to use
335337
// callee.Fset when formatting res.new??
336-
newFile := string(caller.Content[:start]) +
337-
formatNode(caller.Fset, res.new) +
338-
string(caller.Content[end:])
338+
if err := format.Node(&out, caller.Fset, res.new); err != nil {
339+
return nil, err
340+
}
341+
out.Write(caller.Content[end:])
339342
const mode = parser.ParseComments | parser.SkipObjectResolution | parser.AllErrors
340-
var err error
341-
f, err = parser.ParseFile(caller.Fset, "callee.go", newFile, mode)
343+
f, err = parser.ParseFile(caller.Fset, "callee.go", &out, mode)
342344
if err != nil {
343345
// Something has gone very wrong.
344-
logf("failed to parse <<%s>>", newFile) // debugging
346+
logf("failed to parse <<%s>>", &out) // debugging
345347
return nil, err
346348
}
347349
}
@@ -376,6 +378,12 @@ func Inline(logf func(string, ...any), caller *Caller, callee *Callee) ([]byte,
376378
}
377379
}
378380

381+
var out bytes.Buffer
382+
if err := format.Node(&out, caller.Fset, f); err != nil {
383+
return nil, err
384+
}
385+
newSrc := out.Bytes()
386+
379387
// Remove imports that are no longer referenced.
380388
//
381389
// It ought to be possible to compute the set of PkgNames used
@@ -425,16 +433,17 @@ func Inline(logf func(string, ...any), caller *Caller, callee *Callee) ([]byte,
425433
// We could invoke imports.Process and parse its result,
426434
// compare against the original AST, compute a list of import
427435
// fixes, and return that too.
428-
var out bytes.Buffer
429-
if err := format.Node(&out, caller.Fset, f); err != nil {
430-
return nil, err
431-
}
432-
formatted, err := imports.Process("output", out.Bytes(), nil)
433-
if err != nil {
434-
logf("cannot reformat: %v <<%s>>", err, &out)
435-
return nil, err // cannot reformat (a bug?)
436+
437+
// Recompute imports only if there were existing ones.
438+
if len(f.Imports) > 0 {
439+
formatted, err := imports.Process("output", newSrc, nil)
440+
if err != nil {
441+
logf("cannot reformat: %v <<%s>>", err, &out)
442+
return nil, err // cannot reformat (a bug?)
443+
}
444+
newSrc = formatted
436445
}
437-
return formatted, nil
446+
return newSrc, nil
438447
}
439448

440449
type result struct {
@@ -462,6 +471,8 @@ type result struct {
462471
// representation, such as any proposed solution to #20744, or even
463472
// dst or some private fork of go/ast.)
464473
func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*result, error) {
474+
checkInfoFields(caller.Info)
475+
465476
// Inlining of dynamic calls is not currently supported,
466477
// even for local closure calls. (This would be a lot of work.)
467478
calleeSymbol := typeutil.StaticCallee(caller.Info, caller.Call)
@@ -632,9 +643,10 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
632643
}
633644

634645
// replaceCalleeID replaces an identifier in the callee.
646+
// The replacement tree must not belong to the caller; use cloneNode as needed.
635647
replaceCalleeID := func(offset int, repl ast.Expr) {
636648
id := findIdent(calleeDecl, calleeDecl.Pos()+token.Pos(offset))
637-
logf("- replace id %q @ #%d to %q", id.Name, offset, formatNode(calleeFset, repl))
649+
logf("- replace id %q @ #%d to %q", id.Name, offset, debugFormatNode(calleeFset, repl))
638650
replaceNode(calleeDecl, id, repl)
639651
}
640652

@@ -812,12 +824,12 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
812824
if is[*types.Tuple](arg.typ) {
813825
// TODO(adonovan): handle elimination of spread arguments.
814826
logf("keeping param %q: argument %s is spread",
815-
param.Name, formatNode(caller.Fset, arg.expr))
816-
continue
827+
param.Name, debugFormatNode(caller.Fset, arg.expr))
828+
break // spread => last argument, but not last parameter
817829
}
818830
if !arg.pure {
819831
logf("keeping param %q: argument %s is impure",
820-
param.Name, formatNode(caller.Fset, arg.expr))
832+
param.Name, debugFormatNode(caller.Fset, arg.expr))
821833
continue // unsafe to change order or cardinality of effects
822834
}
823835
if len(param.Refs) > 1 && !arg.duplicable {
@@ -872,10 +884,13 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
872884
// It is safe to eliminate param and replace it with arg.
873885
// No additional parens are required around arg for
874886
// the supported "pure" expressions.
887+
//
888+
// Because arg.expr belongs to the caller,
889+
// we clone it before splicing it into the callee tree.
875890
logf("replacing parameter %q by argument %q",
876-
param.Name, formatNode(caller.Fset, arg.expr))
891+
param.Name, debugFormatNode(caller.Fset, arg.expr))
877892
for _, ref := range param.Refs {
878-
replaceCalleeID(ref, arg.expr)
893+
replaceCalleeID(ref, cloneNode(arg.expr).(ast.Expr))
879894
}
880895
eliminatedParams[i] = true
881896
args[i] = nil
@@ -889,6 +904,19 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
889904
}
890905
}
891906

907+
// TODO(adonovan): eliminate all remaining parameters
908+
// by replacing a call f(a1, a2)
909+
// to func f(x T1, y T2) {body} by
910+
// { var x T1 = a1
911+
// var y T2 = a2
912+
// body }
913+
// if x ∉ freevars(a2) or freevars(T2), and so on,
914+
// plus the usual checks for return conversions (if any),
915+
// complex control, etc.
916+
//
917+
// If viable, use this with the reduction strategies below
918+
// that produce a block (not a value).
919+
892920
// -- let the inlining strategies begin --
893921

894922
// TODO(adonovan): split this huge function into a sequence of
@@ -1044,9 +1072,9 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
10441072
// Inlining:
10451073
// return f(args)
10461074
// where:
1047-
// func f(params) (results) { ...body... }
1075+
// func f(params) (results) { body }
10481076
// reduces to:
1049-
// ...body...
1077+
// { body }
10501078
// so long as:
10511079
// - all parameters are eliminated;
10521080
// - call is a tail-call;
@@ -1059,6 +1087,10 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
10591087
//
10601088
// TODO(adonovan): omit the braces if the sets of
10611089
// names in the two blocks are disjoint.
1090+
//
1091+
// TODO(adonovan): add a strategy for a 'void tail
1092+
// call', i.e. a call statement prior to an (explicit
1093+
// or implicit) return.
10621094
if ret, ok := callContext(callerPath).(*ast.ReturnStmt); ok &&
10631095
len(ret.Results) == 1 &&
10641096
callee.TrivialReturns == callee.TotalReturns &&
@@ -1122,15 +1154,21 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu
11221154
// in addition to the usual checks for arg/result conversions,
11231155
// complex control, etc.
11241156
// Also test cases where expr is an n-ary call (spread returns).
1157+
}
11251158

1126-
// TODO(adonovan): replace a call f(a1, a2)
1127-
// to func f(x T1, y T2) {body} by
1128-
// { var x T1 = a1
1129-
// var y T2 = a2
1130-
// body }
1131-
// if x ∉ freevars(a2) or freevars(T2), and so on,
1132-
// plus the usual checks for return conversions (if any),
1133-
// complex control, etc.
1159+
// Literalization isn't quite infallible.
1160+
// Consider a spread call to a method in which
1161+
// no parameters are eliminated, e.g.
1162+
// new(T).f(g())
1163+
// where
1164+
// func (recv *T) f(x, y int) { body }
1165+
// func g() (int, int)
1166+
// This would be literalized to:
1167+
// func (recv *T, x, y int) { body }(new(T), g()),
1168+
// which is not a valid argument list because g() must appear alone.
1169+
// Reject this case for now.
1170+
if len(args) == 2 && args[0] != nil && args[1] != nil && is[*types.Tuple](args[1].typ) {
1171+
return nil, fmt.Errorf("can't yet inline spread call to method")
11341172
}
11351173

11361174
// Infallible general case: literalization.
@@ -1547,6 +1585,54 @@ func replaceNode(root ast.Node, from, to ast.Node) {
15471585
}
15481586
}
15491587

1588+
// cloneNode returns a deep copy of a Node.
1589+
// It omits pointers to ast.{Scope,Object} variables.
1590+
func cloneNode(n ast.Node) ast.Node {
1591+
var clone func(x reflect.Value) reflect.Value
1592+
clone = func(x reflect.Value) reflect.Value {
1593+
switch x.Kind() {
1594+
case reflect.Ptr:
1595+
if x.IsNil() {
1596+
return x
1597+
}
1598+
// Skip fields of types potentially involved in cycles.
1599+
switch x.Interface().(type) {
1600+
case *ast.Object, *ast.Scope:
1601+
return reflect.Zero(x.Type())
1602+
}
1603+
y := reflect.New(x.Type().Elem())
1604+
y.Elem().Set(clone(x.Elem()))
1605+
return y
1606+
1607+
case reflect.Struct:
1608+
y := reflect.New(x.Type()).Elem()
1609+
for i := 0; i < x.Type().NumField(); i++ {
1610+
y.Field(i).Set(clone(x.Field(i)))
1611+
}
1612+
return y
1613+
1614+
case reflect.Slice:
1615+
y := reflect.MakeSlice(x.Type(), x.Len(), x.Cap())
1616+
for i := 0; i < x.Len(); i++ {
1617+
y.Index(i).Set(clone(x.Index(i)))
1618+
}
1619+
return y
1620+
1621+
case reflect.Interface:
1622+
y := reflect.New(x.Type()).Elem()
1623+
y.Set(clone(x.Elem()))
1624+
return y
1625+
1626+
case reflect.Array, reflect.Chan, reflect.Func, reflect.Map, reflect.UnsafePointer:
1627+
panic(x) // unreachable in AST
1628+
1629+
default:
1630+
return x // bool, string, number
1631+
}
1632+
}
1633+
return clone(reflect.ValueOf(n)).Interface().(ast.Node)
1634+
}
1635+
15501636
// clearPositions destroys token.Pos information within the tree rooted at root,
15511637
// as positions in callee trees may cause caller comments to be emitted prematurely.
15521638
//
@@ -1606,7 +1692,9 @@ func prepend[T any](elem T, slice ...T) []T {
16061692
return append([]T{elem}, slice...)
16071693
}
16081694

1609-
func formatNode(fset *token.FileSet, n ast.Node) string {
1695+
// debugFormatNode formats a node or returns a formatting error.
1696+
// Its sloppy treatment of errors is appropriate only for logging.
1697+
func debugFormatNode(fset *token.FileSet, n ast.Node) string {
16101698
var out strings.Builder
16111699
if err := format.Node(&out, fset, n); err != nil {
16121700
out.WriteString(err.Error())

0 commit comments

Comments
 (0)