internal/analysisinternal: rationalize

This CL moves declarations as described below,
and updates comments. No logic was changed except
in merging the DocComment functions, and in
using safetoken from TypeErrorEndPos within gopls.

internal/astutil (pure syntax)
+ 	func Comments
+ 	func DocComment (merging various unexported copies)
+ 	func EnclosingFile
+ 	func Format
+ 	func IsChildOf

internal/typesinternal (typed syntax)
+ 	func EnclosingScope
+ 	func Imports
+ 	func IsFunctionNamed
+ 	func IsMethodNamed
+  	func IsPointerToNamed
+ 	func IsTypeNamed
- 	func IsZeroExpr (moved to fillreturns)

internal/analysisinternal (use of analysis framework)
- 	func AddImport
- 	func Comments
- 	func DeleteDecl
- 	func DeleteSpec
- 	func DeleteStmt
- 	func DeleteVar
- 	func EnclosingFile
- 	func EnclosingScope
- 	func Format
- 	func FreshName
- 	func Imports
- 	func IsChildOf
- 	func IsFunctionNamed
- 	func IsMethodNamed
- 	func IsPointerToNamed
- 	func IsTypeNamed
- 	func TypeErrorEndPos (moved to gopls/internal/cache)

internal/refactor: (computing text edits from typed syntax)
+ 	func AddImport
+ 	func DeleteDecl
+ 	func DeleteSpec
+ 	func DeleteStmt
+ 	func DeleteVar
+ 	func FreshName

Change-Id: I9fad550ee55efbeb217627570e3a2e9abfee2178
Reviewed-on: https://go-review.googlesource.com/c/tools/+/710295
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/go/analysis/passes/assign/assign.go b/go/analysis/passes/assign/assign.go
index 1914bb4..dfe68d9 100644
--- a/go/analysis/passes/assign/assign.go
+++ b/go/analysis/passes/assign/assign.go
@@ -19,7 +19,7 @@
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
 )
 
 //go:embed doc.go
