Skip to content

Commit

Permalink
Merge pull request #306 from dogmatiq/iface-handler-test
Browse files Browse the repository at this point in the history
Add support for generic handlers.
  • Loading branch information
danilvpetrov authored Sep 16, 2024
2 parents df402fe + 9481281 commit 749c645
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 8 deletions.
89 changes: 81 additions & 8 deletions static/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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]
Expand All @@ -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 {
Expand All @@ -119,24 +127,72 @@ 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.
func findIndirectConfigurerCalls(
prog *ssa.Program,
configurers map[ssa.Value]struct{},
c *ssa.Call,
instantiatedTypes map[string]types.Type,
) []*ssa.Call {
com := c.Common()

Expand All @@ -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...,
)
}
Expand Down Expand Up @@ -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() {
Expand Down
69 changes: 69 additions & 0 deletions static/testdata/aureus/generic-handler.md
Original file line number Diff line number Diff line change
@@ -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("<integration>", "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("<app>", "e522c782-48d2-4c47-a4c9-81e0d7cdeba0")
c.RegisterIntegration(&a.Integration)
}

```

```au:output
application <app> (e522c782-48d2-4c47-a4c9-81e0d7cdeba0) App
- integration <integration> (abc7c329-c9da-4161-a8e2-6ab45be2dd83) *GenericIntegration[struct{}, integrationHandler]
handles CommandStub[TypeA]?
```
33 changes: 33 additions & 0 deletions static/testdata/aureus/iface-configurer.md
Original file line number Diff line number Diff line change
@@ -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("<app>", "7468a57f-20f0-4d11-9aad-48fcd553a908")
a.C.ApplyConfiguration(c)
}

```

```au:output
application <app> (7468a57f-20f0-4d11-9aad-48fcd553a908) App
```

0 comments on commit 749c645

Please sign in to comment.