From 417f3ec66dc6452f7f813d038595cb6742823ff2 Mon Sep 17 00:00:00 2001 From: sjh9714 <163989462+sjh9714@users.noreply.github.com> Date: Sun, 5 Jul 2026 09:21:27 +0900 Subject: [PATCH] Fix duplicate embedded callback hooks --- callbacks.go | 28 +++++++++++++++++++++++++++- kong_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/callbacks.go b/callbacks.go index ecc5353..db25646 100644 --- a/callbacks.go +++ b/callbacks.go @@ -3,6 +3,7 @@ package kong import ( "fmt" "reflect" + "runtime" "strings" ) @@ -127,13 +128,38 @@ func getMethod(value reflect.Value, name string) reflect.Value { return method } +func getExplicitMethod(value reflect.Value, name string) reflect.Value { + if isExplicitMethod(value.Type(), name) { + return value.MethodByName(name) + } + if value.CanAddr() && isExplicitMethod(value.Addr().Type(), name) { + return value.Addr().MethodByName(name) + } + return reflect.Value{} +} + +func isExplicitMethod(t reflect.Type, name string) bool { + method, ok := t.MethodByName(name) + if !ok { + return false + } + // Promoted embedded methods are compiler-generated wrappers. The embedded + // value itself is visited separately, so skip those wrappers here. + fn := runtime.FuncForPC(method.Func.Pointer()) + if fn == nil { + return true + } + file, _ := fn.FileLine(method.Func.Pointer()) + return file != "" +} + // getMethods gets all methods with the given name from the given value // and any embedded fields. // // Returns a slice of bound methods that can be called directly. func getMethods(value reflect.Value, name string) (methods []reflect.Value) { walkEmbedded(value, func(v reflect.Value) { - if method := getMethod(v, name); method.IsValid() { + if method := getExplicitMethod(v, name); method.IsValid() { methods = append(methods, method) } }) diff --git a/kong_test.go b/kong_test.go index ef2300c..f638938 100644 --- a/kong_test.go +++ b/kong_test.go @@ -2697,6 +2697,37 @@ func TestApplyCalledOnce(t *testing.T) { assert.NoError(t, err) } +type EmbeddedAfterApplyLeaf struct { + calls *int +} + +func (l EmbeddedAfterApplyLeaf) AfterApply() error { + (*l.calls)++ + return nil +} + +type EmbeddedAfterApplyMiddle struct { + EmbeddedAfterApplyLeaf +} + +type EmbeddedAfterApplyRoot struct { + EmbeddedAfterApplyMiddle +} + +func TestPromotedEmbeddedAfterApplyCalledOnce(t *testing.T) { + calls := 0 + cli := &EmbeddedAfterApplyRoot{ + EmbeddedAfterApplyMiddle: EmbeddedAfterApplyMiddle{ + EmbeddedAfterApplyLeaf: EmbeddedAfterApplyLeaf{ + calls: &calls, + }, + }, + } + _, err := mustNew(t, cli).Parse(nil) + assert.NoError(t, err) + assert.Equal(t, 1, calls) +} + type envOnlyAfterApply struct { called bool }