blob: 59ba9506511de5c53eb7b950c89a770b474faf14 [file] [log] [blame] [edit]
// 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 (
"go/ast"
"go/types"
"reflect"
"strconv"
"strings"
"sync"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/internal/analysis/analyzerutil"
"golang.org/x/tools/internal/astutil"
"golang.org/x/tools/internal/versions"
)
var OmitZeroAnalyzer = &analysis.Analyzer{
Name: "omitzero",
Doc: analyzerutil.MustExtractDoc(doc, "omitzero"),
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: omitzero,
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#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) {
// 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 = 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
}
}
}
// 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"),
},
},
},
}})
}
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 {
checkField(curField)
}
}
}
return nil, nil
}