From a5b29fa812ef507240b8365bf2f381f136c8dfe8 Mon Sep 17 00:00:00 2001 From: yongruilin Date: Tue, 9 Sep 2025 22:23:03 +0000 Subject: [PATCH] refactor(validation-gen): move list-related validators to list.go --- .../cmd/validation-gen/validators/each.go | 323 +---------------- .../cmd/validation-gen/validators/item.go | 5 + .../cmd/validation-gen/validators/list.go | 338 ++++++++++++++++++ 3 files changed, 349 insertions(+), 317 deletions(-) create mode 100644 staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go index aad57134ea2..7283c11cfe6 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/each.go @@ -18,7 +18,6 @@ package validators import ( "fmt" - "strings" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/validation/field" @@ -28,10 +27,8 @@ import ( ) const ( - listTypeTagName = "k8s:listType" - ListMapKeyTagName = "k8s:listMapKey" - eachValTagName = "k8s:eachVal" - eachKeyTagName = "k8s:eachKey" + eachValTagName = "k8s:eachVal" + eachKeyTagName = "k8s:eachKey" ) // We keep the eachVal and eachKey validators around because the main @@ -41,24 +38,9 @@ var globalEachVal *eachValTagValidator var globalEachKey *eachKeyTagValidator func init() { - // Lists with list-map semantics are comprised of multiple tags, which need - // to share metadata about the list between them. - listMeta := map[string]*listMetadata{} // keyed by the field or type path - - // Accumulate list metadata via tags. - RegisterTagValidator(listTypeTagValidator{byPath: listMeta}) - RegisterTagValidator(listMapKeyTagValidator{byPath: listMeta}) - - // Finish work on the accumulated list metadata. - RegisterFieldValidator(listValidator{byPath: listMeta}) - RegisterTypeValidator(listValidator{byPath: listMeta}) - - // Processing item tags requires the list metadata. - RegisterTagValidator(&itemTagValidator{listByPath: listMeta}) - // Iterating values of lists and maps is a special tag, which can be called // directly by the code-generator logic. - globalEachVal = &eachValTagValidator{byPath: listMeta, validator: nil} + globalEachVal = &eachValTagValidator{byPath: globalListMeta, validator: nil} RegisterTagValidator(globalEachVal) // Iterating keys of maps is a special tag, which can be called directly by @@ -67,297 +49,6 @@ func init() { RegisterTagValidator(globalEachKey) } -// This applies to all tags in this file. -var listTagsValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal) - -// listMetadata collects information about a single list with map or set semantics. -type listMetadata struct { - // These will be checked for correctness elsewhere. - declaredAsAtomic bool - declaredAsSet bool - declaredAsMap bool - keyFields []string // iff declaredAsMap - keyNames []string // iff declaredAsMap -} - -// makeListMapMatchFunc generates a function that compares two list-map -// elements by their list-map key fields. -func (lm *listMetadata) makeListMapMatchFunc(t *types.Type) FunctionLiteral { - if !lm.declaredAsMap { - panic("makeListMapMatchFunc called on a non-map list") - } - // If no keys are defined, we will throw a good error later. - - matchFn := FunctionLiteral{ - Parameters: []ParamResult{{"a", t}, {"b", t}}, - Results: []ParamResult{{"", types.Bool}}, - } - buf := strings.Builder{} - buf.WriteString("return ") - // Note: this does not handle pointer fields, which are not - // supposed to be used as listMap keys. - for i, fld := range lm.keyFields { - if i > 0 { - buf.WriteString(" && ") - } - buf.WriteString(fmt.Sprintf("a.%s == b.%s", fld, fld)) - } - matchFn.Body = buf.String() - return matchFn -} - -type listTypeTagValidator struct { - byPath map[string]*listMetadata -} - -func (listTypeTagValidator) Init(Config) {} - -func (listTypeTagValidator) TagName() string { - return listTypeTagName -} - -func (listTypeTagValidator) ValidScopes() sets.Set[Scope] { - return listTagsValidScopes -} - -func (lttv listTypeTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { - // NOTE: pointers to lists are not supported, so we should never see a pointer here. - t := util.NativeType(context.Type) - if t.Kind != types.Slice && t.Kind != types.Array { - return Validations{}, fmt.Errorf("can only be used on list types (%s)", t.Kind) - } - - switch tag.Value { - case "atomic": - // We don't do much with atomic, but this ensures no conflicts between - // tags on typedefs and tags on fields which use those typedefs. - if lttv.byPath[context.Path.String()] == nil { - lttv.byPath[context.Path.String()] = &listMetadata{} - } - lm := lttv.byPath[context.Path.String()] - lm.declaredAsAtomic = true - case "set": - if lttv.byPath[context.Path.String()] == nil { - lttv.byPath[context.Path.String()] = &listMetadata{} - } - lm := lttv.byPath[context.Path.String()] - lm.declaredAsSet = true - // NOTE: we validate uniqueness in the listValidator. - case "map": - // NOTE: maps of pointers are not supported, so we should never see a pointer here. - if util.NativeType(t.Elem).Kind != types.Struct { - return Validations{}, fmt.Errorf("only lists of structs can be list-maps") - } - - // Save the fact that this list is a map. - if lttv.byPath[context.Path.String()] == nil { - lttv.byPath[context.Path.String()] = &listMetadata{} - } - lm := lttv.byPath[context.Path.String()] - lm.declaredAsMap = true - // NOTE: we validate uniqueness of the keys in the listValidator. - default: - return Validations{}, fmt.Errorf("unknown list type %q", tag.Value) - } - - // This tag doesn't generate any validations. It just accumulates - // information for other tags to use. - return Validations{}, nil -} - -func (lttv listTypeTagValidator) Docs() TagDoc { - doc := TagDoc{ - Tag: lttv.TagName(), - Scopes: lttv.ValidScopes().UnsortedList(), - Description: "Declares a list field's semantic type.", - Payloads: []TagPayloadDoc{{ - Description: "", - Docs: "atomic | map | set", - }}, - PayloadsType: codetags.ValueTypeString, - PayloadsRequired: true, - } - return doc -} - -type listMapKeyTagValidator struct { - byPath map[string]*listMetadata -} - -func (listMapKeyTagValidator) Init(Config) {} - -func (listMapKeyTagValidator) TagName() string { - return ListMapKeyTagName -} - -func (listMapKeyTagValidator) ValidScopes() sets.Set[Scope] { - return listTagsValidScopes -} - -func (lmktv listMapKeyTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { - // NOTE: pointers to lists are not supported, so we should never see a pointer here. - t := util.NativeType(context.Type) - if t.Kind != types.Slice && t.Kind != types.Array { - return Validations{}, fmt.Errorf("can only be used on list types (%s)", t.Kind) - } - // NOTE: lists of pointers are not supported, so we should never see a pointer here. - if util.NativeType(t.Elem).Kind != types.Struct { - return Validations{}, fmt.Errorf("only lists of structs can be list-maps") - } - - var fieldName string - if memb := util.GetMemberByJSON(util.NativeType(t.Elem), tag.Value); memb == nil { - return Validations{}, fmt.Errorf("no field for JSON name %q", tag.Value) - } else if k := util.NativeType(memb.Type).Kind; k != types.Builtin { - return Validations{}, fmt.Errorf("only primitive types can be list-map keys (%s)", k) - } else { - fieldName = memb.Name - } - - if lmktv.byPath[context.Path.String()] == nil { - lmktv.byPath[context.Path.String()] = &listMetadata{} - } - lm := lmktv.byPath[context.Path.String()] - lm.keyFields = append(lm.keyFields, fieldName) - lm.keyNames = append(lm.keyNames, tag.Value) - - // This tag doesn't generate any validations. It just accumulates - // information for other tags to use. - return Validations{}, nil -} - -func (lmktv listMapKeyTagValidator) Docs() TagDoc { - doc := TagDoc{ - Tag: lmktv.TagName(), - Scopes: lmktv.ValidScopes().UnsortedList(), - Description: "Declares a named sub-field of a list's value-type to be part of the list-map key.", - Payloads: []TagPayloadDoc{{ - Description: "", - Docs: "The name of the field.", - }}, - PayloadsType: codetags.ValueTypeString, - PayloadsRequired: true, - } - return doc -} - -type listValidator struct { - byPath map[string]*listMetadata -} - -func (listValidator) Init(_ Config) {} - -func (listValidator) Name() string { - return "listValidator" -} - -var ( - validateUnique = types.Name{Package: libValidationPkg, Name: "Unique"} -) - -func (lv listValidator) GetValidations(context Context) (Validations, error) { - nt := util.NativeType(context.Type) - if nt.Kind != types.Slice && nt.Kind != types.Array { - return Validations{}, nil - } - - // Look up the list metadata which is defined on this field or type. - lm := lv.byPath[context.Path.String()] - - // NOTE: We don't really support list-of-list or map-of-list, so this does - // not consider the case of ScopeListVal or ScopeMapVal. If we want to - // support those, we need to look at this and make sure the paths work the - // way we need. - if context.Scope == ScopeField { - // If this is a field, look up the list metadata for the type. - // TypeValidators happen before FieldValidators, so this is safe. - tm := lv.byPath[context.Type.String()] - if lm != nil && tm != nil { - return Validations{}, fmt.Errorf("found list metadata for both a field and its type: %s", context.Path) - } - // TODO(thockin): enable this once the whole codebase is converted or - // if we only run against fields which are opted-in. - // if lm == nil && tm == nil { - // return Validations{}, fmt.Errorf("found a list field without list metadata") - // } - } - - if lm == nil { - // If we don't have metadata for this field, we might have it for the - // field's type. - return Validations{}, nil - } - - // Do this after the above - if we only get one error, the one(s) above - // this are more important. - if err := lv.check(lm); err != nil { - return Validations{}, err - } - - result := Validations{} - - // Generate uniqueness checks for lists with higher-order semantics. - if lm.declaredAsSet { - // Only compare primitive values when possible. Slices and maps are not - // comparable, and structs might hold pointer fields, which are directly - // comparable but not what we need. - // - // NOTE: lists of pointers are not supported, so we should never see a pointer here. - matchArg := validateSemanticDeepEqual - if util.IsDirectComparable(util.NonPointer(util.NativeType(nt.Elem))) { - matchArg = validateDirectEqual - } - f := Function("listValidator", DefaultFlags, validateUnique, Identifier(matchArg)). - WithComment("listType=set requires unique values") - result.AddFunction(f) - } - // TODO: enable the following once we have a way to either opt-out from this validation - // or settle the decision on how to handle the ratcheting cases. - /* - if lm.declaredAsMap { - // TODO: There are some fields which are declared as maps which do not - // enforce uniqueness in manual validation. Those either need to not be - // maps or we need to allow types to opt-out from this validation. SSA - // is also not able to handle these well. - matchArg := lm.makeListMapMatchFunc(nt.Elem) - f := Function("listValidator", DefaultFlags, validateUnique, matchArg). - WithComment("listType=map requires unique keys") - result.AddFunction(f) - } - */ - - return result, nil -} - -// make sure a given listMetadata makes sense. -func (lv listValidator) check(lm *listMetadata) error { - // Check some fundamental constraints on list tags. - decls := []string{} - if lm.declaredAsAtomic { - decls = append(decls, "atomic") - } - if lm.declaredAsSet { - decls = append(decls, "set") - } - if lm.declaredAsMap { - decls = append(decls, "map") - } - if len(decls) > 1 { - return fmt.Errorf("listType cannot have multiple types (%s)", strings.Join(decls, ", ")) - } - if lm.declaredAsMap && len(lm.keyFields) == 0 { - return fmt.Errorf("found listType=map without listMapKey") - } - if len(lm.keyFields) > 0 && !lm.declaredAsMap { - return fmt.Errorf("found listMapKey without listType=map") - } - // Check for missing listType (after the other checks so the more specific errors take priority) - if len(decls) == 0 { - return fmt.Errorf("found list metadata without a listType") - } - return nil -} - type eachValTagValidator struct { byPath map[string]*listMetadata validator Validator @@ -380,11 +71,9 @@ func (eachValTagValidator) ValidScopes() sets.Set[Scope] { func (eachValTagValidator) LateTagValidator() {} var ( - validateEachSliceVal = types.Name{Package: libValidationPkg, Name: "EachSliceVal"} - validateEachMapVal = types.Name{Package: libValidationPkg, Name: "EachMapVal"} - validateSemanticDeepEqual = types.Name{Package: libValidationPkg, Name: "SemanticDeepEqual"} - validateDirectEqual = types.Name{Package: libValidationPkg, Name: "DirectEqual"} - validateDirectEqualPtr = types.Name{Package: libValidationPkg, Name: "DirectEqualPtr"} + validateEachSliceVal = types.Name{Package: libValidationPkg, Name: "EachSliceVal"} + validateEachMapVal = types.Name{Package: libValidationPkg, Name: "EachMapVal"} + validateDirectEqualPtr = types.Name{Package: libValidationPkg, Name: "DirectEqualPtr"} ) func (evtv eachValTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/item.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/item.go index 998b98f3c44..d68166a8f8e 100644 --- a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/item.go +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/item.go @@ -31,6 +31,11 @@ const ( itemTagName = "k8s:item" ) +func init() { + // Processing item tags requires the list metadata. + RegisterTagValidator(&itemTagValidator{listByPath: globalListMeta}) +} + type keyValuePair struct { key string value any diff --git a/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go new file mode 100644 index 00000000000..053bf3df72a --- /dev/null +++ b/staging/src/k8s.io/code-generator/cmd/validation-gen/validators/list.go @@ -0,0 +1,338 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package validators + +import ( + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/code-generator/cmd/validation-gen/util" + "k8s.io/gengo/v2/codetags" + "k8s.io/gengo/v2/types" +) + +const ( + listTypeTagName = "k8s:listType" + ListMapKeyTagName = "k8s:listMapKey" +) + +// globalListMeta is shared between list-related validators. +var globalListMeta = map[string]*listMetadata{} // keyed by the field or type path + +func init() { + // Accumulate list metadata via tags. + RegisterTagValidator(listTypeTagValidator{byPath: globalListMeta}) + RegisterTagValidator(listMapKeyTagValidator{byPath: globalListMeta}) + + // Finish work on the accumulated list metadata. + RegisterFieldValidator(listValidator{byPath: globalListMeta}) + RegisterTypeValidator(listValidator{byPath: globalListMeta}) +} + +// This applies to all tags in this file. +var listTagsValidScopes = sets.New(ScopeType, ScopeField, ScopeListVal, ScopeMapKey, ScopeMapVal) + +// listMetadata collects information about a single list with map or set semantics. +type listMetadata struct { + // These will be checked for correctness elsewhere. + declaredAsAtomic bool + declaredAsSet bool + declaredAsMap bool + keyFields []string // iff declaredAsMap + keyNames []string // iff declaredAsMap +} + +// makeListMapMatchFunc generates a function that compares two list-map +// elements by their list-map key fields. +func (lm *listMetadata) makeListMapMatchFunc(t *types.Type) FunctionLiteral { + if !lm.declaredAsMap { + panic("makeListMapMatchFunc called on a non-map list") + } + // If no keys are defined, we will throw a good error later. + + matchFn := FunctionLiteral{ + Parameters: []ParamResult{{"a", t}, {"b", t}}, + Results: []ParamResult{{"", types.Bool}}, + } + buf := strings.Builder{} + buf.WriteString("return ") + // Note: this does not handle pointer fields, which are not + // supposed to be used as listMap keys. + for i, fld := range lm.keyFields { + if i > 0 { + buf.WriteString(" && ") + } + buf.WriteString(fmt.Sprintf("a.%s == b.%s", fld, fld)) + } + matchFn.Body = buf.String() + return matchFn +} + +type listTypeTagValidator struct { + byPath map[string]*listMetadata +} + +func (listTypeTagValidator) Init(Config) {} + +func (listTypeTagValidator) TagName() string { + return listTypeTagName +} + +func (listTypeTagValidator) ValidScopes() sets.Set[Scope] { + return listTagsValidScopes +} + +func (lttv listTypeTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { + // NOTE: pointers to lists are not supported, so we should never see a pointer here. + t := util.NativeType(context.Type) + if t.Kind != types.Slice && t.Kind != types.Array { + return Validations{}, fmt.Errorf("can only be used on list types (%s)", t.Kind) + } + + switch tag.Value { + case "atomic": + // We don't do much with atomic, but this ensures no conflicts between + // tags on typedefs and tags on fields which use those typedefs. + if lttv.byPath[context.Path.String()] == nil { + lttv.byPath[context.Path.String()] = &listMetadata{} + } + lm := lttv.byPath[context.Path.String()] + lm.declaredAsAtomic = true + case "set": + if lttv.byPath[context.Path.String()] == nil { + lttv.byPath[context.Path.String()] = &listMetadata{} + } + lm := lttv.byPath[context.Path.String()] + lm.declaredAsSet = true + // NOTE: we validate uniqueness in the listValidator. + case "map": + // NOTE: maps of pointers are not supported, so we should never see a pointer here. + if util.NativeType(t.Elem).Kind != types.Struct { + return Validations{}, fmt.Errorf("only lists of structs can be list-maps") + } + + // Save the fact that this list is a map. + if lttv.byPath[context.Path.String()] == nil { + lttv.byPath[context.Path.String()] = &listMetadata{} + } + lm := lttv.byPath[context.Path.String()] + lm.declaredAsMap = true + // NOTE: we validate uniqueness of the keys in the listValidator. + default: + return Validations{}, fmt.Errorf("unknown list type %q", tag.Value) + } + + // This tag doesn't generate any validations. It just accumulates + // information for other tags to use. + return Validations{}, nil +} + +func (lttv listTypeTagValidator) Docs() TagDoc { + doc := TagDoc{ + Tag: lttv.TagName(), + Scopes: lttv.ValidScopes().UnsortedList(), + Description: "Declares a list field's semantic type.", + Payloads: []TagPayloadDoc{{ + Description: "", + Docs: "atomic | map | set", + }}, + PayloadsType: codetags.ValueTypeString, + PayloadsRequired: true, + } + return doc +} + +type listMapKeyTagValidator struct { + byPath map[string]*listMetadata +} + +func (listMapKeyTagValidator) Init(Config) {} + +func (listMapKeyTagValidator) TagName() string { + return ListMapKeyTagName +} + +func (listMapKeyTagValidator) ValidScopes() sets.Set[Scope] { + return listTagsValidScopes +} + +func (lmktv listMapKeyTagValidator) GetValidations(context Context, tag codetags.Tag) (Validations, error) { + // NOTE: pointers to lists are not supported, so we should never see a pointer here. + t := util.NativeType(context.Type) + if t.Kind != types.Slice && t.Kind != types.Array { + return Validations{}, fmt.Errorf("can only be used on list types (%s)", t.Kind) + } + // NOTE: lists of pointers are not supported, so we should never see a pointer here. + if util.NativeType(t.Elem).Kind != types.Struct { + return Validations{}, fmt.Errorf("only lists of structs can be list-maps") + } + + var fieldName string + if memb := util.GetMemberByJSON(util.NativeType(t.Elem), tag.Value); memb == nil { + return Validations{}, fmt.Errorf("no field for JSON name %q", tag.Value) + } else if k := util.NativeType(memb.Type).Kind; k != types.Builtin { + return Validations{}, fmt.Errorf("only primitive types can be list-map keys (%s)", k) + } else { + fieldName = memb.Name + } + + if lmktv.byPath[context.Path.String()] == nil { + lmktv.byPath[context.Path.String()] = &listMetadata{} + } + lm := lmktv.byPath[context.Path.String()] + lm.keyFields = append(lm.keyFields, fieldName) + lm.keyNames = append(lm.keyNames, tag.Value) + + // This tag doesn't generate any validations. It just accumulates + // information for other tags to use. + return Validations{}, nil +} + +func (lmktv listMapKeyTagValidator) Docs() TagDoc { + doc := TagDoc{ + Tag: lmktv.TagName(), + Scopes: lmktv.ValidScopes().UnsortedList(), + Description: "Declares a named sub-field of a list's value-type to be part of the list-map key.", + Payloads: []TagPayloadDoc{{ + Description: "", + Docs: "The name of the field.", + }}, + PayloadsType: codetags.ValueTypeString, + PayloadsRequired: true, + } + return doc +} + +type listValidator struct { + byPath map[string]*listMetadata +} + +func (listValidator) Init(_ Config) {} + +func (listValidator) Name() string { + return "listValidator" +} + +var ( + validateUnique = types.Name{Package: libValidationPkg, Name: "Unique"} + validateSemanticDeepEqual = types.Name{Package: libValidationPkg, Name: "SemanticDeepEqual"} + validateDirectEqual = types.Name{Package: libValidationPkg, Name: "DirectEqual"} +) + +func (lv listValidator) GetValidations(context Context) (Validations, error) { + nt := util.NativeType(context.Type) + if nt.Kind != types.Slice && nt.Kind != types.Array { + return Validations{}, nil + } + + // Look up the list metadata which is defined on this field or type. + lm := lv.byPath[context.Path.String()] + + // NOTE: We don't really support list-of-list or map-of-list, so this does + // not consider the case of ScopeListVal or ScopeMapVal. If we want to + // support those, we need to look at this and make sure the paths work the + // way we need. + if context.Scope == ScopeField { + // If this is a field, look up the list metadata for the type. + // TypeValidators happen before FieldValidators, so this is safe. + tm := lv.byPath[context.Type.String()] + if lm != nil && tm != nil { + return Validations{}, fmt.Errorf("found list metadata for both a field and its type: %s", context.Path) + } + // TODO(thockin): enable this once the whole codebase is converted or + // if we only run against fields which are opted-in. + // if lm == nil && tm == nil { + // return Validations{}, fmt.Errorf("found a list field without list metadata") + // } + } + + if lm == nil { + // If we don't have metadata for this field, we might have it for the + // field's type. + return Validations{}, nil + } + + // Do this after the above - if we only get one error, the one(s) above + // this are more important. + if err := lv.check(lm); err != nil { + return Validations{}, err + } + + result := Validations{} + + // Generate uniqueness checks for lists with higher-order semantics. + if lm.declaredAsSet { + // Only compare primitive values when possible. Slices and maps are not + // comparable, and structs might hold pointer fields, which are directly + // comparable but not what we need. + // + // NOTE: lists of pointers are not supported, so we should never see a pointer here. + matchArg := validateSemanticDeepEqual + if util.IsDirectComparable(util.NonPointer(util.NativeType(nt.Elem))) { + matchArg = validateDirectEqual + } + f := Function("listValidator", DefaultFlags, validateUnique, Identifier(matchArg)). + WithComment("listType=set requires unique values") + result.AddFunction(f) + } + // TODO: enable the following once we have a way to either opt-out from this validation + // or settle the decision on how to handle the ratcheting cases. + /* + if lm.declaredAsMap { + // TODO: There are some fields which are declared as maps which do not + // enforce uniqueness in manual validation. Those either need to not be + // maps or we need to allow types to opt-out from this validation. SSA + // is also not able to handle these well. + matchArg := lm.makeListMapMatchFunc(nt.Elem) + f := Function("listValidator", DefaultFlags, validateUnique, matchArg). + WithComment("listType=map requires unique keys") + result.AddFunction(f) + } + */ + + return result, nil +} + +// make sure a given listMetadata makes sense. +func (lv listValidator) check(lm *listMetadata) error { + // Check some fundamental constraints on list tags. + decls := []string{} + if lm.declaredAsAtomic { + decls = append(decls, "atomic") + } + if lm.declaredAsSet { + decls = append(decls, "set") + } + if lm.declaredAsMap { + decls = append(decls, "map") + } + if len(decls) > 1 { + return fmt.Errorf("listType cannot have multiple types (%s)", strings.Join(decls, ", ")) + } + if lm.declaredAsMap && len(lm.keyFields) == 0 { + return fmt.Errorf("found listType=map without listMapKey") + } + if len(lm.keyFields) > 0 && !lm.declaredAsMap { + return fmt.Errorf("found listMapKey without listType=map") + } + // Check for missing listType (after the other checks so the more specific errors take priority) + if len(decls) == 0 { + return fmt.Errorf("found list metadata without a listType") + } + return nil +}