@@ -66,8 +66,8 @@
 				!isMapIndex(pass.TypesInfo, lhs) &&
 				reflect.TypeOf(lhs) == reflect.TypeOf(rhs) { // short-circuit the heavy-weight gofmt check
 
-				le = analysisinternal.Format(pass.Fset, lhs)
-				re := analysisinternal.Format(pass.Fset, rhs)
+				le = astutil.Format(pass.Fset, lhs)
+				re := astutil.Format(pass.Fset, rhs)
 				if le == re {
 					isSelfAssign = true
 				}
diff --git a/go/analysis/passes/atomic/atomic.go b/go/analysis/passes/atomic/atomic.go
index 82d5439..ddd875b 100644
--- a/go/analysis/passes/atomic/atomic.go
+++ b/go/analysis/passes/atomic/atomic.go
@@ -14,7 +14,8 @@
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 //go:embed doc.go
@@ -30,7 +31,7 @@
 }
 
 func run(pass *analysis.Pass) (any, error) {
-	if !analysisinternal.Imports(pass.Pkg, "sync/atomic") {
+	if !typesinternal.Imports(pass.Pkg, "sync/atomic") {
 		return nil, nil // doesn't directly import sync/atomic
 	}
 
@@ -54,7 +55,7 @@
 				continue
 			}
 			obj := typeutil.Callee(pass.TypesInfo, call)
-			if analysisinternal.IsFunctionNamed(obj, "sync/atomic", "AddInt32", "AddInt64", "AddUint32", "AddUint64", "AddUintptr") {
+			if typesinternal.IsFunctionNamed(obj, "sync/atomic", "AddInt32", "AddInt64", "AddUint32", "AddUint64", "AddUintptr") {
 				checkAtomicAddAssignment(pass, n.Lhs[i], call)
 			}
 		}
@@ -72,7 +73,7 @@
 	arg := call.Args[0]
 	broken := false
 
-	gofmt := func(e ast.Expr) string { return analysisinternal.Format(pass.Fset, e) }
+	gofmt := func(e ast.Expr) string { return astutil.Format(pass.Fset, e) }
 
 	if uarg, ok := arg.(*ast.UnaryExpr); ok && uarg.Op == token.AND {
 		broken = gofmt(left) == gofmt(uarg.X)
diff --git a/go/analysis/passes/atomicalign/atomicalign.go b/go/analysis/passes/atomicalign/atomicalign.go
index 2508b41..84699dd 100644
--- a/go/analysis/passes/atomicalign/atomicalign.go
+++ b/go/analysis/passes/atomicalign/atomicalign.go
@@ -18,7 +18,7 @@
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 const Doc = "check for non-64-bits-aligned arguments to sync/atomic functions"
@@ -35,7 +35,7 @@
 	if 8*pass.TypesSizes.Sizeof(types.Typ[types.Uintptr]) == 64 {
 		return nil, nil // 64-bit platform
 	}
-	if !analysisinternal.Imports(pass.Pkg, "sync/atomic") {
+	if !typesinternal.Imports(pass.Pkg, "sync/atomic") {
 		return nil, nil // doesn't directly import sync/atomic
 	}
 
@@ -54,7 +54,7 @@
 	inspect.Preorder(nodeFilter, func(node ast.Node) {
 		call := node.(*ast.CallExpr)
 		obj := typeutil.Callee(pass.TypesInfo, call)
-		if analysisinternal.IsFunctionNamed(obj, "sync/atomic", funcNames...) {
+		if typesinternal.IsFunctionNamed(obj, "sync/atomic", funcNames...) {
 			// For all the listed functions, the expression to check is always the first function argument.
 			check64BitAlignment(pass, obj.Name(), call.Args[0])
 		}
diff --git a/go/analysis/passes/bools/bools.go b/go/analysis/passes/bools/bools.go
index e1cf9f9..3c2a82d 100644
--- a/go/analysis/passes/bools/bools.go
+++ b/go/analysis/passes/bools/bools.go
@@ -15,7 +15,7 @@
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
 )
 
 const Doc = "check for common mistakes involving boolean operators"
@@ -104,7 +104,7 @@
 func (op boolOp) checkRedundant(pass *analysis.Pass, exprs []ast.Expr) {
 	seen := make(map[string]bool)
 	for _, e := range exprs {
-		efmt := analysisinternal.Format(pass.Fset, e)
+		efmt := astutil.Format(pass.Fset, e)
 		if seen[efmt] {
 			pass.ReportRangef(e, "redundant %s: %s %s %s", op.name, efmt, op.tok, efmt)
 		} else {
@@ -150,8 +150,8 @@
 		}
 
 		// e is of the form 'x != c' or 'x == c'.
-		xfmt := analysisinternal.Format(pass.Fset, x)
-		efmt := analysisinternal.Format(pass.Fset, e)
+		xfmt := astutil.Format(pass.Fset, x)
+		efmt := astutil.Format(pass.Fset, e)
 		if prev, found := seen[xfmt]; found {
 			// checkRedundant handles the case in which efmt == prev.
 			if efmt != prev {
diff --git a/go/analysis/passes/cgocall/cgocall.go b/go/analysis/passes/cgocall/cgocall.go
index d9189b5..bf1202b 100644
--- a/go/analysis/passes/cgocall/cgocall.go
+++ b/go/analysis/passes/cgocall/cgocall.go
@@ -18,7 +18,7 @@
 	"strconv"
 
 	"golang.org/x/tools/go/analysis"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 const debug = false
@@ -41,7 +41,7 @@
 }
 
 func run(pass *analysis.Pass) (any, error) {
-	if !analysisinternal.Imports(pass.Pkg, "runtime/cgo") {
+	if !typesinternal.Imports(pass.Pkg, "runtime/cgo") {
 		return nil, nil // doesn't use cgo
 	}
 
diff --git a/go/analysis/passes/copylock/copylock.go b/go/analysis/passes/copylock/copylock.go
index d35b85f..4190cc5 100644
--- a/go/analysis/passes/copylock/copylock.go
+++ b/go/analysis/passes/copylock/copylock.go
@@ -16,8 +16,9 @@
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/typeparams"
+	"golang.org/x/tools/internal/typesinternal"
 	"golang.org/x/tools/internal/versions"
 )
 
@@ -86,7 +87,7 @@
 	lhs := assign.Lhs
 	for i, x := range assign.Rhs {
 		if path := lockPathRhs(pass, x); path != nil {
-			pass.ReportRangef(x, "assignment copies lock value to %v: %v", analysisinternal.Format(pass.Fset, assign.Lhs[i]), path)
+			pass.ReportRangef(x, "assignment copies lock value to %v: %v", astutil.Format(pass.Fset, assign.Lhs[i]), path)
 			lhs = nil // An lhs has been reported. We prefer the assignment warning and do not report twice.
 		}
 	}
@@ -100,7 +101,7 @@
 				if id, ok := l.(*ast.Ident); ok && id.Name != "_" {
 					if obj := pass.TypesInfo.Defs[id]; obj != nil && obj.Type() != nil {
 						if path := lockPath(pass.Pkg, obj.Type(), nil); path != nil {
-							pass.ReportRangef(l, "for loop iteration copies lock value to %v: %v", analysisinternal.Format(pass.Fset, l), path)
+							pass.ReportRangef(l, "for loop iteration copies lock value to %v: %v", astutil.Format(pass.Fset, l), path)
 						}
 					}
 				}
@@ -132,7 +133,7 @@
 			x = node.Value
 		}
 		if path := lockPathRhs(pass, x); path != nil {
-			pass.ReportRangef(x, "literal copies lock value from %v: %v", analysisinternal.Format(pass.Fset, x), path)
+			pass.ReportRangef(x, "literal copies lock value from %v: %v", astutil.Format(pass.Fset, x), path)
 		}
 	}
 }
@@ -166,7 +167,7 @@
 	}
 	for _, x := range ce.Args {
 		if path := lockPathRhs(pass, x); path != nil {
-			pass.ReportRangef(x, "call of %s copies lock value: %v", analysisinternal.Format(pass.Fset, ce.Fun), path)
+			pass.ReportRangef(x, "call of %s copies lock value: %v", astutil.Format(pass.Fset, ce.Fun), path)
 		}
 	}
 }
@@ -233,7 +234,7 @@
 		return
 	}
 	if path := lockPath(pass.Pkg, typ, nil); path != nil {
-		pass.Reportf(e.Pos(), "range var %s copies lock: %v", analysisinternal.Format(pass.Fset, e), path)
+		pass.Reportf(e.Pos(), "range var %s copies lock: %v", astutil.Format(pass.Fset, e), path)
 	}
 }
 
@@ -353,7 +354,7 @@
 	// In go1.10, sync.noCopy did not implement Locker.
 	// (The Unlock method was added only in CL 121876.)
 	// TODO(adonovan): remove workaround when we drop go1.10.
-	if analysisinternal.IsTypeNamed(typ, "sync", "noCopy") {
+	if typesinternal.IsTypeNamed(typ, "sync", "noCopy") {
 		return []string{typ.String()}
 	}
 
diff --git a/go/analysis/passes/deepequalerrors/deepequalerrors.go b/go/analysis/passes/deepequalerrors/deepequalerrors.go
index d15e3bc..5e3d1a3 100644
--- a/go/analysis/passes/deepequalerrors/deepequalerrors.go
+++ b/go/analysis/passes/deepequalerrors/deepequalerrors.go
@@ -14,7 +14,7 @@
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 const Doc = `check for calls of reflect.DeepEqual on error values
@@ -35,7 +35,7 @@
 }
 
 func run(pass *analysis.Pass) (any, error) {
-	if !analysisinternal.Imports(pass.Pkg, "reflect") {
+	if !typesinternal.Imports(pass.Pkg, "reflect") {
 		return nil, nil // doesn't directly import reflect
 	}
 
@@ -47,7 +47,7 @@
 	inspect.Preorder(nodeFilter, func(n ast.Node) {
 		call := n.(*ast.CallExpr)
 		obj := typeutil.Callee(pass.TypesInfo, call)
-		if analysisinternal.IsFunctionNamed(obj, "reflect", "DeepEqual") && hasError(pass, call.Args[0]) && hasError(pass, call.Args[1]) {
+		if typesinternal.IsFunctionNamed(obj, "reflect", "DeepEqual") && hasError(pass, call.Args[0]) && hasError(pass, call.Args[1]) {
 			pass.ReportRangef(call, "avoid using reflect.DeepEqual with errors")
 		}
 	})
diff --git a/go/analysis/passes/defers/defers.go b/go/analysis/passes/defers/defers.go
index e11957f..bf62d32 100644
--- a/go/analysis/passes/defers/defers.go
+++ b/go/analysis/passes/defers/defers.go
@@ -13,7 +13,7 @@
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 //go:embed doc.go
@@ -29,14 +29,14 @@
 }
 
 func run(pass *analysis.Pass) (any, error) {
-	if !analysisinternal.Imports(pass.Pkg, "time") {
+	if !typesinternal.Imports(pass.Pkg, "time") {
 		return nil, nil
 	}
 
 	checkDeferCall := func(node ast.Node) bool {
 		switch v := node.(type) {
 		case *ast.CallExpr:
-			if analysisinternal.IsFunctionNamed(typeutil.Callee(pass.TypesInfo, v), "time", "Since") {
+			if typesinternal.IsFunctionNamed(typeutil.Callee(pass.TypesInfo, v), "time", "Since") {
 				pass.Reportf(v.Pos(), "call to time.Since is not deferred")
 			}
 		case *ast.FuncLit:
diff --git a/go/analysis/passes/httpmux/httpmux.go b/go/analysis/passes/httpmux/httpmux.go
index 655b78f..a4f00e2 100644
--- a/go/analysis/passes/httpmux/httpmux.go
+++ b/go/analysis/passes/httpmux/httpmux.go
@@ -17,7 +17,6 @@
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/typesinternal"
 )
 
@@ -46,7 +45,7 @@
 			return nil, nil
 		}
 	}
-	if !analysisinternal.Imports(pass.Pkg, "net/http") {
+	if !typesinternal.Imports(pass.Pkg, "net/http") {
 		return nil, nil
 	}
 	// Look for calls to ServeMux.Handle or ServeMux.HandleFunc.
@@ -79,7 +78,7 @@
 	if fn == nil {
 		return false
 	}
-	if analysisinternal.IsFunctionNamed(fn, "net/http", "Handle", "HandleFunc") {
+	if typesinternal.IsFunctionNamed(fn, "net/http", "Handle", "HandleFunc") {
 		return true
 	}
 	if !isMethodNamed(fn, "net/http", "Handle", "HandleFunc") {
@@ -87,7 +86,7 @@
 	}
 	recv := fn.Type().(*types.Signature).Recv() // isMethodNamed() -> non-nil
 	isPtr, named := typesinternal.ReceiverNamed(recv)
-	return isPtr && analysisinternal.IsTypeNamed(named, "net/http", "ServeMux")
+	return isPtr && typesinternal.IsTypeNamed(named, "net/http", "ServeMux")
 }
 
 // isMethodNamed reports when a function f is a method,
diff --git a/go/analysis/passes/httpresponse/httpresponse.go b/go/analysis/passes/httpresponse/httpresponse.go
index e9acd96..37ecb65 100644
--- a/go/analysis/passes/httpresponse/httpresponse.go
+++ b/go/analysis/passes/httpresponse/httpresponse.go
@@ -13,7 +13,6 @@
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/typesinternal"
 )
 
@@ -46,7 +45,7 @@
 
 	// Fast path: if the package doesn't import net/http,
 	// skip the traversal.
-	if !analysisinternal.Imports(pass.Pkg, "net/http") {
+	if !typesinternal.Imports(pass.Pkg, "net/http") {
 		return nil, nil
 	}
 
@@ -118,7 +117,7 @@
 		return false // the function called does not return two values.
 	}
 	isPtr, named := typesinternal.ReceiverNamed(res.At(0))
-	if !isPtr || named == nil || !analysisinternal.IsTypeNamed(named, "net/http", "Response") {
+	if !isPtr || named == nil || !typesinternal.IsTypeNamed(named, "net/http", "Response") {
 		return false // the first return type is not *http.Response.
 	}
 
@@ -133,11 +132,11 @@
 		return ok && id.Name == "http" // function in net/http package.
 	}
 
-	if analysisinternal.IsTypeNamed(typ, "net/http", "Client") {
+	if typesinternal.IsTypeNamed(typ, "net/http", "Client") {
 		return true // method on http.Client.
 	}
 	ptr, ok := types.Unalias(typ).(*types.Pointer)
-	return ok && analysisinternal.IsTypeNamed(ptr.Elem(), "net/http", "Client") // method on *http.Client.
+	return ok && typesinternal.IsTypeNamed(ptr.Elem(), "net/http", "Client") // method on *http.Client.
 }
 
 // restOfBlock, given a traversal stack, finds the innermost containing
diff --git a/go/analysis/passes/inline/gofix.go b/go/analysis/passes/inline/gofix.go
index f6bd570..00d87b0 100644
--- a/go/analysis/passes/inline/gofix.go
+++ b/go/analysis/passes/inline/gofix.go
@@ -21,7 +21,9 @@
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
 	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/diff"
+	"golang.org/x/tools/internal/refactor"
 	"golang.org/x/tools/internal/refactor/inline"
 	"golang.org/x/tools/internal/typesinternal"
 )
@@ -286,7 +288,7 @@
 		} else if _, ok := importPrefixes[pkgPath]; !ok {
 			// Use AddImport to add pkgPath if it's not there already. Associate the prefix it assigns
 			// with the package path for use by the TypeString qualifier below.
-			_, prefix, eds := analysisinternal.AddImport(
+			_, prefix, eds := refactor.AddImport(
 				a.pass.TypesInfo, curFile, pkgName, pkgPath, tn.Name(), id.Pos())
 			importPrefixes[pkgPath] = strings.TrimSuffix(prefix, ".")
 			edits = append(edits, eds...)
@@ -300,7 +302,7 @@
 	//   pkg.Id[T]
 	//   pkg.Id[K, V]
 	var expr ast.Expr = id
-	if analysisinternal.IsChildOf(curId, edge.SelectorExpr_Sel) {
+	if astutil.IsChildOf(curId, edge.SelectorExpr_Sel) {
 		curId = curId.Parent()
 		expr = curId.Node().(ast.Expr)
 	}
@@ -457,12 +459,12 @@
 		edits        []analysis.TextEdit
 	)
 	if incon.RHSPkgPath != a.pass.Pkg.Path() {
-		_, importPrefix, edits = analysisinternal.AddImport(
+		_, importPrefix, edits = refactor.AddImport(
 			a.pass.TypesInfo, curFile, incon.RHSPkgName, incon.RHSPkgPath, incon.RHSName, n.Pos())
 	}
 	// If n is qualified by a package identifier, we'll need the full selector expression.
 	var expr ast.Expr = n
-	if analysisinternal.IsChildOf(cur, edge.SelectorExpr_Sel) {
+	if astutil.IsChildOf(cur, edge.SelectorExpr_Sel) {
 		expr = cur.Parent().Node().(ast.Expr)
 	}
 	a.reportInline("constant", "Constant", expr, edits, importPrefix+incon.RHSName)
@@ -475,7 +477,7 @@
 		End:     ident.End(),
 		NewText: []byte(newText),
 	})
-	name := analysisinternal.Format(a.pass.Fset, ident)
+	name := astutil.Format(a.pass.Fset, ident)
 	a.pass.Report(analysis.Diagnostic{
 		Pos:     ident.Pos(),
 		End:     ident.End(),
diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go
index 2580a0a..8432e96 100644
--- a/go/analysis/passes/loopclosure/loopclosure.go
+++ b/go/analysis/passes/loopclosure/loopclosure.go
@@ -14,7 +14,6 @@
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/typesinternal"
 	"golang.org/x/tools/internal/versions"
 )
@@ -369,5 +368,5 @@
 	// Check that the receiver is a <pkgPath>.<typeName> or
 	// *<pkgPath>.<typeName>.
 	_, named := typesinternal.ReceiverNamed(recv)
-	return analysisinternal.IsTypeNamed(named, pkgPath, typeName)
+	return typesinternal.IsTypeNamed(named, pkgPath, typeName)
 }
diff --git a/go/analysis/passes/lostcancel/lostcancel.go b/go/analysis/passes/lostcancel/lostcancel.go
index c074678..cc0bf0f 100644
--- a/go/analysis/passes/lostcancel/lostcancel.go
+++ b/go/analysis/passes/lostcancel/lostcancel.go
@@ -16,8 +16,8 @@
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/cfg"
-	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/astutil"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 //go:embed doc.go
@@ -50,7 +50,7 @@
 // checkLostCancel analyzes a single named or literal function.
 func run(pass *analysis.Pass) (any, error) {
 	// Fast path: bypass check if file doesn't use context.WithCancel.
-	if !analysisinternal.Imports(pass.Pkg, contextPackage) {
+	if !typesinternal.Imports(pass.Pkg, contextPackage) {
 		return nil, nil
 	}
 
diff --git a/go/analysis/passes/modernize/bloop.go b/go/analysis/passes/modernize/bloop.go
index 9578468..eb1ac17 100644
--- a/go/analysis/passes/modernize/bloop.go
+++ b/go/analysis/passes/modernize/bloop.go
@@ -20,6 +20,7 @@
 	typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
 	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/moreiters"
+	"golang.org/x/tools/internal/typesinternal"
 	"golang.org/x/tools/internal/typesinternal/typeindex"
 )
 
@@ -46,7 +47,7 @@
 func bloop(pass *analysis.Pass) (any, error) {
 	skipGenerated(pass)
 
-	if !analysisinternal.Imports(pass.Pkg, "testing") {
+	if !typesinternal.Imports(pass.Pkg, "testing") {
 		return nil, nil
 	}
 
@@ -75,7 +76,7 @@
 			}
 			if call, ok := stmt.X.(*ast.CallExpr); ok {
 				obj := typeutil.Callee(info, call)
-				if analysisinternal.IsMethodNamed(obj, "testing", "B", "StopTimer", "StartTimer", "ResetTimer") {
+				if typesinternal.IsMethodNamed(obj, "testing", "B", "StopTimer", "StartTimer", "ResetTimer") {
 					// Delete call statement.
 					// TODO(adonovan): delete following newline, or
 					// up to start of next stmt? (May delete a comment.)
@@ -92,7 +93,7 @@
 		return append(edits, analysis.TextEdit{
 			Pos:     start,
 			End:     end,
-			NewText: fmt.Appendf(nil, "%s.Loop()", analysisinternal.Format(pass.Fset, b)),
+			NewText: fmt.Appendf(nil, "%s.Loop()", astutil.Format(pass.Fset, b)),
 		})
 	}
 
@@ -109,7 +110,7 @@
 				if cmp, ok := n.Cond.(*ast.BinaryExpr); ok && cmp.Op == token.LSS {
 					if sel, ok := cmp.Y.(*ast.SelectorExpr); ok &&
 						sel.Sel.Name == "N" &&
-						analysisinternal.IsPointerToNamed(info.TypeOf(sel.X), "testing", "B") && usesBenchmarkNOnce(curLoop, info) {
+						typesinternal.IsPointerToNamed(info.TypeOf(sel.X), "testing", "B") && usesBenchmarkNOnce(curLoop, info) {
 
 						delStart, delEnd := n.Cond.Pos(), n.Cond.End()
 
@@ -144,7 +145,7 @@
 					n.Key == nil &&
 					n.Value == nil &&
 					sel.Sel.Name == "N" &&
-					analysisinternal.IsPointerToNamed(info.TypeOf(sel.X), "testing", "B") && usesBenchmarkNOnce(curLoop, info) {
+					typesinternal.IsPointerToNamed(info.TypeOf(sel.X), "testing", "B") && usesBenchmarkNOnce(curLoop, info) {
 
 					pass.Report(analysis.Diagnostic{
 						// Highlight "range b.N".
@@ -212,7 +213,7 @@
 		case *ast.FuncLit:
 			return false // don't descend into nested function literals
 		case *ast.SelectorExpr:
-			if n.Sel.Name == "N" && analysisinternal.IsPointerToNamed(info.TypeOf(n.X), "testing", "B") {
+			if n.Sel.Name == "N" && typesinternal.IsPointerToNamed(info.TypeOf(n.X), "testing", "B") {
 				bnRefCount++
 			}
 		}
diff --git a/go/analysis/passes/modernize/errorsastype.go b/go/analysis/passes/modernize/errorsastype.go
index e9fab3e..b6387ad 100644
--- a/go/analysis/passes/modernize/errorsastype.go
+++ b/go/analysis/passes/modernize/errorsastype.go
@@ -17,7 +17,9 @@
 	"golang.org/x/tools/internal/analysisinternal"
 	"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/goplsexport"
+	"golang.org/x/tools/internal/refactor"
 	"golang.org/x/tools/internal/typesinternal"
 	"golang.org/x/tools/internal/typesinternal/typeindex"
 )
@@ -94,7 +96,7 @@
 			continue
 		}
 
-		file := analysisinternal.EnclosingFile(curDeclStmt)
+		file := astutil.EnclosingFile(curDeclStmt)
 		if !fileUses(info, file, "go1.26") {
 			continue // errors.AsType is too new
 		}
@@ -134,7 +136,7 @@
 			for curUse := range index.Uses(okVar) {
 				if curIf.Contains(curUse) {
 					scope := info.Scopes[curIf.Node().(*ast.IfStmt)]
-					okName = analysisinternal.FreshName(scope, v.Pos(), "ok")
+					okName = refactor.FreshName(scope, v.Pos(), "ok")
 					break
 				}
 			}
@@ -148,7 +150,7 @@
 				Message: fmt.Sprintf("Replace errors.As with AsType[%s]", errtype),
 				TextEdits: append(
 					// delete "var myerr *MyErr"
-					analysisinternal.DeleteStmt(pass.Fset.File(call.Fun.Pos()), curDeclStmt),
+					refactor.DeleteStmt(pass.Fset.File(call.Fun.Pos()), curDeclStmt),
 					// if              errors.As            (err, &myerr)     { ... }
 					//    -------------       --------------    -------- ----
 					// if myerr, ok := errors.AsType[*MyErr](err        ); ok { ... }
@@ -187,7 +189,7 @@
 // declaration of the typed error var. The var must not be
 // used outside the if statement.
 func canUseErrorsAsType(info *types.Info, index *typeindex.Index, curCall inspector.Cursor) (_ *types.Var, _ inspector.Cursor) {
-	if !analysisinternal.IsChildOf(curCall, edge.IfStmt_Cond) {
+	if !astutil.IsChildOf(curCall, edge.IfStmt_Cond) {
 		return // not beneath if statement
 	}
 	var (
@@ -219,7 +221,7 @@
 			return // v used before/after if statement
 		}
 	}
-	if !analysisinternal.IsChildOf(curDef, edge.ValueSpec_Names) {
+	if !astutil.IsChildOf(curDef, edge.ValueSpec_Names) {
 		return // v not declared by "var v T"
 	}
 	var (
diff --git a/go/analysis/passes/modernize/fmtappendf.go b/go/analysis/passes/modernize/fmtappendf.go
index 4e6a254..f2e5360 100644
--- a/go/analysis/passes/modernize/fmtappendf.go
+++ b/go/analysis/passes/modernize/fmtappendf.go
@@ -16,6 +16,7 @@
 	"golang.org/x/tools/internal/analysisinternal"
 	"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/typesinternal/typeindex"
 )
 
@@ -49,7 +50,7 @@
 				conv := curCall.Parent().Node().(*ast.CallExpr)
 				tv := pass.TypesInfo.Types[conv.Fun]
 				if tv.IsType() && types.Identical(tv.Type, byteSliceType) &&
-					fileUses(pass.TypesInfo, analysisinternal.EnclosingFile(curCall), "go1.19") {
+					fileUses(pass.TypesInfo, astutil.EnclosingFile(curCall), "go1.19") {
 					// Have: []byte(fmt.SprintX(...))
 
 					// Find "Sprint" identifier.
diff --git a/go/analysis/passes/modernize/forvar.go b/go/analysis/passes/modernize/forvar.go
index ad5879a..76e3a8a 100644
--- a/go/analysis/passes/modernize/forvar.go
+++ b/go/analysis/passes/modernize/forvar.go
@@ -14,6 +14,7 @@
 	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/analysisinternal/generated"
 	"golang.org/x/tools/internal/astutil"
+	"golang.org/x/tools/internal/refactor"
 )
 
 var ForVarAnalyzer = &analysis.Analyzer{
@@ -71,7 +72,7 @@
 					isLoopVarRedecl(assign) {
 
 					curStmt, _ := curLoop.FindNode(stmt)
-					edits := analysisinternal.DeleteStmt(pass.Fset.File(stmt.Pos()), curStmt)
+					edits := refactor.DeleteStmt(pass.Fset.File(stmt.Pos()), curStmt)
 					if len(edits) > 0 {
 						pass.Report(analysis.Diagnostic{
 							Pos:     stmt.Pos(),
diff --git a/go/analysis/passes/modernize/maps.go b/go/analysis/passes/modernize/maps.go
index d6a77c4..d8d9b6e 100644
--- a/go/analysis/passes/modernize/maps.go
+++ b/go/analysis/passes/modernize/maps.go
@@ -18,7 +18,9 @@
 	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/analysisinternal/generated"
 	"golang.org/x/tools/internal/astutil"
+	"golang.org/x/tools/internal/refactor"
 	"golang.org/x/tools/internal/typeparams"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 var MapsLoopAnalyzer = &analysis.Analyzer{
@@ -164,7 +166,7 @@
 
 		// Report diagnostic, and suggest fix.
 		rng := curRange.Node()
-		_, prefix, importEdits := analysisinternal.AddImport(info, file, "maps", "maps", funcName, rng.Pos())
+		_, prefix, importEdits := refactor.AddImport(info, file, "maps", "maps", funcName, rng.Pos())
 		var (
 			newText    []byte
 			start, end token.Pos
@@ -183,10 +185,10 @@
 			start, end = curPrev.Node().Pos(), rng.End()
 			newText = fmt.Appendf(nil, "%s%s = %s%s(%s)",
 				allComments(file, start, end),
-				analysisinternal.Format(pass.Fset, m),
+				astutil.Format(pass.Fset, m),
 				prefix,
 				funcName,
-				analysisinternal.Format(pass.Fset, x))
+				astutil.Format(pass.Fset, x))
 		} else {
 			// Replace loop with call statement.
 			//
@@ -201,8 +203,8 @@
 				allComments(file, start, end),
 				prefix,
 				funcName,
-				analysisinternal.Format(pass.Fset, m),
-				analysisinternal.Format(pass.Fset, x))
+				astutil.Format(pass.Fset, m),
+				astutil.Format(pass.Fset, x))
 		}
 		pass.Report(analysis.Diagnostic{
 			Pos:     assign.Lhs[0].Pos(),
@@ -256,7 +258,7 @@
 func assignableToIterSeq2(t types.Type) (k, v types.Type, ok bool) {
 	// The only named type assignable to iter.Seq2 is iter.Seq2.
 	if is[*types.Named](t) {
-		if !analysisinternal.IsTypeNamed(t, "iter", "Seq2") {
+		if !typesinternal.IsTypeNamed(t, "iter", "Seq2") {
 			return
 		}
 		t = t.Underlying()
diff --git a/go/analysis/passes/modernize/minmax.go b/go/analysis/passes/modernize/minmax.go
index 8ad2000..7ebf837 100644
--- a/go/analysis/passes/modernize/minmax.go
+++ b/go/analysis/passes/modernize/minmax.go
@@ -79,7 +79,7 @@
 				return cond(arg == b, ", ", "") + // second argument needs a comma
 					cond(comments != "", "\n", "") + // comments need their own line
 					comments +
-					analysisinternal.Format(pass.Fset, arg)
+					astutil.Format(pass.Fset, arg)
 			}
 		)
 
@@ -124,7 +124,7 @@
 							Pos: ifStmt.Pos(),
 							End: ifStmt.End(),
 							NewText: fmt.Appendf(nil, "%s = %s(%s%s)",
-								analysisinternal.Format(pass.Fset, lhs),
+								astutil.Format(pass.Fset, lhs),
 								sym,
 								callArg(a, ifStmt.Pos(), ifStmt.Else.Pos()),
 								callArg(b, ifStmt.Else.Pos(), ifStmt.End()),
@@ -186,7 +186,7 @@
 							End: ifStmt.End(),
 							// Replace "x := a; if ... {}" with "x = min(...)", preserving comments.
 							NewText: fmt.Appendf(nil, "%s %s %s(%s%s)",
-								analysisinternal.Format(pass.Fset, lhs),
+								astutil.Format(pass.Fset, lhs),
 								fassign.Tok.String(),
 								sym,
 								callArg(a, fassign.Pos(), ifStmt.Pos()),
@@ -213,7 +213,7 @@
 			// (This case would require introducing another block
 			//    if cond { ... } else { if a < b { lhs = rhs } }
 			// and checking that there is no following "else".)
-			if analysisinternal.IsChildOf(curIfStmt, edge.IfStmt_Else) {
+			if astutil.IsChildOf(curIfStmt, edge.IfStmt_Else) {
 				continue
 			}
 
@@ -235,7 +235,7 @@
 // allComments collects all the comments from start to end.
 func allComments(file *ast.File, start, end token.Pos) string {
 	var buf strings.Builder
-	for co := range analysisinternal.Comments(file, start, end) {
+	for co := range astutil.Comments(file, start, end) {
 		_, _ = fmt.Fprintf(&buf, "%s\n", co.Text)
 	}
 	return buf.String()
@@ -301,7 +301,7 @@
 				if canUseBuiltinMinMax(fn, decl.Body) {
 					// Expand to include leading doc comment
 					pos := decl.Pos()
-					if docs := docComment(decl); docs != nil {
+					if docs := astutil.DocComment(decl); docs != nil {
 						pos = docs.Pos()
 					}
 
@@ -438,18 +438,3 @@
 		return f
 	}
 }
-
-// docComment returns the doc comment for a node, if any.
-func docComment(n ast.Node) *ast.CommentGroup {
-	switch n := n.(type) {
-	case *ast.FuncDecl:
-		return n.Doc
-	case *ast.GenDecl:
-		return n.Doc
-	case *ast.ValueSpec:
-		return n.Doc
-	case *ast.TypeSpec:
-		return n.Doc
-	}
-	return nil // includes File, ImportSpec, Field
-}
diff --git a/go/analysis/passes/modernize/modernize.go b/go/analysis/passes/modernize/modernize.go
index b00f09b..59adee1 100644
--- a/go/analysis/passes/modernize/modernize.go
+++ b/go/analysis/passes/modernize/modernize.go
@@ -20,8 +20,10 @@
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/analysisinternal/generated"
+	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/moreiters"
 	"golang.org/x/tools/internal/stdlib"
+	"golang.org/x/tools/internal/typesinternal"
 	"golang.org/x/tools/internal/versions"
 )
 
@@ -129,7 +131,7 @@
 // unparenEnclosing removes enclosing parens from cur in
 // preparation for a call to [Cursor.ParentEdge].
 func unparenEnclosing(cur inspector.Cursor) inspector.Cursor {
-	for analysisinternal.IsChildOf(cur, edge.ParenExpr_X) {
+	for astutil.IsChildOf(cur, edge.ParenExpr_X) {
 		cur = cur.Parent()
 	}
 	return cur
@@ -153,7 +155,7 @@
 
 // lookup returns the symbol denoted by name at the position of the cursor.
 func lookup(info *types.Info, cur inspector.Cursor, name string) types.Object {
-	scope := analysisinternal.EnclosingScope(info, cur)
+	scope := typesinternal.EnclosingScope(info, cur)
 	_, obj := scope.LookupParent(name, cur.Node().Pos())
 	return obj
 }
diff --git a/go/analysis/passes/modernize/newexpr.go b/go/analysis/passes/modernize/newexpr.go
index 2522cf2..b889324 100644
--- a/go/analysis/passes/modernize/newexpr.go
+++ b/go/analysis/passes/modernize/newexpr.go
@@ -18,6 +18,7 @@
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
 	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
 )
 
 var NewExprAnalyzer = &analysis.Analyzer{
@@ -58,7 +59,7 @@
 								pass.ExportObjectFact(fn, &newLike{})
 
 								// Check file version.
-								file := analysisinternal.EnclosingFile(curFuncDecl)
+								file := astutil.EnclosingFile(curFuncDecl)
 								if !fileUses(info, file, "go1.26") {
 									continue // new(expr) not available in this file
 								}
@@ -138,7 +139,7 @@
 			pass.ImportObjectFact(fn, &fact) {
 
 			// Check file version.
-			file := analysisinternal.EnclosingFile(curCall)
+			file := astutil.EnclosingFile(curCall)
 			if !fileUses(info, file, "go1.26") {
 				continue // new(expr) not available in this file
 			}
diff --git a/go/analysis/passes/modernize/rangeint.go b/go/analysis/passes/modernize/rangeint.go
index 62c9740..adc840f 100644
--- a/go/analysis/passes/modernize/rangeint.go
+++ b/go/analysis/passes/modernize/rangeint.go
@@ -229,7 +229,7 @@
 							Message: "for loop can be modernized using range over int",
 							SuggestedFixes: []analysis.SuggestedFix{{
 								Message: fmt.Sprintf("Replace for loop with range %s",
-									analysisinternal.Format(pass.Fset, limit)),
+									astutil.Format(pass.Fset, limit)),
 								TextEdits: append(edits, []analysis.TextEdit{
 									// for i := 0; i < limit; i++ {}
 									//     -----              ---
diff --git a/go/analysis/passes/modernize/reflect.go b/go/analysis/passes/modernize/reflect.go
index cadcb1d..1a4e3eb 100644
--- a/go/analysis/passes/modernize/reflect.go
+++ b/go/analysis/passes/modernize/reflect.go
@@ -17,6 +17,7 @@
 	"golang.org/x/tools/internal/analysisinternal"
 	"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/typesinternal"
 	"golang.org/x/tools/internal/typesinternal/typeindex"
 	"golang.org/x/tools/internal/versions"
@@ -58,12 +59,12 @@
 
 		// Special case for TypeOf((*T)(nil)).Elem(),
 		// needed when T is an interface type.
-		if analysisinternal.IsChildOf(curCall, edge.SelectorExpr_X) {
+		if astutil.IsChildOf(curCall, edge.SelectorExpr_X) {
 			curSel := unparenEnclosing(curCall).Parent()
-			if analysisinternal.IsChildOf(curSel, edge.CallExpr_Fun) {
+			if astutil.IsChildOf(curSel, edge.CallExpr_Fun) {
 				call2 := unparenEnclosing(curSel).Parent().Node().(*ast.CallExpr)
 				obj := typeutil.Callee(info, call2)
-				if analysisinternal.IsMethodNamed(obj, "reflect", "Type", "Elem") {
+				if typesinternal.IsMethodNamed(obj, "reflect", "Type", "Elem") {
 					if ptr, ok := t.(*types.Pointer); ok {
 						// Have: reflect.TypeOf(...*T value...).Elem()
 						// => reflect.TypeFor[T]()
@@ -87,7 +88,7 @@
 			continue
 		}
 
-		file := analysisinternal.EnclosingFile(curCall)
+		file := astutil.EnclosingFile(curCall)
 		if versions.Before(info.FileVersions[file], "go1.22") {
 			continue // TypeFor requires go1.22
 		}
diff --git a/go/analysis/passes/modernize/slices.go b/go/analysis/passes/modernize/slices.go
index 42b01d2..52e58f2 100644
--- a/go/analysis/passes/modernize/slices.go
+++ b/go/analysis/passes/modernize/slices.go
@@ -18,6 +18,8 @@
 	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/analysisinternal/generated"
 	"golang.org/x/tools/internal/astutil"
+	"golang.org/x/tools/internal/refactor"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 // Warning: this analyzer is not safe to enable by default.
@@ -124,7 +126,7 @@
 			// append(zerocap, os.Environ()...) -> os.Environ()
 			if scall, ok := s.(*ast.CallExpr); ok {
 				obj := typeutil.Callee(info, scall)
-				if analysisinternal.IsFunctionNamed(obj, "os", "Environ") {
+				if typesinternal.IsFunctionNamed(obj, "os", "Environ") {
 					pass.Report(analysis.Diagnostic{
 						Pos:     call.Pos(),
 						End:     call.End(),
@@ -134,7 +136,7 @@
 							TextEdits: []analysis.TextEdit{{
 								Pos:     call.Pos(),
 								End:     call.End(),
-								NewText: []byte(analysisinternal.Format(pass.Fset, s)),
+								NewText: []byte(astutil.Format(pass.Fset, s)),
 							}},
 						}},
 					})
@@ -162,7 +164,7 @@
 			//
 			// This is unsound if s is empty and its nilness
 			// differs from zerocap (#73557).
-			_, prefix, importEdits := analysisinternal.AddImport(info, file, clonepkg, clonepkg, "Clone", call.Pos())
+			_, prefix, importEdits := refactor.AddImport(info, file, clonepkg, clonepkg, "Clone", call.Pos())
 			message := fmt.Sprintf("Replace append with %s.Clone", clonepkg)
 			pass.Report(analysis.Diagnostic{
 				Pos:     call.Pos(),
@@ -173,7 +175,7 @@
 					TextEdits: append(importEdits, []analysis.TextEdit{{
 						Pos:     call.Pos(),
 						End:     call.End(),
-						NewText: fmt.Appendf(nil, "%sClone(%s)", prefix, analysisinternal.Format(pass.Fset, s)),
+						NewText: fmt.Appendf(nil, "%sClone(%s)", prefix, astutil.Format(pass.Fset, s)),
 					}}...),
 				}},
 			})
@@ -183,7 +185,7 @@
 		// append(append(append(base, a...), b..., c...) -> slices.Concat(base, a, b, c)
 		//
 		// This is unsound if all slices are empty and base is non-nil (#73557).
-		_, prefix, importEdits := analysisinternal.AddImport(info, file, "slices", "slices", "Concat", call.Pos())
+		_, prefix, importEdits := refactor.AddImport(info, file, "slices", "slices", "Concat", call.Pos())
 		pass.Report(analysis.Diagnostic{
 			Pos:     call.Pos(),
 			End:     call.End(),
@@ -284,7 +286,7 @@
 
 		// slices.Clip(x)?
 		obj := typeutil.Callee(info, e)
-		if analysisinternal.IsFunctionNamed(obj, "slices", "Clip") {
+		if typesinternal.IsFunctionNamed(obj, "slices", "Clip") {
 			return e.Args[0], false // slices.Clip(x) -> x
 		}
 
diff --git a/go/analysis/passes/modernize/slicescontains.go b/go/analysis/passes/modernize/slicescontains.go
index 5480cd9..2d521dd 100644
--- a/go/analysis/passes/modernize/slicescontains.go
+++ b/go/analysis/passes/modernize/slicescontains.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/typeparams"
 	"golang.org/x/tools/internal/typesinternal/typeindex"
 )
@@ -201,12 +202,12 @@
 		}
 
 		// Prepare slices.Contains{,Func} call.
-		_, prefix, importEdits := analysisinternal.AddImport(info, file, "slices", "slices", funcName, rng.Pos())
+		_, prefix, importEdits := refactor.AddImport(info, file, "slices", "slices", funcName, rng.Pos())
 		contains := fmt.Sprintf("%s%s(%s, %s)",
 			prefix,
 			funcName,
-			analysisinternal.Format(pass.Fset, rng.X),
-			analysisinternal.Format(pass.Fset, arg2))
+			astutil.Format(pass.Fset, rng.X),
+			astutil.Format(pass.Fset, arg2))
 
 		report := func(edits []analysis.TextEdit) {
 			pass.Report(analysis.Diagnostic{
diff --git a/go/analysis/passes/modernize/slicesdelete.go b/go/analysis/passes/modernize/slicesdelete.go
index aa7d817..305000f 100644
--- a/go/analysis/passes/modernize/slicesdelete.go
+++ b/go/analysis/passes/modernize/slicesdelete.go
@@ -16,6 +16,7 @@
 	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/analysisinternal/generated"
 	"golang.org/x/tools/internal/astutil"
+	"golang.org/x/tools/internal/refactor"
 	"golang.org/x/tools/internal/typesinternal"
 )
 
@@ -61,7 +62,7 @@
 			return false
 		}
 
-		_, prefix, edits := analysisinternal.AddImport(info, file, "slices", "slices", "Delete", call.Pos())
+		_, prefix, edits := refactor.AddImport(info, file, "slices", "slices", "Delete", call.Pos())
 		// append's indices may be any integer type; slices.Delete requires int.
 		// Insert int conversions as needed (and if possible).
 		if isIntShadowed() && (!isIntExpr(slice1.High) || !isIntExpr(slice2.Low)) {
diff --git a/go/analysis/passes/modernize/sortslice.go b/go/analysis/passes/modernize/sortslice.go
index c216aab..b2d04e1 100644
--- a/go/analysis/passes/modernize/sortslice.go
+++ b/go/analysis/passes/modernize/sortslice.go
@@ -15,6 +15,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/typeindex"
 )
 
@@ -84,12 +85,12 @@
 							is[*ast.Ident](index.Index) &&
 							info.Uses[index.Index.(*ast.Ident)] == v
 					}
-					file := analysisinternal.EnclosingFile(curCall)
+					file := astutil.EnclosingFile(curCall)
 					if isIndex(compare.X, i) && isIndex(compare.Y, j) &&
 						fileUses(info, file, "go1.21") {
 						// Have: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
 
-						_, prefix, importEdits := analysisinternal.AddImport(
+						_, prefix, importEdits := refactor.AddImport(
 							info, file, "slices", "slices", "Sort", call.Pos())
 
 						pass.Report(analysis.Diagnostic{
diff --git a/go/analysis/passes/modernize/stditerators.go b/go/analysis/passes/modernize/stditerators.go
index e297e67..2081752 100644
--- a/go/analysis/passes/modernize/stditerators.go
+++ b/go/analysis/passes/modernize/stditerators.go
@@ -19,6 +19,7 @@
 	typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
 	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/goplsexport"
+	"golang.org/x/tools/internal/refactor"
 	"golang.org/x/tools/internal/stdlib"
 	"golang.org/x/tools/internal/typesinternal/typeindex"
 )
@@ -145,7 +146,7 @@
 			}
 
 			loop := curBody.Parent().Node()
-			return analysisinternal.FreshName(info.Scopes[loop], loop.Pos(), row.elemname), nil
+			return refactor.FreshName(info.Scopes[loop], loop.Pos(), row.elemname), nil
 		}
 
 		// Process each call of x.Len().
@@ -175,7 +176,7 @@
 					cmp    = curCmp.Node().(*ast.BinaryExpr)
 				)
 				if cmp.Op != token.LSS ||
-					!analysisinternal.IsChildOf(curCmp, edge.ForStmt_Cond) {
+					!astutil.IsChildOf(curCmp, edge.ForStmt_Cond) {
 					continue
 				}
 				if id, ok := cmp.X.(*ast.Ident); ok {
@@ -312,7 +313,7 @@
 			// may be somewhat expensive.)
 			if v, ok := methodGoVersion(row.pkgpath, row.typename, row.itermethod); !ok {
 				panic("no version found")
-			} else if file := analysisinternal.EnclosingFile(curLenCall); !fileUses(info, file, v.String()) {
+			} else if file := astutil.EnclosingFile(curLenCall); !fileUses(info, file, v.String()) {
 				continue nextCall
 			}
 
diff --git a/go/analysis/passes/modernize/stringsbuilder.go b/go/analysis/passes/modernize/stringsbuilder.go
index 7cbda73..ef128d5 100644
--- a/go/analysis/passes/modernize/stringsbuilder.go
+++ b/go/analysis/passes/modernize/stringsbuilder.go
@@ -18,6 +18,8 @@
 	"golang.org/x/tools/internal/analysisinternal"
 	"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"
 )
@@ -100,8 +102,8 @@
 			}
 
 			// Add strings import.
-			_, prefix, importEdits := analysisinternal.AddImport(
-				pass.TypesInfo, analysisinternal.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
+			_, prefix, importEdits := refactor.AddImport(
+				pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
 			edits = append(edits, importEdits...)
 
 			if isEmptyString(pass.TypesInfo, assign.Rhs[0]) {
@@ -139,8 +141,8 @@
 			// => var s strings.Builder; s.WriteString(expr)
 
 			// Add strings import.
-			_, prefix, importEdits := analysisinternal.AddImport(
-				pass.TypesInfo, analysisinternal.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
+			_, prefix, importEdits := refactor.AddImport(
+				pass.TypesInfo, astutil.EnclosingFile(def), "strings", "strings", "Builder", v.Pos())
 			edits = append(edits, importEdits...)
 
 			spec := def.Parent().Node().(*ast.ValueSpec)
diff --git a/go/analysis/passes/modernize/stringscutprefix.go b/go/analysis/passes/modernize/stringscutprefix.go
index 4422bdd..b2a5ae7 100644
--- a/go/analysis/passes/modernize/stringscutprefix.go
+++ b/go/analysis/passes/modernize/stringscutprefix.go
@@ -18,6 +18,8 @@
 	"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"
 )
 
@@ -78,8 +80,8 @@
 			if call, ok := ifStmt.Cond.(*ast.CallExpr); ok && ifStmt.Init == nil && len(ifStmt.Body.List) > 0 {
 
 				obj := typeutil.Callee(info, call)
-				if !analysisinternal.IsFunctionNamed(obj, "strings", "HasPrefix", "HasSuffix") &&
-					!analysisinternal.IsFunctionNamed(obj, "bytes", "HasPrefix", "HasSuffix") {
+				if !typesinternal.IsFunctionNamed(obj, "strings", "HasPrefix", "HasSuffix") &&
+					!typesinternal.IsFunctionNamed(obj, "bytes", "HasPrefix", "HasSuffix") {
 					continue
 				}
 				isPrefix := strings.HasSuffix(obj.Name(), "Prefix")
@@ -125,8 +127,8 @@
 					// check whether the obj1 uses the exact the same argument with strings.HasPrefix
 					// shadow variables won't be valid because we only access the first statement (ditto Suffix).
 					if astutil.EqualSyntax(s0, s) && astutil.EqualSyntax(pre0, pre) {
-						after := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), varName)
-						_, prefix, importEdits := analysisinternal.AddImport(
+						after := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), varName)
+						_, prefix, importEdits := refactor.AddImport(
 							info,
 							curFile.Node().(*ast.File),
 							obj1.Pkg().Name(),
@@ -134,7 +136,7 @@
 							cutFuncName,
 							call.Pos(),
 						)
-						okVarName := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
+						okVarName := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
 						pass.Report(analysis.Diagnostic{
 							// highlight at HasPrefix call (ditto Suffix).
 							Pos:     call.Pos(),
@@ -204,14 +206,14 @@
 
 					if astutil.EqualSyntax(lhs, bin.X) && astutil.EqualSyntax(call.Args[0], bin.Y) ||
 						(astutil.EqualSyntax(lhs, bin.Y) && astutil.EqualSyntax(call.Args[0], bin.X)) {
-						okVarName := analysisinternal.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
+						okVarName := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
 						// Have one of:
 						//   if rest := TrimPrefix(s, prefix); rest != s { (ditto Suffix)
 						//   if rest := TrimPrefix(s, prefix); s != rest { (ditto Suffix)
 
 						// We use AddImport not to add an import (since it exists already)
 						// but to compute the correct prefix in the dot-import case.
-						_, prefix, importEdits := analysisinternal.AddImport(
+						_, prefix, importEdits := refactor.AddImport(
 							info,
 							curFile.Node().(*ast.File),
 							obj.Pkg().Name(),
diff --git a/go/analysis/passes/modernize/testingcontext.go b/go/analysis/passes/modernize/testingcontext.go
index d14bf97..558cf14 100644
--- a/go/analysis/passes/modernize/testingcontext.go
+++ b/go/analysis/passes/modernize/testingcontext.go
@@ -20,6 +20,8 @@
 	"golang.org/x/tools/internal/analysisinternal"
 	"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/typesinternal"
 	"golang.org/x/tools/internal/typesinternal/typeindex"
 )
 
@@ -72,7 +74,7 @@
 		if !ok {
 			continue
 		}
-		if !analysisinternal.IsFunctionNamed(typeutil.Callee(info, arg), "context", "Background", "TODO") {
+		if !typesinternal.IsFunctionNamed(typeutil.Callee(info, arg), "context", "Background", "TODO") {
 			continue
 		}
 		// Have: context.WithCancel(context.{Background,TODO}())
@@ -122,8 +124,8 @@
 				if ek, idx := curFunc.ParentEdge(); ek == edge.CallExpr_Args && idx == 1 {
 					// Have: call(..., func(...) { ...context.WithCancel(...)... })
 					obj := typeutil.Callee(info, curFunc.Parent().Node().(*ast.CallExpr))
-					if (analysisinternal.IsMethodNamed(obj, "testing", "T", "Run") ||
-						analysisinternal.IsMethodNamed(obj, "testing", "B", "Run")) &&
+					if (typesinternal.IsMethodNamed(obj, "testing", "T", "Run") ||
+						typesinternal.IsMethodNamed(obj, "testing", "B", "Run")) &&
 						len(n.Type.Params.List[0].Names) == 1 {
 
 						// Have tb.Run(..., func(..., tb *testing.[TB]) { ...context.WithCancel(...)... }
@@ -135,7 +137,7 @@
 				testObj = isTestFn(info, n)
 			}
 		}
-		if testObj != nil && fileUses(info, analysisinternal.EnclosingFile(cur), "go1.24") {
+		if testObj != nil && fileUses(info, astutil.EnclosingFile(cur), "go1.24") {
 			// Have a test function. Check that we can resolve the relevant
 			// testing.{T,B,F} at the current position.
 			if _, obj := lhs[0].Parent().LookupParent(testObj.Name(), lhs[0].Pos()); obj == testObj {
@@ -215,7 +217,7 @@
 		name = "F"
 	}
 
-	if !analysisinternal.IsPointerToNamed(obj.Type(), "testing", name) {
+	if !typesinternal.IsPointerToNamed(obj.Type(), "testing", name) {
 		return nil
 	}
 	return obj
diff --git a/go/analysis/passes/modernize/waitgroup.go b/go/analysis/passes/modernize/waitgroup.go
index 7ff7023..b890f33 100644
--- a/go/analysis/passes/modernize/waitgroup.go
+++ b/go/analysis/passes/modernize/waitgroup.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/typeindex"
 )
 
@@ -126,7 +127,7 @@
 			panic("can't find Cursor for 'done' statement")
 		}
 
-		file := analysisinternal.EnclosingFile(curAddCall)
+		file := astutil.EnclosingFile(curAddCall)
 		if !fileUses(info, file, "go1.25") {
 			continue
 		}
@@ -146,9 +147,9 @@
 				Message: "Simplify by using WaitGroup.Go",
 				TextEdits: slices.Concat(
 					// delete "wg.Add(1)"
-					analysisinternal.DeleteStmt(tokFile, curAddStmt),
+					refactor.DeleteStmt(tokFile, curAddStmt),
 					// delete "wg.Done()" or "defer wg.Done()"
-					analysisinternal.DeleteStmt(tokFile, curDoneStmt),
+					refactor.DeleteStmt(tokFile, curDoneStmt),
 					[]analysis.TextEdit{
 						// go    func()
 						// ------
diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go
index bf814fd..0850a58 100644
--- a/go/analysis/passes/printf/printf.go
+++ b/go/analysis/passes/printf/printf.go
@@ -26,6 +26,7 @@
 	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/fmtstr"
 	"golang.org/x/tools/internal/typeparams"
+	"golang.org/x/tools/internal/typesinternal"
 	"golang.org/x/tools/internal/versions"
 )
 
@@ -585,7 +586,7 @@
 	sig := fn.Type().(*types.Signature)
 	return sig.Params().Len() == 2 &&
 		sig.Results().Len() == 0 &&
-		analysisinternal.IsTypeNamed(sig.Params().At(0).Type(), "fmt", "State") &&
+		typesinternal.IsTypeNamed(sig.Params().At(0).Type(), "fmt", "State") &&
 		types.Identical(sig.Params().At(1).Type(), types.Typ[types.Rune])
 }
 
@@ -824,7 +825,7 @@
 			if reason != "" {
 				details = " (" + reason + ")"
 			}
-			pass.ReportRangef(rng, "%s format %s uses non-int %s%s as argument of *", name, operation.Text, analysisinternal.Format(pass.Fset, arg), details)
+			pass.ReportRangef(rng, "%s format %s uses non-int %s%s as argument of *", name, operation.Text, astutil.Format(pass.Fset, arg), details)
 			return false
 		}
 	}
@@ -851,7 +852,7 @@
 	}
 	arg := call.Args[verbArgIndex]
 	if isFunctionValue(pass, arg) && verb != 'p' && verb != 'T' {
-		pass.ReportRangef(rng, "%s format %s arg %s is a func value, not called", name, operation.Text, analysisinternal.Format(pass.Fset, arg))
+		pass.ReportRangef(rng, "%s format %s arg %s is a func value, not called", name, operation.Text, astutil.Format(pass.Fset, arg))
 		return false
 	}
 	if reason, ok := matchArgType(pass, v.typ, arg); !ok {
@@ -863,14 +864,14 @@
 		if reason != "" {
 			details = " (" + reason + ")"
 		}
-		pass.ReportRangef(rng, "%s format %s has arg %s of wrong type %s%s", name, operation.Text, analysisinternal.Format(pass.Fset, arg), typeString, details)
+		pass.ReportRangef(rng, "%s format %s has arg %s of wrong type %s%s", name, operation.Text, astutil.Format(pass.Fset, arg), typeString, details)
 		return false
 	}
 	// Detect recursive formatting via value's String/Error methods.
 	// The '#' flag suppresses the methods, except with %x, %X, and %q.
 	if v.typ&argString != 0 && v.verb != 'T' && (!strings.Contains(operation.Flags, "#") || strings.ContainsRune("qxX", v.verb)) {
 		if methodName, ok := recursiveStringer(pass, arg); ok {
-			pass.ReportRangef(rng, "%s format %s with arg %s causes recursive %s method call", name, operation.Text, analysisinternal.Format(pass.Fset, arg), methodName)
+			pass.ReportRangef(rng, "%s format %s with arg %s causes recursive %s method call", name, operation.Text, astutil.Format(pass.Fset, arg), methodName)
 			return false
 		}
 	}
@@ -1022,7 +1023,7 @@
 		if sel, ok := call.Args[0].(*ast.SelectorExpr); ok {
 			if x, ok := sel.X.(*ast.Ident); ok {
 				if x.Name == "os" && strings.HasPrefix(sel.Sel.Name, "Std") {
-					pass.ReportRangef(call, "%s does not take io.Writer but has first arg %s", name, analysisinternal.Format(pass.Fset, call.Args[0]))
+					pass.ReportRangef(call, "%s does not take io.Writer but has first arg %s", name, astutil.Format(pass.Fset, call.Args[0]))
 				}
 			}
 		}
@@ -1056,10 +1057,10 @@
 	}
 	for _, arg := range args {
 		if isFunctionValue(pass, arg) {
-			pass.ReportRangef(call, "%s arg %s is a func value, not called", name, analysisinternal.Format(pass.Fset, arg))
+			pass.ReportRangef(call, "%s arg %s is a func value, not called", name, astutil.Format(pass.Fset, arg))
 		}
 		if methodName, ok := recursiveStringer(pass, arg); ok {
-			pass.ReportRangef(call, "%s arg %s causes recursive call to %s method", name, analysisinternal.Format(pass.Fset, arg), methodName)
+			pass.ReportRangef(call, "%s arg %s causes recursive call to %s method", name, astutil.Format(pass.Fset, arg), methodName)
 		}
 	}
 }
diff --git a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go
index d0632db..5626ac1 100644
--- a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go
+++ b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go
@@ -14,7 +14,7 @@
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 //go:embed doc.go
@@ -50,7 +50,7 @@
 			}
 		case *ast.CallExpr:
 			obj := typeutil.Callee(pass.TypesInfo, n)
-			if analysisinternal.IsFunctionNamed(obj, "reflect", "DeepEqual") && (isReflectValue(pass, n.Args[0]) || isReflectValue(pass, n.Args[1])) {
+			if typesinternal.IsFunctionNamed(obj, "reflect", "DeepEqual") && (isReflectValue(pass, n.Args[0]) || isReflectValue(pass, n.Args[1])) {
 				pass.ReportRangef(n, "avoid using reflect.DeepEqual with reflect.Value")
 			}
 		}
@@ -65,7 +65,7 @@
 		return false
 	}
 	// See if the type is reflect.Value
-	if !analysisinternal.IsTypeNamed(tv.Type, "reflect", "Value") {
+	if !typesinternal.IsTypeNamed(tv.Type, "reflect", "Value") {
 		return false
 	}
 	if _, ok := e.(*ast.CompositeLit); ok {
diff --git a/go/analysis/passes/shift/shift.go b/go/analysis/passes/shift/shift.go
index 57987b3..3669273 100644
--- a/go/analysis/passes/shift/shift.go
+++ b/go/analysis/passes/shift/shift.go
@@ -20,7 +20,7 @@
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/typeparams"
 )
 
@@ -123,7 +123,7 @@
 		}
 	}
 	if amt >= minSize {
-		ident := analysisinternal.Format(pass.Fset, x)
+		ident := astutil.Format(pass.Fset, x)
 		qualifier := ""
 		if len(sizes) > 1 {
 			qualifier = "may be "
diff --git a/go/analysis/passes/sigchanyzer/sigchanyzer.go b/go/analysis/passes/sigchanyzer/sigchanyzer.go
index 78a2fa5..c339fa0 100644
--- a/go/analysis/passes/sigchanyzer/sigchanyzer.go
+++ b/go/analysis/passes/sigchanyzer/sigchanyzer.go
@@ -20,7 +20,7 @@
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 //go:embed doc.go
@@ -36,7 +36,7 @@
 }
 
 func run(pass *analysis.Pass) (any, error) {
-	if !analysisinternal.Imports(pass.Pkg, "os/signal") {
+	if !typesinternal.Imports(pass.Pkg, "os/signal") {
 		return nil, nil // doesn't directly import signal
 	}
 
diff --git a/go/analysis/passes/slog/slog.go b/go/analysis/passes/slog/slog.go
index c1ac960..cc58396 100644
--- a/go/analysis/passes/slog/slog.go
+++ b/go/analysis/passes/slog/slog.go
@@ -20,7 +20,7 @@
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/typesinternal"
 )
 
@@ -115,10 +115,10 @@
 				default:
 					if unknownArg == nil {
 						pass.ReportRangef(arg, "%s arg %q should be a string or a slog.Attr (possible missing key or value)",
-							shortName(fn), analysisinternal.Format(pass.Fset, arg))
+							shortName(fn), astutil.Format(pass.Fset, arg))
 					} else {
 						pass.ReportRangef(arg, "%s arg %q should probably be a string or a slog.Attr (previous arg %q cannot be a key)",
-							shortName(fn), analysisinternal.Format(pass.Fset, arg), analysisinternal.Format(pass.Fset, unknownArg))
+							shortName(fn), astutil.Format(pass.Fset, arg), astutil.Format(pass.Fset, unknownArg))
 					}
 					// Stop here so we report at most one missing key per call.
 					return
@@ -158,7 +158,7 @@
 }
 
 func isAttr(t types.Type) bool {
-	return analysisinternal.IsTypeNamed(t, "log/slog", "Attr")
+	return typesinternal.IsTypeNamed(t, "log/slog", "Attr")
 }
 
 // shortName returns a name for the function that is shorter than FullName.
diff --git a/go/analysis/passes/sortslice/analyzer.go b/go/analysis/passes/sortslice/analyzer.go
index 9fe0d20..2b18820 100644
--- a/go/analysis/passes/sortslice/analyzer.go
+++ b/go/analysis/passes/sortslice/analyzer.go
@@ -17,7 +17,7 @@
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 const Doc = `check the argument type of sort.Slice
@@ -34,7 +34,7 @@
 }
 
 func run(pass *analysis.Pass) (any, error) {
-	if !analysisinternal.Imports(pass.Pkg, "sort") {
+	if !typesinternal.Imports(pass.Pkg, "sort") {
 		return nil, nil // doesn't directly import sort
 	}
 
@@ -47,7 +47,7 @@
 	inspect.Preorder(nodeFilter, func(n ast.Node) {
 		call := n.(*ast.CallExpr)
 		obj := typeutil.Callee(pass.TypesInfo, call)
-		if !analysisinternal.IsFunctionNamed(obj, "sort", "Slice", "SliceStable", "SliceIsSorted") {
+		if !typesinternal.IsFunctionNamed(obj, "sort", "Slice", "SliceStable", "SliceIsSorted") {
 			return
 		}
 		callee := obj.(*types.Func)
diff --git a/go/analysis/passes/stringintconv/string.go b/go/analysis/passes/stringintconv/string.go
index 7dbff1e..164fb27 100644
--- a/go/analysis/passes/stringintconv/string.go
+++ b/go/analysis/passes/stringintconv/string.go
@@ -15,7 +15,7 @@
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/refactor"
 	"golang.org/x/tools/internal/typeparams"
 	"golang.org/x/tools/internal/typesinternal"
 )
@@ -198,7 +198,7 @@
 		// the type has methods, as some {String,GoString,Format}
 		// may change the behavior of fmt.Sprint.
 		if len(ttypes) == 1 && len(vtypes) == 1 && types.NewMethodSet(V0).Len() == 0 {
-			_, prefix, importEdits := analysisinternal.AddImport(pass.TypesInfo, file, "fmt", "fmt", "Sprint", arg.Pos())
+			_, prefix, importEdits := refactor.AddImport(pass.TypesInfo, file, "fmt", "fmt", "Sprint", arg.Pos())
 			if types.Identical(T0, types.Typ[types.String]) {
 				// string(x) -> fmt.Sprint(x)
 				addFix("Format the number as a decimal", append(importEdits,
diff --git a/go/analysis/passes/testinggoroutine/testinggoroutine.go b/go/analysis/passes/testinggoroutine/testinggoroutine.go
index 360ba0e..400a696 100644
--- a/go/analysis/passes/testinggoroutine/testinggoroutine.go
+++ b/go/analysis/passes/testinggoroutine/testinggoroutine.go
@@ -16,7 +16,6 @@
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/typesinternal"
 )
 
@@ -40,7 +39,7 @@
 func run(pass *analysis.Pass) (any, error) {
 	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
 
-	if !analysisinternal.Imports(pass.Pkg, "testing") {
+	if !typesinternal.Imports(pass.Pkg, "testing") {
 		return nil, nil
 	}
 
diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go
index d4e9b02..1c0e92d 100644
--- a/go/analysis/passes/tests/tests.go
+++ b/go/analysis/passes/tests/tests.go
@@ -17,6 +17,7 @@
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 //go:embed doc.go
@@ -258,7 +259,7 @@
 	if !ok {
 		return false
 	}
-	return analysisinternal.IsTypeNamed(ptr.Elem(), "testing", testingType)
+	return typesinternal.IsTypeNamed(ptr.Elem(), "testing", testingType)
 }
 
 // Validate that fuzz target function's arguments are of accepted types.
diff --git a/go/analysis/passes/timeformat/timeformat.go b/go/analysis/passes/timeformat/timeformat.go
index 4fdbb2b..db91d37 100644
--- a/go/analysis/passes/timeformat/timeformat.go
+++ b/go/analysis/passes/timeformat/timeformat.go
@@ -19,7 +19,7 @@
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 const badFormat = "2006-02-01"
@@ -50,8 +50,8 @@
 	inspect.Preorder(nodeFilter, func(n ast.Node) {
 		call := n.(*ast.CallExpr)
 		obj := typeutil.Callee(pass.TypesInfo, call)
-		if !analysisinternal.IsMethodNamed(obj, "time", "Time", "Format") &&
-			!analysisinternal.IsFunctionNamed(obj, "time", "Parse") {
+		if !typesinternal.IsMethodNamed(obj, "time", "Time", "Format") &&
+			!typesinternal.IsFunctionNamed(obj, "time", "Parse") {
 			return
 		}
 		if len(call.Args) > 0 {
diff --git a/go/analysis/passes/unsafeptr/unsafeptr.go b/go/analysis/passes/unsafeptr/unsafeptr.go
index 57c6da6..778010b 100644
--- a/go/analysis/passes/unsafeptr/unsafeptr.go
+++ b/go/analysis/passes/unsafeptr/unsafeptr.go
@@ -16,7 +16,7 @@
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 //go:embed doc.go
@@ -105,7 +105,7 @@
 		}
 		switch sel.Sel.Name {
 		case "Pointer", "UnsafeAddr":
-			if analysisinternal.IsTypeNamed(info.Types[sel.X].Type, "reflect", "Value") {
+			if typesinternal.IsTypeNamed(info.Types[sel.X].Type, "reflect", "Value") {
 				return true
 			}
 		}
@@ -153,5 +153,5 @@
 
 // isReflectHeader reports whether t is reflect.SliceHeader or reflect.StringHeader.
 func isReflectHeader(t types.Type) bool {
-	return analysisinternal.IsTypeNamed(t, "reflect", "SliceHeader", "StringHeader")
+	return typesinternal.IsTypeNamed(t, "reflect", "SliceHeader", "StringHeader")
 }
diff --git a/go/analysis/passes/waitgroup/waitgroup.go b/go/analysis/passes/waitgroup/waitgroup.go
index 14c6986..5ed1814 100644
--- a/go/analysis/passes/waitgroup/waitgroup.go
+++ b/go/analysis/passes/waitgroup/waitgroup.go
@@ -16,7 +16,7 @@
 	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/typesinternal"
 )
 
 //go:embed doc.go
@@ -31,7 +31,7 @@
 }
 
 func run(pass *analysis.Pass) (any, error) {
-	if !analysisinternal.Imports(pass.Pkg, "sync") {
+	if !typesinternal.Imports(pass.Pkg, "sync") {
 		return nil, nil // doesn't directly import sync
 	}
 
@@ -44,7 +44,7 @@
 		if push {
 			call := n.(*ast.CallExpr)
 			obj := typeutil.Callee(pass.TypesInfo, call)
-			if analysisinternal.IsMethodNamed(obj, "sync", "WaitGroup", "Add") &&
+			if typesinternal.IsMethodNamed(obj, "sync", "WaitGroup", "Add") &&
 				hasSuffix(stack, wantSuffix) &&
 				backindex(stack, 1) == backindex(stack, 2).(*ast.BlockStmt).List[0] { // ExprStmt must be Block's first stmt
 
diff --git a/gopls/internal/analysis/embeddirective/embeddirective.go b/gopls/internal/analysis/embeddirective/embeddirective.go
index 58b4a36..b5218ce 100644
--- a/gopls/internal/analysis/embeddirective/embeddirective.go
+++ b/gopls/internal/analysis/embeddirective/embeddirective.go
@@ -13,6 +13,7 @@
 
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/refactor"
 )
 
 //go:embed doc.go
@@ -46,7 +47,7 @@
 
 			if !hasEmbedImport {
 				// Add blank import of "embed".
-				_, _, edits := analysisinternal.AddImport(pass.TypesInfo, f, "_", "embed", "", c.Pos())
+				_, _, edits := refactor.AddImport(pass.TypesInfo, f, "_", "embed", "", c.Pos())
 				if len(edits) > 0 {
 					pass.Report(analysis.Diagnostic{
 						Pos:     pos,
diff --git a/gopls/internal/analysis/fillreturns/fillreturns.go b/gopls/internal/analysis/fillreturns/fillreturns.go
index 33cb076..fac651d 100644
--- a/gopls/internal/analysis/fillreturns/fillreturns.go
+++ b/gopls/internal/analysis/fillreturns/fillreturns.go
@@ -121,7 +121,7 @@
 				if t := info.TypeOf(val); t == nil || !matchingTypes(t, retTyp) {
 					continue
 				}
-				if !typesinternal.IsZeroExpr(val) {
+				if !isZeroExpr(val) {
 					match, idx = val, j
 					break
 				}
@@ -154,7 +154,7 @@
 		// Remove any non-matching "zero values" from the leftover values.
 		var nonZeroRemaining []ast.Expr
 		for _, expr := range remaining {
-			if !typesinternal.IsZeroExpr(expr) {
+			if !isZeroExpr(expr) {
 				nonZeroRemaining = append(nonZeroRemaining, expr)
 			}
 		}
@@ -228,3 +228,17 @@
 func enclosingFunc(c inspector.Cursor) (inspector.Cursor, bool) {
 	return moreiters.First(c.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)))
 }
+
+// isZeroExpr uses simple syntactic heuristics to report whether expr
+// is a obvious zero value, such as 0, "", nil, or false.
+// It cannot do better without type information.
+func isZeroExpr(expr ast.Expr) bool {
+	switch e := expr.(type) {
+	case *ast.BasicLit:
+		return e.Value == "0" || e.Value == `""`
+	case *ast.Ident:
+		return e.Name == "nil" || e.Name == "false"
+	default:
+		return false
+	}
+}
diff --git a/gopls/internal/analysis/maprange/maprange.go b/gopls/internal/analysis/maprange/maprange.go
index 3faee3f..1e44a4b 100644
--- a/gopls/internal/analysis/maprange/maprange.go
+++ b/gopls/internal/analysis/maprange/maprange.go
@@ -16,6 +16,7 @@
 	"golang.org/x/tools/gopls/internal/util/cursorutil"
 	"golang.org/x/tools/internal/analysisinternal"
 	typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
+	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/typesinternal/typeindex"
 	"golang.org/x/tools/internal/versions"
 )
@@ -49,7 +50,7 @@
 	)
 	for _, callee := range []types.Object{mapsKeys, mapsValues, xmapsKeys, xmapsValues} {
 		for curCall := range index.Calls(callee) {
-			if analysisinternal.IsChildOf(curCall, edge.RangeStmt_X) {
+			if astutil.IsChildOf(curCall, edge.RangeStmt_X) {
 				analyzeRangeStmt(pass, callee, curCall)
 			}
 		}
diff --git a/gopls/internal/analysis/unusedfunc/unusedfunc.go b/gopls/internal/analysis/unusedfunc/unusedfunc.go
index 0bf738e..5acee0c 100644
--- a/gopls/internal/analysis/unusedfunc/unusedfunc.go
+++ b/gopls/internal/analysis/unusedfunc/unusedfunc.go
@@ -18,6 +18,7 @@
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/internal/analysisinternal"
 	typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
+	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/typesinternal/typeindex"
 )
 
@@ -118,7 +119,7 @@
 
 		// Expand to include leading doc comment.
 		pos := node.Pos()
-		if doc := docComment(node); doc != nil {
+		if doc := astutil.DocComment(node); doc != nil {
 			pos = doc.Pos()
 		}
 
@@ -170,7 +171,7 @@
 			// but it is bad style; and the directive may
 			// appear anywhere, not just on the preceding line,
 			// but again that is poor form.)
-			if doc := docComment(decl); doc != nil {
+			if doc := astutil.DocComment(decl); doc != nil {
 				for _, comment := range doc.List {
 					// TODO(adonovan): use ast.ParseDirective when #68021 lands.
 					if strings.HasPrefix(comment.Text, "//go:linkname ") {
@@ -238,20 +239,6 @@
 	return nil, nil
 }
 
-func docComment(n ast.Node) *ast.CommentGroup {
-	switch n := n.(type) {
-	case *ast.FuncDecl:
-		return n.Doc
-	case *ast.GenDecl:
-		return n.Doc
-	case *ast.ValueSpec:
-		return n.Doc
-	case *ast.TypeSpec:
-		return n.Doc
-	}
-	return nil // includes File, ImportSpec, Field
-}
-
 func eolComment(n ast.Node) *ast.CommentGroup {
 	// TODO(adonovan): support:
 	//    func f() {...} // comment
diff --git a/gopls/internal/analysis/unusedvariable/unusedvariable.go b/gopls/internal/analysis/unusedvariable/unusedvariable.go
index 1cd8249..3129d0f 100644
--- a/gopls/internal/analysis/unusedvariable/unusedvariable.go
+++ b/gopls/internal/analysis/unusedvariable/unusedvariable.go
@@ -14,7 +14,7 @@
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/analysis/passes/inspect"
 	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/refactor"
 )
 
 const Doc = `check for unused variables and suggest fixes`
@@ -56,7 +56,7 @@
 			}
 
 			tokFile := pass.Fset.File(ident.Pos())
-			edits := analysisinternal.DeleteVar(tokFile, pass.TypesInfo, curId)
+			edits := refactor.DeleteVar(tokFile, pass.TypesInfo, curId)
 			if len(edits) > 0 {
 				pass.Report(analysis.Diagnostic{
 					Pos:     ident.Pos(),
diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go
index 1752421..fae6d57 100644
--- a/gopls/internal/cache/check.go
+++ b/gopls/internal/cache/check.go
@@ -5,12 +5,14 @@
 package cache
 
 import (
+	"bytes"
 	"context"
 	"crypto/sha256"
 	"fmt"
 	"go/ast"
 	"go/build"
 	"go/parser"
+	"go/scanner"
 	"go/token"
 	"go/types"
 	"regexp"
@@ -36,7 +38,6 @@
 	"golang.org/x/tools/gopls/internal/util/moremaps"
 	"golang.org/x/tools/gopls/internal/util/safetoken"
 	"golang.org/x/tools/gopls/internal/util/tokeninternal"
-	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/gcimporter"
 	"golang.org/x/tools/internal/packagesinternal"
@@ -2061,7 +2062,7 @@
 				//
 				// TODO(adonovan): It is the type checker's responsibility
 				// to ensure that (start, end) are meaningful; see #71803.
-				end = analysisinternal.TypeErrorEndPos(pgf.Tok, pgf.Src, start)
+				end = typeErrorEndPos(pgf.Tok, pgf.Src, start)
 
 				// debugging golang/go#65960
 				if _, err := safetoken.Offset(pgf.Tok, end); err != nil {
@@ -2161,6 +2162,51 @@
 	return result
 }
 
+// This heuristic is ill-defined.
+func typeErrorEndPos(tokFile *token.File, src []byte, start token.Pos) token.Pos {
+	// Get the end position for the type error.
+	offset, err := safetoken.Offset(tokFile, start)
+	if err != nil || offset > len(src) {
+		return start
+	}
+	src = src[offset:]
+
+	// Attempt to find a reasonable end position for the type error.
+	//
+	// TODO(rfindley): the heuristic implemented here is unclear. It looks like
+	// it seeks the end of the primary operand starting at start, but that is not
+	// quite implemented (for example, given a func literal this heuristic will
+	// return the range of the func keyword).
+	//
+	// We should formalize this heuristic, or deprecate it by finally proposing
+	// to add end position to all type checker errors.
+	//
+	// Nevertheless, ensure that the end position at least spans the current
+	// token at the cursor (this was golang/go#69505).
+	end := start
+	{
+		var s scanner.Scanner
+		fset := token.NewFileSet()
+		f := fset.AddFile("", fset.Base(), len(src))
+		s.Init(f, src, nil /* no error handler */, scanner.ScanComments)
+		pos, tok, lit := s.Scan()
+		if tok != token.SEMICOLON {
+			if off, err := safetoken.Offset(f, pos); err == nil {
+				off += len(lit)
+				src = src[off:]
+				end += token.Pos(off)
+			}
+		}
+	}
+
+	// Look for bytes that might terminate the current operand. See note above:
+	// this is imprecise.
+	if width := bytes.IndexAny(src, " \n,():;[]+-*/"); width > 0 {
+		end += token.Pos(width)
+	}
+	return end
+}
+
 // An importFunc is an implementation of the single-method
 // types.Importer interface based on a function value.
 type importerFunc func(path string) (*types.Package, error)
diff --git a/gopls/internal/cache/parsego/parse_test.go b/gopls/internal/cache/parsego/parse_test.go
index be940ca..f93a5a6 100644
--- a/gopls/internal/cache/parsego/parse_test.go
+++ b/gopls/internal/cache/parsego/parse_test.go
@@ -17,7 +17,7 @@
 	"golang.org/x/tools/gopls/internal/cache/parsego"
 	"golang.org/x/tools/gopls/internal/util/safetoken"
 	"golang.org/x/tools/gopls/internal/util/tokeninternal"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
 )
 
 // TODO(golang/go#64335): we should have many more tests for fixed syntax.
@@ -122,7 +122,7 @@
 						return
 					}
 
-					if got := analysisinternal.Format(fset, call); got != tc.wantFix {
+					if got := astutil.Format(fset, call); got != tc.wantFix {
 						t.Fatalf("got %v want %v", got, tc.wantFix)
 					}
 				})
@@ -230,12 +230,12 @@
 				fset := tokeninternal.FileSetFor(pgf.Tok)
 				inspect(t, pgf, func(n ast.Stmt) {
 					if init, cond, ok := info(n, keyword); ok {
-						if got := analysisinternal.Format(fset, init); got != tc.wantInitFix {
+						if got := astutil.Format(fset, init); got != tc.wantInitFix {
 							t.Fatalf("%s: Init got %v want %v", tc.source, got, tc.wantInitFix)
 						}
 
 						wantCond := getWantCond(keyword)
-						if got := analysisinternal.Format(fset, cond); got != wantCond {
+						if got := astutil.Format(fset, cond); got != wantCond {
 							t.Fatalf("%s: Cond got %v want %v", tc.source, got, wantCond)
 						}
 					}
@@ -307,7 +307,7 @@
 			fset := tokeninternal.FileSetFor(pgf.Tok)
 			inspect(t, pgf, func(sel *ast.SelectorExpr) {
 				// the fix should restore the selector as is.
-				if got, want := analysisinternal.Format(fset, sel), tc.source; got != want {
+				if got, want := astutil.Format(fset, sel), tc.source; got != want {
 					t.Fatalf("got %v want %v", got, want)
 				}
 			})
diff --git a/gopls/internal/golang/addtest.go b/gopls/internal/golang/addtest.go
index ac91f3e..805a4f2 100644
--- a/gopls/internal/golang/addtest.go
+++ b/gopls/internal/golang/addtest.go
@@ -27,7 +27,6 @@
 	"golang.org/x/tools/gopls/internal/cache/parsego"
 	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/util/moremaps"
-	"golang.org/x/tools/internal/analysisinternal"
 	internalastutil "golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/imports"
 	"golang.org/x/tools/internal/typesinternal"
@@ -484,7 +483,7 @@
 	}
 
 	isContextType := func(t types.Type) bool {
-		return analysisinternal.IsTypeNamed(t, "context", "Context")
+		return typesinternal.IsTypeNamed(t, "context", "Context")
 	}
 
 	for i := range sig.Params().Len() {
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go
index f8a0fb7..792c8b7 100644
--- a/gopls/internal/golang/codeaction.go
+++ b/gopls/internal/golang/codeaction.go
@@ -15,7 +15,7 @@
 	"slices"
 	"strings"
 
-	"golang.org/x/tools/go/ast/astutil"
+	goastutil "golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/go/ast/edge"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/gopls/internal/analysis/fillstruct"
@@ -28,7 +28,7 @@
 	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/protocol/command"
 	"golang.org/x/tools/gopls/internal/settings"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/imports"
 	"golang.org/x/tools/internal/typesinternal"
@@ -535,7 +535,7 @@
 		}
 		desc := string(text)
 		if len(desc) >= 40 || strings.Contains(desc, "\n") {
-			desc = astutil.NodeDescription(exprs[0])
+			desc = goastutil.NodeDescription(exprs[0])
 		}
 		constant := info.Types[exprs[0]].Value != nil
 		if (req.kind == settings.RefactorExtractConstantAll) == constant {
@@ -569,7 +569,7 @@
 		return nil
 	}
 
-	path, _ := astutil.PathEnclosingInterval(req.pgf.File, req.start, req.end)
+	path, _ := goastutil.PathEnclosingInterval(req.pgf.File, req.start, req.end)
 	if len(path) < 2 {
 		return nil
 	}
@@ -754,7 +754,7 @@
 		// that reference package-level symbols.
 		// All other references to a symbol imported from another package
 		// are nested within a select expression (pkg.Foo, v.Method, v.Field).
-		if analysisinternal.IsChildOf(curId, edge.SelectorExpr_Sel) {
+		if astutil.IsChildOf(curId, edge.SelectorExpr_Sel) {
 			continue // qualified identifier (pkg.X) or selector (T.X or e.X)
 		}
 		if !typesinternal.IsPackageLevel(use) {
diff --git a/gopls/internal/golang/inlay_hint.go b/gopls/internal/golang/inlay_hint.go
index 281158a..a596550 100644
--- a/gopls/internal/golang/inlay_hint.go
+++ b/gopls/internal/golang/inlay_hint.go
@@ -23,7 +23,6 @@
 	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/settings"
 	"golang.org/x/tools/gopls/internal/util/safetoken"
-	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/typeparams"
 	"golang.org/x/tools/internal/typesinternal"
@@ -164,10 +163,10 @@
 
 		// Suppress some common false positives.
 		obj := typeutil.Callee(info, call)
-		if analysisinternal.IsFunctionNamed(obj, "fmt", "Print", "Printf", "Println", "Fprint", "Fprintf", "Fprintln") ||
-			analysisinternal.IsMethodNamed(obj, "bytes", "Buffer", "Write", "WriteByte", "WriteRune", "WriteString") ||
-			analysisinternal.IsMethodNamed(obj, "strings", "Builder", "Write", "WriteByte", "WriteRune", "WriteString") ||
-			analysisinternal.IsFunctionNamed(obj, "io", "WriteString") {
+		if typesinternal.IsFunctionNamed(obj, "fmt", "Print", "Printf", "Println", "Fprint", "Fprintf", "Fprintln") ||
+			typesinternal.IsMethodNamed(obj, "bytes", "Buffer", "Write", "WriteByte", "WriteRune", "WriteString") ||
+			typesinternal.IsMethodNamed(obj, "strings", "Builder", "Write", "WriteByte", "WriteRune", "WriteString") ||
+			typesinternal.IsFunctionNamed(obj, "io", "WriteString") {
 			continue
 		}
 
diff --git a/gopls/internal/golang/inline.go b/gopls/internal/golang/inline.go
index 60459fd..ae3bbe2 100644
--- a/gopls/internal/golang/inline.go
+++ b/gopls/internal/golang/inline.go
@@ -14,7 +14,7 @@
 	"go/types"
 
 	"golang.org/x/tools/go/analysis"
-	"golang.org/x/tools/go/ast/astutil"
+	goastutil "golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/go/ast/edge"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/go/types/typeutil"
@@ -22,7 +22,7 @@
 	"golang.org/x/tools/gopls/internal/cache/parsego"
 	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/util/safetoken"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
 	internalastutil "golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/diff"
 	"golang.org/x/tools/internal/event"
@@ -32,7 +32,8 @@
 // enclosingStaticCall returns the innermost function call enclosing
 // the selected range, along with the callee.
 func enclosingStaticCall(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*ast.CallExpr, *types.Func, error) {
-	path, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
+	// TODO(adonovan): simplify using pgf.Cursor
+	path, _ := goastutil.PathEnclosingInterval(pgf.File, start, end)
 
 	var call *ast.CallExpr
 loop:
@@ -208,7 +209,7 @@
 // unparenEnclosing removes enclosing parens from cur in
 // preparation for a call to [Cursor.ParentEdge].
 func unparenEnclosing(cur inspector.Cursor) inspector.Cursor {
-	for analysisinternal.IsChildOf(cur, edge.ParenExpr_X) {
+	for astutil.IsChildOf(cur, edge.ParenExpr_X) {
 		cur = cur.Parent()
 	}
 	return cur
@@ -231,7 +232,7 @@
 		scope = info.Scopes[pgf.File].Innermost(pos)
 	)
 	for curIdent := range curRHS.Preorder((*ast.Ident)(nil)) {
-		if analysisinternal.IsChildOf(curIdent, edge.SelectorExpr_Sel) {
+		if astutil.IsChildOf(curIdent, edge.SelectorExpr_Sel) {
 			continue // ignore f in x.f
 		}
 		id := curIdent.Node().(*ast.Ident)
diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go
index 1a1fded..ac2f8d1 100644
--- a/gopls/internal/golang/rename_check.go
+++ b/gopls/internal/golang/rename_check.go
@@ -47,7 +47,6 @@
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/gopls/internal/cache"
 	"golang.org/x/tools/gopls/internal/util/safetoken"
-	"golang.org/x/tools/internal/analysisinternal"
 	"golang.org/x/tools/internal/typeparams"
 	"golang.org/x/tools/internal/typesinternal"
 	"golang.org/x/tools/refactor/satisfy"
@@ -355,7 +354,7 @@
 		switch n := cur.Node().(type) {
 		case *ast.Ident:
 			if pkg.TypesInfo().Uses[n] == obj {
-				block := analysisinternal.EnclosingScope(pkg.TypesInfo(), cur)
+				block := typesinternal.EnclosingScope(pkg.TypesInfo(), cur)
 				if !fn(n, block) {
 					ok = false
 				}
diff --git a/gopls/internal/golang/undeclared.go b/gopls/internal/golang/undeclared.go
index 39cfd59..1654fa6 100644
--- a/gopls/internal/golang/undeclared.go
+++ b/gopls/internal/golang/undeclared.go
@@ -15,14 +15,14 @@
 	"unicode"
 
 	"golang.org/x/tools/go/analysis"
-	"golang.org/x/tools/go/ast/astutil"
+	goastutil "golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/go/ast/edge"
 	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/gopls/internal/cache"
 	"golang.org/x/tools/gopls/internal/cache/parsego"
 	"golang.org/x/tools/gopls/internal/util/cursorutil"
 	"golang.org/x/tools/gopls/internal/util/typesutil"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/moreiters"
 	"golang.org/x/tools/internal/typesinternal"
 )
@@ -57,7 +57,7 @@
 	}
 
 	// Offer a fix.
-	noun := cond(analysisinternal.IsChildOf(curId, edge.CallExpr_Fun), "function", "variable")
+	noun := cond(astutil.IsChildOf(curId, edge.CallExpr_Fun), "function", "variable")
 	return fmt.Sprintf("Create %s %s", noun, name)
 }
 
@@ -77,7 +77,7 @@
 
 	// Check for a possible call expression, in which case we should add a
 	// new function declaration.
-	if analysisinternal.IsChildOf(curId, edge.CallExpr_Fun) {
+	if astutil.IsChildOf(curId, edge.CallExpr_Fun) {
 		return newFunctionDeclaration(curId, file, pkg.Types(), info, fset)
 	}
 	var (
@@ -97,7 +97,7 @@
 		n := curRef.Node().(*ast.Ident)
 		if n.Name == ident.Name && info.ObjectOf(n) == nil {
 			firstRef = n
-			if analysisinternal.IsChildOf(curRef, edge.AssignStmt_Lhs) {
+			if astutil.IsChildOf(curRef, edge.AssignStmt_Lhs) {
 				assign := curRef.Parent().Node().(*ast.AssignStmt)
 				if assign.Tok == token.ASSIGN && !referencesIdent(info, assign, ident) {
 					assignTokPos = assign.TokPos
@@ -121,7 +121,8 @@
 	if firstRef == nil {
 		return nil, nil, fmt.Errorf("no identifier found")
 	}
-	p, _ := astutil.PathEnclosingInterval(file, firstRef.Pos(), firstRef.Pos())
+	// TODO(adonovan): replace this with cursor.
+	p, _ := goastutil.PathEnclosingInterval(file, firstRef.Pos(), firstRef.Pos())
 	insertBeforeStmt, err := stmtToInsertVarBefore(p, nil)
 	if err != nil {
 		return nil, nil, fmt.Errorf("could not locate insertion point: %v", err)
diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go
index 26c15cc..2b4a8eb 100644
--- a/internal/analysisinternal/analysis.go
+++ b/internal/analysisinternal/analysis.go
@@ -7,74 +7,23 @@
 package analysisinternal
 
 import (
-	"bytes"
 	"cmp"
 	"fmt"
 	"go/ast"
-	"go/printer"
-	"go/scanner"
 	"go/token"
 	"go/types"
-	"iter"
-	pathpkg "path"
 	"slices"
 	"strings"
 
 	"golang.org/x/tools/go/analysis"
-	"golang.org/x/tools/go/ast/edge"
-	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/moreiters"
-	"golang.org/x/tools/internal/typesinternal"
 )
 
-// Deprecated: this heuristic is ill-defined.
-// TODO(adonovan): move to sole use in gopls/internal/cache.
-func TypeErrorEndPos(tokFile *token.File, src []byte, start token.Pos) token.Pos {
-	// Get the end position for the type error.
-	if offset := tokFile.PositionFor(start, false).Offset; offset > len(src) {
-		return start
-	} else {
-		src = src[offset:]
-	}
-
-	// Attempt to find a reasonable end position for the type error.
-	//
-	// TODO(rfindley): the heuristic implemented here is unclear. It looks like
-	// it seeks the end of the primary operand starting at start, but that is not
-	// quite implemented (for example, given a func literal this heuristic will
-	// return the range of the func keyword).
-	//
-	// We should formalize this heuristic, or deprecate it by finally proposing
-	// to add end position to all type checker errors.
-	//
-	// Nevertheless, ensure that the end position at least spans the current
-	// token at the cursor (this was golang/go#69505).
-	end := start
-	{
-		var s scanner.Scanner
-		fset := token.NewFileSet()
-		f := fset.AddFile("", fset.Base(), len(src))
-		s.Init(f, src, nil /* no error handler */, scanner.ScanComments)
-		pos, tok, lit := s.Scan()
-		if tok != token.SEMICOLON && token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size()) {
-			off := tokFile.Offset(pos) + len(lit)
-			src = src[off:]
-			end += token.Pos(off)
-		}
-	}
-
-	// Look for bytes that might terminate the current operand. See note above:
-	// this is imprecise.
-	if width := bytes.IndexAny(src, " \n,():;[]+-*/"); width > 0 {
-		end += token.Pos(width)
-	}
-	return end
-}
-
 // MatchingIdents finds the names of all identifiers in 'node' that match any of the given types.
 // 'pos' represents the position at which the identifiers may be inserted. 'pos' must be within
 // the scope of each of identifier we select. Otherwise, we will insert a variable at 'pos' that
 // is unrecognized.
+//
+// TODO(adonovan): this is only used by gopls/internal/analysis/fill{returns,struct}. Move closer.
 func MatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *types.Info, pkg *types.Package) map[types.Type][]string {
 
 	// Initialize matches to contain the variable types we are searching for.
@@ -190,207 +139,6 @@
 	return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename)
 }
 
-// AddImport checks whether this file already imports pkgpath and that
-// the import is in scope at pos. If so, it returns the name under
-// which it was imported and no edits. Otherwise, it adds a new import
-// of pkgpath, using a name derived from the preferred name, and
-// returns the chosen name, a prefix to be concatenated with member to
-// form a qualified name, and the edit for the new import.
-//
-// The member argument indicates the name of the desired symbol within
-// the imported package. This is needed in the case when the existing
-// import is a dot import, because then it is possible that the
-// desired symbol is shadowed by other declarations in the current
-// package. If member is not shadowed at pos, AddImport returns (".",
-// "", nil). (AddImport accepts the caller's implicit claim that the
-// imported package declares member.)
-//
-// Use a preferredName of "_" to request a blank import;
-// member is ignored in this case.
-//
-// It does not mutate its arguments.
-func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member string, pos token.Pos) (name, prefix string, newImport []analysis.TextEdit) {
-	// Find innermost enclosing lexical block.
-	scope := info.Scopes[file].Innermost(pos)
-	if scope == nil {
-		panic("no enclosing lexical block")
-	}
-
-	// Is there an existing import of this package?
-	// If so, are we in its scope? (not shadowed)
-	for _, spec := range file.Imports {
-		pkgname := info.PkgNameOf(spec)
-		if pkgname != nil && pkgname.Imported().Path() == pkgpath {
-			name = pkgname.Name()
-			if preferredName == "_" {
-				// Request for blank import; any existing import will do.
-				return name, "", nil
-			}
-			if name == "." {
-				// The scope of ident must be the file scope.
-				if s, _ := scope.LookupParent(member, pos); s == info.Scopes[file] {
-					return name, "", nil
-				}
-			} else if _, obj := scope.LookupParent(name, pos); obj == pkgname {
-				return name, name + ".", nil
-			}
-		}
-	}
-
-	// We must add a new import.
-
-	// Ensure we have a fresh name.
-	newName := preferredName
-	if preferredName != "_" {
-		newName = FreshName(scope, pos, preferredName)
-	}
-
-	// Create a new import declaration either before the first existing
-	// declaration (which must exist), including its comments; or
-	// inside the declaration, if it is an import group.
-	//
-	// Use a renaming import whenever the preferred name is not
-	// available, or the chosen name does not match the last
-	// segment of its path.
-	newText := fmt.Sprintf("%q", pkgpath)
-	if newName != preferredName || newName != pathpkg.Base(pkgpath) {
-		newText = fmt.Sprintf("%s %q", newName, pkgpath)
-	}
-
-	decl0 := file.Decls[0]
-	var before ast.Node = decl0
-	switch decl0 := decl0.(type) {
-	case *ast.GenDecl:
-		if decl0.Doc != nil {
-			before = decl0.Doc
-		}
-	case *ast.FuncDecl:
-		if decl0.Doc != nil {
-			before = decl0.Doc
-		}
-	}
-	if gd, ok := before.(*ast.GenDecl); ok && gd.Tok == token.IMPORT && gd.Rparen.IsValid() {
-		// Have existing grouped import ( ... ) decl.
-		if IsStdPackage(pkgpath) && len(gd.Specs) > 0 {
-			// Add spec for a std package before
-			// first existing spec, followed by
-			// a blank line if the next one is non-std.
-			first := gd.Specs[0].(*ast.ImportSpec)
-			pos = first.Pos()
-			if !IsStdPackage(first.Path.Value) {
-				newText += "\n"
-			}
-			newText += "\n\t"
-		} else {
-			// Add spec at end of group.
-			pos = gd.Rparen
-			newText = "\t" + newText + "\n"
-		}
-	} else {
-		// No import decl, or non-grouped import.
-		// Add a new import decl before first decl.
-		// (gofmt will merge multiple import decls.)
-		pos = before.Pos()
-		newText = "import " + newText + "\n\n"
-	}
-	return newName, newName + ".", []analysis.TextEdit{{
-		Pos:     pos,
-		End:     pos,
-		NewText: []byte(newText),
-	}}
-}
-
-// FreshName returns the name of an identifier that is undefined
-// at the specified position, based on the preferred name.
-func FreshName(scope *types.Scope, pos token.Pos, preferred string) string {
-	newName := preferred
-	for i := 0; ; i++ {
-		if _, obj := scope.LookupParent(newName, pos); obj == nil {
-			break // fresh
-		}
-		newName = fmt.Sprintf("%s%d", preferred, i)
-	}
-	return newName
-}
-
-// Format returns a string representation of the node n.
-func Format(fset *token.FileSet, n ast.Node) string {
-	var buf strings.Builder
-	printer.Fprint(&buf, fset, n) // ignore errors
-	return buf.String()
-}
-
-// Imports returns true if path is imported by pkg.
-func Imports(pkg *types.Package, path string) bool {
-	for _, imp := range pkg.Imports() {
-		if imp.Path() == path {
-			return true
-		}
-	}
-	return false
-}
-
-// IsTypeNamed reports whether t is (or is an alias for) a
-// package-level defined type with the given package path and one of
-// the given names. It returns false if t is nil.
-//
-// This function avoids allocating the concatenation of "pkg.Name",
-// which is important for the performance of syntax matching.
-func IsTypeNamed(t types.Type, pkgPath string, names ...string) bool {
-	if named, ok := types.Unalias(t).(*types.Named); ok {
-		tname := named.Obj()
-		return tname != nil &&
-			typesinternal.IsPackageLevel(tname) &&
-			tname.Pkg().Path() == pkgPath &&
-			slices.Contains(names, tname.Name())
-	}
-	return false
-}
-
-// IsPointerToNamed reports whether t is (or is an alias for) a pointer to a
-// package-level defined type with the given package path and one of the given
-// names. It returns false if t is not a pointer type.
-func IsPointerToNamed(t types.Type, pkgPath string, names ...string) bool {
-	r := typesinternal.Unpointer(t)
-	if r == t {
-		return false
-	}
-	return IsTypeNamed(r, pkgPath, names...)
-}
-
-// IsFunctionNamed reports whether obj is a package-level function
-// defined in the given package and has one of the given names.
-// It returns false if obj is nil.
-//
-// This function avoids allocating the concatenation of "pkg.Name",
-// which is important for the performance of syntax matching.
-func IsFunctionNamed(obj types.Object, pkgPath string, names ...string) bool {
-	f, ok := obj.(*types.Func)
-	return ok &&
-		typesinternal.IsPackageLevel(obj) &&
-		f.Pkg().Path() == pkgPath &&
-		f.Type().(*types.Signature).Recv() == nil &&
-		slices.Contains(names, f.Name())
-}
-
-// IsMethodNamed reports whether obj is a method defined on a
-// package-level type with the given package and type name, and has
-// one of the given names. It returns false if obj is nil.
-//
-// This function avoids allocating the concatenation of "pkg.TypeName.Name",
-// which is important for the performance of syntax matching.
-func IsMethodNamed(obj types.Object, pkgPath string, typeName string, names ...string) bool {
-	if fn, ok := obj.(*types.Func); ok {
-		if recv := fn.Type().(*types.Signature).Recv(); recv != nil {
-			_, T := typesinternal.ReceiverNamed(recv)
-			return T != nil &&
-				IsTypeNamed(T, pkgPath, typeName) &&
-				slices.Contains(names, fn.Name())
-		}
-	}
-	return false
-}
-
 // ValidateFixes validates the set of fixes for a single diagnostic.
 // Any error indicates a bug in the originating analyzer.
 //
@@ -493,6 +241,20 @@
 	return nil
 }
 
+// Range returns an [analysis.Range] for the specified start and end positions.
+func Range(pos, end token.Pos) analysis.Range {
+	return tokenRange{pos, end}
+}
+
+// tokenRange is an implementation of the [analysis.Range] interface.
+type tokenRange struct{ StartPos, EndPos token.Pos }
+
+func (r tokenRange) Pos() token.Pos { return r.StartPos }
+func (r tokenRange) End() token.Pos { return r.EndPos }
+
+// TODO(adonovan): the import-related functions below don't depend on
+// analysis (or even on go/types or go/ast). Move somewhere more logical.
+
 // CanImport reports whether one package is allowed to import another.
 //
 // TODO(adonovan): allow customization of the accessibility relation
@@ -520,132 +282,6 @@
 	return true
 }
 
-// DeleteStmt returns the edits to remove the [ast.Stmt] identified by
-// curStmt, if it is contained within a BlockStmt, CaseClause,
-// CommClause, or is the STMT in switch STMT; ... {...}. It returns nil otherwise.
-func DeleteStmt(tokFile *token.File, curStmt inspector.Cursor) []analysis.TextEdit {
-	stmt := curStmt.Node().(ast.Stmt)
-	// if the stmt is on a line by itself delete the whole line
-	// otherwise just delete the statement.
-
-	// this logic would be a lot simpler with the file contents, and somewhat simpler
-	// if the cursors included the comments.
-
-	lineOf := tokFile.Line
-	stmtStartLine, stmtEndLine := lineOf(stmt.Pos()), lineOf(stmt.End())
-
-	var from, to token.Pos
-	// bounds of adjacent syntax/comments on same line, if any
-	limits := func(left, right token.Pos) {
-		if lineOf(left) == stmtStartLine {
-			from = left
-		}
-		if lineOf(right) == stmtEndLine {
-			to = right
-		}
-	}
-	// TODO(pjw): there are other places a statement might be removed:
-	// IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
-	// (removing the blocks requires more rewriting than this routine would do)
-	// CommCase   = "case" ( SendStmt | RecvStmt ) | "default" .
-	// (removing the stmt requires more rewriting, and it's unclear what the user means)
-	switch parent := curStmt.Parent().Node().(type) {
-	case *ast.SwitchStmt:
-		limits(parent.Switch, parent.Body.Lbrace)
-	case *ast.TypeSwitchStmt:
-		limits(parent.Switch, parent.Body.Lbrace)
-		if parent.Assign == stmt {
-			return nil // don't let the user break the type switch
-		}
-	case *ast.BlockStmt:
-		limits(parent.Lbrace, parent.Rbrace)
-	case *ast.CommClause:
-		limits(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
-		if parent.Comm == stmt {
-			return nil // maybe the user meant to remove the entire CommClause?
-		}
-	case *ast.CaseClause:
-		limits(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
-	case *ast.ForStmt:
-		limits(parent.For, parent.Body.Lbrace)
-
-	default:
-		return nil // not one of ours
-	}
-
-	if prev, found := curStmt.PrevSibling(); found && lineOf(prev.Node().End()) == stmtStartLine {
-		from = prev.Node().End() // preceding statement ends on same line
-	}
-	if next, found := curStmt.NextSibling(); found && lineOf(next.Node().Pos()) == stmtEndLine {
-		to = next.Node().Pos() // following statement begins on same line
-	}
-	// and now for the comments
-Outer:
-	for _, cg := range EnclosingFile(curStmt).Comments {
-		for _, co := range cg.List {
-			if lineOf(co.End()) < stmtStartLine {
-				continue
-			} else if lineOf(co.Pos()) > stmtEndLine {
-				break Outer // no more are possible
-			}
-			if lineOf(co.End()) == stmtStartLine && co.End() < stmt.Pos() {
-				if !from.IsValid() || co.End() > from {
-					from = co.End()
-					continue // maybe there are more
-				}
-			}
-			if lineOf(co.Pos()) == stmtEndLine && co.Pos() > stmt.End() {
-				if !to.IsValid() || co.Pos() < to {
-					to = co.Pos()
-					continue // maybe there are more
-				}
-			}
-		}
-	}
-	// if either from or to is valid, just remove the statement
-	// otherwise remove the line
-	edit := analysis.TextEdit{Pos: stmt.Pos(), End: stmt.End()}
-	if from.IsValid() || to.IsValid() {
-		// remove just the statement.
-		// we can't tell if there is a ; or whitespace right after the statement
-		// ideally we'd like to remove the former and leave the latter
-		// (if gofmt has run, there likely won't be a ;)
-		// In type switches we know there's a semicolon somewhere after the statement,
-		// but the extra work for this special case is not worth it, as gofmt will fix it.
-		return []analysis.TextEdit{edit}
-	}
-	// remove the whole line
-	for lineOf(edit.Pos) == stmtStartLine {
-		edit.Pos--
-	}
-	edit.Pos++ // get back tostmtStartLine
-	for lineOf(edit.End) == stmtEndLine {
-		edit.End++
-	}
-	return []analysis.TextEdit{edit}
-}
-
-// Comments returns an iterator over the comments overlapping the specified interval.
-func Comments(file *ast.File, start, end token.Pos) iter.Seq[*ast.Comment] {
-	// TODO(adonovan): optimize use binary O(log n) instead of linear O(n) search.
-	return func(yield func(*ast.Comment) bool) {
-		for _, cg := range file.Comments {
-			for _, co := range cg.List {
-				if co.Pos() > end {
-					return
-				}
-				if co.End() < start {
-					continue
-				}
-
-				if !yield(co) {
-					return
-				}
-			}
-		}
-	}
-}
-
 // IsStdPackage reports whether the specified package path belongs to a
 // package in the standard library (including internal dependencies).
 func IsStdPackage(path string) bool {
@@ -657,48 +293,3 @@
 	}
 	return !strings.Contains(path[:slash], ".") && path != "testdata"
 }
-
-// Range returns an [analysis.Range] for the specified start and end positions.
-func Range(pos, end token.Pos) analysis.Range {
-	return tokenRange{pos, end}
-}
-
-// tokenRange is an implementation of the [analysis.Range] interface.
-type tokenRange struct{ StartPos, EndPos token.Pos }
-
-func (r tokenRange) Pos() token.Pos { return r.StartPos }
-func (r tokenRange) End() token.Pos { return r.EndPos }
-
-// EnclosingFile returns the syntax tree for the file enclosing c.
-//
-// TODO(adonovan): promote this to a method of Cursor.
-func EnclosingFile(c inspector.Cursor) *ast.File {
-	c, _ = moreiters.First(c.Enclosing((*ast.File)(nil)))
-	return c.Node().(*ast.File)
-}
-
-// EnclosingScope returns the innermost block logically enclosing the cursor.
-func EnclosingScope(info *types.Info, cur inspector.Cursor) *types.Scope {
-	for cur := range cur.Enclosing() {
-		n := cur.Node()
-		// A function's Scope is associated with its FuncType.
-		switch f := n.(type) {
-		case *ast.FuncDecl:
-			n = f.Type
-		case *ast.FuncLit:
-			n = f.Type
-		}
-		if b := info.Scopes[n]; b != nil {
-			return b
-		}
-	}
-	panic("no Scope for *ast.File")
-}
-
-// IsChildOf reports whether cur.ParentEdge is ek.
-//
-// TODO(adonovan): promote to a method of Cursor.
-func IsChildOf(cur inspector.Cursor, ek edge.Kind) bool {
-	got, _ := cur.ParentEdge()
-	return got == ek
-}
diff --git a/internal/analysisinternal/analysis_test.go b/internal/analysisinternal/analysis_test.go
index 3989051..7fe4e22 100644
--- a/internal/analysisinternal/analysis_test.go
+++ b/internal/analysisinternal/analysis_test.go
@@ -2,16 +2,12 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package analysisinternal
+package analysisinternal_test
 
 import (
-	"go/ast"
-	"go/parser"
-	"go/token"
-	"slices"
 	"testing"
 
-	"golang.org/x/tools/go/ast/inspector"
+	"golang.org/x/tools/internal/analysisinternal"
 )
 
 func TestCanImport(t *testing.T) {
@@ -36,265 +32,30 @@
 		{"a.com/b", "a.com/c/internal/foo", false},
 		{"a.com/b", "a.com/c/xinternal/foo", true},
 	} {
-		got := CanImport(tt.from, tt.to)
+		got := analysisinternal.CanImport(tt.from, tt.to)
 		if got != tt.want {
 			t.Errorf("CanImport(%q, %q) = %v, want %v", tt.from, tt.to, got, tt.want)
 		}
 	}
 }
 
-func TestDeleteStmt(t *testing.T) {
-	type testCase struct {
-		in    string
-		which int // count of ast.Stmt in ast.Inspect traversal to remove
-		want  string
-		name  string // should contain exactly one of [block,switch,case,comm,for,type]
-	}
-	tests := []testCase{
-		{ // do nothing when asked to remove a function body
-			in:    "package p; func f() {  }",
-			which: 0,
-			want:  "package p; func f() {  }",
-			name:  "block0",
-		},
-		{
-			in:    "package p; func f() { abcd()}",
-			which: 1,
-			want:  "package p; func f() { }",
-			name:  "block1",
-		},
-		{
-			in:    "package p; func f() { a() }",
-			which: 1,
-			want:  "package p; func f() {  }",
-			name:  "block2",
-		},
-		{
-			in:    "package p; func f() { a();}",
-			which: 1,
-			want:  "package p; func f() { ;}",
-			name:  "block3",
-		},
-		{
-			in:    "package p; func f() {\n a() \n\n}",
-			which: 1,
-			want:  "package p; func f() {\n\n}",
-			name:  "block4",
-		},
-		{
-			in:    "package p; func f() { a()// comment\n}",
-			which: 1,
-			want:  "package p; func f() { // comment\n}",
-			name:  "block5",
-		},
-		{
-			in:    "package p; func f() { /*c*/a() \n}",
-			which: 1,
-			want:  "package p; func f() { /*c*/ \n}",
-			name:  "block6",
-		},
-		{
-			in:    "package p; func f() { a();b();}",
-			which: 2,
-			want:  "package p; func f() { a();;}",
-			name:  "block7",
-		},
-		{
-			in:    "package p; func f() {\n\ta()\n\tb()\n}",
-			which: 2,
-			want:  "package p; func f() {\n\ta()\n}",
-			name:  "block8",
-		},
-		{
-			in:    "package p; func f() {\n\ta()\n\tb()\n\tc()\n}",
-			which: 2,
-			want:  "package p; func f() {\n\ta()\n\tc()\n}",
-			name:  "block9",
-		},
-		{
-			in:    "package p\nfunc f() {a()+b()}",
-			which: 1,
-			want:  "package p\nfunc f() {}",
-			name:  "block10",
-		},
-		{
-			in:    "package p\nfunc f() {(a()+b())}",
-			which: 1,
-			want:  "package p\nfunc f() {}",
-			name:  "block11",
-		},
-		{
-			in:    "package p; func f() { switch a(); b() {}}",
-			which: 2, // 0 is the func body, 1 is the switch statement
-			want:  "package p; func f() { switch ; b() {}}",
-			name:  "switch0",
-		},
-		{
-			in:    "package p; func f() { switch /*c*/a(); {}}",
-			which: 2, // 0 is the func body, 1 is the switch statement
-			want:  "package p; func f() { switch /*c*/; {}}",
-			name:  "switch1",
-		},
-		{
-			in:    "package p; func f() { switch a()/*c*/; {}}",
-			which: 2, // 0 is the func body, 1 is the switch statement
-			want:  "package p; func f() { switch /*c*/; {}}",
-			name:  "switch2",
-		},
-		{
-			in:    "package p; func f() { select {default: a()}}",
-			which: 4, // 0 is the func body, 1 is the select statement, 2 is its body, 3 is the comm clause
-			want:  "package p; func f() { select {default: }}",
-			name:  "comm0",
-		},
-		{
-			in:    "package p; func f(x chan any) { select {case x <- a: a(x)}}",
-			which: 5, // 0 is the func body, 1 is the select statement, 2 is its body, 3 is the comm clause
-			want:  "package p; func f(x chan any) { select {case x <- a: }}",
-			name:  "comm1",
-		},
-		{
-			in:    "package p; func f(x chan any) { select {case x <- a: a(x)}}",
-			which: 4, // 0 is the func body, 1 is the select statement, 2 is its body, 3 is the comm clause
-			want:  "package p; func f(x chan any) { select {case x <- a: a(x)}}",
-			name:  "comm2",
-		},
-		{
-			in:    "package p; func f() { switch {default: a()}}",
-			which: 4, // 0 is the func body, 1 is the select statement, 2 is its body
-			want:  "package p; func f() { switch {default: }}",
-			name:  "case0",
-		},
-		{
-			in:    "package p; func f() { switch {case 3: a()}}",
-			which: 4, // 0 is the func body, 1 is the select statement, 2 is its body
-			want:  "package p; func f() { switch {case 3: }}",
-			name:  "case1",
-		},
-		{
-			in:    "package p; func f() {for a();;b() {}}",
-			which: 2,
-			want:  "package p; func f() {for ;;b() {}}",
-			name:  "for0",
-		},
-		{
-			in:    "package p; func f() {for a();c();b() {}}",
-			which: 3,
-			want:  "package p; func f() {for a();c(); {}}",
-			name:  "for1",
-		},
-		{
-			in:    "package p; func f() {for\na();c()\nb() {}}",
-			which: 2,
-			want:  "package p; func f() {for\n;c()\nb() {}}",
-			name:  "for2",
-		},
-		{
-			in:    "package p; func f() {for a();\nc();b() {}}",
-			which: 3,
-			want:  "package p; func f() {for a();\nc(); {}}",
-			name:  "for3",
-		},
-		{
-			in:    "package p; func f() {switch a();b().(type){}}",
-			which: 2,
-			want:  "package p; func f() {switch ;b().(type){}}",
-			name:  "type0",
-		},
-		{
-			in:    "package p; func f() {switch a();b().(type){}}",
-			which: 3,
-			want:  "package p; func f() {switch a();b().(type){}}",
-			name:  "type1",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			fset := token.NewFileSet()
-			f, err := parser.ParseFile(fset, tt.name, tt.in, parser.ParseComments)
-			if err != nil {
-				t.Fatalf("%s: %v", tt.name, err)
-			}
-			insp := inspector.New([]*ast.File{f})
-			root := insp.Root()
-			var stmt inspector.Cursor
-			cnt := 0
-			for cn := range root.Preorder() { // Preorder(ast.Stmt(nil)) doesn't work
-				if _, ok := cn.Node().(ast.Stmt); !ok {
-					continue
-				}
-				if cnt == tt.which {
-					stmt = cn
-					break
-				}
-				cnt++
-			}
-			if cnt != tt.which {
-				t.Fatalf("test %s does not contain desired statement %d", tt.name, tt.which)
-			}
-			tokFile := fset.File(f.Pos())
-			edits := DeleteStmt(tokFile, stmt)
-			if tt.want == tt.in {
-				if len(edits) != 0 {
-					t.Fatalf("%s: got %d edits, expected 0", tt.name, len(edits))
-				}
-				return
-			}
-			if len(edits) != 1 {
-				t.Fatalf("%s: got %d edits, expected 1", tt.name, len(edits))
-			}
-
-			left := tokFile.Offset(edits[0].Pos)
-			right := tokFile.Offset(edits[0].End)
-
-			got := tt.in[:left] + tt.in[right:]
-			if got != tt.want {
-				t.Errorf("%s: got\n%q, want\n%q", tt.name, got, tt.want)
-			}
-		})
-
-	}
-}
-
-func TestComments(t *testing.T) {
-	src := `
-package main
-
-// A
-func fn() { }`
-	var fset token.FileSet
-	f, err := parser.ParseFile(&fset, "", []byte(src), parser.ParseComments|parser.AllErrors)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	commentA := f.Comments[0].List[0]
-	commentAMidPos := (commentA.Pos() + commentA.End()) / 2
-
-	want := []*ast.Comment{commentA}
+func TestIsStdPackage(t *testing.T) {
 	testCases := []struct {
-		name       string
-		start, end token.Pos
-		want       []*ast.Comment
+		pkgpath string
+		isStd   bool
 	}{
-		{name: "comment totally overlaps with given interval", start: f.Pos(), end: f.End(), want: want},
-		{name: "interval from file start to mid of comment A", start: f.Pos(), end: commentAMidPos, want: want},
-		{name: "interval from mid of comment A to file end", start: commentAMidPos, end: commentA.End(), want: want},
-		{name: "interval from start of comment A to mid of comment A", start: commentA.Pos(), end: commentAMidPos, want: want},
-		{name: "interval from mid of comment A to comment A end", start: commentAMidPos, end: commentA.End(), want: want},
-		{name: "interval at the start of comment A", start: commentA.Pos(), end: commentA.Pos(), want: want},
-		{name: "interval at the end of comment A", start: commentA.End(), end: commentA.End(), want: want},
-		{name: "interval from file start to the front of comment A start", start: f.Pos(), end: commentA.Pos() - 1, want: nil},
-		{name: "interval from the position after end of comment A to file end", start: commentA.End() + 1, end: f.End(), want: nil},
+		{pkgpath: "os", isStd: true},
+		{pkgpath: "net/http", isStd: true},
+		{pkgpath: "vendor/golang.org/x/net/dns/dnsmessage", isStd: true},
+		{pkgpath: "golang.org/x/net/dns/dnsmessage", isStd: false},
+		{pkgpath: "testdata", isStd: false},
 	}
+
 	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			var got []*ast.Comment
-			for co := range Comments(f, tc.start, tc.end) {
-				got = append(got, co)
-			}
-			if !slices.Equal(got, tc.want) {
-				t.Errorf("%s: got %v, want %v", tc.name, got, tc.want)
+		t.Run(tc.pkgpath, func(t *testing.T) {
+			got := analysisinternal.IsStdPackage(tc.pkgpath)
+			if got != tc.isStd {
+				t.Fatalf("got %t want %t", got, tc.isStd)
 			}
 		})
 	}
diff --git a/internal/analysisinternal/deletevar_test.go b/internal/analysisinternal/deletevar_test.go
deleted file mode 100644
index 738aa0f..0000000
--- a/internal/analysisinternal/deletevar_test.go
+++ /dev/null
@@ -1,341 +0,0 @@
-// Copyright 2025 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 analysisinternal_test
-
-import (
-	"bytes"
-	"fmt"
-	"go/ast"
-	"go/format"
-	"go/parser"
-	"go/token"
-	"go/types"
-	"slices"
-	"testing"
-
-	"golang.org/x/tools/go/ast/inspector"
-	"golang.org/x/tools/internal/analysisinternal"
-	"golang.org/x/tools/internal/diff"
-)
-
-func TestDeleteVar(t *testing.T) {
-	// Each example deletes var v.
-	for i, test := range []struct {
-		src  string
-		want string
-	}{
-		// package-level GenDecl > ValueSpec
-		{
-			"package p; var v int",
-			"package p; ",
-		},
-		{
-			"package p; var x, v int",
-			"package p; var x int",
-		},
-		{
-			"package p; var v, x int",
-			"package p; var x int",
-		},
-		{
-			"package p; var ( v int )",
-			"package p;",
-		},
-		{
-			"package p; var ( x, v int )",
-			"package p; var ( x int )",
-		},
-		{
-			"package p; var ( v, x int )",
-			"package p; var ( x int )",
-		},
-		{
-			"package p; var v, x = 1, 2",
-			"package p; var x = 2",
-		},
-		{
-			"package p; var x, v = 1, 2",
-			"package p; var x = 1",
-		},
-		{
-			"package p; var v, x = fx(), fx()",
-			"package p; var _, x = fx(), fx()",
-		},
-		{
-			"package p; var v, _ = fx(), fx()",
-			"package p; var _, _ = fx(), fx()",
-		},
-		{
-			"package p; var _, v = fx(), fx()",
-			"package p; var _, _ = fx(), fx()",
-		},
-		{
-			"package p; var v = fx()",
-			"package p; var _ = fx()",
-		},
-		{
-			"package p; var ( a int; v int; c int )",
-			"package p; var ( a int; c int )",
-		},
-		{
-			"package p; var ( a int; v int = 2; c int )",
-			"package p; var ( a int; c int )",
-		},
-		// GenDecl doc comments are not deleted unless decl is deleted.
-		{
-			"package p\n// comment\nvar ( v int )",
-			"package p",
-		},
-		{
-			"package p\n// comment\nvar v int",
-			"package p",
-		},
-		{
-			"package p\n/* comment */\nvar v int",
-			"package p",
-		},
-		{
-			"package p\n// comment\nvar ( v, x int )",
-			"package p\n// comment\nvar ( x int )",
-		},
-		{
-			"package p\n// comment\nvar v, x int",
-			"package p\n// comment\nvar x int",
-		},
-		{
-			"package p\n/* comment */\nvar x, v int",
-			"package p\n/* comment */\nvar x int",
-		},
-		// ValueSpec leading doc comments
-		{
-			"package p\nvar (\n// comment\nv int; x int )",
-			"package p\nvar (\nx int )",
-		},
-		{
-			"package p\nvar (\n// comment\nx int; v int )",
-			"package p\nvar (\n// comment\nx int )",
-		},
-		// ValueSpec trailing line comments
-		{
-			"package p; var ( v int // comment\nx int )",
-			"package p; var ( x int )",
-		},
-		{
-			"package p; var ( x int // comment\nv int )",
-			"package p; var ( x int // comment\n )",
-		},
-		{
-			"package p; var ( v int /* comment */)",
-			"package p;",
-		},
-		{
-			"package p; var ( v int // comment\n)",
-			"package p;",
-		},
-		{
-			"package p; var ( v int ) // comment",
-			"package p;",
-		},
-		{
-			"package p; var ( x, v int /* comment */ )",
-			"package p; var ( x int /* comment */ )",
-		},
-		{
-			"package p; var ( v, x int /* comment */ )",
-			"package p; var ( x int /* comment */ )",
-		},
-		{
-			"package p; var ( x, v int // comment\n)",
-			"package p; var ( x int // comment\n)",
-		},
-		{
-			"package p; var ( v, x int // comment\n)",
-			"package p; var ( x int // comment\n)",
-		},
-		{
-			"package p; var ( v, x int ) // comment",
-			"package p; var ( x int ) // comment",
-		},
-		{
-			"package p; var ( x int; v int // comment\n)",
-			"package p; var ( x int )",
-		},
-		{
-			"package p; var ( v int // comment\n x int )",
-			"package p; var ( x int )",
-		},
-		// local DeclStmt > GenDecl > ValueSpec
-		// (The only interesting cases
-		// here are the total deletions.)
-		{
-			"package p; func _() { var v int }",
-			"package p; func _() {}",
-		},
-		{
-			"package p; func _() { var ( v int ) }",
-			"package p; func _() {}",
-		},
-		{
-			"package p; func _() { var ( v int // comment\n) }",
-			"package p; func _() {}",
-		},
-		// TODO(adonovan,pjw): change DeleteStmt's trailing comment handling.
-		// {
-		// 	"package p; func _() { var ( v int ) // comment\n }",
-		// 	"package p; func _() {}",
-		// },
-		// {
-		// 	"package p; func _() { var v int // comment\n }",
-		// 	"package p; func _() {}",
-		// },
-		// AssignStmt
-		{
-			"package p; func _() { v := 0 }",
-			"package p; func _() {}",
-		},
-		{
-			"package p; func _() { x, v := 0, 1 }",
-			"package p; func _() { x := 0 }",
-		},
-		{
-			"package p; func _() { v, x := 0, 1 }",
-			"package p; func _() { x := 1 }",
-		},
-		{
-			"package p; func _() { v, x := f() }",
-			"package p; func _() { _, x := f() }",
-		},
-		{
-			"package p; func _() { v, x := fx(), fx() }",
-			"package p; func _() { _, x := fx(), fx() }",
-		},
-		{
-			"package p; func _() { v, _ := fx(), fx() }",
-			"package p; func _() { _, _ = fx(), fx() }",
-		},
-		{
-			"package p; func _() { _, v := fx(), fx() }",
-			"package p; func _() { _, _ = fx(), fx() }",
-		},
-		{
-			"package p; func _() { v := fx() }",
-			"package p; func _() { _ = fx() }",
-		},
-		// TODO(adonovan,pjw): change DeleteStmt's trailing comment handling.
-		// {
-		// 	"package p; func _() { v := 1 // comment\n }",
-		// 	"package p; func _() {}",
-		// },
-		{
-			"package p; func _() { v, x := 0, 1 // comment\n }",
-			"package p; func _() { x := 1 // comment\n }",
-		},
-		{
-			"package p; func _() { if v := 1; cond {} }", // (DeleteStmt fails within IfStmt)
-			"package p; func _() { if _ = 1; cond {} }",
-		},
-		{
-			"package p; func _() { if v, x := 1, 2; cond {} }",
-			"package p; func _() { if x := 2; cond {} }",
-		},
-		{
-			"package p; func _() { switch v := 0; cond {} }",
-			"package p; func _() { switch cond {} }",
-		},
-		{
-			"package p; func _() { switch v := fx(); cond {} }",
-			"package p; func _() { switch _ = fx(); cond {} }",
-		},
-		{
-			"package p; func _() { for v := 0; ; {} }",
-			"package p; func _() { for {} }",
-		},
-		// unhandled cases
-		{
-			"package p; func _(v int) {}", // parameter
-			"package p; func _(v int) {}",
-		},
-		{
-			"package p; func _() (v int) {}", // result
-			"package p; func _() (v int) {}",
-		},
-		{
-			"package p; type T int; func _(v T) {}", // receiver
-			"package p; type T int; func _(v T) {}",
-		},
-		// There is no defining Ident in this case.
-		// {
-		// 	"package p; func _() { switch v := any(nil).(type) {} }",
-		// 	"package p; func _() { switch v := any(nil).(type) {} }",
-		// },
-	} {
-		t.Run(fmt.Sprint(i), func(t *testing.T) {
-			t.Logf("src: %s", test.src)
-			fset := token.NewFileSet()
-			f, _ := parser.ParseFile(fset, "p", test.src, parser.ParseComments) // allow errors
-			conf := types.Config{
-				Error: func(err error) {}, // allow errors
-			}
-			info := &types.Info{
-				Types: make(map[ast.Expr]types.TypeAndValue),
-				Defs:  make(map[*ast.Ident]types.Object),
-			}
-			files := []*ast.File{f}
-			conf.Check("p", fset, files, info) // ignore error
-
-			curId := func() inspector.Cursor {
-				for curId := range inspector.New(files).Root().Preorder((*ast.Ident)(nil)) {
-					id := curId.Node().(*ast.Ident)
-					if id.Name == "v" && info.Defs[id] != nil {
-						return curId
-					}
-				}
-				t.Fatalf("can't find Defs[v]")
-				panic("unreachable")
-			}()
-			tokFile := fset.File(f.Pos())
-			edits := analysisinternal.DeleteVar(tokFile, info, curId)
-
-			// TODO(adonovan): extract this helper for
-			// applying TextEdits and comparing against
-			// expectations. (This code was mostly copied
-			// from analysistest.)
-			var dedits []diff.Edit
-			for _, edit := range edits {
-				file := fset.File(edit.Pos)
-				dedits = append(dedits, diff.Edit{
-					Start: file.Offset(edit.Pos),
-					End:   file.Offset(edit.End),
-					New:   string(edit.NewText),
-				})
-			}
-			fixed, err := diff.ApplyBytes([]byte(test.src), dedits)
-			if err != nil {
-				t.Fatalf("diff.Apply: %v", err)
-			}
-			t.Logf("fixed: %s", fixed)
-			fixed, err = format.Source(fixed)
-			if err != nil {
-				t.Fatalf("format: %v", err)
-			}
-			want, err := format.Source([]byte(test.want))
-			if err != nil {
-				t.Fatalf("formatting want: %v", err)
-			}
-			t.Logf("want: %s", want)
-			unified := func(xlabel, ylabel string, x, y []byte) string {
-				x = append(slices.Clip(bytes.TrimSpace(x)), '\n')
-				y = append(slices.Clip(bytes.TrimSpace(y)), '\n')
-				return diff.Unified(xlabel, ylabel, string(x), string(y))
-			}
-			if diff := unified("fixed", "want", fixed, want); diff != "" {
-				t.Errorf("-- diff original fixed --\n%s\n"+
-					"-- diff fixed want --\n%s",
-					unified("original", "fixed", []byte(test.src), fixed),
-					diff)
-			}
-		})
-	}
-}
diff --git a/internal/astutil/comment.go b/internal/astutil/comment.go
index c3a256c..7e52aea 100644
--- a/internal/astutil/comment.go
+++ b/internal/astutil/comment.go
@@ -7,6 +7,7 @@
 import (
 	"go/ast"
 	"go/token"
+	"iter"
 	"strings"
 )
 
@@ -111,3 +112,24 @@
 	}
 	return
 }
+
+// Comments returns an iterator over the comments overlapping the specified interval.
+func Comments(file *ast.File, start, end token.Pos) iter.Seq[*ast.Comment] {
+	// TODO(adonovan): optimize use binary O(log n) instead of linear O(n) search.
+	return func(yield func(*ast.Comment) bool) {
+		for _, cg := range file.Comments {
+			for _, co := range cg.List {
+				if co.Pos() > end {
+					return
+				}
+				if co.End() < start {
+					continue
+				}
+
+				if !yield(co) {
+					return
+				}
+			}
+		}
+	}
+}
diff --git a/internal/astutil/comment_test.go b/internal/astutil/comment_test.go
new file mode 100644
index 0000000..bb4c432
--- /dev/null
+++ b/internal/astutil/comment_test.go
@@ -0,0 +1,59 @@
+// Copyright 2025 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 astutil_test
+
+import (
+	"go/ast"
+	"go/parser"
+	"go/token"
+	"slices"
+	"testing"
+
+	"golang.org/x/tools/internal/astutil"
+)
+
+func TestComments(t *testing.T) {
+	src := `
+package main
+
+// A
+func fn() { }`
+	var fset token.FileSet
+	f, err := parser.ParseFile(&fset, "", []byte(src), parser.ParseComments|parser.AllErrors)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	commentA := f.Comments[0].List[0]
+	commentAMidPos := (commentA.Pos() + commentA.End()) / 2
+
+	want := []*ast.Comment{commentA}
+	testCases := []struct {
+		name       string
+		start, end token.Pos
+		want       []*ast.Comment
+	}{
+		{name: "comment totally overlaps with given interval", start: f.Pos(), end: f.End(), want: want},
+		{name: "interval from file start to mid of comment A", start: f.Pos(), end: commentAMidPos, want: want},
+		{name: "interval from mid of comment A to file end", start: commentAMidPos, end: commentA.End(), want: want},
+		{name: "interval from start of comment A to mid of comment A", start: commentA.Pos(), end: commentAMidPos, want: want},
+		{name: "interval from mid of comment A to comment A end", start: commentAMidPos, end: commentA.End(), want: want},
+		{name: "interval at the start of comment A", start: commentA.Pos(), end: commentA.Pos(), want: want},
+		{name: "interval at the end of comment A", start: commentA.End(), end: commentA.End(), want: want},
+		{name: "interval from file start to the front of comment A start", start: f.Pos(), end: commentA.Pos() - 1, want: nil},
+		{name: "interval from the position after end of comment A to file end", start: commentA.End() + 1, end: f.End(), want: nil},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var got []*ast.Comment
+			for co := range astutil.Comments(f, tc.start, tc.end) {
+				got = append(got, co)
+			}
+			if !slices.Equal(got, tc.want) {
+				t.Errorf("%s: got %v, want %v", tc.name, got, tc.want)
+			}
+		})
+	}
+}
diff --git a/internal/astutil/util.go b/internal/astutil/util.go
index 1418915..a1c0983 100644
--- a/internal/astutil/util.go
+++ b/internal/astutil/util.go
@@ -6,7 +6,13 @@
 
 import (
 	"go/ast"
+	"go/printer"
 	"go/token"
+	"strings"
+
+	"golang.org/x/tools/go/ast/edge"
+	"golang.org/x/tools/go/ast/inspector"
+	"golang.org/x/tools/internal/moreiters"
 )
 
 // PreorderStack traverses the tree rooted at root,
@@ -67,3 +73,47 @@
 	}
 	return start <= pos && pos <= end
 }
+
+// IsChildOf reports whether cur.ParentEdge is ek.
+//
+// TODO(adonovan): promote to a method of Cursor.
+func IsChildOf(cur inspector.Cursor, ek edge.Kind) bool {
+	got, _ := cur.ParentEdge()
+	return got == ek
+}
+
+// EnclosingFile returns the syntax tree for the file enclosing c.
+//
+// TODO(adonovan): promote this to a method of Cursor.
+func EnclosingFile(c inspector.Cursor) *ast.File {
+	c, _ = moreiters.First(c.Enclosing((*ast.File)(nil)))
+	return c.Node().(*ast.File)
+}
+
+// DocComment returns the doc comment for a node, if any.
+func DocComment(n ast.Node) *ast.CommentGroup {
+	switch n := n.(type) {
+	case *ast.FuncDecl:
+		return n.Doc
+	case *ast.GenDecl:
+		return n.Doc
+	case *ast.ValueSpec:
+		return n.Doc
+	case *ast.TypeSpec:
+		return n.Doc
+	case *ast.File:
+		return n.Doc
+	case *ast.ImportSpec:
+		return n.Doc
+	case *ast.Field:
+		return n.Doc
+	}
+	return nil
+}
+
+// Format returns a string representation of the node n.
+func Format(fset *token.FileSet, n ast.Node) string {
+	var buf strings.Builder
+	printer.Fprint(&buf, fset, n) // ignore errors
+	return buf.String()
+}
diff --git a/internal/analysisinternal/deletevar.go b/internal/refactor/delete.go
similarity index 67%
rename from internal/analysisinternal/deletevar.go
rename to internal/refactor/delete.go
index f81db49..aa8ba5a 100644
--- a/internal/analysisinternal/deletevar.go
+++ b/internal/refactor/delete.go
@@ -2,7 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package analysisinternal
+package refactor
+
+// This file defines operations for computing deletion edits.
 
 import (
 	"fmt"
@@ -14,6 +16,7 @@
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/ast/edge"
 	"golang.org/x/tools/go/ast/inspector"
+	"golang.org/x/tools/internal/astutil"
 	"golang.org/x/tools/internal/typesinternal"
 )
 
@@ -278,7 +281,7 @@
 
 // DeleteDecl returns edits to delete the ast.Decl identified by curDecl.
 //
-// TODO(adonovan): add test suite. Test for consts as well.
+// TODO(adonovan): add test suite.
 func DeleteDecl(tokFile *token.File, curDecl inspector.Cursor) []analysis.TextEdit {
 	decl := curDecl.Node().(ast.Decl)
 
@@ -289,7 +292,7 @@
 
 	case edge.File_Decls:
 		pos, end := decl.Pos(), decl.End()
-		if doc := docComment(decl); doc != nil {
+		if doc := astutil.DocComment(decl); doc != nil {
 			pos = doc.Pos()
 		}
 
@@ -324,20 +327,107 @@
 	}
 }
 
-// docComment returns the doc comment for a node, if any.
-//
-// TODO(adonovan): we have 5 copies of this in x/tools.
-// Share it in typesinternal.
-func docComment(n ast.Node) *ast.CommentGroup {
-	switch n := n.(type) {
-	case *ast.FuncDecl:
-		return n.Doc
-	case *ast.GenDecl:
-		return n.Doc
-	case *ast.ValueSpec:
-		return n.Doc
-	case *ast.TypeSpec:
-		return n.Doc
+// DeleteStmt returns the edits to remove the [ast.Stmt] identified by
+// curStmt, if it is contained within a BlockStmt, CaseClause,
+// CommClause, or is the STMT in switch STMT; ... {...}. It returns nil otherwise.
+func DeleteStmt(tokFile *token.File, curStmt inspector.Cursor) []analysis.TextEdit {
+	stmt := curStmt.Node().(ast.Stmt)
+	// if the stmt is on a line by itself delete the whole line
+	// otherwise just delete the statement.
+
+	// this logic would be a lot simpler with the file contents, and somewhat simpler
+	// if the cursors included the comments.
+
+	lineOf := tokFile.Line
+	stmtStartLine, stmtEndLine := lineOf(stmt.Pos()), lineOf(stmt.End())
+
+	var from, to token.Pos
+	// bounds of adjacent syntax/comments on same line, if any
+	limits := func(left, right token.Pos) {
+		if lineOf(left) == stmtStartLine {
+			from = left
+		}
+		if lineOf(right) == stmtEndLine {
+			to = right
+		}
 	}
-	return nil // includes File, ImportSpec, Field
+	// TODO(pjw): there are other places a statement might be removed:
+	// IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
+	// (removing the blocks requires more rewriting than this routine would do)
+	// CommCase   = "case" ( SendStmt | RecvStmt ) | "default" .
+	// (removing the stmt requires more rewriting, and it's unclear what the user means)
+	switch parent := curStmt.Parent().Node().(type) {
+	case *ast.SwitchStmt:
+		limits(parent.Switch, parent.Body.Lbrace)
+	case *ast.TypeSwitchStmt:
+		limits(parent.Switch, parent.Body.Lbrace)
+		if parent.Assign == stmt {
+			return nil // don't let the user break the type switch
+		}
+	case *ast.BlockStmt:
+		limits(parent.Lbrace, parent.Rbrace)
+	case *ast.CommClause:
+		limits(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
+		if parent.Comm == stmt {
+			return nil // maybe the user meant to remove the entire CommClause?
+		}
+	case *ast.CaseClause:
+		limits(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
+	case *ast.ForStmt:
+		limits(parent.For, parent.Body.Lbrace)
+
+	default:
+		return nil // not one of ours
+	}
+
+	if prev, found := curStmt.PrevSibling(); found && lineOf(prev.Node().End()) == stmtStartLine {
+		from = prev.Node().End() // preceding statement ends on same line
+	}
+	if next, found := curStmt.NextSibling(); found && lineOf(next.Node().Pos()) == stmtEndLine {
+		to = next.Node().Pos() // following statement begins on same line
+	}
+	// and now for the comments
+Outer:
+	for _, cg := range astutil.EnclosingFile(curStmt).Comments {
+		for _, co := range cg.List {
+			if lineOf(co.End()) < stmtStartLine {
+				continue
+			} else if lineOf(co.Pos()) > stmtEndLine {
+				break Outer // no more are possible
+			}
+			if lineOf(co.End()) == stmtStartLine && co.End() < stmt.Pos() {
+				if !from.IsValid() || co.End() > from {
+					from = co.End()
+					continue // maybe there are more
+				}
+			}
+			if lineOf(co.Pos()) == stmtEndLine && co.Pos() > stmt.End() {
+				if !to.IsValid() || co.Pos() < to {
+					to = co.Pos()
+					continue // maybe there are more
+				}
+			}
+		}
+	}
+	// if either from or to is valid, just remove the statement
+	// otherwise remove the line
+	edit := analysis.TextEdit{Pos: stmt.Pos(), End: stmt.End()}
+	if from.IsValid() || to.IsValid() {
+		// remove just the statement.
+		// we can't tell if there is a ; or whitespace right after the statement
+		// ideally we'd like to remove the former and leave the latter
+		// (if gofmt has run, there likely won't be a ;)
+		// In type switches we know there's a semicolon somewhere after the statement,
+		// but the extra work for this special case is not worth it, as gofmt will fix it.
+		return []analysis.TextEdit{edit}
+	}
+	// remove the whole line
+	for lineOf(edit.Pos) == stmtStartLine {
+		edit.Pos--
+	}
+	edit.Pos++ // get back tostmtStartLine
+	for lineOf(edit.End) == stmtEndLine {
+		edit.End++
+	}
+	return []analysis.TextEdit{edit}
 }
diff --git a/internal/refactor/delete_test.go b/internal/refactor/delete_test.go
new file mode 100644
index 0000000..2fa2294
--- /dev/null
+++ b/internal/refactor/delete_test.go
@@ -0,0 +1,554 @@
+// Copyright 2025 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 refactor_test
+
+import (
+	"bytes"
+	"fmt"
+	"go/ast"
+	"go/format"
+	"go/parser"
+	"go/token"
+	"go/types"
+	"slices"
+	"testing"
+
+	"golang.org/x/tools/go/ast/inspector"
+	"golang.org/x/tools/internal/diff"
+	"golang.org/x/tools/internal/refactor"
+)
+
+func TestDeleteStmt(t *testing.T) {
+	type testCase struct {
+		in    string
+		which int // count of ast.Stmt in ast.Inspect traversal to remove
+		want  string
+		name  string // should contain exactly one of [block,switch,case,comm,for,type]
+	}
+	tests := []testCase{
+		{ // do nothing when asked to remove a function body
+			in:    "package p; func f() {  }",
+			which: 0,
+			want:  "package p; func f() {  }",
+			name:  "block0",
+		},
+		{
+			in:    "package p; func f() { abcd()}",
+			which: 1,
+			want:  "package p; func f() { }",
+			name:  "block1",
+		},
+		{
+			in:    "package p; func f() { a() }",
+			which: 1,
+			want:  "package p; func f() {  }",
+			name:  "block2",
+		},
+		{
+			in:    "package p; func f() { a();}",
+			which: 1,
+			want:  "package p; func f() { ;}",
+			name:  "block3",
+		},
+		{
+			in:    "package p; func f() {\n a() \n\n}",
+			which: 1,
+			want:  "package p; func f() {\n\n}",
+			name:  "block4",
+		},
+		{
+			in:    "package p; func f() { a()// comment\n}",
+			which: 1,
+			want:  "package p; func f() { // comment\n}",
+			name:  "block5",
+		},
+		{
+			in:    "package p; func f() { /*c*/a() \n}",
+			which: 1,
+			want:  "package p; func f() { /*c*/ \n}",
+			name:  "block6",
+		},
+		{
+			in:    "package p; func f() { a();b();}",
+			which: 2,
+			want:  "package p; func f() { a();;}",
+			name:  "block7",
+		},
+		{
+			in:    "package p; func f() {\n\ta()\n\tb()\n}",
+			which: 2,
+			want:  "package p; func f() {\n\ta()\n}",
+			name:  "block8",
+		},
+		{
+			in:    "package p; func f() {\n\ta()\n\tb()\n\tc()\n}",
+			which: 2,
+			want:  "package p; func f() {\n\ta()\n\tc()\n}",
+			name:  "block9",
+		},
+		{
+			in:    "package p\nfunc f() {a()+b()}",
+			which: 1,
+			want:  "package p\nfunc f() {}",
+			name:  "block10",
+		},
+		{
+			in:    "package p\nfunc f() {(a()+b())}",
+			which: 1,
+			want:  "package p\nfunc f() {}",
+			name:  "block11",
+		},
+		{
+			in:    "package p; func f() { switch a(); b() {}}",
+			which: 2, // 0 is the func body, 1 is the switch statement
+			want:  "package p; func f() { switch ; b() {}}",
+			name:  "switch0",
+		},
+		{
+			in:    "package p; func f() { switch /*c*/a(); {}}",
+			which: 2, // 0 is the func body, 1 is the switch statement
+			want:  "package p; func f() { switch /*c*/; {}}",
+			name:  "switch1",
+		},
+		{
+			in:    "package p; func f() { switch a()/*c*/; {}}",
+			which: 2, // 0 is the func body, 1 is the switch statement
+			want:  "package p; func f() { switch /*c*/; {}}",
+			name:  "switch2",
+		},
+		{
+			in:    "package p; func f() { select {default: a()}}",
+			which: 4, // 0 is the func body, 1 is the select statement, 2 is its body, 3 is the comm clause
+			want:  "package p; func f() { select {default: }}",
+			name:  "comm0",
+		},
+		{
+			in:    "package p; func f(x chan any) { select {case x <- a: a(x)}}",
+			which: 5, // 0 is the func body, 1 is the select statement, 2 is its body, 3 is the comm clause
+			want:  "package p; func f(x chan any) { select {case x <- a: }}",
+			name:  "comm1",
+		},
+		{
+			in:    "package p; func f(x chan any) { select {case x <- a: a(x)}}",
+			which: 4, // 0 is the func body, 1 is the select statement, 2 is its body, 3 is the comm clause
+			want:  "package p; func f(x chan any) { select {case x <- a: a(x)}}",
+			name:  "comm2",
+		},
+		{
+			in:    "package p; func f() { switch {default: a()}}",
+			which: 4, // 0 is the func body, 1 is the select statement, 2 is its body
+			want:  "package p; func f() { switch {default: }}",
+			name:  "case0",
+		},
+		{
+			in:    "package p; func f() { switch {case 3: a()}}",
+			which: 4, // 0 is the func body, 1 is the select statement, 2 is its body
+			want:  "package p; func f() { switch {case 3: }}",
+			name:  "case1",
+		},
+		{
+			in:    "package p; func f() {for a();;b() {}}",
+			which: 2,
+			want:  "package p; func f() {for ;;b() {}}",
+			name:  "for0",
+		},
+		{
+			in:    "package p; func f() {for a();c();b() {}}",
+			which: 3,
+			want:  "package p; func f() {for a();c(); {}}",
+			name:  "for1",
+		},
+		{
+			in:    "package p; func f() {for\na();c()\nb() {}}",
+			which: 2,
+			want:  "package p; func f() {for\n;c()\nb() {}}",
+			name:  "for2",
+		},
+		{
+			in:    "package p; func f() {for a();\nc();b() {}}",
+			which: 3,
+			want:  "package p; func f() {for a();\nc(); {}}",
+			name:  "for3",
+		},
+		{
+			in:    "package p; func f() {switch a();b().(type){}}",
+			which: 2,
+			want:  "package p; func f() {switch ;b().(type){}}",
+			name:  "type0",
+		},
+		{
+			in:    "package p; func f() {switch a();b().(type){}}",
+			which: 3,
+			want:  "package p; func f() {switch a();b().(type){}}",
+			name:  "type1",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			fset := token.NewFileSet()
+			f, err := parser.ParseFile(fset, tt.name, tt.in, parser.ParseComments)
+			if err != nil {
+				t.Fatalf("%s: %v", tt.name, err)
+			}
+			insp := inspector.New([]*ast.File{f})
+			root := insp.Root()
+			var stmt inspector.Cursor
+			cnt := 0
+			for cn := range root.Preorder() { // Preorder(ast.Stmt(nil)) doesn't work
+				if _, ok := cn.Node().(ast.Stmt); !ok {
+					continue
+				}
+				if cnt == tt.which {
+					stmt = cn
+					break
+				}
+				cnt++
+			}
+			if cnt != tt.which {
+				t.Fatalf("test %s does not contain desired statement %d", tt.name, tt.which)
+			}
+			tokFile := fset.File(f.Pos())
+			edits := refactor.DeleteStmt(tokFile, stmt)
+			if tt.want == tt.in {
+				if len(edits) != 0 {
+					t.Fatalf("%s: got %d edits, expected 0", tt.name, len(edits))
+				}
+				return
+			}
+			if len(edits) != 1 {
+				t.Fatalf("%s: got %d edits, expected 1", tt.name, len(edits))
+			}
+
+			left := tokFile.Offset(edits[0].Pos)
+			right := tokFile.Offset(edits[0].End)
+
+			got := tt.in[:left] + tt.in[right:]
+			if got != tt.want {
+				t.Errorf("%s: got\n%q, want\n%q", tt.name, got, tt.want)
+			}
+		})
+
+	}
+}
+
+func TestDeleteVar(t *testing.T) {
+	// Each example deletes var v.
+	for i, test := range []struct {
+		src  string
+		want string
+	}{
+		// package-level GenDecl > ValueSpec
+		{
+			"package p; var v int",
+			"package p; ",
+		},
+		{
+			"package p; var x, v int",
+			"package p; var x int",
+		},
+		{
+			"package p; var v, x int",
+			"package p; var x int",
+		},
+		{
+			"package p; var ( v int )",
+			"package p;",
+		},
+		{
+			"package p; var ( x, v int )",
+			"package p; var ( x int )",
+		},
+		{
+			"package p; var ( v, x int )",
+			"package p; var ( x int )",
+		},
+		{
+			"package p; var v, x = 1, 2",
+			"package p; var x = 2",
+		},
+		{
+			"package p; var x, v = 1, 2",
+			"package p; var x = 1",
+		},
+		{
+			"package p; var v, x = fx(), fx()",
+			"package p; var _, x = fx(), fx()",
+		},
+		{
+			"package p; var v, _ = fx(), fx()",
+			"package p; var _, _ = fx(), fx()",
+		},
+		{
+			"package p; var _, v = fx(), fx()",
+			"package p; var _, _ = fx(), fx()",
+		},
+		{
+			"package p; var v = fx()",
+			"package p; var _ = fx()",
+		},
+		{
+			"package p; var ( a int; v int; c int )",
+			"package p; var ( a int; c int )",
+		},
+		{
+			"package p; var ( a int; v int = 2; c int )",
+			"package p; var ( a int; c int )",
+		},
+		// GenDecl doc comments are not deleted unless decl is deleted.
+		{
+			"package p\n// comment\nvar ( v int )",
+			"package p",
+		},
+		{
+			"package p\n// comment\nvar v int",
+			"package p",
+		},
+		{
+			"package p\n/* comment */\nvar v int",
+			"package p",
+		},
+		{
+			"package p\n// comment\nvar ( v, x int )",
+			"package p\n// comment\nvar ( x int )",
+		},
+		{
+			"package p\n// comment\nvar v, x int",
+			"package p\n// comment\nvar x int",
+		},
+		{
+			"package p\n/* comment */\nvar x, v int",
+			"package p\n/* comment */\nvar x int",
+		},
+		// ValueSpec leading doc comments
+		{
+			"package p\nvar (\n// comment\nv int; x int )",
+			"package p\nvar (\nx int )",
+		},
+		{
+			"package p\nvar (\n// comment\nx int; v int )",
+			"package p\nvar (\n// comment\nx int )",
+		},
+		// ValueSpec trailing line comments
+		{
+			"package p; var ( v int // comment\nx int )",
+			"package p; var ( x int )",
+		},
+		{
+			"package p; var ( x int // comment\nv int )",
+			"package p; var ( x int // comment\n )",
+		},
+		{
+			"package p; var ( v int /* comment */)",
+			"package p;",
+		},
+		{
+			"package p; var ( v int // comment\n)",
+			"package p;",
+		},
+		{
+			"package p; var ( v int ) // comment",
+			"package p;",
+		},
+		{
+			"package p; var ( x, v int /* comment */ )",
+			"package p; var ( x int /* comment */ )",
+		},
+		{
+			"package p; var ( v, x int /* comment */ )",
+			"package p; var ( x int /* comment */ )",
+		},
+		{
+			"package p; var ( x, v int // comment\n)",
+			"package p; var ( x int // comment\n)",
+		},
+		{
+			"package p; var ( v, x int // comment\n)",
+			"package p; var ( x int // comment\n)",
+		},
+		{
+			"package p; var ( v, x int ) // comment",
+			"package p; var ( x int ) // comment",
+		},
+		{
+			"package p; var ( x int; v int // comment\n)",
+			"package p; var ( x int )",
+		},
+		{
+			"package p; var ( v int // comment\n x int )",
+			"package p; var ( x int )",
+		},
+		// local DeclStmt > GenDecl > ValueSpec
+		// (The only interesting cases
+		// here are the total deletions.)
+		{
+			"package p; func _() { var v int }",
+			"package p; func _() {}",
+		},
+		{
+			"package p; func _() { var ( v int ) }",
+			"package p; func _() {}",
+		},
+		{
+			"package p; func _() { var ( v int // comment\n) }",
+			"package p; func _() {}",
+		},
+		// TODO(adonovan,pjw): change DeleteStmt's trailing comment handling.
+		// {
+		// 	"package p; func _() { var ( v int ) // comment\n }",
+		// 	"package p; func _() {}",
+		// },
+		// {
+		// 	"package p; func _() { var v int // comment\n }",
+		// 	"package p; func _() {}",
+		// },
+		// AssignStmt
+		{
+			"package p; func _() { v := 0 }",
+			"package p; func _() {}",
+		},
+		{
+			"package p; func _() { x, v := 0, 1 }",
+			"package p; func _() { x := 0 }",
+		},
+		{
+			"package p; func _() { v, x := 0, 1 }",
+			"package p; func _() { x := 1 }",
+		},
+		{
+			"package p; func _() { v, x := f() }",
+			"package p; func _() { _, x := f() }",
+		},
+		{
+			"package p; func _() { v, x := fx(), fx() }",
+			"package p; func _() { _, x := fx(), fx() }",
+		},
+		{
+			"package p; func _() { v, _ := fx(), fx() }",
+			"package p; func _() { _, _ = fx(), fx() }",
+		},
+		{
+			"package p; func _() { _, v := fx(), fx() }",
+			"package p; func _() { _, _ = fx(), fx() }",
+		},
+		{
+			"package p; func _() { v := fx() }",
+			"package p; func _() { _ = fx() }",
+		},
+		// TODO(adonovan,pjw): change DeleteStmt's trailing comment handling.
+		// {
+		// 	"package p; func _() { v := 1 // comment\n }",
+		// 	"package p; func _() {}",
+		// },
+		{
+			"package p; func _() { v, x := 0, 1 // comment\n }",
+			"package p; func _() { x := 1 // comment\n }",
+		},
+		{
+			"package p; func _() { if v := 1; cond {} }", // (DeleteStmt fails within IfStmt)
+			"package p; func _() { if _ = 1; cond {} }",
+		},
+		{
+			"package p; func _() { if v, x := 1, 2; cond {} }",
+			"package p; func _() { if x := 2; cond {} }",
+		},
+		{
+			"package p; func _() { switch v := 0; cond {} }",
+			"package p; func _() { switch cond {} }",
+		},
+		{
+			"package p; func _() { switch v := fx(); cond {} }",
+			"package p; func _() { switch _ = fx(); cond {} }",
+		},
+		{
+			"package p; func _() { for v := 0; ; {} }",
+			"package p; func _() { for {} }",
+		},
+		// unhandled cases
+		{
+			"package p; func _(v int) {}", // parameter
+			"package p; func _(v int) {}",
+		},
+		{
+			"package p; func _() (v int) {}", // result
+			"package p; func _() (v int) {}",
+		},
+		{
+			"package p; type T int; func _(v T) {}", // receiver
+			"package p; type T int; func _(v T) {}",
+		},
+		// There is no defining Ident in this case.
+		// {
+		// 	"package p; func _() { switch v := any(nil).(type) {} }",
+		// 	"package p; func _() { switch v := any(nil).(type) {} }",
+		// },
+	} {
+		t.Run(fmt.Sprint(i), func(t *testing.T) {
+			t.Logf("src: %s", test.src)
+			fset := token.NewFileSet()
+			f, _ := parser.ParseFile(fset, "p", test.src, parser.ParseComments) // allow errors
+			conf := types.Config{
+				Error: func(err error) {}, // allow errors
+			}
+			info := &types.Info{
+				Types: make(map[ast.Expr]types.TypeAndValue),
+				Defs:  make(map[*ast.Ident]types.Object),
+			}
+			files := []*ast.File{f}
+			conf.Check("p", fset, files, info) // ignore error
+
+			curId := func() inspector.Cursor {
+				for curId := range inspector.New(files).Root().Preorder((*ast.Ident)(nil)) {
+					id := curId.Node().(*ast.Ident)
+					if id.Name == "v" && info.Defs[id] != nil {
+						return curId
+					}
+				}
+				t.Fatalf("can't find Defs[v]")
+				panic("unreachable")
+			}()
+			tokFile := fset.File(f.Pos())
+			edits := refactor.DeleteVar(tokFile, info, curId)
+
+			// TODO(adonovan): extract this helper for
+			// applying TextEdits and comparing against
+			// expectations. (This code was mostly copied
+			// from analysistest.)
+			var dedits []diff.Edit
+			for _, edit := range edits {
+				file := fset.File(edit.Pos)
+				dedits = append(dedits, diff.Edit{
+					Start: file.Offset(edit.Pos),
+					End:   file.Offset(edit.End),
+					New:   string(edit.NewText),
+				})
+			}
+			fixed, err := diff.ApplyBytes([]byte(test.src), dedits)
+			if err != nil {
+				t.Fatalf("diff.Apply: %v", err)
+			}
+			t.Logf("fixed: %s", fixed)
+			fixed, err = format.Source(fixed)
+			if err != nil {
+				t.Fatalf("format: %v", err)
+			}
+			want, err := format.Source([]byte(test.want))
+			if err != nil {
+				t.Fatalf("formatting want: %v", err)
+			}
+			t.Logf("want: %s", want)
+			unified := func(xlabel, ylabel string, x, y []byte) string {
+				x = append(slices.Clip(bytes.TrimSpace(x)), '\n')
+				y = append(slices.Clip(bytes.TrimSpace(y)), '\n')
+				return diff.Unified(xlabel, ylabel, string(x), string(y))
+			}
+			if diff := unified("fixed", "want", fixed, want); diff != "" {
+				t.Errorf("-- diff original fixed --\n%s\n"+
+					"-- diff fixed want --\n%s",
+					unified("original", "fixed", []byte(test.src), fixed),
+					diff)
+			}
+		})
+	}
+}
diff --git a/internal/refactor/imports.go b/internal/refactor/imports.go
new file mode 100644
index 0000000..e52567c
--- /dev/null
+++ b/internal/refactor/imports.go
@@ -0,0 +1,130 @@
+// Copyright 2025 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 refactor
+
+// This file defines operations for computing edits to imports.
+
+import (
+	"fmt"
+	"go/ast"
+	"go/token"
+	"go/types"
+	pathpkg "path"
+
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/internal/analysisinternal"
+)
+
+// AddImport checks whether this file already imports pkgpath and that
+// the import is in scope at pos. If so, it returns the name under
+// which it was imported and no edits. Otherwise, it adds a new import
+// of pkgpath, using a name derived from the preferred name, and
+// returns the chosen name, a prefix to be concatenated with member to
+// form a qualified name, and the edit for the new import.
+//
+// The member argument indicates the name of the desired symbol within
+// the imported package. This is needed in the case when the existing
+// import is a dot import, because then it is possible that the
+// desired symbol is shadowed by other declarations in the current
+// package. If member is not shadowed at pos, AddImport returns (".",
+// "", nil). (AddImport accepts the caller's implicit claim that the
+// imported package declares member.)
+//
+// Use a preferredName of "_" to request a blank import;
+// member is ignored in this case.
+//
+// It does not mutate its arguments.
+//
+// TODO(adonovan): needs dedicated tests.
+func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member string, pos token.Pos) (name, prefix string, newImport []analysis.TextEdit) {
+	// Find innermost enclosing lexical block.
+	scope := info.Scopes[file].Innermost(pos)
+	if scope == nil {
+		panic("no enclosing lexical block")
+	}
+
+	// Is there an existing import of this package?
+	// If so, are we in its scope? (not shadowed)
+	for _, spec := range file.Imports {
+		pkgname := info.PkgNameOf(spec)
+		if pkgname != nil && pkgname.Imported().Path() == pkgpath {
+			name = pkgname.Name()
+			if preferredName == "_" {
+				// Request for blank import; any existing import will do.
+				return name, "", nil
+			}
+			if name == "." {
+				// The scope of ident must be the file scope.
+				if s, _ := scope.LookupParent(member, pos); s == info.Scopes[file] {
+					return name, "", nil
+				}
+			} else if _, obj := scope.LookupParent(name, pos); obj == pkgname {
+				return name, name + ".", nil
+			}
+		}
+	}
+
+	// We must add a new import.
+
+	// Ensure we have a fresh name.
+	newName := preferredName
+	if preferredName != "_" {
+		newName = FreshName(scope, pos, preferredName)
+	}
+
+	// Create a new import declaration either before the first existing
+	// declaration (which must exist), including its comments; or
+	// inside the declaration, if it is an import group.
+	//
+	// Use a renaming import whenever the preferred name is not
+	// available, or the chosen name does not match the last
+	// segment of its path.
+	newText := fmt.Sprintf("%q", pkgpath)
+	if newName != preferredName || newName != pathpkg.Base(pkgpath) {
+		newText = fmt.Sprintf("%s %q", newName, pkgpath)
+	}
+
+	decl0 := file.Decls[0]
+	var before ast.Node = decl0
+	switch decl0 := decl0.(type) {
+	case *ast.GenDecl:
+		if decl0.Doc != nil {
+			before = decl0.Doc
+		}
+	case *ast.FuncDecl:
+		if decl0.Doc != nil {
+			before = decl0.Doc
+		}
+	}
+	if gd, ok := before.(*ast.GenDecl); ok && gd.Tok == token.IMPORT && gd.Rparen.IsValid() {
+		// Have existing grouped import ( ... ) decl.
+		if analysisinternal.IsStdPackage(pkgpath) && len(gd.Specs) > 0 {
+			// Add spec for a std package before
+			// first existing spec, followed by
+			// a blank line if the next one is non-std.
+			first := gd.Specs[0].(*ast.ImportSpec)
+			pos = first.Pos()
+			if !analysisinternal.IsStdPackage(first.Path.Value) {
+				newText += "\n"
+			}
+			newText += "\n\t"
+		} else {
+			// Add spec at end of group.
+			pos = gd.Rparen
+			newText = "\t" + newText + "\n"
+		}
+	} else {
+		// No import decl, or non-grouped import.
+		// Add a new import decl before first decl.
+		// (gofmt will merge multiple import decls.)
+		pos = before.Pos()
+		newText = "import " + newText + "\n\n"
+	}
+	return newName, newName + ".", []analysis.TextEdit{{
+		Pos:     pos,
+		End:     pos,
+		NewText: []byte(newText),
+	}}
+}
diff --git a/internal/analysisinternal/addimport_test.go b/internal/refactor/imports_test.go
similarity index 88%
rename from internal/analysisinternal/addimport_test.go
rename to internal/refactor/imports_test.go
index 05971b9..db40c25 100644
--- a/internal/analysisinternal/addimport_test.go
+++ b/internal/refactor/imports_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-package analysisinternal_test
+package refactor_test
 
 import (
 	"fmt"
@@ -17,7 +17,7 @@
 
 	"github.com/google/go-cmp/cmp"
 	"golang.org/x/tools/go/analysis"
-	"golang.org/x/tools/internal/analysisinternal"
+	"golang.org/x/tools/internal/refactor"
 	"golang.org/x/tools/internal/testenv"
 )
 
@@ -357,7 +357,7 @@
 
 			// add import
 			// The "Print" argument is only relevant for dot-import tests.
-			name, prefix, edits := analysisinternal.AddImport(info, f, name, path, "Print", pos)
+			name, prefix, edits := refactor.AddImport(info, f, name, path, "Print", pos)
 
 			var edit analysis.TextEdit
 			switch len(edits) {
@@ -389,25 +389,3 @@
 		})
 	}
 }
-
-func TestIsStdPackage(t *testing.T) {
-	testCases := []struct {
-		pkgpath string
-		isStd   bool
-	}{
-		{pkgpath: "os", isStd: true},
-		{pkgpath: "net/http", isStd: true},
-		{pkgpath: "vendor/golang.org/x/net/dns/dnsmessage", isStd: true},
-		{pkgpath: "golang.org/x/net/dns/dnsmessage", isStd: false},
-		{pkgpath: "testdata", isStd: false},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.pkgpath, func(t *testing.T) {
-			got := analysisinternal.IsStdPackage(tc.pkgpath)
-			if got != tc.isStd {
-				t.Fatalf("got %t want %t", got, tc.isStd)
-			}
-		})
-	}
-}
diff --git a/internal/refactor/refactor.go b/internal/refactor/refactor.go
new file mode 100644
index 0000000..27b9750
--- /dev/null
+++ b/internal/refactor/refactor.go
@@ -0,0 +1,29 @@
+// Copyright 2025 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 refactor provides operators to compute common textual edits
+// for refactoring tools.
+//
+// This package should not use features of the analysis API
+// other than [analysis.TextEdit].
+package refactor
+
+import (
+	"fmt"
+	"go/token"
+	"go/types"
+)
+
+// FreshName returns the name of an identifier that is undefined
+// at the specified position, based on the preferred name.
+func FreshName(scope *types.Scope, pos token.Pos, preferred string) string {
+	newName := preferred
+	for i := 0; ; i++ {
+		if _, obj := scope.LookupParent(newName, pos); obj == nil {
+			break // fresh
+		}
+		newName = fmt.Sprintf("%s%d", preferred, i)
+	}
+	return newName
+}
diff --git a/internal/typesinternal/isnamed.go b/internal/typesinternal/isnamed.go
new file mode 100644
index 0000000..f2affec
--- /dev/null
+++ b/internal/typesinternal/isnamed.go
@@ -0,0 +1,71 @@
+// Copyright 2025 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 typesinternal
+
+import (
+	"go/types"
+	"slices"
+)
+
+// IsTypeNamed reports whether t is (or is an alias for) a
+// package-level defined type with the given package path and one of
+// the given names. It returns false if t is nil.
+//
+// This function avoids allocating the concatenation of "pkg.Name",
+// which is important for the performance of syntax matching.
+func IsTypeNamed(t types.Type, pkgPath string, names ...string) bool {
+	if named, ok := types.Unalias(t).(*types.Named); ok {
+		tname := named.Obj()
+		return tname != nil &&
+			IsPackageLevel(tname) &&
+			tname.Pkg().Path() == pkgPath &&
+			slices.Contains(names, tname.Name())
+	}
+	return false
+}
+
+// IsPointerToNamed reports whether t is (or is an alias for) a pointer to a
+// package-level defined type with the given package path and one of the given
+// names. It returns false if t is not a pointer type.
+func IsPointerToNamed(t types.Type, pkgPath string, names ...string) bool {
+	r := Unpointer(t)
+	if r == t {
+		return false
+	}
+	return IsTypeNamed(r, pkgPath, names...)
+}
+
+// IsFunctionNamed reports whether obj is a package-level function
+// defined in the given package and has one of the given names.
+// It returns false if obj is nil.
+//
+// This function avoids allocating the concatenation of "pkg.Name",
+// which is important for the performance of syntax matching.
+func IsFunctionNamed(obj types.Object, pkgPath string, names ...string) bool {
+	f, ok := obj.(*types.Func)
+	return ok &&
+		IsPackageLevel(obj) &&
+		f.Pkg().Path() == pkgPath &&
+		f.Type().(*types.Signature).Recv() == nil &&
+		slices.Contains(names, f.Name())
+}
+
+// IsMethodNamed reports whether obj is a method defined on a
+// package-level type with the given package and type name, and has
+// one of the given names. It returns false if obj is nil.
+//
+// This function avoids allocating the concatenation of "pkg.TypeName.Name",
+// which is important for the performance of syntax matching.
+func IsMethodNamed(obj types.Object, pkgPath string, typeName string, names ...string) bool {
+	if fn, ok := obj.(*types.Func); ok {
+		if recv := fn.Type().(*types.Signature).Recv(); recv != nil {
+			_, T := ReceiverNamed(recv)
+			return T != nil &&
+				IsTypeNamed(T, pkgPath, typeName) &&
+				slices.Contains(names, fn.Name())
+		}
+	}
+	return false
+}
diff --git a/internal/typesinternal/types.go b/internal/typesinternal/types.go
index a5cd7e8..fef74a7 100644
--- a/internal/typesinternal/types.go
+++ b/internal/typesinternal/types.go
@@ -2,8 +2,20 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package typesinternal provides access to internal go/types APIs that are not
-// yet exported.
+// Package typesinternal provides helpful operators for dealing with
+// go/types:
+//
+//   - operators for querying typed syntax trees (e.g. [Imports], [IsFunctionNamed]);
+//   - functions for converting types to strings or syntax (e.g. [TypeExpr], FileQualifier]);
+//   - helpers for working with the [go/types] API (e.g. [NewTypesInfo]);
+//   - access to internal go/types APIs that are not yet
+//     exported (e.g. [SetUsesCgo], [ErrorCodeStartEnd], [VarKind]); and
+//   - common algorithms related to types (e.g. [TooNewStdSymbols]).
+//
+// See also:
+//   - [golang.org/x/tools/internal/astutil], for operations on untyped syntax;
+//   - [golang.org/x/tools/internal/analysisinernal], for helpers for analyzers;
+//   - [golang.org/x/tools/internal/refactor], for operators to compute text edits.
 package typesinternal
 
 import (
@@ -13,6 +25,7 @@
 	"reflect"
 	"unsafe"
 
+	"golang.org/x/tools/go/ast/inspector"
 	"golang.org/x/tools/internal/aliases"
 )
 
@@ -60,6 +73,9 @@
 // which is often excessive.)
 //
 // If pkg is nil, it is equivalent to [*types.Package.Name].
+//
+// TODO(adonovan): all uses of this with TypeString should be
+// eliminated when https://go.dev/issues/75604 is resolved.
 func NameRelativeTo(pkg *types.Package) types.Qualifier {
 	return func(other *types.Package) string {
 		if pkg != nil && pkg == other {
@@ -153,3 +169,31 @@
 		FileVersions: map[*ast.File]string{},
 	}
 }
+
+// EnclosingScope returns the innermost block logically enclosing the cursor.
+func EnclosingScope(info *types.Info, cur inspector.Cursor) *types.Scope {
+	for cur := range cur.Enclosing() {
+		n := cur.Node()
+		// A function's Scope is associated with its FuncType.
+		switch f := n.(type) {
+		case *ast.FuncDecl:
+			n = f.Type
+		case *ast.FuncLit:
+			n = f.Type
+		}
+		if b := info.Scopes[n]; b != nil {
+			return b
+		}
+	}
+	panic("no Scope for *ast.File")
+}
+
+// Imports reports whether path is imported by pkg.
+func Imports(pkg *types.Package, path string) bool {
+	for _, imp := range pkg.Imports() {
+		if imp.Path() == path {
+			return true
+		}
+	}
+	return false
+}
diff --git a/internal/typesinternal/zerovalue.go b/internal/typesinternal/zerovalue.go
index d272949..453bba2 100644
--- a/internal/typesinternal/zerovalue.go
+++ b/internal/typesinternal/zerovalue.go
@@ -204,23 +204,12 @@
 	}
 }
 
-// IsZeroExpr uses simple syntactic heuristics to report whether expr
-// is a obvious zero value, such as 0, "", nil, or false.
-// It cannot do better without type information.
-func IsZeroExpr(expr ast.Expr) bool {
-	switch e := expr.(type) {
-	case *ast.BasicLit:
-		return e.Value == "0" || e.Value == `""`
-	case *ast.Ident:
-		return e.Name == "nil" || e.Name == "false"
-	default:
-		return false
-	}
-}
-
 // TypeExpr returns syntax for the specified type. References to named types
 // are qualified by an appropriate (optional) qualifier function.
 // It may panic for types such as Tuple or Union.
+//
+// See also https://go.dev/issues/75604, which will provide a robust
+// Type-to-valid-Go-syntax formatter.
 func TypeExpr(t types.Type, qual types.Qualifier) ast.Expr {
 	switch t := t.(type) {
 	case *types.Basic: