cmd: update x/tools to 59ff18c

$ go get golang.org/x/tools@59ff18c
	$ GOWORK=off go mod tidy
	$ GOWORK=off go mod vendor

This implies golang.org/x/sys@v0.38.0, for which I have also
updated src/go.mod for consistency.

I also upgraded x/mod@3f03020 to bring in some fixes to
code that go vet would otherwise have flagged in this CL.

This brings in a number of fixes and improvements to the analysis
tools within cmd/fix, which I will apply to std and cmd presently.

For golang/go#71859

Change-Id: I56c49e7ab28eb6ba55408a3721e9a898ee3067a1
Reviewed-on: https://go-review.googlesource.com/c/go/+/719760
Auto-Submit: Alan Donovan <adonovan@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Alan Donovan 2025-11-11 14:56:59 -05:00 committed by Gopher Robot
parent 50128a2154
commit 410ef44f00
136 changed files with 3430 additions and 1509 deletions

View file

@ -6,12 +6,12 @@ require (
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5
golang.org/x/arch v0.22.1-0.20251016010524-fea4a9ec4938 golang.org/x/arch v0.22.1-0.20251016010524-fea4a9ec4938
golang.org/x/build v0.0.0-20250806225920-b7c66c047964 golang.org/x/build v0.0.0-20250806225920-b7c66c047964
golang.org/x/mod v0.29.0 golang.org/x/mod v0.30.1-0.20251114215501-3f03020ad526
golang.org/x/sync v0.17.0 golang.org/x/sync v0.18.0
golang.org/x/sys v0.37.0 golang.org/x/sys v0.38.0
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54
golang.org/x/term v0.34.0 golang.org/x/term v0.34.0
golang.org/x/tools v0.38.1-0.20251015192825-7d9453ccc0f5 golang.org/x/tools v0.39.1-0.20251114194111-59ff18ce4883
) )
require ( require (

View file

@ -10,19 +10,19 @@ golang.org/x/arch v0.22.1-0.20251016010524-fea4a9ec4938 h1:VJ182b/ajNehMFRltVfCh
golang.org/x/arch v0.22.1-0.20251016010524-fea4a9ec4938/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.22.1-0.20251016010524-fea4a9ec4938/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/build v0.0.0-20250806225920-b7c66c047964 h1:yRs1K51GKq7hsIO+YHJ8LsslrvwFceNPIv0tYjpcBd0= 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/build v0.0.0-20250806225920-b7c66c047964/go.mod h1:i9Vx7+aOQUpYJRxSO+OpRStVBCVL/9ccI51xblWm5WY=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.30.1-0.20251114215501-3f03020ad526 h1:LPpBM4CGUFMC47OqgAr2YIUxEUjH1Ur+D3KR/1LiuuQ=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.30.1-0.20251114215501-3f03020ad526/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.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-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= 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/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= 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/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.39.1-0.20251114194111-59ff18ce4883 h1:aeO0AW8d+a+5+hNQx9f4J5egD89zftrY2x42KGQjLzI=
golang.org/x/tools v0.38.1-0.20251015192825-7d9453ccc0f5/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools v0.39.1-0.20251114194111-59ff18ce4883/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef h1:mqLYrXCXYEZOop9/Dbo6RPX11539nwiCNBb1icVPmw8= rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef h1:mqLYrXCXYEZOop9/Dbo6RPX11539nwiCNBb1icVPmw8=
rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef/go.mod h1:8xcPgWmwlZONN1D9bjxtHEjrUtSEa3fakVF8iaewYKQ= rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef/go.mod h1:8xcPgWmwlZONN1D9bjxtHEjrUtSEa3fakVF8iaewYKQ=

View file

@ -33,7 +33,7 @@ type printer struct {
} }
// printf prints to the buffer. // printf prints to the buffer.
func (p *printer) printf(format string, args ...interface{}) { func (p *printer) printf(format string, args ...any) {
fmt.Fprintf(p, format, args...) fmt.Fprintf(p, format, args...)
} }

View file

@ -94,7 +94,7 @@ func (x *FileSyntax) Span() (start, end Position) {
// line, the new line is added at the end of the block containing hint, // line, the new line is added at the end of the block containing hint,
// extracting hint into a new block if it is not yet in one. // extracting hint into a new block if it is not yet in one.
// //
// If the hint is non-nil buts its first token does not match, // If the hint is non-nil but its first token does not match,
// the new line is added after the block containing hint // the new line is added after the block containing hint
// (or hint itself, if not in a block). // (or hint itself, if not in a block).
// //
@ -600,7 +600,7 @@ func (in *input) readToken() {
// Checked all punctuation. Must be identifier token. // Checked all punctuation. Must be identifier token.
if c := in.peekRune(); !isIdent(c) { if c := in.peekRune(); !isIdent(c) {
in.Error(fmt.Sprintf("unexpected input character %#q", c)) in.Error(fmt.Sprintf("unexpected input character %#q", rune(c)))
} }
// Scan over identifier. // Scan over identifier.

View file

@ -368,7 +368,7 @@ func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, a
Err: err, Err: err,
}) })
} }
errorf := func(format string, args ...interface{}) { errorf := func(format string, args ...any) {
wrapError(fmt.Errorf(format, args...)) wrapError(fmt.Errorf(format, args...))
} }
@ -574,7 +574,7 @@ func parseReplace(filename string, line *Line, verb string, args []string, fix V
Err: err, Err: err,
} }
} }
errorf := func(format string, args ...interface{}) *Error { errorf := func(format string, args ...any) *Error {
return wrapError(fmt.Errorf(format, args...)) return wrapError(fmt.Errorf(format, args...))
} }
@ -685,7 +685,7 @@ func (f *WorkFile) add(errs *ErrorList, line *Line, verb string, args []string,
Err: err, Err: err,
}) })
} }
errorf := func(format string, args ...interface{}) { errorf := func(format string, args ...any) {
wrapError(fmt.Errorf(format, args...)) wrapError(fmt.Errorf(format, args...))
} }
@ -1594,7 +1594,7 @@ func (f *File) AddRetract(vi VersionInterval, rationale string) error {
r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]") r.Syntax = f.Syntax.addLine(nil, "retract", "[", AutoQuote(vi.Low), ",", AutoQuote(vi.High), "]")
} }
if rationale != "" { if rationale != "" {
for _, line := range strings.Split(rationale, "\n") { for line := range strings.SplitSeq(rationale, "\n") {
com := Comment{Token: "// " + line} com := Comment{Token: "// " + line}
r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com) r.Syntax.Comment().Before = append(r.Syntax.Comment().Before, com)
} }

View file

@ -261,7 +261,7 @@ func modPathOK(r rune) bool {
// importPathOK reports whether r can appear in a package import path element. // importPathOK reports whether r can appear in a package import path element.
// //
// Import paths are intermediate between module paths and file paths: we allow // Import paths are intermediate between module paths and file paths: we
// disallow characters that would be confusing or ambiguous as arguments to // disallow characters that would be confusing or ambiguous as arguments to
// 'go get' (such as '@' and ' ' ), but allow certain characters that are // 'go get' (such as '@' and ' ' ), but allow certain characters that are
// otherwise-unambiguous on the command line and historically used for some // otherwise-unambiguous on the command line and historically used for some
@ -802,8 +802,8 @@ func MatchPrefixPatterns(globs, target string) bool {
for globs != "" { for globs != "" {
// Extract next non-empty glob in comma-separated list. // Extract next non-empty glob in comma-separated list.
var glob string var glob string
if i := strings.Index(globs, ","); i >= 0 { if before, after, ok := strings.Cut(globs, ","); ok {
glob, globs = globs[:i], globs[i+1:] glob, globs = before, after
} else { } else {
glob, globs = globs, "" glob, globs = globs, ""
} }

View file

@ -45,8 +45,8 @@ func IsValid(v string) bool {
// Canonical returns the canonical formatting of the semantic version v. // Canonical returns the canonical formatting of the semantic version v.
// It fills in any missing .MINOR or .PATCH and discards build metadata. // It fills in any missing .MINOR or .PATCH and discards build metadata.
// Two semantic versions compare equal only if their canonical formattings // Two semantic versions compare equal only if their canonical formatting
// are identical strings. // is an identical string.
// The canonical invalid semantic version is the empty string. // The canonical invalid semantic version is the empty string.
func Canonical(v string) string { func Canonical(v string) string {
p, ok := parse(v) p, ok := parse(v)

View file

@ -20,13 +20,13 @@ type parCache struct {
type cacheEntry struct { type cacheEntry struct {
done uint32 done uint32
mu sync.Mutex mu sync.Mutex
result interface{} result any
} }
// Do calls the function f if and only if Do is being called for the first time with this key. // Do calls the function f if and only if Do is being called for the first time with this key.
// No call to Do with a given key returns until the one call to f returns. // No call to Do with a given key returns until the one call to f returns.
// Do returns the value returned by the one call to f. // Do returns the value returned by the one call to f.
func (c *parCache) Do(key interface{}, f func() interface{}) interface{} { func (c *parCache) Do(key any, f func() any) any {
entryIface, ok := c.m.Load(key) entryIface, ok := c.m.Load(key)
if !ok { if !ok {
entryIface, _ = c.m.LoadOrStore(key, new(cacheEntry)) entryIface, _ = c.m.LoadOrStore(key, new(cacheEntry))
@ -46,7 +46,7 @@ func (c *parCache) Do(key interface{}, f func() interface{}) interface{} {
// Get returns the cached result associated with key. // Get returns the cached result associated with key.
// It returns nil if there is no such result. // It returns nil if there is no such result.
// If the result for key is being computed, Get does not wait for the computation to finish. // If the result for key is being computed, Get does not wait for the computation to finish.
func (c *parCache) Get(key interface{}) interface{} { func (c *parCache) Get(key any) any {
entryIface, ok := c.m.Load(key) entryIface, ok := c.m.Load(key)
if !ok { if !ok {
return nil return nil

View file

@ -244,7 +244,7 @@ func (c *Client) Lookup(path, vers string) (lines []string, err error) {
data []byte data []byte
err error err error
} }
result := c.record.Do(file, func() interface{} { result := c.record.Do(file, func() any {
// Try the on-disk cache, or else get from web. // Try the on-disk cache, or else get from web.
writeCache := false writeCache := false
data, err := c.ops.ReadCache(file) data, err := c.ops.ReadCache(file)
@ -284,7 +284,7 @@ func (c *Client) Lookup(path, vers string) (lines []string, err error) {
// (with or without /go.mod). // (with or without /go.mod).
prefix := path + " " + vers + " " prefix := path + " " + vers + " "
var hashes []string var hashes []string
for _, line := range strings.Split(string(result.data), "\n") { for line := range strings.SplitSeq(string(result.data), "\n") {
if strings.HasPrefix(line, prefix) { if strings.HasPrefix(line, prefix) {
hashes = append(hashes, line) hashes = append(hashes, line)
} }
@ -552,7 +552,7 @@ func (c *Client) readTile(tile tlog.Tile) ([]byte, error) {
err error err error
} }
result := c.tileCache.Do(tile, func() interface{} { result := c.tileCache.Do(tile, func() any {
// Try the requested tile in on-disk cache. // Try the requested tile in on-disk cache.
data, err := c.ops.ReadCache(c.tileCacheKey(tile)) data, err := c.ops.ReadCache(c.tileCacheKey(tile))
if err == nil { if err == nil {

View file

@ -240,8 +240,8 @@ func isValidName(name string) bool {
// NewVerifier construct a new [Verifier] from an encoded verifier key. // NewVerifier construct a new [Verifier] from an encoded verifier key.
func NewVerifier(vkey string) (Verifier, error) { func NewVerifier(vkey string) (Verifier, error) {
name, vkey := chop(vkey, "+") name, vkey, _ := strings.Cut(vkey, "+")
hash16, key64 := chop(vkey, "+") hash16, key64, _ := strings.Cut(vkey, "+")
hash, err1 := strconv.ParseUint(hash16, 16, 32) hash, err1 := strconv.ParseUint(hash16, 16, 32)
key, err2 := base64.StdEncoding.DecodeString(key64) key, err2 := base64.StdEncoding.DecodeString(key64)
if len(hash16) != 8 || err1 != nil || err2 != nil || !isValidName(name) || len(key) == 0 { if len(hash16) != 8 || err1 != nil || err2 != nil || !isValidName(name) || len(key) == 0 {
@ -276,12 +276,8 @@ func NewVerifier(vkey string) (Verifier, error) {
// chop chops s at the first instance of sep, if any, // chop chops s at the first instance of sep, if any,
// and returns the text before and after sep. // and returns the text before and after sep.
// If sep is not present, chop returns before is s and after is empty. // If sep is not present, chop returns before is s and after is empty.
func chop(s, sep string) (before, after string) { func chop(s, sep string) (before, after string, ok bool) {
i := strings.Index(s, sep) return strings.Cut(s, sep)
if i < 0 {
return s, ""
}
return s[:i], s[i+len(sep):]
} }
// verifier is a trivial Verifier implementation. // verifier is a trivial Verifier implementation.
@ -297,10 +293,10 @@ func (v *verifier) Verify(msg, sig []byte) bool { return v.verify(msg, sig) }
// NewSigner constructs a new [Signer] from an encoded signer key. // NewSigner constructs a new [Signer] from an encoded signer key.
func NewSigner(skey string) (Signer, error) { func NewSigner(skey string) (Signer, error) {
priv1, skey := chop(skey, "+") priv1, skey, _ := strings.Cut(skey, "+")
priv2, skey := chop(skey, "+") priv2, skey, _ := strings.Cut(skey, "+")
name, skey := chop(skey, "+") name, skey, _ := strings.Cut(skey, "+")
hash16, key64 := chop(skey, "+") hash16, key64, _ := strings.Cut(skey, "+")
hash, err1 := strconv.ParseUint(hash16, 16, 32) hash, err1 := strconv.ParseUint(hash16, 16, 32)
key, err2 := base64.StdEncoding.DecodeString(key64) key, err2 := base64.StdEncoding.DecodeString(key64)
if priv1 != "PRIVATE" || priv2 != "KEY" || len(hash16) != 8 || err1 != nil || err2 != nil || !isValidName(name) || len(key) == 0 { if priv1 != "PRIVATE" || priv2 != "KEY" || len(hash16) != 8 || err1 != nil || err2 != nil || !isValidName(name) || len(key) == 0 {
@ -557,7 +553,7 @@ func Open(msg []byte, known Verifiers) (*Note, error) {
return nil, errMalformedNote return nil, errMalformedNote
} }
line = line[len(sigPrefix):] line = line[len(sigPrefix):]
name, b64 := chop(string(line), " ") name, b64, _ := chop(string(line), " ")
sig, err := base64.StdEncoding.DecodeString(b64) sig, err := base64.StdEncoding.DecodeString(b64)
if err != nil || !isValidName(name) || b64 == "" || len(sig) < 5 { if err != nil || !isValidName(name) || b64 == "" || len(sig) < 5 {
return nil, errMalformedNote return nil, errMalformedNote

View file

@ -76,8 +76,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "invalid module@version syntax", http.StatusBadRequest) http.Error(w, "invalid module@version syntax", http.StatusBadRequest)
return return
} }
i := strings.Index(mod, "@") escPath, escVers, _ := strings.Cut(mod, "@")
escPath, escVers := mod[:i], mod[i+1:]
path, err := module.UnescapePath(escPath) path, err := module.UnescapePath(escPath)
if err != nil { if err != nil {
reportError(w, err) reportError(w, err)

View file

@ -66,7 +66,7 @@ func (s *TestServer) ReadRecords(ctx context.Context, id, n int64) ([][]byte, er
defer s.mu.Unlock() defer s.mu.Unlock()
var list [][]byte var list [][]byte
for i := int64(0); i < n; i++ { for i := range n {
if id+i >= int64(len(s.records)) { if id+i >= int64(len(s.records)) {
return nil, fmt.Errorf("missing records") return nil, fmt.Errorf("missing records")
} }

View file

@ -35,7 +35,7 @@ type Tree struct {
// A future backwards-incompatible encoding would use a different // A future backwards-incompatible encoding would use a different
// first line (for example, "go.sum database tree v2"). // first line (for example, "go.sum database tree v2").
func FormatTree(tree Tree) []byte { func FormatTree(tree Tree) []byte {
return []byte(fmt.Sprintf("go.sum database tree\n%d\n%s\n", tree.N, tree.Hash)) return fmt.Appendf(nil, "go.sum database tree\n%d\n%s\n", tree.N, tree.Hash)
} }
var errMalformedTree = errors.New("malformed tree note") var errMalformedTree = errors.New("malformed tree note")
@ -87,7 +87,7 @@ func FormatRecord(id int64, text []byte) (msg []byte, err error) {
if !isValidRecordText(text) { if !isValidRecordText(text) {
return nil, errMalformedRecord return nil, errMalformedRecord
} }
msg = []byte(fmt.Sprintf("%d\n", id)) msg = fmt.Appendf(nil, "%d\n", id)
msg = append(msg, text...) msg = append(msg, text...)
msg = append(msg, '\n') msg = append(msg, '\n')
return msg, nil return msg, nil

View file

@ -194,7 +194,7 @@ func StoredHashesForRecordHash(n int64, h Hash, r HashReader) ([]Hash, error) {
// and consumes a hash from an adjacent subtree. // and consumes a hash from an adjacent subtree.
m := int(bits.TrailingZeros64(uint64(n + 1))) m := int(bits.TrailingZeros64(uint64(n + 1)))
indexes := make([]int64, m) indexes := make([]int64, m)
for i := 0; i < m; i++ { for i := range m {
// We arrange indexes in sorted order. // We arrange indexes in sorted order.
// Note that n>>i is always odd. // Note that n>>i is always odd.
indexes[m-1-i] = StoredHashIndex(i, n>>uint(i)-1) indexes[m-1-i] = StoredHashIndex(i, n>>uint(i)-1)
@ -210,7 +210,7 @@ func StoredHashesForRecordHash(n int64, h Hash, r HashReader) ([]Hash, error) {
} }
// Build new hashes. // Build new hashes.
for i := 0; i < m; i++ { for i := range m {
h = NodeHash(old[m-1-i], h) h = NodeHash(old[m-1-i], h)
hashes = append(hashes, h) hashes = append(hashes, h)
} }

View file

@ -780,7 +780,7 @@ func (fi dataFileInfo) Size() int64 { return int64(len(fi.f.data)) }
func (fi dataFileInfo) Mode() os.FileMode { return 0644 } func (fi dataFileInfo) Mode() os.FileMode { return 0644 }
func (fi dataFileInfo) ModTime() time.Time { return time.Time{} } func (fi dataFileInfo) ModTime() time.Time { return time.Time{} }
func (fi dataFileInfo) IsDir() bool { return false } func (fi dataFileInfo) IsDir() bool { return false }
func (fi dataFileInfo) Sys() interface{} { return nil } func (fi dataFileInfo) Sys() any { return nil }
// isVendoredPackage attempts to report whether the given filename is contained // isVendoredPackage attempts to report whether the given filename is contained
// in a package whose import path contains (but does not end with) the component // in a package whose import path contains (but does not end with) the component

View file

@ -3,7 +3,7 @@
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// Package errgroup provides synchronization, error propagation, and Context // Package errgroup provides synchronization, error propagation, and Context
// cancelation for groups of goroutines working on subtasks of a common task. // cancellation for groups of goroutines working on subtasks of a common task.
// //
// [errgroup.Group] is related to [sync.WaitGroup] but adds handling of tasks // [errgroup.Group] is related to [sync.WaitGroup] but adds handling of tasks
// returning errors. // returning errors.

View file

@ -226,6 +226,7 @@ struct ltchars {
#include <linux/cryptouser.h> #include <linux/cryptouser.h>
#include <linux/devlink.h> #include <linux/devlink.h>
#include <linux/dm-ioctl.h> #include <linux/dm-ioctl.h>
#include <linux/elf.h>
#include <linux/errqueue.h> #include <linux/errqueue.h>
#include <linux/ethtool_netlink.h> #include <linux/ethtool_netlink.h>
#include <linux/falloc.h> #include <linux/falloc.h>
@ -529,6 +530,7 @@ ccflags="$@"
$2 ~ /^O[CNPFPL][A-Z]+[^_][A-Z]+$/ || $2 ~ /^O[CNPFPL][A-Z]+[^_][A-Z]+$/ ||
$2 ~ /^(NL|CR|TAB|BS|VT|FF)DLY$/ || $2 ~ /^(NL|CR|TAB|BS|VT|FF)DLY$/ ||
$2 ~ /^(NL|CR|TAB|BS|VT|FF)[0-9]$/ || $2 ~ /^(NL|CR|TAB|BS|VT|FF)[0-9]$/ ||
$2 ~ /^(DT|EI|ELF|EV|NN|NT|PF|SHF|SHN|SHT|STB|STT|VER)_/ ||
$2 ~ /^O?XTABS$/ || $2 ~ /^O?XTABS$/ ||
$2 ~ /^TC[IO](ON|OFF)$/ || $2 ~ /^TC[IO](ON|OFF)$/ ||
$2 ~ /^IN_/ || $2 ~ /^IN_/ ||

View file

@ -2643,3 +2643,9 @@ func SchedGetAttr(pid int, flags uint) (*SchedAttr, error) {
//sys Cachestat(fd uint, crange *CachestatRange, cstat *Cachestat_t, flags uint) (err error) //sys Cachestat(fd uint, crange *CachestatRange, cstat *Cachestat_t, flags uint) (err error)
//sys Mseal(b []byte, flags uint) (err error) //sys Mseal(b []byte, flags uint) (err error)
//sys setMemPolicy(mode int, mask *CPUSet, size int) (err error) = SYS_SET_MEMPOLICY
func SetMemPolicy(mode int, mask *CPUSet) error {
return setMemPolicy(mode, mask, _CPU_SETSIZE)
}

View file

@ -853,20 +853,86 @@ const (
DM_VERSION_MAJOR = 0x4 DM_VERSION_MAJOR = 0x4
DM_VERSION_MINOR = 0x32 DM_VERSION_MINOR = 0x32
DM_VERSION_PATCHLEVEL = 0x0 DM_VERSION_PATCHLEVEL = 0x0
DT_ADDRRNGHI = 0x6ffffeff
DT_ADDRRNGLO = 0x6ffffe00
DT_BLK = 0x6 DT_BLK = 0x6
DT_CHR = 0x2 DT_CHR = 0x2
DT_DEBUG = 0x15
DT_DIR = 0x4 DT_DIR = 0x4
DT_ENCODING = 0x20
DT_FIFO = 0x1 DT_FIFO = 0x1
DT_FINI = 0xd
DT_FLAGS_1 = 0x6ffffffb
DT_GNU_HASH = 0x6ffffef5
DT_HASH = 0x4
DT_HIOS = 0x6ffff000
DT_HIPROC = 0x7fffffff
DT_INIT = 0xc
DT_JMPREL = 0x17
DT_LNK = 0xa DT_LNK = 0xa
DT_LOOS = 0x6000000d
DT_LOPROC = 0x70000000
DT_NEEDED = 0x1
DT_NULL = 0x0
DT_PLTGOT = 0x3
DT_PLTREL = 0x14
DT_PLTRELSZ = 0x2
DT_REG = 0x8 DT_REG = 0x8
DT_REL = 0x11
DT_RELA = 0x7
DT_RELACOUNT = 0x6ffffff9
DT_RELAENT = 0x9
DT_RELASZ = 0x8
DT_RELCOUNT = 0x6ffffffa
DT_RELENT = 0x13
DT_RELSZ = 0x12
DT_RPATH = 0xf
DT_SOCK = 0xc DT_SOCK = 0xc
DT_SONAME = 0xe
DT_STRSZ = 0xa
DT_STRTAB = 0x5
DT_SYMBOLIC = 0x10
DT_SYMENT = 0xb
DT_SYMTAB = 0x6
DT_TEXTREL = 0x16
DT_UNKNOWN = 0x0 DT_UNKNOWN = 0x0
DT_VALRNGHI = 0x6ffffdff
DT_VALRNGLO = 0x6ffffd00
DT_VERDEF = 0x6ffffffc
DT_VERDEFNUM = 0x6ffffffd
DT_VERNEED = 0x6ffffffe
DT_VERNEEDNUM = 0x6fffffff
DT_VERSYM = 0x6ffffff0
DT_WHT = 0xe DT_WHT = 0xe
ECHO = 0x8 ECHO = 0x8
ECRYPTFS_SUPER_MAGIC = 0xf15f ECRYPTFS_SUPER_MAGIC = 0xf15f
EFD_SEMAPHORE = 0x1 EFD_SEMAPHORE = 0x1
EFIVARFS_MAGIC = 0xde5e81e4 EFIVARFS_MAGIC = 0xde5e81e4
EFS_SUPER_MAGIC = 0x414a53 EFS_SUPER_MAGIC = 0x414a53
EI_CLASS = 0x4
EI_DATA = 0x5
EI_MAG0 = 0x0
EI_MAG1 = 0x1
EI_MAG2 = 0x2
EI_MAG3 = 0x3
EI_NIDENT = 0x10
EI_OSABI = 0x7
EI_PAD = 0x8
EI_VERSION = 0x6
ELFCLASS32 = 0x1
ELFCLASS64 = 0x2
ELFCLASSNONE = 0x0
ELFCLASSNUM = 0x3
ELFDATA2LSB = 0x1
ELFDATA2MSB = 0x2
ELFDATANONE = 0x0
ELFMAG = "\177ELF"
ELFMAG0 = 0x7f
ELFMAG1 = 'E'
ELFMAG2 = 'L'
ELFMAG3 = 'F'
ELFOSABI_LINUX = 0x3
ELFOSABI_NONE = 0x0
EM_386 = 0x3 EM_386 = 0x3
EM_486 = 0x6 EM_486 = 0x6
EM_68K = 0x4 EM_68K = 0x4
@ -1152,14 +1218,24 @@ const (
ETH_P_WCCP = 0x883e ETH_P_WCCP = 0x883e
ETH_P_X25 = 0x805 ETH_P_X25 = 0x805
ETH_P_XDSA = 0xf8 ETH_P_XDSA = 0xf8
ET_CORE = 0x4
ET_DYN = 0x3
ET_EXEC = 0x2
ET_HIPROC = 0xffff
ET_LOPROC = 0xff00
ET_NONE = 0x0
ET_REL = 0x1
EV_ABS = 0x3 EV_ABS = 0x3
EV_CNT = 0x20 EV_CNT = 0x20
EV_CURRENT = 0x1
EV_FF = 0x15 EV_FF = 0x15
EV_FF_STATUS = 0x17 EV_FF_STATUS = 0x17
EV_KEY = 0x1 EV_KEY = 0x1
EV_LED = 0x11 EV_LED = 0x11
EV_MAX = 0x1f EV_MAX = 0x1f
EV_MSC = 0x4 EV_MSC = 0x4
EV_NONE = 0x0
EV_NUM = 0x2
EV_PWR = 0x16 EV_PWR = 0x16
EV_REL = 0x2 EV_REL = 0x2
EV_REP = 0x14 EV_REP = 0x14
@ -2276,7 +2352,167 @@ const (
NLM_F_REPLACE = 0x100 NLM_F_REPLACE = 0x100
NLM_F_REQUEST = 0x1 NLM_F_REQUEST = 0x1
NLM_F_ROOT = 0x100 NLM_F_ROOT = 0x100
NN_386_IOPERM = "LINUX"
NN_386_TLS = "LINUX"
NN_ARC_V2 = "LINUX"
NN_ARM_FPMR = "LINUX"
NN_ARM_GCS = "LINUX"
NN_ARM_HW_BREAK = "LINUX"
NN_ARM_HW_WATCH = "LINUX"
NN_ARM_PACA_KEYS = "LINUX"
NN_ARM_PACG_KEYS = "LINUX"
NN_ARM_PAC_ENABLED_KEYS = "LINUX"
NN_ARM_PAC_MASK = "LINUX"
NN_ARM_POE = "LINUX"
NN_ARM_SSVE = "LINUX"
NN_ARM_SVE = "LINUX"
NN_ARM_SYSTEM_CALL = "LINUX"
NN_ARM_TAGGED_ADDR_CTRL = "LINUX"
NN_ARM_TLS = "LINUX"
NN_ARM_VFP = "LINUX"
NN_ARM_ZA = "LINUX"
NN_ARM_ZT = "LINUX"
NN_AUXV = "CORE"
NN_FILE = "CORE"
NN_GNU_PROPERTY_TYPE_0 = "GNU"
NN_LOONGARCH_CPUCFG = "LINUX"
NN_LOONGARCH_CSR = "LINUX"
NN_LOONGARCH_HW_BREAK = "LINUX"
NN_LOONGARCH_HW_WATCH = "LINUX"
NN_LOONGARCH_LASX = "LINUX"
NN_LOONGARCH_LBT = "LINUX"
NN_LOONGARCH_LSX = "LINUX"
NN_MIPS_DSP = "LINUX"
NN_MIPS_FP_MODE = "LINUX"
NN_MIPS_MSA = "LINUX"
NN_PPC_DEXCR = "LINUX"
NN_PPC_DSCR = "LINUX"
NN_PPC_EBB = "LINUX"
NN_PPC_HASHKEYR = "LINUX"
NN_PPC_PKEY = "LINUX"
NN_PPC_PMU = "LINUX"
NN_PPC_PPR = "LINUX"
NN_PPC_SPE = "LINUX"
NN_PPC_TAR = "LINUX"
NN_PPC_TM_CDSCR = "LINUX"
NN_PPC_TM_CFPR = "LINUX"
NN_PPC_TM_CGPR = "LINUX"
NN_PPC_TM_CPPR = "LINUX"
NN_PPC_TM_CTAR = "LINUX"
NN_PPC_TM_CVMX = "LINUX"
NN_PPC_TM_CVSX = "LINUX"
NN_PPC_TM_SPR = "LINUX"
NN_PPC_VMX = "LINUX"
NN_PPC_VSX = "LINUX"
NN_PRFPREG = "CORE"
NN_PRPSINFO = "CORE"
NN_PRSTATUS = "CORE"
NN_PRXFPREG = "LINUX"
NN_RISCV_CSR = "LINUX"
NN_RISCV_TAGGED_ADDR_CTRL = "LINUX"
NN_RISCV_VECTOR = "LINUX"
NN_S390_CTRS = "LINUX"
NN_S390_GS_BC = "LINUX"
NN_S390_GS_CB = "LINUX"
NN_S390_HIGH_GPRS = "LINUX"
NN_S390_LAST_BREAK = "LINUX"
NN_S390_PREFIX = "LINUX"
NN_S390_PV_CPU_DATA = "LINUX"
NN_S390_RI_CB = "LINUX"
NN_S390_SYSTEM_CALL = "LINUX"
NN_S390_TDB = "LINUX"
NN_S390_TIMER = "LINUX"
NN_S390_TODCMP = "LINUX"
NN_S390_TODPREG = "LINUX"
NN_S390_VXRS_HIGH = "LINUX"
NN_S390_VXRS_LOW = "LINUX"
NN_SIGINFO = "CORE"
NN_TASKSTRUCT = "CORE"
NN_VMCOREDD = "LINUX"
NN_X86_SHSTK = "LINUX"
NN_X86_XSAVE_LAYOUT = "LINUX"
NN_X86_XSTATE = "LINUX"
NSFS_MAGIC = 0x6e736673 NSFS_MAGIC = 0x6e736673
NT_386_IOPERM = 0x201
NT_386_TLS = 0x200
NT_ARC_V2 = 0x600
NT_ARM_FPMR = 0x40e
NT_ARM_GCS = 0x410
NT_ARM_HW_BREAK = 0x402
NT_ARM_HW_WATCH = 0x403
NT_ARM_PACA_KEYS = 0x407
NT_ARM_PACG_KEYS = 0x408
NT_ARM_PAC_ENABLED_KEYS = 0x40a
NT_ARM_PAC_MASK = 0x406
NT_ARM_POE = 0x40f
NT_ARM_SSVE = 0x40b
NT_ARM_SVE = 0x405
NT_ARM_SYSTEM_CALL = 0x404
NT_ARM_TAGGED_ADDR_CTRL = 0x409
NT_ARM_TLS = 0x401
NT_ARM_VFP = 0x400
NT_ARM_ZA = 0x40c
NT_ARM_ZT = 0x40d
NT_AUXV = 0x6
NT_FILE = 0x46494c45
NT_GNU_PROPERTY_TYPE_0 = 0x5
NT_LOONGARCH_CPUCFG = 0xa00
NT_LOONGARCH_CSR = 0xa01
NT_LOONGARCH_HW_BREAK = 0xa05
NT_LOONGARCH_HW_WATCH = 0xa06
NT_LOONGARCH_LASX = 0xa03
NT_LOONGARCH_LBT = 0xa04
NT_LOONGARCH_LSX = 0xa02
NT_MIPS_DSP = 0x800
NT_MIPS_FP_MODE = 0x801
NT_MIPS_MSA = 0x802
NT_PPC_DEXCR = 0x111
NT_PPC_DSCR = 0x105
NT_PPC_EBB = 0x106
NT_PPC_HASHKEYR = 0x112
NT_PPC_PKEY = 0x110
NT_PPC_PMU = 0x107
NT_PPC_PPR = 0x104
NT_PPC_SPE = 0x101
NT_PPC_TAR = 0x103
NT_PPC_TM_CDSCR = 0x10f
NT_PPC_TM_CFPR = 0x109
NT_PPC_TM_CGPR = 0x108
NT_PPC_TM_CPPR = 0x10e
NT_PPC_TM_CTAR = 0x10d
NT_PPC_TM_CVMX = 0x10a
NT_PPC_TM_CVSX = 0x10b
NT_PPC_TM_SPR = 0x10c
NT_PPC_VMX = 0x100
NT_PPC_VSX = 0x102
NT_PRFPREG = 0x2
NT_PRPSINFO = 0x3
NT_PRSTATUS = 0x1
NT_PRXFPREG = 0x46e62b7f
NT_RISCV_CSR = 0x900
NT_RISCV_TAGGED_ADDR_CTRL = 0x902
NT_RISCV_VECTOR = 0x901
NT_S390_CTRS = 0x304
NT_S390_GS_BC = 0x30c
NT_S390_GS_CB = 0x30b
NT_S390_HIGH_GPRS = 0x300
NT_S390_LAST_BREAK = 0x306
NT_S390_PREFIX = 0x305
NT_S390_PV_CPU_DATA = 0x30e
NT_S390_RI_CB = 0x30d
NT_S390_SYSTEM_CALL = 0x307
NT_S390_TDB = 0x308
NT_S390_TIMER = 0x301
NT_S390_TODCMP = 0x302
NT_S390_TODPREG = 0x303
NT_S390_VXRS_HIGH = 0x30a
NT_S390_VXRS_LOW = 0x309
NT_SIGINFO = 0x53494749
NT_TASKSTRUCT = 0x4
NT_VMCOREDD = 0x700
NT_X86_SHSTK = 0x204
NT_X86_XSAVE_LAYOUT = 0x205
NT_X86_XSTATE = 0x202
OCFS2_SUPER_MAGIC = 0x7461636f OCFS2_SUPER_MAGIC = 0x7461636f
OCRNL = 0x8 OCRNL = 0x8
OFDEL = 0x80 OFDEL = 0x80
@ -2463,6 +2699,59 @@ const (
PERF_RECORD_MISC_USER = 0x2 PERF_RECORD_MISC_USER = 0x2
PERF_SAMPLE_BRANCH_PLM_ALL = 0x7 PERF_SAMPLE_BRANCH_PLM_ALL = 0x7
PERF_SAMPLE_WEIGHT_TYPE = 0x1004000 PERF_SAMPLE_WEIGHT_TYPE = 0x1004000
PF_ALG = 0x26
PF_APPLETALK = 0x5
PF_ASH = 0x12
PF_ATMPVC = 0x8
PF_ATMSVC = 0x14
PF_AX25 = 0x3
PF_BLUETOOTH = 0x1f
PF_BRIDGE = 0x7
PF_CAIF = 0x25
PF_CAN = 0x1d
PF_DECnet = 0xc
PF_ECONET = 0x13
PF_FILE = 0x1
PF_IB = 0x1b
PF_IEEE802154 = 0x24
PF_INET = 0x2
PF_INET6 = 0xa
PF_IPX = 0x4
PF_IRDA = 0x17
PF_ISDN = 0x22
PF_IUCV = 0x20
PF_KCM = 0x29
PF_KEY = 0xf
PF_LLC = 0x1a
PF_LOCAL = 0x1
PF_MAX = 0x2e
PF_MCTP = 0x2d
PF_MPLS = 0x1c
PF_NETBEUI = 0xd
PF_NETLINK = 0x10
PF_NETROM = 0x6
PF_NFC = 0x27
PF_PACKET = 0x11
PF_PHONET = 0x23
PF_PPPOX = 0x18
PF_QIPCRTR = 0x2a
PF_R = 0x4
PF_RDS = 0x15
PF_ROSE = 0xb
PF_ROUTE = 0x10
PF_RXRPC = 0x21
PF_SECURITY = 0xe
PF_SMC = 0x2b
PF_SNA = 0x16
PF_TIPC = 0x1e
PF_UNIX = 0x1
PF_UNSPEC = 0x0
PF_VSOCK = 0x28
PF_W = 0x2
PF_WANPIPE = 0x19
PF_X = 0x1
PF_X25 = 0x9
PF_XDP = 0x2c
PID_FS_MAGIC = 0x50494446 PID_FS_MAGIC = 0x50494446
PIPEFS_MAGIC = 0x50495045 PIPEFS_MAGIC = 0x50495045
PPPIOCGNPMODE = 0xc008744c PPPIOCGNPMODE = 0xc008744c
@ -2758,6 +3047,23 @@ const (
PTRACE_SYSCALL_INFO_NONE = 0x0 PTRACE_SYSCALL_INFO_NONE = 0x0
PTRACE_SYSCALL_INFO_SECCOMP = 0x3 PTRACE_SYSCALL_INFO_SECCOMP = 0x3
PTRACE_TRACEME = 0x0 PTRACE_TRACEME = 0x0
PT_AARCH64_MEMTAG_MTE = 0x70000002
PT_DYNAMIC = 0x2
PT_GNU_EH_FRAME = 0x6474e550
PT_GNU_PROPERTY = 0x6474e553
PT_GNU_RELRO = 0x6474e552
PT_GNU_STACK = 0x6474e551
PT_HIOS = 0x6fffffff
PT_HIPROC = 0x7fffffff
PT_INTERP = 0x3
PT_LOAD = 0x1
PT_LOOS = 0x60000000
PT_LOPROC = 0x70000000
PT_NOTE = 0x4
PT_NULL = 0x0
PT_PHDR = 0x6
PT_SHLIB = 0x5
PT_TLS = 0x7
P_ALL = 0x0 P_ALL = 0x0
P_PGID = 0x2 P_PGID = 0x2
P_PID = 0x1 P_PID = 0x1
@ -3091,6 +3397,47 @@ const (
SEEK_MAX = 0x4 SEEK_MAX = 0x4
SEEK_SET = 0x0 SEEK_SET = 0x0
SELINUX_MAGIC = 0xf97cff8c SELINUX_MAGIC = 0xf97cff8c
SHF_ALLOC = 0x2
SHF_EXCLUDE = 0x8000000
SHF_EXECINSTR = 0x4
SHF_GROUP = 0x200
SHF_INFO_LINK = 0x40
SHF_LINK_ORDER = 0x80
SHF_MASKOS = 0xff00000
SHF_MASKPROC = 0xf0000000
SHF_MERGE = 0x10
SHF_ORDERED = 0x4000000
SHF_OS_NONCONFORMING = 0x100
SHF_RELA_LIVEPATCH = 0x100000
SHF_RO_AFTER_INIT = 0x200000
SHF_STRINGS = 0x20
SHF_TLS = 0x400
SHF_WRITE = 0x1
SHN_ABS = 0xfff1
SHN_COMMON = 0xfff2
SHN_HIPROC = 0xff1f
SHN_HIRESERVE = 0xffff
SHN_LIVEPATCH = 0xff20
SHN_LOPROC = 0xff00
SHN_LORESERVE = 0xff00
SHN_UNDEF = 0x0
SHT_DYNAMIC = 0x6
SHT_DYNSYM = 0xb
SHT_HASH = 0x5
SHT_HIPROC = 0x7fffffff
SHT_HIUSER = 0xffffffff
SHT_LOPROC = 0x70000000
SHT_LOUSER = 0x80000000
SHT_NOBITS = 0x8
SHT_NOTE = 0x7
SHT_NULL = 0x0
SHT_NUM = 0xc
SHT_PROGBITS = 0x1
SHT_REL = 0x9
SHT_RELA = 0x4
SHT_SHLIB = 0xa
SHT_STRTAB = 0x3
SHT_SYMTAB = 0x2
SHUT_RD = 0x0 SHUT_RD = 0x0
SHUT_RDWR = 0x2 SHUT_RDWR = 0x2
SHUT_WR = 0x1 SHUT_WR = 0x1
@ -3317,6 +3664,16 @@ const (
STATX_UID = 0x8 STATX_UID = 0x8
STATX_WRITE_ATOMIC = 0x10000 STATX_WRITE_ATOMIC = 0x10000
STATX__RESERVED = 0x80000000 STATX__RESERVED = 0x80000000
STB_GLOBAL = 0x1
STB_LOCAL = 0x0
STB_WEAK = 0x2
STT_COMMON = 0x5
STT_FILE = 0x4
STT_FUNC = 0x2
STT_NOTYPE = 0x0
STT_OBJECT = 0x1
STT_SECTION = 0x3
STT_TLS = 0x6
SYNC_FILE_RANGE_WAIT_AFTER = 0x4 SYNC_FILE_RANGE_WAIT_AFTER = 0x4
SYNC_FILE_RANGE_WAIT_BEFORE = 0x1 SYNC_FILE_RANGE_WAIT_BEFORE = 0x1
SYNC_FILE_RANGE_WRITE = 0x2 SYNC_FILE_RANGE_WRITE = 0x2
@ -3553,6 +3910,8 @@ const (
UTIME_OMIT = 0x3ffffffe UTIME_OMIT = 0x3ffffffe
V9FS_MAGIC = 0x1021997 V9FS_MAGIC = 0x1021997
VERASE = 0x2 VERASE = 0x2
VER_FLG_BASE = 0x1
VER_FLG_WEAK = 0x2
VINTR = 0x0 VINTR = 0x0
VKILL = 0x3 VKILL = 0x3
VLNEXT = 0xf VLNEXT = 0xf

View file

@ -2238,3 +2238,13 @@ func Mseal(b []byte, flags uint) (err error) {
} }
return return
} }
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT
func setMemPolicy(mode int, mask *CPUSet, size int) (err error) {
_, _, e1 := Syscall(SYS_SET_MEMPOLICY, uintptr(mode), uintptr(unsafe.Pointer(mask)), uintptr(size))
if e1 != 0 {
err = errnoErr(e1)
}
return
}

View file

@ -3590,6 +3590,8 @@ type Nhmsg struct {
Flags uint32 Flags uint32
} }
const SizeofNhmsg = 0x8
type NexthopGrp struct { type NexthopGrp struct {
Id uint32 Id uint32
Weight uint8 Weight uint8
@ -3597,6 +3599,8 @@ type NexthopGrp struct {
Resvd2 uint16 Resvd2 uint16
} }
const SizeofNexthopGrp = 0x8
const ( const (
NHA_UNSPEC = 0x0 NHA_UNSPEC = 0x0
NHA_ID = 0x1 NHA_ID = 0x1
@ -6332,3 +6336,30 @@ type SockDiagReq struct {
} }
const RTM_NEWNVLAN = 0x70 const RTM_NEWNVLAN = 0x70
const (
MPOL_BIND = 0x2
MPOL_DEFAULT = 0x0
MPOL_F_ADDR = 0x2
MPOL_F_MEMS_ALLOWED = 0x4
MPOL_F_MOF = 0x8
MPOL_F_MORON = 0x10
MPOL_F_NODE = 0x1
MPOL_F_NUMA_BALANCING = 0x2000
MPOL_F_RELATIVE_NODES = 0x4000
MPOL_F_SHARED = 0x1
MPOL_F_STATIC_NODES = 0x8000
MPOL_INTERLEAVE = 0x3
MPOL_LOCAL = 0x4
MPOL_MAX = 0x7
MPOL_MF_INTERNAL = 0x10
MPOL_MF_LAZY = 0x8
MPOL_MF_MOVE_ALL = 0x4
MPOL_MF_MOVE = 0x2
MPOL_MF_STRICT = 0x1
MPOL_MF_VALID = 0x7
MPOL_MODE_FLAGS = 0xe000
MPOL_PREFERRED = 0x1
MPOL_PREFERRED_MANY = 0x5
MPOL_WEIGHTED_INTERLEAVE = 0x6
)

View file

@ -892,8 +892,12 @@ const socket_error = uintptr(^uint32(0))
//sys MultiByteToWideChar(codePage uint32, dwFlags uint32, str *byte, nstr int32, wchar *uint16, nwchar int32) (nwrite int32, err error) = kernel32.MultiByteToWideChar //sys MultiByteToWideChar(codePage uint32, dwFlags uint32, str *byte, nstr int32, wchar *uint16, nwchar int32) (nwrite int32, err error) = kernel32.MultiByteToWideChar
//sys getBestInterfaceEx(sockaddr unsafe.Pointer, pdwBestIfIndex *uint32) (errcode error) = iphlpapi.GetBestInterfaceEx //sys getBestInterfaceEx(sockaddr unsafe.Pointer, pdwBestIfIndex *uint32) (errcode error) = iphlpapi.GetBestInterfaceEx
//sys GetIfEntry2Ex(level uint32, row *MibIfRow2) (errcode error) = iphlpapi.GetIfEntry2Ex //sys GetIfEntry2Ex(level uint32, row *MibIfRow2) (errcode error) = iphlpapi.GetIfEntry2Ex
//sys GetIpForwardEntry2(row *MibIpForwardRow2) (errcode error) = iphlpapi.GetIpForwardEntry2
//sys GetIpForwardTable2(family uint16, table **MibIpForwardTable2) (errcode error) = iphlpapi.GetIpForwardTable2
//sys GetUnicastIpAddressEntry(row *MibUnicastIpAddressRow) (errcode error) = iphlpapi.GetUnicastIpAddressEntry //sys GetUnicastIpAddressEntry(row *MibUnicastIpAddressRow) (errcode error) = iphlpapi.GetUnicastIpAddressEntry
//sys FreeMibTable(memory unsafe.Pointer) = iphlpapi.FreeMibTable
//sys NotifyIpInterfaceChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyIpInterfaceChange //sys NotifyIpInterfaceChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyIpInterfaceChange
//sys NotifyRouteChange2(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyRouteChange2
//sys NotifyUnicastIpAddressChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyUnicastIpAddressChange //sys NotifyUnicastIpAddressChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyUnicastIpAddressChange
//sys CancelMibChangeNotify2(notificationHandle Handle) (errcode error) = iphlpapi.CancelMibChangeNotify2 //sys CancelMibChangeNotify2(notificationHandle Handle) (errcode error) = iphlpapi.CancelMibChangeNotify2
@ -916,6 +920,17 @@ type RawSockaddrInet6 struct {
Scope_id uint32 Scope_id uint32
} }
// RawSockaddrInet is a union that contains an IPv4, an IPv6 address, or an address family. See
// https://learn.microsoft.com/en-us/windows/win32/api/ws2ipdef/ns-ws2ipdef-sockaddr_inet.
//
// A [*RawSockaddrInet] may be converted to a [*RawSockaddrInet4] or [*RawSockaddrInet6] using
// unsafe, depending on the address family.
type RawSockaddrInet struct {
Family uint16
Port uint16
Data [6]uint32
}
type RawSockaddr struct { type RawSockaddr struct {
Family uint16 Family uint16
Data [14]int8 Data [14]int8

View file

@ -2320,6 +2320,82 @@ type MibIfRow2 struct {
OutQLen uint64 OutQLen uint64
} }
// IP_ADDRESS_PREFIX stores an IP address prefix. See
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-ip_address_prefix.
type IpAddressPrefix struct {
Prefix RawSockaddrInet
PrefixLength uint8
}
// NL_ROUTE_ORIGIN enumeration from nldef.h or
// https://learn.microsoft.com/en-us/windows/win32/api/nldef/ne-nldef-nl_route_origin.
const (
NlroManual = 0
NlroWellKnown = 1
NlroDHCP = 2
NlroRouterAdvertisement = 3
Nlro6to4 = 4
)
// NL_ROUTE_ORIGIN enumeration from nldef.h or
// https://learn.microsoft.com/en-us/windows/win32/api/nldef/ne-nldef-nl_route_protocol.
const (
MIB_IPPROTO_OTHER = 1
MIB_IPPROTO_LOCAL = 2
MIB_IPPROTO_NETMGMT = 3
MIB_IPPROTO_ICMP = 4
MIB_IPPROTO_EGP = 5
MIB_IPPROTO_GGP = 6
MIB_IPPROTO_HELLO = 7
MIB_IPPROTO_RIP = 8
MIB_IPPROTO_IS_IS = 9
MIB_IPPROTO_ES_IS = 10
MIB_IPPROTO_CISCO = 11
MIB_IPPROTO_BBN = 12
MIB_IPPROTO_OSPF = 13
MIB_IPPROTO_BGP = 14
MIB_IPPROTO_IDPR = 15
MIB_IPPROTO_EIGRP = 16
MIB_IPPROTO_DVMRP = 17
MIB_IPPROTO_RPL = 18
MIB_IPPROTO_DHCP = 19
MIB_IPPROTO_NT_AUTOSTATIC = 10002
MIB_IPPROTO_NT_STATIC = 10006
MIB_IPPROTO_NT_STATIC_NON_DOD = 10007
)
// MIB_IPFORWARD_ROW2 stores information about an IP route entry. See
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipforward_row2.
type MibIpForwardRow2 struct {
InterfaceLuid uint64
InterfaceIndex uint32
DestinationPrefix IpAddressPrefix
NextHop RawSockaddrInet
SitePrefixLength uint8
ValidLifetime uint32
PreferredLifetime uint32
Metric uint32
Protocol uint32
Loopback uint8
AutoconfigureAddress uint8
Publish uint8
Immortal uint8
Age uint32
Origin uint32
}
// MIB_IPFORWARD_TABLE2 contains a table of IP route entries. See
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipforward_table2.
type MibIpForwardTable2 struct {
NumEntries uint32
Table [1]MibIpForwardRow2
}
// Rows returns the IP route entries in the table.
func (t *MibIpForwardTable2) Rows() []MibIpForwardRow2 {
return unsafe.Slice(&t.Table[0], t.NumEntries)
}
// MIB_UNICASTIPADDRESS_ROW stores information about a unicast IP address. See // MIB_UNICASTIPADDRESS_ROW stores information about a unicast IP address. See
// https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_unicastipaddress_row. // https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_unicastipaddress_row.
type MibUnicastIpAddressRow struct { type MibUnicastIpAddressRow struct {

View file

@ -182,13 +182,17 @@ var (
procDwmGetWindowAttribute = moddwmapi.NewProc("DwmGetWindowAttribute") procDwmGetWindowAttribute = moddwmapi.NewProc("DwmGetWindowAttribute")
procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute") procDwmSetWindowAttribute = moddwmapi.NewProc("DwmSetWindowAttribute")
procCancelMibChangeNotify2 = modiphlpapi.NewProc("CancelMibChangeNotify2") procCancelMibChangeNotify2 = modiphlpapi.NewProc("CancelMibChangeNotify2")
procFreeMibTable = modiphlpapi.NewProc("FreeMibTable")
procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses") procGetAdaptersAddresses = modiphlpapi.NewProc("GetAdaptersAddresses")
procGetAdaptersInfo = modiphlpapi.NewProc("GetAdaptersInfo") procGetAdaptersInfo = modiphlpapi.NewProc("GetAdaptersInfo")
procGetBestInterfaceEx = modiphlpapi.NewProc("GetBestInterfaceEx") procGetBestInterfaceEx = modiphlpapi.NewProc("GetBestInterfaceEx")
procGetIfEntry = modiphlpapi.NewProc("GetIfEntry") procGetIfEntry = modiphlpapi.NewProc("GetIfEntry")
procGetIfEntry2Ex = modiphlpapi.NewProc("GetIfEntry2Ex") procGetIfEntry2Ex = modiphlpapi.NewProc("GetIfEntry2Ex")
procGetIpForwardEntry2 = modiphlpapi.NewProc("GetIpForwardEntry2")
procGetIpForwardTable2 = modiphlpapi.NewProc("GetIpForwardTable2")
procGetUnicastIpAddressEntry = modiphlpapi.NewProc("GetUnicastIpAddressEntry") procGetUnicastIpAddressEntry = modiphlpapi.NewProc("GetUnicastIpAddressEntry")
procNotifyIpInterfaceChange = modiphlpapi.NewProc("NotifyIpInterfaceChange") procNotifyIpInterfaceChange = modiphlpapi.NewProc("NotifyIpInterfaceChange")
procNotifyRouteChange2 = modiphlpapi.NewProc("NotifyRouteChange2")
procNotifyUnicastIpAddressChange = modiphlpapi.NewProc("NotifyUnicastIpAddressChange") procNotifyUnicastIpAddressChange = modiphlpapi.NewProc("NotifyUnicastIpAddressChange")
procAddDllDirectory = modkernel32.NewProc("AddDllDirectory") procAddDllDirectory = modkernel32.NewProc("AddDllDirectory")
procAssignProcessToJobObject = modkernel32.NewProc("AssignProcessToJobObject") procAssignProcessToJobObject = modkernel32.NewProc("AssignProcessToJobObject")
@ -1624,6 +1628,11 @@ func CancelMibChangeNotify2(notificationHandle Handle) (errcode error) {
return return
} }
func FreeMibTable(memory unsafe.Pointer) {
syscall.SyscallN(procFreeMibTable.Addr(), uintptr(memory))
return
}
func GetAdaptersAddresses(family uint32, flags uint32, reserved uintptr, adapterAddresses *IpAdapterAddresses, sizePointer *uint32) (errcode error) { func GetAdaptersAddresses(family uint32, flags uint32, reserved uintptr, adapterAddresses *IpAdapterAddresses, sizePointer *uint32) (errcode error) {
r0, _, _ := syscall.SyscallN(procGetAdaptersAddresses.Addr(), uintptr(family), uintptr(flags), uintptr(reserved), uintptr(unsafe.Pointer(adapterAddresses)), uintptr(unsafe.Pointer(sizePointer))) r0, _, _ := syscall.SyscallN(procGetAdaptersAddresses.Addr(), uintptr(family), uintptr(flags), uintptr(reserved), uintptr(unsafe.Pointer(adapterAddresses)), uintptr(unsafe.Pointer(sizePointer)))
if r0 != 0 { if r0 != 0 {
@ -1664,6 +1673,22 @@ func GetIfEntry2Ex(level uint32, row *MibIfRow2) (errcode error) {
return return
} }
func GetIpForwardEntry2(row *MibIpForwardRow2) (errcode error) {
r0, _, _ := syscall.SyscallN(procGetIpForwardEntry2.Addr(), uintptr(unsafe.Pointer(row)))
if r0 != 0 {
errcode = syscall.Errno(r0)
}
return
}
func GetIpForwardTable2(family uint16, table **MibIpForwardTable2) (errcode error) {
r0, _, _ := syscall.SyscallN(procGetIpForwardTable2.Addr(), uintptr(family), uintptr(unsafe.Pointer(table)))
if r0 != 0 {
errcode = syscall.Errno(r0)
}
return
}
func GetUnicastIpAddressEntry(row *MibUnicastIpAddressRow) (errcode error) { func GetUnicastIpAddressEntry(row *MibUnicastIpAddressRow) (errcode error) {
r0, _, _ := syscall.SyscallN(procGetUnicastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row))) r0, _, _ := syscall.SyscallN(procGetUnicastIpAddressEntry.Addr(), uintptr(unsafe.Pointer(row)))
if r0 != 0 { if r0 != 0 {
@ -1684,6 +1709,18 @@ func NotifyIpInterfaceChange(family uint16, callback uintptr, callerContext unsa
return return
} }
func NotifyRouteChange2(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) {
var _p0 uint32
if initialNotification {
_p0 = 1
}
r0, _, _ := syscall.SyscallN(procNotifyRouteChange2.Addr(), uintptr(family), uintptr(callback), uintptr(callerContext), uintptr(_p0), uintptr(unsafe.Pointer(notificationHandle)))
if r0 != 0 {
errcode = syscall.Errno(r0)
}
return
}
func NotifyUnicastIpAddressChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) { func NotifyUnicastIpAddressChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) {
var _p0 uint32 var _p0 uint32
if initialNotification { if initialNotification {

1
src/cmd/vendor/golang.org/x/telemetry/codereview.cfg generated vendored Normal file
View file

@ -0,0 +1 @@
issuerepo: golang/go

View file

@ -33,8 +33,9 @@ type Diagnostic struct {
URL string URL string
// SuggestedFixes is an optional list of fixes to address the // SuggestedFixes is an optional list of fixes to address the
// problem described by the diagnostic. Each one represents // problem described by the diagnostic. Each one represents an
// an alternative strategy; at most one may be applied. // alternative strategy, and should have a distinct and
// descriptive message; at most one may be applied.
// //
// Fixes for different diagnostics should be treated as // Fixes for different diagnostics should be treated as
// independent changes to the same baseline file state, // independent changes to the same baseline file state,

View file

@ -13,12 +13,10 @@ import (
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
"go/token"
"io" "io"
"log" "log"
"os" "os"
"strconv" "strconv"
"strings"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
) )
@ -28,7 +26,7 @@ var (
JSON = false // -json JSON = false // -json
Context = -1 // -c=N: if N>0, display offending line plus N lines of context Context = -1 // -c=N: if N>0, display offending line plus N lines of context
Fix bool // -fix Fix bool // -fix
diffFlag bool // -diff (changes [ApplyFixes] behavior) Diff bool // -diff
) )
// Parse creates a flag for each of the analyzer's flags, // Parse creates a flag for each of the analyzer's flags,
@ -78,7 +76,7 @@ func Parse(analyzers []*analysis.Analyzer, multi bool) []*analysis.Analyzer {
flag.BoolVar(&JSON, "json", JSON, "emit JSON output") flag.BoolVar(&JSON, "json", JSON, "emit JSON output")
flag.IntVar(&Context, "c", Context, `display offending line with this many lines of context`) flag.IntVar(&Context, "c", Context, `display offending line with this many lines of context`)
flag.BoolVar(&Fix, "fix", false, "apply all suggested fixes") flag.BoolVar(&Fix, "fix", false, "apply all suggested fixes")
flag.BoolVar(&diffFlag, "diff", false, "with -fix, don't update the files, but print a unified diff") flag.BoolVar(&Diff, "diff", false, "with -fix, don't update the files, but print a unified diff")
// Add shims for legacy vet flags to enable existing // Add shims for legacy vet flags to enable existing
// scripts that run vet to continue to work. // scripts that run vet to continue to work.
@ -310,150 +308,3 @@ var vetLegacyFlags = map[string]string{
"unusedfuncs": "unusedresult.funcs", "unusedfuncs": "unusedresult.funcs",
"unusedstringmethods": "unusedresult.stringmethods", "unusedstringmethods": "unusedresult.stringmethods",
} }
// ---- output helpers common to all drivers ----
//
// These functions should not depend on global state (flags)!
// Really they belong in a different package.
// TODO(adonovan): don't accept an io.Writer if we don't report errors.
// Either accept a bytes.Buffer (infallible), or return a []byte.
// PrintPlain prints a diagnostic in plain text form.
// If contextLines is nonnegative, it also prints the
// offending line plus this many lines of context.
func PrintPlain(out io.Writer, fset *token.FileSet, contextLines int, diag analysis.Diagnostic) {
print := func(pos, end token.Pos, message string) {
posn := fset.Position(pos)
fmt.Fprintf(out, "%s: %s\n", posn, message)
// show offending line plus N lines of context.
if contextLines >= 0 {
end := fset.Position(end)
if !end.IsValid() {
end = posn
}
// TODO(adonovan): highlight the portion of the line indicated
// by pos...end using ASCII art, terminal colors, etc?
data, _ := os.ReadFile(posn.Filename)
lines := strings.Split(string(data), "\n")
for i := posn.Line - contextLines; i <= end.Line+contextLines; i++ {
if 1 <= i && i <= len(lines) {
fmt.Fprintf(out, "%d\t%s\n", i, lines[i-1])
}
}
}
}
print(diag.Pos, diag.End, diag.Message)
for _, rel := range diag.Related {
print(rel.Pos, rel.End, "\t"+rel.Message)
}
}
// A JSONTree is a mapping from package ID to analysis name to result.
// Each result is either a jsonError or a list of JSONDiagnostic.
type JSONTree map[string]map[string]any
// A TextEdit describes the replacement of a portion of a file.
// Start and End are zero-based half-open indices into the original byte
// sequence of the file, and New is the new text.
type JSONTextEdit struct {
Filename string `json:"filename"`
Start int `json:"start"`
End int `json:"end"`
New string `json:"new"`
}
// A JSONSuggestedFix describes an edit that should be applied as a whole or not
// at all. It might contain multiple TextEdits/text_edits if the SuggestedFix
// consists of multiple non-contiguous edits.
type JSONSuggestedFix struct {
Message string `json:"message"`
Edits []JSONTextEdit `json:"edits"`
}
// A JSONDiagnostic describes the JSON schema of an analysis.Diagnostic.
//
// TODO(matloob): include End position if present.
type JSONDiagnostic struct {
Category string `json:"category,omitempty"`
Posn string `json:"posn"` // e.g. "file.go:line:column"
Message string `json:"message"`
SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"`
Related []JSONRelatedInformation `json:"related,omitempty"`
}
// A JSONRelated describes a secondary position and message related to
// a primary diagnostic.
//
// TODO(adonovan): include End position if present.
type JSONRelatedInformation struct {
Posn string `json:"posn"` // e.g. "file.go:line:column"
Message string `json:"message"`
}
// Add adds the result of analysis 'name' on package 'id'.
// The result is either a list of diagnostics or an error.
func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis.Diagnostic, err error) {
var v any
if err != nil {
type jsonError struct {
Err string `json:"error"`
}
v = jsonError{err.Error()}
} else if len(diags) > 0 {
diagnostics := make([]JSONDiagnostic, 0, len(diags))
for _, f := range diags {
var fixes []JSONSuggestedFix
for _, fix := range f.SuggestedFixes {
var edits []JSONTextEdit
for _, edit := range fix.TextEdits {
edits = append(edits, JSONTextEdit{
Filename: fset.Position(edit.Pos).Filename,
Start: fset.Position(edit.Pos).Offset,
End: fset.Position(edit.End).Offset,
New: string(edit.NewText),
})
}
fixes = append(fixes, JSONSuggestedFix{
Message: fix.Message,
Edits: edits,
})
}
var related []JSONRelatedInformation
for _, r := range f.Related {
related = append(related, JSONRelatedInformation{
Posn: fset.Position(r.Pos).String(),
Message: r.Message,
})
}
jdiag := JSONDiagnostic{
Category: f.Category,
Posn: fset.Position(f.Pos).String(),
Message: f.Message,
SuggestedFixes: fixes,
Related: related,
}
diagnostics = append(diagnostics, jdiag)
}
v = diagnostics
}
if v != nil {
m, ok := tree[id]
if !ok {
m = make(map[string]any)
tree[id] = m
}
m[name] = v
}
}
func (tree JSONTree) Print(out io.Writer) error {
data, err := json.MarshalIndent(tree, "", "\t")
if err != nil {
log.Panicf("internal error: JSON marshaling failed: %v", err)
}
_, err = fmt.Fprintf(out, "%s\n", data)
return err
}

View file

@ -15,7 +15,7 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
) )
//go:embed doc.go //go:embed doc.go
@ -23,7 +23,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "appends", Name: "appends",
Doc: analysisinternal.MustExtractDoc(doc, "appends"), Doc: analyzerutil.MustExtractDoc(doc, "appends"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/appends", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/appends",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,

View file

@ -19,7 +19,7 @@ import (
"strings" "strings"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
) )
const Doc = "report mismatches between assembly files and Go declarations" const Doc = "report mismatches between assembly files and Go declarations"
@ -175,7 +175,7 @@ func run(pass *analysis.Pass) (any, error) {
Files: Files:
for _, fname := range sfiles { for _, fname := range sfiles {
content, tf, err := analysisinternal.ReadFile(pass, fname) content, tf, err := analyzerutil.ReadFile(pass, fname)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -18,7 +18,7 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
@ -29,7 +29,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "assign", Name: "assign",
Doc: analysisinternal.MustExtractDoc(doc, "assign"), Doc: analyzerutil.MustExtractDoc(doc, "assign"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/assign", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/assign",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,

View file

@ -13,7 +13,7 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -23,7 +23,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "atomic", Name: "atomic",
Doc: analysisinternal.MustExtractDoc(doc, "atomic"), Doc: analyzerutil.MustExtractDoc(doc, "atomic"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/atomic", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/atomic",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
RunDespiteErrors: true, RunDespiteErrors: true,

View file

@ -14,7 +14,7 @@ import (
"unicode" "unicode"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
) )
const Doc = "check //go:build and // +build directives" const Doc = "check //go:build and // +build directives"
@ -86,7 +86,7 @@ func checkOtherFile(pass *analysis.Pass, filename string) error {
// We cannot use the Go parser, since this may not be a Go source file. // We cannot use the Go parser, since this may not be a Go source file.
// Read the raw bytes instead. // Read the raw bytes instead.
content, tf, err := analysisinternal.ReadFile(pass, filename) content, tf, err := analyzerutil.ReadFile(pass, filename)
if err != nil { if err != nil {
return err return err
} }

View file

@ -350,8 +350,8 @@ func typeOKForCgoCall(t types.Type, m map[types.Type]bool) bool {
case *types.Array: case *types.Array:
return typeOKForCgoCall(t.Elem(), m) return typeOKForCgoCall(t.Elem(), m)
case *types.Struct: case *types.Struct:
for i := 0; i < t.NumFields(); i++ { for field := range t.Fields() {
if !typeOKForCgoCall(t.Field(i).Type(), m) { if !typeOKForCgoCall(field.Type(), m) {
return false return false
} }
} }

View file

@ -328,8 +328,8 @@ func lockPath(tpkg *types.Package, typ types.Type, seen map[types.Type]bool) typ
ttyp, ok := typ.Underlying().(*types.Tuple) ttyp, ok := typ.Underlying().(*types.Tuple)
if ok { if ok {
for i := 0; i < ttyp.Len(); i++ { for v := range ttyp.Variables() {
subpath := lockPath(tpkg, ttyp.At(i).Type(), seen) subpath := lockPath(tpkg, v.Type(), seen)
if subpath != nil { if subpath != nil {
return append(subpath, typ.String()) return append(subpath, typ.String())
} }

View file

@ -16,9 +16,12 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/analysis/passes/internal/ctrlflowinternal"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/cfg" "golang.org/x/tools/go/cfg"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/cfginternal"
"golang.org/x/tools/internal/typesinternal"
) )
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
@ -26,7 +29,7 @@ var Analyzer = &analysis.Analyzer{
Doc: "build a control-flow graph", Doc: "build a control-flow graph",
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/ctrlflow", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/ctrlflow",
Run: run, Run: run,
ResultType: reflect.TypeOf(new(CFGs)), ResultType: reflect.TypeFor[*CFGs](),
FactTypes: []analysis.Fact{new(noReturn)}, FactTypes: []analysis.Fact{new(noReturn)},
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
} }
@ -44,9 +47,22 @@ type CFGs struct {
defs map[*ast.Ident]types.Object // from Pass.TypesInfo.Defs defs map[*ast.Ident]types.Object // from Pass.TypesInfo.Defs
funcDecls map[*types.Func]*declInfo funcDecls map[*types.Func]*declInfo
funcLits map[*ast.FuncLit]*litInfo funcLits map[*ast.FuncLit]*litInfo
noReturn map[*types.Func]bool // functions lacking a reachable return statement
pass *analysis.Pass // transient; nil after construction pass *analysis.Pass // transient; nil after construction
} }
// TODO(adonovan): add (*CFGs).NoReturn to public API.
func (c *CFGs) isNoReturn(fn *types.Func) bool {
return c.noReturn[fn]
}
func init() {
// Expose the hidden method to callers in x/tools.
ctrlflowinternal.NoReturn = func(c any, fn *types.Func) bool {
return c.(*CFGs).isNoReturn(fn)
}
}
// CFGs has two maps: funcDecls for named functions and funcLits for // CFGs has two maps: funcDecls for named functions and funcLits for
// unnamed ones. Unlike funcLits, the funcDecls map is not keyed by its // unnamed ones. Unlike funcLits, the funcDecls map is not keyed by its
// syntax node, *ast.FuncDecl, because callMayReturn needs to do a // syntax node, *ast.FuncDecl, because callMayReturn needs to do a
@ -57,12 +73,11 @@ type declInfo struct {
decl *ast.FuncDecl decl *ast.FuncDecl
cfg *cfg.CFG // iff decl.Body != nil cfg *cfg.CFG // iff decl.Body != nil
started bool // to break cycles started bool // to break cycles
noReturn bool
} }
type litInfo struct { type litInfo struct {
cfg *cfg.CFG cfg *cfg.CFG
noReturn bool noReturn bool // (currently unused)
} }
// FuncDecl returns the control-flow graph for a named function. // FuncDecl returns the control-flow graph for a named function.
@ -118,6 +133,7 @@ func run(pass *analysis.Pass) (any, error) {
defs: pass.TypesInfo.Defs, defs: pass.TypesInfo.Defs,
funcDecls: funcDecls, funcDecls: funcDecls,
funcLits: funcLits, funcLits: funcLits,
noReturn: make(map[*types.Func]bool),
pass: pass, pass: pass,
} }
@ -138,7 +154,7 @@ func run(pass *analysis.Pass) (any, error) {
li := funcLits[lit] li := funcLits[lit]
if li.cfg == nil { if li.cfg == nil {
li.cfg = cfg.New(lit.Body, c.callMayReturn) li.cfg = cfg.New(lit.Body, c.callMayReturn)
if !hasReachableReturn(li.cfg) { if cfginternal.IsNoReturn(li.cfg) {
li.noReturn = true li.noReturn = true
} }
} }
@ -158,26 +174,27 @@ func (c *CFGs) buildDecl(fn *types.Func, di *declInfo) {
// The buildDecl call tree thus resembles the static call graph. // The buildDecl call tree thus resembles the static call graph.
// We mark each node when we start working on it to break cycles. // We mark each node when we start working on it to break cycles.
if !di.started { // break cycle if di.started {
return // break cycle
}
di.started = true di.started = true
if isIntrinsicNoReturn(fn) { noreturn := isIntrinsicNoReturn(fn)
di.noReturn = true
}
if di.decl.Body != nil { if di.decl.Body != nil {
di.cfg = cfg.New(di.decl.Body, c.callMayReturn) di.cfg = cfg.New(di.decl.Body, c.callMayReturn)
if !hasReachableReturn(di.cfg) { if cfginternal.IsNoReturn(di.cfg) {
di.noReturn = true noreturn = true
} }
} }
if di.noReturn { if noreturn {
c.pass.ExportObjectFact(fn, new(noReturn)) c.pass.ExportObjectFact(fn, new(noReturn))
c.noReturn[fn] = true
} }
// debugging // debugging
if false { if false {
log.Printf("CFG for %s:\n%s (noreturn=%t)\n", fn, di.cfg.Format(c.pass.Fset), di.noReturn) log.Printf("CFG for %s:\n%s (noreturn=%t)\n", fn, di.cfg.Format(c.pass.Fset), noreturn)
}
} }
} }
@ -201,31 +218,26 @@ func (c *CFGs) callMayReturn(call *ast.CallExpr) (r bool) {
// Function or method declared in this package? // Function or method declared in this package?
if di, ok := c.funcDecls[fn]; ok { if di, ok := c.funcDecls[fn]; ok {
c.buildDecl(fn, di) c.buildDecl(fn, di)
return !di.noReturn return !c.noReturn[fn]
} }
// Not declared in this package. // Not declared in this package.
// Is there a fact from another package? // Is there a fact from another package?
return !c.pass.ImportObjectFact(fn, new(noReturn)) if c.pass.ImportObjectFact(fn, new(noReturn)) {
c.noReturn[fn] = true
return false
}
return true
} }
var panicBuiltin = types.Universe.Lookup("panic").(*types.Builtin) var panicBuiltin = types.Universe.Lookup("panic").(*types.Builtin)
func hasReachableReturn(g *cfg.CFG) bool {
for _, b := range g.Blocks {
if b.Live && b.Return() != nil {
return true
}
}
return false
}
// isIntrinsicNoReturn reports whether a function intrinsically never // isIntrinsicNoReturn reports whether a function intrinsically never
// returns because it stops execution of the calling thread. // returns because it stops execution of the calling thread.
// It is the base case in the recursion. // It is the base case in the recursion.
func isIntrinsicNoReturn(fn *types.Func) bool { func isIntrinsicNoReturn(fn *types.Func) bool {
// Add functions here as the need arises, but don't allocate memory. // Add functions here as the need arises, but don't allocate memory.
path, name := fn.Pkg().Path(), fn.Name() return typesinternal.IsFunctionNamed(fn, "syscall", "Exit", "ExitProcess", "ExitThread") ||
return path == "syscall" && (name == "Exit" || name == "ExitProcess" || name == "ExitThread") || typesinternal.IsFunctionNamed(fn, "runtime", "Goexit")
path == "runtime" && name == "Goexit"
} }

View file

@ -12,7 +12,7 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -23,7 +23,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "defers", Name: "defers",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Doc: analysisinternal.MustExtractDoc(doc, "defers"), Doc: analyzerutil.MustExtractDoc(doc, "defers"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/defers", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/defers",
Run: run, Run: run,
} }

View file

@ -14,7 +14,7 @@ import (
"unicode/utf8" "unicode/utf8"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
) )
const Doc = `check Go toolchain directives such as //go:debug const Doc = `check Go toolchain directives such as //go:debug
@ -86,7 +86,7 @@ func checkGoFile(pass *analysis.Pass, f *ast.File) {
func checkOtherFile(pass *analysis.Pass, filename string) error { func checkOtherFile(pass *analysis.Pass, filename string) error {
// We cannot use the Go parser, since is not a Go source file. // We cannot use the Go parser, since is not a Go source file.
// Read the raw bytes instead. // Read the raw bytes instead.
content, tf, err := analysisinternal.ReadFile(pass, filename) content, tf, err := analyzerutil.ReadFile(pass, filename)
if err != nil { if err != nil {
return err return err
} }

View file

@ -12,7 +12,7 @@ import (
"go/types" "go/types"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
) )

View file

@ -13,7 +13,7 @@ import (
"unicode" "unicode"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
) )
const Doc = "report assembly that clobbers the frame pointer before saving it" const Doc = "report assembly that clobbers the frame pointer before saving it"
@ -98,7 +98,7 @@ func run(pass *analysis.Pass) (any, error) {
} }
for _, fname := range sfiles { for _, fname := range sfiles {
content, tf, err := analysisinternal.ReadFile(pass, fname) content, tf, err := analyzerutil.ReadFile(pass, fname)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -17,7 +17,7 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
) )

View file

@ -12,7 +12,7 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typeparams"
) )
@ -21,7 +21,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "ifaceassert", Name: "ifaceassert",
Doc: analysisinternal.MustExtractDoc(doc, "ifaceassert"), Doc: analyzerutil.MustExtractDoc(doc, "ifaceassert"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/ifaceassert", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/ifaceassert",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,

View file

@ -20,9 +20,10 @@ import (
"golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/diff"
"golang.org/x/tools/internal/moreiters"
"golang.org/x/tools/internal/packagepath" "golang.org/x/tools/internal/packagepath"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/refactor/inline" "golang.org/x/tools/internal/refactor/inline"
@ -34,7 +35,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "inline", Name: "inline",
Doc: analysisinternal.MustExtractDoc(doc, "inline"), Doc: analyzerutil.MustExtractDoc(doc, "inline"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/inline", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/inline",
Run: run, Run: run,
FactTypes: []analysis.Fact{ FactTypes: []analysis.Fact{
@ -132,8 +133,6 @@ func (a *analyzer) HandleConst(nameIdent, rhsIdent *ast.Ident) {
// inline inlines each static call to an inlinable function // inline inlines each static call to an inlinable function
// and each reference to an inlinable constant or type alias. // and each reference to an inlinable constant or type alias.
//
// TODO(adonovan): handle multiple diffs that each add the same import.
func (a *analyzer) inline() { func (a *analyzer) inline() {
for cur := range a.root.Preorder((*ast.CallExpr)(nil), (*ast.Ident)(nil)) { for cur := range a.root.Preorder((*ast.CallExpr)(nil), (*ast.Ident)(nil)) {
switch n := cur.Node().(type) { switch n := cur.Node().(type) {
@ -167,6 +166,10 @@ func (a *analyzer) inlineCall(call *ast.CallExpr, cur inspector.Cursor) {
return // nope return // nope
} }
if a.withinTestOf(cur, fn) {
return // don't inline a function from within its own test
}
// Inline the call. // Inline the call.
content, err := a.readFile(call) content, err := a.readFile(call)
if err != nil { if err != nil {
@ -229,6 +232,44 @@ func (a *analyzer) inlineCall(call *ast.CallExpr, cur inspector.Cursor) {
} }
} }
// withinTestOf reports whether cur is within a dedicated test
// function for the inlinable target function.
// A call within its dedicated test should not be inlined.
func (a *analyzer) withinTestOf(cur inspector.Cursor, target *types.Func) bool {
curFuncDecl, ok := moreiters.First(cur.Enclosing((*ast.FuncDecl)(nil)))
if !ok {
return false // not in a function
}
funcDecl := curFuncDecl.Node().(*ast.FuncDecl)
if funcDecl.Recv != nil {
return false // not a test func
}
if strings.TrimSuffix(a.pass.Pkg.Path(), "_test") != target.Pkg().Path() {
return false // different package
}
if !strings.HasSuffix(a.pass.Fset.File(funcDecl.Pos()).Name(), "_test.go") {
return false // not a test file
}
// Computed expected SYMBOL portion of "TestSYMBOL_comment"
// for the target symbol.
symbol := target.Name()
if recv := target.Signature().Recv(); recv != nil {
_, named := typesinternal.ReceiverNamed(recv)
symbol = named.Obj().Name() + "_" + symbol
}
// TODO(adonovan): use a proper Test function parser.
fname := funcDecl.Name.Name
for _, pre := range []string{"Test", "Example", "Bench"} {
if fname == pre+symbol || strings.HasPrefix(fname, pre+symbol+"_") {
return true
}
}
return false
}
// If tn is the TypeName of an inlinable alias, suggest inlining its use at cur. // If tn is the TypeName of an inlinable alias, suggest inlining its use at cur.
func (a *analyzer) inlineAlias(tn *types.TypeName, curId inspector.Cursor) { func (a *analyzer) inlineAlias(tn *types.TypeName, curId inspector.Cursor) {
inalias, ok := a.inlinableAliases[tn] inalias, ok := a.inlinableAliases[tn]
@ -375,8 +416,8 @@ func typenames(t types.Type) []*types.TypeName {
visit(t.Key()) visit(t.Key())
visit(t.Elem()) visit(t.Elem())
case *types.Struct: case *types.Struct:
for i := range t.NumFields() { for field := range t.Fields() {
visit(t.Field(i).Type()) visit(field.Type())
} }
case *types.Signature: case *types.Signature:
// Ignore the receiver: although it may be present, it has no meaning // Ignore the receiver: although it may be present, it has no meaning
@ -389,11 +430,11 @@ func typenames(t types.Type) []*types.TypeName {
visit(t.Params()) visit(t.Params())
visit(t.Results()) visit(t.Results())
case *types.Interface: case *types.Interface:
for i := range t.NumEmbeddeds() { for etyp := range t.EmbeddedTypes() {
visit(t.EmbeddedType(i)) visit(etyp)
} }
for i := range t.NumExplicitMethods() { for method := range t.ExplicitMethods() {
visit(t.ExplicitMethod(i).Type()) visit(method.Type())
} }
case *types.Tuple: case *types.Tuple:
for v := range t.Variables() { for v := range t.Variables() {

View file

@ -41,7 +41,7 @@ var Analyzer = &analysis.Analyzer{
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/inspect", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/inspect",
Run: run, Run: run,
RunDespiteErrors: true, RunDespiteErrors: true,
ResultType: reflect.TypeOf(new(inspector.Inspector)), ResultType: reflect.TypeFor[*inspector.Inspector](),
} }
func run(pass *analysis.Pass) (any, error) { func run(pass *analysis.Pass) (any, error) {

View file

@ -0,0 +1,17 @@
// 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 ctrlflowinternal exposes internals of ctrlflow.
// It cannot actually depend on symbols from ctrlflow.
package ctrlflowinternal
import "go/types"
// NoReturn exposes the (*ctrlflow.CFGs).NoReturn method to the buildssa analyzer.
//
// You must link [golang.org/x/tools/go/analysis/passes/ctrlflow] into your
// application for it to be non-nil.
var NoReturn = func(cfgs any, fn *types.Func) bool {
panic("x/tools/go/analysis/passes/ctrlflow is not linked into this application")
}

View file

@ -13,7 +13,7 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/versions" "golang.org/x/tools/internal/versions"
) )
@ -23,7 +23,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "loopclosure", Name: "loopclosure",
Doc: analysisinternal.MustExtractDoc(doc, "loopclosure"), Doc: analyzerutil.MustExtractDoc(doc, "loopclosure"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/loopclosure", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/loopclosure",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,
@ -55,8 +55,8 @@ func run(pass *analysis.Pass) (any, error) {
switch n := n.(type) { switch n := n.(type) {
case *ast.File: case *ast.File:
// Only traverse the file if its goversion is strictly before go1.22. // Only traverse the file if its goversion is strictly before go1.22.
goversion := versions.FileVersion(pass.TypesInfo, n) return !analyzerutil.FileUsesGoVersion(pass, n, versions.Go1_22)
return versions.Before(goversion, versions.Go1_22)
case *ast.RangeStmt: case *ast.RangeStmt:
body = n.Body body = n.Body
addVar(n.Key) addVar(n.Key)
@ -308,12 +308,11 @@ func parallelSubtest(info *types.Info, call *ast.CallExpr) []ast.Stmt {
if !ok { if !ok {
continue continue
} }
expr := exprStmt.X call, ok := exprStmt.X.(*ast.CallExpr)
if isMethodCall(info, expr, "testing", "T", "Parallel") { if !ok {
call, _ := expr.(*ast.CallExpr)
if call == nil {
continue continue
} }
if isMethodCall(info, call, "testing", "T", "Parallel") {
x, _ := call.Fun.(*ast.SelectorExpr) x, _ := call.Fun.(*ast.SelectorExpr)
if x == nil { if x == nil {
continue continue
@ -347,26 +346,6 @@ func unlabel(stmt ast.Stmt) (ast.Stmt, bool) {
} }
} }
// isMethodCall reports whether expr is a method call of func isMethodCall(info *types.Info, call *ast.CallExpr, pkgPath, typeName, method string) bool {
// <pkgPath>.<typeName>.<method>. return typesinternal.IsMethodNamed(typeutil.Callee(info, call), pkgPath, typeName, method)
func isMethodCall(info *types.Info, expr ast.Expr, pkgPath, typeName, method string) bool {
call, ok := expr.(*ast.CallExpr)
if !ok {
return false
}
// Check that we are calling a method <method>
f := typeutil.StaticCallee(info, call)
if f == nil || f.Name() != method {
return false
}
recv := f.Type().(*types.Signature).Recv()
if recv == nil {
return false
}
// Check that the receiver is a <pkgPath>.<typeName> or
// *<pkgPath>.<typeName>.
_, named := typesinternal.ReceiverNamed(recv)
return typesinternal.IsTypeNamed(named, pkgPath, typeName)
} }

View file

@ -15,7 +15,7 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/cfg" "golang.org/x/tools/go/cfg"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -25,7 +25,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "lostcancel", Name: "lostcancel",
Doc: analysisinternal.MustExtractDoc(doc, "lostcancel"), Doc: analyzerutil.MustExtractDoc(doc, "lostcancel"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/lostcancel", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/lostcancel",
Run: run, Run: run,
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
@ -316,8 +316,8 @@ outer:
} }
func tupleContains(tuple *types.Tuple, v *types.Var) bool { func tupleContains(tuple *types.Tuple, v *types.Var) bool {
for i := 0; i < tuple.Len(); i++ { for v0 := range tuple.Variables() {
if tuple.At(i) == v { if v0 == v {
return true return true
} }
} }

View file

@ -9,29 +9,21 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/versions"
"golang.org/x/tools/internal/analysisinternal/generated"
) )
var AnyAnalyzer = &analysis.Analyzer{ var AnyAnalyzer = &analysis.Analyzer{
Name: "any", Name: "any",
Doc: analysisinternal.MustExtractDoc(doc, "any"), Doc: analyzerutil.MustExtractDoc(doc, "any"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{inspect.Analyzer},
generated.Analyzer,
inspect.Analyzer,
},
Run: runAny, Run: runAny,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any",
} }
// The any pass replaces interface{} with go1.18's 'any'. // The any pass replaces interface{} with go1.18's 'any'.
func runAny(pass *analysis.Pass) (any, error) { func runAny(pass *analysis.Pass) (any, error) {
skipGenerated(pass) for curFile := range filesUsingGoVersion(pass, versions.Go1_18) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.18") {
for curIface := range curFile.Preorder((*ast.InterfaceType)(nil)) { for curIface := range curFile.Preorder((*ast.InterfaceType)(nil)) {
iface := curIface.Node().(*ast.InterfaceType) iface := curIface.Node().(*ast.InterfaceType)

View file

@ -15,20 +15,19 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/moreiters" "golang.org/x/tools/internal/moreiters"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
) )
var BLoopAnalyzer = &analysis.Analyzer{ var BLoopAnalyzer = &analysis.Analyzer{
Name: "bloop", Name: "bloop",
Doc: analysisinternal.MustExtractDoc(doc, "bloop"), Doc: analyzerutil.MustExtractDoc(doc, "bloop"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -45,14 +44,11 @@ var BLoopAnalyzer = &analysis.Analyzer{
// for i := 0; i < b.N; i++ {} => for b.Loop() {} // for i := 0; i < b.N; i++ {} => for b.Loop() {}
// for range b.N {} // for range b.N {}
func bloop(pass *analysis.Pass) (any, error) { func bloop(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
if !typesinternal.Imports(pass.Pkg, "testing") { if !typesinternal.Imports(pass.Pkg, "testing") {
return nil, nil return nil, nil
} }
var ( var (
inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo info = pass.TypesInfo
) )
@ -102,7 +98,7 @@ func bloop(pass *analysis.Pass) (any, error) {
(*ast.ForStmt)(nil), (*ast.ForStmt)(nil),
(*ast.RangeStmt)(nil), (*ast.RangeStmt)(nil),
} }
for curFile := range filesUsing(inspect, info, "go1.24") { for curFile := range filesUsingGoVersion(pass, versions.Go1_24) {
for curLoop := range curFile.Preorder(loops...) { for curLoop := range curFile.Preorder(loops...) {
switch n := curLoop.Node().(type) { switch n := curLoop.Node().(type) {
case *ast.ForStmt: case *ast.ForStmt:
@ -189,6 +185,7 @@ func enclosingFunc(c inspector.Cursor) (inspector.Cursor, bool) {
// 2. The only b.N loop in that benchmark function // 2. The only b.N loop in that benchmark function
// - b.Loop() can only be called once per benchmark execution // - b.Loop() can only be called once per benchmark execution
// - Multiple calls result in "B.Loop called with timer stopped" error // - Multiple calls result in "B.Loop called with timer stopped" error
// - Multiple loops may have complex interdependencies that are hard to analyze
func usesBenchmarkNOnce(c inspector.Cursor, info *types.Info) bool { func usesBenchmarkNOnce(c inspector.Cursor, info *types.Info) bool {
// Find the enclosing benchmark function // Find the enclosing benchmark function
curFunc, ok := enclosingFunc(c) curFunc, ok := enclosingFunc(c)
@ -205,18 +202,15 @@ func usesBenchmarkNOnce(c inspector.Cursor, info *types.Info) bool {
return false return false
} }
// Count b.N references in this benchmark function // Count all b.N references in this benchmark function (including nested functions)
bnRefCount := 0 bnRefCount := 0
filter := []ast.Node{(*ast.SelectorExpr)(nil), (*ast.FuncLit)(nil)} filter := []ast.Node{(*ast.SelectorExpr)(nil)}
curFunc.Inspect(filter, func(cur inspector.Cursor) bool { curFunc.Inspect(filter, func(cur inspector.Cursor) bool {
switch n := cur.Node().(type) { sel := cur.Node().(*ast.SelectorExpr)
case *ast.FuncLit: if sel.Sel.Name == "N" &&
return false // don't descend into nested function literals typesinternal.IsPointerToNamed(info.TypeOf(sel.X), "testing", "B") {
case *ast.SelectorExpr:
if n.Sel.Name == "N" && typesinternal.IsPointerToNamed(info.TypeOf(n.X), "testing", "B") {
bnRefCount++ bnRefCount++
} }
}
return true return true
}) })
@ -240,7 +234,7 @@ func isIncrementLoop(info *types.Info, loop *ast.ForStmt) *types.Var {
if assign, ok := loop.Init.(*ast.AssignStmt); ok && if assign, ok := loop.Init.(*ast.AssignStmt); ok &&
assign.Tok == token.DEFINE && assign.Tok == token.DEFINE &&
len(assign.Rhs) == 1 && len(assign.Rhs) == 1 &&
isZeroIntLiteral(info, assign.Rhs[0]) && isZeroIntConst(info, assign.Rhs[0]) &&
is[*ast.IncDecStmt](loop.Post) && is[*ast.IncDecStmt](loop.Post) &&
loop.Post.(*ast.IncDecStmt).Tok == token.INC && loop.Post.(*ast.IncDecStmt).Tok == token.INC &&
astutil.EqualSyntax(loop.Post.(*ast.IncDecStmt).X, assign.Lhs[0]) { astutil.EqualSyntax(loop.Post.(*ast.IncDecStmt).X, assign.Lhs[0]) {

View file

@ -176,8 +176,12 @@ This analyzer finds declarations of functions of this form:
and suggests a fix to turn them into inlinable wrappers around and suggests a fix to turn them into inlinable wrappers around
go1.26's built-in new(expr) function: go1.26's built-in new(expr) function:
//go:fix inline
func varOf(x int) *int { return new(x) } func varOf(x int) *int { return new(x) }
(The directive comment causes the 'inline' analyzer to suggest
that calls to such functions are inlined.)
In addition, this analyzer suggests a fix for each call In addition, this analyzer suggests a fix for each call
to one of the functions before it is transformed, so that to one of the functions before it is transformed, so that
@ -187,9 +191,9 @@ is replaced by:
use(new(123)) use(new(123))
(Wrapper functions such as varOf are common when working with Go Wrapper functions such as varOf are common when working with Go
serialization packages such as for JSON or protobuf, where pointers serialization packages such as for JSON or protobuf, where pointers
are often used to express optionality.) are often used to express optionality.
# Analyzer omitzero # Analyzer omitzero
@ -327,6 +331,44 @@ iterator offered by the same data type:
where x is one of various well-known types in the standard library. where x is one of various well-known types in the standard library.
# Analyzer stringscut
stringscut: replace strings.Index etc. with strings.Cut
This analyzer replaces certain patterns of use of [strings.Index] and string slicing by [strings.Cut], added in go1.18.
For example:
idx := strings.Index(s, substr)
if idx >= 0 {
return s[:idx]
}
is replaced by:
before, _, ok := strings.Cut(s, substr)
if ok {
return before
}
And:
idx := strings.Index(s, substr)
if idx >= 0 {
return
}
is replaced by:
found := strings.Contains(s, substr)
if found {
return
}
It also handles variants using [strings.IndexByte] instead of Index, or the bytes package instead of strings.
Fixes are offered only in cases in which there are no potential modifications of the idx, s, or substr expressions between their definition and use.
# Analyzer stringscutprefix # Analyzer stringscutprefix
stringscutprefix: replace HasPrefix/TrimPrefix with CutPrefix stringscutprefix: replace HasPrefix/TrimPrefix with CutPrefix

View file

@ -14,21 +14,21 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/goplsexport" "golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
) )
var errorsastypeAnalyzer = &analysis.Analyzer{ var errorsastypeAnalyzer = &analysis.Analyzer{
Name: "errorsastype", Name: "errorsastype",
Doc: analysisinternal.MustExtractDoc(doc, "errorsastype"), Doc: analyzerutil.MustExtractDoc(doc, "errorsastype"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#errorsastype", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#errorsastype",
Requires: []*analysis.Analyzer{generated.Analyzer, typeindexanalyzer.Analyzer}, Requires: []*analysis.Analyzer{typeindexanalyzer.Analyzer},
Run: errorsastype, Run: errorsastype,
} }
@ -78,8 +78,6 @@ func init() {
// //
// - if errors.As(err, myerr) && othercond { ... } // - if errors.As(err, myerr) && othercond { ... }
func errorsastype(pass *analysis.Pass) (any, error) { func errorsastype(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
var ( var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo info = pass.TypesInfo
@ -97,7 +95,7 @@ func errorsastype(pass *analysis.Pass) (any, error) {
} }
file := astutil.EnclosingFile(curDeclStmt) file := astutil.EnclosingFile(curDeclStmt)
if !fileUses(info, file, "go1.26") { if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_26) {
continue // errors.AsType is too new continue // errors.AsType is too new
} }
@ -127,6 +125,8 @@ func errorsastype(pass *analysis.Pass) (any, error) {
errtype := types.TypeString(v.Type(), qual) errtype := types.TypeString(v.Type(), qual)
// Choose a name for the "ok" variable. // Choose a name for the "ok" variable.
// TODO(adonovan): this pattern also appears in stditerators,
// and is wanted elsewhere; factor.
okName := "ok" okName := "ok"
if okVar := lookup(info, curCall, "ok"); okVar != nil { if okVar := lookup(info, curCall, "ok"); okVar != nil {
// The name 'ok' is already declared, but // The name 'ok' is already declared, but

View file

@ -13,18 +13,17 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
) )
var FmtAppendfAnalyzer = &analysis.Analyzer{ var FmtAppendfAnalyzer = &analysis.Analyzer{
Name: "fmtappendf", Name: "fmtappendf",
Doc: analysisinternal.MustExtractDoc(doc, "fmtappendf"), Doc: analyzerutil.MustExtractDoc(doc, "fmtappendf"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -35,8 +34,6 @@ var FmtAppendfAnalyzer = &analysis.Analyzer{
// The fmtappend function replaces []byte(fmt.Sprintf(...)) by // The fmtappend function replaces []byte(fmt.Sprintf(...)) by
// fmt.Appendf(nil, ...), and similarly for Sprint, Sprintln. // fmt.Appendf(nil, ...), and similarly for Sprint, Sprintln.
func fmtappendf(pass *analysis.Pass) (any, error) { func fmtappendf(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
for _, fn := range []types.Object{ for _, fn := range []types.Object{
index.Object("fmt", "Sprintf"), index.Object("fmt", "Sprintf"),
@ -50,7 +47,7 @@ func fmtappendf(pass *analysis.Pass) (any, error) {
conv := curCall.Parent().Node().(*ast.CallExpr) conv := curCall.Parent().Node().(*ast.CallExpr)
tv := pass.TypesInfo.Types[conv.Fun] tv := pass.TypesInfo.Types[conv.Fun]
if tv.IsType() && types.Identical(tv.Type, byteSliceType) && if tv.IsType() && types.Identical(tv.Type, byteSliceType) &&
fileUses(pass.TypesInfo, astutil.EnclosingFile(curCall), "go1.19") { analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_19) {
// Have: []byte(fmt.SprintX(...)) // Have: []byte(fmt.SprintX(...))
// Find "Sprint" identifier. // Find "Sprint" identifier.

View file

@ -10,20 +10,16 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal"
"golang.org/x/tools/internal/analysisinternal/generated"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/versions"
) )
var ForVarAnalyzer = &analysis.Analyzer{ var ForVarAnalyzer = &analysis.Analyzer{
Name: "forvar", Name: "forvar",
Doc: analysisinternal.MustExtractDoc(doc, "forvar"), Doc: analyzerutil.MustExtractDoc(doc, "forvar"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{inspect.Analyzer},
generated.Analyzer,
inspect.Analyzer,
},
Run: forvar, Run: forvar,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#forvar", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#forvar",
} }
@ -39,37 +35,63 @@ var ForVarAnalyzer = &analysis.Analyzer{
// where the two idents are the same, // where the two idents are the same,
// and the ident is defined (:=) as a variable in the for statement. // and the ident is defined (:=) as a variable in the for statement.
// (Note that this 'fix' does not work for three clause loops // (Note that this 'fix' does not work for three clause loops
// because the Go specification says "The variable used by each subsequent iteration // because the Go specfilesUsingGoVersionsays "The variable used by each subsequent iteration
// is declared implicitly before executing the post statement and initialized to the // is declared implicitly before executing the post statement and initialized to the
// value of the previous iteration's variable at that moment.") // value of the previous iteration's variable at that moment.")
//
// Variant: same thing in an IfStmt.Init, when the IfStmt is the sole
// loop body statement:
//
// for _, x := range foo {
// if x := x; cond { ... }
// }
//
// (The restriction is necessary to avoid potential problems arising
// from merging two distinct variables.)
//
// This analyzer is synergistic with stditerators,
// which may create redundant "x := x" statements.
func forvar(pass *analysis.Pass) (any, error) { func forvar(pass *analysis.Pass) (any, error) {
skipGenerated(pass) for curFile := range filesUsingGoVersion(pass, versions.Go1_22) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.22") {
for curLoop := range curFile.Preorder((*ast.RangeStmt)(nil)) { for curLoop := range curFile.Preorder((*ast.RangeStmt)(nil)) {
loop := curLoop.Node().(*ast.RangeStmt) loop := curLoop.Node().(*ast.RangeStmt)
if loop.Tok != token.DEFINE { if loop.Tok != token.DEFINE {
continue continue
} }
isLoopVarRedecl := func(assign *ast.AssignStmt) bool { isLoopVarRedecl := func(stmt ast.Stmt) bool {
if assign, ok := stmt.(*ast.AssignStmt); ok &&
assign.Tok == token.DEFINE &&
len(assign.Lhs) == len(assign.Rhs) {
for i, lhs := range assign.Lhs { for i, lhs := range assign.Lhs {
if !(astutil.EqualSyntax(lhs, assign.Rhs[i]) && if !(astutil.EqualSyntax(lhs, assign.Rhs[i]) &&
(astutil.EqualSyntax(lhs, loop.Key) || astutil.EqualSyntax(lhs, loop.Value))) { (astutil.EqualSyntax(lhs, loop.Key) ||
astutil.EqualSyntax(lhs, loop.Value))) {
return false return false
} }
} }
return true return true
} }
return false
}
// Have: for k, v := range x { stmts } // Have: for k, v := range x { stmts }
// //
// Delete the prefix of stmts that are // Delete the prefix of stmts that are
// of the form k := k; v := v; k, v := k, v; v, k := v, k. // of the form k := k; v := v; k, v := k, v; v, k := v, k.
for _, stmt := range loop.Body.List { for _, stmt := range loop.Body.List {
if assign, ok := stmt.(*ast.AssignStmt); ok && if isLoopVarRedecl(stmt) {
assign.Tok == token.DEFINE && // { x := x; ... }
len(assign.Lhs) == len(assign.Rhs) && // ------
isLoopVarRedecl(assign) { } else if ifstmt, ok := stmt.(*ast.IfStmt); ok &&
ifstmt.Init != nil &&
len(loop.Body.List) == 1 && // must be sole statement in loop body
isLoopVarRedecl(ifstmt.Init) {
// if x := x; cond {
// ------
stmt = ifstmt.Init
} else {
break // stop at first other statement
}
curStmt, _ := curLoop.FindNode(stmt) curStmt, _ := curLoop.FindNode(stmt)
edits := refactor.DeleteStmt(pass.Fset.File(stmt.Pos()), curStmt) edits := refactor.DeleteStmt(pass.Fset.File(stmt.Pos()), curStmt)
@ -84,9 +106,6 @@ func forvar(pass *analysis.Pass) (any, error) {
}}, }},
}) })
} }
} else {
break // stop at first other statement
}
} }
} }
} }

View file

@ -15,21 +15,18 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/versions"
) )
var MapsLoopAnalyzer = &analysis.Analyzer{ var MapsLoopAnalyzer = &analysis.Analyzer{
Name: "mapsloop", Name: "mapsloop",
Doc: analysisinternal.MustExtractDoc(doc, "mapsloop"), Doc: analyzerutil.MustExtractDoc(doc, "mapsloop"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{inspect.Analyzer},
generated.Analyzer,
inspect.Analyzer,
},
Run: mapsloop, Run: mapsloop,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop",
} }
@ -55,8 +52,6 @@ var MapsLoopAnalyzer = &analysis.Analyzer{
// m = make(M) // m = make(M)
// m = M{} // m = M{}
func mapsloop(pass *analysis.Pass) (any, error) { func mapsloop(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
// Skip the analyzer in packages where its // Skip the analyzer in packages where its
// fixes would create an import cycle. // fixes would create an import cycle.
if within(pass, "maps", "bytes", "runtime") { if within(pass, "maps", "bytes", "runtime") {
@ -223,8 +218,7 @@ func mapsloop(pass *analysis.Pass) (any, error) {
} }
// Find all range loops around m[k] = v. // Find all range loops around m[k] = v.
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) for curFile := range filesUsingGoVersion(pass, versions.Go1_23) {
for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.23") {
file := curFile.Node().(*ast.File) file := curFile.Node().(*ast.File)
for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) { for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {

View file

@ -15,19 +15,18 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
) )
var MinMaxAnalyzer = &analysis.Analyzer{ var MinMaxAnalyzer = &analysis.Analyzer{
Name: "minmax", Name: "minmax",
Doc: analysisinternal.MustExtractDoc(doc, "minmax"), Doc: analyzerutil.MustExtractDoc(doc, "minmax"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -56,8 +55,6 @@ var MinMaxAnalyzer = &analysis.Analyzer{
// - "x := a" or "x = a" or "var x = a" in pattern 2 // - "x := a" or "x = a" or "var x = a" in pattern 2
// - "x < b" or "a < b" in pattern 2 // - "x < b" or "a < b" in pattern 2
func minmax(pass *analysis.Pass) (any, error) { func minmax(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
// Check for user-defined min/max functions that can be removed // Check for user-defined min/max functions that can be removed
checkUserDefinedMinMax(pass) checkUserDefinedMinMax(pass)
@ -201,8 +198,7 @@ func minmax(pass *analysis.Pass) (any, error) {
// Find all "if a < b { lhs = rhs }" statements. // Find all "if a < b { lhs = rhs }" statements.
info := pass.TypesInfo info := pass.TypesInfo
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
for curFile := range filesUsing(inspect, info, "go1.21") {
astFile := curFile.Node().(*ast.File) astFile := curFile.Node().(*ast.File)
for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) { for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) {
ifStmt := curIfStmt.Node().(*ast.IfStmt) ifStmt := curIfStmt.Node().(*ast.IfStmt)

View file

@ -16,15 +16,15 @@ import (
"strings" "strings"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal/generated" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/moreiters" "golang.org/x/tools/internal/moreiters"
"golang.org/x/tools/internal/packagepath" "golang.org/x/tools/internal/packagepath"
"golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/stdlib"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/versions"
) )
//go:embed doc.go //go:embed doc.go
@ -48,6 +48,7 @@ var Suite = []*analysis.Analyzer{
// SlicesDeleteAnalyzer, // not nil-preserving! // SlicesDeleteAnalyzer, // not nil-preserving!
SlicesSortAnalyzer, SlicesSortAnalyzer,
stditeratorsAnalyzer, stditeratorsAnalyzer,
stringscutAnalyzer,
StringsCutPrefixAnalyzer, StringsCutPrefixAnalyzer,
StringsSeqAnalyzer, StringsSeqAnalyzer,
StringsBuilderAnalyzer, StringsBuilderAnalyzer,
@ -57,18 +58,6 @@ var Suite = []*analysis.Analyzer{
// -- helpers -- // -- helpers --
// skipGenerated decorates pass.Report to suppress diagnostics in generated files.
func skipGenerated(pass *analysis.Pass) {
report := pass.Report
pass.Report = func(diag analysis.Diagnostic) {
generated := pass.ResultOf[generated.Analyzer].(*generated.Result)
if generated.IsGenerated(diag.Pos) {
return // skip
}
report(diag)
}
}
// formatExprs formats a comma-separated list of expressions. // formatExprs formats a comma-separated list of expressions.
func formatExprs(fset *token.FileSet, exprs []ast.Expr) string { func formatExprs(fset *token.FileSet, exprs []ast.Expr) string {
var buf strings.Builder var buf strings.Builder
@ -81,8 +70,8 @@ func formatExprs(fset *token.FileSet, exprs []ast.Expr) string {
return buf.String() return buf.String()
} }
// isZeroIntLiteral reports whether e is an integer whose value is 0. // isZeroIntConst reports whether e is an integer whose value is 0.
func isZeroIntLiteral(info *types.Info, e ast.Expr) bool { func isZeroIntConst(info *types.Info, e ast.Expr) bool {
return isIntLiteral(info, e, 0) return isIntLiteral(info, e, 0)
} }
@ -91,36 +80,28 @@ func isIntLiteral(info *types.Info, e ast.Expr, n int64) bool {
return info.Types[e].Value == constant.MakeInt64(n) return info.Types[e].Value == constant.MakeInt64(n)
} }
// filesUsing returns a cursor for each *ast.File in the inspector // filesUsingGoVersion returns a cursor for each *ast.File in the inspector
// that uses at least the specified version of Go (e.g. "go1.24"). // that uses at least the specified version of Go (e.g. "go1.24").
// //
// The pass's analyzer must require [inspect.Analyzer].
//
// TODO(adonovan): opt: eliminate this function, instead following the // TODO(adonovan): opt: eliminate this function, instead following the
// approach of [fmtappendf], which uses typeindex and [fileUses]. // approach of [fmtappendf], which uses typeindex and
// See "Tip" at [fileUses] for motivation. // [analyzerutil.FileUsesGoVersion]; see "Tip" documented at the
func filesUsing(inspect *inspector.Inspector, info *types.Info, version string) iter.Seq[inspector.Cursor] { // latter function for motivation.
func filesUsingGoVersion(pass *analysis.Pass, version string) iter.Seq[inspector.Cursor] {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
return func(yield func(inspector.Cursor) bool) { return func(yield func(inspector.Cursor) bool) {
for curFile := range inspect.Root().Children() { for curFile := range inspect.Root().Children() {
file := curFile.Node().(*ast.File) file := curFile.Node().(*ast.File)
if !versions.Before(info.FileVersions[file], version) && !yield(curFile) { if analyzerutil.FileUsesGoVersion(pass, file, version) && !yield(curFile) {
break break
} }
} }
} }
} }
// fileUses reports whether the specified file uses at least the
// specified version of Go (e.g. "go1.24").
//
// Tip: we recommend using this check "late", just before calling
// pass.Report, rather than "early" (when entering each ast.File, or
// each candidate node of interest, during the traversal), because the
// operation is not free, yet is not a highly selective filter: the
// fraction of files that pass most version checks is high and
// increases over time.
func fileUses(info *types.Info, file *ast.File, version string) bool {
return !versions.Before(info.FileVersions[file], version)
}
// within reports whether the current pass is analyzing one of the // within reports whether the current pass is analyzing one of the
// specified standard packages or their dependencies. // specified standard packages or their dependencies.
func within(pass *analysis.Pass, pkgs ...string) bool { func within(pass *analysis.Pass, pkgs ...string) bool {

View file

@ -17,13 +17,14 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/versions"
) )
var NewExprAnalyzer = &analysis.Analyzer{ var NewExprAnalyzer = &analysis.Analyzer{
Name: "newexpr", Name: "newexpr",
Doc: analysisinternal.MustExtractDoc(doc, "newexpr"), Doc: analyzerutil.MustExtractDoc(doc, "newexpr"),
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#newexpr", URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#newexpr",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,
@ -60,7 +61,7 @@ func run(pass *analysis.Pass) (any, error) {
// Check file version. // Check file version.
file := astutil.EnclosingFile(curFuncDecl) file := astutil.EnclosingFile(curFuncDecl)
if !fileUses(info, file, "go1.26") { if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_26) {
continue // new(expr) not available in this file continue // new(expr) not available in this file
} }
@ -87,17 +88,11 @@ func run(pass *analysis.Pass) (any, error) {
} }
} }
// Disabled until we resolve https://go.dev/issue/75726
// (Go version skew between caller and callee in inliner.)
// TODO(adonovan): fix and reenable.
//
// Also, restore these lines to our section of doc.go:
// //go:fix inline
// ...
// (The directive comment causes the inline analyzer to suggest
// that calls to such functions are inlined.)
if false {
// Add a //go:fix inline annotation, if not already present. // Add a //go:fix inline annotation, if not already present.
//
// The inliner will not inline a newer callee body into an
// older Go file; see https://go.dev/issue/75726.
//
// TODO(adonovan): use ast.ParseDirective when go1.26 is assured. // TODO(adonovan): use ast.ParseDirective when go1.26 is assured.
if !strings.Contains(decl.Doc.Text(), "go:fix inline") { if !strings.Contains(decl.Doc.Text(), "go:fix inline") {
edits = append(edits, analysis.TextEdit{ edits = append(edits, analysis.TextEdit{
@ -106,7 +101,6 @@ func run(pass *analysis.Pass) (any, error) {
NewText: []byte("//go:fix inline\n"), NewText: []byte("//go:fix inline\n"),
}) })
} }
}
if len(edits) > 0 { if len(edits) > 0 {
pass.Report(analysis.Diagnostic{ pass.Report(analysis.Diagnostic{
@ -140,7 +134,7 @@ func run(pass *analysis.Pass) (any, error) {
// Check file version. // Check file version.
file := astutil.EnclosingFile(curCall) file := astutil.EnclosingFile(curCall)
if !fileUses(info, file, "go1.26") { if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_26) {
continue // new(expr) not available in this file continue // new(expr) not available in this file
} }

View file

@ -12,19 +12,15 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal"
"golang.org/x/tools/internal/analysisinternal/generated"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/versions"
) )
var OmitZeroAnalyzer = &analysis.Analyzer{ var OmitZeroAnalyzer = &analysis.Analyzer{
Name: "omitzero", Name: "omitzero",
Doc: analysisinternal.MustExtractDoc(doc, "omitzero"), Doc: analyzerutil.MustExtractDoc(doc, "omitzero"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{inspect.Analyzer},
generated.Analyzer,
inspect.Analyzer,
},
Run: omitzero, Run: omitzero,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero",
} }
@ -48,25 +44,20 @@ func checkOmitEmptyField(pass *analysis.Pass, info *types.Info, curField *ast.Fi
// No omitempty in json tag // No omitempty in json tag
return return
} }
omitEmptyPos, omitEmptyEnd, err := astutil.RangeInStringLiteral(curField.Tag, match[2], match[3]) omitEmpty, err := astutil.RangeInStringLiteral(curField.Tag, match[2], match[3])
if err != nil { if err != nil {
return return
} }
removePos, removeEnd := omitEmptyPos, omitEmptyEnd var remove analysis.Range = omitEmpty
jsonTag := reflect.StructTag(tagconv).Get("json") jsonTag := reflect.StructTag(tagconv).Get("json")
if jsonTag == ",omitempty" { if jsonTag == ",omitempty" {
// Remove the entire struct tag if json is the only package used // Remove the entire struct tag if json is the only package used
if match[1]-match[0] == len(tagconv) { if match[1]-match[0] == len(tagconv) {
removePos = curField.Tag.Pos() remove = curField.Tag
removeEnd = curField.Tag.End()
} else { } else {
// Remove the json tag if omitempty is the only field // Remove the json tag if omitempty is the only field
removePos, err = astutil.PosInStringLiteral(curField.Tag, match[0]) remove, err = astutil.RangeInStringLiteral(curField.Tag, match[0], match[1])
if err != nil {
return
}
removeEnd, err = astutil.PosInStringLiteral(curField.Tag, match[1])
if err != nil { if err != nil {
return return
} }
@ -81,8 +72,8 @@ func checkOmitEmptyField(pass *analysis.Pass, info *types.Info, curField *ast.Fi
Message: "Remove redundant omitempty tag", Message: "Remove redundant omitempty tag",
TextEdits: []analysis.TextEdit{ TextEdits: []analysis.TextEdit{
{ {
Pos: removePos, Pos: remove.Pos(),
End: removeEnd, End: remove.End(),
}, },
}, },
}, },
@ -90,8 +81,8 @@ func checkOmitEmptyField(pass *analysis.Pass, info *types.Info, curField *ast.Fi
Message: "Replace omitempty with omitzero (behavior change)", Message: "Replace omitempty with omitzero (behavior change)",
TextEdits: []analysis.TextEdit{ TextEdits: []analysis.TextEdit{
{ {
Pos: omitEmptyPos, Pos: omitEmpty.Pos(),
End: omitEmptyEnd, End: omitEmpty.End(),
NewText: []byte(",omitzero"), NewText: []byte(",omitzero"),
}, },
}, },
@ -100,18 +91,14 @@ func checkOmitEmptyField(pass *analysis.Pass, info *types.Info, curField *ast.Fi
} }
// The omitzero pass searches for instances of "omitempty" in a json field tag on a // The omitzero pass searches for instances of "omitempty" in a json field tag on a
// struct. Since "omitempty" does not have any effect when applied to a struct field, // struct. Since "omitfilesUsingGoVersions not have any effect when applied to a struct field,
// it suggests either deleting "omitempty" or replacing it with "omitzero", which // it suggests either deleting "omitempty" or replacing it with "omitzero", which
// correctly excludes structs from a json encoding. // correctly excludes structs from a json encoding.
func omitzero(pass *analysis.Pass) (any, error) { func omitzero(pass *analysis.Pass) (any, error) {
skipGenerated(pass) for curFile := range filesUsingGoVersion(pass, versions.Go1_24) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
info := pass.TypesInfo
for curFile := range filesUsing(inspect, info, "go1.24") {
for curStruct := range curFile.Preorder((*ast.StructType)(nil)) { for curStruct := range curFile.Preorder((*ast.StructType)(nil)) {
for _, curField := range curStruct.Node().(*ast.StructType).Fields.List { for _, curField := range curStruct.Node().(*ast.StructType).Fields.List {
checkOmitEmptyField(pass, info, curField) checkOmitEmptyField(pass, pass.TypesInfo, curField)
} }
} }
} }

View file

@ -10,13 +10,14 @@ import (
"strings" "strings"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/goplsexport" "golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/versions"
) )
var plusBuildAnalyzer = &analysis.Analyzer{ var plusBuildAnalyzer = &analysis.Analyzer{
Name: "plusbuild", Name: "plusbuild",
Doc: analysisinternal.MustExtractDoc(doc, "plusbuild"), Doc: analyzerutil.MustExtractDoc(doc, "plusbuild"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#plusbuild", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#plusbuild",
Run: plusbuild, Run: plusbuild,
} }
@ -28,7 +29,7 @@ func init() {
func plusbuild(pass *analysis.Pass) (any, error) { func plusbuild(pass *analysis.Pass) (any, error) {
check := func(f *ast.File) { check := func(f *ast.File) {
if !fileUses(pass.TypesInfo, f, "go1.18") { if !analyzerutil.FileUsesGoVersion(pass, f, versions.Go1_18) {
return return
} }

View file

@ -15,19 +15,18 @@ import (
"golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
) )
var RangeIntAnalyzer = &analysis.Analyzer{ var RangeIntAnalyzer = &analysis.Analyzer{
Name: "rangeint", Name: "rangeint",
Doc: analysisinternal.MustExtractDoc(doc, "rangeint"), Doc: analyzerutil.MustExtractDoc(doc, "rangeint"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -66,21 +65,19 @@ var RangeIntAnalyzer = &analysis.Analyzer{
// - a constant; or // - a constant; or
// - len(s), where s has the above properties. // - len(s), where s has the above properties.
func rangeint(pass *analysis.Pass) (any, error) { func rangeint(pass *analysis.Pass) (any, error) {
skipGenerated(pass) var (
info = pass.TypesInfo
typeindex = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
)
info := pass.TypesInfo for curFile := range filesUsingGoVersion(pass, versions.Go1_22) {
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
typeindex := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
for curFile := range filesUsing(inspect, info, "go1.22") {
nextLoop: nextLoop:
for curLoop := range curFile.Preorder((*ast.ForStmt)(nil)) { for curLoop := range curFile.Preorder((*ast.ForStmt)(nil)) {
loop := curLoop.Node().(*ast.ForStmt) loop := curLoop.Node().(*ast.ForStmt)
if init, ok := loop.Init.(*ast.AssignStmt); ok && if init, ok := loop.Init.(*ast.AssignStmt); ok &&
isSimpleAssign(init) && isSimpleAssign(init) &&
is[*ast.Ident](init.Lhs[0]) && is[*ast.Ident](init.Lhs[0]) &&
isZeroIntLiteral(info, init.Rhs[0]) { isZeroIntConst(info, init.Rhs[0]) {
// Have: for i = 0; ... (or i := 0) // Have: for i = 0; ... (or i := 0)
index := init.Lhs[0].(*ast.Ident) index := init.Lhs[0].(*ast.Ident)

View file

@ -14,9 +14,8 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
@ -26,9 +25,8 @@ import (
var ReflectTypeForAnalyzer = &analysis.Analyzer{ var ReflectTypeForAnalyzer = &analysis.Analyzer{
Name: "reflecttypefor", Name: "reflecttypefor",
Doc: analysisinternal.MustExtractDoc(doc, "reflecttypefor"), Doc: analyzerutil.MustExtractDoc(doc, "reflecttypefor"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -37,8 +35,6 @@ var ReflectTypeForAnalyzer = &analysis.Analyzer{
} }
func reflecttypefor(pass *analysis.Pass) (any, error) { func reflecttypefor(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
var ( var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo info = pass.TypesInfo
@ -89,7 +85,7 @@ func reflecttypefor(pass *analysis.Pass) (any, error) {
} }
file := astutil.EnclosingFile(curCall) file := astutil.EnclosingFile(curCall)
if versions.Before(info.FileVersions[file], "go1.22") { if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_22) {
continue // TypeFor requires go1.22 continue // TypeFor requires go1.22
} }
tokFile := pass.Fset.File(file.Pos()) tokFile := pass.Fset.File(file.Pos())

View file

@ -13,23 +13,19 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/versions"
) )
// Warning: this analyzer is not safe to enable by default. // Warning: this analyzer is not safe to enable by default.
var AppendClippedAnalyzer = &analysis.Analyzer{ var AppendClippedAnalyzer = &analysis.Analyzer{
Name: "appendclipped", Name: "appendclipped",
Doc: analysisinternal.MustExtractDoc(doc, "appendclipped"), Doc: analyzerutil.MustExtractDoc(doc, "appendclipped"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{inspect.Analyzer},
generated.Analyzer,
inspect.Analyzer,
},
Run: appendclipped, Run: appendclipped,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#appendclipped", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#appendclipped",
} }
@ -59,8 +55,6 @@ var AppendClippedAnalyzer = &analysis.Analyzer{
// The fix does not always preserve nilness the of base slice when the // The fix does not always preserve nilness the of base slice when the
// addends (a, b, c) are all empty (see #73557). // addends (a, b, c) are all empty (see #73557).
func appendclipped(pass *analysis.Pass) (any, error) { func appendclipped(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
// Skip the analyzer in packages where its // Skip the analyzer in packages where its
// fixes would create an import cycle. // fixes would create an import cycle.
if within(pass, "slices", "bytes", "runtime") { if within(pass, "slices", "bytes", "runtime") {
@ -205,8 +199,7 @@ func appendclipped(pass *analysis.Pass) (any, error) {
skip := make(map[*ast.CallExpr]bool) skip := make(map[*ast.CallExpr]bool)
// Visit calls of form append(x, y...). // Visit calls of form append(x, y...).
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
for curFile := range filesUsing(inspect, info, "go1.21") {
file := curFile.Node().(*ast.File) file := curFile.Node().(*ast.File)
for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) { for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) {
@ -266,7 +259,7 @@ func clippedSlice(info *types.Info, e ast.Expr) (res ast.Expr, empty bool) {
// x[:0:0], x[:len(x):len(x)], x[:k:k] // x[:0:0], x[:len(x):len(x)], x[:k:k]
if e.Slice3 && e.High != nil && e.Max != nil && astutil.EqualSyntax(e.High, e.Max) { // x[:k:k] if e.Slice3 && e.High != nil && e.Max != nil && astutil.EqualSyntax(e.High, e.Max) { // x[:k:k]
res = e res = e
empty = isZeroIntLiteral(info, e.High) // x[:0:0] empty = isZeroIntConst(info, e.High) // x[:0:0]
if call, ok := e.High.(*ast.CallExpr); ok && if call, ok := e.High.(*ast.CallExpr); ok &&
typeutil.Callee(info, call) == builtinLen && typeutil.Callee(info, call) == builtinLen &&
astutil.EqualSyntax(call.Args[0], e.X) { astutil.EqualSyntax(call.Args[0], e.X) {

View file

@ -14,20 +14,19 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
) )
var SlicesContainsAnalyzer = &analysis.Analyzer{ var SlicesContainsAnalyzer = &analysis.Analyzer{
Name: "slicescontains", Name: "slicescontains",
Doc: analysisinternal.MustExtractDoc(doc, "slicescontains"), Doc: analyzerutil.MustExtractDoc(doc, "slicescontains"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -66,8 +65,6 @@ var SlicesContainsAnalyzer = &analysis.Analyzer{
// TODO(adonovan): Add a check that needle/predicate expression from // TODO(adonovan): Add a check that needle/predicate expression from
// if-statement has no effects. Now the program behavior may change. // if-statement has no effects. Now the program behavior may change.
func slicescontains(pass *analysis.Pass) (any, error) { func slicescontains(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
// Skip the analyzer in packages where its // Skip the analyzer in packages where its
// fixes would create an import cycle. // fixes would create an import cycle.
if within(pass, "slices", "runtime") { if within(pass, "slices", "runtime") {
@ -75,7 +72,6 @@ func slicescontains(pass *analysis.Pass) (any, error) {
} }
var ( var (
inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo info = pass.TypesInfo
) )
@ -312,7 +308,7 @@ func slicescontains(pass *analysis.Pass) (any, error) {
// Special case: // Special case:
// prev="lhs = false" body={ lhs = true; break } // prev="lhs = false" body={ lhs = true; break }
// => lhs = slices.Contains(...) (or negation) // => lhs = slices.Contains(...) (or its negation)
if assign, ok := body.List[0].(*ast.AssignStmt); ok && if assign, ok := body.List[0].(*ast.AssignStmt); ok &&
len(body.List) == 2 && len(body.List) == 2 &&
assign.Tok == token.ASSIGN && assign.Tok == token.ASSIGN &&
@ -320,13 +316,12 @@ func slicescontains(pass *analysis.Pass) (any, error) {
len(assign.Rhs) == 1 { len(assign.Rhs) == 1 {
// Have: body={ lhs = rhs; break } // Have: body={ lhs = rhs; break }
if prevAssign, ok := prevStmt.(*ast.AssignStmt); ok && if prevAssign, ok := prevStmt.(*ast.AssignStmt); ok &&
len(prevAssign.Lhs) == 1 && len(prevAssign.Lhs) == 1 &&
len(prevAssign.Rhs) == 1 && len(prevAssign.Rhs) == 1 &&
astutil.EqualSyntax(prevAssign.Lhs[0], assign.Lhs[0]) && astutil.EqualSyntax(prevAssign.Lhs[0], assign.Lhs[0]) &&
is[*ast.Ident](assign.Rhs[0]) && isTrueOrFalse(info, assign.Rhs[0]) ==
info.Uses[assign.Rhs[0].(*ast.Ident)] == builtinTrue { -isTrueOrFalse(info, prevAssign.Rhs[0]) {
// Have: // Have:
// lhs = false // lhs = false
@ -336,15 +331,14 @@ func slicescontains(pass *analysis.Pass) (any, error) {
// //
// TODO(adonovan): // TODO(adonovan):
// - support "var lhs bool = false" and variants. // - support "var lhs bool = false" and variants.
// - support negation.
// Both these variants seem quite significant.
// - allow the break to be omitted. // - allow the break to be omitted.
neg := cond(isTrueOrFalse(info, assign.Rhs[0]) < 0, "!", "")
report([]analysis.TextEdit{ report([]analysis.TextEdit{
// Replace "rhs" of previous assignment by slices.Contains(...) // Replace "rhs" of previous assignment by [!]slices.Contains(...)
{ {
Pos: prevAssign.Rhs[0].Pos(), Pos: prevAssign.Rhs[0].Pos(),
End: prevAssign.Rhs[0].End(), End: prevAssign.Rhs[0].End(),
NewText: []byte(contains), NewText: []byte(neg + contains),
}, },
// Delete the loop and preceding space. // Delete the loop and preceding space.
{ {
@ -388,7 +382,7 @@ func slicescontains(pass *analysis.Pass) (any, error) {
} }
} }
for curFile := range filesUsing(inspect, info, "go1.21") { for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
file := curFile.Node().(*ast.File) file := curFile.Node().(*ast.File)
for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) { for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
@ -420,7 +414,14 @@ func slicescontains(pass *analysis.Pass) (any, error) {
// isReturnTrueOrFalse returns nonzero if stmt returns true (+1) or false (-1). // isReturnTrueOrFalse returns nonzero if stmt returns true (+1) or false (-1).
func isReturnTrueOrFalse(info *types.Info, stmt ast.Stmt) int { func isReturnTrueOrFalse(info *types.Info, stmt ast.Stmt) int {
if ret, ok := stmt.(*ast.ReturnStmt); ok && len(ret.Results) == 1 { if ret, ok := stmt.(*ast.ReturnStmt); ok && len(ret.Results) == 1 {
if id, ok := ret.Results[0].(*ast.Ident); ok { return isTrueOrFalse(info, ret.Results[0])
}
return 0
}
// isTrueOrFalse returns nonzero if expr is literally true (+1) or false (-1).
func isTrueOrFalse(info *types.Info, expr ast.Expr) int {
if id, ok := expr.(*ast.Ident); ok {
switch info.Uses[id] { switch info.Uses[id] {
case builtinTrue: case builtinTrue:
return +1 return +1
@ -428,6 +429,5 @@ func isReturnTrueOrFalse(info *types.Info, stmt ast.Stmt) int {
return -1 return -1
} }
} }
}
return 0 return 0
} }

View file

@ -12,22 +12,18 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal"
"golang.org/x/tools/internal/analysisinternal/generated"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/versions"
) )
// Warning: this analyzer is not safe to enable by default (not nil-preserving). // Warning: this analyzer is not safe to enable by default (not nil-preserving).
var SlicesDeleteAnalyzer = &analysis.Analyzer{ var SlicesDeleteAnalyzer = &analysis.Analyzer{
Name: "slicesdelete", Name: "slicesdelete",
Doc: analysisinternal.MustExtractDoc(doc, "slicesdelete"), Doc: analyzerutil.MustExtractDoc(doc, "slicesdelete"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{inspect.Analyzer},
generated.Analyzer,
inspect.Analyzer,
},
Run: slicesdelete, Run: slicesdelete,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesdelete", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesdelete",
} }
@ -37,15 +33,12 @@ var SlicesDeleteAnalyzer = &analysis.Analyzer{
// Other variations that will also have suggested replacements include: // Other variations that will also have suggested replacements include:
// append(s[:i-1], s[i:]...) and append(s[:i+k1], s[i+k2:]) where k2 > k1. // append(s[:i-1], s[i:]...) and append(s[:i+k1], s[i+k2:]) where k2 > k1.
func slicesdelete(pass *analysis.Pass) (any, error) { func slicesdelete(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
// Skip the analyzer in packages where its // Skip the analyzer in packages where its
// fixes would create an import cycle. // fixes would create an import cycle.
if within(pass, "slices", "runtime") { if within(pass, "slices", "runtime") {
return nil, nil return nil, nil
} }
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
info := pass.TypesInfo info := pass.TypesInfo
report := func(file *ast.File, call *ast.CallExpr, slice1, slice2 *ast.SliceExpr) { report := func(file *ast.File, call *ast.CallExpr, slice1, slice2 *ast.SliceExpr) {
insert := func(pos token.Pos, text string) analysis.TextEdit { insert := func(pos token.Pos, text string) analysis.TextEdit {
@ -55,7 +48,7 @@ func slicesdelete(pass *analysis.Pass) (any, error) {
return types.Identical(types.Default(info.TypeOf(e)), builtinInt.Type()) return types.Identical(types.Default(info.TypeOf(e)), builtinInt.Type())
} }
isIntShadowed := func() bool { isIntShadowed := func() bool {
scope := pass.TypesInfo.Scopes[file].Innermost(call.Lparen) scope := info.Scopes[file].Innermost(call.Lparen)
if _, obj := scope.LookupParent("int", call.Lparen); obj != builtinInt { if _, obj := scope.LookupParent("int", call.Lparen); obj != builtinInt {
return true // int type is shadowed return true // int type is shadowed
} }
@ -130,7 +123,7 @@ func slicesdelete(pass *analysis.Pass) (any, error) {
}}, }},
}) })
} }
for curFile := range filesUsing(inspect, info, "go1.21") { for curFile := range filesUsingGoVersion(pass, versions.Go1_21) {
file := curFile.Node().(*ast.File) file := curFile.Node().(*ast.File)
for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) { for curCall := range curFile.Preorder((*ast.CallExpr)(nil)) {
call := curCall.Node().(*ast.CallExpr) call := curCall.Node().(*ast.CallExpr)

View file

@ -11,20 +11,19 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
) )
// (Not to be confused with go/analysis/passes/sortslice.) // (Not to be confused with go/analysis/passes/sortslice.)
var SlicesSortAnalyzer = &analysis.Analyzer{ var SlicesSortAnalyzer = &analysis.Analyzer{
Name: "slicessort", Name: "slicessort",
Doc: analysisinternal.MustExtractDoc(doc, "slicessort"), Doc: analyzerutil.MustExtractDoc(doc, "slicessort"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -52,8 +51,6 @@ var SlicesSortAnalyzer = &analysis.Analyzer{
// - sort.Sort(x) where x has a named slice type whose Less method is the natural order. // - sort.Sort(x) where x has a named slice type whose Less method is the natural order.
// -> sort.Slice(x) // -> sort.Slice(x)
func slicessort(pass *analysis.Pass) (any, error) { func slicessort(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
// Skip the analyzer in packages where its // Skip the analyzer in packages where its
// fixes would create an import cycle. // fixes would create an import cycle.
if within(pass, "slices", "sort", "runtime") { if within(pass, "slices", "sort", "runtime") {
@ -87,7 +84,7 @@ func slicessort(pass *analysis.Pass) (any, error) {
} }
file := astutil.EnclosingFile(curCall) file := astutil.EnclosingFile(curCall)
if isIndex(compare.X, i) && isIndex(compare.Y, j) && if isIndex(compare.X, i) && isIndex(compare.Y, j) &&
fileUses(info, file, "go1.21") { analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_21) {
// Have: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) // Have: sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
prefix, importEdits := refactor.AddImport( prefix, importEdits := refactor.AddImport(

View file

@ -14,9 +14,8 @@ import (
"golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/goplsexport" "golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
@ -26,9 +25,8 @@ import (
var stditeratorsAnalyzer = &analysis.Analyzer{ var stditeratorsAnalyzer = &analysis.Analyzer{
Name: "stditerators", Name: "stditerators",
Doc: analysisinternal.MustExtractDoc(doc, "stditerators"), Doc: analyzerutil.MustExtractDoc(doc, "stditerators"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
Run: stditerators, Run: stditerators,
@ -89,8 +87,6 @@ var stditeratorsTable = [...]struct {
// iterator for that reason? We don't want to go fix to // iterator for that reason? We don't want to go fix to
// undo optimizations. Do we need a suppression mechanism? // undo optimizations. Do we need a suppression mechanism?
func stditerators(pass *analysis.Pass) (any, error) { func stditerators(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
var ( var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo info = pass.TypesInfo
@ -116,6 +112,10 @@ func stditerators(pass *analysis.Pass) (any, error) {
// //
// for ... { e := x.At(i); use(e) } // for ... { e := x.At(i); use(e) }
// //
// or
//
// for ... { if e := x.At(i); cond { use(e) } }
//
// then chooseName prefers the name e and additionally // then chooseName prefers the name e and additionally
// returns the var's symbol. We'll transform this to: // returns the var's symbol. We'll transform this to:
// //
@ -124,10 +124,11 @@ func stditerators(pass *analysis.Pass) (any, error) {
// which leaves a redundant assignment that a // which leaves a redundant assignment that a
// subsequent 'forvar' pass will eliminate. // subsequent 'forvar' pass will eliminate.
chooseName := func(curBody inspector.Cursor, x ast.Expr, i *types.Var) (string, *types.Var) { chooseName := func(curBody inspector.Cursor, x ast.Expr, i *types.Var) (string, *types.Var) {
// Is body { elem := x.At(i); ... } ?
body := curBody.Node().(*ast.BlockStmt) // isVarAssign reports whether stmt has the form v := x.At(i)
if len(body.List) > 0 { // and returns the variable if so.
if assign, ok := body.List[0].(*ast.AssignStmt); ok && isVarAssign := func(stmt ast.Stmt) *types.Var {
if assign, ok := stmt.(*ast.AssignStmt); ok &&
assign.Tok == token.DEFINE && assign.Tok == token.DEFINE &&
len(assign.Lhs) == 1 && len(assign.Lhs) == 1 &&
len(assign.Rhs) == 1 && len(assign.Rhs) == 1 &&
@ -138,15 +139,47 @@ func stditerators(pass *analysis.Pass) (any, error) {
astutil.EqualSyntax(ast.Unparen(call.Fun).(*ast.SelectorExpr).X, x) && astutil.EqualSyntax(ast.Unparen(call.Fun).(*ast.SelectorExpr).X, x) &&
is[*ast.Ident](call.Args[0]) && is[*ast.Ident](call.Args[0]) &&
info.Uses[call.Args[0].(*ast.Ident)] == i { info.Uses[call.Args[0].(*ast.Ident)] == i {
// Have: { elem := x.At(i); ... } // Have: elem := x.At(i)
id := assign.Lhs[0].(*ast.Ident) id := assign.Lhs[0].(*ast.Ident)
return id.Name, info.Defs[id].(*types.Var) return info.Defs[id].(*types.Var)
}
}
return nil
}
body := curBody.Node().(*ast.BlockStmt)
if len(body.List) > 0 {
// Is body { elem := x.At(i); ... } ?
if v := isVarAssign(body.List[0]); v != nil {
return v.Name(), v
}
// Or { if elem := x.At(i); cond { ... } } ?
if ifstmt, ok := body.List[0].(*ast.IfStmt); ok && ifstmt.Init != nil {
if v := isVarAssign(ifstmt.Init); v != nil {
return v.Name(), v
} }
} }
} }
loop := curBody.Parent().Node() loop := curBody.Parent().Node()
return refactor.FreshName(info.Scopes[loop], loop.Pos(), row.elemname), nil
// Choose a fresh name only if
// (a) the preferred name is already declared here, and
// (b) there are references to it from the loop body.
// TODO(adonovan): this pattern also appears in errorsastype,
// and is wanted elsewhere; factor.
name := row.elemname
if v := lookup(info, curBody, name); v != nil {
// is it free in body?
for curUse := range index.Uses(v) {
if curBody.Contains(curUse) {
name = refactor.FreshName(info.Scopes[loop], loop.Pos(), name)
break
}
}
}
return name, nil
} }
// Process each call of x.Len(). // Process each call of x.Len().
@ -191,7 +224,7 @@ func stditerators(pass *analysis.Pass) (any, error) {
} }
// Have: for i := 0; i < x.Len(); i++ { ... }. // Have: for i := 0; i < x.Len(); i++ { ... }.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
rng = analysisinternal.Range(loop.For, loop.Post.End()) rng = astutil.RangeOf(loop.For, loop.Post.End())
indexVar = v indexVar = v
curBody = curFor.ChildAt(edge.ForStmt_Body, -1) curBody = curFor.ChildAt(edge.ForStmt_Body, -1)
elem, elemVar = chooseName(curBody, lenSel.X, indexVar) elem, elemVar = chooseName(curBody, lenSel.X, indexVar)
@ -234,7 +267,7 @@ func stditerators(pass *analysis.Pass) (any, error) {
// Have: for i := range x.Len() { ... } // Have: for i := range x.Len() { ... }
// ~~~~~~~~~~~~~ // ~~~~~~~~~~~~~
rng = analysisinternal.Range(loop.Range, loop.X.End()) rng = astutil.RangeOf(loop.Range, loop.X.End())
indexVar = info.Defs[id].(*types.Var) indexVar = info.Defs[id].(*types.Var)
curBody = curRange.ChildAt(edge.RangeStmt_Body, -1) curBody = curRange.ChildAt(edge.RangeStmt_Body, -1)
elem, elemVar = chooseName(curBody, lenSel.X, indexVar) elem, elemVar = chooseName(curBody, lenSel.X, indexVar)
@ -313,7 +346,7 @@ func stditerators(pass *analysis.Pass) (any, error) {
// may be somewhat expensive.) // may be somewhat expensive.)
if v, ok := methodGoVersion(row.pkgpath, row.typename, row.itermethod); !ok { if v, ok := methodGoVersion(row.pkgpath, row.typename, row.itermethod); !ok {
panic("no version found") panic("no version found")
} else if file := astutil.EnclosingFile(curLenCall); !fileUses(info, file, v.String()) { } else if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curLenCall), v.String()) {
continue nextCall continue nextCall
} }

View file

@ -15,9 +15,8 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
@ -26,9 +25,8 @@ import (
var StringsBuilderAnalyzer = &analysis.Analyzer{ var StringsBuilderAnalyzer = &analysis.Analyzer{
Name: "stringsbuilder", Name: "stringsbuilder",
Doc: analysisinternal.MustExtractDoc(doc, "stringsbuilder"), Doc: analyzerutil.MustExtractDoc(doc, "stringsbuilder"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -38,8 +36,6 @@ var StringsBuilderAnalyzer = &analysis.Analyzer{
// stringsbuilder replaces string += string in a loop by strings.Builder. // stringsbuilder replaces string += string in a loop by strings.Builder.
func stringsbuilder(pass *analysis.Pass) (any, error) { func stringsbuilder(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
// Skip the analyzer in packages where its // Skip the analyzer in packages where its
// fixes would create an import cycle. // fixes would create an import cycle.
if within(pass, "strings", "runtime") { if within(pass, "strings", "runtime") {

View file

@ -0,0 +1,580 @@
// 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 modernize
import (
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
"iter"
"strconv"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysis/analyzerutil"
typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/goplsexport"
"golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
)
var stringscutAnalyzer = &analysis.Analyzer{
Name: "stringscut",
Doc: analyzerutil.MustExtractDoc(doc, "stringscut"),
Requires: []*analysis.Analyzer{
inspect.Analyzer,
typeindexanalyzer.Analyzer,
},
Run: stringscut,
URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/modernize#stringscut",
}
func init() {
// Export to gopls until this is a published modernizer.
goplsexport.StringsCutModernizer = stringscutAnalyzer
}
// stringscut offers a fix to replace an occurrence of strings.Index{,Byte} with
// strings.{Cut,Contains}, and similar fixes for functions in the bytes package.
// Consider some candidate for replacement i := strings.Index(s, substr).
// The following must hold for a replacement to occur:
//
// 1. All instances of i and s must be in one of these forms.
// Binary expressions:
// (a): establishing that i < 0: e.g.: i < 0, 0 > i, i == -1, -1 == i
// (b): establishing that i > -1: e.g.: i >= 0, 0 <= i, i == 0, 0 == i
//
// Slice expressions:
// a: s[:i], s[0:i]
// b: s[i+len(substr):], s[len(substr) + i:], s[i + const], s[k + i] (where k = len(substr))
//
// 2. There can be no uses of s, substr, or i where they are
// potentially modified (i.e. in assignments, or function calls with unknown side
// effects).
//
// Then, the replacement involves the following substitutions:
//
// 1. Replace "i := strings.Index(s, substr)" with "before, after, ok := strings.Cut(s, substr)"
//
// 2. Replace instances of binary expressions (a) with !ok and binary expressions (b) with ok.
//
// 3. Replace slice expressions (a) with "before" and slice expressions (b) with after.
//
// 4. The assignments to before, after, and ok may use the blank identifier "_" if they are unused.
//
// For example:
//
// i := strings.Index(s, substr)
// if i >= 0 {
// use(s[:i], s[i+len(substr):])
// }
//
// Would become:
//
// before, after, ok := strings.Cut(s, substr)
// if ok {
// use(before, after)
// }
//
// If the condition involving `i` establishes that i > -1, then we replace it with
// `if ok“. Variants listed above include i >= 0, i > 0, and i == 0.
// If the condition is negated (e.g. establishes `i < 0`), we use `if !ok` instead.
// If the slices of `s` match `s[:i]` or `s[i+len(substr):]` or their variants listed above,
// then we replace them with before and after.
//
// When the index `i` is used only to check for the presence of the substring or byte slice,
// the suggested fix uses Contains() instead of Cut.
//
// For example:
//
// i := strings.Index(s, substr)
// if i >= 0 {
// return
// }
//
// Would become:
//
// found := strings.Contains(s, substr)
// if found {
// return
// }
func stringscut(pass *analysis.Pass) (any, error) {
var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo
stringsIndex = index.Object("strings", "Index")
stringsIndexByte = index.Object("strings", "IndexByte")
bytesIndex = index.Object("bytes", "Index")
bytesIndexByte = index.Object("bytes", "IndexByte")
)
for _, obj := range []types.Object{
stringsIndex,
stringsIndexByte,
bytesIndex,
bytesIndexByte,
} {
// (obj may be nil)
nextcall:
for curCall := range index.Calls(obj) {
// Check file version.
if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curCall), versions.Go1_18) {
continue // strings.Index not available in this file
}
indexCall := curCall.Node().(*ast.CallExpr) // the call to strings.Index, etc.
obj := typeutil.Callee(info, indexCall)
if obj == nil {
continue
}
var iIdent *ast.Ident // defining identifier of i var
switch ek, idx := curCall.ParentEdge(); ek {
case edge.ValueSpec_Values:
// Have: var i = strings.Index(...)
curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, idx)
iIdent = curName.Node().(*ast.Ident)
case edge.AssignStmt_Rhs:
// Have: i := strings.Index(...)
// (Must be i's definition.)
curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, idx)
iIdent, _ = curLhs.Node().(*ast.Ident) // may be nil
}
if iIdent == nil {
continue
}
// Inv: iIdent is i's definition. The following would be skipped: 'var i int; i = strings.Index(...)'
// Get uses of i.
iObj := info.ObjectOf(iIdent)
if iObj == nil {
continue
}
var (
s = indexCall.Args[0]
substr = indexCall.Args[1]
)
// Check that there are no statements that alter the value of s
// or substr after the call to Index().
if !indexArgValid(info, index, s, indexCall.Pos()) ||
!indexArgValid(info, index, substr, indexCall.Pos()) {
continue nextcall
}
// Next, examine all uses of i. If the only uses are of the
// forms mentioned above (e.g. i < 0, i >= 0, s[:i] and s[i +
// len(substr)]), then we can replace the call to Index()
// with a call to Cut() and use the returned ok, before,
// and after variables accordingly.
lessZero, greaterNegOne, beforeSlice, afterSlice := checkIdxUses(pass.TypesInfo, index.Uses(iObj), s, substr)
// Either there are no uses of before, after, or ok, or some use
// of i does not match our criteria - don't suggest a fix.
if lessZero == nil && greaterNegOne == nil && beforeSlice == nil && afterSlice == nil {
continue
}
// If the only uses are ok and !ok, don't suggest a Cut() fix - these should be using Contains()
isContains := (len(lessZero) > 0 || len(greaterNegOne) > 0) && len(beforeSlice) == 0 && len(afterSlice) == 0
scope := iObj.Parent()
var (
// TODO(adonovan): avoid FreshName when not needed; see errorsastype.
okVarName = refactor.FreshName(scope, iIdent.Pos(), "ok")
beforeVarName = refactor.FreshName(scope, iIdent.Pos(), "before")
afterVarName = refactor.FreshName(scope, iIdent.Pos(), "after")
foundVarName = refactor.FreshName(scope, iIdent.Pos(), "found") // for Contains()
)
// If there will be no uses of ok, before, or after, use the
// blank identifier instead.
if len(lessZero) == 0 && len(greaterNegOne) == 0 {
okVarName = "_"
}
if len(beforeSlice) == 0 {
beforeVarName = "_"
}
if len(afterSlice) == 0 {
afterVarName = "_"
}
var edits []analysis.TextEdit
replace := func(exprs []ast.Expr, new string) {
for _, expr := range exprs {
edits = append(edits, analysis.TextEdit{
Pos: expr.Pos(),
End: expr.End(),
NewText: []byte(new),
})
}
}
// Get the ident for the call to strings.Index, which could just be
// "Index" if the strings package is dot imported.
indexCallId := typesinternal.UsedIdent(info, indexCall.Fun)
replacedFunc := "Cut"
if isContains {
replacedFunc = "Contains"
replace(lessZero, "!"+foundVarName) // idx < 0 -> !found
replace(greaterNegOne, foundVarName) // idx > -1 -> found
// Replace the assignment with found, and replace the call to
// Index or IndexByte with a call to Contains.
// i := strings.Index (...)
// ----- --------
// found := strings.Contains(...)
edits = append(edits, analysis.TextEdit{
Pos: iIdent.Pos(),
End: iIdent.End(),
NewText: []byte(foundVarName),
}, analysis.TextEdit{
Pos: indexCallId.Pos(),
End: indexCallId.End(),
NewText: []byte("Contains"),
})
} else {
replace(lessZero, "!"+okVarName) // idx < 0 -> !ok
replace(greaterNegOne, okVarName) // idx > -1 -> ok
replace(beforeSlice, beforeVarName) // s[:idx] -> before
replace(afterSlice, afterVarName) // s[idx+k:] -> after
// Replace the assignment with before, after, ok, and replace
// the call to Index or IndexByte with a call to Cut.
// i := strings.Index(...)
// ----------------- -----
// before, after, ok := strings.Cut (...)
edits = append(edits, analysis.TextEdit{
Pos: iIdent.Pos(),
End: iIdent.End(),
NewText: fmt.Appendf(nil, "%s, %s, %s", beforeVarName, afterVarName, okVarName),
}, analysis.TextEdit{
Pos: indexCallId.Pos(),
End: indexCallId.End(),
NewText: []byte("Cut"),
})
}
// Calls to IndexByte have a byte as their second arg, which
// must be converted to a string or []byte to be a valid arg for Cut/Contains.
if obj.Name() == "IndexByte" {
switch obj.Pkg().Name() {
case "strings":
searchByteVal := info.Types[substr].Value
if searchByteVal == nil {
// substr is a variable, e.g. substr := byte('b')
// use string(substr)
edits = append(edits, []analysis.TextEdit{
{
Pos: substr.Pos(),
NewText: []byte("string("),
},
{
Pos: substr.End(),
NewText: []byte(")"),
},
}...)
} else {
// substr is a byte constant
val, _ := constant.Int64Val(searchByteVal) // inv: must be a valid byte
// strings.Cut/Contains requires a string, so convert byte literal to string literal; e.g. 'a' -> "a", 55 -> "7"
edits = append(edits, analysis.TextEdit{
Pos: substr.Pos(),
End: substr.End(),
NewText: strconv.AppendQuote(nil, string(byte(val))),
})
}
case "bytes":
// bytes.Cut/Contains requires a []byte, so wrap substr in a []byte{}
edits = append(edits, []analysis.TextEdit{
{
Pos: substr.Pos(),
NewText: []byte("[]byte{"),
},
{
Pos: substr.End(),
NewText: []byte("}"),
},
}...)
}
}
pass.Report(analysis.Diagnostic{
Pos: indexCall.Fun.Pos(),
End: indexCall.Fun.End(),
Message: fmt.Sprintf("%s.%s can be simplified using %s.%s",
obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc),
Category: "stringscut",
SuggestedFixes: []analysis.SuggestedFix{{
Message: fmt.Sprintf("Simplify %s.%s call using %s.%s", obj.Pkg().Name(), obj.Name(), obj.Pkg().Name(), replacedFunc),
TextEdits: edits,
}},
})
}
}
return nil, nil
}
// indexArgValid reports whether expr is a valid strings.Index(_, _) arg
// for the transformation. An arg is valid iff it is:
// - constant;
// - a local variable with no modifying uses after the Index() call; or
// - []byte(x) where x is also valid by this definition.
// All other expressions are assumed not referentially transparent,
// so we cannot be sure that all uses are safe to replace.
func indexArgValid(info *types.Info, index *typeindex.Index, expr ast.Expr, afterPos token.Pos) bool {
tv := info.Types[expr]
if tv.Value != nil {
return true // constant
}
switch expr := expr.(type) {
case *ast.CallExpr:
return types.Identical(tv.Type, byteSliceType) &&
indexArgValid(info, index, expr.Args[0], afterPos) // check s in []byte(s)
case *ast.Ident:
sObj := info.Uses[expr]
sUses := index.Uses(sObj)
return !hasModifyingUses(info, sUses, afterPos)
default:
// For now, skip instances where s or substr are not
// identifers, basic lits, or call expressions of the form
// []byte(s).
// TODO(mkalil): Handle s and substr being expressions like ptr.field[i].
// From adonovan: We'd need to analyze s and substr to see
// whether they are referentially transparent, and if not,
// analyze all code between declaration and use and see if
// there are statements or expressions with potential side
// effects.
return false
}
}
// checkIdxUses inspects the uses of i to make sure they match certain criteria that
// allows us to suggest a modernization. If all uses of i, s and substr match
// one of the following four valid formats, it returns a list of occurrences for
// each format. If any of the uses do not match one of the formats, return nil
// for all values, since we should not offer a replacement.
// 1. lessZero - a condition involving i establishing that i is negative (e.g. i < 0, 0 > i, i == -1, -1 == i)
// 2. greaterNegOne - a condition involving i establishing that i is non-negative (e.g. i >= 0, 0 <= i, i == 0, 0 == i)
// 3. beforeSlice - a slice of `s` that matches either s[:i], s[0:i]
// 4. afterSlice - a slice of `s` that matches one of: s[i+len(substr):], s[len(substr) + i:], s[i + const], s[k + i] (where k = len(substr))
func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr ast.Expr) (lessZero, greaterNegOne, beforeSlice, afterSlice []ast.Expr) {
use := func(cur inspector.Cursor) bool {
ek, _ := cur.ParentEdge()
n := cur.Parent().Node()
switch ek {
case edge.BinaryExpr_X, edge.BinaryExpr_Y:
check := n.(*ast.BinaryExpr)
switch checkIdxComparison(info, check) {
case -1:
lessZero = append(lessZero, check)
return true
case 1:
greaterNegOne = append(greaterNegOne, check)
return true
}
// Check does not establish that i < 0 or i > -1.
// Might be part of an outer slice expression like s[i + k]
// which requires a different check.
// Check that the thing being sliced is s and that the slice
// doesn't have a max index.
if slice, ok := cur.Parent().Parent().Node().(*ast.SliceExpr); ok &&
sameObject(info, s, slice.X) &&
slice.Max == nil {
if isBeforeSlice(info, ek, slice) {
beforeSlice = append(beforeSlice, slice)
return true
} else if isAfterSlice(info, ek, slice, substr) {
afterSlice = append(afterSlice, slice)
return true
}
}
case edge.SliceExpr_Low, edge.SliceExpr_High:
slice := n.(*ast.SliceExpr)
// Check that the thing being sliced is s and that the slice doesn't
// have a max index.
if sameObject(info, s, slice.X) && slice.Max == nil {
if isBeforeSlice(info, ek, slice) {
beforeSlice = append(beforeSlice, slice)
return true
} else if isAfterSlice(info, ek, slice, substr) {
afterSlice = append(afterSlice, slice)
return true
}
}
}
return false
}
for curIdent := range uses {
if !use(curIdent) {
return nil, nil, nil, nil
}
}
return lessZero, greaterNegOne, beforeSlice, afterSlice
}
// hasModifyingUses reports whether any of the uses involve potential
// modifications. Uses involving assignments before the "afterPos" won't be
// considered.
func hasModifyingUses(info *types.Info, uses iter.Seq[inspector.Cursor], afterPos token.Pos) bool {
for curUse := range uses {
ek, _ := curUse.ParentEdge()
if ek == edge.AssignStmt_Lhs {
if curUse.Node().Pos() <= afterPos {
continue
}
assign := curUse.Parent().Node().(*ast.AssignStmt)
if sameObject(info, assign.Lhs[0], curUse.Node().(*ast.Ident)) {
// Modifying use because we are reassigning the value of the object.
return true
}
} else if ek == edge.UnaryExpr_X &&
curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND {
// Modifying use because we might be passing the object by reference (an explicit &).
// We can ignore the case where we have a method call on the expression (which
// has an implicit &) because we know the type of s and substr are strings
// which cannot have methods on them.
return true
}
}
return false
}
// checkIdxComparison reports whether the check establishes that i is negative
// or non-negative. It returns -1 in the first case, 1 in the second, and 0 if
// we can confirm neither condition. We assume that a check passed to
// checkIdxComparison has i as one of its operands.
func checkIdxComparison(info *types.Info, check *ast.BinaryExpr) int {
// Check establishes that i is negative.
// e.g.: i < 0, 0 > i, i == -1, -1 == i
if check.Op == token.LSS && (isNegativeConst(info, check.Y) || isZeroIntConst(info, check.Y)) || //i < (0 or neg)
check.Op == token.GTR && (isNegativeConst(info, check.X) || isZeroIntConst(info, check.X)) || // (0 or neg) > i
check.Op == token.LEQ && (isNegativeConst(info, check.Y)) || //i <= (neg)
check.Op == token.GEQ && (isNegativeConst(info, check.X)) || // (neg) >= i
check.Op == token.EQL &&
(isNegativeConst(info, check.X) || isNegativeConst(info, check.Y)) { // i == neg; neg == i
return -1
}
// Check establishes that i is non-negative.
// e.g.: i >= 0, 0 <= i, i == 0, 0 == i
if check.Op == token.GTR && (isNonNegativeConst(info, check.Y) || isIntLiteral(info, check.Y, -1)) || // i > (non-neg or -1)
check.Op == token.LSS && (isNonNegativeConst(info, check.X) || isIntLiteral(info, check.X, -1)) || // (non-neg or -1) < i
check.Op == token.GEQ && isNonNegativeConst(info, check.Y) || // i >= (non-neg)
check.Op == token.LEQ && isNonNegativeConst(info, check.X) || // (non-neg) <= i
check.Op == token.EQL &&
(isNonNegativeConst(info, check.X) || isNonNegativeConst(info, check.Y)) { // i == non-neg; non-neg == i
return 1
}
return 0
}
// isNegativeConst returns true if the expr is a const int with value < zero.
func isNegativeConst(info *types.Info, expr ast.Expr) bool {
if tv, ok := info.Types[expr]; ok && tv.Value != nil && tv.Value.Kind() == constant.Int {
if v, ok := constant.Int64Val(tv.Value); ok {
return v < 0
}
}
return false
}
// isNoneNegativeConst returns true if the expr is a const int with value >= zero.
func isNonNegativeConst(info *types.Info, expr ast.Expr) bool {
if tv, ok := info.Types[expr]; ok && tv.Value != nil && tv.Value.Kind() == constant.Int {
if v, ok := constant.Int64Val(tv.Value); ok {
return v >= 0
}
}
return false
}
// isBeforeSlice reports whether the SliceExpr is of the form s[:i] or s[0:i].
func isBeforeSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr) bool {
return ek == edge.SliceExpr_High && (slice.Low == nil || isZeroIntConst(info, slice.Low))
}
// isAfterSlice reports whether the SliceExpr is of the form s[i+len(substr):],
// or s[i + k:] where k is a const is equal to len(substr).
func isAfterSlice(info *types.Info, ek edge.Kind, slice *ast.SliceExpr, substr ast.Expr) bool {
lowExpr, ok := slice.Low.(*ast.BinaryExpr)
if !ok || slice.High != nil {
return false
}
// Returns true if the expression is a call to len(substr).
isLenCall := func(expr ast.Expr) bool {
call, ok := expr.(*ast.CallExpr)
if !ok || len(call.Args) != 1 {
return false
}
return sameObject(info, substr, call.Args[0]) && typeutil.Callee(info, call) == builtinLen
}
// Handle len([]byte(substr))
if is[*ast.CallExpr](substr) {
call := substr.(*ast.CallExpr)
tv := info.Types[call.Fun]
if tv.IsType() && types.Identical(tv.Type, byteSliceType) {
// Only one arg in []byte conversion.
substr = call.Args[0]
}
}
substrLen := -1
substrVal := info.Types[substr].Value
if substrVal != nil {
switch substrVal.Kind() {
case constant.String:
substrLen = len(constant.StringVal(substrVal))
case constant.Int:
// constant.Value is a byte literal, e.g. bytes.IndexByte(_, 'a')
// or a numeric byte literal, e.g. bytes.IndexByte(_, 65)
substrLen = 1
}
}
switch ek {
case edge.BinaryExpr_X:
kVal := info.Types[lowExpr.Y].Value
if kVal == nil {
// i + len(substr)
return lowExpr.Op == token.ADD && isLenCall(lowExpr.Y)
} else {
// i + k
kInt, ok := constant.Int64Val(kVal)
return ok && substrLen == int(kInt)
}
case edge.BinaryExpr_Y:
kVal := info.Types[lowExpr.X].Value
if kVal == nil {
// len(substr) + i
return lowExpr.Op == token.ADD && isLenCall(lowExpr.X)
} else {
// k + i
kInt, ok := constant.Int64Val(kVal)
return ok && substrLen == int(kInt)
}
}
return false
}
// sameObject reports whether we know that the expressions resolve to the same object.
func sameObject(info *types.Info, expr1, expr2 ast.Expr) bool {
if ident1, ok := expr1.(*ast.Ident); ok {
if ident2, ok := expr2.(*ast.Ident); ok {
uses1, ok1 := info.Uses[ident1]
uses2, ok2 := info.Uses[ident2]
return ok1 && ok2 && uses1 == uses2
}
}
return false
}

View file

@ -12,22 +12,20 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
) )
var StringsCutPrefixAnalyzer = &analysis.Analyzer{ var StringsCutPrefixAnalyzer = &analysis.Analyzer{
Name: "stringscutprefix", Name: "stringscutprefix",
Doc: analysisinternal.MustExtractDoc(doc, "stringscutprefix"), Doc: analyzerutil.MustExtractDoc(doc, "stringscutprefix"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -56,10 +54,7 @@ var StringsCutPrefixAnalyzer = &analysis.Analyzer{
// Variants: // Variants:
// - bytes.HasPrefix/HasSuffix usage as pattern 1. // - bytes.HasPrefix/HasSuffix usage as pattern 1.
func stringscutprefix(pass *analysis.Pass) (any, error) { func stringscutprefix(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
var ( var (
inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo info = pass.TypesInfo
@ -72,7 +67,7 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
return nil, nil return nil, nil
} }
for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.20") { for curFile := range filesUsingGoVersion(pass, versions.Go1_20) {
for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) { for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) {
ifStmt := curIfStmt.Node().(*ast.IfStmt) ifStmt := curIfStmt.Node().(*ast.IfStmt)
@ -206,6 +201,7 @@ func stringscutprefix(pass *analysis.Pass) (any, error) {
if astutil.EqualSyntax(lhs, bin.X) && astutil.EqualSyntax(call.Args[0], bin.Y) || if astutil.EqualSyntax(lhs, bin.X) && astutil.EqualSyntax(call.Args[0], bin.Y) ||
(astutil.EqualSyntax(lhs, bin.Y) && astutil.EqualSyntax(call.Args[0], bin.X)) { (astutil.EqualSyntax(lhs, bin.Y) && astutil.EqualSyntax(call.Args[0], bin.X)) {
// TODO(adonovan): avoid FreshName when not needed; see errorsastype.
okVarName := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok") okVarName := refactor.FreshName(info.Scopes[ifStmt], ifStmt.Pos(), "ok")
// Have one of: // Have one of:
// if rest := TrimPrefix(s, prefix); rest != s { (ditto Suffix) // if rest := TrimPrefix(s, prefix); rest != s { (ditto Suffix)

View file

@ -13,19 +13,17 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge" "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/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
) )
var StringsSeqAnalyzer = &analysis.Analyzer{ var StringsSeqAnalyzer = &analysis.Analyzer{
Name: "stringsseq", Name: "stringsseq",
Doc: analysisinternal.MustExtractDoc(doc, "stringsseq"), Doc: analyzerutil.MustExtractDoc(doc, "stringsseq"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -48,10 +46,7 @@ var StringsSeqAnalyzer = &analysis.Analyzer{
// - bytes.SplitSeq // - bytes.SplitSeq
// - bytes.FieldsSeq // - bytes.FieldsSeq
func stringsseq(pass *analysis.Pass) (any, error) { func stringsseq(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
var ( var (
inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo info = pass.TypesInfo
@ -64,7 +59,7 @@ func stringsseq(pass *analysis.Pass) (any, error) {
return nil, nil return nil, nil
} }
for curFile := range filesUsing(inspect, info, "go1.24") { for curFile := range filesUsingGoVersion(pass, versions.Go1_24) {
for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) { for curRange := range curFile.Preorder((*ast.RangeStmt)(nil)) {
rng := curRange.Node().(*ast.RangeStmt) rng := curRange.Node().(*ast.RangeStmt)

View file

@ -17,19 +17,18 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
) )
var TestingContextAnalyzer = &analysis.Analyzer{ var TestingContextAnalyzer = &analysis.Analyzer{
Name: "testingcontext", Name: "testingcontext",
Doc: analysisinternal.MustExtractDoc(doc, "testingcontext"), Doc: analyzerutil.MustExtractDoc(doc, "testingcontext"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -56,8 +55,6 @@ var TestingContextAnalyzer = &analysis.Analyzer{
// - the call is within a test or subtest function // - the call is within a test or subtest function
// - the relevant testing.{T,B,F} is named and not shadowed at the call // - the relevant testing.{T,B,F} is named and not shadowed at the call
func testingContext(pass *analysis.Pass) (any, error) { func testingContext(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
var ( var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo info = pass.TypesInfo
@ -137,7 +134,7 @@ calls:
testObj = isTestFn(info, n) testObj = isTestFn(info, n)
} }
} }
if testObj != nil && fileUses(info, astutil.EnclosingFile(cur), "go1.24") { if testObj != nil && analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(cur), versions.Go1_24) {
// Have a test function. Check that we can resolve the relevant // Have a test function. Check that we can resolve the relevant
// testing.{T,B,F} at the current position. // testing.{T,B,F} at the current position.
if _, obj := lhs[0].Parent().LookupParent(testObj.Name(), lhs[0].Pos()); obj == testObj { if _, obj := lhs[0].Parent().LookupParent(testObj.Name(), lhs[0].Pos()); obj == testObj {

View file

@ -14,19 +14,18 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/analysisinternal/generated" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
typeindexanalyzer "golang.org/x/tools/internal/analysisinternal/typeindex"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/typesinternal/typeindex"
"golang.org/x/tools/internal/versions"
) )
var WaitGroupAnalyzer = &analysis.Analyzer{ var WaitGroupAnalyzer = &analysis.Analyzer{
Name: "waitgroup", Name: "waitgroup",
Doc: analysisinternal.MustExtractDoc(doc, "waitgroup"), Doc: analyzerutil.MustExtractDoc(doc, "waitgroup"),
Requires: []*analysis.Analyzer{ Requires: []*analysis.Analyzer{
generated.Analyzer,
inspect.Analyzer, inspect.Analyzer,
typeindexanalyzer.Analyzer, typeindexanalyzer.Analyzer,
}, },
@ -61,8 +60,6 @@ var WaitGroupAnalyzer = &analysis.Analyzer{
// other effects, or blocked, or if WaitGroup.Go propagated panics // other effects, or blocked, or if WaitGroup.Go propagated panics
// from child to parent goroutine, the argument would be different.) // from child to parent goroutine, the argument would be different.)
func waitgroup(pass *analysis.Pass) (any, error) { func waitgroup(pass *analysis.Pass) (any, error) {
skipGenerated(pass)
var ( var (
index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
info = pass.TypesInfo info = pass.TypesInfo
@ -128,7 +125,7 @@ func waitgroup(pass *analysis.Pass) (any, error) {
} }
file := astutil.EnclosingFile(curAddCall) file := astutil.EnclosingFile(curAddCall)
if !fileUses(info, file, "go1.25") { if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_25) {
continue continue
} }
tokFile := pass.Fset.File(file.Pos()) tokFile := pass.Fset.File(file.Pos())

View file

@ -15,7 +15,7 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -24,7 +24,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "nilfunc", Name: "nilfunc",
Doc: analysisinternal.MustExtractDoc(doc, "nilfunc"), Doc: analyzerutil.MustExtractDoc(doc, "nilfunc"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/nilfunc", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/nilfunc",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,

View file

@ -21,7 +21,7 @@ import (
"golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/fmtstr" "golang.org/x/tools/internal/fmtstr"
"golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typeparams"
@ -38,7 +38,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "printf", Name: "printf",
Doc: analysisinternal.MustExtractDoc(doc, "printf"), Doc: analyzerutil.MustExtractDoc(doc, "printf"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/printf", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/printf",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,
@ -612,7 +612,7 @@ func checkPrintf(pass *analysis.Pass, fileVersion string, kind Kind, call *ast.C
// breaking existing tests and CI scripts. // breaking existing tests and CI scripts.
if idx == len(call.Args)-1 && if idx == len(call.Args)-1 &&
fileVersion != "" && // fail open fileVersion != "" && // fail open
versions.AtLeast(fileVersion, "go1.24") { versions.AtLeast(fileVersion, versions.Go1_24) {
pass.Report(analysis.Diagnostic{ pass.Report(analysis.Diagnostic{
Pos: formatArg.Pos(), Pos: formatArg.Pos(),
@ -662,7 +662,7 @@ func checkPrintf(pass *analysis.Pass, fileVersion string, kind Kind, call *ast.C
anyIndex = true anyIndex = true
} }
rng := opRange(formatArg, op) rng := opRange(formatArg, op)
if !okPrintfArg(pass, call, rng, &maxArgIndex, firstArg, name, op) { if !okPrintfArg(pass, fileVersion, call, rng, &maxArgIndex, firstArg, name, op) {
// One error per format is enough. // One error per format is enough.
return return
} }
@ -694,9 +694,9 @@ func checkPrintf(pass *analysis.Pass, fileVersion string, kind Kind, call *ast.C
// such as the position of the %v substring of "...%v...". // such as the position of the %v substring of "...%v...".
func opRange(formatArg ast.Expr, op *fmtstr.Operation) analysis.Range { func opRange(formatArg ast.Expr, op *fmtstr.Operation) analysis.Range {
if lit, ok := formatArg.(*ast.BasicLit); ok { if lit, ok := formatArg.(*ast.BasicLit); ok {
start, end, err := astutil.RangeInStringLiteral(lit, op.Range.Start, op.Range.End) rng, err := astutil.RangeInStringLiteral(lit, op.Range.Start, op.Range.End)
if err == nil { if err == nil {
return analysisinternal.Range(start, end) // position of "%v" return rng // position of "%v"
} }
} }
return formatArg // entire format string return formatArg // entire format string
@ -707,6 +707,7 @@ type printfArgType int
const ( const (
argBool printfArgType = 1 << iota argBool printfArgType = 1 << iota
argByte
argInt argInt
argRune argRune
argString argString
@ -751,7 +752,7 @@ var printVerbs = []printVerb{
{'o', sharpNumFlag, argInt | argPointer}, {'o', sharpNumFlag, argInt | argPointer},
{'O', sharpNumFlag, argInt | argPointer}, {'O', sharpNumFlag, argInt | argPointer},
{'p', "-#", argPointer}, {'p', "-#", argPointer},
{'q', " -+.0#", argRune | argInt | argString}, {'q', " -+.0#", argRune | argInt | argString}, // note: when analyzing go1.26 code, argInt => argByte
{'s', " -+.0", argString}, {'s', " -+.0", argString},
{'t', "-", argBool}, {'t', "-", argBool},
{'T', "-", anyType}, {'T', "-", anyType},
@ -765,7 +766,7 @@ var printVerbs = []printVerb{
// okPrintfArg compares the operation to the arguments actually present, // okPrintfArg compares the operation to the arguments actually present,
// reporting any discrepancies it can discern, maxArgIndex was the index of the highest used index. // reporting any discrepancies it can discern, maxArgIndex was the index of the highest used index.
// If the final argument is ellipsissed, there's little it can do for that. // If the final argument is ellipsissed, there's little it can do for that.
func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, rng analysis.Range, maxArgIndex *int, firstArg int, name string, operation *fmtstr.Operation) (ok bool) { func okPrintfArg(pass *analysis.Pass, fileVersion string, call *ast.CallExpr, rng analysis.Range, maxArgIndex *int, firstArg int, name string, operation *fmtstr.Operation) (ok bool) {
verb := operation.Verb.Verb verb := operation.Verb.Verb
var v printVerb var v printVerb
found := false found := false
@ -777,6 +778,13 @@ func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, rng analysis.Range, ma
} }
} }
// When analyzing go1.26 code, rune and byte are the only %q integers (#72850).
if verb == 'q' &&
fileVersion != "" && // fail open
versions.AtLeast(fileVersion, versions.Go1_26) {
v.typ = argRune | argByte | argString
}
// Could verb's arg implement fmt.Formatter? // Could verb's arg implement fmt.Formatter?
// Skip check for the %w verb, which requires an error. // Skip check for the %w verb, which requires an error.
formatter := false formatter := false

View file

@ -204,8 +204,7 @@ func (m *argMatcher) match(typ types.Type, topLevel bool) bool {
case *types.Struct: case *types.Struct:
// report whether all the elements of the struct match the expected type. For // report whether all the elements of the struct match the expected type. For
// instance, with "%d" all the elements must be printable with the "%d" format. // instance, with "%d" all the elements must be printable with the "%d" format.
for i := 0; i < typ.NumFields(); i++ { for typf := range typ.Fields() {
typf := typ.Field(i)
if !m.match(typf.Type(), false) { if !m.match(typf.Type(), false) {
return false return false
} }
@ -228,14 +227,20 @@ func (m *argMatcher) match(typ types.Type, topLevel bool) bool {
types.Bool: types.Bool:
return m.t&argBool != 0 return m.t&argBool != 0
case types.Byte:
return m.t&(argInt|argByte) != 0
case types.Rune, types.UntypedRune:
return m.t&(argInt|argRune) != 0
case types.UntypedInt, case types.UntypedInt,
types.Int, types.Int,
types.Int8, types.Int8,
types.Int16, types.Int16,
types.Int32, // see case Rune for int32
types.Int64, types.Int64,
types.Uint, types.Uint,
types.Uint8, // see case Byte for uint8
types.Uint16, types.Uint16,
types.Uint32, types.Uint32,
types.Uint64, types.Uint64,
@ -259,9 +264,6 @@ func (m *argMatcher) match(typ types.Type, topLevel bool) bool {
case types.UnsafePointer: case types.UnsafePointer:
return m.t&(argPointer|argInt) != 0 return m.t&(argPointer|argInt) != 0
case types.UntypedRune:
return m.t&(argInt|argRune) != 0
case types.UntypedNil: case types.UntypedNil:
return false return false

View file

@ -19,7 +19,7 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -29,7 +29,7 @@ var doc string
// Analyzer describes sigchanyzer analysis function detector. // Analyzer describes sigchanyzer analysis function detector.
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "sigchanyzer", Name: "sigchanyzer",
Doc: analysisinternal.MustExtractDoc(doc, "sigchanyzer"), Doc: analyzerutil.MustExtractDoc(doc, "sigchanyzer"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/sigchanyzer", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/sigchanyzer",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,

View file

@ -19,7 +19,7 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -29,7 +29,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "slog", Name: "slog",
Doc: analysisinternal.MustExtractDoc(doc, "slog"), Doc: analyzerutil.MustExtractDoc(doc, "slog"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/slog", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/slog",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,
@ -168,7 +168,7 @@ func isAttr(t types.Type) bool {
// "slog.Logger.With" (instead of "(*log/slog.Logger).With") // "slog.Logger.With" (instead of "(*log/slog.Logger).With")
func shortName(fn *types.Func) string { func shortName(fn *types.Func) string {
var r string var r string
if recv := fn.Type().(*types.Signature).Recv(); recv != nil { if recv := fn.Signature().Recv(); recv != nil {
if _, named := typesinternal.ReceiverNamed(recv); named != nil { if _, named := typesinternal.ReceiverNamed(recv); named != nil {
r = named.Obj().Name() r = named.Obj().Name()
} else { } else {
@ -188,7 +188,7 @@ func kvFuncSkipArgs(fn *types.Func) (int, bool) {
return 0, false return 0, false
} }
var recvName string // by default a slog package function var recvName string // by default a slog package function
if recv := fn.Type().(*types.Signature).Recv(); recv != nil { if recv := fn.Signature().Recv(); recv != nil {
_, named := typesinternal.ReceiverNamed(recv) _, named := typesinternal.ReceiverNamed(recv)
if named == nil { if named == nil {
return 0, false // anon struct/interface return 0, false // anon struct/interface

View file

@ -13,7 +13,7 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
) )
//go:embed doc.go //go:embed doc.go
@ -21,7 +21,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "stdmethods", Name: "stdmethods",
Doc: analysisinternal.MustExtractDoc(doc, "stdmethods"), Doc: analyzerutil.MustExtractDoc(doc, "stdmethods"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stdmethods", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stdmethods",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,
@ -131,12 +131,12 @@ func canonicalMethod(pass *analysis.Pass, id *ast.Ident) {
} }
// Do the =s (if any) all match? // Do the =s (if any) all match?
if !matchParams(pass, expect.args, args, "=") || !matchParams(pass, expect.results, results, "=") { if !matchParams(expect.args, args, "=") || !matchParams(expect.results, results, "=") {
return return
} }
// Everything must match. // Everything must match.
if !matchParams(pass, expect.args, args, "") || !matchParams(pass, expect.results, results, "") { if !matchParams(expect.args, args, "") || !matchParams(expect.results, results, "") {
expectFmt := id.Name + "(" + argjoin(expect.args) + ")" expectFmt := id.Name + "(" + argjoin(expect.args) + ")"
if len(expect.results) == 1 { if len(expect.results) == 1 {
expectFmt += " " + argjoin(expect.results) expectFmt += " " + argjoin(expect.results)
@ -168,7 +168,7 @@ func argjoin(x []string) string {
} }
// Does each type in expect with the given prefix match the corresponding type in actual? // Does each type in expect with the given prefix match the corresponding type in actual?
func matchParams(pass *analysis.Pass, expect []string, actual *types.Tuple, prefix string) bool { func matchParams(expect []string, actual *types.Tuple, prefix string) bool {
for i, x := range expect { for i, x := range expect {
if !strings.HasPrefix(x, prefix) { if !strings.HasPrefix(x, prefix) {
continue continue

View file

@ -14,7 +14,7 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
"golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
@ -25,7 +25,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "stringintconv", Name: "stringintconv",
Doc: analysisinternal.MustExtractDoc(doc, "stringintconv"), Doc: analyzerutil.MustExtractDoc(doc, "stringintconv"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stringintconv", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stringintconv",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,

View file

@ -15,7 +15,7 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -30,7 +30,7 @@ func init() {
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "testinggoroutine", Name: "testinggoroutine",
Doc: analysisinternal.MustExtractDoc(doc, "testinggoroutine"), Doc: analyzerutil.MustExtractDoc(doc, "testinggoroutine"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/testinggoroutine", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/testinggoroutine",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,

View file

@ -36,7 +36,7 @@ func localFunctionDecls(info *types.Info, files []*ast.File) func(*types.Func) *
// isMethodNamed returns true if f is a method defined // isMethodNamed returns true if f is a method defined
// in package with the path pkgPath with a name in names. // in package with the path pkgPath with a name in names.
// //
// (Unlike [analysisinternal.IsMethodNamed], it ignores the receiver type name.) // (Unlike [analysis.IsMethodNamed], it ignores the receiver type name.)
func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool { func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool {
if f == nil { if f == nil {
return false return false
@ -44,7 +44,7 @@ func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool {
if f.Pkg() == nil || f.Pkg().Path() != pkgPath { if f.Pkg() == nil || f.Pkg().Path() != pkgPath {
return false return false
} }
if f.Type().(*types.Signature).Recv() == nil { if f.Signature().Recv() == nil {
return false return false
} }
return slices.Contains(names, f.Name()) return slices.Contains(names, f.Name())

View file

@ -15,7 +15,8 @@ import (
"unicode/utf8" "unicode/utf8"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -24,7 +25,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "tests", Name: "tests",
Doc: analysisinternal.MustExtractDoc(doc, "tests"), Doc: analyzerutil.MustExtractDoc(doc, "tests"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/tests", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/tests",
Run: run, Run: run,
} }
@ -464,7 +465,7 @@ func checkTest(pass *analysis.Pass, fn *ast.FuncDecl, prefix string) {
if tparams := fn.Type.TypeParams; tparams != nil && len(tparams.List) > 0 { if tparams := fn.Type.TypeParams; tparams != nil && len(tparams.List) > 0 {
// Note: cmd/go/internal/load also errors about TestXXX and BenchmarkXXX functions with type parameters. // Note: cmd/go/internal/load also errors about TestXXX and BenchmarkXXX functions with type parameters.
// We have currently decided to also warn before compilation/package loading. This can help users in IDEs. // We have currently decided to also warn before compilation/package loading. This can help users in IDEs.
pass.ReportRangef(analysisinternal.Range(tparams.Opening, tparams.Closing), pass.ReportRangef(astutil.RangeOf(tparams.Opening, tparams.Closing),
"%s has type parameters: it will not be run by go test as a %sXXX function", "%s has type parameters: it will not be run by go test as a %sXXX function",
fn.Name.Name, prefix) fn.Name.Name, prefix)
} }

View file

@ -18,7 +18,7 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -30,7 +30,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "timeformat", Name: "timeformat",
Doc: analysisinternal.MustExtractDoc(doc, "timeformat"), Doc: analyzerutil.MustExtractDoc(doc, "timeformat"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/timeformat", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/timeformat",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,
@ -39,7 +39,7 @@ var Analyzer = &analysis.Analyzer{
func run(pass *analysis.Pass) (any, error) { func run(pass *analysis.Pass) (any, error) {
// Note: (time.Time).Format is a method and can be a typeutil.Callee // Note: (time.Time).Format is a method and can be a typeutil.Callee
// without directly importing "time". So we cannot just skip this package // without directly importing "time". So we cannot just skip this package
// when !analysisinternal.Imports(pass.Pkg, "time"). // when !analysis.Imports(pass.Pkg, "time").
// TODO(taking): Consider using a prepass to collect typeutil.Callees. // TODO(taking): Consider using a prepass to collect typeutil.Callees.
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

View file

@ -13,7 +13,7 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -22,7 +22,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "unmarshal", Name: "unmarshal",
Doc: analysisinternal.MustExtractDoc(doc, "unmarshal"), Doc: analyzerutil.MustExtractDoc(doc, "unmarshal"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unmarshal", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unmarshal",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,
@ -39,7 +39,7 @@ func run(pass *analysis.Pass) (any, error) {
// Note: (*"encoding/json".Decoder).Decode, (* "encoding/gob".Decoder).Decode // Note: (*"encoding/json".Decoder).Decode, (* "encoding/gob".Decoder).Decode
// and (* "encoding/xml".Decoder).Decode are methods and can be a typeutil.Callee // 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 // without directly importing their packages. So we cannot just skip this package
// when !analysisinternal.Imports(pass.Pkg, "encoding/..."). // when !analysis.Imports(pass.Pkg, "encoding/...").
// TODO(taking): Consider using a prepass to collect typeutil.Callees. // TODO(taking): Consider using a prepass to collect typeutil.Callees.
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
@ -57,7 +57,7 @@ func run(pass *analysis.Pass) (any, error) {
// Classify the callee (without allocating memory). // Classify the callee (without allocating memory).
argidx := -1 argidx := -1
recv := fn.Type().(*types.Signature).Recv() recv := fn.Signature().Recv()
if fn.Name() == "Unmarshal" && recv == nil { if fn.Name() == "Unmarshal" && recv == nil {
// "encoding/json".Unmarshal // "encoding/json".Unmarshal
// "encoding/xml".Unmarshal // "encoding/xml".Unmarshal

View file

@ -15,7 +15,7 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor"
) )
@ -24,7 +24,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "unreachable", Name: "unreachable",
Doc: analysisinternal.MustExtractDoc(doc, "unreachable"), Doc: analyzerutil.MustExtractDoc(doc, "unreachable"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unreachable", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unreachable",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
RunDespiteErrors: true, RunDespiteErrors: true,

View file

@ -15,7 +15,7 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -24,7 +24,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "unsafeptr", Name: "unsafeptr",
Doc: analysisinternal.MustExtractDoc(doc, "unsafeptr"), Doc: analyzerutil.MustExtractDoc(doc, "unsafeptr"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unsafeptr", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unsafeptr",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,

View file

@ -25,7 +25,8 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil"
) )
//go:embed doc.go //go:embed doc.go
@ -33,7 +34,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "unusedresult", Name: "unusedresult",
Doc: analysisinternal.MustExtractDoc(doc, "unusedresult"), Doc: analyzerutil.MustExtractDoc(doc, "unusedresult"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedresult", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedresult",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,
@ -149,11 +150,11 @@ func run(pass *analysis.Pass) (any, error) {
if !ok { if !ok {
return // e.g. var or builtin return // e.g. var or builtin
} }
if sig := fn.Type().(*types.Signature); sig.Recv() != nil { if sig := fn.Signature(); sig.Recv() != nil {
// method (e.g. foo.String()) // method (e.g. foo.String())
if types.Identical(sig, sigNoArgsStringResult) { if types.Identical(sig, sigNoArgsStringResult) {
if stringMethods[fn.Name()] { if stringMethods[fn.Name()] {
pass.ReportRangef(analysisinternal.Range(call.Pos(), call.Lparen), pass.ReportRangef(astutil.RangeOf(call.Pos(), call.Lparen),
"result of (%s).%s call not used", "result of (%s).%s call not used",
sig.Recv().Type(), fn.Name()) sig.Recv().Type(), fn.Name())
} }
@ -161,7 +162,7 @@ func run(pass *analysis.Pass) (any, error) {
} else { } else {
// package-level function (e.g. fmt.Errorf) // package-level function (e.g. fmt.Errorf)
if pkgFuncs[[2]string{fn.Pkg().Path(), fn.Name()}] { if pkgFuncs[[2]string{fn.Pkg().Path(), fn.Name()}] {
pass.ReportRangef(analysisinternal.Range(call.Pos(), call.Lparen), pass.ReportRangef(astutil.RangeOf(call.Pos(), call.Lparen),
"result of %s.%s call not used", "result of %s.%s call not used",
fn.Pkg().Path(), fn.Name()) fn.Pkg().Path(), fn.Name())
} }

View file

@ -15,7 +15,7 @@ import (
"golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal"
) )
@ -24,7 +24,7 @@ var doc string
var Analyzer = &analysis.Analyzer{ var Analyzer = &analysis.Analyzer{
Name: "waitgroup", Name: "waitgroup",
Doc: analysisinternal.MustExtractDoc(doc, "waitgroup"), Doc: analyzerutil.MustExtractDoc(doc, "waitgroup"),
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/waitgroup", URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/waitgroup",
Requires: []*analysis.Analyzer{inspect.Analyzer}, Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run, Run: run,

View file

@ -49,7 +49,7 @@ import (
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/internal/analysisflags" "golang.org/x/tools/go/analysis/internal/analysisflags"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/analysis/driverutil"
"golang.org/x/tools/internal/facts" "golang.org/x/tools/internal/facts"
) )
@ -183,16 +183,18 @@ func processResults(fset *token.FileSet, id string, results []result) (exit int)
// but apply all fixes from the root actions. // but apply all fixes from the root actions.
// Convert results to form needed by ApplyFixes. // Convert results to form needed by ApplyFixes.
fixActions := make([]analysisflags.FixAction, len(results)) fixActions := make([]driverutil.FixAction, len(results))
for i, res := range results { for i, res := range results {
fixActions[i] = analysisflags.FixAction{ fixActions[i] = driverutil.FixAction{
Name: res.a.Name, Name: res.a.Name,
Pkg: res.pkg,
Files: res.files,
FileSet: fset, FileSet: fset,
ReadFileFunc: os.ReadFile, ReadFileFunc: os.ReadFile, // TODO(adonovan): respect overlays
Diagnostics: res.diagnostics, Diagnostics: res.diagnostics,
} }
} }
if err := analysisflags.ApplyFixes(fixActions, false); err != nil { if err := driverutil.ApplyFixes(fixActions, analysisflags.Diff, false); err != nil {
// Fail when applying fixes failed. // Fail when applying fixes failed.
log.Print(err) log.Print(err)
exit = 1 exit = 1
@ -209,7 +211,7 @@ func processResults(fset *token.FileSet, id string, results []result) (exit int)
if analysisflags.JSON { if analysisflags.JSON {
// JSON output // JSON output
tree := make(analysisflags.JSONTree) tree := make(driverutil.JSONTree)
for _, res := range results { for _, res := range results {
tree.Add(fset, id, res.a.Name, res.diagnostics, res.err) tree.Add(fset, id, res.a.Name, res.diagnostics, res.err)
} }
@ -225,7 +227,7 @@ func processResults(fset *token.FileSet, id string, results []result) (exit int)
} }
for _, res := range results { for _, res := range results {
for _, diag := range res.diagnostics { for _, diag := range res.diagnostics {
analysisflags.PrintPlain(os.Stderr, fset, analysisflags.Context, diag) driverutil.PrintPlain(os.Stderr, fset, analysisflags.Context, diag)
exit = 1 exit = 1
} }
} }
@ -428,7 +430,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
ResultOf: inputs, ResultOf: inputs,
Report: func(d analysis.Diagnostic) { Report: func(d analysis.Diagnostic) {
// Unitchecker doesn't apply fixes, but it does report them in the JSON output. // Unitchecker doesn't apply fixes, but it does report them in the JSON output.
if err := analysisinternal.ValidateFixes(fset, a, d.SuggestedFixes); err != nil { if err := driverutil.ValidateFixes(fset, a, d.SuggestedFixes); err != nil {
// Since we have diagnostics, the exit code will be nonzero, // Since we have diagnostics, the exit code will be nonzero,
// so logging these errors is sufficient. // so logging these errors is sufficient.
log.Println(err) log.Println(err)
@ -444,14 +446,14 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
AllPackageFacts: func() []analysis.PackageFact { return facts.AllPackageFacts(factFilter) }, AllPackageFacts: func() []analysis.PackageFact { return facts.AllPackageFacts(factFilter) },
Module: module, Module: module,
} }
pass.ReadFile = analysisinternal.CheckedReadFile(pass, os.ReadFile) pass.ReadFile = driverutil.CheckedReadFile(pass, os.ReadFile)
t0 := time.Now() t0 := time.Now()
act.result, act.err = a.Run(pass) act.result, act.err = a.Run(pass)
if act.err == nil { // resolve URLs on diagnostics. if act.err == nil { // resolve URLs on diagnostics.
for i := range act.diagnostics { for i := range act.diagnostics {
if url, uerr := analysisflags.ResolveURL(a, act.diagnostics[i]); uerr == nil { if url, uerr := driverutil.ResolveURL(a, act.diagnostics[i]); uerr == nil {
act.diagnostics[i].URL = url act.diagnostics[i].URL = url
} else { } else {
act.err = uerr // keep the last error act.err = uerr // keep the last error
@ -482,9 +484,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
results := make([]result, len(analyzers)) results := make([]result, len(analyzers))
for i, a := range analyzers { for i, a := range analyzers {
act := actions[a] act := actions[a]
results[i].a = a results[i] = result{pkg, files, a, act.diagnostics, act.err}
results[i].err = act.err
results[i].diagnostics = act.diagnostics
} }
data := facts.Encode() data := facts.Encode()
@ -499,6 +499,8 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
} }
type result struct { type result struct {
pkg *types.Package
files []*ast.File
a *analysis.Analyzer a *analysis.Analyzer
diagnostics []analysis.Diagnostic diagnostics []analysis.Diagnostic
err error err error

View file

@ -467,7 +467,9 @@ func (c Cursor) FindByPos(start, end token.Pos) (Cursor, bool) {
// This algorithm could be implemented using c.Inspect, // This algorithm could be implemented using c.Inspect,
// but it is about 2.5x slower. // but it is about 2.5x slower.
best := int32(-1) // push index of latest (=innermost) node containing range // best is the push-index of the latest (=innermost) node containing range.
// (Beware: latest is not always innermost because FuncDecl.{Name,Type} overlap.)
best := int32(-1)
for i, limit := c.indices(); i < limit; i++ { for i, limit := c.indices(); i < limit; i++ {
ev := events[i] ev := events[i]
if ev.index > i { // push? if ev.index > i { // push?
@ -481,6 +483,19 @@ func (c Cursor) FindByPos(start, end token.Pos) (Cursor, bool) {
continue continue
} }
} else { } else {
// Edge case: FuncDecl.Name and .Type overlap:
// Don't update best from Name to FuncDecl.Type.
//
// The condition can be read as:
// - n is FuncType
// - n.parent is FuncDecl
// - best is strictly beneath the FuncDecl
if ev.typ == 1<<nFuncType &&
events[ev.parent].typ == 1<<nFuncDecl &&
best > ev.parent {
continue
}
nodeEnd = n.End() nodeEnd = n.End()
if n.Pos() > start { if n.Pos() > start {
break // disjoint, after; stop break // disjoint, after; stop

View file

@ -13,7 +13,7 @@ import (
) )
type builder struct { type builder struct {
cfg *CFG blocks []*Block
mayReturn func(*ast.CallExpr) bool mayReturn func(*ast.CallExpr) bool
current *Block current *Block
lblocks map[string]*lblock // labeled blocks lblocks map[string]*lblock // labeled blocks
@ -32,12 +32,18 @@ start:
*ast.SendStmt, *ast.SendStmt,
*ast.IncDecStmt, *ast.IncDecStmt,
*ast.GoStmt, *ast.GoStmt,
*ast.DeferStmt,
*ast.EmptyStmt, *ast.EmptyStmt,
*ast.AssignStmt: *ast.AssignStmt:
// No effect on control flow. // No effect on control flow.
b.add(s) b.add(s)
case *ast.DeferStmt:
b.add(s)
// Assume conservatively that this behaves like:
// defer func() { recover() }
// so any subsequent panic may act like a return.
b.current.returns = true
case *ast.ExprStmt: case *ast.ExprStmt:
b.add(s) b.add(s)
if call, ok := s.X.(*ast.CallExpr); ok && !b.mayReturn(call) { if call, ok := s.X.(*ast.CallExpr); ok && !b.mayReturn(call) {
@ -64,6 +70,7 @@ start:
goto start // effectively: tailcall stmt(g, s.Stmt, label) goto start // effectively: tailcall stmt(g, s.Stmt, label)
case *ast.ReturnStmt: case *ast.ReturnStmt:
b.current.returns = true
b.add(s) b.add(s)
b.current = b.newBlock(KindUnreachable, s) b.current = b.newBlock(KindUnreachable, s)
@ -483,14 +490,13 @@ func (b *builder) labeledBlock(label *ast.Ident, stmt *ast.LabeledStmt) *lblock
// It does not automatically become the current block. // It does not automatically become the current block.
// comment is an optional string for more readable debugging output. // comment is an optional string for more readable debugging output.
func (b *builder) newBlock(kind BlockKind, stmt ast.Stmt) *Block { func (b *builder) newBlock(kind BlockKind, stmt ast.Stmt) *Block {
g := b.cfg
block := &Block{ block := &Block{
Index: int32(len(g.Blocks)), Index: int32(len(b.blocks)),
Kind: kind, Kind: kind,
Stmt: stmt, Stmt: stmt,
} }
block.Succs = block.succs2[:0] block.Succs = block.succs2[:0]
g.Blocks = append(g.Blocks, block) b.blocks = append(b.blocks, block)
return block return block
} }

View file

@ -47,6 +47,8 @@ import (
"go/ast" "go/ast"
"go/format" "go/format"
"go/token" "go/token"
"golang.org/x/tools/internal/cfginternal"
) )
// A CFG represents the control-flow graph of a single function. // A CFG represents the control-flow graph of a single function.
@ -54,6 +56,7 @@ import (
// The entry point is Blocks[0]; there may be multiple return blocks. // The entry point is Blocks[0]; there may be multiple return blocks.
type CFG struct { type CFG struct {
Blocks []*Block // block[0] is entry; order otherwise undefined Blocks []*Block // block[0] is entry; order otherwise undefined
noreturn bool // function body lacks a reachable return statement
} }
// A Block represents a basic block: a list of statements and // A Block represents a basic block: a list of statements and
@ -71,6 +74,7 @@ type Block struct {
Succs []*Block // successor nodes in the graph Succs []*Block // successor nodes in the graph
Index int32 // index within CFG.Blocks Index int32 // index within CFG.Blocks
Live bool // block is reachable from entry Live bool // block is reachable from entry
returns bool // block contains return or defer (which may recover and return)
Kind BlockKind // block kind Kind BlockKind // block kind
Stmt ast.Stmt // statement that gave rise to this block (see BlockKind for details) Stmt ast.Stmt // statement that gave rise to this block (see BlockKind for details)
@ -141,14 +145,14 @@ func (kind BlockKind) String() string {
func New(body *ast.BlockStmt, mayReturn func(*ast.CallExpr) bool) *CFG { func New(body *ast.BlockStmt, mayReturn func(*ast.CallExpr) bool) *CFG {
b := builder{ b := builder{
mayReturn: mayReturn, mayReturn: mayReturn,
cfg: new(CFG),
} }
b.current = b.newBlock(KindBody, body) b.current = b.newBlock(KindBody, body)
b.stmt(body) b.stmt(body)
// Compute liveness (reachability from entry point), breadth-first. // Compute liveness (reachability from entry point),
q := make([]*Block, 0, len(b.cfg.Blocks)) // breadth-first, marking Block.Live flags.
q = append(q, b.cfg.Blocks[0]) // entry point q := make([]*Block, 0, len(b.blocks))
q = append(q, b.blocks[0]) // entry point
for len(q) > 0 { for len(q) > 0 {
b := q[len(q)-1] b := q[len(q)-1]
q = q[:len(q)-1] q = q[:len(q)-1]
@ -162,12 +166,30 @@ func New(body *ast.BlockStmt, mayReturn func(*ast.CallExpr) bool) *CFG {
// Does control fall off the end of the function's body? // Does control fall off the end of the function's body?
// Make implicit return explicit. // Make implicit return explicit.
if b.current != nil && b.current.Live { if b.current != nil && b.current.Live {
b.current.returns = true
b.add(&ast.ReturnStmt{ b.add(&ast.ReturnStmt{
Return: body.End() - 1, Return: body.End() - 1,
}) })
} }
return b.cfg // Is any return (or defer+recover) block reachable?
noreturn := true
for _, bl := range b.blocks {
if bl.Live && bl.returns {
noreturn = false
break
}
}
return &CFG{Blocks: b.blocks, noreturn: noreturn}
}
// isNoReturn reports whether the function has no reachable return.
// TODO(adonovan): add (*CFG).NoReturn to public API.
func isNoReturn(_cfg any) bool { return _cfg.(*CFG).noreturn }
func init() {
cfginternal.IsNoReturn = isNoReturn // expose to ctrlflow analyzer
} }
func (b *Block) String() string { func (b *Block) String() string {
@ -187,6 +209,14 @@ func (b *Block) comment(fset *token.FileSet) string {
// //
// When control falls off the end of the function, the ReturnStmt is synthetic // When control falls off the end of the function, the ReturnStmt is synthetic
// and its [ast.Node.End] position may be beyond the end of the file. // and its [ast.Node.End] position may be beyond the end of the file.
//
// A function that contains no return statement (explicit or implied)
// may yet return normally, and may even return a nonzero value. For example:
//
// func() (res any) {
// defer func() { res = recover() }()
// panic(123)
// }
func (b *Block) Return() (ret *ast.ReturnStmt) { func (b *Block) Return() (ret *ast.ReturnStmt) {
if len(b.Nodes) > 0 { if len(b.Nodes) > 0 {
ret, _ = b.Nodes[len(b.Nodes)-1].(*ast.ReturnStmt) ret, _ = b.Nodes[len(b.Nodes)-1].(*ast.ReturnStmt)

View file

@ -249,7 +249,7 @@ func (enc *Encoder) For(obj types.Object) (Path, error) {
case *types.Func: case *types.Func:
// A func, if not package-level, must be a method. // A func, if not package-level, must be a method.
if recv := obj.Type().(*types.Signature).Recv(); recv == nil { if recv := obj.Signature().Recv(); recv == nil {
return "", fmt.Errorf("func is not a method: %v", obj) return "", fmt.Errorf("func is not a method: %v", obj)
} }
@ -405,7 +405,7 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) {
return "", false return "", false
} }
_, named := typesinternal.ReceiverNamed(meth.Type().(*types.Signature).Recv()) _, named := typesinternal.ReceiverNamed(meth.Signature().Recv())
if named == nil { if named == nil {
return "", false return "", false
} }

View file

@ -304,8 +304,7 @@ func (h hasher) hash(t types.Type) uint32 {
case *types.Named: case *types.Named:
hash := h.hashTypeName(t.Obj()) hash := h.hashTypeName(t.Obj())
targs := t.TypeArgs() targs := t.TypeArgs()
for i := 0; i < targs.Len(); i++ { for targ := range targs.Types() {
targ := targs.At(i)
hash += 2 * h.hash(targ) hash += 2 * h.hash(targ)
} }
return hash return hash

View file

@ -0,0 +1,6 @@
// 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 analyzerutil provides implementation helpers for analyzers.
package analyzerutil

View file

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package analysisinternal package analyzerutil
import ( import (
"fmt" "fmt"
@ -35,7 +35,7 @@ import (
// //
// var Analyzer = &analysis.Analyzer{ // var Analyzer = &analysis.Analyzer{
// Name: "halting", // Name: "halting",
// Doc: analysisinternal.MustExtractDoc(doc, "halting"), // Doc: analyzerutil.MustExtractDoc(doc, "halting"),
// ... // ...
// } // }
func MustExtractDoc(content, name string) string { func MustExtractDoc(content, name string) string {

View file

@ -0,0 +1,30 @@
// 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 analyzerutil
// This file defines helpers for calling [analysis.Pass.ReadFile].
import (
"go/token"
"os"
"golang.org/x/tools/go/analysis"
)
// 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
}
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
}

View file

@ -0,0 +1,42 @@
// 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 analyzerutil
import (
"go/ast"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/packagepath"
"golang.org/x/tools/internal/stdlib"
"golang.org/x/tools/internal/versions"
)
// FileUsesGoVersion reports whether the specified file may use features of the
// specified version of Go (e.g. "go1.24").
//
// Tip: we recommend using this check "late", just before calling
// pass.Report, rather than "early" (when entering each ast.File, or
// each candidate node of interest, during the traversal), because the
// operation is not free, yet is not a highly selective filter: the
// fraction of files that pass most version checks is high and
// increases over time.
func FileUsesGoVersion(pass *analysis.Pass, file *ast.File, version string) (_res bool) {
fileVersion := pass.TypesInfo.FileVersions[file]
// Standard packages that are part of toolchain bootstrapping
// are not considered to use a version of Go later than the
// current bootstrap toolchain version.
// The bootstrap rule does not cover tests,
// and some tests (e.g. debug/elf/file_test.go) rely on this.
pkgpath := pass.Pkg.Path()
if packagepath.IsStdPackage(pkgpath) &&
stdlib.IsBootstrapPackage(pkgpath) && // (excludes "*_test" external test packages)
!strings.HasSuffix(pass.Fset.File(file.Pos()).Name(), "_test.go") { // (excludes all tests)
fileVersion = stdlib.BootstrapVersion.String() // package must bootstrap
}
return !versions.Before(fileVersion, version)
}

View file

@ -2,22 +2,31 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package analysisflags // Package driverutil defines implementation helper functions for
// analysis drivers such as unitchecker, {single,multi}checker, and
// analysistest.
package driverutil
// This file defines the -fix logic common to unitchecker and // This file defines the -fix logic common to unitchecker and
// {single,multi}checker. // {single,multi}checker.
import ( import (
"bytes"
"fmt" "fmt"
"go/format" "go/ast"
"go/parser"
"go/printer"
"go/token" "go/token"
"go/types"
"log" "log"
"maps" "maps"
"os" "os"
"sort" "sort"
"strconv"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/astutil/free"
"golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/diff"
) )
@ -25,14 +34,16 @@ import (
// package) for the purposes of applying its diagnostics' fixes. // package) for the purposes of applying its diagnostics' fixes.
type FixAction struct { type FixAction struct {
Name string // e.g. "analyzer@package" Name string // e.g. "analyzer@package"
Pkg *types.Package // (for import removal)
Files []*ast.File
FileSet *token.FileSet FileSet *token.FileSet
ReadFileFunc analysisinternal.ReadFileFunc ReadFileFunc ReadFileFunc
Diagnostics []analysis.Diagnostic Diagnostics []analysis.Diagnostic
} }
// ApplyFixes attempts to apply the first suggested fix associated // ApplyFixes attempts to apply the first suggested fix associated
// with each diagnostic reported by the specified actions. // with each diagnostic reported by the specified actions.
// All fixes must have been validated by [analysisinternal.ValidateFixes]. // All fixes must have been validated by [ValidateFixes].
// //
// Each fix is treated as an independent change; fixes are merged in // Each fix is treated as an independent change; fixes are merged in
// an arbitrary deterministic order as if by a three-way diff tool // an arbitrary deterministic order as if by a three-way diff tool
@ -58,15 +69,15 @@ type FixAction struct {
// composition of the two fixes is semantically correct. Coalescing // composition of the two fixes is semantically correct. Coalescing
// identical edits is appropriate for imports, but not for, say, // identical edits is appropriate for imports, but not for, say,
// increments to a counter variable; the correct resolution in that // increments to a counter variable; the correct resolution in that
// case might be to increment it twice. Or consider two fixes that // case might be to increment it twice.
// each delete the penultimate reference to an import or local //
// variable: each fix is sound individually, and they may be textually // Or consider two fixes that each delete the penultimate reference to
// distant from each other, but when both are applied, the program is // a local variable: each fix is sound individually, and they may be
// no longer valid because it has an unreferenced import or local // textually distant from each other, but when both are applied, the
// variable. // program is no longer valid because it has an unreferenced local
// TODO(adonovan): investigate replacing the final "gofmt" step with a // variable. (ApplyFixes solves the analogous problem for imports by
// formatter that applies the unused-import deletion logic of // eliminating imports whose name is unreferenced in the remainder of
// "goimports". // the fixed file.)
// //
// Merging depends on both the order of fixes and they order of edits // Merging depends on both the order of fixes and they order of edits
// within them. For example, if three fixes add import "a" twice and // within them. For example, if three fixes add import "a" twice and
@ -80,12 +91,15 @@ type FixAction struct {
// applyFixes returns success if all fixes are valid, could be cleanly // applyFixes returns success if all fixes are valid, could be cleanly
// merged, and the corresponding files were successfully updated. // merged, and the corresponding files were successfully updated.
// //
// If the -diff flag was set, instead of updating the files it display the final // If printDiff (from the -diff flag) is set, instead of updating the
// patch composed of all the cleanly merged fixes. // files it display the final patch composed of all the cleanly merged
// fixes.
// //
// TODO(adonovan): handle file-system level aliases such as symbolic // TODO(adonovan): handle file-system level aliases such as symbolic
// links using robustio.FileID. // links using robustio.FileID.
func ApplyFixes(actions []FixAction, verbose bool) error { func ApplyFixes(actions []FixAction, printDiff, verbose bool) error {
generated := make(map[*token.File]bool)
// Select fixes to apply. // Select fixes to apply.
// //
// If there are several for a given Diagnostic, choose the first. // If there are several for a given Diagnostic, choose the first.
@ -96,6 +110,15 @@ func ApplyFixes(actions []FixAction, verbose bool) error {
} }
var fixes []*fixact var fixes []*fixact
for _, act := range actions { for _, act := range actions {
for _, file := range act.Files {
tokFile := act.FileSet.File(file.FileStart)
// Memoize, since there may be many actions
// for the same package (list of files).
if _, seen := generated[tokFile]; !seen {
generated[tokFile] = ast.IsGenerated(file)
}
}
for _, diag := range act.Diagnostics { for _, diag := range act.Diagnostics {
for i := range diag.SuggestedFixes { for i := range diag.SuggestedFixes {
fix := &diag.SuggestedFixes[i] fix := &diag.SuggestedFixes[i]
@ -119,7 +142,7 @@ func ApplyFixes(actions []FixAction, verbose bool) error {
// packages are not disjoint, due to test variants, so this // packages are not disjoint, due to test variants, so this
// would not really address the issue.) // would not really address the issue.)
baselineContent := make(map[string][]byte) baselineContent := make(map[string][]byte)
getBaseline := func(readFile analysisinternal.ReadFileFunc, filename string) ([]byte, error) { getBaseline := func(readFile ReadFileFunc, filename string) ([]byte, error) {
content, ok := baselineContent[filename] content, ok := baselineContent[filename]
if !ok { if !ok {
var err error var err error
@ -134,16 +157,32 @@ func ApplyFixes(actions []FixAction, verbose bool) error {
// Apply each fix, updating the current state // Apply each fix, updating the current state
// only if the entire fix can be cleanly merged. // only if the entire fix can be cleanly merged.
accumulatedEdits := make(map[string][]diff.Edit) var (
goodFixes := 0 accumulatedEdits = make(map[string][]diff.Edit)
filePkgs = make(map[string]*types.Package) // maps each file to an arbitrary package that includes it
goodFixes = 0 // number of fixes cleanly applied
skippedFixes = 0 // number of fixes skipped (because e.g. edits a generated file)
)
fixloop: fixloop:
for _, fixact := range fixes { for _, fixact := range fixes {
// Skip a fix if any of its edits touch a generated file.
for _, edit := range fixact.fix.TextEdits {
file := fixact.act.FileSet.File(edit.Pos)
if generated[file] {
skippedFixes++
continue fixloop
}
}
// Convert analysis.TextEdits to diff.Edits, grouped by file. // Convert analysis.TextEdits to diff.Edits, grouped by file.
// Precondition: a prior call to validateFix succeeded. // Precondition: a prior call to validateFix succeeded.
fileEdits := make(map[string][]diff.Edit) fileEdits := make(map[string][]diff.Edit)
for _, edit := range fixact.fix.TextEdits { for _, edit := range fixact.fix.TextEdits {
file := fixact.act.FileSet.File(edit.Pos) file := fixact.act.FileSet.File(edit.Pos)
filePkgs[file.Name()] = fixact.act.Pkg
baseline, err := getBaseline(fixact.act.ReadFileFunc, file.Name()) baseline, err := getBaseline(fixact.act.ReadFileFunc, file.Name())
if err != nil { if err != nil {
log.Printf("skipping fix to file %s: %v", file.Name(), err) log.Printf("skipping fix to file %s: %v", file.Name(), err)
@ -191,7 +230,7 @@ fixloop:
log.Printf("%s: fix %s applied", fixact.act.Name, fixact.fix.Message) log.Printf("%s: fix %s applied", fixact.act.Name, fixact.fix.Message)
} }
} }
badFixes := len(fixes) - goodFixes badFixes := len(fixes) - goodFixes - skippedFixes // number of fixes that could not be applied
// Show diff or update files to final state. // Show diff or update files to final state.
var files []string var files []string
@ -214,11 +253,11 @@ fixloop:
} }
// Attempt to format each file. // Attempt to format each file.
if formatted, err := format.Source(final); err == nil { if formatted, err := FormatSourceRemoveImports(filePkgs[file], final); err == nil {
final = formatted final = formatted
} }
if diffFlag { if printDiff {
// Since we formatted the file, we need to recompute the diff. // Since we formatted the file, we need to recompute the diff.
unified := diff.Unified(file+" (old)", file+" (new)", string(baseline), string(final)) unified := diff.Unified(file+" (old)", file+" (new)", string(baseline), string(final))
// TODO(adonovan): abstract the I/O. // TODO(adonovan): abstract the I/O.
@ -262,23 +301,149 @@ fixloop:
// These numbers are potentially misleading: // These numbers are potentially misleading:
// The denominator includes duplicate conflicting fixes due to // The denominator includes duplicate conflicting fixes due to
// common files in packages "p" and "p [p.test]", which may // common files in packages "p" and "p [p.test]", which may
// have been fixed fixed and won't appear in the re-run. // have been fixed and won't appear in the re-run.
// TODO(adonovan): eliminate identical fixes as an initial // TODO(adonovan): eliminate identical fixes as an initial
// filtering step. // filtering step.
// //
// TODO(adonovan): should we log that n files were updated in case of total victory? // TODO(adonovan): should we log that n files were updated in case of total victory?
if badFixes > 0 || filesUpdated < totalFiles { if badFixes > 0 || filesUpdated < totalFiles {
if diffFlag { if printDiff {
return fmt.Errorf("%d of %d fixes skipped (e.g. due to conflicts)", badFixes, len(fixes)) return fmt.Errorf("%d of %s skipped (e.g. due to conflicts)",
badFixes,
plural(len(fixes), "fix", "fixes"))
} else { } else {
return fmt.Errorf("applied %d of %d fixes; %d files updated. (Re-run the command to apply more.)", return fmt.Errorf("applied %d of %s; %s updated. (Re-run the command to apply more.)",
goodFixes, len(fixes), filesUpdated) goodFixes,
plural(len(fixes), "fix", "fixes"),
plural(filesUpdated, "file", "files"))
} }
} }
if verbose { if verbose {
log.Printf("applied %d fixes, updated %d files", len(fixes), filesUpdated) if skippedFixes > 0 {
log.Printf("skipped %s that would edit generated files",
plural(skippedFixes, "fix", "fixes"))
}
log.Printf("applied %s, updated %s",
plural(len(fixes), "fix", "fixes"),
plural(filesUpdated, "file", "files"))
} }
return nil return nil
} }
// FormatSourceRemoveImports is a variant of [format.Source] that
// removes imports that became redundant when fixes were applied.
//
// Import removal is necessarily heuristic since we do not have type
// information for the fixed file and thus cannot accurately tell
// whether k is among the free names of T{k: 0}, which requires
// knowledge of whether T is a struct type.
func FormatSourceRemoveImports(pkg *types.Package, src []byte) ([]byte, error) {
// This function was reduced from the "strict entire file"
// path through [format.Source].
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "fixed.go", src, parser.ParseComments|parser.SkipObjectResolution)
if err != nil {
return nil, err
}
ast.SortImports(fset, file)
removeUnneededImports(fset, pkg, file)
// printerNormalizeNumbers means to canonicalize number literal prefixes
// and exponents while printing. See https://golang.org/doc/go1.13#gofmt.
//
// This value is defined in go/printer specifically for go/format and cmd/gofmt.
const printerNormalizeNumbers = 1 << 30
cfg := &printer.Config{
Mode: printer.UseSpaces | printer.TabIndent | printerNormalizeNumbers,
Tabwidth: 8,
}
var buf bytes.Buffer
if err := cfg.Fprint(&buf, fset, file); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// removeUnneededImports removes import specs that are not referenced
// within the fixed file. It uses [free.Names] to heuristically
// approximate the set of imported names needed by the body of the
// file based only on syntax.
//
// pkg provides type information about the unmodified package, in
// particular the name that would implicitly be declared by a
// non-renaming import of a given existing dependency.
func removeUnneededImports(fset *token.FileSet, pkg *types.Package, file *ast.File) {
// Map each existing dependency to its default import name.
// (We'll need this to interpret non-renaming imports.)
packageNames := make(map[string]string)
for _, imp := range pkg.Imports() {
packageNames[imp.Path()] = imp.Name()
}
// Compute the set of free names of the file,
// ignoring its import decls.
freenames := make(map[string]bool)
for _, decl := range file.Decls {
if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT {
continue // skip import
}
// TODO(adonovan): we could do better than includeComplitIdents=false
// since we have type information about the unmodified package,
// which is a good source of heuristics.
const includeComplitIdents = false
maps.Copy(freenames, free.Names(decl, includeComplitIdents))
}
// Check whether each import's declared name is free (referenced) by the file.
var deletions []func()
for _, spec := range file.Imports {
path, err := strconv.Unquote(spec.Path.Value)
if err != nil {
continue // malformed import; ignore
}
explicit := "" // explicit PkgName, if any
if spec.Name != nil {
explicit = spec.Name.Name
}
name := explicit // effective PkgName
if name == "" {
// Non-renaming import: use package's default name.
name = packageNames[path]
}
switch name {
case "":
continue // assume it's a new import
case ".":
continue // dot imports are tricky
case "_":
continue // keep blank imports
}
if !freenames[name] {
// Import's effective name is not free in (not used by) the file.
// Enqueue it for deletion after the loop.
deletions = append(deletions, func() {
astutil.DeleteNamedImport(fset, file, explicit, path)
})
}
}
// Apply the deletions.
for _, del := range deletions {
del()
}
}
// plural returns "n nouns", selecting the plural form as approriate.
func plural(n int, singular, plural string) string {
if n == 1 {
return "1 " + singular
} else {
return fmt.Sprintf("%d %s", n, plural)
}
}

View file

@ -0,0 +1,161 @@
// 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 driverutil
// This file defined output helpers common to all drivers.
import (
"encoding/json"
"fmt"
"go/token"
"io"
"log"
"os"
"strings"
"golang.org/x/tools/go/analysis"
)
// TODO(adonovan): don't accept an io.Writer if we don't report errors.
// Either accept a bytes.Buffer (infallible), or return a []byte.
// PrintPlain prints a diagnostic in plain text form.
// If contextLines is nonnegative, it also prints the
// offending line plus this many lines of context.
func PrintPlain(out io.Writer, fset *token.FileSet, contextLines int, diag analysis.Diagnostic) {
print := func(pos, end token.Pos, message string) {
posn := fset.Position(pos)
fmt.Fprintf(out, "%s: %s\n", posn, message)
// show offending line plus N lines of context.
if contextLines >= 0 {
end := fset.Position(end)
if !end.IsValid() {
end = posn
}
// TODO(adonovan): highlight the portion of the line indicated
// by pos...end using ASCII art, terminal colors, etc?
data, _ := os.ReadFile(posn.Filename)
lines := strings.Split(string(data), "\n")
for i := posn.Line - contextLines; i <= end.Line+contextLines; i++ {
if 1 <= i && i <= len(lines) {
fmt.Fprintf(out, "%d\t%s\n", i, lines[i-1])
}
}
}
}
print(diag.Pos, diag.End, diag.Message)
for _, rel := range diag.Related {
print(rel.Pos, rel.End, "\t"+rel.Message)
}
}
// A JSONTree is a mapping from package ID to analysis name to result.
// Each result is either a jsonError or a list of JSONDiagnostic.
type JSONTree map[string]map[string]any
// A TextEdit describes the replacement of a portion of a file.
// Start and End are zero-based half-open indices into the original byte
// sequence of the file, and New is the new text.
type JSONTextEdit struct {
Filename string `json:"filename"`
Start int `json:"start"`
End int `json:"end"`
New string `json:"new"`
}
// A JSONSuggestedFix describes an edit that should be applied as a whole or not
// at all. It might contain multiple TextEdits/text_edits if the SuggestedFix
// consists of multiple non-contiguous edits.
type JSONSuggestedFix struct {
Message string `json:"message"`
Edits []JSONTextEdit `json:"edits"`
}
// A JSONDiagnostic describes the JSON schema of an analysis.Diagnostic.
//
// TODO(matloob): include End position if present.
type JSONDiagnostic struct {
Category string `json:"category,omitempty"`
Posn string `json:"posn"` // e.g. "file.go:line:column"
Message string `json:"message"`
SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"`
Related []JSONRelatedInformation `json:"related,omitempty"`
}
// A JSONRelated describes a secondary position and message related to
// a primary diagnostic.
//
// TODO(adonovan): include End position if present.
type JSONRelatedInformation struct {
Posn string `json:"posn"` // e.g. "file.go:line:column"
Message string `json:"message"`
}
// Add adds the result of analysis 'name' on package 'id'.
// The result is either a list of diagnostics or an error.
func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis.Diagnostic, err error) {
var v any
if err != nil {
type jsonError struct {
Err string `json:"error"`
}
v = jsonError{err.Error()}
} else if len(diags) > 0 {
diagnostics := make([]JSONDiagnostic, 0, len(diags))
for _, f := range diags {
var fixes []JSONSuggestedFix
for _, fix := range f.SuggestedFixes {
var edits []JSONTextEdit
for _, edit := range fix.TextEdits {
edits = append(edits, JSONTextEdit{
Filename: fset.Position(edit.Pos).Filename,
Start: fset.Position(edit.Pos).Offset,
End: fset.Position(edit.End).Offset,
New: string(edit.NewText),
})
}
fixes = append(fixes, JSONSuggestedFix{
Message: fix.Message,
Edits: edits,
})
}
var related []JSONRelatedInformation
for _, r := range f.Related {
related = append(related, JSONRelatedInformation{
Posn: fset.Position(r.Pos).String(),
Message: r.Message,
})
}
jdiag := JSONDiagnostic{
Category: f.Category,
Posn: fset.Position(f.Pos).String(),
Message: f.Message,
SuggestedFixes: fixes,
Related: related,
}
diagnostics = append(diagnostics, jdiag)
}
v = diagnostics
}
if v != nil {
m, ok := tree[id]
if !ok {
m = make(map[string]any)
tree[id] = m
}
m[name] = v
}
}
func (tree JSONTree) Print(out io.Writer) error {
data, err := json.MarshalIndent(tree, "", "\t")
if err != nil {
log.Panicf("internal error: JSON marshaling failed: %v", err)
}
_, err = fmt.Fprintf(out, "%s\n", data)
return err
}

View file

@ -0,0 +1,43 @@
// Copyright 2020 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 driverutil
// This file defines helpers for implementing [analysis.Pass.ReadFile].
import (
"fmt"
"slices"
"golang.org/x/tools/go/analysis"
)
// A ReadFileFunc is a function that returns the
// contents of a file, such as [os.ReadFile].
type ReadFileFunc = func(filename string) ([]byte, error)
// CheckedReadFile returns a wrapper around a Pass.ReadFile
// function that performs the appropriate checks.
func CheckedReadFile(pass *analysis.Pass, readFile ReadFileFunc) ReadFileFunc {
return func(filename string) ([]byte, error) {
if err := CheckReadable(pass, filename); err != nil {
return nil, err
}
return readFile(filename)
}
}
// CheckReadable enforces the access policy defined by the ReadFile field of [analysis.Pass].
func CheckReadable(pass *analysis.Pass, filename string) error {
if slices.Contains(pass.OtherFiles, filename) ||
slices.Contains(pass.IgnoredFiles, filename) {
return nil
}
for _, f := range pass.Files {
if pass.Fset.File(f.FileStart).Name() == filename {
return nil
}
}
return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename)
}

Some files were not shown because too many files have changed in this diff Show more