Skip to content

Commit ec8439e

Browse files
authored
feature: add configuration notation in struct-tag rule to omit checking a tag (#1515)
1 parent 5736df3 commit ec8439e

File tree

4 files changed

+140
-4
lines changed

4 files changed

+140
-4
lines changed

RULES_DESCRIPTIONS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,15 @@ To accept the `inline` option in JSON tags (and `outline` and `gnu` in BSON tags
13181318
arguments = ["json,inline", "bson,outline,gnu"]
13191319
```
13201320

1321+
To prevent a tag from being checked, simply add a `!` before its name.
1322+
For example, to instruct the rule not to check `validate` tags
1323+
(and accept `outline` and `gnu` in BSON tags) you can provide the following configuration
1324+
1325+
```toml
1326+
[rule.struct-tag]
1327+
arguments = ["!validate", "bson,outline,gnu"]
1328+
```
1329+
13211330
## superfluous-else
13221331

13231332
_Description_: To improve the readability of code, it is recommended to reduce the indentation as much as possible.

rule/struct_tag.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
// StructTagRule lints struct tags.
1717
type StructTagRule struct {
1818
userDefined map[tagKey][]string // map: key -> []option
19+
omittedTags map[tagKey]struct{} // set of tags that must not be analyzed
1920
}
2021

2122
type tagKey string
@@ -107,17 +108,23 @@ func (r *StructTagRule) Configure(arguments lint.Arguments) error {
107108
return err
108109
}
109110

110-
r.userDefined = make(map[tagKey][]string, len(arguments))
111+
r.userDefined = map[tagKey][]string{}
112+
r.omittedTags = map[tagKey]struct{}{}
111113
for _, arg := range arguments {
112114
item, ok := arg.(string)
113115
if !ok {
114116
return fmt.Errorf("invalid argument to the %s rule. Expecting a string, got %v (of type %T)", r.Name(), arg, arg)
115117
}
118+
116119
parts := strings.Split(item, ",")
117-
if len(parts) < 2 {
118-
return fmt.Errorf("invalid argument to the %s rule. Expecting a string of the form key[,option]+, got %s", r.Name(), item)
120+
keyStr := strings.TrimSpace(parts[0])
121+
keyStr, isOmitted := strings.CutPrefix(keyStr, "!")
122+
key := tagKey(keyStr)
123+
if isOmitted {
124+
r.omittedTags[key] = struct{}{}
125+
continue
119126
}
120-
key := tagKey(strings.TrimSpace(parts[0]))
127+
121128
for i := 1; i < len(parts); i++ {
122129
option := strings.TrimSpace(parts[i])
123130
r.userDefined[key] = append(r.userDefined[key], option)
@@ -137,6 +144,7 @@ func (r *StructTagRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure
137144
w := lintStructTagRule{
138145
onFailure: onFailure,
139146
userDefined: r.userDefined,
147+
omittedTags: r.omittedTags,
140148
isAtLeastGo124: file.Pkg.IsAtLeastGoVersion(lint.Go124),
141149
tagCheckers: tagCheckers,
142150
}
@@ -154,6 +162,7 @@ func (*StructTagRule) Name() string {
154162
type lintStructTagRule struct {
155163
onFailure func(lint.Failure)
156164
userDefined map[tagKey][]string // map: key -> []option
165+
omittedTags map[tagKey]struct{}
157166
isAtLeastGo124 bool
158167
tagCheckers map[tagKey]tagChecker
159168
}
@@ -193,6 +202,11 @@ func (w lintStructTagRule) checkTaggedField(checkCtx *checkContext, field *ast.F
193202

194203
analyzedTags := map[tagKey]struct{}{}
195204
for _, tag := range tags.Tags() {
205+
_, mustOmit := w.omittedTags[tagKey(tag.Key)]
206+
if mustOmit {
207+
continue
208+
}
209+
196210
if msg, ok := w.checkTagNameIfNeed(checkCtx, tag); !ok {
197211
w.addFailureWithTagKey(field.Tag, msg, tag.Key)
198212
}

test/struct_tag_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@ func TestStructTagWithUserOptions(t *testing.T) {
2727
})
2828
}
2929

30+
func TestStructTagWithOmittedTags(t *testing.T) {
31+
testRule(t, "struct_tag_user_options_omit", &rule.StructTagRule{}, &lint.RuleConfig{
32+
Arguments: []any{
33+
"!validate",
34+
"!toml",
35+
"json,inline,outline",
36+
"bson,gnu",
37+
"url,myURLOption",
38+
"datastore,myDatastoreOption",
39+
"mapstructure,myMapstructureOption",
40+
"spanner,mySpannerOption",
41+
},
42+
})
43+
}
44+
3045
func TestStructTagAfterGo1_24(t *testing.T) {
3146
testRule(t, "go1.24/struct_tag", &rule.StructTagRule{})
3247
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package fixtures
2+
3+
import "time"
4+
5+
type RangeAllocation struct {
6+
metav1.TypeMeta `json:",inline"`
7+
metav1.ObjectMeta `json:"metadata,omitempty"`
8+
Range string `json:"range,outline"`
9+
Data []byte `json:"data,flow"` // MATCH /unknown option "flow" in json tag/
10+
}
11+
12+
type RangeAllocation struct {
13+
metav1.TypeMeta `bson:",minsize,gnu"`
14+
metav1.ObjectMeta `bson:"metadata,omitempty"`
15+
Range string `bson:"range,flow"` // MATCH /unknown option "flow" in bson tag/
16+
Data []byte `bson:"data,inline"`
17+
}
18+
19+
type RequestQueryOptions struct {
20+
Properties []string `url:"properties,commmma,omitempty"` // MATCH /unknown option "commmma" in url tag/
21+
CustomProperties []string `url:"-"`
22+
Archived bool `url:"archived,myURLOption"`
23+
}
24+
25+
type Fields struct {
26+
Field string `datastore:",noindex,flatten,omitempty,myDatastoreOption"`
27+
OtherField string `datastore:",unknownOption"` // MATCH /unknown option "unknownOption" in datastore tag/
28+
}
29+
30+
type MapStruct struct {
31+
Field1 string `mapstructure:",squash,reminder,omitempty,myMapstructureOption"`
32+
OtherField string `mapstructure:",unknownOption"` // MATCH /unknown option "unknownOption" in mapstructure tag/
33+
}
34+
35+
type ValidateUser struct {
36+
Username string `validate:"required,min=3,max=32"`
37+
Email string `validate:"required,email"`
38+
Password string `validate:"required,min=8,max=32"`
39+
Biography string `validate:"min=0,max=1000"`
40+
DisplayName string `validate:"displayName,min=3,max=32"`
41+
Complex string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,required"`
42+
BadComplex string `validate:"gt=0,keys,eq=1|eq=2,endkeys,required"`
43+
BadComplex2 string `validate:"gt=0,dive,eq=1|eq=2,endkeys,required"`
44+
BadComplex3 string `validate:"gt=0,dive,keys,eq=1|eq=2,endkeys,endkeys,required"`
45+
}
46+
47+
type TomlUser struct {
48+
Username string `toml:"username,omitempty"`
49+
Location string `toml:"location,unknown"`
50+
}
51+
52+
type SpannerUserOptions struct {
53+
ID int `spanner:"user_id,mySpannerOption"`
54+
A int `spanner:"-,mySpannerOption"` // MATCH /useless option mySpannerOption for ignored field in spanner tag/
55+
Name string `spanner:"full_name,unknownOption"` // MATCH /unknown option "unknownOption" in spanner tag/
56+
}
57+
58+
type uselessOptions struct {
59+
A int `bson:"-,"`
60+
B int `bson:"-,omitempty"` // MATCH /useless option omitempty for ignored field in bson tag/
61+
C int `bson:"-,omitempty,omitempty"` // MATCH /useless options omitempty,omitempty for ignored field in bson tag/
62+
D int `datastore:"-,"`
63+
E int `datastore:"-,omitempty"` // MATCH /useless option omitempty for ignored field in datastore tag/
64+
F int `datastore:"-,omitempty,omitempty"` // MATCH /useless options omitempty,omitempty for ignored field in datastore tag/
65+
G int `json:"-,"`
66+
H int `json:"-,omitempty"` // MATCH /useless option omitempty for ignored field in json tag/
67+
I int `json:"-,omitempty,omitempty"` // MATCH /useless options omitempty,omitempty for ignored field in json tag/
68+
J int `mapstructure:"-,"`
69+
K int `mapstructure:"-,squash"` // MATCH /useless option squash for ignored field in mapstructure tag/
70+
L int `mapstructure:"-,omitempty,omitempty"` // MATCH /useless options omitempty,omitempty for ignored field in mapstructure tag/
71+
M int `properties:"-,"`
72+
N int `properties:"-,default=15"` // MATCH /useless option default=15 for ignored field in properties tag/
73+
O time.Time `properties:"-,layout=2006-01-02,default=2006-01-02"` // MATCH /useless options layout=2006-01-02,default=2006-01-02 for ignored field in properties tag/
74+
P int `spanner:"-,"`
75+
Q int `spanner:"-,mySpannerOption"` // MATCH /useless option mySpannerOption for ignored field in spanner tag/
76+
R int `spanner:"-,mySpannerOption,mySpannerOption"` // MATCH /useless options mySpannerOption,mySpannerOption for ignored field in spanner tag/
77+
S int `toml:"-,"`
78+
T int `toml:"-,omitempty"`
79+
U int `toml:"-,omitempty,omitempty"`
80+
V int `url:"-,"`
81+
W int `url:"-,omitempty"` // MATCH /useless option omitempty for ignored field in url tag/
82+
X int `url:"-,omitempty,omitempty"` // MATCH /useless options omitempty,omitempty for ignored field in url tag/
83+
Y int `xml:"-,"`
84+
Z int `xml:"-,omitempty"` // MATCH /useless option omitempty for ignored field in xml tag/
85+
Aa int `xml:"-,omitempty,omitempty"` // MATCH /useless options omitempty,omitempty for ignored field in xml tag/
86+
Ba int `yaml:"-,"`
87+
Ca int `yaml:"-,omitempty"` // MATCH /useless option omitempty for ignored field in yaml tag/
88+
Da int `yaml:"-,omitempty,omitempty"` // MATCH /useless options omitempty,omitempty for ignored field in yaml tag/
89+
90+
// MATCH:59 /unknown option "" in bson tag/
91+
// MATCH:62 /unknown option "" in datastore tag/
92+
// MATCH:68 /unknown option "" in mapstructure tag/
93+
// MATCH:71 /unknown or malformed option "" in properties tag/
94+
// MATCH:74 /unknown option "" in spanner tag/
95+
// MATCH:80 /unknown option "" in url tag/
96+
// MATCH:83 /unknown option "" in xml tag/
97+
// MATCH:86 /unknown option "" in yaml tag/
98+
}

0 commit comments

Comments
 (0)