go/analysis/passes/modernize: add reflecttypeassert analyzer

The new analyzer suggests replacing two-valued type assertions on the
result of (reflect.Value).Interface, x, ok := v.Interface().(T), with
reflect.TypeAssert[T](v), added in go1.25, which avoids allocating the
intermediate interface value.

Single-valued assertions are not rewritten, since they panic when the
assertion fails whereas TypeAssert does not, per the discussion on the
issue. Pointer receivers and files below go1.25 are also skipped.

Fixes golang/go#75422

Change-Id: Ia804899edc25e9291116614311ecf360126bfcf3
GitHub-Last-Rev: c92ceb1ab2f2ab5ed4260f23dafdbf52beff5a31
GitHub-Pull-Request: golang/tools#654
Reviewed-on: https://go-review.googlesource.com/c/tools/+/796740
Commit-Queue: Alan Donovan <adonovan@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
Reviewed-by: Madeline Kalil <mkalil@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Bypass: Alan Donovan <adonovan@google.com>
diff --git a/go/analysis/passes/modernize/doc.go b/go/analysis/passes/modernize/doc.go
index c5545c0..bf83de3 100644
--- a/go/analysis/passes/modernize/doc.go
+++ b/go/analysis/passes/modernize/doc.go
@@ -344,6 +344,21 @@
 
 or when the operand has potential side effects.
 
+# Analyzer reflecttypeassert
+
+reflecttypeassert: replace v.Interface().(T) with reflect.TypeAssert[T](v)
+
+This analyzer suggests fixes to replace two-valued type assertions on
+the result of (reflect.Value).Interface with reflect.TypeAssert,
+introduced in go1.25, which avoids the intermediate allocation of an
+interface value, for example:
+
+	x, ok := v.Interface().(string)  ->  x, ok := reflect.TypeAssert[string](v)
+
+No fix is offered for single-valued assertions, since they panic when
+the assertion fails whereas reflect.TypeAssert does not. Nor is a fix
+offered for a type switch.
+
 # Analyzer slicesbackward
 
 slicesbackward: replace backward loops over slices with slices.Backward
diff --git a/go/analysis/passes/modernize/export_test.go b/go/analysis/passes/modernize/export_test.go
index 0401cf0..1ca7539 100644
--- a/go/analysis/passes/modernize/export_test.go
+++ b/go/analysis/passes/modernize/export_test.go
@@ -7,7 +7,8 @@
 package modernize
 
 var (
-	ImportCommentAnalyzer  = importCommentAnalyzer
-	SlicesBackwardAnalyzer = slicesBackwardAnalyzer
-	UnsafeFuncsAnalyzer    = unsafeFuncsAnalyzer
+	ImportCommentAnalyzer     = importCommentAnalyzer
+	ReflectTypeAssertAnalyzer = reflectTypeAssertAnalyzer
+	SlicesBackwardAnalyzer    = slicesBackwardAnalyzer
+	UnsafeFuncsAnalyzer       = unsafeFuncsAnalyzer
 )
diff --git a/go/analysis/passes/modernize/modernize.go b/go/analysis/passes/modernize/modernize.go
index 23dd8ab..5f44ba8 100644
--- a/go/analysis/passes/modernize/modernize.go
+++ b/go/analysis/passes/modernize/modernize.go
@@ -45,6 +45,7 @@
 	OmitZeroAnalyzer,
 	PlusBuildAnalyzer,
 	RangeIntAnalyzer,
+	reflectTypeAssertAnalyzer, // awaiting public symbol
 	ReflectTypeForAnalyzer,
 	slicesBackwardAnalyzer, // awaiting public symbol
 	SlicesContainsAnalyzer,
diff --git a/go/analysis/passes/modernize/modernize_test.go b/go/analysis/passes/modernize/modernize_test.go
index bb2f7ac..e939510 100644
--- a/go/analysis/passes/modernize/modernize_test.go
+++ b/go/analysis/passes/modernize/modernize_test.go
@@ -99,6 +99,11 @@
 	RunWithSuggestedFixes(t, TestData(), modernize.ReflectTypeForAnalyzer, "reflecttypefor")
 }
 
