diff --git a/static/analysis.go b/static/analysis.go index 0a091f8f..ce4a9eb4 100644 --- a/static/analysis.go +++ b/static/analysis.go @@ -290,7 +290,7 @@ func addHandlerFromConfigureMethod( hdr.IdentityValue = analyzeIdentityCall(c) case "Routes": addMessagesFromRoutes( - c.Common().Value.Parent(), + args, hdr.MessageNamesValue.Produced, hdr.MessageNamesValue.Consumed, ) @@ -339,64 +339,136 @@ func addHandlerFromConfigureMethod( // method Routes() to populate the messages that are produced and consumed by // the handler. func addMessagesFromRoutes( - method *ssa.Function, + args []ssa.Value, produced, consumed message.NameRoles, ) { - for _, b := range method.Blocks { - for _, i := range b.Instrs { - if mi, ok := i.(*ssa.MakeInterface); ok { - // If this is the boxing to the following interfaces, - // we need to analyze the concrete types: - switch mi.X.Type().String() { - case "github.com/dogmatiq/dogma.HandlesCommandRoute", - "github.com/dogmatiq/dogma.HandlesEventRoute", - "github.com/dogmatiq/dogma.ExecutesCommandRoute", - "github.com/dogmatiq/dogma.SchedulesTimeoutRoute", - "github.com/dogmatiq/dogma.RecordsEventRoute": - - // At this point we should expect that the interfaces above - // are produced as a result of calls to following functions: - // (At the time of writing this code, there is no other way - // to produce these interfaces) - // `github.com/dogmatiq/dogma.HandlesCommand() - // `github.com/dogmatiq/dogma.HandlesEvent()` - // `github.com/dogmatiq/dogma.ExecutesCommand()` - // `github.com/dogmatiq/dogma.RecordsEvent()` - // `github.com/dogmatiq/dogma.SchedulesTimeout()` - if f, ok := mi.X.(*ssa.Call).Common().Value.(*ssa.Function); ok { - switch { - case strings.HasPrefix(f.Name(), "ExecutesCommand["): - produced.Add( - message.NameFromType(f.TypeArgs()[0]), - message.CommandRole, - ) - case strings.HasPrefix(f.Name(), "RecordsEvent["): - produced.Add( - message.NameFromType(f.TypeArgs()[0]), - message.EventRole, - ) - case strings.HasPrefix(f.Name(), "HandlesCommand["): - consumed.Add( - message.NameFromType(f.TypeArgs()[0]), - message.CommandRole, - ) - case strings.HasPrefix(f.Name(), "HandlesEvent["): - consumed.Add( - message.NameFromType(f.TypeArgs()[0]), - message.EventRole, - ) - case strings.HasPrefix(f.Name(), "SchedulesTimeout["): - produced.Add( - message.NameFromType(f.TypeArgs()[0]), - message.TimeoutRole, - ) - consumed.Add( - message.NameFromType(f.TypeArgs()[0]), - message.TimeoutRole, - ) - } + var mii []*ssa.MakeInterface + for _, arg := range args { + recurseSSAValues( + arg, + &[]ssa.Value{}, + func(v ssa.Value) bool { + if v, ok := v.(*ssa.Call); ok { + // We don't want to recurse into the call to Routes() method + // itself. + if v.Common().IsInvoke() && + v.Common().Method.Name() == "Routes" { + return true } } + + // We do want to collect all of the MakeInterface instructions + // that can potentially indicate boxing into Dogma route + // interfaces. + if v, ok := v.(*ssa.MakeInterface); ok { + mii = append(mii, v) + return true + } + + return false + }, + ) + } + + for _, mi := range mii { + // If this is the boxing to the following interfaces, + // we need to analyze the concrete types: + switch mi.X.Type().String() { + case "github.com/dogmatiq/dogma.HandlesCommandRoute", + "github.com/dogmatiq/dogma.HandlesEventRoute", + "github.com/dogmatiq/dogma.ExecutesCommandRoute", + "github.com/dogmatiq/dogma.SchedulesTimeoutRoute", + "github.com/dogmatiq/dogma.RecordsEventRoute": + + // At this point we should expect that the interfaces above + // are produced as a result of calls to following functions: + // (At the time of writing this code, there is no other way + // to produce these interfaces) + // `github.com/dogmatiq/dogma.HandlesCommand() + // `github.com/dogmatiq/dogma.HandlesEvent()` + // `github.com/dogmatiq/dogma.ExecutesCommand()` + // `github.com/dogmatiq/dogma.RecordsEvent()` + // `github.com/dogmatiq/dogma.SchedulesTimeout()` + if f, ok := mi.X.(*ssa.Call).Common().Value.(*ssa.Function); ok { + switch { + case strings.HasPrefix(f.Name(), "ExecutesCommand["): + produced.Add( + message.NameFromType(f.TypeArgs()[0]), + message.CommandRole, + ) + case strings.HasPrefix(f.Name(), "RecordsEvent["): + produced.Add( + message.NameFromType(f.TypeArgs()[0]), + message.EventRole, + ) + case strings.HasPrefix(f.Name(), "HandlesCommand["): + consumed.Add( + message.NameFromType(f.TypeArgs()[0]), + message.CommandRole, + ) + case strings.HasPrefix(f.Name(), "HandlesEvent["): + consumed.Add( + message.NameFromType(f.TypeArgs()[0]), + message.EventRole, + ) + case strings.HasPrefix(f.Name(), "SchedulesTimeout["): + produced.Add( + message.NameFromType(f.TypeArgs()[0]), + message.TimeoutRole, + ) + consumed.Add( + message.NameFromType(f.TypeArgs()[0]), + message.TimeoutRole, + ) + } + } + } + } +} + +// recurseSSAValues recursively traverses the SSA values reachable from val. +// It calls f for each value it encounters. If f returns true, recursion stops. +func recurseSSAValues( + val ssa.Value, + seen *[]ssa.Value, + f func(ssa.Value) bool, +) { + if val == nil { + return + } + + for _, v := range *seen { + if v == val { + return + } + } + + *seen = append(*seen, val) + + if f(val) { + return + } + + // If the value is an instruction, recurse into its operands. + if instr, ok := val.(ssa.Instruction); ok { + for _, v := range instr.Operands(nil) { + recurseSSAValues(*v, seen, f) + } + } + + // Check if val has any referrers. + if referrers := val.Referrers(); referrers != nil { + for _, instr := range *referrers { + + // If the referrer is a value, recurse into it. + if v, ok := instr.(ssa.Value); ok { + recurseSSAValues(v, seen, f) + continue + } + + // Otherwise, recurse into the referrer's operands. + for _, v := range instr.Operands(nil) { + recurseSSAValues(*v, seen, f) } } } diff --git a/static/handler_test.go b/static/handler_test.go index 72a1f697..4375677b 100644 --- a/static/handler_test.go +++ b/static/handler_test.go @@ -157,7 +157,142 @@ var _ = Describe("func FromPackages() (handler analysis)", func() { )) }) - When("messages are passed to the *Configurer.Routes() method as a dynamicly populated splice", func() { + When("messages are passed to the *Configurer.Routes() method", func() { + It("includes messages passed as args to *Configurer.Routes() method only", func() { + cfg := packages.Config{ + Mode: packages.LoadAllSyntax, + Dir: "testdata/handlers/only-routes-args", + } + + pkgs := loadPackages(cfg) + + apps := FromPackages(pkgs) + Expect(apps).To(HaveLen(1)) + Expect(apps[0].Handlers().Aggregates()).To(HaveLen(1)) + Expect(apps[0].Handlers().Processes()).To(HaveLen(1)) + Expect(apps[0].Handlers().Projections()).To(HaveLen(1)) + Expect(apps[0].Handlers().Integrations()).To(HaveLen(1)) + + aggregate := apps[0].Handlers().Aggregates()[0] + Expect(aggregate.Identity()).To( + Equal( + configkit.Identity{ + Name: "", + Key: "dcfdd034-e374-478b-8faa-bc688ff59f1f", + }, + ), + ) + Expect(aggregate.TypeName()).To( + Equal( + "github.com/dogmatiq/configkit/static/testdata/handlers/only-routes-args.AggregateHandler", + ), + ) + Expect(aggregate.HandlerType()).To(Equal(configkit.AggregateHandlerType)) + + Expect(aggregate.MessageNames()).To(Equal( + configkit.EntityMessageNames{ + Consumed: message.NameRoles{ + cfixtures.MessageATypeName: message.CommandRole, + cfixtures.MessageBTypeName: message.CommandRole, + }, + Produced: message.NameRoles{ + cfixtures.MessageCTypeName: message.EventRole, + cfixtures.MessageDTypeName: message.EventRole, + }, + }, + )) + + process := apps[0].Handlers().Processes()[0] + Expect(process.Identity()).To( + Equal( + configkit.Identity{ + Name: "", + Key: "24c61438-e7ae-4d54-8e28-2fc6e848c948", + }, + ), + ) + Expect(process.TypeName()).To( + Equal( + "github.com/dogmatiq/configkit/static/testdata/handlers/only-routes-args.ProcessHandler", + ), + ) + Expect(process.HandlerType()).To(Equal(configkit.ProcessHandlerType)) + + Expect(process.MessageNames()).To(Equal( + configkit.EntityMessageNames{ + Consumed: message.NameRoles{ + cfixtures.MessageATypeName: message.EventRole, + cfixtures.MessageBTypeName: message.EventRole, + cfixtures.MessageETypeName: message.TimeoutRole, + cfixtures.MessageFTypeName: message.TimeoutRole, + }, + Produced: message.NameRoles{ + cfixtures.MessageCTypeName: message.CommandRole, + cfixtures.MessageDTypeName: message.CommandRole, + cfixtures.MessageETypeName: message.TimeoutRole, + cfixtures.MessageFTypeName: message.TimeoutRole, + }, + }, + )) + + projection := apps[0].Handlers().Projections()[0] + Expect(projection.Identity()).To( + Equal( + configkit.Identity{ + Name: "", + Key: "6b9acb05-cd77-4342-bf10-b3de9d2d5bba", + }, + ), + ) + Expect(projection.TypeName()).To( + Equal( + "github.com/dogmatiq/configkit/static/testdata/handlers/only-routes-args.ProjectionHandler", + ), + ) + Expect(projection.HandlerType()).To(Equal(configkit.ProjectionHandlerType)) + + Expect(projection.MessageNames()).To(Equal( + configkit.EntityMessageNames{ + Consumed: message.NameRoles{ + cfixtures.MessageATypeName: message.EventRole, + cfixtures.MessageBTypeName: message.EventRole, + }, + Produced: message.NameRoles{}, + }, + )) + + integration := apps[0].Handlers().Integrations()[0] + Expect(integration.Identity()).To( + Equal( + configkit.Identity{ + Name: "", + Key: "ac391765-da58-4e7c-a478-e4725eb2b0e9", + }, + ), + ) + Expect(integration.TypeName()).To( + Equal( + "github.com/dogmatiq/configkit/static/testdata/handlers/only-routes-args.IntegrationHandler", + ), + ) + Expect(integration.HandlerType()).To(Equal(configkit.IntegrationHandlerType)) + + Expect(integration.MessageNames()).To(Equal( + configkit.EntityMessageNames{ + Consumed: message.NameRoles{ + cfixtures.MessageATypeName: message.CommandRole, + cfixtures.MessageBTypeName: message.CommandRole, + }, + Produced: message.NameRoles{ + cfixtures.MessageCTypeName: message.EventRole, + cfixtures.MessageDTypeName: message.EventRole, + }, + }, + )) + }) + }) + + When("messages are passed to the *Configurer.Routes() method as a dynamically populated splice", func() { It("returns a single configuration for each handler type", func() { cfg := packages.Config{ Mode: packages.LoadAllSyntax, diff --git a/static/testdata/handlers/only-routes-args/aggregate.go b/static/testdata/handlers/only-routes-args/aggregate.go new file mode 100644 index 00000000..d4a01164 --- /dev/null +++ b/static/testdata/handlers/only-routes-args/aggregate.go @@ -0,0 +1,57 @@ +package app + +import ( + "github.com/dogmatiq/dogma" + "github.com/dogmatiq/dogma/fixtures" +) + +// Aggregate is an aggregate used for testing. +type Aggregate struct{} + +// ApplyEvent updates the aggregate instance to reflect the occurrence of an +// event that was recorded against this instance. +func (Aggregate) ApplyEvent(m dogma.Message) {} + +// AggregateHandler is a test implementation of dogma.AggregateMessageHandler. +type AggregateHandler struct{} + +// New returns a new account instance. +func (AggregateHandler) New() dogma.AggregateRoot { + return Aggregate{} +} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (AggregateHandler) Configure(c dogma.AggregateConfigurer) { + c.Identity("", "dcfdd034-e374-478b-8faa-bc688ff59f1f") + + c.Routes( + dogma.HandlesCommand[fixtures.MessageA](), + dogma.HandlesCommand[fixtures.MessageB](), + dogma.RecordsEvent[fixtures.MessageC](), + dogma.RecordsEvent[fixtures.MessageD](), + ) + + // These assignments should not be included into handler configuration + // as they are not arguments to the Routes() method. + var r1 dogma.AggregateRoute = dogma.RecordsEvent[fixtures.MessageE]() + var r2 dogma.AggregateRoute = dogma.HandlesCommand[fixtures.MessageF]() + // Assign to blank identifiers to avoid unused variable errors during + // compilation. + _ = r1 + _ = r2 +} + +// RouteCommandToInstance returns the ID of the aggregate instance that is +// targetted by m. +func (AggregateHandler) RouteCommandToInstance(m dogma.Message) string { + return "" +} + +// HandleCommand handles a command message that has been routed to this handler. +func (AggregateHandler) HandleCommand( + r dogma.AggregateRoot, + s dogma.AggregateCommandScope, + m dogma.Message, +) { +} diff --git a/static/testdata/handlers/only-routes-args/app.go b/static/testdata/handlers/only-routes-args/app.go new file mode 100644 index 00000000..b17ff0c9 --- /dev/null +++ b/static/testdata/handlers/only-routes-args/app.go @@ -0,0 +1,17 @@ +package app + +import "github.com/dogmatiq/dogma" + +// App implements dogma.Application interface. +type App struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("", "f2c08525-623e-4c76-851c-3172953269e3") + + c.RegisterIntegration(IntegrationHandler{}) + c.RegisterProjection(ProjectionHandler{}) + c.RegisterAggregate(AggregateHandler{}) + c.RegisterProcess(ProcessHandler{}) +} diff --git a/static/testdata/handlers/only-routes-args/integration.go b/static/testdata/handlers/only-routes-args/integration.go new file mode 100644 index 00000000..63cd2440 --- /dev/null +++ b/static/testdata/handlers/only-routes-args/integration.go @@ -0,0 +1,56 @@ +package app + +import ( + "context" + "time" + + "github.com/dogmatiq/dogma" + "github.com/dogmatiq/dogma/fixtures" +) + +// IntegrationHandler is a test implementation of +// dogma.IntegrationMessageHandler. +type IntegrationHandler struct{} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (IntegrationHandler) Configure(c dogma.IntegrationConfigurer) { + c.Identity("", "ac391765-da58-4e7c-a478-e4725eb2b0e9") + + c.Routes( + dogma.HandlesCommand[fixtures.MessageA](), + dogma.HandlesCommand[fixtures.MessageB](), + dogma.RecordsEvent[fixtures.MessageC](), + dogma.RecordsEvent[fixtures.MessageD](), + ) + + // These assignments should not be included into handler configuration + // as they are not arguments to the Routes() method. + var r1 dogma.IntegrationRoute = dogma.RecordsEvent[fixtures.MessageE]() + var r2 dogma.IntegrationRoute = dogma.HandlesCommand[fixtures.MessageF]() + // Assign to blank identifiers to avoid unused variable errors during + // compilation. + _ = r1 + _ = r2 +} + +// RouteCommandToInstance returns the ID of the integration instance that is +// targetted by m. +func (IntegrationHandler) RouteCommandToInstance(m dogma.Message) string { + return "" +} + +// HandleCommand handles a command message that has been routed to this handler. +func (IntegrationHandler) HandleCommand( + ctx context.Context, + s dogma.IntegrationCommandScope, + m dogma.Message, +) error { + return nil +} + +// TimeoutHint returns a duration that is suitable for computing a deadline +// for the handling of the given message by this handler. +func (IntegrationHandler) TimeoutHint(m dogma.Message) time.Duration { + return 0 +} diff --git a/static/testdata/handlers/only-routes-args/process.go b/static/testdata/handlers/only-routes-args/process.go new file mode 100644 index 00000000..2d2601e0 --- /dev/null +++ b/static/testdata/handlers/only-routes-args/process.go @@ -0,0 +1,81 @@ +package app + +import ( + "context" + "time" + + "github.com/dogmatiq/dogma" + "github.com/dogmatiq/dogma/fixtures" +) + +// Process is a process used for testing. +type Process struct{} + +// ProcessHandler is a test implementation of dogma.ProcessMessageHandler. +type ProcessHandler struct{} + +// New constructs a new process instance initialized with any default values and +// returns the process root. +func (ProcessHandler) New() dogma.ProcessRoot { + return Process{} +} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (ProcessHandler) Configure(c dogma.ProcessConfigurer) { + c.Identity("", "24c61438-e7ae-4d54-8e28-2fc6e848c948") + + c.Routes( + dogma.HandlesEvent[fixtures.MessageA](), + dogma.HandlesEvent[fixtures.MessageB](), + dogma.ExecutesCommand[fixtures.MessageC](), + dogma.ExecutesCommand[fixtures.MessageD](), + dogma.SchedulesTimeout[fixtures.MessageE](), + dogma.SchedulesTimeout[fixtures.MessageF](), + ) + + // These assignments should not be included into handler configuration + // as they are not arguments to the Routes() method. + var r1 dogma.ProcessRoute = dogma.HandlesEvent[fixtures.MessageE]() + var r2 dogma.ProcessRoute = dogma.ExecutesCommand[fixtures.MessageF]() + // Assign to blank identifiers to avoid unused variable errors during + // compilation. + _ = r1 + _ = r2 +} + +// RouteEventToInstance returns the ID of the process instance that is +// targeted by m. +func (ProcessHandler) RouteEventToInstance( + ctx context.Context, + m dogma.Message, +) (string, bool, error) { + return "", true, nil +} + +// HandleEvent handles an event message. +func (ProcessHandler) HandleEvent( + ctx context.Context, + r dogma.ProcessRoot, + s dogma.ProcessEventScope, + m dogma.Message, +) error { + return nil +} + +// HandleTimeout handles a timeout message that has been scheduled with +// ProcessScope.ScheduleTimeout(). +func (ProcessHandler) HandleTimeout( + ctx context.Context, + r dogma.ProcessRoot, + s dogma.ProcessTimeoutScope, + m dogma.Message, +) error { + return nil +} + +// TimeoutHint returns a duration that is suitable for computing a deadline +// for the handling of the given message by this handler. +func (ProcessHandler) TimeoutHint(m dogma.Message) time.Duration { + return 0 +} diff --git a/static/testdata/handlers/only-routes-args/projection.go b/static/testdata/handlers/only-routes-args/projection.go new file mode 100644 index 00000000..9b08ecc4 --- /dev/null +++ b/static/testdata/handlers/only-routes-args/projection.go @@ -0,0 +1,65 @@ +package app + +import ( + "context" + "time" + + "github.com/dogmatiq/dogma" + "github.com/dogmatiq/dogma/fixtures" +) + +// ProjectionHandler is a test implementation of dogma.ProjectionMessageHandler. +type ProjectionHandler struct{} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (ProjectionHandler) Configure(c dogma.ProjectionConfigurer) { + c.Identity("", "6b9acb05-cd77-4342-bf10-b3de9d2d5bba") + + c.Routes( + dogma.HandlesEvent[fixtures.MessageA](), + dogma.HandlesEvent[fixtures.MessageB](), + ) + + // This assignment should not be included into handler configuration + // as it is not an argument to the Routes() method. + var r1 dogma.ProjectionRoute = dogma.HandlesEvent[fixtures.MessageE]() + // Assign to a blank identifier to avoid unused variable error during + // compilation. + _ = r1 +} + +// HandleEvent updates the projection to reflect the occurrence of an event. +func (ProjectionHandler) HandleEvent( + ctx context.Context, + r, c, n []byte, + s dogma.ProjectionEventScope, + m dogma.Message, +) (ok bool, err error) { + return false, nil +} + +// ResourceVersion returns the version of the resource r. +func (ProjectionHandler) ResourceVersion( + ctx context.Context, + r []byte, +) ([]byte, error) { + return nil, nil +} + +// CloseResource informs the projection that the resource r will not be +// used in any future calls to HandleEvent(). +func (ProjectionHandler) CloseResource(ctx context.Context, r []byte) error { + return nil +} + +// TimeoutHint returns a duration that is suitable for computing a deadline +// for the handling of the given message by this handler. +func (ProjectionHandler) TimeoutHint(m dogma.Message) time.Duration { + return 0 +} + +// Compact reduces the size of the projection's data. +func (ProjectionHandler) Compact(ctx context.Context, s dogma.ProjectionCompactScope) error { + return nil +}