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: