Skip to content

Commit 67fe26e

Browse files
committed
PatchStrategy is not required on CRD conditions
1 parent cece271 commit 67fe26e

File tree

14 files changed

+870
-56
lines changed

14 files changed

+870
-56
lines changed

README.md

+11-2
Original file line numberDiff line numberDiff line change
@@ -151,25 +151,34 @@ Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge"
151151

152152
Conditions are idiomatically the first field within the status struct, and the linter will highlight when the Conditions are not the first field.
153153

154+
Protobuf tags and patch strategy are required for in-tree API types, but not for CRDs.
155+
When linting CRD based types, set the `useProtobuf` and `usePatchStrategy` config option to `Ignore` or `Forbid`.
156+
154157
### Configuration
155158

156159
```yaml
157160
lintersConfig:
158161
conditions:
159162
isFirstField: Warn | Ignore # The policy for the Conditions field being the first field. Defaults to `Warn`.
160-
useProtobuf: SuggestFix | Warn | Ignore # The policy for the protobuf tag on the Conditions field. Defaults to `SuggestFix`.
163+
useProtobuf: SuggestFix | Warn | Ignore | Forbid # The policy for the protobuf tag on the Conditions field. Defaults to `SuggestFix`.
164+
usePatchStrategy: SuggestFix | Warn | Ignore | Forbid # The policy for the patchStrategy tag on the Conditions field. Defaults to `SuggestFix`.
161165
```
162166
163167
### Fixes (via standalone binary only)
164168
165169
The `conditions` linter can automatically fix the tags on the `Conditions` field.
166170
When they do not match the expected format, the linter will suggest to update the tags to match the expected format.
167171

168-
For CRDs, protobuf tags are not expected. By setting the `useProtobuf` configuration to `Ignore`, the linter will not suggest to add the protobuf tag to the `Conditions` field tags.
172+
For CRDs, protobuf tags and patch strategy are not expected.
173+
By setting the `useProtobuf`/`usePatchStrategy` configuration to `Ignore`, the linter will not suggest to add the protobuf/patch strategy tag to the `Conditions` field tags.
174+
By setting the `useProtobuf`/`usePatchStrategy` configuration to `Forbid`, the linter will suggest to remove the protobuf/patch strategy tag from the `Conditions` field tags.
169175

170176
The linter will also suggest to add missing markers.
171177
If any of the 5 markers in the example above are missing, the linter will suggest to add them directly above the field.
172178

179+
When `usePatchStrategy` is set to `Ignore`, the linter will not suggest to add the `patchStrategy` and `patchMergeKey` tags to the `Conditions` field markers.
180+
When `usePatchStrategy` is set to `Forbid`, the linter will suggest to remove the `patchStrategy` and `patchMergeKey` tags from the `Conditions` field markers.
181+
173182
## CommentStart
174183

175184
The `commentstart` linter checks that all comments in the API types start with the serialized form of the type they are commenting on.

pkg/analysis/conditions/analyzer.go

+170-48
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"go/ast"
77
"go/token"
8+
"slices"
89
"strings"
910

1011
"github.com/JoelSpeed/kal/pkg/analysis/helpers/extractjsontags"
@@ -23,14 +24,17 @@ const (
2324
patchStrategyMarkerID = "patchStrategy"
2425
patchMergeKeyMarkerID = "patchMergeKey"
2526

26-
listTypeMap = "listType=map"
27-
listMapKeyType = "listMapKey=type"
28-
patchStrategy = "patchStrategy=merge"
29-
patchMergeKeyType = "patchMergeKey=type"
30-
optional = "optional"
31-
32-
expectedTagWithProtobufFmt = "`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,%d,rep,name=conditions\"`"
33-
expectedTagWithoutProtobuf = "`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`"
27+
listTypeMap = "listType=map"
28+
listMapKeyType = "listMapKey=type"
29+
patchStrategy = "patchStrategy"
30+
patchStrategyMerge = "patchStrategy=merge"
31+
patchMergeKey = "patchMergeKey"
32+
patchMergeKeyType = "patchMergeKey=type"
33+
optional = "optional"
34+
35+
expectedJSONTag = "json:\"conditions,omitempty\""
36+
expectedPatchTag = "patchStrategy:\"merge\" patchMergeKey:\"type\""
37+
expectedProtobufTag = "protobuf:\"bytes,%d,rep,name=conditions\""
3438
)
3539

3640
func init() {
@@ -49,17 +53,19 @@ var (
4953
)
5054

5155
type analyzer struct {
52-
isFirstField config.ConditionsFirstField
53-
useProtobuf config.ConditionsUseProtobuf
56+
isFirstField config.ConditionsFirstField
57+
useProtobuf config.ConditionsUseProtobuf
58+
usePatchStrategy config.ConditionsUsePatchStrategy
5459
}
5560

5661
// newAnalyzer creates a new analyzer.
5762
func newAnalyzer(cfg config.ConditionsConfig) *analysis.Analyzer {
5863
defaultConfig(&cfg)
5964

6065
a := &analyzer{
61-
isFirstField: cfg.IsFirstField,
62-
useProtobuf: cfg.UseProtobuf,
66+
isFirstField: cfg.IsFirstField,
67+
useProtobuf: cfg.UseProtobuf,
68+
usePatchStrategy: cfg.UsePatchStrategy,
6369
}
6470

6571
return &analysis.Analyzer{
@@ -117,16 +123,17 @@ func (a *analyzer) checkField(pass *analysis.Pass, index int, field *ast.Field,
117123
return
118124
}
119125

120-
checkFieldMarkers(pass, field, fieldMarkers)
126+
checkFieldMarkers(pass, field, fieldMarkers, a.usePatchStrategy)
121127
a.checkFieldTags(pass, index, field)
122128

123129
if a.isFirstField == config.ConditionsFirstFieldWarn && index != 0 {
124130
pass.Reportf(field.Pos(), "Conditions field must be the first field in the struct")
125131
}
126132
}
127133

128-
func checkFieldMarkers(pass *analysis.Pass, field *ast.Field, fieldMarkers markers.MarkerSet) {
134+
func checkFieldMarkers(pass *analysis.Pass, field *ast.Field, fieldMarkers markers.MarkerSet, usePatchStrategy config.ConditionsUsePatchStrategy) {
129135
missingMarkers := []string{}
136+
additionalMarkers := []markers.Marker{}
130137

131138
if !fieldMarkers.HasWithValue(listTypeMap) {
132139
missingMarkers = append(missingMarkers, listTypeMap)
@@ -136,37 +143,112 @@ func checkFieldMarkers(pass *analysis.Pass, field *ast.Field, fieldMarkers marke
136143
missingMarkers = append(missingMarkers, listMapKeyType)
137144
}
138145

139-
if !fieldMarkers.HasWithValue(patchStrategy) {
140-
missingMarkers = append(missingMarkers, patchStrategy)
141-
}
142-
143-
if !fieldMarkers.HasWithValue(patchMergeKeyType) {
144-
missingMarkers = append(missingMarkers, patchMergeKeyType)
145-
}
146+
patchMissingMarkers, patchAdditionalMarkers := checkPatchStrategyMarkers(fieldMarkers, usePatchStrategy)
147+
missingMarkers = append(missingMarkers, patchMissingMarkers...)
148+
additionalMarkers = append(additionalMarkers, patchAdditionalMarkers...)
146149

147150
if !fieldMarkers.Has(optional) {
148151
missingMarkers = append(missingMarkers, optional)
149152
}
150153

151154
if len(missingMarkers) != 0 {
152-
pass.Report(analysis.Diagnostic{
153-
Pos: field.Pos(),
154-
End: field.End(),
155-
Message: "Conditions field is missing the following markers: " + strings.Join(missingMarkers, ", "),
156-
SuggestedFixes: []analysis.SuggestedFix{
157-
{
158-
Message: "Add missing markers",
159-
TextEdits: []analysis.TextEdit{
160-
{
161-
Pos: field.Pos(),
162-
End: token.NoPos,
163-
NewText: getNewMarkers(missingMarkers),
164-
},
155+
reportMissingMarkers(pass, field, missingMarkers, usePatchStrategy)
156+
}
157+
158+
if len(additionalMarkers) != 0 {
159+
reportAdditionalMarkers(pass, field, additionalMarkers)
160+
}
161+
}
162+
163+
func checkPatchStrategyMarkers(fieldMarkers markers.MarkerSet, usePatchStrategy config.ConditionsUsePatchStrategy) ([]string, []markers.Marker) {
164+
missingMarkers := []string{}
165+
additionalMarkers := []markers.Marker{}
166+
167+
switch usePatchStrategy {
168+
case config.ConditionsUsePatchStrategySuggestFix, config.ConditionsUsePatchStrategyWarn:
169+
if !fieldMarkers.HasWithValue(patchStrategyMerge) {
170+
missingMarkers = append(missingMarkers, patchStrategyMerge)
171+
}
172+
173+
if !fieldMarkers.HasWithValue(patchMergeKeyType) {
174+
missingMarkers = append(missingMarkers, patchMergeKeyType)
175+
}
176+
case config.ConditionsUsePatchStrategyIgnore:
177+
// If it's there, we don't care.
178+
case config.ConditionsUsePatchStrategyForbid:
179+
if fieldMarkers.HasWithValue(patchStrategyMerge) {
180+
additionalMarkers = append(additionalMarkers, fieldMarkers[patchStrategy]...)
181+
}
182+
183+
if fieldMarkers.HasWithValue(patchMergeKeyType) {
184+
additionalMarkers = append(additionalMarkers, fieldMarkers[patchMergeKey]...)
185+
}
186+
default:
187+
panic("unexpected usePatchStrategy value")
188+
}
189+
190+
return missingMarkers, additionalMarkers
191+
}
192+
193+
func reportMissingMarkers(pass *analysis.Pass, field *ast.Field, missingMarkers []string, usePatchStrategy config.ConditionsUsePatchStrategy) {
194+
suggestedFixes := []analysis.SuggestedFix{}
195+
196+
// If patch strategy is warn, and the only markers in the list are patchStrategy and patchMergeKeyType, we don't need to suggest a fix.
197+
if usePatchStrategy != config.ConditionsUsePatchStrategyWarn || slices.ContainsFunc[[]string, string](missingMarkers, func(marker string) bool {
198+
switch marker {
199+
case patchStrategyMerge, patchMergeKeyType:
200+
return false
201+
default:
202+
return true
203+
}
204+
}) {
205+
suggestedFixes = []analysis.SuggestedFix{
206+
{
207+
Message: "Add missing markers",
208+
TextEdits: []analysis.TextEdit{
209+
{
210+
Pos: field.Pos(),
211+
End: token.NoPos,
212+
NewText: getNewMarkers(missingMarkers),
165213
},
166214
},
167215
},
216+
}
217+
}
218+
219+
pass.Report(analysis.Diagnostic{
220+
Pos: field.Pos(),
221+
End: field.End(),
222+
Message: "Conditions field is missing the following markers: " + strings.Join(missingMarkers, ", "),
223+
SuggestedFixes: suggestedFixes,
224+
})
225+
}
226+
227+
func reportAdditionalMarkers(pass *analysis.Pass, field *ast.Field, additionalMarkers []markers.Marker) {
228+
suggestedFixes := []analysis.SuggestedFix{}
229+
additionalMarkerValues := []string{}
230+
231+
for _, marker := range additionalMarkers {
232+
additionalMarkerValues = append(additionalMarkerValues, marker.String())
233+
234+
suggestedFixes = append(suggestedFixes, analysis.SuggestedFix{
235+
Message: "Remove additional marker",
236+
TextEdits: []analysis.TextEdit{
237+
{
238+
Pos: marker.Pos,
239+
End: marker.End + 1, // Add 1 to position to include the new line
240+
NewText: nil,
241+
},
242+
},
168243
})
169244
}
245+
246+
pass.Report(analysis.Diagnostic{
247+
Pos: field.Pos(),
248+
End: field.End(),
249+
Message: "Conditions field has the following additional markers: " + strings.Join(additionalMarkerValues, ", "),
250+
SuggestedFixes: suggestedFixes,
251+
})
170252
}
171253

172254
func getNewMarkers(missingMarkers []string) []byte {
@@ -181,7 +263,7 @@ func getNewMarkers(missingMarkers []string) []byte {
181263

182264
func (a *analyzer) checkFieldTags(pass *analysis.Pass, index int, field *ast.Field) {
183265
if field.Tag == nil {
184-
expectedTag := getExpectedTag(a.useProtobuf, a.isFirstField, index)
266+
expectedTag := getExpectedTag(a.usePatchStrategy, a.useProtobuf, a.isFirstField, index)
185267

186268
pass.Report(analysis.Diagnostic{
187269
Pos: field.Pos(),
@@ -204,9 +286,9 @@ func (a *analyzer) checkFieldTags(pass *analysis.Pass, index int, field *ast.Fie
204286
return
205287
}
206288

207-
asExpected, shouldFix := tagIsAsExpected(field.Tag.Value, a.useProtobuf, a.isFirstField, index)
289+
asExpected, shouldFix := tagIsAsExpected(field.Tag.Value, a.usePatchStrategy, a.useProtobuf, a.isFirstField, index)
208290
if !asExpected {
209-
expectedTag := getExpectedTag(a.useProtobuf, a.isFirstField, index)
291+
expectedTag := getExpectedTag(a.usePatchStrategy, a.useProtobuf, a.isFirstField, index)
210292

211293
if !shouldFix {
212294
pass.Reportf(field.Tag.ValuePos, "Conditions field has incorrect tags, should be: %s", expectedTag)
@@ -232,27 +314,63 @@ func (a *analyzer) checkFieldTags(pass *analysis.Pass, index int, field *ast.Fie
232314
}
233315
}
234316

235-
func getExpectedTag(useProtobuf config.ConditionsUseProtobuf, isFirstField config.ConditionsFirstField, index int) string {
317+
func getExpectedTag(usePatchStrategy config.ConditionsUsePatchStrategy, useProtobuf config.ConditionsUseProtobuf, isFirstField config.ConditionsFirstField, index int) string {
318+
expectedTag := fmt.Sprintf("`%s", expectedJSONTag)
319+
320+
if usePatchStrategy == config.ConditionsUsePatchStrategySuggestFix || usePatchStrategy == config.ConditionsUsePatchStrategyWarn {
321+
expectedTag += fmt.Sprintf(" %s", expectedPatchTag)
322+
}
323+
236324
if useProtobuf == config.ConditionsUseProtobufSuggestFix || useProtobuf == config.ConditionsUseProtobufWarn {
237-
i := 1
238-
if isFirstField == config.ConditionsFirstFieldIgnore {
239-
i = index + 1
240-
}
325+
expectedTag += fmt.Sprintf(" %s", getExpectedProtobufTag(isFirstField, index))
326+
}
241327

242-
return fmt.Sprintf(expectedTagWithProtobufFmt, i)
328+
expectedTag += "`"
329+
330+
return expectedTag
331+
}
332+
333+
func getExpectedProtobufTag(isFirstField config.ConditionsFirstField, index int) string {
334+
i := 1
335+
if isFirstField == config.ConditionsFirstFieldIgnore {
336+
i = index + 1
243337
}
244338

245-
return expectedTagWithoutProtobuf
339+
return fmt.Sprintf(expectedProtobufTag, i)
340+
}
341+
342+
func tagIsAsExpected(tag string, usePatchStrategy config.ConditionsUsePatchStrategy, useProtobuf config.ConditionsUseProtobuf, isFirstField config.ConditionsFirstField, index int) (bool, bool) {
343+
patchTagCorrect, patchShouldSuggestFix := patchStrategyTagIsAsExpected(tag, usePatchStrategy)
344+
protoTagCorrect, protoShouldSuggestFix := protobufTagIsAsExpected(tag, useProtobuf, isFirstField, index)
345+
346+
return patchTagCorrect && protoTagCorrect, patchShouldSuggestFix || protoShouldSuggestFix
347+
}
348+
349+
func patchStrategyTagIsAsExpected(tag string, usePatchStrategy config.ConditionsUsePatchStrategy) (bool, bool) {
350+
switch usePatchStrategy {
351+
case config.ConditionsUsePatchStrategySuggestFix:
352+
return strings.Contains(tag, expectedPatchTag), true
353+
case config.ConditionsUsePatchStrategyWarn:
354+
return strings.Contains(tag, expectedPatchTag), false
355+
case config.ConditionsUsePatchStrategyIgnore:
356+
return true, false
357+
case config.ConditionsUsePatchStrategyForbid:
358+
return !strings.Contains(tag, expectedPatchTag), true
359+
default:
360+
panic("unexpected usePatchStrategy value")
361+
}
246362
}
247363

248-
func tagIsAsExpected(tag string, useProtobuf config.ConditionsUseProtobuf, isFirstField config.ConditionsFirstField, index int) (bool, bool) {
364+
func protobufTagIsAsExpected(tag string, useProtobuf config.ConditionsUseProtobuf, isFirstField config.ConditionsFirstField, index int) (bool, bool) {
249365
switch useProtobuf {
250366
case config.ConditionsUseProtobufSuggestFix:
251-
return tag == getExpectedTag(config.ConditionsUseProtobufSuggestFix, isFirstField, index), true
367+
return strings.Contains(tag, getExpectedProtobufTag(isFirstField, index)), true
252368
case config.ConditionsUseProtobufWarn:
253-
return tag == getExpectedTag(config.ConditionsUseProtobufWarn, isFirstField, index), false
369+
return strings.Contains(tag, getExpectedProtobufTag(isFirstField, index)), false
254370
case config.ConditionsUseProtobufIgnore:
255-
return tag == getExpectedTag(config.ConditionsUseProtobufIgnore, isFirstField, index) || tag == getExpectedTag(config.ConditionsUseProtobufSuggestFix, isFirstField, index), true
371+
return true, false
372+
case config.ConditionsUseProtobufForbid:
373+
return !strings.Contains(tag, getExpectedProtobufTag(isFirstField, index)), true
256374
default:
257375
panic("unexpected useProtobuf value")
258376
}
@@ -309,4 +427,8 @@ func defaultConfig(cfg *config.ConditionsConfig) {
309427
if cfg.UseProtobuf == "" {
310428
cfg.UseProtobuf = config.ConditionsUseProtobufSuggestFix
311429
}
430+
431+
if cfg.UsePatchStrategy == "" {
432+
cfg.UsePatchStrategy = config.ConditionsUsePatchStrategySuggestFix
433+
}
312434
}

0 commit comments

Comments
 (0)