Consider this (contrived simplified) example:
package main
import (
"fmt"
"github.com/alecthomas/kong"
)
type Cli struct {
CommonOptions
}
type CommonOptions struct {
VerboseOption
}
type VerboseOption struct {
Verbose bool
}
func (v VerboseOption) AfterApply() error {
fmt.Printf("Verbose is set to %v\n", v.Verbose)
return nil
}
func main() {
kong.Parse(&Cli{})
}
Upon execution, the output is:
Verbose is set to false
Verbose is set to false
Verbose is set to false
The hook has fired three times, unexpectedly. The issue is that the code at
|
// 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)...) |
|
} |
|
} |
recurses through embedded structs, but also, a method on an embedded struct is also part of the method set of the struct it is embedded into. Effectively, Kong ends up invoking:
(&Cli{}).AfterApply()
(&Cli{}).CommonOptions.AfterApply()
(&Cli{}).CommonOptions.VerboseOption.AfterApply()
This promotion into the embedding struct's method set only applies if there is no name conflict... so if the above is modified to add, e.g. a DebugOption which also has an AfterApply hook defined, as a sibling to VerboseOption, then the problematic behaviour is suppressed.
Consider this (contrived simplified) example:
Upon execution, the output is:
The hook has fired three times, unexpectedly. The issue is that the code at
kong/callbacks.go
Lines 149 to 170 in 9bc3bf9
This promotion into the embedding struct's method set only applies if there is no name conflict... so if the above is modified to add, e.g. a
DebugOptionwhich also has anAfterApplyhook defined, as a sibling toVerboseOption, then the problematic behaviour is suppressed.