github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5
golang.org/x/arch v0.20.1-0.20250808194827-46ba08e3ae58
golang.org/x/build v0.0.0-20250806225920-b7c66c047964
- golang.org/x/mod v0.28.0
+ golang.org/x/mod v0.29.0
golang.org/x/sync v0.17.0
- golang.org/x/sys v0.36.0
- golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053
+ golang.org/x/sys v0.37.0
+ golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8
golang.org/x/term v0.34.0
- golang.org/x/tools v0.37.1-0.20250924232827-4df13e317ce4
+ golang.org/x/tools v0.38.1-0.20251015192825-7d9453ccc0f5
)
require (
github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b // indirect
- golang.org/x/text v0.29.0 // indirect
+ golang.org/x/text v0.30.0 // indirect
rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef // indirect
)
golang.org/x/arch v0.20.1-0.20250808194827-46ba08e3ae58/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/build v0.0.0-20250806225920-b7c66c047964 h1:yRs1K51GKq7hsIO+YHJ8LsslrvwFceNPIv0tYjpcBd0=
golang.org/x/build v0.0.0-20250806225920-b7c66c047964/go.mod h1:i9Vx7+aOQUpYJRxSO+OpRStVBCVL/9ccI51xblWm5WY=
-golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
-golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
+golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
+golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
-golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
-golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8=
-golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
+golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
-golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
-golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
-golang.org/x/tools v0.37.1-0.20250924232827-4df13e317ce4 h1:IcXDtHggZZo+GzNzvVRPyNFLnOc2/Z1gg3ZVIWF2uCU=
-golang.org/x/tools v0.37.1-0.20250924232827-4df13e317ce4/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/tools v0.38.1-0.20251015192825-7d9453ccc0f5 h1:cz7f45KGWAtyIrz6bm45Gc+lw8beIxBSW3EQh4Bwbg4=
+golang.org/x/tools v0.38.1-0.20251015192825-7d9453ccc0f5/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef h1:mqLYrXCXYEZOop9/Dbo6RPX11539nwiCNBb1icVPmw8=
rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef/go.mod h1:8xcPgWmwlZONN1D9bjxtHEjrUtSEa3fakVF8iaewYKQ=
clear(s[:])
}
+// Fill adds all possible CPU bits to the set s. On Linux, [SchedSetaffinity]
+// will silently ignore any invalid CPU bits in [CPUSet] so this is an
+// efficient way of resetting the CPU affinity of a process.
+func (s *CPUSet) Fill() {
+ for i := range s {
+ s[i] = ^cpuMask(0)
+ }
+}
+
func cpuBitsIndex(cpu int) int {
return cpu / _NCPUBITS
}
// Zero clears the set fds.
func (fds *FdSet) Zero() {
- for i := range fds.Bits {
- fds.Bits[i] = 0
- }
+ clear(fds.Bits[:])
}
// clear zeroes the ifreq's union field to prevent trailing garbage data from
// being sent to the kernel if an ifreq is reused.
func (ifr *Ifreq) clear() {
- for i := range ifr.raw.Ifru {
- ifr.raw.Ifru[i] = 0
- }
+ clear(ifr.raw.Ifru[:])
}
// TODO(mdlayher): export as IfreqData? For now we can provide helpers such as
if [[ "$GOOS" = "linux" ]]; then
# Use the Docker-based build system
# Files generated through docker (use $cmd so you can Ctl-C the build or run)
+ set -e
$cmd docker build --tag generate:$GOOS $GOOS
$cmd docker run --interactive --tty --volume $(cd -- "$(dirname -- "$0")/.." && pwd):/build generate:$GOOS
exit
// one. The kernel expects SID to be in network byte order.
binary.BigEndian.PutUint16(sa.raw[6:8], sa.SID)
copy(sa.raw[8:14], sa.Remote)
- for i := 14; i < 14+IFNAMSIZ; i++ {
- sa.raw[i] = 0
- }
+ clear(sa.raw[14 : 14+IFNAMSIZ])
copy(sa.raw[14:], sa.Dev)
return unsafe.Pointer(&sa.raw), SizeofSockaddrPPPoX, nil
}
return Statvfs1(path, buf, ST_WAIT)
}
+func Getvfsstat(buf []Statvfs_t, flags int) (n int, err error) {
+ var (
+ _p0 unsafe.Pointer
+ bufsize uintptr
+ )
+ if len(buf) > 0 {
+ _p0 = unsafe.Pointer(&buf[0])
+ bufsize = unsafe.Sizeof(Statvfs_t{}) * uintptr(len(buf))
+ }
+ r0, _, e1 := Syscall(SYS_GETVFSSTAT, uintptr(_p0), bufsize, uintptr(flags))
+ n = int(r0)
+ if e1 != 0 {
+ err = e1
+ }
+ return
+}
+
/*
* Exposed directly
*/
//sys SetConsoleOutputCP(cp uint32) (err error) = kernel32.SetConsoleOutputCP
//sys WriteConsole(console Handle, buf *uint16, towrite uint32, written *uint32, reserved *byte) (err error) = kernel32.WriteConsoleW
//sys ReadConsole(console Handle, buf *uint16, toread uint32, read *uint32, inputControl *byte) (err error) = kernel32.ReadConsoleW
+//sys GetNumberOfConsoleInputEvents(console Handle, numevents *uint32) (err error) = kernel32.GetNumberOfConsoleInputEvents
+//sys FlushConsoleInputBuffer(console Handle) (err error) = kernel32.FlushConsoleInputBuffer
//sys resizePseudoConsole(pconsole Handle, size uint32) (hr error) = kernel32.ResizePseudoConsole
//sys CreateToolhelp32Snapshot(flags uint32, processId uint32) (handle Handle, err error) [failretval==InvalidHandle] = kernel32.CreateToolhelp32Snapshot
//sys Module32First(snapshot Handle, moduleEntry *ModuleEntry32) (err error) = kernel32.Module32FirstW
15: "terminated",
}
+// File flags for [os.OpenFile]. The O_ prefix is used to indicate
+// that these flags are specific to the OpenFile function.
+const (
+ O_FILE_FLAG_OPEN_NO_RECALL = FILE_FLAG_OPEN_NO_RECALL
+ O_FILE_FLAG_OPEN_REPARSE_POINT = FILE_FLAG_OPEN_REPARSE_POINT
+ O_FILE_FLAG_SESSION_AWARE = FILE_FLAG_SESSION_AWARE
+ O_FILE_FLAG_POSIX_SEMANTICS = FILE_FLAG_POSIX_SEMANTICS
+ O_FILE_FLAG_BACKUP_SEMANTICS = FILE_FLAG_BACKUP_SEMANTICS
+ O_FILE_FLAG_DELETE_ON_CLOSE = FILE_FLAG_DELETE_ON_CLOSE
+ O_FILE_FLAG_SEQUENTIAL_SCAN = FILE_FLAG_SEQUENTIAL_SCAN
+ O_FILE_FLAG_RANDOM_ACCESS = FILE_FLAG_RANDOM_ACCESS
+ O_FILE_FLAG_NO_BUFFERING = FILE_FLAG_NO_BUFFERING
+ O_FILE_FLAG_OVERLAPPED = FILE_FLAG_OVERLAPPED
+ O_FILE_FLAG_WRITE_THROUGH = FILE_FLAG_WRITE_THROUGH
+)
+
const (
FILE_READ_DATA = 0x00000001
FILE_READ_ATTRIBUTES = 0x00000080
procFindResourceW = modkernel32.NewProc("FindResourceW")
procFindVolumeClose = modkernel32.NewProc("FindVolumeClose")
procFindVolumeMountPointClose = modkernel32.NewProc("FindVolumeMountPointClose")
+ procFlushConsoleInputBuffer = modkernel32.NewProc("FlushConsoleInputBuffer")
procFlushFileBuffers = modkernel32.NewProc("FlushFileBuffers")
procFlushViewOfFile = modkernel32.NewProc("FlushViewOfFile")
procFormatMessageW = modkernel32.NewProc("FormatMessageW")
procGetNamedPipeHandleStateW = modkernel32.NewProc("GetNamedPipeHandleStateW")
procGetNamedPipeInfo = modkernel32.NewProc("GetNamedPipeInfo")
procGetNamedPipeServerProcessId = modkernel32.NewProc("GetNamedPipeServerProcessId")
+ procGetNumberOfConsoleInputEvents = modkernel32.NewProc("GetNumberOfConsoleInputEvents")
procGetOverlappedResult = modkernel32.NewProc("GetOverlappedResult")
procGetPriorityClass = modkernel32.NewProc("GetPriorityClass")
procGetProcAddress = modkernel32.NewProc("GetProcAddress")
return
}
+func FlushConsoleInputBuffer(console Handle) (err error) {
+ r1, _, e1 := syscall.SyscallN(procFlushConsoleInputBuffer.Addr(), uintptr(console))
+ if r1 == 0 {
+ err = errnoErr(e1)
+ }
+ return
+}
+
func FlushFileBuffers(handle Handle) (err error) {
r1, _, e1 := syscall.SyscallN(procFlushFileBuffers.Addr(), uintptr(handle))
if r1 == 0 {
return
}
+func GetNumberOfConsoleInputEvents(console Handle, numevents *uint32) (err error) {
+ r1, _, e1 := syscall.SyscallN(procGetNumberOfConsoleInputEvents.Addr(), uintptr(console), uintptr(unsafe.Pointer(numevents)))
+ if r1 == 0 {
+ err = errnoErr(e1)
+ }
+ return
+}
+
func GetOverlappedResult(handle Handle, overlapped *Overlapped, done *uint32, wait bool) (err error) {
var _p0 uint32
if wait {
}
return pcs, nil
}
-
-func min(x, y int) int {
- if x < y {
- return x
- } else {
- return y
- }
-}
const help = `PROGNAME is a tool for static analysis of Go programs.
-PROGNAME examines Go source code and reports suspicious constructs,
-such as Printf calls whose arguments do not align with the format
-string. It uses heuristics that do not guarantee all reports are
-genuine problems, but it can find errors not caught by the compilers.
+PROGNAME examines Go source code and reports diagnostics for
+suspicious constructs or opportunities for improvement.
+Diagnostics may include suggested fixes.
+
+An example of a suspicious construct is a Printf call whose arguments
+do not align with the format string. Analyzers may use heuristics that
+do not guarantee all reports are genuine problems, but can find
+mistakes not caught by the compiler.
+
+An example of an opportunity for improvement is a loop over
+strings.Split(doc, "\n"), which may be replaced by a loop over the
+strings.SplitSeq iterator, avoiding an array allocation.
+Diagnostics in such cases may report non-problems,
+but should carry fixes that may be safely applied.
+
+For analyzers of the first kind, use "go vet -vettool=PROGRAM"
+to run the tool and report diagnostics.
+
+For analyzers of the second kind, use "go fix -fixtool=PROGRAM"
+to run the tool and apply the fixes it suggests.
`
// Help implements the help subcommand for a multichecker or unitchecker
func Help(progname string, analyzers []*analysis.Analyzer, args []string) {
// No args: show summary of all analyzers.
if len(args) == 0 {
- fmt.Println(strings.Replace(help, "PROGNAME", progname, -1))
+ fmt.Println(strings.ReplaceAll(help, "PROGNAME", progname))
fmt.Println("Registered analyzers:")
fmt.Println()
sort.Slice(analyzers, func(i, j int) bool {
"golang.org/x/tools/go/analysis"
"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/go/types/typeutil"
+ "golang.org/x/tools/internal/analysisinternal"
)
//go:embed doc.go
var Analyzer = &analysis.Analyzer{
Name: "appends",
- Doc: analysisutil.MustExtractDoc(doc, "appends"),
+ Doc: analysisinternal.MustExtractDoc(doc, "appends"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/appends",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
"strings"
"golang.org/x/tools/go/analysis"
- "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/internal/analysisinternal"
)
const Doc = "report mismatches between assembly files and Go declarations"
Files:
for _, fname := range sfiles {
- content, tf, err := analysisutil.ReadFile(pass, fname)
+ content, tf, err := analysisinternal.ReadFile(pass, fname)
if err != nil {
return nil, err
}
resultStr = "result register"
}
for _, line := range retLine {
- pass.Reportf(analysisutil.LineStart(tf, line), "[%s] %s: RET without writing to %s", arch, fnName, resultStr)
+ pass.Reportf(tf.LineStart(line), "[%s] %s: RET without writing to %s", arch, fnName, resultStr)
}
}
retLine = nil
lineno++
badf := func(format string, args ...any) {
- pass.Reportf(analysisutil.LineStart(tf, lineno), "[%s] %s: %s", arch, fnName, fmt.Sprintf(format, args...))
+ pass.Reportf(tf.LineStart(lineno), "[%s] %s: %s", arch, fnName, fmt.Sprintf(format, args...))
}
if arch == "" {
"golang.org/x/tools/go/analysis"
"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"
+ "golang.org/x/tools/internal/refactor"
+ "golang.org/x/tools/internal/typesinternal"
)
//go:embed doc.go
var Analyzer = &analysis.Analyzer{
Name: "assign",
- Doc: analysisutil.MustExtractDoc(doc, "assign"),
+ Doc: analysisinternal.MustExtractDoc(doc, "assign"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/assign",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
func run(pass *analysis.Pass) (any, error) {
- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ var (
+ inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ info = pass.TypesInfo
+ )
- nodeFilter := []ast.Node{
- (*ast.AssignStmt)(nil),
- }
- inspect.Preorder(nodeFilter, func(n ast.Node) {
- stmt := n.(*ast.AssignStmt)
+ for curAssign := range inspect.Root().Preorder((*ast.AssignStmt)(nil)) {
+ stmt := curAssign.Node().(*ast.AssignStmt)
if stmt.Tok != token.ASSIGN {
- return // ignore :=
+ continue // ignore :=
}
if len(stmt.Lhs) != len(stmt.Rhs) {
// If LHS and RHS have different cardinality, they can't be the same.
- return
+ continue
}
// Delete redundant LHS, RHS pairs, taking care
isSelfAssign := false
var le string
- if !analysisutil.HasSideEffects(pass.TypesInfo, lhs) &&
- !analysisutil.HasSideEffects(pass.TypesInfo, rhs) &&
- !isMapIndex(pass.TypesInfo, lhs) &&
+ if typesinternal.NoEffects(info, lhs) &&
+ typesinternal.NoEffects(info, rhs) &&
+ !isMapIndex(info, 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
}
}
if len(exprs) == 0 {
- return
+ continue
}
if len(exprs) == len(stmt.Lhs) {
// If every part of the statement is a self-assignment,
// remove the whole statement.
- edits = []analysis.TextEdit{{Pos: stmt.Pos(), End: stmt.End()}}
+ tokFile := pass.Fset.File(stmt.Pos())
+ edits = refactor.DeleteStmt(tokFile, curAssign)
}
pass.Report(analysis.Diagnostic{
TextEdits: edits,
}},
})
- })
+ }
return nil, nil
}
"golang.org/x/tools/go/analysis"
"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/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
var Analyzer = &analysis.Analyzer{
Name: "atomic",
- Doc: analysisutil.MustExtractDoc(doc, "atomic"),
+ Doc: analysisinternal.MustExtractDoc(doc, "atomic"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/atomic",
Requires: []*analysis.Analyzer{inspect.Analyzer},
RunDespiteErrors: true,
}
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
}
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)
}
}
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)
"golang.org/x/tools/go/analysis"
"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"
+ "golang.org/x/tools/internal/typesinternal"
)
const Doc = "check for common mistakes involving boolean operators"
i := 0
var sets [][]ast.Expr
for j := 0; j <= len(exprs); j++ {
- if j == len(exprs) || analysisutil.HasSideEffects(info, exprs[j]) {
+ if j == len(exprs) || !typesinternal.NoEffects(info, exprs[j]) {
if i < j {
sets = append(sets, exprs[i:j])
}
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 {
}
// 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 {
"unicode"
"golang.org/x/tools/go/analysis"
- "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/internal/analysisinternal"
)
const Doc = "check //go:build and // +build directives"
// We cannot use the Go parser, since this may not be a Go source file.
// Read the raw bytes instead.
- content, tf, err := analysisutil.ReadFile(pass, filename)
+ content, tf, err := analysisinternal.ReadFile(pass, filename)
if err != nil {
return err
}
"strconv"
"golang.org/x/tools/go/analysis"
- "golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/typesinternal"
)
const debug = false
}
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
}
"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"
)
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.
}
}
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)
}
}
}
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)
}
}
}
}
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)
}
}
}
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)
}
}
// 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()}
}
"golang.org/x/tools/go/analysis"
"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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/typesinternal"
)
//go:embed doc.go
var Analyzer = &analysis.Analyzer{
Name: "defers",
Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Doc: analysisinternal.MustExtractDoc(doc, "defers"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/defers",
- Doc: analysisutil.MustExtractDoc(doc, "defers"),
Run: run,
}
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:
"unicode/utf8"
"golang.org/x/tools/go/analysis"
- "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/internal/analysisinternal"
)
const Doc = `check Go toolchain directives such as //go:debug
func checkOtherFile(pass *analysis.Pass, filename string) error {
// We cannot use the Go parser, since is not a Go source file.
// Read the raw bytes instead.
- content, tf, err := analysisutil.ReadFile(pass, filename)
+ content, tf, err := analysisinternal.ReadFile(pass, filename)
if err != nil {
return err
}
"go/types"
"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/go/types/typeutil"
- "golang.org/x/tools/internal/analysisinternal"
+ typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
)
const Doc = `report passing non-pointer or non-error values to errors.As
-The errorsas analysis reports calls to errors.As where the type
+The errorsas analyzer reports calls to errors.As where the type
of the second argument is not a pointer to a type implementing error.`
var Analyzer = &analysis.Analyzer{
Name: "errorsas",
Doc: Doc,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/errorsas",
- Requires: []*analysis.Analyzer{inspect.Analyzer},
+ Requires: []*analysis.Analyzer{typeindexanalyzer.Analyzer},
Run: run,
}
return nil, nil
}
- if !analysisinternal.Imports(pass.Pkg, "errors") {
- return nil, nil // doesn't directly import errors
- }
-
- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ var (
+ index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
+ info = pass.TypesInfo
+ )
- nodeFilter := []ast.Node{
- (*ast.CallExpr)(nil),
- }
- inspect.Preorder(nodeFilter, func(n ast.Node) {
- call := n.(*ast.CallExpr)
- obj := typeutil.Callee(pass.TypesInfo, call)
- if !analysisinternal.IsFunctionNamed(obj, "errors", "As") {
- return
- }
+ for curCall := range index.Calls(index.Object("errors", "As")) {
+ call := curCall.Node().(*ast.CallExpr)
if len(call.Args) < 2 {
- return // not enough arguments, e.g. called with return values of another function
+ continue // spread call: errors.As(pair())
}
- if err := checkAsTarget(pass, call.Args[1]); err != nil {
+
+ // Check for incorrect arguments.
+ if err := checkAsTarget(info, call.Args[1]); err != nil {
pass.ReportRangef(call, "%v", err)
+ continue
}
- })
+ }
return nil, nil
}
-var errorType = types.Universe.Lookup("error").Type()
-
// checkAsTarget reports an error if the second argument to errors.As is invalid.
-func checkAsTarget(pass *analysis.Pass, e ast.Expr) error {
- t := pass.TypesInfo.Types[e].Type
- if it, ok := t.Underlying().(*types.Interface); ok && it.NumMethods() == 0 {
- // A target of interface{} is always allowed, since it often indicates
+func checkAsTarget(info *types.Info, e ast.Expr) error {
+ t := info.Types[e].Type
+ if types.Identical(t.Underlying(), anyType) {
+ // A target of any is always allowed, since it often indicates
// a value forwarded from another source.
return nil
}
if !ok {
return errors.New("second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type")
}
- if pt.Elem() == errorType {
+ if types.Identical(pt.Elem(), errorType) {
return errors.New("second argument to errors.As should not be *error")
}
- _, ok = pt.Elem().Underlying().(*types.Interface)
- if ok || types.Implements(pt.Elem(), errorType.Underlying().(*types.Interface)) {
- return nil
+ if !types.IsInterface(pt.Elem()) && !types.AssignableTo(pt.Elem(), errorType) {
+ return errors.New("second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type")
}
- return errors.New("second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type")
+ return nil
}
+
+var (
+ anyType = types.Universe.Lookup("any").Type()
+ errorType = types.Universe.Lookup("error").Type()
+)
"unicode"
"golang.org/x/tools/go/analysis"
- "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/internal/analysisinternal"
)
const Doc = "report assembly that clobbers the frame pointer before saving it"
}
for _, fname := range sfiles {
- content, tf, err := analysisutil.ReadFile(pass, fname)
+ content, tf, err := analysisinternal.ReadFile(pass, fname)
if err != nil {
return nil, err
}
}
if arch.isFPWrite(line) {
- pass.Reportf(analysisutil.LineStart(tf, lineno), "frame pointer is clobbered before saving")
+ pass.Reportf(tf.LineStart(lineno), "frame pointer is clobbered before saving")
active = false
continue
}
"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"
)
// 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
}
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.
}
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
"golang.org/x/tools/go/analysis"
"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/typeparams"
)
var Analyzer = &analysis.Analyzer{
Name: "ifaceassert",
- Doc: analysisutil.MustExtractDoc(doc, "ifaceassert"),
+ Doc: analysisinternal.MustExtractDoc(doc, "ifaceassert"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/ifaceassert",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
+++ /dev/null
-// Copyright 2018 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 analysisutil defines various helper functions
-// used by two or more packages beneath go/analysis.
-package analysisutil
-
-import (
- "go/ast"
- "go/token"
- "go/types"
- "os"
-
- "golang.org/x/tools/go/analysis"
- "golang.org/x/tools/internal/analysisinternal"
-)
-
-// HasSideEffects reports whether evaluation of e has side effects.
-func HasSideEffects(info *types.Info, e ast.Expr) bool {
- safe := true
- ast.Inspect(e, func(node ast.Node) bool {
- switch n := node.(type) {
- case *ast.CallExpr:
- typVal := info.Types[n.Fun]
- switch {
- case typVal.IsType():
- // Type conversion, which is safe.
- case typVal.IsBuiltin():
- // Builtin func, conservatively assumed to not
- // be safe for now.
- safe = false
- return false
- default:
- // A non-builtin func or method call.
- // Conservatively assume that all of them have
- // side effects for now.
- safe = false
- return false
- }
- case *ast.UnaryExpr:
- if n.Op == token.ARROW {
- safe = false
- return false
- }
- }
- return true
- })
- return !safe
-}
-
-// ReadFile reads a file and adds it to the FileSet
-// so that we can report errors against it using lineStart.
-func ReadFile(pass *analysis.Pass, filename string) ([]byte, *token.File, error) {
- readFile := pass.ReadFile
- if readFile == nil {
- readFile = os.ReadFile
- }
- content, err := readFile(filename)
- if err != nil {
- return nil, nil, err
- }
- tf := pass.Fset.AddFile(filename, -1, len(content))
- tf.SetLinesForContent(content)
- return content, tf, nil
-}
-
-// LineStart returns the position of the start of the specified line
-// within file f, or NoPos if there is no line of that number.
-func LineStart(f *token.File, line int) token.Pos {
- // Use binary search to find the start offset of this line.
- //
- // TODO(adonovan): eventually replace this function with the
- // simpler and more efficient (*go/token.File).LineStart, added
- // in go1.12.
-
- min := 0 // inclusive
- max := f.Size() // exclusive
- for {
- offset := (min + max) / 2
- pos := f.Pos(offset)
- posn := f.Position(pos)
- if posn.Line == line {
- return pos - (token.Pos(posn.Column) - 1)
- }
-
- if min+1 >= max {
- return token.NoPos
- }
-
- if posn.Line < line {
- min = offset
- } else {
- max = offset
- }
- }
-}
-
-var MustExtractDoc = analysisinternal.MustExtractDoc
"golang.org/x/tools/go/analysis"
"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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
var Analyzer = &analysis.Analyzer{
Name: "loopclosure",
- Doc: analysisutil.MustExtractDoc(doc, "loopclosure"),
+ Doc: analysisinternal.MustExtractDoc(doc, "loopclosure"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/loopclosure",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
// 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)
}
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/ctrlflow"
"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/go/cfg"
"golang.org/x/tools/internal/analysisinternal"
"golang.org/x/tools/internal/astutil"
+ "golang.org/x/tools/internal/typesinternal"
)
//go:embed doc.go
var Analyzer = &analysis.Analyzer{
Name: "lostcancel",
- Doc: analysisutil.MustExtractDoc(doc, "lostcancel"),
+ Doc: analysisinternal.MustExtractDoc(doc, "lostcancel"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/lostcancel",
Run: run,
Requires: []*analysis.Analyzer{
// 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
}
"golang.org/x/tools/go/analysis"
"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"
)
var Analyzer = &analysis.Analyzer{
Name: "nilfunc",
- Doc: analysisutil.MustExtractDoc(doc, "nilfunc"),
+ Doc: analysisinternal.MustExtractDoc(doc, "nilfunc"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/nilfunc",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
// ...
// }
//
+// A local function may also be inferred as a printf wrapper. If it
+// is assigned to a variable, each call made through that variable will
+// be checked just like a call to a function:
+//
+// logf := func(format string, args ...any) {
+// message := fmt.Sprintf(format, args...)
+// log.Printf("%s: %s", prefix, message)
+// }
+// logf("%s", 123) // logf format %s has arg 123 of wrong type int
+//
// # Specifying printf wrappers by flag
//
// The -funcs flag specifies a comma-separated list of names of
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
- "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+ "golang.org/x/tools/go/ast/edge"
"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/fmtstr"
"golang.org/x/tools/internal/typeparams"
+ "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/versions"
)
var Analyzer = &analysis.Analyzer{
Name: "printf",
- Doc: analysisutil.MustExtractDoc(doc, "printf"),
+ Doc: analysisinternal.MustExtractDoc(doc, "printf"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/printf",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
- ResultType: reflect.TypeOf((*Result)(nil)),
+ ResultType: reflect.TypeFor[*Result](),
FactTypes: []analysis.Fact{new(isWrapper)},
}
// Result is the printf analyzer's result type. Clients may query the result
// to learn whether a function behaves like fmt.Print or fmt.Printf.
type Result struct {
- funcs map[*types.Func]Kind
+ funcs map[types.Object]Kind
}
// Kind reports whether fn behaves like fmt.Print or fmt.Printf.
func run(pass *analysis.Pass) (any, error) {
res := &Result{
- funcs: make(map[*types.Func]Kind),
+ funcs: make(map[types.Object]Kind),
}
- findPrintfLike(pass, res)
- checkCalls(pass)
+ findPrintLike(pass, res)
+ checkCalls(pass, res)
return res, nil
}
-type printfWrapper struct {
- obj *types.Func
- fdecl *ast.FuncDecl
- format *types.Var
- args *types.Var
+// A wrapper is a candidate print/printf wrapper function.
+//
+// We represent functions generally as types.Object, not *Func, so
+// that we can analyze anonymous functions such as
+//
+// printf := func(format string, args ...any) {...},
+//
+// representing them by the *types.Var symbol for the local variable
+// 'printf'.
+type wrapper struct {
+ obj types.Object // *Func or *Var
+ curBody inspector.Cursor // for *ast.BlockStmt
+ format *types.Var // optional "format string" parameter in the Func{Decl,Lit}
+ args *types.Var // "args ...any" parameter in the Func{Decl,Lit}
callers []printfCaller
- failed bool // if true, not a printf wrapper
}
type printfCaller struct {
- w *printfWrapper
+ w *wrapper
call *ast.CallExpr
}
-// maybePrintfWrapper decides whether decl (a declared function) may be a wrapper
-// around a fmt.Printf or fmt.Print function. If so it returns a printfWrapper
-// function describing the declaration. Later processing will analyze the
-// graph of potential printf wrappers to pick out the ones that are true wrappers.
-// A function may be a Printf or Print wrapper if its last argument is ...interface{}.
-// If the next-to-last argument is a string, then this may be a Printf wrapper.
-// Otherwise it may be a Print wrapper.
-func maybePrintfWrapper(info *types.Info, decl ast.Decl) *printfWrapper {
- // Look for functions with final argument type ...interface{}.
- fdecl, ok := decl.(*ast.FuncDecl)
- if !ok || fdecl.Body == nil {
- return nil
- }
- fn, ok := info.Defs[fdecl.Name].(*types.Func)
- // Type information may be incomplete.
- if !ok {
- return nil
- }
-
- sig := fn.Type().(*types.Signature)
+// formatArgsParams returns the "format string" and "args ...any"
+// parameters of a potential print or printf wrapper function.
+// (The format is nil in the print-like case.)
+func formatArgsParams(sig *types.Signature) (format, args *types.Var) {
if !sig.Variadic() {
- return nil // not variadic
+ return nil, nil // not variadic
}
params := sig.Params()
nparams := params.Len() // variadic => nonzero
- // Check final parameter is "args ...interface{}".
- args := params.At(nparams - 1)
- iface, ok := types.Unalias(args.Type().(*types.Slice).Elem()).(*types.Interface)
- if !ok || !iface.Empty() {
- return nil
- }
-
// Is second last param 'format string'?
- var format *types.Var
if nparams >= 2 {
if p := params.At(nparams - 2); p.Type() == types.Typ[types.String] {
format = p
}
}
- return &printfWrapper{
- obj: fn,
- fdecl: fdecl,
- format: format,
- args: args,
+ // Check final parameter is "args ...any".
+ // (variadic => slice)
+ args = params.At(nparams - 1)
+ iface, ok := types.Unalias(args.Type().(*types.Slice).Elem()).(*types.Interface)
+ if !ok || !iface.Empty() {
+ return nil, nil
}
+
+ return format, args
}
-// findPrintfLike scans the entire package to find printf-like functions.
-func findPrintfLike(pass *analysis.Pass, res *Result) (any, error) {
- // Gather potential wrappers and call graph between them.
- byObj := make(map[*types.Func]*printfWrapper)
- var wrappers []*printfWrapper
- for _, file := range pass.Files {
- for _, decl := range file.Decls {
- w := maybePrintfWrapper(pass.TypesInfo, decl)
- if w == nil {
- continue
+// findPrintLike scans the entire package to find print or printf-like functions.
+// When it returns, all such functions have been identified.
+func findPrintLike(pass *analysis.Pass, res *Result) {
+ var (
+ inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ info = pass.TypesInfo
+ )
+
+ // Pass 1: gather candidate wrapper functions (and populate wrappers).
+ var (
+ wrappers []*wrapper
+ byObj = make(map[types.Object]*wrapper)
+ )
+ for cur := range inspect.Root().Preorder((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) {
+ var (
+ curBody inspector.Cursor // for *ast.BlockStmt
+ sig *types.Signature
+ obj types.Object
+ )
+ switch f := cur.Node().(type) {
+ case *ast.FuncDecl:
+ // named function or method:
+ //
+ // func wrapf(format string, args ...any) {...}
+ if f.Body != nil {
+ curBody = cur.ChildAt(edge.FuncDecl_Body, -1)
+ obj = info.Defs[f.Name]
+ sig = obj.Type().(*types.Signature)
}
- byObj[w.obj] = w
- wrappers = append(wrappers, w)
- }
- }
- // Walk the graph to figure out which are really printf wrappers.
- for _, w := range wrappers {
- // Scan function for calls that could be to other printf-like functions.
- ast.Inspect(w.fdecl.Body, func(n ast.Node) bool {
- if w.failed {
- return false
+ case *ast.FuncLit:
+ // anonymous function directly assigned to a variable:
+ //
+ // var wrapf = func(format string, args ...any) {...}
+ // wrapf := func(format string, args ...any) {...}
+ // wrapf = func(format string, args ...any) {...}
+ //
+ // The LHS may also be a struct field x.wrapf or
+ // an imported var pkg.Wrapf.
+ //
+ sig = info.TypeOf(f).(*types.Signature)
+ curBody = cur.ChildAt(edge.FuncLit_Body, -1)
+ var lhs ast.Expr
+ switch ek, idx := cur.ParentEdge(); ek {
+ case edge.ValueSpec_Values:
+ curName := cur.Parent().ChildAt(edge.ValueSpec_Names, idx)
+ lhs = curName.Node().(*ast.Ident)
+ case edge.AssignStmt_Rhs:
+ curLhs := cur.Parent().ChildAt(edge.AssignStmt_Lhs, idx)
+ lhs = curLhs.Node().(ast.Expr)
}
- // TODO: Relax these checks; issue 26555.
- if assign, ok := n.(*ast.AssignStmt); ok {
- for _, lhs := range assign.Lhs {
- if match(pass.TypesInfo, lhs, w.format) ||
- match(pass.TypesInfo, lhs, w.args) {
- // Modifies the format
- // string or args in
- // some way, so not a
- // simple wrapper.
- w.failed = true
- return false
- }
+ switch lhs := lhs.(type) {
+ case *ast.Ident:
+ // variable: wrapf = func(...)
+ obj = info.ObjectOf(lhs).(*types.Var)
+ case *ast.SelectorExpr:
+ if sel, ok := info.Selections[lhs]; ok {
+ // struct field: x.wrapf = func(...)
+ obj = sel.Obj().(*types.Var)
+ } else {
+ // imported var: pkg.Wrapf = func(...)
+ obj = info.Uses[lhs.Sel].(*types.Var)
}
}
- if un, ok := n.(*ast.UnaryExpr); ok && un.Op == token.AND {
- if match(pass.TypesInfo, un.X, w.format) ||
- match(pass.TypesInfo, un.X, w.args) {
- // Taking the address of the
- // format string or args,
- // so not a simple wrapper.
- w.failed = true
- return false
+ }
+ if obj != nil {
+ format, args := formatArgsParams(sig)
+ if args != nil {
+ // obj (the symbol for a function/method, or variable
+ // assigned to an anonymous function) is a potential
+ // print or printf wrapper.
+ //
+ // Later processing will analyze the graph of potential
+ // wrappers and their function bodies to pick out the
+ // ones that are true wrappers.
+ w := &wrapper{
+ obj: obj,
+ curBody: curBody,
+ format: format, // non-nil => printf
+ args: args,
}
+ byObj[w.obj] = w
+ wrappers = append(wrappers, w)
}
+ }
+ }
- call, ok := n.(*ast.CallExpr)
- if !ok || len(call.Args) == 0 || !match(pass.TypesInfo, call.Args[len(call.Args)-1], w.args) {
- return true
- }
+ // Pass 2: scan the body of each wrapper function
+ // for calls to other printf-like functions.
+ //
+ // Also, reject tricky cases where the parameters
+ // are potentially mutated by AssignStmt or UnaryExpr.
+ // TODO: Relax these checks; issue 26555.
+ for _, w := range wrappers {
+ scan:
+ for cur := range w.curBody.Preorder(
+ (*ast.AssignStmt)(nil),
+ (*ast.UnaryExpr)(nil),
+ (*ast.CallExpr)(nil),
+ ) {
+ switch n := cur.Node().(type) {
+ case *ast.AssignStmt:
+ // If the wrapper updates format or args
+ // it is not a simple wrapper.
+ for _, lhs := range n.Lhs {
+ if w.format != nil && match(info, lhs, w.format) ||
+ match(info, lhs, w.args) {
+ break scan
+ }
+ }
- fn, kind := printfNameAndKind(pass, call)
- if kind != 0 {
- checkPrintfFwd(pass, w, call, kind, res)
- return true
- }
+ case *ast.UnaryExpr:
+ // If the wrapper computes &format or &args,
+ // it is not a simple wrapper.
+ if n.Op == token.AND &&
+ (w.format != nil && match(info, n.X, w.format) ||
+ match(info, n.X, w.args)) {
+ break scan
+ }
- // If the call is to another function in this package,
- // maybe we will find out it is printf-like later.
- // Remember this call for later checking.
- if fn != nil && fn.Pkg() == pass.Pkg && byObj[fn] != nil {
- callee := byObj[fn]
- callee.callers = append(callee.callers, printfCaller{w, call})
+ case *ast.CallExpr:
+ if len(n.Args) > 0 && match(info, n.Args[len(n.Args)-1], w.args) {
+ if callee := typeutil.Callee(pass.TypesInfo, n); callee != nil {
+
+ // Call from one wrapper candidate to another?
+ // Record the edge so that if callee is found to be
+ // a true wrapper, w will be too.
+ if w2, ok := byObj[callee]; ok {
+ w2.callers = append(w2.callers, printfCaller{w, n})
+ }
+
+ // Is the candidate a true wrapper, because it calls
+ // a known print{,f}-like function from the allowlist
+ // or an imported fact, or another wrapper found
+ // to be a true wrapper?
+ // If so, convert all w's callers to kind.
+ kind := callKind(pass, callee, res)
+ if kind != KindNone {
+ checkForward(pass, w, n, kind, res)
+ }
+ }
+ }
}
-
- return true
- })
+ }
}
- return nil, nil
}
func match(info *types.Info, arg ast.Expr, param *types.Var) bool {
return ok && info.ObjectOf(id) == param
}
-// checkPrintfFwd checks that a printf-forwarding wrapper is forwarding correctly.
+// checkForward checks that a forwarding wrapper is forwarding correctly.
// It diagnoses writing fmt.Printf(format, args) instead of fmt.Printf(format, args...).
-func checkPrintfFwd(pass *analysis.Pass, w *printfWrapper, call *ast.CallExpr, kind Kind, res *Result) {
+func checkForward(pass *analysis.Pass, w *wrapper, call *ast.CallExpr, kind Kind, res *Result) {
matched := kind == KindPrint ||
kind != KindNone && len(call.Args) >= 2 && match(pass.TypesInfo, call.Args[len(call.Args)-2], w.format)
if !matched {
pass.ReportRangef(call, "missing ... in args forwarded to %s-like function", desc)
return
}
- fn := w.obj
- var fact isWrapper
- if !pass.ImportObjectFact(fn, &fact) {
- fact.Kind = kind
- pass.ExportObjectFact(fn, &fact)
- res.funcs[fn] = kind
+
+ // If the candidate's print{,f} status becomes known,
+ // propagate it back to all its so-far known callers.
+ if res.funcs[w.obj] != kind {
+ res.funcs[w.obj] = kind
+
+ // Export a fact.
+ // (This is a no-op for local symbols.)
+ // We can't export facts on a symbol of another package,
+ // but we can treat the symbol as a wrapper within
+ // the current analysis unit.
+ if w.obj.Pkg() == pass.Pkg {
+ // Facts are associated with origins.
+ pass.ExportObjectFact(origin(w.obj), &isWrapper{Kind: kind})
+ }
+
+ // Propagate kind back to known callers.
for _, caller := range w.callers {
- checkPrintfFwd(pass, caller.w, caller.call, kind, res)
+ checkForward(pass, caller.w, caller.call, kind, res)
}
}
}
+func origin(obj types.Object) types.Object {
+ switch obj := obj.(type) {
+ case *types.Func:
+ return obj.Origin()
+ case *types.Var:
+ return obj.Origin()
+ }
+ return obj
+}
+
// isPrint records the print functions.
// If a key ends in 'f' then it is assumed to be a formatted print.
//
// checkCalls triggers the print-specific checks for calls that invoke a print
// function.
-func checkCalls(pass *analysis.Pass) {
+func checkCalls(pass *analysis.Pass, res *Result) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.File)(nil),
fileVersion = versions.Lang(versions.FileVersion(pass.TypesInfo, n))
case *ast.CallExpr:
- fn, kind := printfNameAndKind(pass, n)
- switch kind {
- case KindPrintf, KindErrorf:
- checkPrintf(pass, fileVersion, kind, n, fn.FullName())
- case KindPrint:
- checkPrint(pass, n, fn.FullName())
+ if callee := typeutil.Callee(pass.TypesInfo, n); callee != nil {
+ kind := callKind(pass, callee, res)
+ switch kind {
+ case KindPrintf, KindErrorf:
+ checkPrintf(pass, fileVersion, kind, n, fullname(callee))
+ case KindPrint:
+ checkPrint(pass, n, fullname(callee))
+ }
}
}
})
}
-func printfNameAndKind(pass *analysis.Pass, call *ast.CallExpr) (fn *types.Func, kind Kind) {
- fn, _ = typeutil.Callee(pass.TypesInfo, call).(*types.Func)
- if fn == nil {
- return nil, 0
+func fullname(obj types.Object) string {
+ if fn, ok := obj.(*types.Func); ok {
+ return fn.FullName()
}
+ return obj.Name()
+}
- // Facts are associated with generic declarations, not instantiations.
- fn = fn.Origin()
-
- _, ok := isPrint[fn.FullName()]
+// callKind returns the symbol of the called function
+// and its print/printf kind, if any.
+// (The symbol may be a var for an anonymous function.)
+// The result is memoized in res.funcs.
+func callKind(pass *analysis.Pass, obj types.Object, res *Result) Kind {
+ kind, ok := res.funcs[obj]
if !ok {
- // Next look up just "printf", for use with -printf.funcs.
- _, ok = isPrint[strings.ToLower(fn.Name())]
- }
- if ok {
- if fn.FullName() == "fmt.Errorf" {
- kind = KindErrorf
- } else if strings.HasSuffix(fn.Name(), "f") {
- kind = KindPrintf
+ // cache miss
+ _, ok := isPrint[fullname(obj)]
+ if !ok {
+ // Next look up just "printf", for use with -printf.funcs.
+ _, ok = isPrint[strings.ToLower(obj.Name())]
+ }
+ if ok {
+ // well-known printf functions
+ if fullname(obj) == "fmt.Errorf" {
+ kind = KindErrorf
+ } else if strings.HasSuffix(obj.Name(), "f") {
+ kind = KindPrintf
+ } else {
+ kind = KindPrint
+ }
} else {
- kind = KindPrint
+ // imported wrappers
+ // Facts are associated with generic declarations, not instantiations.
+ obj = origin(obj)
+ var fact isWrapper
+ if pass.ImportObjectFact(obj, &fact) {
+ kind = fact.Kind
+ }
}
- return fn, kind
+ res.funcs[obj] = kind // cache
}
-
- var fact isWrapper
- if pass.ImportObjectFact(fn, &fact) {
- return fn, fact.Kind
- }
-
- return fn, KindNone
+ return kind
}
// isFormatter reports whether t could satisfy fmt.Formatter.
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])
}
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
}
}
}
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 {
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
}
}
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]))
}
}
}
}
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)
}
}
}
"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"
)
}
}
if amt >= minSize {
- ident := analysisinternal.Format(pass.Fset, x)
+ ident := astutil.Format(pass.Fset, x)
qualifier := ""
if len(sizes) > 1 {
qualifier = "may be "
"golang.org/x/tools/go/analysis"
"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
// Analyzer describes sigchanyzer analysis function detector.
var Analyzer = &analysis.Analyzer{
Name: "sigchanyzer",
- Doc: analysisutil.MustExtractDoc(doc, "sigchanyzer"),
+ Doc: analysisinternal.MustExtractDoc(doc, "sigchanyzer"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/sigchanyzer",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
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
}
"golang.org/x/tools/go/analysis"
"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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typesinternal"
)
var Analyzer = &analysis.Analyzer{
Name: "slog",
- Doc: analysisutil.MustExtractDoc(doc, "slog"),
+ Doc: analysisinternal.MustExtractDoc(doc, "slog"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/slog",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
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
}
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.
"golang.org/x/tools/go/analysis"
"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"
)
//go:embed doc.go
var Analyzer = &analysis.Analyzer{
Name: "stdmethods",
- Doc: analysisutil.MustExtractDoc(doc, "stdmethods"),
+ Doc: analysisinternal.MustExtractDoc(doc, "stdmethods"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stdmethods",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
"golang.org/x/tools/go/analysis"
"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"
)
var Analyzer = &analysis.Analyzer{
Name: "stringintconv",
- Doc: analysisutil.MustExtractDoc(doc, "stringintconv"),
+ Doc: analysisinternal.MustExtractDoc(doc, "stringintconv"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stringintconv",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
// 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,
"golang.org/x/tools/go/analysis"
"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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
var Analyzer = &analysis.Analyzer{
Name: "testinggoroutine",
- Doc: analysisutil.MustExtractDoc(doc, "testinggoroutine"),
+ Doc: analysisinternal.MustExtractDoc(doc, "testinggoroutine"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/testinggoroutine",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
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
}
"unicode/utf8"
"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
var Analyzer = &analysis.Analyzer{
Name: "tests",
- Doc: analysisutil.MustExtractDoc(doc, "tests"),
+ Doc: analysisinternal.MustExtractDoc(doc, "tests"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/tests",
Run: run,
}
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.
"golang.org/x/tools/go/analysis"
"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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/typesinternal"
)
const badFormat = "2006-02-01"
var Analyzer = &analysis.Analyzer{
Name: "timeformat",
- Doc: analysisutil.MustExtractDoc(doc, "timeformat"),
+ Doc: analysisinternal.MustExtractDoc(doc, "timeformat"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/timeformat",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
func run(pass *analysis.Pass) (any, error) {
// Note: (time.Time).Format is a method and can be a typeutil.Callee
// without directly importing "time". So we cannot just skip this package
- // when !analysisutil.Imports(pass.Pkg, "time").
+ // when !analysisinternal.Imports(pass.Pkg, "time").
// TODO(taking): Consider using a prepass to collect typeutil.Callees.
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
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 {
"golang.org/x/tools/go/analysis"
"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/go/types/typeutil"
+ "golang.org/x/tools/internal/analysisinternal"
"golang.org/x/tools/internal/typesinternal"
)
var Analyzer = &analysis.Analyzer{
Name: "unmarshal",
- Doc: analysisutil.MustExtractDoc(doc, "unmarshal"),
+ Doc: analysisinternal.MustExtractDoc(doc, "unmarshal"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unmarshal",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
// Note: (*"encoding/json".Decoder).Decode, (* "encoding/gob".Decoder).Decode
// and (* "encoding/xml".Decoder).Decode are methods and can be a typeutil.Callee
// without directly importing their packages. So we cannot just skip this package
- // when !analysisutil.Imports(pass.Pkg, "encoding/...").
+ // when !analysisinternal.Imports(pass.Pkg, "encoding/...").
// TODO(taking): Consider using a prepass to collect typeutil.Callees.
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
"golang.org/x/tools/go/analysis"
"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"
)
//go:embed doc.go
var Analyzer = &analysis.Analyzer{
Name: "unreachable",
- Doc: analysisutil.MustExtractDoc(doc, "unreachable"),
+ Doc: analysisinternal.MustExtractDoc(doc, "unreachable"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unreachable",
Requires: []*analysis.Analyzer{inspect.Analyzer},
RunDespiteErrors: true,
case *ast.EmptyStmt:
// do not warn about unreachable empty statements
default:
+ var (
+ inspect = d.pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ curStmt, _ = inspect.Root().FindNode(stmt)
+ tokFile = d.pass.Fset.File(stmt.Pos())
+ )
// (This call to pass.Report is a frequent source
// of diagnostics beyond EOF in a truncated file;
// see #71659.)
End: stmt.End(),
Message: "unreachable code",
SuggestedFixes: []analysis.SuggestedFix{{
- Message: "Remove",
- TextEdits: []analysis.TextEdit{{
- Pos: stmt.Pos(),
- End: stmt.End(),
- }},
+ Message: "Remove",
+ TextEdits: refactor.DeleteStmt(tokFile, curStmt),
}},
})
d.reachable = true // silence error about next statement
"golang.org/x/tools/go/analysis"
"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
var Analyzer = &analysis.Analyzer{
Name: "unsafeptr",
- Doc: analysisutil.MustExtractDoc(doc, "unsafeptr"),
+ Doc: analysisinternal.MustExtractDoc(doc, "unsafeptr"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unsafeptr",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
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
}
}
// 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")
}
"golang.org/x/tools/go/analysis"
"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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
var Analyzer = &analysis.Analyzer{
Name: "unusedresult",
- Doc: analysisutil.MustExtractDoc(doc, "unusedresult"),
+ Doc: analysisinternal.MustExtractDoc(doc, "unusedresult"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedresult",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
"golang.org/x/tools/go/analysis"
"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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal"
+ "golang.org/x/tools/internal/typesinternal"
)
//go:embed doc.go
var Analyzer = &analysis.Analyzer{
Name: "waitgroup",
- Doc: analysisutil.MustExtractDoc(doc, "waitgroup"),
+ Doc: analysisinternal.MustExtractDoc(doc, "waitgroup"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/waitgroup",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
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
}
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
VetxOutput string // where to write file of fact information
Stdout string // write stdout (e.g. JSON, unified diff) to this file
SucceedOnTypecheckFailure bool // obsolete awful hack; see #18395 and below
- WarnDiagnostics bool // printing diagnostics should not cause a non-zero exit
}
// Main is the main function of a vet-like analysis tool that must be
// -V=full describe executable for build caching
// foo.cfg perform separate modular analyze on the single
// unit described by a JSON config file foo.cfg.
-//
-// Also, subject to approval of proposal #71859:
-//
// -fix don't print each diagnostic, apply its first fix
// -diff don't apply a fix, print the diff (requires -fix)
-//
-// Additionally, the environment variable GOVET has the value "vet" or
-// "fix" depending on whether the command is being invoked by "go vet",
-// to report diagnostics, or "go fix", to apply fixes. This is
-// necessary so that callers of Main can select their analyzer suite
-// before flag parsing. (Vet analyzers must report real code problems,
-// whereas Fix analyzers may fix non-problems such as style issues.)
+// -json print diagnostics and fixes in JSON form
func Main(analyzers ...*analysis.Analyzer) {
progname := filepath.Base(os.Args[0])
log.SetFlags(0)
// In VetxOnly mode, the analysis is run only for facts.
if !cfg.VetxOnly {
- code = processResults(fset, cfg.ID, results, cfg.WarnDiagnostics)
+ code = processResults(fset, cfg.ID, results)
}
os.Exit(code)
return cfg, nil
}
-func processResults(fset *token.FileSet, id string, results []result, warnDiagnostics bool) (exit int) {
+func processResults(fset *token.FileSet, id string, results []result) (exit int) {
if analysisflags.Fix {
// Don't print the diagnostics,
// but apply all fixes from the root actions.
for _, res := range results {
for _, diag := range res.diagnostics {
analysisflags.PrintPlain(os.Stderr, fset, analysisflags.Context, diag)
- if !warnDiagnostics {
- exit = 1
- }
+ exit = 1
}
}
}
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Package analysisinternal provides gopls' internal analyses with a
-// number of helper functions that operate on typed syntax trees.
+// Package analysisinternal provides helper functions for use in both
+// the analysis drivers in go/analysis and gopls, and in various
+// analyzers.
+//
+// TODO(adonovan): this is not ideal as it may lead to unnecessary
+// dependencies between drivers and analyzers. Split into analyzerlib
+// and driverlib?
package analysisinternal
import (
- "bytes"
"cmp"
"fmt"
- "go/ast"
- "go/printer"
- "go/scanner"
"go/token"
- "go/types"
- "iter"
- pathpkg "path"
+ "os"
"slices"
- "strings"
"golang.org/x/tools/go/analysis"
- "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(fset *token.FileSet, src []byte, start token.Pos) token.Pos {
- // Get the end position for the type error.
- file := fset.File(start)
- if file == nil {
- return start
- }
- if offset := file.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 := file.Offset(pos) + len(lit)
- src = src[off:]
- end += token.Pos(off)
- }
+// ReadFile reads a file and adds it to the FileSet in pass
+// so that we can report errors against it using lineStart.
+func ReadFile(pass *analysis.Pass, filename string) ([]byte, *token.File, error) {
+ readFile := pass.ReadFile
+ if readFile == nil {
+ readFile = os.ReadFile
}
-
- // 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)
+ content, err := readFile(filename)
+ if err != nil {
+ return nil, nil, err
}
- 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.
-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.
- matches := make(map[types.Type][]string)
- for _, typ := range typs {
- if typ == nil {
- continue // TODO(adonovan): is this reachable?
- }
- matches[typ] = nil // create entry
- }
-
- seen := map[types.Object]struct{}{}
- ast.Inspect(node, func(n ast.Node) bool {
- if n == nil {
- return false
- }
- // Prevent circular definitions. If 'pos' is within an assignment statement, do not
- // allow any identifiers in that assignment statement to be selected. Otherwise,
- // we could do the following, where 'x' satisfies the type of 'f0':
- //
- // x := fakeStruct{f0: x}
- //
- if assign, ok := n.(*ast.AssignStmt); ok && pos > assign.Pos() && pos <= assign.End() {
- return false
- }
- if n.End() > pos {
- return n.Pos() <= pos
- }
- ident, ok := n.(*ast.Ident)
- if !ok || ident.Name == "_" {
- return true
- }
- obj := info.Defs[ident]
- if obj == nil || obj.Type() == nil {
- return true
- }
- if _, ok := obj.(*types.TypeName); ok {
- return true
- }
- // Prevent duplicates in matches' values.
- if _, ok = seen[obj]; ok {
- return true
- }
- seen[obj] = struct{}{}
- // Find the scope for the given position. Then, check whether the object
- // exists within the scope.
- innerScope := pkg.Scope().Innermost(pos)
- if innerScope == nil {
- return true
- }
- _, foundObj := innerScope.LookupParent(ident.Name, pos)
- if foundObj != obj {
- return true
- }
- // The object must match one of the types that we are searching for.
- // TODO(adonovan): opt: use typeutil.Map?
- if names, ok := matches[obj.Type()]; ok {
- matches[obj.Type()] = append(names, ident.Name)
- } else {
- // If the object type does not exactly match
- // any of the target types, greedily find the first
- // target type that the object type can satisfy.
- for typ := range matches {
- if equivalentTypes(obj.Type(), typ) {
- matches[typ] = append(matches[typ], ident.Name)
- }
- }
- }
- return true
- })
- return matches
-}
-
-func equivalentTypes(want, got types.Type) bool {
- if types.Identical(want, got) {
- return true
- }
- // Code segment to help check for untyped equality from (golang/go#32146).
- if rhs, ok := want.(*types.Basic); ok && rhs.Info()&types.IsUntyped > 0 {
- if lhs, ok := got.Underlying().(*types.Basic); ok {
- return rhs.Info()&types.IsConstType == lhs.Info()&types.IsConstType
- }
- }
- return types.AssignableTo(want, got)
+ tf := pass.Fset.AddFile(filename, -1, len(content))
+ tf.SetLinesForContent(content)
+ return content, tf, nil
}
// A ReadFileFunc is a function that returns the
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.
//
return nil
}
-// CanImport reports whether one package is allowed to import another.
-//
-// TODO(adonovan): allow customization of the accessibility relation
-// (e.g. for Bazel).
-func CanImport(from, to string) bool {
- // TODO(adonovan): better segment hygiene.
- if to == "internal" || strings.HasPrefix(to, "internal/") {
- // Special case: only std packages may import internal/...
- // We can't reliably know whether we're in std, so we
- // use a heuristic on the first segment.
- first, _, _ := strings.Cut(from, "/")
- if strings.Contains(first, ".") {
- return false // example.com/foo ∉ std
- }
- if first == "testdata" {
- return false // testdata/foo ∉ std
- }
- }
- if strings.HasSuffix(to, "/internal") {
- return strings.HasPrefix(from, to[:len(to)-len("/internal")])
- }
- if i := strings.LastIndex(to, "/internal/"); i >= 0 {
- return strings.HasPrefix(from, to[:i])
- }
- 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(fset *token.FileSet, 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.
-
- tokFile := fset.File(stmt.Pos())
- 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 {
- // A standard package has no dot in its first segment.
- // (It may yet have a dot, e.g. "vendor/golang.org/x/foo".)
- slash := strings.IndexByte(path, '/')
- if slash < 0 {
- slash = len(path)
- }
- 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}
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.
-func enclosingFile(c inspector.Cursor) *ast.File {
- c, _ = moreiters.First(c.Enclosing((*ast.File)(nil)))
- return c.Node().(*ast.File)
-}
//
// var Analyzer = &analysis.Analyzer{
// Name: "halting",
-// Doc: analysisutil.MustExtractDoc(doc, "halting"),
+// Doc: analysisinternal.MustExtractDoc(doc, "halting"),
// ...
// }
func MustExtractDoc(content, name string) string {
import (
"go/ast"
"go/token"
+ "iter"
"strings"
)
}
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
+ }
+ }
+ }
+ }
+}
return equal(reflect.ValueOf(x), reflect.ValueOf(y), identical)
}
+// EqualSyntax reports whether x and y are equal.
+// Identifiers are considered equal if they are spelled the same.
+// Comments are ignored.
+func EqualSyntax(x, y ast.Expr) bool {
+ sameName := func(x, y *ast.Ident) bool { return x.Name == y.Name }
+ return Equal(x, y, sameName)
+}
+
func equal(x, y reflect.Value, identical func(x, y *ast.Ident) bool) bool {
// Ensure types are the same
if x.Type() != y.Type() {
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,
}
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()
+}
return 0, false // diagonals cannot overlap
}
kmin := max(-df, -db+e.delta)
- kmax := db + e.delta
- if df < kmax {
- kmax = df
- }
+ kmax := min(df, db+e.delta)
for k := kmin; k <= kmax; k += 2 {
x := e.vf.get(df, k)
u := e.vb.get(db, k-e.delta)
}
return i
}
-
-func min(x, y int) int {
- if x < y {
- return x
- } else {
- return y
- }
-}
--- /dev/null
+// 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 packagepath provides metadata operations on package path
+// strings.
+package packagepath
+
+// (This package should not depend on go/ast.)
+import "strings"
+
+// CanImport reports whether one package is allowed to import another.
+//
+// TODO(adonovan): allow customization of the accessibility relation
+// (e.g. for Bazel).
+func CanImport(from, to string) bool {
+ // TODO(adonovan): better segment hygiene.
+ if to == "internal" || strings.HasPrefix(to, "internal/") {
+ // Special case: only std packages may import internal/...
+ // We can't reliably know whether we're in std, so we
+ // use a heuristic on the first segment.
+ first, _, _ := strings.Cut(from, "/")
+ if strings.Contains(first, ".") {
+ return false // example.com/foo ∉ std
+ }
+ if first == "testdata" {
+ return false // testdata/foo ∉ std
+ }
+ }
+ if strings.HasSuffix(to, "/internal") {
+ return strings.HasPrefix(from, to[:len(to)-len("/internal")])
+ }
+ if i := strings.LastIndex(to, "/internal/"); i >= 0 {
+ return strings.HasPrefix(from, to[:i])
+ }
+ return true
+}
+
+// IsStdPackage reports whether the specified package path belongs to a
+// package in the standard library (including internal dependencies).
+func IsStdPackage(path string) bool {
+ // A standard package has no dot in its first segment.
+ // (It may yet have a dot, e.g. "vendor/golang.org/x/foo".)
+ slash := strings.IndexByte(path, '/')
+ if slash < 0 {
+ slash = len(path)
+ }
+ return !strings.Contains(path[:slash], ".") && path != "testdata"
+}
--- /dev/null
+// 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 deletion edits.
+
+import (
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+ "slices"
+
+ "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"
+ "golang.org/x/tools/internal/typesinternal/typeindex"
+)
+
+// DeleteVar returns edits to delete the declaration of a variable or
+// constant whose defining identifier is curId.
+//
+// It handles variants including:
+// - GenDecl > ValueSpec versus AssignStmt;
+// - RHS expression has effects, or not;
+// - entire statement/declaration may be eliminated;
+// and removes associated comments.
+//
+// If it cannot make the necessary edits, such as for a function
+// parameter or result, it returns nil.
+func DeleteVar(tokFile *token.File, info *types.Info, curId inspector.Cursor) []analysis.TextEdit {
+ switch ek, _ := curId.ParentEdge(); ek {
+ case edge.ValueSpec_Names:
+ return deleteVarFromValueSpec(tokFile, info, curId)
+
+ case edge.AssignStmt_Lhs:
+ return deleteVarFromAssignStmt(tokFile, info, curId)
+ }
+
+ // e.g. function receiver, parameter, or result,
+ // or "switch v := expr.(T) {}" (which has no object).
+ return nil
+}
+
+// deleteVarFromValueSpec returns edits to delete the declaration of a
+// variable or constant within a ValueSpec.
+//
+// Precondition: curId is Ident beneath ValueSpec.Names beneath GenDecl.
+//
+// See also [deleteVarFromAssignStmt], which has parallel structure.
+func deleteVarFromValueSpec(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []analysis.TextEdit {
+ var (
+ id = curIdent.Node().(*ast.Ident)
+ curSpec = curIdent.Parent()
+ spec = curSpec.Node().(*ast.ValueSpec)
+ )
+
+ declaresOtherNames := slices.ContainsFunc(spec.Names, func(name *ast.Ident) bool {
+ return name != id && name.Name != "_"
+ })
+ noRHSEffects := !slices.ContainsFunc(spec.Values, func(rhs ast.Expr) bool {
+ return !typesinternal.NoEffects(info, rhs)
+ })
+ if !declaresOtherNames && noRHSEffects {
+ // The spec is no longer needed, either to declare
+ // other variables, or for its side effects.
+ return DeleteSpec(tokFile, curSpec)
+ }
+
+ // The spec is still needed, either for
+ // at least one LHS, or for effects on RHS.
+ // Blank out or delete just one LHS.
+
+ _, index := curIdent.ParentEdge() // index of LHS within ValueSpec.Names
+
+ // If there is no RHS, we can delete the LHS.
+ if len(spec.Values) == 0 {
+ var pos, end token.Pos
+ if index == len(spec.Names)-1 {
+ // Delete final name.
+ //
+ // var _, lhs1 T
+ // ------
+ pos = spec.Names[index-1].End()
+ end = spec.Names[index].End()
+ } else {
+ // Delete non-final name.
+ //
+ // var lhs0, _ T
+ // ------
+ pos = spec.Names[index].Pos()
+ end = spec.Names[index+1].Pos()
+ }
+ return []analysis.TextEdit{{
+ Pos: pos,
+ End: end,
+ }}
+ }
+
+ // If the assignment is n:n and the RHS has no effects,
+ // we can delete the LHS and its corresponding RHS.
+ if len(spec.Names) == len(spec.Values) &&
+ typesinternal.NoEffects(info, spec.Values[index]) {
+
+ if index == len(spec.Names)-1 {
+ // Delete final items.
+ //
+ // var _, lhs1 = rhs0, rhs1
+ // ------ ------
+ return []analysis.TextEdit{
+ {
+ Pos: spec.Names[index-1].End(),
+ End: spec.Names[index].End(),
+ },
+ {
+ Pos: spec.Values[index-1].End(),
+ End: spec.Values[index].End(),
+ },
+ }
+ } else {
+ // Delete non-final items.
+ //
+ // var lhs0, _ = rhs0, rhs1
+ // ------ ------
+ return []analysis.TextEdit{
+ {
+ Pos: spec.Names[index].Pos(),
+ End: spec.Names[index+1].Pos(),
+ },
+ {
+ Pos: spec.Values[index].Pos(),
+ End: spec.Values[index+1].Pos(),
+ },
+ }
+ }
+ }
+
+ // We cannot delete the RHS.
+ // Blank out the LHS.
+ return []analysis.TextEdit{{
+ Pos: id.Pos(),
+ End: id.End(),
+ NewText: []byte("_"),
+ }}
+}
+
+// Precondition: curId is Ident beneath AssignStmt.Lhs.
+//
+// See also [deleteVarFromValueSpec], which has parallel structure.
+func deleteVarFromAssignStmt(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []analysis.TextEdit {
+ var (
+ id = curIdent.Node().(*ast.Ident)
+ curStmt = curIdent.Parent()
+ assign = curStmt.Node().(*ast.AssignStmt)
+ )
+
+ declaresOtherNames := slices.ContainsFunc(assign.Lhs, func(lhs ast.Expr) bool {
+ lhsId, ok := lhs.(*ast.Ident)
+ return ok && lhsId != id && lhsId.Name != "_"
+ })
+ noRHSEffects := !slices.ContainsFunc(assign.Rhs, func(rhs ast.Expr) bool {
+ return !typesinternal.NoEffects(info, rhs)
+ })
+ if !declaresOtherNames && noRHSEffects {
+ // The assignment is no longer needed, either to
+ // declare other variables, or for its side effects.
+ if edits := DeleteStmt(tokFile, curStmt); edits != nil {
+ return edits
+ }
+ // Statement could not not be deleted in this context.
+ // Fall back to conservative deletion.
+ }
+
+ // The assign is still needed, either for
+ // at least one LHS, or for effects on RHS,
+ // or because it cannot deleted because of its context.
+ // Blank out or delete just one LHS.
+
+ // If the assignment is 1:1 and the RHS has no effects,
+ // we can delete the LHS and its corresponding RHS.
+ _, index := curIdent.ParentEdge()
+ if len(assign.Lhs) > 1 &&
+ len(assign.Lhs) == len(assign.Rhs) &&
+ typesinternal.NoEffects(info, assign.Rhs[index]) {
+
+ if index == len(assign.Lhs)-1 {
+ // Delete final items.
+ //
+ // _, lhs1 := rhs0, rhs1
+ // ------ ------
+ return []analysis.TextEdit{
+ {
+ Pos: assign.Lhs[index-1].End(),
+ End: assign.Lhs[index].End(),
+ },
+ {
+ Pos: assign.Rhs[index-1].End(),
+ End: assign.Rhs[index].End(),
+ },
+ }
+ } else {
+ // Delete non-final items.
+ //
+ // lhs0, _ := rhs0, rhs1
+ // ------ ------
+ return []analysis.TextEdit{
+ {
+ Pos: assign.Lhs[index].Pos(),
+ End: assign.Lhs[index+1].Pos(),
+ },
+ {
+ Pos: assign.Rhs[index].Pos(),
+ End: assign.Rhs[index+1].Pos(),
+ },
+ }
+ }
+ }
+
+ // We cannot delete the RHS.
+ // Blank out the LHS.
+ edits := []analysis.TextEdit{{
+ Pos: id.Pos(),
+ End: id.End(),
+ NewText: []byte("_"),
+ }}
+
+ // If this eliminates the final variable declared by
+ // an := statement, we need to turn it into an =
+ // assignment to avoid a "no new variables on left
+ // side of :=" error.
+ if !declaresOtherNames {
+ edits = append(edits, analysis.TextEdit{
+ Pos: assign.TokPos,
+ End: assign.TokPos + token.Pos(len(":=")),
+ NewText: []byte("="),
+ })
+ }
+
+ return edits
+}
+
+// DeleteSpec returns edits to delete the {Type,Value}Spec identified by curSpec.
+//
+// TODO(adonovan): add test suite. Test for consts as well.
+func DeleteSpec(tokFile *token.File, curSpec inspector.Cursor) []analysis.TextEdit {
+ var (
+ spec = curSpec.Node().(ast.Spec)
+ curDecl = curSpec.Parent()
+ decl = curDecl.Node().(*ast.GenDecl)
+ )
+
+ // If it is the sole spec in the decl,
+ // delete the entire decl.
+ if len(decl.Specs) == 1 {
+ return DeleteDecl(tokFile, curDecl)
+ }
+
+ // Delete the spec and its comments.
+ _, index := curSpec.ParentEdge() // index of ValueSpec within GenDecl.Specs
+ pos, end := spec.Pos(), spec.End()
+ if doc := astutil.DocComment(spec); doc != nil {
+ pos = doc.Pos() // leading comment
+ }
+ if index == len(decl.Specs)-1 {
+ // Delete final spec.
+ if c := eolComment(spec); c != nil {
+ // var (v int // comment \n)
+ end = c.End()
+ }
+ } else {
+ // Delete non-final spec.
+ // var ( a T; b T )
+ // -----
+ end = decl.Specs[index+1].Pos()
+ }
+ return []analysis.TextEdit{{
+ Pos: pos,
+ End: end,
+ }}
+}
+
+// DeleteDecl returns edits to delete the ast.Decl identified by curDecl.
+//
+// TODO(adonovan): add test suite.
+func DeleteDecl(tokFile *token.File, curDecl inspector.Cursor) []analysis.TextEdit {
+ decl := curDecl.Node().(ast.Decl)
+
+ ek, _ := curDecl.ParentEdge()
+ switch ek {
+ case edge.DeclStmt_Decl:
+ return DeleteStmt(tokFile, curDecl.Parent())
+
+ case edge.File_Decls:
+ pos, end := decl.Pos(), decl.End()
+ if doc := astutil.DocComment(decl); doc != nil {
+ pos = doc.Pos()
+ }
+
+ // Delete free-floating comments on same line as rparen.
+ // var (...) // comment
+ var (
+ file = curDecl.Parent().Node().(*ast.File)
+ lineOf = tokFile.Line
+ declEndLine = lineOf(decl.End())
+ )
+ for _, cg := range file.Comments {
+ for _, c := range cg.List {
+ if c.Pos() < end {
+ continue // too early
+ }
+ commentEndLine := lineOf(c.End())
+ if commentEndLine > declEndLine {
+ break // too late
+ } else if lineOf(c.Pos()) == declEndLine && commentEndLine == declEndLine {
+ end = c.End()
+ }
+ }
+ }
+
+ return []analysis.TextEdit{{
+ Pos: pos,
+ End: end,
+ }}
+
+ default:
+ panic(fmt.Sprintf("Decl parent is %v, want DeclStmt or File", ek))
+ }
+}
+
+// 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 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}
+}
+
+// DeleteUnusedVars computes the edits required to delete the
+// declarations of any local variables whose last uses are in the
+// curDelend subtree, which is about to be deleted.
+func DeleteUnusedVars(index *typeindex.Index, info *types.Info, tokFile *token.File, curDelend inspector.Cursor) []analysis.TextEdit {
+ // TODO(adonovan): we might want to generalize this by
+ // splitting the two phases below, so that we can gather
+ // across a whole sequence of deletions then finally compute the
+ // set of variables that are no longer wanted.
+
+ // Count number of deletions of each var.
+ delcount := make(map[*types.Var]int)
+ for curId := range curDelend.Preorder((*ast.Ident)(nil)) {
+ id := curId.Node().(*ast.Ident)
+ if v, ok := info.Uses[id].(*types.Var); ok &&
+ typesinternal.GetVarKind(v) == typesinternal.LocalVar { // always false before go1.25
+ delcount[v]++
+ }
+ }
+
+ // Delete declaration of each var that became unused.
+ var edits []analysis.TextEdit
+ for v, count := range delcount {
+ if len(slices.Collect(index.Uses(v))) == count {
+ if curDefId, ok := index.Def(v); ok {
+ edits = append(edits, DeleteVar(tokFile, info, curDefId)...)
+ }
+ }
+ }
+ return edits
+}
+
+func eolComment(n ast.Node) *ast.CommentGroup {
+ // TODO(adonovan): support:
+ // func f() {...} // comment
+ switch n := n.(type) {
+ case *ast.GenDecl:
+ if !n.TokPos.IsValid() && len(n.Specs) == 1 {
+ return eolComment(n.Specs[0])
+ }
+ case *ast.ValueSpec:
+ return n.Comment
+ case *ast.TypeSpec:
+ return n.Comment
+ }
+ return nil
+}
--- /dev/null
+// 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/packagepath"
+)
+
+// AddImport returns the prefix (either "pkg." or "") that should be
+// used to qualify references to the desired symbol (member) imported
+// from the specified package, plus any necessary edits to the file's
+// import declaration to add a new import.
+//
+// If the import already exists, and is accessible at pos, AddImport
+// returns the existing name and no edits. (If the existing import is
+// a dot import, the prefix is "".)
+//
+// Otherwise, it adds a new import, using a local name derived from
+// the preferred name. To request a blank import, use a preferredName
+// of "_", and discard the prefix result; member is ignored in this
+// case.
+//
+// AddImport accepts the caller's implicit claim that the imported
+// package declares member.
+//
+// AddImport does not mutate its arguments.
+func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member string, pos token.Pos) (prefix string, edits []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 "", nil
+ }
+ if name == "." {
+ // The scope of ident must be the file scope.
+ if s, _ := scope.LookupParent(member, pos); s == info.Scopes[file] {
+ return "", nil
+ }
+ } else if _, obj := scope.LookupParent(name, pos); obj == pkgname {
+ return 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 packagepath.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 !packagepath.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 + ".", []analysis.TextEdit{{
+ Pos: pos,
+ End: pos,
+ NewText: []byte(newText),
+ }}
+}
--- /dev/null
+// 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
+}
--- /dev/null
+// 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/ast"
+ "go/token"
+ "go/types"
+)
+
+// NoEffects reports whether the expression has no side effects, i.e., it
+// does not modify the memory state. This function is conservative: it may
+// return false even when the expression has no effect.
+func NoEffects(info *types.Info, expr ast.Expr) bool {
+ noEffects := true
+ ast.Inspect(expr, func(n ast.Node) bool {
+ switch v := n.(type) {
+ case nil, *ast.Ident, *ast.BasicLit, *ast.BinaryExpr, *ast.ParenExpr,
+ *ast.SelectorExpr, *ast.IndexExpr, *ast.SliceExpr, *ast.TypeAssertExpr,
+ *ast.StarExpr, *ast.CompositeLit,
+ // non-expressions that may appear within expressions
+ *ast.KeyValueExpr,
+ *ast.FieldList,
+ *ast.Field,
+ *ast.Ellipsis,
+ *ast.IndexListExpr:
+ // No effect.
+
+ case *ast.ArrayType,
+ *ast.StructType,
+ *ast.ChanType,
+ *ast.FuncType,
+ *ast.MapType,
+ *ast.InterfaceType:
+ // Type syntax: no effects, recursively.
+ // Prune descent.
+ return false
+
+ case *ast.UnaryExpr:
+ // Channel send <-ch has effects.
+ if v.Op == token.ARROW {
+ noEffects = false
+ }
+
+ case *ast.CallExpr:
+ // Type conversion has no effects.
+ if !info.Types[v.Fun].IsType() {
+ if CallsPureBuiltin(info, v) {
+ // A call such as len(e) has no effects of its
+ // own, though the subexpression e might.
+ } else {
+ noEffects = false
+ }
+ }
+
+ case *ast.FuncLit:
+ // A FuncLit has no effects, but do not descend into it.
+ return false
+
+ default:
+ // All other expressions have effects
+ noEffects = false
+ }
+
+ return noEffects
+ })
+ return noEffects
+}
+
+// CallsPureBuiltin reports whether call is a call of a built-in
+// function that is a pure computation over its operands (analogous to
+// a + operator). Because it does not depend on program state, it may
+// be evaluated at any point--though not necessarily at multiple
+// points (consider new, make).
+func CallsPureBuiltin(info *types.Info, call *ast.CallExpr) bool {
+ if id, ok := ast.Unparen(call.Fun).(*ast.Ident); ok {
+ if b, ok := info.ObjectOf(id).(*types.Builtin); ok {
+ switch b.Name() {
+ case "len", "cap", "complex", "imag", "real", "make", "new", "max", "min":
+ return true
+ }
+ // Not: append clear close copy delete panic print println recover
+ }
+ }
+ return false
+}
--- /dev/null
+// 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
+}
// 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 (
"reflect"
"unsafe"
+ "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/aliases"
)
// 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 {
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
+}
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-package typesinternal
+//go:build go1.25
-// TODO(adonovan): when CL 645115 lands, define the go1.25 version of
-// this API that actually does something.
+package typesinternal
import "go/types"
-type VarKind uint8
+type VarKind = types.VarKind
const (
- _ VarKind = iota // (not meaningful)
- PackageVar // a package-level variable
- LocalVar // a local variable
- RecvVar // a method receiver variable
- ParamVar // a function parameter variable
- ResultVar // a function result variable
- FieldVar // a struct field
+ PackageVar = types.PackageVar
+ LocalVar = types.LocalVar
+ RecvVar = types.RecvVar
+ ParamVar = types.ParamVar
+ ResultVar = types.ResultVar
+ FieldVar = types.FieldVar
)
-func (kind VarKind) String() string {
- return [...]string{
- 0: "VarKind(0)",
- PackageVar: "PackageVar",
- LocalVar: "LocalVar",
- RecvVar: "RecvVar",
- ParamVar: "ParamVar",
- ResultVar: "ResultVar",
- FieldVar: "FieldVar",
- }[kind]
-}
-
-// GetVarKind returns an invalid VarKind.
-func GetVarKind(v *types.Var) VarKind { return 0 }
-
-// SetVarKind has no effect.
-func SetVarKind(v *types.Var, kind VarKind) {}
+func GetVarKind(v *types.Var) VarKind { return v.Kind() }
+func SetVarKind(v *types.Var, kind VarKind) { v.SetKind(kind) }
--- /dev/null
+// Copyright 2024 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.
+
+//go:build !go1.25
+
+package typesinternal
+
+import "go/types"
+
+type VarKind uint8
+
+const (
+ _ VarKind = iota // (not meaningful)
+ PackageVar // a package-level variable
+ LocalVar // a local variable
+ RecvVar // a method receiver variable
+ ParamVar // a function parameter variable
+ ResultVar // a function result variable
+ FieldVar // a struct field
+)
+
+func (kind VarKind) String() string {
+ return [...]string{
+ 0: "VarKind(0)",
+ PackageVar: "PackageVar",
+ LocalVar: "LocalVar",
+ RecvVar: "RecvVar",
+ ParamVar: "ParamVar",
+ ResultVar: "ResultVar",
+ FieldVar: "FieldVar",
+ }[kind]
+}
+
+// GetVarKind returns an invalid VarKind.
+func GetVarKind(v *types.Var) VarKind { return 0 }
+
+// SetVarKind has no effect.
+func SetVarKind(v *types.Var, kind VarKind) {}
}
}
-// 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:
# golang.org/x/build v0.0.0-20250806225920-b7c66c047964
## explicit; go 1.23.0
golang.org/x/build/relnote
-# golang.org/x/mod v0.28.0
+# golang.org/x/mod v0.29.0
## explicit; go 1.24.0
golang.org/x/mod/internal/lazyregexp
golang.org/x/mod/modfile
## explicit; go 1.24.0
golang.org/x/sync/errgroup
golang.org/x/sync/semaphore
-# golang.org/x/sys v0.36.0
+# golang.org/x/sys v0.37.0
## explicit; go 1.24.0
golang.org/x/sys/plan9
golang.org/x/sys/unix
golang.org/x/sys/windows
-# golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053
+# golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8
## explicit; go 1.24.0
golang.org/x/telemetry
golang.org/x/telemetry/counter
# golang.org/x/term v0.34.0
## explicit; go 1.23.0
golang.org/x/term
-# golang.org/x/text v0.29.0
+# golang.org/x/text v0.30.0
## explicit; go 1.24.0
golang.org/x/text/cases
golang.org/x/text/internal
golang.org/x/text/language
golang.org/x/text/transform
golang.org/x/text/unicode/norm
-# golang.org/x/tools v0.37.1-0.20250924232827-4df13e317ce4
+# golang.org/x/tools v0.38.1-0.20251015192825-7d9453ccc0f5
## explicit; go 1.24.0
golang.org/x/tools/cmd/bisect
golang.org/x/tools/cover
golang.org/x/tools/go/analysis/passes/httpresponse
golang.org/x/tools/go/analysis/passes/ifaceassert
golang.org/x/tools/go/analysis/passes/inspect
-golang.org/x/tools/go/analysis/passes/internal/analysisutil
golang.org/x/tools/go/analysis/passes/loopclosure
golang.org/x/tools/go/analysis/passes/lostcancel
golang.org/x/tools/go/analysis/passes/nilfunc
golang.org/x/tools/internal/facts
golang.org/x/tools/internal/fmtstr
golang.org/x/tools/internal/moreiters
+golang.org/x/tools/internal/packagepath
+golang.org/x/tools/internal/refactor
golang.org/x/tools/internal/stdlib
golang.org/x/tools/internal/typeparams
golang.org/x/tools/internal/typesinternal
go 1.26
require (
- golang.org/x/crypto v0.42.0
- golang.org/x/net v0.44.1-0.20251002015445-edb764c2296f
+ golang.org/x/crypto v0.43.0
+ golang.org/x/net v0.46.0
)
require (
- golang.org/x/sys v0.36.0 // indirect
- golang.org/x/text v0.29.0 // indirect
+ golang.org/x/sys v0.37.0 // indirect
+ golang.org/x/text v0.30.0 // indirect
)
-golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
-golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
-golang.org/x/net v0.44.1-0.20251002015445-edb764c2296f h1:vNklv+oJQSYNGsWXHoCPi2MHMcpj9/Q7aBhvvfnJvGg=
-golang.org/x/net v0.44.1-0.20251002015445-edb764c2296f/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
-golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
-golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
-golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
+golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
+golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
+golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
q.prev.next = q
q.next.prev = q
}
+
+ // Update the metadata.
+ ws.streams[streamID] = http2streamMetadata{
+ location: q,
+ priority: priority,
+ }
}
func (ws *http2priorityWriteSchedulerRFC9218) Push(wr http2FrameWriteRequest) {
func (i *isolatingRunSequence) Len() int { return len(i.indexes) }
-func maxLevel(a, b level) level {
- if a > b {
- return a
- }
- return b
-}
-
// Rule X10, second bullet: Determine the start-of-sequence (sos) and end-of-sequence (eos) types,
// either L or R, for each isolating run sequence.
func (p *paragraph) isolatingRunSequence(indexes []int) *isolatingRunSequence {
indexes: indexes,
types: types,
level: level,
- sos: typeForLevel(maxLevel(prevLevel, level)),
- eos: typeForLevel(maxLevel(succLevel, level)),
+ sos: typeForLevel(max(prevLevel, level)),
+ eos: typeForLevel(max(succLevel, level)),
}
}
-# golang.org/x/crypto v0.42.0
+# golang.org/x/crypto v0.43.0
## explicit; go 1.24.0
golang.org/x/crypto/chacha20
golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/cryptobyte/asn1
golang.org/x/crypto/internal/alias
golang.org/x/crypto/internal/poly1305
-# golang.org/x/net v0.44.1-0.20251002015445-edb764c2296f
+# golang.org/x/net v0.46.0
## explicit; go 1.24.0
golang.org/x/net/dns/dnsmessage
golang.org/x/net/http/httpguts
golang.org/x/net/idna
golang.org/x/net/lif
golang.org/x/net/nettest
-# golang.org/x/sys v0.36.0
+# golang.org/x/sys v0.37.0
## explicit; go 1.24.0
golang.org/x/sys/cpu
-# golang.org/x/text v0.29.0
+# golang.org/x/text v0.30.0
## explicit; go 1.24.0
golang.org/x/text/secure/bidirule
golang.org/x/text/transform