]> git.feebdaed.xyz Git - 0xmirror/go.git/commitdiff
all: update vendored x/tools
authorDmitri Shuralyov <dmitshur@golang.org>
Thu, 4 Dec 2025 22:57:40 +0000 (17:57 -0500)
committerGopher Robot <gobot@golang.org>
Fri, 5 Dec 2025 01:35:46 +0000 (17:35 -0800)
Pull in the following x/tools changes:

- CL 726000: go/analysis/passes/modernize: omitzero: suppress on kubebuilder
- CL 726621: internal/refactor/inline: built-ins may affect inference
- CL 727041: go/analysis/passes/modernize: fix stringscut false positives
- CL 727040: go/analysis/unitchecker: write fixed files to an archive

Fixes #76649.
Fixes #76287.
Fixes #76687.
For #71859.

[git-generate]
go install golang.org/x/build/cmd/updatestd@latest
go install golang.org/x/tools/cmd/bundle@latest
updatestd -goroot=$(pwd) -branch=internal-branch.go1.26-vendor

Change-Id: I0a369ad85b06adab3a977c2c523b8214fb53271a
Reviewed-on: https://go-review.googlesource.com/c/go/+/727022
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
src/cmd/go.mod
src/cmd/go.sum
src/cmd/vendor/golang.org/x/tools/go/analysis/passes/modernize/doc.go
src/cmd/vendor/golang.org/x/tools/go/analysis/passes/modernize/omitzero.go
src/cmd/vendor/golang.org/x/tools/go/analysis/passes/modernize/stringscut.go
src/cmd/vendor/golang.org/x/tools/go/analysis/unitchecker/unitchecker.go
src/cmd/vendor/golang.org/x/tools/internal/analysis/driverutil/fix.go
src/cmd/vendor/golang.org/x/tools/internal/refactor/inline/callee.go
src/cmd/vendor/modules.txt

