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: