Skip to content

Commit 375334c

Browse files
committed
(feat): markers: add support for parsing declarative validation tags
Signed-off-by: Bryce Palmer <[email protected]>
1 parent 38ad5ff commit 375334c

File tree

4 files changed

+359
-76
lines changed

4 files changed

+359
-76
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/onsi/gomega v1.38.0
1010
golang.org/x/tools v0.37.0
1111
k8s.io/apimachinery v0.32.3
12+
k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b
1213
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
1314
sigs.k8s.io/yaml v1.4.0
1415
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,8 @@ honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
993993
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
994994
k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
995995
k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
996+
k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ=
997+
k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM=
996998
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
997999
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
9981000
mvdan.cc/gofumpt v0.9.1 h1:p5YT2NfFWsYyTieYgwcQ8aKV3xRvFH4uuN/zB2gBbMQ=

pkg/analysis/helpers/markers/analyzer.go

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ limitations under the License.
1616
package markers
1717

1818
import (
19+
"fmt"
1920
"go/ast"
2021
"go/token"
2122
"reflect"
@@ -26,6 +27,8 @@ import (
2627
"golang.org/x/tools/go/analysis/passes/inspect"
2728
"golang.org/x/tools/go/ast/inspector"
2829

30+
"k8s.io/gengo/v2/codetags"
31+
2932
kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
3033
)
3134

@@ -158,47 +161,60 @@ func run(pass *analysis.Pass) (any, error) {
158161
return results, nil
159162
}
160163

161-
func extractGenDeclMarkers(typ *ast.GenDecl, results *markers) {
164+
func extractGenDeclMarkers(typ *ast.GenDecl, results *markers) error {
162165
declMarkers := NewMarkerSet()
163166

164167
if typ.Doc != nil {
165168
for _, comment := range typ.Doc.List {
166-
if marker := extractMarker(comment); marker.Identifier != "" {
169+
marker, err := extractMarker(comment)
170+
if err != nil {
171+
return err
172+
}
173+
174+
if marker.Identifier != "" {
167175
declMarkers.Insert(marker)
168176
}
169177
}
170178
}
171179

172180
if len(typ.Specs) == 0 {
173-
return
181+
return nil
174182
}
175183

176184
tSpec, ok := typ.Specs[0].(*ast.TypeSpec)
177185
if !ok {
178-
return
186+
return nil
179187
}
180188

181189
results.insertTypeMarkers(tSpec, declMarkers)
182190

183191
if sTyp, ok := tSpec.Type.(*ast.StructType); ok {
184192
results.insertStructMarkers(sTyp, declMarkers)
185193
}
194+
195+
return nil
186196
}
187197

188-
func extractFieldMarkers(field *ast.Field, results *markers) {
198+
func extractFieldMarkers(field *ast.Field, results *markers) error {
189199
if field == nil || field.Doc == nil {
190-
return
200+
return nil
191201
}
192202

193203
fieldMarkers := NewMarkerSet()
194204

195205
for _, comment := range field.Doc.List {
196-
if marker := extractMarker(comment); marker.Identifier != "" {
206+
marker, err := extractMarker(comment)
207+
if err != nil {
208+
return err
209+
}
210+
211+
if marker.Identifier != "" {
197212
fieldMarkers.Insert(marker)
198213
}
199214
}
200215

201216
results.insertFieldMarkers(field, fieldMarkers)
217+
return nil
202218
}
203219

204220
// validMarkerStart validates that a marker starts with an alphabetic character
@@ -207,9 +223,9 @@ func extractFieldMarkers(field *ast.Field, results *markers) {
207223
// while supporting declarative validation tags with parentheses and nested markers.
208224
var validMarkerStart = regexp.MustCompile(`^[a-zA-Z]([a-zA-Z0-9:\(\)\"\" ,])+=?`)
209225

210-
func extractMarker(comment *ast.Comment) Marker {
226+
func extractMarker(comment *ast.Comment) (Marker, error) {
211227
if !strings.HasPrefix(comment.Text, "// +") {
212-
return Marker{}
228+
return Marker{}, nil
213229
}
214230

215231
markerContent := strings.TrimPrefix(comment.Text, "// +")
@@ -218,7 +234,16 @@ func extractMarker(comment *ast.Comment) Marker {
218234
// This excludes markdown tables (e.g., "// +-------") and other non-marker content,
219235
// while supporting declarative validation tags that may include parentheses and nested markers.
220236
if !validMarkerStart.MatchString(markerContent) {
221-
return Marker{}
237+
return Marker{}, nil
238+
}
239+
240+
if isDeclarativeValidationMarker(markerContent) {
241+
marker, err := extractDeclarativeValidationMarker(markerContent, comment)
242+
if err != nil {
243+
return Marker{}, fmt.Errorf("parsing declarative validation marker %q: %w", markerContent, err)
244+
}
245+
246+
return *marker, nil
222247
}
223248

224249
id, expressions := extractMarkerIDAndExpressions(DefaultRegistry(), markerContent)
@@ -229,7 +254,7 @@ func extractMarker(comment *ast.Comment) Marker {
229254
RawComment: comment.Text,
230255
Pos: comment.Pos(),
231256
End: comment.End(),
232-
}
257+
}, nil
233258
}
234259

235260
func extractMarkerIDAndExpressions(knownMarkers Registry, marker string) (string, map[string]string) {
@@ -240,6 +265,52 @@ func extractMarkerIDAndExpressions(knownMarkers Registry, marker string) (string
240265
return extractUnknownMarkerIDAndExpressions(marker)
241266
}
242267

268+
func isDeclarativeValidationMarker(marker string) bool {
269+
return strings.HasPrefix(marker, "k8s:")
270+
}
271+
272+
func extractDeclarativeValidationMarker(marker string, comment *ast.Comment) (*Marker, error) {
273+
tag, err := codetags.Parse(marker)
274+
if err != nil {
275+
return nil, fmt.Errorf("encountered an error parsing declarative validation marker %q: %v", marker, err)
276+
}
277+
278+
return markerForTag(&tag, comment)
279+
}
280+
281+
func markerForTag(tag *codetags.Tag, comment *ast.Comment) (*Marker, error) {
282+
out := &Marker{
283+
Identifier: tag.Name,
284+
Expressions: make(map[string]string),
285+
RawComment: comment.Text,
286+
Pos: comment.Pos(),
287+
End: comment.End(),
288+
}
289+
290+
for _, arg := range tag.Args {
291+
out.Expressions[arg.Name] = arg.Value
292+
}
293+
294+
switch tag.ValueType {
295+
case codetags.ValueTypeString, codetags.ValueTypeInt, codetags.ValueTypeBool, codetags.ValueTypeRaw:
296+
// all resolvable to an exact string value
297+
out.Expressions["payload"] = tag.Value
298+
case codetags.ValueTypeNone:
299+
// nothing
300+
case codetags.ValueTypeTag:
301+
// TODO: Better position evaluation
302+
marker, err := markerForTag(tag.ValueTag, comment)
303+
if err != nil {
304+
return nil, err
305+
}
306+
out.Marker = marker
307+
default:
308+
return nil, fmt.Errorf("unknown tag value type %v", tag.ValueType)
309+
}
310+
311+
return out, nil
312+
}
313+
243314
func extractKnownMarkerIDAndExpressions(id string, marker string) (string, map[string]string) {
244315
return id, extractExpressions(strings.TrimPrefix(marker, id))
245316
}
@@ -325,9 +396,17 @@ type Marker struct {
325396
// Identifier is the value of the marker once the leading comment, '+', and expressions are trimmed.
326397
Identifier string
327398

328-
// Expressions are the set of expressions that have been specified for the marker
399+
// Expressions are the set of expressions that have been specified for the marker.
400+
// Marker named and unnamed arguments are included in this map. If the marker's payload is
401+
// not another marker, the key "payload" will contain the marker payload.
329402
Expressions map[string]string
330403

404+
// Marker is a nested marker. This is present in cases
405+
// where we've parsed a declarative validation tag
406+
// and it has another declarative validation tag in it's payload.
407+
// If this is nil, the payload was _not_ another dv tag.
408+
Marker *Marker
409+
331410
// RawComment is the raw comment line, unfiltered.
332411
RawComment string
333412

0 commit comments

Comments
 (0)