+func TestReflectTypeAssert(t *testing.T) {
+	testenv.NeedsGo1Point(t, 25) // reflect.TypeAssert requires go1.25
+	RunWithSuggestedFixes(t, TestData(), modernize.ReflectTypeAssertAnalyzer, "reflecttypeassert")
+}
+
 func TestSlicesBackward(t *testing.T) {
 	testenv.NeedsGo1Point(t, 23)
 	RunWithSuggestedFixes(t, TestData(), modernize.SlicesBackwardAnalyzer, "slicesbackward")
diff --git a/go/analysis/passes/modernize/reflecttypeassert.go b/go/analysis/passes/modernize/reflecttypeassert.go
new file mode 100644
index 0000000..f9c1207
--- /dev/null
+++ b/go/analysis/passes/modernize/reflecttypeassert.go
@@ -0,0 +1,118 @@
+// Copyright 2026 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 modernize
+
+import (
+	"go/ast"
+	"go/token"
+
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/passes/inspect"
+	"golang.org/x/tools/go/ast/edge"
+	"golang.org/x/tools/internal/analysis/analyzerutil"
+	typeindexanalyzer "golang.org/x/tools/internal/analysis/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"
+)
+
+var reflectTypeAssertAnalyzer = &analysis.Analyzer{
+	Name: "reflecttypeassert",
+	Doc:  analyzerutil.MustExtractDoc(doc, "reflecttypeassert"),
+	Requires: []*analysis.Analyzer{
+		inspect.Analyzer,
+		typeindexanalyzer.Analyzer,
+	},
+	Run: reflecttypeassert,
+	URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#reflecttypeassert",
+}
+
+func reflecttypeassert(pass *analysis.Pass) (any, error) {
+	var (
+		index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+		info  = pass.TypesInfo
+
+		valueInterface = index.Selection("reflect", "Value", "Interface")
+	)
+
+	for curCall := range index.Calls(valueInterface) {
+		call := curCall.Node().(*ast.CallExpr)
+		// Have: v.Interface()
+
+		sel, ok := call.Fun.(*ast.SelectorExpr)
+		if !ok {
+			continue // method expression reflect.Value.Interface(v)
+		}
+
+		// TypeAssert's argument must be a reflect.Value; a pointer
+		// receiver would need an explicit dereference in the rewrite.
+		if !typesinternal.IsTypeNamed(info.TypeOf(sel.X), "reflect", "Value") {
+			continue
+		}
+
+		// The call must be the operand of a type assertion
+		// (not a type switch, whose Type field is nil).
+		curOperand := astutil.UnparenEnclosingCursor(curCall)
+		if curOperand.ParentEdgeKind() != edge.TypeAssertExpr_X {
+			continue
+		}
+		curAssert := curOperand.Parent()
+		assert := curAssert.Node().(*ast.TypeAssertExpr)
+		if assert.Type == nil {
+			continue // type switch
+		}
+
+		// The assertion must be the sole RHS of a two-valued
+		// assignment, x, ok := v.Interface().(T), so that the
+		// rewrite preserves the "commaOK" semantics; a single-valued
+		// assertion panics on failure whereas TypeAssert does not.
+		curRhs := astutil.UnparenEnclosingCursor(curAssert)
+		if curRhs.ParentEdgeKind() != edge.AssignStmt_Rhs {
+			continue
+		}
+		assign := curRhs.Parent().Node().(*ast.AssignStmt)
+		if len(assign.Lhs) != 2 || len(assign.Rhs) != 1 ||
+			(assign.Tok != token.ASSIGN && assign.Tok != token.DEFINE) {
+			continue
+		}
+
+		file := astutil.EnclosingFile(curCall)
+		if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_25) {
+			continue // TypeAssert requires go1.25
+		}
+
+		prefix, importEdits := refactor.AddImport(info, file, "reflect", "reflect", "TypeAssert", assert.Pos())
+
+		tstr := astutil.Format(pass.Fset, assert.Type)
+		pass.Report(analysis.Diagnostic{
+			Pos:     assert.Pos(),
+			End:     assert.End(),
+			Message: "Interface().(" + tstr + ") can be simplified using reflect.TypeAssert",
+			SuggestedFixes: []analysis.SuggestedFix{{
+				// v.Interface().(T)  ->  reflect.TypeAssert[T](v)
+				Message: "Replace Interface().(" + tstr + ") by reflect.TypeAssert[" + tstr + "]",
+				// Edit around sel.X instead of reformatting it, so its
+				// comments and spacing are preserved; only the type,
+				// which must move, is reformatted.
+				TextEdits: append(importEdits,
+					analysis.TextEdit{
+						Pos:     assert.Pos(),
+						End:     sel.X.Pos(),
+						NewText: []byte(prefix + "TypeAssert[" + tstr + "]("),
+					},
+					analysis.TextEdit{
+						Pos:     sel.X.End(),
+						End:     assert.End(),
+						NewText: []byte(")"),
+					},
+				),
+			}},
+		})
+	}
+
+	return nil, nil
+}
diff --git a/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go b/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go
new file mode 100644
index 0000000..b7ad8a3
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go
@@ -0,0 +1,59 @@
+package reflecttypeassert
+
+import (
+	"io"
+	"reflect"
+)
+
+type payload struct{ n int }
+
+func twoValued(v reflect.Value) {
+	x, ok := v.Interface().(string) // want "Interface\\(\\)\\.\\(string\\) can be simplified using reflect.TypeAssert"
+	_, _ = x, ok
+
+	p, ok := v.Interface().(payload) // want "Interface\\(\\)\\.\\(payload\\) can be simplified using reflect.TypeAssert"
+	_, _ = p, ok
+
+	r, ok := v.Interface().(io.Reader) // want "Interface\\(\\)\\.\\(io.Reader\\) can be simplified using reflect.TypeAssert"
+	_, _ = r, ok
+}
+
+func assignment(v reflect.Value) {
+	var y int
+	var ok bool
+	y, ok = v.Interface().(int) // want "Interface\\(\\)\\.\\(int\\) can be simplified using reflect.TypeAssert"
+	_, _ = y, ok
+}
+
+func inIfInit(v reflect.Value) {
+	if s, ok := v.Interface().(string); ok { // want "Interface\\(\\)\\.\\(string\\) can be simplified using reflect.TypeAssert"
+		_ = s
+	}
+}
+
+func receiverExpr(vs []reflect.Value) {
+	e, ok := vs[0].Interface().(error) // want "Interface\\(\\)\\.\\(error\\) can be simplified using reflect.TypeAssert"
+	_, _ = e, ok
+}
+
+func nomatch(v reflect.Value, pv *reflect.Value, any1 any) {
+	// Single-valued assertion panics on failure; TypeAssert doesn't.
+	s := v.Interface().(string)
+	_ = s
+
+	// Not a type assertion on Value.Interface.
+	i, ok := any1.(int)
+	_, _ = i, ok
+
+	// Type switches have no TypeAssert equivalent.
+	switch v.Interface().(type) {
+	case string:
+	}
+
+	// Pointer receiver would need an explicit dereference; leave it alone.
+	ps, ok := pv.Interface().(string)
+	_, _ = ps, ok
+
+	// Interface method value invocation, not part of an assignment.
+	_ = v.Interface()
+}
diff --git a/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go.golden b/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go.golden
new file mode 100644
index 0000000..6d53437
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go.golden
@@ -0,0 +1,59 @@
+package reflecttypeassert
+
+import (
+	"io"
+	"reflect"
+)
+
+type payload struct{ n int }
+
+func twoValued(v reflect.Value) {
+	x, ok := reflect.TypeAssert[string](v) // want "Interface\\(\\)\\.\\(string\\) can be simplified using reflect.TypeAssert"
+	_, _ = x, ok
+
+	p, ok := reflect.TypeAssert[payload](v) // want "Interface\\(\\)\\.\\(payload\\) can be simplified using reflect.TypeAssert"
+	_, _ = p, ok
+
+	r, ok := reflect.TypeAssert[io.Reader](v) // want "Interface\\(\\)\\.\\(io.Reader\\) can be simplified using reflect.TypeAssert"
+	_, _ = r, ok
+}
+
+func assignment(v reflect.Value) {
+	var y int
+	var ok bool
+	y, ok = reflect.TypeAssert[int](v) // want "Interface\\(\\)\\.\\(int\\) can be simplified using reflect.TypeAssert"
+	_, _ = y, ok
+}
+
+func inIfInit(v reflect.Value) {
+	if s, ok := reflect.TypeAssert[string](v); ok { // want "Interface\\(\\)\\.\\(string\\) can be simplified using reflect.TypeAssert"
+		_ = s
+	}
+}
+
+func receiverExpr(vs []reflect.Value) {
+	e, ok := reflect.TypeAssert[error](vs[0]) // want "Interface\\(\\)\\.\\(error\\) can be simplified using reflect.TypeAssert"
+	_, _ = e, ok
+}
+
+func nomatch(v reflect.Value, pv *reflect.Value, any1 any) {
+	// Single-valued assertion panics on failure; TypeAssert doesn't.
+	s := v.Interface().(string)
+	_ = s
+
+	// Not a type assertion on Value.Interface.
+	i, ok := any1.(int)
+	_, _ = i, ok
+
+	// Type switches have no TypeAssert equivalent.
+	switch v.Interface().(type) {
+	case string:
+	}
+
+	// Pointer receiver would need an explicit dereference; leave it alone.
+	ps, ok := pv.Interface().(string)
+	_, _ = ps, ok
+
+	// Interface method value invocation, not part of an assignment.
+	_ = v.Interface()
+}
diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md
index 70d2b91..d97f05b 100644
--- a/gopls/doc/analyzers.md
+++ b/gopls/doc/analyzers.md
@@ -3838,6 +3838,20 @@
 
 Package documentation: [recursiveiter](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/recursiveiter)
 
