go/analysis/passes/modernize: reflecttypefor: remove orphaned var

This CL fixes a bug in reflecttypefor that caused it to leave
a declaration for a local variable that was no longer used,
which is not legal.  Now, it deletes the variable:

- var zero T
- reflect.TypeOf(zero)
+ reflect.TypeFor[T]()

The bookkeeping is done by the new refactor.DeleteUnusedVars function.

Fixes golang/go#75767

Change-Id: If51af1500a63c968098b4a518b941315e826fcbf
Reviewed-on: https://go-review.googlesource.com/c/tools/+/710656
Reviewed-by: Madeline Kalil <mkalil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: shuang cui <imcusg@gmail.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
diff --git a/go/analysis/passes/modernize/modernize_test.go b/go/analysis/passes/modernize/modernize_test.go
index 0ca72ca..64bf60c 100644
--- a/go/analysis/passes/modernize/modernize_test.go
+++ b/go/analysis/passes/modernize/modernize_test.go
@@ -10,6 +10,7 @@
 	. "golang.org/x/tools/go/analysis/analysistest"
 	"golang.org/x/tools/go/analysis/passes/modernize"
 	"golang.org/x/tools/internal/goplsexport"
+	"golang.org/x/tools/internal/testenv"
 )
 
 func TestAppendClipped(t *testing.T) {
@@ -61,6 +62,7 @@
 }
 
 func TestReflectTypeFor(t *testing.T) {
+	testenv.NeedsGo1Point(t, 25) // requires go1.25 types.Var.Kind
 	RunWithSuggestedFixes(t, TestData(), modernize.ReflectTypeForAnalyzer, "reflecttypefor")
 }
 
diff --git a/go/analysis/passes/modernize/reflect.go b/go/analysis/passes/modernize/reflect.go
index 1a4e3eb..c9b0fa4 100644
--- a/go/analysis/passes/modernize/reflect.go
+++ b/go/analysis/passes/modernize/reflect.go
@@ -18,6 +18,7 @@
 	"golang.org/x/tools/internal/analysisinternal/generated"
 	typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
 	"golang.org/x/tools/internal/astutil"
+	"golang.org/x/tools/internal/refactor"
 	"golang.org/x/tools/internal/typesinternal"
 	"golang.org/x/tools/internal/typesinternal/typeindex"
 	"golang.org/x/tools/internal/versions"
@@ -66,16 +67,15 @@
 				obj := typeutil.Callee(info, call2)
 				if typesinternal.IsMethodNamed(obj, "reflect", "Type", "Elem") {
 					if ptr, ok := t.(*types.Pointer); ok {
-						// Have: reflect.TypeOf(...*T value...).Elem()
-						// => reflect.TypeFor[T]()
+						// Have: TypeOf(expr).Elem() where expr : *T
 						t = ptr.Elem()
-						edits = []analysis.TextEdit{
-							{
-								// delete .Elem()
-								Pos: call.End(),
-								End: call2.End(),
-							},
-						}
+						// reflect.TypeOf(expr).Elem()
+						//                     -------
+						// reflect.TypeOf(expr)
+						edits = []analysis.TextEdit{{
+							Pos: call.End(),
+							End: call2.End(),
+						}}
 					}
 				}
 			}
@@ -92,6 +92,7 @@
 		if versions.Before(info.FileVersions[file], "go1.22") {
 			continue // TypeFor requires go1.22
 		}
+		tokFile := pass.Fset.File(file.Pos())
 
 		// Format the type as valid Go syntax.
 		// TODO(adonovan): FileQualifier needs to respect
@@ -105,6 +106,14 @@
 			continue // e.g. reflect was dot-imported
 		}
 
+		// If the call argument contains the last use
+		// of a variable, as in:
+		//	var zero T
+		//	reflect.TypeOf(zero)
+		// remove the declaration of that variable.
+		curArg0 := curCall.ChildAt(edge.CallExpr_Args, 0)
+		edits = append(edits, refactor.DeleteUnusedVars(index, info, tokFile, curArg0)...)
+
 		pass.Report(analysis.Diagnostic{
 			Pos:     call.Fun.Pos(),
 			End:     call.Fun.End(),
diff --git a/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go b/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go
index a1042de..6979aa9 100644
--- a/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go
+++ b/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go
@@ -19,3 +19,13 @@
 	_ = reflect.TypeOf(time.Time{})       // want "reflect.TypeOf call can be simplified using TypeFor"
 	_ = reflect.TypeOf(time.Duration(0))  // want "reflect.TypeOf call can be simplified using TypeFor"
 )
+
+// Eliminate local var if we deleted its last use.
+func _() {
+	var zero string
+	_ = reflect.TypeOf(zero) // want "reflect.TypeOf call can be simplified using TypeFor"
+
+	var z2 string
+	_ = reflect.TypeOf(z2) // want "reflect.TypeOf call can be simplified using TypeFor"
+	_ = z2                 // z2 has multiple uses
+}
diff --git a/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go.golden b/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go.golden
index a3e7c07..3f137a5 100644
--- a/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go.golden
+++ b/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go.golden
@@ -20,3 +20,11 @@
 	_ = reflect.TypeFor[time.Duration]() // want "reflect.TypeOf call can be simplified using TypeFor"
 )
 
+// Eliminate local var if we deleted its last use.
+func _() {
+	_ = reflect.TypeFor[string]() // want "reflect.TypeOf call can be simplified using TypeFor"
+
+	var z2 string
+	_ = reflect.TypeFor[string]() // want "reflect.TypeOf call can be simplified using TypeFor"
+	_ = z2                 // z2 has multiple uses
+}
diff --git a/internal/refactor/delete.go b/internal/refactor/delete.go
index aa8ba5a..19c3af1 100644
--- a/internal/refactor/delete.go
+++ b/internal/refactor/delete.go
@@ -18,6 +18,7 @@
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/typesinternal"
+	"golang.org/x/tools/internal/typesinternal/typeindex"
 )
 
 // DeleteVar returns edits to delete the declaration of a variable
@@ -431,3 +432,34 @@
 	}
 	return []analysis.TextEdit{edit}
 }
+
+// DeleteUnusedVars computes the edits required to delete the
+// declarations of any local variables whose last uses are in the
+// curDelend subtree, which is about to be deleted.
+func DeleteUnusedVars(index *typeindex.Index, info *types.Info, tokFile *token.File, curDelend inspector.Cursor) []analysis.TextEdit {
+	// TODO(adonovan): we might want to generalize this by
+	// splitting the two phases below, so that we can gather
+	// across a whole sequence of deletions then finally compute the
+	// set of variables that are no longer wanted.
+
+	// Count number of deletions of each var.
+	delcount := make(map[*types.Var]int)
+	for curId := range curDelend.Preorder((*ast.Ident)(nil)) {
+		id := curId.Node().(*ast.Ident)
+		if v, ok := info.Uses[id].(*types.Var); ok &&
+			typesinternal.GetVarKind(v) == typesinternal.LocalVar { // always false before go1.25
+			delcount[v]++
+		}
+	}
+
+	// Delete declaration of each var that became unused.
+	var edits []analysis.TextEdit
+	for v, count := range delcount {
+		if len(slices.Collect(index.Uses(v))) == count {
+			if curDefId, ok := index.Def(v); ok {
+				edits = append(edits, DeleteVar(tokFile, info, curDefId)...)
+			}
+		}
+	}
+	return edits
+}