| // 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" |
| |
| "golang.org/x/tools/go/analysis" |
| "golang.org/x/tools/go/analysis/passes/inspect" |
| "golang.org/x/tools/go/ast/inspector" |
| "golang.org/x/tools/internal/astutil" |
| ) |
| |
| 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 |
| } |
| omitEmptyPos, omitEmptyEnd, err := astutil.RangeInStringLiteral(curField.Tag, match[2], match[3]) |
| if err != nil { |
| return |
| } |
| removePos, removeEnd := omitEmptyPos, omitEmptyEnd |
| |
| 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) { |
| removePos = curField.Tag.Pos() |
| removeEnd = curField.Tag.End() |
| } else { |
| // Remove the json tag if omitempty is the only field |
| removePos, err = astutil.PosInStringLiteral(curField.Tag, match[0]) |
| if err != nil { |
| return |
| } |
| removeEnd, err = astutil.PosInStringLiteral(curField.Tag, match[1]) |
| if err != nil { |
| return |
| } |
| } |
| } |
| pass.Report(analysis.Diagnostic{ |
| Pos: curField.Tag.Pos(), |
| End: curField.Tag.End(), |
| Category: "omitzero", |
| Message: "Omitempty has no effect on nested struct fields", |
| SuggestedFixes: []analysis.SuggestedFix{ |
| { |
| Message: "Remove redundant omitempty tag", |
| TextEdits: []analysis.TextEdit{ |
| { |
| Pos: removePos, |
| End: removeEnd, |
| }, |
| }, |
| }, |
| { |
| Message: "Replace omitempty with omitzero (behavior change)", |
| TextEdits: []analysis.TextEdit{ |
| { |
| Pos: omitEmptyPos, |
| End: omitEmptyEnd, |
| NewText: []byte(",omitzero"), |
| }, |
| }, |
| }, |
| }}) |
| } |
| |
| // 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, |
| // it suggests either deleting "omitempty" or replacing it with "omitzero", which |
| // correctly excludes structs from a json encoding. |
| func omitzero(pass *analysis.Pass) { |
| 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 _, curField := range curStruct.Node().(*ast.StructType).Fields.List { |
| checkOmitEmptyField(pass, info, curField) |
| } |
| } |
| } |
| } |