internal/refactor/inline: avoid unnecessary import renames

When an inline eliminates the last use of an import,
that name is available for re-use. Don't choose a worse
name when adding imports for the callee.

Fixes golang/go#67281

Change-Id: Idb731e0d3073292c639697819236448f99b7602a
Reviewed-on: https://go-review.googlesource.com/c/tools/+/592575
Reviewed-by: Robert Findley <rfindley@google.com>
Reviewed-by: Lasse Folger <lassefolger@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/go/ast/astutil/util.go b/go/ast/astutil/util.go
index 919d530..6bdcf70 100644
--- a/go/ast/astutil/util.go
+++ b/go/ast/astutil/util.go
@@ -7,6 +7,7 @@
 import "go/ast"
 
 // Unparen returns e with any enclosing parentheses stripped.
+// TODO(adonovan): use go1.22's ast.Unparen.
 func Unparen(e ast.Expr) ast.Expr {
 	for {
 		p, ok := e.(*ast.ParenExpr)
diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go
index 4cf14e8..da2d26e 100644
--- a/internal/refactor/inline/inline.go
+++ b/internal/refactor/inline/inline.go
@@ -273,6 +273,32 @@
 		}
 	}
 
+	// Delete imports referenced only by caller.Call.Fun.
+	//
+	// (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 _, 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 {
+					imp := spec.(*ast.ImportSpec)
+					// Since we re-parsed the file, we can't match by identity;
+					// instead look for syntactic equivalence.
+					return imp.Path.Value == specToDelete.Path.Value &&
+						(imp.Name != nil) == (specToDelete.Name != nil) &&
+						(imp.Name == nil || imp.Name.Name == specToDelete.Name.Name)
+				})
+
+				// Edge case: import "foo" => import ().
+				if !decl.Lparen.IsValid() {
+					decl.Lparen = decl.TokPos + token.Pos(len("import"))
+					decl.Rparen = decl.Lparen + 1
+				}
+			}
+		}
+	}
+
 	var out bytes.Buffer
 	if err := format.Node(&out, caller.Fset, f); err != nil {
 		return nil, err
@@ -357,7 +383,9 @@
 }
 
 type inlineCallResult struct {
-	newImports []newImport
+	newImports []newImport       // to add
+	oldImports []*ast.ImportSpec // 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
 	// the block is OK; if elideBraces is unset and old is an ast.Stmt and new is
@@ -456,6 +484,8 @@
 		}
 	}
 
+	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 {
@@ -481,6 +511,38 @@
 			return false
 		}
 
+		// 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 := astutil.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
+							}
+						}
+					}
+				}
+			}
+
+			return existing != nil
+		}
+
 		// import added by callee
 		//
 		// Choose local PkgName based on last segment of
@@ -494,7 +556,7 @@
 		// here?
 		base := obj.PkgName
 		name := base
-		for n := 0; obj.Shadow[name] || caller.lookup(name) != nil || newlyAdded(name) || name == "init"; n++ {
+		for n := 0; obj.Shadow[name] || shadowedInCaller(name) || newlyAdded(name) || name == "init"; n++ {
 			name = fmt.Sprintf("%s%d", base, n)
 		}
 
@@ -600,6 +662,7 @@
 
 	res := &inlineCallResult{
 		newImports: newImports,
+		oldImports: oldImports,
 	}
 
 	// Parse callee function declaration.
@@ -3178,4 +3241,49 @@
 	return false
 }
 
+// soleUse returns the ident that refers to obj, if there is exactly one.
+func soleUse(info *types.Info, obj types.Object) (sole *ast.Ident) {
+	// This is not efficient, but it is called infrequently.
+	for id, obj2 := range info.Uses {
+		if obj2 == obj {
+			if sole != nil {
+				return nil // not unique
+			}
+			sole = id
+		}
+	}
+	return sole
+}
+
 type unit struct{} // for representing sets as maps
+
+// slicesDeleteFunc removes any elements from s for which del returns true,
+// returning the modified slice.
+// slicesDeleteFunc zeroes the elements between the new length and the original length.
+// TODO(adonovan): use go1.21 slices.DeleteFunc
+func slicesDeleteFunc[S ~[]E, E any](s S, del func(E) bool) S {
+	i := slicesIndexFunc(s, del)
+	if i == -1 {
+		return s
+	}
+	// Don't start copying elements until we find one to delete.
+	for j := i + 1; j < len(s); j++ {
+		if v := s[j]; !del(v) {
+			s[i] = v
+			i++
+		}
+	}
+	//	clear(s[i:]) // zero/nil out the obsolete elements, for GC
+	return s[:i]
+}
+
+// slicesIndexFunc returns the first index i satisfying f(s[i]),
+// or -1 if none do.
+func slicesIndexFunc[S ~[]E, E any](s S, f func(E) bool) int {
+	for i := range s {
+		if f(s[i]) {
+			return i
+		}
+	}
+	return -1
+}
diff --git a/internal/refactor/inline/testdata/import-rename.txtar b/internal/refactor/inline/testdata/import-rename.txtar
new file mode 100644
index 0000000..0b567f6
--- /dev/null
+++ b/internal/refactor/inline/testdata/import-rename.txtar
@@ -0,0 +1,40 @@
+Regtest for https://github.com/golang/go/issues/67281
+
+-- go.mod --
+module example.com
+go 1.19
+
+-- main/main.go --
+package main
+
+import "example.com/a"
+
+func main() {
+	a.A() //@ inline(re"A", result)
+}
+
+-- a/a.go --
+package a
+
+import "example.com/other/a"
+
+func A() {
+	a.A()
+}
+
+-- other/a/a.go --
+package a
+
+func A() {
+}
+
+-- result --
+package main
+
+import (
+	"example.com/other/a"
+)
+
+func main() {
+	a.A() //@ inline(re"A", result)
+}