Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 24 additions & 19 deletions callbacks.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,43 +132,48 @@ func getMethod(value reflect.Value, name string) reflect.Value {
//
// Returns a slice of bound methods that can be called directly.
func getMethods(value reflect.Value, name string) (methods []reflect.Value) {
if value.Kind() == reflect.Ptr {
walkEmbedded(value, func(v reflect.Value) {
if method := getMethod(v, name); method.IsValid() {
methods = append(methods, method)
}
})
return
}

// walkEmbedded calls visit on v and recursively on every exported field
// of v that is either a standard Go anonymous field or tagged `embed:""`.
// Pointer values are dereferenced before traversal; nil/invalid pointers
// are skipped. [Plugins] are descended into element-by-element, matching how
// [flattenedFields] treats them at build time.
func walkEmbedded(value reflect.Value, visit func(reflect.Value)) {
if value.Kind() == reflect.Pointer {
value = value.Elem()
}
if !value.IsValid() {
return
}

if method := getMethod(value, name); method.IsValid() {
methods = append(methods, method)
visit(value)
if value.Type() == reflect.TypeOf(Plugins{}) {
for i := 0; i < value.Len(); i++ {
walkEmbedded(value.Index(i).Elem(), visit)
}
return
}

if value.Kind() != reflect.Struct {
return
}
// If the current value is a struct, also consider embedded fields.
// Two kinds of embedded fields are considered if they're exported:
//
// - standard Go embedded fields
// - fields tagged with `embed:""`
t := value.Type()
for i := 0; i < value.NumField(); i++ {
fieldValue := value.Field(i)
field := t.Field(i)

if !field.IsExported() {
continue
}

// Consider a field embedded if it's actually embedded
// or if it's tagged with `embed:""`.
_, isEmbedded := field.Tag.Lookup("embed")
isEmbedded = isEmbedded || field.Anonymous
if isEmbedded {
methods = append(methods, getMethods(fieldValue, name)...)
if !isEmbedded && !field.Anonymous {
continue
}
walkEmbedded(value.Field(i), visit)
}
return
}

func callFunction(f reflect.Value, bindings bindings) error {
Expand Down
13 changes: 12 additions & 1 deletion context.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func (c *Context) Validate() error { //nolint: gocyclo
value = node.Target
desc = node.Path()
}
if validate := isValidatable(value); validate != nil {
for _, validate := range getValidators(value) {
if err := validate.Validate(c); err != nil {
if desc != "" {
return fmt.Errorf("%s: %w", desc, err)
Expand Down Expand Up @@ -1178,6 +1178,17 @@ func isValidatable(v reflect.Value) extendedValidatable {
return nil
}

// getValidators returns validators implemented by v and by any embedded fields,
// matching how hooks are discovered (see getMethods).
func getValidators(v reflect.Value) (validators []extendedValidatable) {
walkEmbedded(v, func(v reflect.Value) {
if validate := isValidatable(v); validate != nil {
validators = append(validators, validate)
}
})
return
}

func atLeastOneEnvSet(envs []string) bool {
for _, env := range envs {
if _, ok := os.LookupEnv(env); ok {
Expand Down
76 changes: 76 additions & 0 deletions kong_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1600,6 +1600,82 @@ func TestExtendedValidateFlag(t *testing.T) {
assert.EqualError(t, err, "--flag: flag error")
}

type embeddedValidate struct {
Flag string
}

func (v *embeddedValidate) Validate() error { return errors.New("embedded error") }

func TestValidateEmbed(t *testing.T) {
cli := struct {
Embedded embeddedValidate `embed:""`
}{}
p := mustNew(t, &cli)
_, err := p.Parse([]string{})
assert.EqualError(t, err, "embedded error")
}

func TestValidateEmbedOnCommand(t *testing.T) {
type cmd struct {
Embedded embeddedValidate `embed:""`
}
cli := struct {
Cmd cmd `cmd:""`
}{}
p := mustNew(t, &cli)
_, err := p.Parse([]string{"cmd"})
assert.EqualError(t, err, "cmd: embedded error")
}

type pluginValidate struct {
PluginFlag string
}

func (v *pluginValidate) Validate() error { return errors.New("plugin error") }

func TestValidatePlugin(t *testing.T) {
plugin := &pluginValidate{}
cli := struct {
Base string
kong.Plugins
}{
Plugins: kong.Plugins{plugin},
}
p := mustNew(t, &cli)
_, err := p.Parse([]string{})
assert.EqualError(t, err, "plugin error")
}

type nestedEmbedOuter struct {
Inner embeddedValidate `embed:""`
}

func TestValidateNestedEmbed(t *testing.T) {
cli := struct {
Outer nestedEmbedOuter `embed:""`
}{}
p := mustNew(t, &cli)
_, err := p.Parse([]string{})
assert.EqualError(t, err, "embedded error")
}

type extendedEmbeddedValidate struct {
Flag string
}

func (v *extendedEmbeddedValidate) Validate(kctx *kong.Context) error {
return errors.New("extended embedded error")
}

func TestExtendedValidateEmbed(t *testing.T) {
cli := struct {
Embedded extendedEmbeddedValidate `embed:""`
}{}
p := mustNew(t, &cli)
_, err := p.Parse([]string{})
assert.EqualError(t, err, "extended embedded error")
}

func TestPointers(t *testing.T) {
cli := struct {
Mapped *mappedValue
Expand Down