internal/refactor: handle qualified names in inlined assignments
Rewrite our call site processing of imports, to simplify, and so that we
can re-use the import lookup logic in the assignStmts strategy for the
purpose of writing out types.
A large TODO is included for a hypothetical refactoring of the inlining
logic that could formalize these types of interactions between call site
analysis and inlining strategy.
Fixes golang/go#65217
Change-Id: Ifd99ea14430deba3a03cdfb936b6edee9e81d0bf
Reviewed-on: https://go-review.googlesource.com/c/tools/+/629435
Reviewed-by: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/refactor/inline/callee.go b/internal/refactor/inline/callee.go
index c72699c..d2656eb 100644
--- a/internal/refactor/inline/callee.go
+++ b/internal/refactor/inline/callee.go
@@ -202,19 +202,19 @@
objidx, ok := freeObjIndex[obj]
if !ok {
objidx = len(freeObjIndex)
- var pkgpath, pkgname string
+ var pkgPath, pkgName string
if pn, ok := obj.(*types.PkgName); ok {
- pkgpath = pn.Imported().Path()
- pkgname = pn.Imported().Name()
+ pkgPath = pn.Imported().Path()
+ pkgName = pn.Imported().Name()
} else if obj.Pkg() != nil {
- pkgpath = obj.Pkg().Path()
- pkgname = obj.Pkg().Name()
+ pkgPath = obj.Pkg().Path()
+ pkgName = obj.Pkg().Name()
}
freeObjs = append(freeObjs, object{
Name: obj.Name(),
Kind: objectKind(obj),
- PkgName: pkgname,
- PkgPath: pkgpath,
+ PkgName: pkgName,
+ PkgPath: pkgPath,
ValidPos: obj.Pos().IsValid(),
})
freeObjIndex[obj] = objidx
diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go
index 6a7fc12..f93e740 100644
--- a/internal/refactor/inline/inline.go
+++ b/internal/refactor/inline/inline.go
@@ -279,7 +279,8 @@
// (We can't let imports.Process take care of it as it may
// mistake obsolete imports for missing new imports when the
// names are similar, as is common during a package migration.)
- for _, specToDelete := range res.oldImports {
+ for _, oldImport := range res.oldImports {
+ specToDelete := oldImport.spec
for _, decl := range f.Decls {
if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT {
decl.Specs = slicesDeleteFunc(decl.Specs, func(spec ast.Spec) bool {
@@ -375,17 +376,23 @@
Content: newSrc,
Literalized: literalized,
}, nil
-
}
+// An oldImport is an import that will be deleted from the caller file.
+type oldImport struct {
+ pkgName *types.PkgName
+ spec *ast.ImportSpec
+}
+
+// A newImport is an import that will be added to the caller file.
type newImport struct {
pkgName string
spec *ast.ImportSpec
}
type inlineCallResult struct {
- newImports []newImport // to add
- oldImports []*ast.ImportSpec // to remove
+ newImports []newImport // to add
+ oldImports []oldImport // to remove
// If elideBraces is set, old is an ast.Stmt and new is an ast.BlockStmt to
// be spliced in. This allows the inlining analysis to assert that inlining
@@ -412,6 +419,9 @@
// transformation replacing the call and adding new variable
// declarations, for example, or replacing a call statement by zero or
// many statements.)
+// NOTE(rfindley): we've sort-of done this, with the 'elideBraces' flag that
+// allows inlining a statement list. However, due to loss of comments, more
+// sophisticated rewrites are challenging.
//
// TODO(adonovan): in earlier drafts, the transformation was expressed
// by splicing substrings of the two source files because syntax
@@ -421,6 +431,33 @@
// candidate for evaluating an alternative fully self-contained tree
// representation, such as any proposed solution to #20744, or even
// dst or some private fork of go/ast.)
+// TODO(rfindley): see if we can reduce the amount of comment lossiness by
+// using printer.CommentedNode, which has been useful elsewhere.
+//
+// TODO(rfindley): inlineCall is getting very long, and very stateful, making
+// it very hard to read. The following refactoring may improve readability and
+// maintainability:
+// - Rename 'state' to 'callsite', since that is what it encapsulates.
+// - Add results of pre-processing analysis into the callsite struct, such as
+// the effective importMap, new/old imports, arguments, etc. Essentially
+// anything that resulted from initial analysis of the call site, and which
+// may be useful to inlining strategies.
+// - Delegate this call site analysis to a constructor or initializer, such
+// as 'analyzeCallsite', so that it does not consume bandwidth in the
+// 'inlineCall' logical flow.
+// - Once analyzeCallsite returns, the callsite is immutable, much in the
+// same way as the Callee and Caller are immutable.
+// - Decide on a standard interface for strategies (and substrategies), such
+// that they may be delegated to a separate method on callsite.
+//
+// In this way, the logical flow of inline call will clearly follow the
+// following structure:
+// 1. Analyze the call site.
+// 2. Try strategies, in order, until one succeeds.
+// 3. Process the results.
+//
+// If any expensive analysis may be avoided by earlier strategies, it can be
+// encapsulated in its own type and passed to subsequent strategies.
func (st *state) inlineCall() (*inlineCallResult, error) {
logf, caller, callee := st.opts.Logf, st.caller, &st.callee.impl
@@ -469,39 +506,83 @@
assign1 = func(v *types.Var) bool { return !updatedLocals[v] }
}
- // import map, initially populated with caller imports.
+ // import map, initially populated with caller imports, and updated below
+ // with new imports necessary to reference free symbols in the callee.
//
- // 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.
+ // 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(s)
+ var oldImports []oldImport // imports referenced only by caller.Call.Fun
+
for _, imp := range caller.File.Imports {
- if pkgname, ok := importedPkgName(caller.Info, imp); ok &&
- pkgname.Name() != "." &&
- pkgname.Name() != "_" {
- path := pkgname.Imported().Path()
- importMap[path] = append(importMap[path], pkgname.Name())
+ if pkgName, ok := importedPkgName(caller.Info, imp); ok &&
+ pkgName.Name() != "." &&
+ pkgName.Name() != "_" {
+
+ // If the import's sole use is in caller.Call.Fun of the form p.F(...),
+ // where p.F is a qualified identifier, the p import may not be
+ // necessary.
+ //
+ // Only the qualified identifier case matters, as other references to
+ // imported package names in the Call.Fun expression (e.g.
+ // x.after(3*time.Second).f() or time.Second.String()) will remain after
+ // inlining, as arguments.
+ //
+ // If that is the case, proactively check if any of the callee FreeObjs
+ // need this import. Doing so eagerly simplifies the resulting logic.
+ needed := true
+ sel, ok := ast.Unparen(caller.Call.Fun).(*ast.SelectorExpr)
+ if ok && soleUse(caller.Info, pkgName) == sel.X {
+ needed = false // no longer needed by caller
+ // Check to see if any of the inlined free objects need this package.
+ for _, obj := range callee.FreeObjs {
+ if obj.PkgPath == pkgName.Imported().Path() && !obj.Shadow[pkgName.Name()] {
+ needed = true // needed by callee
+ break
+ }
+ }
+ }
+
+ if needed {
+ path := pkgName.Imported().Path()
+ importMap[path] = append(importMap[path], pkgName.Name())
+ } else {
+ oldImports = append(oldImports, oldImport{pkgName: pkgName, spec: imp})
+ }
}
}
- var oldImports []*ast.ImportSpec // imports referenced only caller.Call.Fun
-
- // localImportName returns the local name for a given imported package path.
- var newImports []newImport
- localImportName := func(obj *object) string {
- // Does an import exist?
- for _, name := range importMap[obj.PkgPath] {
- // Check that either the import preexisted,
- // or that it was newly added (no PkgName) but is not shadowed,
- // either in the callee (shadows) or caller (caller.lookup).
- if !obj.Shadow[name] {
+ // importName finds an existing import name to use in a particular shadowing
+ // context. It is used to determine the set of new imports in
+ // getOrMakeImportName, and is also used for writing out names in inlining
+ // strategies below.
+ importName := func(pkgPath string, shadow map[string]bool) string {
+ for _, name := range importMap[pkgPath] {
+ // Check that either the import preexisted, or that it was newly added
+ // (no PkgName) but is not shadowed, either in the callee (shadows) or
+ // caller (caller.lookup).
+ if !shadow[name] {
found := caller.lookup(name)
if is[*types.PkgName](found) || found == nil {
return name
}
}
}
+ return ""
+ }
+
+ // keep track of new imports that are necessary to reference any free names
+ // in the callee.
+ var newImports []newImport
+
+ // getOrMakeImportName returns the local name for a given imported package path,
+ // adding one if it doesn't exists.
+ getOrMakeImportName := func(pkgPath, pkgName string, shadow map[string]bool) string {
+ // Does an import already exist that works in this shadowing context?
+ if name := importName(pkgPath, shadow); name != "" {
+ return name
+ }
newlyAdded := func(name string) bool {
for _, new := range newImports {
@@ -515,33 +596,17 @@
// shadowedInCaller reports whether a candidate package name
// already refers to a declaration in the caller.
shadowedInCaller := func(name string) bool {
- existing := caller.lookup(name)
-
- // If the candidate refers to a PkgName p whose sole use is
- // in caller.Call.Fun of the form p.F(...), where p.F is a
- // qualified identifier, the p import will be deleted,
- // so it's safe (and better) to recycle the name.
- //
- // Only the qualified identifier case matters, as other
- // references to imported package names in the Call.Fun
- // expression (e.g. x.after(3*time.Second).f()
- // or time.Second.String()) will remain after
- // inlining, as arguments.
- if pkgName, ok := existing.(*types.PkgName); ok {
- if sel, ok := ast.Unparen(caller.Call.Fun).(*ast.SelectorExpr); ok {
- if sole := soleUse(caller.Info, pkgName); sole == sel.X {
- for _, spec := range caller.File.Imports {
- pkgName2, ok := importedPkgName(caller.Info, spec)
- if ok && pkgName2 == pkgName {
- oldImports = append(oldImports, spec)
- return false
- }
- }
- }
+ obj := caller.lookup(name)
+ if obj == nil {
+ return false
+ }
+ // If obj will be removed, the name is available.
+ for _, old := range oldImports {
+ if old.pkgName == obj {
+ return false
}
}
-
- return existing != nil
+ return true
}
// import added by callee
@@ -555,29 +620,28 @@
// TODO(rfindley): is it worth preserving local package names for callee
// imports? Are they likely to be better or worse than the name we choose
// here?
- base := obj.PkgName
+ base := pkgName
name := base
- for n := 0; obj.Shadow[name] || shadowedInCaller(name) || newlyAdded(name) || name == "init"; n++ {
+ for n := 0; shadow[name] || shadowedInCaller(name) || newlyAdded(name) || name == "init"; n++ {
name = fmt.Sprintf("%s%d", base, n)
}
-
- logf("adding import %s %q", name, obj.PkgPath)
+ logf("adding import %s %q", name, pkgPath)
spec := &ast.ImportSpec{
Path: &ast.BasicLit{
Kind: token.STRING,
- Value: strconv.Quote(obj.PkgPath),
+ Value: strconv.Quote(pkgPath),
},
}
// Use explicit pkgname (out of necessity) when it differs from the declared name,
// or (for good style) when it differs from base(pkgpath).
- if name != obj.PkgName || name != pathpkg.Base(obj.PkgPath) {
+ if name != pkgName || name != pathpkg.Base(pkgPath) {
spec.Name = makeIdent(name)
}
newImports = append(newImports, newImport{
pkgName: name,
spec: spec,
})
- importMap[obj.PkgPath] = append(importMap[obj.PkgPath], name)
+ importMap[pkgPath] = append(importMap[pkgPath], name)
return name
}
@@ -607,7 +671,8 @@
var newName ast.Expr
if obj.Kind == "pkgname" {
// Use locally appropriate import, creating as needed.
- newName = makeIdent(localImportName(&obj)) // imported package
+ n := getOrMakeImportName(obj.PkgPath, obj.PkgName, obj.Shadow)
+ newName = makeIdent(n) // imported package
} else if !obj.ValidPos {
// Built-in function, type, or value (e.g. nil, zero):
// check not shadowed at caller.
@@ -651,7 +716,7 @@
// Form a qualified identifier, pkg.Name.
if qualify {
- pkgName := localImportName(&obj)
+ pkgName := getOrMakeImportName(obj.PkgPath, obj.PkgName, obj.Shadow)
newName = &ast.SelectorExpr{
X: makeIdent(pkgName),
Sel: makeIdent(obj.Name),
@@ -992,8 +1057,9 @@
(!needBindingDecl || (bindingDecl != nil && len(bindingDecl.names) == 0)) {
// Reduces to: { var (bindings); lhs... := rhs... }
- if newStmts, ok := st.assignStmts(stmt, results); ok {
+ if newStmts, ok := st.assignStmts(stmt, results, importName); ok {
logf("strategy: reduce assign-context call to { return exprs }")
+
clearPositions(calleeDecl.Body)
block := &ast.BlockStmt{
@@ -2890,6 +2956,15 @@
return names
}
+// A importNameFunc is used to query local import names in the caller, in a
+// particular shadowing context.
+//
+// The shadow map contains additional names shadowed in the inlined code, at
+// the position the local import name is to be used. The shadow map only needs
+// to contain newly introduced names in the inlined code; names shadowed at the
+// caller are handled automatically.
+type importNameFunc = func(pkgPath string, shadow map[string]bool) string
+
// assignStmts rewrites a statement assigning the results of a call into zero
// or more statements that assign its return operands, or (nil, false) if no
// such rewrite is possible. The set of bindings created by the result of
@@ -2932,7 +3007,7 @@
//
// Note: assignStmts may return (nil, true) if it determines that the rewritten
// assignment consists only of _ = nil assignments.
-func (st *state) assignStmts(callerStmt *ast.AssignStmt, returnOperands []ast.Expr) ([]ast.Stmt, bool) {
+func (st *state) assignStmts(callerStmt *ast.AssignStmt, returnOperands []ast.Expr, importName importNameFunc) ([]ast.Stmt, bool) {
logf, caller, callee := st.opts.Logf, st.caller, &st.callee.impl
assert(len(callee.Returns) == 1, "unexpected multiple returns")
@@ -3035,18 +3110,23 @@
// Inlining techniques below will need to write type information in order to
// preserve the correct types of LHS identifiers.
//
- // writeType is a simple helper to write out type expressions.
+ // typeExpr is a simple helper to write out type expressions. It currently
+ // handles (possibly qualified) type names.
+ //
// TODO(rfindley):
- // 1. handle qualified type names (potentially adding new imports)
- // 2. expand this to handle more type expressions.
- // 3. refactor to share logic with callee rewriting.
+ // 1. expand this to handle more type expressions.
+ // 2. refactor to share logic with callee rewriting.
universeAny := types.Universe.Lookup("any")
- typeExpr := func(typ types.Type, shadows ...map[string]bool) ast.Expr {
- var typeName string
+ typeExpr := func(typ types.Type, shadow map[string]bool) ast.Expr {
+ var (
+ typeName string
+ obj *types.TypeName // nil for basic types
+ )
switch typ := typ.(type) {
case *types.Basic:
typeName = typ.Name()
case interface{ Obj() *types.TypeName }: // Named, Alias, TypeParam
+ obj = typ.Obj()
typeName = typ.Obj().Name()
}
@@ -3060,15 +3140,20 @@
return nil
}
- for _, shadow := range shadows {
+ if obj == nil || obj.Pkg() == nil || obj.Pkg() == caller.Types { // local type or builtin
if shadow[typeName] {
logf("cannot write shadowed type name %q", typeName)
return nil
}
- }
- obj, _ := caller.lookup(typeName).(*types.TypeName)
- if obj != nil && types.Identical(obj.Type(), typ) {
- return ast.NewIdent(typeName)
+ obj, _ := caller.lookup(typeName).(*types.TypeName)
+ if obj != nil && types.Identical(obj.Type(), typ) {
+ return ast.NewIdent(typeName)
+ }
+ } else if pkgName := importName(obj.Pkg().Path(), shadow); pkgName != "" {
+ return &ast.SelectorExpr{
+ X: ast.NewIdent(pkgName),
+ Sel: ast.NewIdent(typeName),
+ }
}
return nil
}
@@ -3187,7 +3272,7 @@
idx := origIdxs[i]
if nonTrivial[idx] && defs[idx] != nil {
typ := caller.Info.TypeOf(lhs[i])
- texpr := typeExpr(typ)
+ texpr := typeExpr(typ, nil)
if texpr == nil {
return nil, false
}
diff --git a/internal/refactor/inline/testdata/assignment.txtar b/internal/refactor/inline/testdata/assignment.txtar
new file mode 100644
index 0000000..1037cce
--- /dev/null
+++ b/internal/refactor/inline/testdata/assignment.txtar
@@ -0,0 +1,139 @@
+Basic tests of inlining a call on the RHS of an assignment.
+
+-- go.mod --
+module testdata
+go 1.12
+
+-- a/a1.go --
+package a
+
+import "testdata/b"
+
+func _() {
+ var y int
+ x, y := b.B1() //@ inline(re"B", b1)
+ _, _ = x, y
+}
+
+-- a/a2.go --
+package a
+
+import "testdata/b"
+
+func _() {
+ var y int
+ x, y := b.B2() //@ inline(re"B", b2)
+ _, _ = x, y
+}
+
+-- a/a3.go --
+package a
+
+import "testdata/b"
+
+func _() {
+ x, y := b.B3() //@ inline(re"B", b3)
+ _, _ = x, y
+}
+
+-- a/a4.go --
+package a
+
+import "testdata/b"
+
+func _() {
+ x, y := b.B4() //@ inline(re"B", b4)
+ _, _ = x, y
+}
+
+-- b/b.go --
+package b
+
+import (
+ "testdata/c"
+)
+
+func B1() (c.C, int) {
+ return 0, 1
+}
+
+func B2() (c.C, int) {
+ return B1()
+}
+
+func B3() (c.C, c.C) {
+ return 0, 1
+}
+
+-- b/b4.go --
+package b
+
+import (
+ c1 "testdata/c"
+ c2 "testdata/c2"
+)
+
+func B4() (c1.C, c2.C) {
+ return 0, 1
+}
+
+-- c/c.go --
+package c
+
+type C int
+
+-- c2/c.go --
+package c
+
+type C int
+
+-- b1 --
+package a
+
+import (
+ "testdata/c"
+)
+
+func _() {
+ var y int
+ x, y := c.C(0), 1 //@ inline(re"B", b1)
+ _, _ = x, y
+}
+-- b2 --
+package a
+
+import (
+ "testdata/b"
+ "testdata/c"
+)
+
+func _() {
+ var y int
+ var x c.C
+ x, y = b.B1() //@ inline(re"B", b2)
+ _, _ = x, y
+}
+-- b3 --
+package a
+
+import (
+ "testdata/c"
+)
+
+func _() {
+ x, y := c.C(0), c.C(1) //@ inline(re"B", b3)
+ _, _ = x, y
+}
+
+-- b4 --
+package a
+
+import (
+ "testdata/c"
+ c0 "testdata/c2"
+)
+
+func _() {
+ x, y := c.C(0), c0.C(1) //@ inline(re"B", b4)
+ _, _ = x, y
+}