+<a id='reflecttypeassert'></a>
+## `reflecttypeassert`: replace v.Interface().(T) with reflect.TypeAssert[T](v)
+
+This analyzer suggests fixes to replace two-valued type assertions on the result of (reflect.Value).Interface with reflect.TypeAssert, introduced in go1.25, which avoids the intermediate allocation of an interface value, for example:
+
+	x, ok := v.Interface().(string)  ->  x, ok := reflect.TypeAssert[string](v)
+
+No fix is offered for single-valued assertions, since they panic when the assertion fails whereas reflect.TypeAssert does not. Nor is a fix offered for a type switch.
+
+
+Default: on.
+
+Package documentation: [reflecttypeassert](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#reflecttypeassert)
+
 <a id='reflecttypefor'></a>
 ## `reflecttypefor`: replace reflect.TypeOf(x) with TypeFor[T]()
 
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json
index c01dbca..7df2149 100644
--- a/gopls/internal/doc/api.json
+++ b/gopls/internal/doc/api.json
@@ -1635,6 +1635,12 @@
 							"Status": ""
 						},
 						{
+							"Name": "\"reflecttypeassert\"",
+							"Doc": "replace v.Interface().(T) with reflect.TypeAssert[T](v)\n\nThis analyzer suggests fixes to replace two-valued type assertions on\nthe result of (reflect.Value).Interface with reflect.TypeAssert,\nintroduced in go1.25, which avoids the intermediate allocation of an\ninterface value, for example:\n\n\tx, ok := v.Interface().(string)  -\u003e  x, ok := reflect.TypeAssert[string](v)\n\nNo fix is offered for single-valued assertions, since they panic when\nthe assertion fails whereas reflect.TypeAssert does not. Nor is a fix\noffered for a type switch.",
+							"Default": "true",
+							"Status": ""
+						},
+						{
 							"Name": "\"reflecttypefor\"",
 							"Doc": "replace reflect.TypeOf(x) with TypeFor[T]()\n\nThis analyzer suggests fixes to replace uses of reflect.TypeOf(x) with\nreflect.TypeFor, introduced in go1.22, when the desired runtime type\nis known at compile time, for example:\n\n\treflect.TypeOf(uint32(0))        -\u003e reflect.TypeFor[uint32]()\n\treflect.TypeOf((*ast.File)(nil)) -\u003e reflect.TypeFor[*ast.File]()\n\nIt also offers a fix to simplify the constructions below, which use\nreflect.TypeOf to return the runtime type for an interface type,\n\n\treflect.TypeOf((*io.Reader)(nil)).Elem()\n\nor:\n\n\treflect.TypeOf([]io.Reader(nil)).Elem()\n\nto:\n\n\treflect.TypeFor[io.Reader]()\n\nNo fix is offered in cases when the runtime type is dynamic, such as:\n\n\tvar r io.Reader = ...\n\treflect.TypeOf(r)\n\nor when the operand has potential side effects.",
 							"Default": "true",
@@ -3670,6 +3676,12 @@
 			"Default": true
 		},
 		{
+			"Name": "reflecttypeassert",
+			"Doc": "replace v.Interface().(T) with reflect.TypeAssert[T](v)\n\nThis analyzer suggests fixes to replace two-valued type assertions on\nthe result of (reflect.Value).Interface with reflect.TypeAssert,\nintroduced in go1.25, which avoids the intermediate allocation of an\ninterface value, for example:\n\n\tx, ok := v.Interface().(string)  -\u003e  x, ok := reflect.TypeAssert[string](v)\n\nNo fix is offered for single-valued assertions, since they panic when\nthe assertion fails whereas reflect.TypeAssert does not. Nor is a fix\noffered for a type switch.",
+			"URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#reflecttypeassert",
+			"Default": true
+		},
+		{
 			"Name": "reflecttypefor",
 			"Doc": "replace reflect.TypeOf(x) with TypeFor[T]()\n\nThis analyzer suggests fixes to replace uses of reflect.TypeOf(x) with\nreflect.TypeFor, introduced in go1.22, when the desired runtime type\nis known at compile time, for example:\n\n\treflect.TypeOf(uint32(0))        -\u003e reflect.TypeFor[uint32]()\n\treflect.TypeOf((*ast.File)(nil)) -\u003e reflect.TypeFor[*ast.File]()\n\nIt also offers a fix to simplify the constructions below, which use\nreflect.TypeOf to return the runtime type for an interface type,\n\n\treflect.TypeOf((*io.Reader)(nil)).Elem()\n\nor:\n\n\treflect.TypeOf([]io.Reader(nil)).Elem()\n\nto:\n\n\treflect.TypeFor[io.Reader]()\n\nNo fix is offered in cases when the runtime type is dynamic, such as:\n\n\tvar r io.Reader = ...\n\treflect.TypeOf(r)\n\nor when the operand has potential side effects.",
 			"URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#reflecttypefor",