@@ -26,19 +26,24 @@ import (
2626 "golang.org/x/tools/go/analysis/passes/inspect"
2727 "golang.org/x/tools/go/ast/inspector"
2828
29+ "k8s.io/gengo/v2/codetags"
30+
2931 kalerrors "sigs.k8s.io/kube-api-linter/pkg/analysis/errors"
3032)
3133
32- // UnnamedExpression is the expression key used
34+ // UnnamedArgument is the argument key used
3335// when parsing markers that don't have a specific
34- // named expression.
36+ // named argument.
37+ //
38+ // This is specific to declarative validation markers only.
39+ // Kubebuilder-style markers either have named arguments or a payload.
3540//
36- // An example of a marker without a named expression
37- // is "kubebuilder:default:=foo ".
41+ // An example of a Declarative Validation marker with an unnamed argument
42+ // is "k8s:ifEnabled(\"my-feature\")=... ".
3843//
39- // An example of a marker with named expressions
40- // is "kubebuilder:validation:XValidation:rule=' ...',message='...' ".
41- const UnnamedExpression = ""
44+ // An example of a Declarative Validation marker with named arguments
45+ // is "k8s:item(one: "value", two: "value")= ...".
46+ const UnnamedArgument = ""
4247
4348// Markers allows access to markers extracted from the
4449// go types.
@@ -163,7 +168,8 @@ func extractGenDeclMarkers(typ *ast.GenDecl, results *markers) {
163168
164169 if typ .Doc != nil {
165170 for _ , comment := range typ .Doc .List {
166- if marker := extractMarker (comment ); marker .Identifier != "" {
171+ marker := extractMarker (comment )
172+ if marker .Identifier != "" {
167173 declMarkers .Insert (marker )
168174 }
169175 }
@@ -193,7 +199,8 @@ func extractFieldMarkers(field *ast.Field, results *markers) {
193199 fieldMarkers := NewMarkerSet ()
194200
195201 for _ , comment := range field .Doc .List {
196- if marker := extractMarker (comment ); marker .Identifier != "" {
202+ marker := extractMarker (comment )
203+ if marker .Identifier != "" {
197204 fieldMarkers .Insert (marker )
198205 }
199206 }
@@ -221,34 +228,98 @@ func extractMarker(comment *ast.Comment) Marker {
221228 return Marker {}
222229 }
223230
224- id , expressions := extractMarkerIDAndExpressions (DefaultRegistry (), markerContent )
231+ if isDeclarativeValidationMarker (markerContent ) {
232+ marker := extractDeclarativeValidationMarker (markerContent , comment )
233+ if marker == nil {
234+ return Marker {}
235+ }
236+
237+ return * marker
238+ }
239+
240+ return extractKubebuilderMarker (markerContent , comment )
241+ }
242+
243+ func extractKubebuilderMarker (markerContent string , comment * ast.Comment ) Marker {
244+ id , arguments , payload := extractMarkerIDArgumentsAndPayload (DefaultRegistry (), markerContent )
225245
226246 return Marker {
227- Identifier : id ,
228- Expressions : expressions ,
229- RawComment : comment .Text ,
230- Pos : comment .Pos (),
231- End : comment .End (),
247+ Type : MarkerTypeKubebuilder ,
248+ Identifier : id ,
249+ Arguments : arguments ,
250+ Payload : payload ,
251+ RawComment : comment .Text ,
252+ Pos : comment .Pos (),
253+ End : comment .End (),
232254 }
233255}
234256
235- func extractMarkerIDAndExpressions (knownMarkers Registry , marker string ) (string , map [string ]string ) {
257+ func extractMarkerIDArgumentsAndPayload (knownMarkers Registry , marker string ) (string , map [string ]string , * Payload ) {
236258 if id , ok := knownMarkers .Match (marker ); ok {
237- return extractKnownMarkerIDAndExpressions (id , marker )
259+ return extractKnownMarkerIDArgumentsAndPayload (id , marker )
260+ }
261+
262+ return extractUnknownMarkerIDArgumentsAndPayload (marker )
263+ }
264+
265+ func isDeclarativeValidationMarker (marker string ) bool {
266+ return strings .HasPrefix (marker , "k8s:" )
267+ }
268+
269+ func extractDeclarativeValidationMarker (marker string , comment * ast.Comment ) * Marker {
270+ tag , err := codetags .Parse (marker )
271+ if err != nil {
272+ return nil
238273 }
239274
240- return extractUnknownMarkerIDAndExpressions ( marker )
275+ return markerForTag ( & tag , comment )
241276}
242277
243- func extractKnownMarkerIDAndExpressions (id string , marker string ) (string , map [string ]string ) {
244- return id , extractExpressions (strings .TrimPrefix (marker , id ))
278+ func markerForTag (tag * codetags.Tag , comment * ast.Comment ) * Marker {
279+ out := & Marker {
280+ Type : MarkerTypeDeclarativeValidation ,
281+ Identifier : tag .Name ,
282+ Arguments : make (map [string ]string ),
283+ RawComment : comment .Text ,
284+ Pos : comment .Pos (),
285+ End : comment .End (),
286+ }
287+
288+ for _ , arg := range tag .Args {
289+ out .Arguments [arg .Name ] = arg .Value
290+ }
291+
292+ switch tag .ValueType {
293+ case codetags .ValueTypeString , codetags .ValueTypeInt , codetags .ValueTypeBool , codetags .ValueTypeRaw :
294+ // all resolvable to an exact string value
295+ out .Payload = & Payload {
296+ Value : tag .Value ,
297+ }
298+ case codetags .ValueTypeNone :
299+ // nothing
300+ case codetags .ValueTypeTag :
301+ out .Payload = & Payload {
302+ Marker : markerForTag (tag .ValueTag , comment ),
303+ }
304+ default :
305+ return nil
306+ }
307+
308+ return out
309+ }
310+
311+ func extractKnownMarkerIDArgumentsAndPayload (id string , marker string ) (string , map [string ]string , * Payload ) {
312+ args , payload := extractArgumentsAndPayload (strings .TrimPrefix (marker , id ))
313+ return id , args , payload
245314}
246315
247316var expressionRegex = regexp .MustCompile ("\\ w*=(?:'[^']*'|\" (\\ \\ \" |[^\" ])*\" |[\\ w;\\ -\" ]+|`[^`]*`)" )
248317
249- func extractExpressions (expressionStr string ) map [string ]string {
318+ func extractArgumentsAndPayload (expressionStr string ) ( map [string ]string , * Payload ) {
250319 expressionsMap := map [string ]string {}
251320
321+ var payload * Payload
322+
252323 // Do some normalization work to ensure we can parse expressions in
253324 // a standard way. Trim any lingering colons (:) and replace all ':='s with '='
254325 expressionStr = strings .TrimPrefix (expressionStr , ":" )
@@ -261,13 +332,21 @@ func extractExpressions(expressionStr string) map[string]string {
261332 continue
262333 }
263334
335+ if key == UnnamedArgument {
336+ payload = & Payload {
337+ Value : value ,
338+ }
339+
340+ continue
341+ }
342+
264343 expressionsMap [key ] = value
265344 }
266345
267- return expressionsMap
346+ return expressionsMap , payload
268347}
269348
270- func extractUnknownMarkerIDAndExpressions (marker string ) (string , map [string ]string ) {
349+ func extractUnknownMarkerIDArgumentsAndPayload (marker string ) (string , map [string ]string , * Payload ) {
271350 // if there is only a single "=" split on the equal sign and trim any
272351 // dangling ":" characters.
273352 if strings .Count (marker , "=" ) == 1 {
@@ -277,10 +356,8 @@ func extractUnknownMarkerIDAndExpressions(marker string) (string, map[string]str
277356 identifier := strings .TrimSuffix (splits [0 ], ":" )
278357
279358 // If there is a single "=" sign that means the left side of the
280- // marker is the identifier and there is no real expression identifier.
281- expressions := map [string ]string {UnnamedExpression : splits [1 ]}
282-
283- return identifier , expressions
359+ // marker is the identifier and there is no real argument identifier.
360+ return identifier , make (map [string ]string ), & Payload {Value : splits [1 ]}
284361 }
285362
286363 // split on :
@@ -315,18 +392,65 @@ func extractUnknownMarkerIDAndExpressions(marker string) (string, map[string]str
315392 expressionString = strings .Join ([]string {expressionString , item }, "," )
316393 }
317394
318- expressions := extractExpressions (expressionString )
395+ expressions , payload := extractArgumentsAndPayload (expressionString )
319396
320- return identifier , expressions
397+ return identifier , expressions , payload
398+ }
399+
400+ // MarkerType is a representation of the style of marker.
401+ // Currently can be one of Kubebuilder or DeclarativeValidation.
402+ type MarkerType string
403+
404+ const (
405+ // MarkerTypeKubebuilder represents a Kubebuilder-style marker.
406+ MarkerTypeKubebuilder MarkerType = "Kubebuilder"
407+ // MarkerTypeDeclarativeValidation represents a Declarative Validation marker.
408+ MarkerTypeDeclarativeValidation MarkerType = "DeclarativeValidation"
409+ )
410+
411+ // Payload represents the payload of a marker.
412+ type Payload struct {
413+ // Value is the payload value of a marker represented as a string.
414+ // Value is set when the payload value of a marker is not another marker.
415+ Value string
416+
417+ // Marker is the marker in the payload value of another marker.
418+ // Marker is only set when the payload value of a marker is another marker.
419+ Marker * Marker
321420}
322421
323422// Marker represents a marker extracted from a comment on a declaration.
324423type Marker struct {
424+ // Type is the marker representation this marker was identified as.
425+ // Currently, the two marker format types are DeclarativeValidation and Kubebuilder.
426+ // Because the Kubebuilder style has been around the longest and is widely
427+ // used in projects that have CustomResourceDefinitions we default to Kubebuilder
428+ // style parsing unless we detect that the marker follows the declarative validation
429+ // format (i.e begins with +k8s:).
430+ Type MarkerType
431+
325432 // Identifier is the value of the marker once the leading comment, '+', and expressions are trimmed.
326433 Identifier string
327434
328- // Expressions are the set of expressions that have been specified for the marker
329- Expressions map [string ]string
435+ // Arguments are the set of named and unnamed arguments that have been specified for the marker.
436+ //
437+ // For Markers with Type == Kubebuilder, there will only ever be named arguments. The following examples highlight how arguments are extracted:
438+ // - `+kubebuilder:validation:Required` would result in *no* arguments.
439+ // - `+required` would result in *no* arguments.
440+ // - `+kubebuilder:validation:MinLength=10` would result in no arguments`.
441+ // - `+kubebuilder:validation:XValidation:rule="has(self)",message="should have self"` would result in 2 named arguments, `rule` and `message` with their respective values in string representation.
442+ //
443+ // For Markers with Type == DeclarativeValidation, arguments are extracted from the marker parameters. Arguments may be named or unnamed.
444+ // Some examples:
445+ // - `+k8s:forbidden` would result in *no* arguments.
446+ // - `+k8s:ifEnabled("my-feature")=...` would result in a single unnamed argument (represented by key `""`) with a value of `"my-feature"`.
447+ // - `+k8s:item(one: "value", two: "value")=...` would result in 2 named arguments, `one` and `two` with their respective values in string representation.
448+ Arguments map [string ]string
449+
450+ // Payload is the payload specified by the marker.
451+ // In general, it is what is present after the first `=` symbol
452+ // of a marker.
453+ Payload * Payload
330454
331455 // RawComment is the raw comment line, unfiltered.
332456 RawComment string
@@ -377,22 +501,33 @@ func (ms MarkerSet) Has(identifier string) bool {
377501}
378502
379503// HasWithValue returns whether marker(s) with the given identifier and
380- // expression values (i.e "kubebuilder:object:root:=true") is present
504+ // argument/payload values (i.e "kubebuilder:object:root:=true") is present
381505// in the MarkerSet.
382506func (ms MarkerSet ) HasWithValue (marker string ) bool {
383- return ms .HasWithExpressions (extractMarkerIDAndExpressions (DefaultRegistry (), marker ))
507+ if isDeclarativeValidationMarker (marker ) {
508+ marker := extractDeclarativeValidationMarker (marker , & ast.Comment {})
509+ if marker == nil {
510+ return false
511+ }
512+
513+ return ms .HasWithArgumentsAndPayload (marker .Identifier , marker .Arguments , marker .Payload )
514+ }
515+
516+ id , args , payload := extractMarkerIDArgumentsAndPayload (DefaultRegistry (), marker )
517+
518+ return ms .HasWithArgumentsAndPayload (id , args , payload )
384519}
385520
386- // HasWithExpressions returns whether marker(s) with the identifier and
387- // expressions are present in the MarkerSet.
388- func (ms MarkerSet ) HasWithExpressions (identifier string , expressions map [string ]string ) bool {
521+ // HasWithArgumentsAndPayload returns whether marker(s) with the
522+ // identifier, arguments, and payload are present in the MarkerSet.
523+ func (ms MarkerSet ) HasWithArgumentsAndPayload (identifier string , arguments map [string ]string , payload * Payload ) bool {
389524 markers , ok := ms [identifier ]
390525 if ! ok {
391526 return false
392527 }
393528
394529 for _ , marker := range markers {
395- if reflect .DeepEqual (marker .Expressions , expressions ) {
530+ if reflect .DeepEqual (marker .Arguments , arguments ) && reflect . DeepEqual ( marker . Payload , payload ) {
396531 return true
397532 }
398533 }
0 commit comments