From ae69c0cdb10eba7f31fbcaaa1b7f34198d9679eb Mon Sep 17 00:00:00 2001 From: Danil Petrov Date: Fri, 13 Sep 2024 17:02:50 +1000 Subject: [PATCH 1/2] Add support for generic handlers. --- debug.test2852268322 | 0 static/analysis.go | 89 ++++++++++++++++++++-- static/testdata/aureus/generic-handler.md | 69 +++++++++++++++++ static/testdata/aureus/iface-configurer.md | 33 ++++++++ 4 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 debug.test2852268322 create mode 100644 static/testdata/aureus/generic-handler.md create mode 100644 static/testdata/aureus/iface-configurer.md diff --git a/debug.test2852268322 b/debug.test2852268322 new file mode 100644 index 00000000..e69de29b diff --git a/static/analysis.go b/static/analysis.go index ef5a5d0d..a9ff49e6 100644 --- a/static/analysis.go +++ b/static/analysis.go @@ -28,7 +28,7 @@ func analyzeApplication( pkg := pkgOfNamedType(typ) fn := prog.LookupMethod(typ, pkg, "Configure") - for _, c := range findConfigurerCalls(prog, fn) { + for _, c := range findConfigurerCalls(prog, fn, make(map[string]types.Type)) { args := c.Common().Args switch c.Common().Method.Name() { @@ -86,19 +86,23 @@ func analyzeApplication( // fn is the Configure() method on an application or handler. In this case the // first parameter is the receiver, so the second parameter is the configurer // itself. +// +// The instantiatedTypes map is used to store the types that have been +// instantiated in the function. This is necessary because the SSA +// representation of a function does not include type information for the +// arguments, so we need to track this information ourselves. The keys are the +// names of the type parameters and the values are the concrete types that have +// been instantiated. func findConfigurerCalls( prog *ssa.Program, fn *ssa.Function, + instantiatedTypes map[string]types.Type, indices ...int, ) []*ssa.Call { if len(indices) == 0 { indices = []int{1} } - if fn == nil { - return nil - } - configurers := map[ssa.Value]struct{}{} for _, i := range indices { v := fn.Params[i] @@ -107,6 +111,10 @@ func findConfigurerCalls( var calls []*ssa.Call + if fn.Synthetic != "" { + populateInstantiatedTypes(fn, instantiatedTypes) + } + for _, b := range fn.Blocks { for _, i := range b.Instrs { if c, ok := i.(*ssa.Call); ok { @@ -119,17 +127,64 @@ func findConfigurerCalls( // to see if *it* makes any calls to the configurer. calls = append( calls, - findIndirectConfigurerCalls(prog, configurers, c)..., + findIndirectConfigurerCalls( + prog, + configurers, + c, + instantiatedTypes, + )..., ) } } - } } return calls } +// populateInstantiatedTypes populates the instantiatedTypes map with the types +// that have been instantiated in the synthetic function. +func populateInstantiatedTypes( + fn *ssa.Function, + instantiatedTypes map[string]types.Type, +) { + for _, b := range fn.Blocks { + for _, i := range b.Instrs { + if c, ok := i.(*ssa.ChangeType); ok { + var ( + tpl *types.TypeParamList + tal *types.TypeList + ) + + if ok, nt := namedType(c.Type()); ok { + tpl = nt.TypeParams() + } + + if ok, nt := namedType(c.X.Type()); ok { + tal = nt.TypeArgs() + } + + for i := 0; i < tpl.Len(); i++ { + instantiatedTypes[tpl.At(i).String()] = tal.At(i) + } + } + } + } +} + +// namedType returns true and the named type if t is a named type or a pointer +// to a named type. +func namedType(t types.Type) (ok bool, nt *types.Named) { + switch t := t.(type) { + case *types.Named: + return true, t + case *types.Pointer: + return namedType(t.Elem()) + default: + return false, nil + } +} + // findIndirectConfigurerCalls returns all of the calls to methods on the Dogma // application or handler "configurer" within the an arbitrary function that has // been called within a Configure() method. @@ -137,6 +192,7 @@ func findIndirectConfigurerCalls( prog *ssa.Program, configurers map[ssa.Value]struct{}, c *ssa.Call, + instantiatedTypes map[string]types.Type, ) []*ssa.Call { com := c.Common() @@ -151,9 +207,26 @@ func findIndirectConfigurerCalls( return nil } + if com.IsInvoke() { + t, ok := instantiatedTypes[com.Value.Type().String()] + if !ok { + // If we cannot find any instantiated types in mapping, most likely + // we hit the interface method and cannot analyze any further. + return nil + } + + return findConfigurerCalls( + prog, + prog.LookupMethod(t, com.Method.Pkg(), com.Method.Name()), + instantiatedTypes, + // don't pass indices here, as we are already in the method. + ) + } + return findConfigurerCalls( prog, com.StaticCallee(), + instantiatedTypes, indices..., ) } @@ -286,7 +359,7 @@ func addHandlerFromConfigureMethod( }, } - for _, c := range findConfigurerCalls(prog, method) { + for _, c := range findConfigurerCalls(prog, method, make(map[string]types.Type)) { args := c.Common().Args switch c.Common().Method.Name() { diff --git a/static/testdata/aureus/generic-handler.md b/static/testdata/aureus/generic-handler.md new file mode 100644 index 00000000..9586f6e4 --- /dev/null +++ b/static/testdata/aureus/generic-handler.md @@ -0,0 +1,69 @@ +# Interface as an entity configurer. + +This test ensures that the static analyzer can recognize the type of a handler +when it is used in instantiating a generic handler. + +```go au:input +package app + +import ( + "context" + . "github.com/dogmatiq/dogma" + . "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +type GenericIntegration[T any, H IntegrationMessageHandler] struct { + Handler H +} + +func (i *GenericIntegration[T, H]) Configure(c IntegrationConfigurer) { + i.Handler.Configure(c) +} + +func (i *GenericIntegration[T, H]) HandleCommand( + ctx context.Context, + s IntegrationCommandScope, + cmd Command, +) error { + return i.Handler.HandleCommand(ctx, s, cmd) +} + +type integrationHandler struct {} + +func (integrationHandler) Configure(c IntegrationConfigurer) { + c.Identity("", "abc7c329-c9da-4161-a8e2-6ab45be2dd83") + + routes := []IntegrationRoute{ + HandlesCommand[CommandStub[TypeA]](), + } + + c.Routes(routes...) +} + +func (integrationHandler) HandleCommand( + _ context.Context, + _ IntegrationCommandScope, + _ Command, +) error { + return nil +} + +type InstantiatedIntegration = GenericIntegration[struct{}, integrationHandler] + +type App struct { + Integration InstantiatedIntegration +} + +func (a App) Configure(c ApplicationConfigurer) { + c.Identity("", "e522c782-48d2-4c47-a4c9-81e0d7cdeba0") + c.RegisterIntegration(&a.Integration) +} + +``` + +```au:output +application (e522c782-48d2-4c47-a4c9-81e0d7cdeba0) App + + - integration (abc7c329-c9da-4161-a8e2-6ab45be2dd83) *GenericIntegration[struct{}, integrationHandler] + handles CommandStub[TypeA]? +``` diff --git a/static/testdata/aureus/iface-configurer.md b/static/testdata/aureus/iface-configurer.md new file mode 100644 index 00000000..ea0a5133 --- /dev/null +++ b/static/testdata/aureus/iface-configurer.md @@ -0,0 +1,33 @@ +# Interface as an entity configurer. + +This test ensures that the static analyzer does not behaves abnormally when it +encounters an interface that handles configuration. In this case, the static +analysis is not capable of gathering data about what particular entity is +configured behind the interface. + +```go au:input +package app + +import ( + . "github.com/dogmatiq/dogma" +) + + +type Configurer interface { + ApplyConfiguration(c ApplicationConfigurer) +} + +type App struct { + C Configurer +} + +func (a App) Configure(c ApplicationConfigurer) { + c.Identity("", "7468a57f-20f0-4d11-9aad-48fcd553a908") + a.C.ApplyConfiguration(c) +} + +``` + +```au:output +application (7468a57f-20f0-4d11-9aad-48fcd553a908) App +``` From 948128167c305bb36a76874de22b38c0ddbbd145 Mon Sep 17 00:00:00 2001 From: Danil Petrov Date: Sat, 14 Sep 2024 18:21:28 +1000 Subject: [PATCH 2/2] Remove test debug artifact. --- debug.test2852268322 | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 debug.test2852268322 diff --git a/debug.test2852268322 b/debug.test2852268322 deleted file mode 100644 index e69de29b..00000000