index 9a1a38bccca6b5a1abf818ba08f21ec39b1147b4..90c3717334cd850b2d61af8536f0da1d2bd7b672 100644 (file)
@@ -11,7 +11,7 @@ require (
        golang.org/x/sys v0.38.1-0.20251125153526-08e54827f670
        golang.org/x/telemetry v0.0.0-20251128220624-abf20d0e57ec
        golang.org/x/term v0.37.0
-       golang.org/x/tools v0.39.1-0.20251130212600-1ad6f3d02713
+       golang.org/x/tools v0.39.1-0.20251205000126-062ef7b6ced2
 )
 
 require (
index 52d55d81732f7f2671f4f7aa3033b1c31cf69bf9..774820d54c19bde75d46c95cbe7c18564f3b9037 100644 (file)
@@ -22,7 +22,7 @@ golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
 golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
 golang.org/x/text v0.31.1-0.20251128220601-087616b6cde9 h1:IjQf87/qLz2y0SiCc0uY3DwajALXkAgP1Pxal0mmdrM=
 golang.org/x/text v0.31.1-0.20251128220601-087616b6cde9/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
-golang.org/x/tools v0.39.1-0.20251130212600-1ad6f3d02713 h1:i4GzAuZW4RuKXltwKyLYAfk7E1TSKQBxRAI7XKfLjSk=
-golang.org/x/tools v0.39.1-0.20251130212600-1ad6f3d02713/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
+golang.org/x/tools v0.39.1-0.20251205000126-062ef7b6ced2 h1:2Qqv605Nus9iUp3ErvEU/q92Q3HAzeROztzl9pzAno8=
+golang.org/x/tools v0.39.1-0.20251205000126-062ef7b6ced2/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
 rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef h1:mqLYrXCXYEZOop9/Dbo6RPX11539nwiCNBb1icVPmw8=
 rsc.io/markdown v0.0.0-20240306144322-0bf8f97ee8ef/go.mod h1:8xcPgWmwlZONN1D9bjxtHEjrUtSEa3fakVF8iaewYKQ=
index 7469002f56e43fbb0c4a04825febeb8a29097089..45aed7909c36348da0cdf378680dc8aef1271fac 100644 (file)
@@ -199,12 +199,19 @@ are often used to express optionality.
 
 omitzero: suggest replacing omitempty with omitzero for struct fields
 
-The omitzero analyzer identifies uses of the `omitempty` JSON struct tag on
-fields that are themselves structs. The `omitempty` tag has no effect on
-struct-typed fields. The analyzer offers two suggestions: either remove the
+The omitzero analyzer identifies uses of the `omitempty` JSON struct
+tag on fields that are themselves structs. For struct-typed fields,
+the `omitempty` tag has no effect on the behavior of json.Marshal and
+json.Unmarshal. The analyzer offers two suggestions: either remove the
 tag, or replace it with `omitzero` (added in Go 1.24), which correctly
 omits the field if the struct value is zero.
 
+However, some other serialization packages (notably kubebuilder, see
+https://book.kubebuilder.io/reference/markers.html) may have their own
+interpretation of the `json:",omitzero"` tag, so removing it may affect
+program behavior. For this reason, the omitzero modernizer will not
+make changes in any package that contains +kubebuilder annotations.
+
 Replacing `omitempty` with `omitzero` is a change in behavior. The
 original code would always encode the struct field, whereas the
 modified code will omit it if it is a zero-value.
index 4a05d64f42a002f0e958369d4117a7bebf993098..59ba9506511de5c53eb7b950c89a770b474faf14 100644 (file)
@@ -9,6 +9,8 @@ import (
        "go/types"
        "reflect"
        "strconv"
+       "strings"
+       "sync"
 
        "golang.org/x/tools/go/analysis"
        "golang.org/x/tools/go/analysis/passes/inspect"
@@ -25,82 +27,106 @@ var OmitZeroAnalyzer = &analysis.Analyzer{
        URL:      "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero",
 }
 
-func checkOmitEmptyField(pass *analysis.Pass, info *types.Info, curField *ast.Field) {
-       typ := info.TypeOf(curField.Type)
-       _, ok := typ.Underlying().(*types.Struct)
-       if !ok {
-               // Not a struct
-               return
-       }
-       tag := curField.Tag
-       if tag == nil {
-               // No tag to check
-               return
-       }
-       // The omitempty tag may be used by other packages besides json, but we should only modify its use with json
-       tagconv, _ := strconv.Unquote(tag.Value)
-       match := omitemptyRegex.FindStringSubmatchIndex(tagconv)
-       if match == nil {
-               // No omitempty in json tag
-               return
-       }
-       omitEmpty, err := astutil.RangeInStringLiteral(curField.Tag, match[2], match[3])
-       if err != nil {
-               return
-       }
-       var remove analysis.Range = omitEmpty
+// The omitzero pass searches for instances of "omitempty" in a json field tag on a
+// struct. Since "omitfilesUsingGoVersions not have any effect when applied to a struct field,
+// it suggests either deleting "omitempty" or replacing it with "omitzero", which
+// correctly excludes structs from a json encoding.
+func omitzero(pass *analysis.Pass) (any, error) {
+       // usesKubebuilder reports whether "+kubebuilder:" appears in
+       // any comment in the package, since it has its own
+       // interpretation of what omitzero means; see go.dev/issue/76649.
+       // It is computed once, on demand.
+       usesKubebuilder := sync.OnceValue[bool](func() bool {
+               for _, file := range pass.Files {
+                       for _, comment := range file.Comments {
+                               if strings.Contains(comment.Text(), "+kubebuilder:") {
+                                       return true
+                               }
+                       }
+               }
+               return false
+       })
+
+       checkField := func(field *ast.Field) {
+               typ := pass.TypesInfo.TypeOf(field.Type)
+               _, ok := typ.Underlying().(*types.Struct)
+               if !ok {
+                       // Not a struct
+                       return
+               }
+               tag := field.Tag
+               if tag == nil {
+                       // No tag to check
+                       return
+               }
+               // The omitempty tag may be used by other packages besides json, but we should only modify its use with json
+               tagconv, _ := strconv.Unquote(tag.Value)
+               match := omitemptyRegex.FindStringSubmatchIndex(tagconv)
+               if match == nil {
+                       // No omitempty in json tag
+                       return
+               }
+               omitEmpty, err := astutil.RangeInStringLiteral(field.Tag, match[2], match[3])
+               if err != nil {
+                       return
+               }
+               var remove analysis.Range = omitEmpty
 
-       jsonTag := reflect.StructTag(tagconv).Get("json")
-       if jsonTag == ",omitempty" {
-               // Remove the entire struct tag if json is the only package used
-               if match[1]-match[0] == len(tagconv) {
-                       remove = curField.Tag
-               } else {
-                       // Remove the json tag if omitempty is the only field
-                       remove, err = astutil.RangeInStringLiteral(curField.Tag, match[0], match[1])
-                       if err != nil {
-                               return
+               jsonTag := reflect.StructTag(tagconv).Get("json")
+               if jsonTag == ",omitempty" {
+                       // Remove the entire struct tag if json is the only package used
+                       if match[1]-match[0] == len(tagconv) {
+                               remove = field.Tag
+                       } else {
+                               // Remove the json tag if omitempty is the only field
+                               remove, err = astutil.RangeInStringLiteral(field.Tag, match[0], match[1])
+                               if err != nil {
+                                       return
+                               }
                        }
                }
-       }
-       pass.Report(analysis.Diagnostic{
-               Pos:     curField.Tag.Pos(),
-               End:     curField.Tag.End(),
-               Message: "Omitempty has no effect on nested struct fields",
-               SuggestedFixes: []analysis.SuggestedFix{
-                       {
-                               Message: "Remove redundant omitempty tag",
-                               TextEdits: []analysis.TextEdit{
-                                       {
-                                               Pos: remove.Pos(),
-                                               End: remove.End(),
+
+               // Don't offer a fix if the package seems to use kubebuilder,
+               // as it has its own intepretation of "omitzero" tags.
+               // https://book.kubebuilder.io/reference/markers.html
+               if usesKubebuilder() {
+                       return
+               }
+
+               pass.Report(analysis.Diagnostic{
+                       Pos:     field.Tag.Pos(),
+                       End:     field.Tag.End(),
+                       Message: "Omitempty has no effect on nested struct fields",
+                       SuggestedFixes: []analysis.SuggestedFix{
+                               {
+                                       Message: "Remove redundant omitempty tag",
+                                       TextEdits: []analysis.TextEdit{
+                                               {
+                                                       Pos: remove.Pos(),
+                                                       End: remove.End(),
+                                               },
                                        },
                                },
-                       },
-                       {
-                               Message: "Replace omitempty with omitzero (behavior change)",
-                               TextEdits: []analysis.TextEdit{
-                                       {
-                                               Pos:     omitEmpty.Pos(),
-                                               End:     omitEmpty.End(),
-                                               NewText: []byte(",omitzero"),
+                               {
+                                       Message: "Replace omitempty with omitzero (behavior change)",
+                                       TextEdits: []analysis.TextEdit{
+                                               {
+                                                       Pos:     omitEmpty.Pos(),
+                                                       End:     omitEmpty.End(),
+                                                       NewText: []byte(",omitzero"),
+                                               },
                                        },
                                },
-                       },
-               }})
-}
+                       }})
+       }
 
-// The omitzero pass searches for instances of "omitempty" in a json field tag on a
-// struct. Since "omitfilesUsingGoVersions not have any effect when applied to a struct field,
-// it suggests either deleting "omitempty" or replacing it with "omitzero", which
-// correctly excludes structs from a json encoding.
-func omitzero(pass *analysis.Pass) (any, error) {
        for curFile := range filesUsingGoVersion(pass, versions.Go1_24) {
                for curStruct := range curFile.Preorder((*ast.StructType)(nil)) {
                        for _, curField := range curStruct.Node().(*ast.StructType).Fields.List {
-                               checkOmitEmptyField(pass, pass.TypesInfo, curField)
+                               checkField(curField)
                        }
                }
        }
+
        return nil, nil
 }
index bc4ad677cd093c5792e748372c7fe489e0525dda..954997ad6f1c89ad1b7db5a5953e8054c99a90fc 100644 (file)
@@ -50,9 +50,14 @@ func init() {
 // 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
+//
+//     Binary expressions must be inequalities equivalent to
+//     "Index failed" (e.g. i < 0) or "Index succeeded" (i >= 0),
+//     or identities such as these (and their negations):
+//
+//     0 > i                 (flips left and right)
+//     i <= -1, -1 >= i      (replace strict inequality by non-strict)
+//     i == -1, -1 == i      (Index() guarantees i < 0 => i == -1)
 //
 //     Slice expressions:
 //     a: s[:i], s[0:i]
@@ -86,9 +91,9 @@ func init() {
 //     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 condition involving `i` is equivalent to i >= 0, then we replace it with
+// `if ok“.
+// If the condition is negated (e.g. equivalent to `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.
 //
@@ -178,16 +183,16 @@ func stringscut(pass *analysis.Pass) (any, error) {
                        // 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)
+                       negative, nonnegative, 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 {
+                       if negative == nil && nonnegative == 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
+                       isContains := (len(negative) > 0 || len(nonnegative) > 0) && len(beforeSlice) == 0 && len(afterSlice) == 0
 
                        scope := iObj.Parent()
                        var (
@@ -200,7 +205,7 @@ func stringscut(pass *analysis.Pass) (any, error) {
 
                        // If there will be no uses of ok, before, or after, use the
                        // blank identifier instead.
-                       if len(lessZero) == 0 && len(greaterNegOne) == 0 {
+                       if len(negative) == 0 && len(nonnegative) == 0 {
                                okVarName = "_"
                        }
                        if len(beforeSlice) == 0 {
@@ -226,8 +231,8 @@ func stringscut(pass *analysis.Pass) (any, error) {
                        replacedFunc := "Cut"
                        if isContains {
                                replacedFunc = "Contains"
-                               replace(lessZero, "!"+foundVarName)  // idx < 0   ->  !found
-                               replace(greaterNegOne, foundVarName) // idx > -1  ->   found
+                               replace(negative, "!"+foundVarName) // idx < 0   ->  !found
+                               replace(nonnegative, foundVarName)  // idx > -1  ->   found
 
                                // Replace the assignment with found, and replace the call to
                                // Index or IndexByte with a call to Contains.
@@ -244,8 +249,8 @@ func stringscut(pass *analysis.Pass) (any, error) {
                                        NewText: []byte("Contains"),
                                })
                        } else {
-                               replace(lessZero, "!"+okVarName)    // idx < 0   ->  !ok
-                               replace(greaterNegOne, okVarName)   // idx > -1  ->   ok
+                               replace(negative, "!"+okVarName)    // idx < 0   ->  !ok
+                               replace(nonnegative, okVarName)     // idx > -1  ->   ok
                                replace(beforeSlice, beforeVarName) // s[:idx]   ->   before
                                replace(afterSlice, afterVarName)   // s[idx+k:] ->   after
 
@@ -364,11 +369,11 @@ func indexArgValid(info *types.Info, index *typeindex.Index, expr ast.Expr, afte
 // 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)
+// 1. negative - a condition equivalent to i < 0
+// 2. nonnegative - a condition equivalent to i >= 0
 // 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) {
+func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr ast.Expr) (negative, nonnegative, beforeSlice, afterSlice []ast.Expr) {
        use := func(cur inspector.Cursor) bool {
                ek, _ := cur.ParentEdge()
                n := cur.Parent().Node()
@@ -377,13 +382,13 @@ func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr a
                        check := n.(*ast.BinaryExpr)
                        switch checkIdxComparison(info, check) {
                        case -1:
-                               lessZero = append(lessZero, check)
+                               negative = append(negative, check)
                                return true
                        case 1:
-                               greaterNegOne = append(greaterNegOne, check)
+                               nonnegative = append(nonnegative, check)
                                return true
                        }
-                       // Check does not establish that i < 0 or i > -1.
+                       // Check is not equivalent to that i < 0 or i >= 0.
                        // 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
@@ -421,7 +426,7 @@ func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr a
                        return nil, nil, nil, nil
                }
        }
-       return lessZero, greaterNegOne, beforeSlice, afterSlice
+       return negative, nonnegative, beforeSlice, afterSlice
 }
 
 // hasModifyingUses reports whether any of the uses involve potential
@@ -451,52 +456,57 @@ func hasModifyingUses(info *types.Info, uses iter.Seq[inspector.Cursor], afterPo
        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.
+// checkIdxComparison reports whether the check is equivalent to i < 0 or its negation, or neither.
+// For equivalent to i >= 0, we only accept this exact BinaryExpr since
+// expressions like i > 0 or i >= 1 make a stronger statement about the value of i.
+// We avoid suggesting a fix in this case since it may result in an invalid
+// transformation (See golang/go#76687).
+// Since strings.Index returns exactly -1 if the substring is not found, we
+// don't need to handle expressions like i <= -3.
+// We return 0 if the expression does not match any of these options.
+// 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
+       // Ensure that the constant (if any) is on the right.
+       x, op, y := check.X, check.Op, check.Y
+       if info.Types[x].Value != nil {
+               x, op, y = y, flip(op), x
        }
-       // 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
+
+       yIsInt := func(k int64) bool {
+               return isIntLiteral(info, y, k)
        }
-       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
-               }
+       if op == token.LSS && yIsInt(0) || // i < 0
+               op == token.EQL && yIsInt(-1) || // i == -1
+               op == token.LEQ && yIsInt(-1) { // i <= -1
+               return -1 // check <=> i is negative
        }
-       return false
+
+       if op == token.GEQ && yIsInt(0) || // i >= 0
+               op == token.NEQ && yIsInt(-1) || // i != -1
+               op == token.GTR && yIsInt(-1) { // i > -1
+               return +1 // check <=> i is non-negative
+       }
+
+       return 0 // unknown
 }
 
-// isNonNegativeConst 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
-               }
+// flip changes the comparison token as if the operands were flipped.
+// It is defined only for == and the four inequalities.
+func flip(op token.Token) token.Token {
+       switch op {
+       case token.EQL:
+               return token.EQL // (same)
+       case token.GEQ:
+               return token.LEQ
+       case token.GTR:
+               return token.LSS
+       case token.LEQ:
+               return token.GEQ
+       case token.LSS:
+               return token.GTR
        }
-       return false
+       return op
 }
 
 // isBeforeSlice reports whether the SliceExpr is of the form s[:i] or s[0:i].
index 0180a341e5606b193323cae11bead085201068be..bc15ef8b968ebce2d33603c32a5b301c5936d3a3 100644 (file)
@@ -27,6 +27,7 @@ package unitchecker
 //   printf checker.
 
 import (
+       "archive/zip"
        "encoding/gob"
        "encoding/json"
        "flag"
@@ -74,6 +75,7 @@ type Config struct {
        VetxOnly                  bool              // run analysis only for facts, not diagnostics
        VetxOutput                string            // where to write file of fact information
        Stdout                    string            // write stdout (e.g. JSON, unified diff) to this file
+       FixArchive                string            // write fixed files to this zip archive, if non-empty
        SucceedOnTypecheckFailure bool              // obsolete awful hack; see #18395 and below
 }
 
@@ -153,7 +155,7 @@ func Run(configFile string, analyzers []*analysis.Analyzer) {
 
        // In VetxOnly mode, the analysis is run only for facts.
        if !cfg.VetxOnly {
-               code = processResults(fset, cfg.ID, results)
+               code = processResults(fset, cfg.ID, cfg.FixArchive, results)
        }
 
        os.Exit(code)
@@ -177,7 +179,7 @@ func readConfig(filename string) (*Config, error) {
        return cfg, nil
 }
 
-func processResults(fset *token.FileSet, id string, results []result) (exit int) {
+func processResults(fset *token.FileSet, id, fixArchive string, results []result) (exit int) {
        if analysisflags.Fix {
                // Don't print the diagnostics,
                // but apply all fixes from the root actions.
@@ -194,7 +196,40 @@ func processResults(fset *token.FileSet, id string, results []result) (exit int)
                                Diagnostics:  res.diagnostics,
                        }
                }
-               if err := driverutil.ApplyFixes(fixActions, analysisflags.Diff, false); err != nil {
+
+               // By default, fixes overwrite the original file.
+               // With the -diff flag, print the diffs to stdout.
+               // If "go fix" provides a fix archive, we write files
+               // into it so that mutations happen after the build.
+               write := func(filename string, content []byte) error {
+                       return os.WriteFile(filename, content, 0644)
+               }
+               if fixArchive != "" {
+                       f, err := os.Create(fixArchive)
+                       if err != nil {
+                               log.Fatalf("can't create -fix archive: %v", err)
+                       }
+                       zw := zip.NewWriter(f)
+                       zw.SetComment(id) // ignore error
+                       defer func() {
+                               if err := zw.Close(); err != nil {
+                                       log.Fatalf("closing -fix archive zip writer: %v", err)
+                               }
+                               if err := f.Close(); err != nil {
+                                       log.Fatalf("closing -fix archive file: %v", err)
+                               }
+                       }()
+                       write = func(filename string, content []byte) error {
+                               f, err := zw.Create(filename)
+                               if err != nil {
+                                       return err
+                               }
+                               _, err = f.Write(content)
+                               return err
+                       }
+               }
+
+               if err := driverutil.ApplyFixes(fixActions, write, analysisflags.Diff, false); err != nil {
                        // Fail when applying fixes failed.
                        log.Print(err)
                        exit = 1
index 37b09588a7a1b8d0aef66320d66ad86f65f6a009..7769b39beb8fbb94f997ae5269f4df4f89a9ef93 100644 (file)
@@ -93,11 +93,13 @@ type FixAction struct {
 //
 // If printDiff (from the -diff flag) is set, instead of updating the
 // files it display the final patch composed of all the cleanly merged
-// fixes.
+// fixes. (It is tempting to factor printDiff as just a variant of
+// writeFile that is provided the old and new content, but it's hard
+// to generate a good summary that way.)
 //
 // TODO(adonovan): handle file-system level aliases such as symbolic
 // links using robustio.FileID.
-func ApplyFixes(actions []FixAction, printDiff, verbose bool) error {
+func ApplyFixes(actions []FixAction, writeFile func(filename string, content []byte) error, printDiff, verbose bool) error {
        generated := make(map[*token.File]bool)
 
        // Select fixes to apply.
@@ -264,12 +266,11 @@ fixloop:
                        os.Stdout.WriteString(unified)
 
                } else {
-                       // write
+                       // write file
                        totalFiles++
-                       // TODO(adonovan): abstract the I/O.
-                       if err := os.WriteFile(file, final, 0644); err != nil {
+                       if err := writeFile(file, final); err != nil {
                                log.Println(err)
-                               continue
+                               continue // (causes ApplyFix to return an error)
                        }
                        filesUpdated++
                }
index ce5beb27244bb3940203d83fe584aa1f221f7a21..9a960bd293cb55990451f64d86c6f5dbc60de519 100644 (file)
@@ -725,8 +725,48 @@ func analyzeAssignment(info *types.Info, stack []ast.Node) (assignable, ifaceAss
                                        paramType := paramTypeAtIndex(sig, call, i)
                                        ifaceAssign := paramType == nil || types.IsInterface(paramType)
                                        affectsInference := false
-                                       if fn := typeutil.StaticCallee(info, call); fn != nil {
-                                               if sig2 := fn.Type().(*types.Signature); sig2.Recv() == nil {
+                                       switch callee := typeutil.Callee(info, call).(type) {
+                                       case *types.Builtin:
+                                               // Consider this litmus test:
+                                               //
+                                               //   func f(x int64) any { return max(x) }
+                                               //   func main() { fmt.Printf("%T", f(42)) }
+                                               //
+                                               // If we lose the implicit conversion from untyped int
+                                               // to int64, the type inferred for the max(x) call changes,
+                                               // resulting in a different dynamic behavior: it prints
+                                               // int, not int64.
+                                               //
+                                               // Inferred result type affected:
+                                               //    new
+                                               //    complex, real, imag
+                                               //    min, max
+                                               //
+                                               // Dynamic behavior change:
+                                               //    append         -- dynamic type of append([]any(nil), x)[0]
+                                               //    delete(m, x)   -- dynamic key type where m is map[any]unit
+                                               //    panic          -- dynamic type of panic value
+                                               //
+                                               // Unaffected:
+                                               //    recover
+                                               //    make
+                                               //    len, cap
+                                               //    clear
+                                               //    close
+                                               //    copy
+                                               //    print, println  -- only uses underlying types (?)
+                                               //
+                                               // The dynamic type cases are all covered by
+                                               // the ifaceAssign logic.
+                                               switch callee.Name() {
+                                               case "new", "complex", "real", "imag", "min", "max":
+                                                       affectsInference = true
+                                               }
+
+                                       case *types.Func:
+                                               // Only standalone (non-method) functions have type
+                                               // parameters affected by the call arguments.
+                                               if sig2 := callee.Signature(); sig2.Recv() == nil {
                                                        originParamType := paramTypeAtIndex(sig2, call, i)
                                                        affectsInference = originParamType == nil || new(typeparams.Free).Has(originParamType)
                                                }
index 8641f2dca3dde88aeb8d43f768b491f60d83f11d..7e48798071ff8fd55f77108c1e9148c793a23a99 100644 (file)
@@ -73,7 +73,7 @@ golang.org/x/text/internal/tag
 golang.org/x/text/language
 golang.org/x/text/transform
 golang.org/x/text/unicode/norm
-# golang.org/x/tools v0.39.1-0.20251130212600-1ad6f3d02713
+# golang.org/x/tools v0.39.1-0.20251205000126-062ef7b6ced2
 ## explicit; go 1.24.0
 golang.org/x/tools/cmd/bisect
 golang.org/x/tools/cover