Skip to content

Commit

Permalink
Ensure static analysis processes messages only within .Routes() call.
Browse files Browse the repository at this point in the history
  • Loading branch information
danilvpetrov committed Jan 19, 2024
1 parent f7436d1 commit d62751a
Show file tree
Hide file tree
Showing 7 changed files with 539 additions and 56 deletions.
182 changes: 127 additions & 55 deletions static/analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func addHandlerFromConfigureMethod(
hdr.IdentityValue = analyzeIdentityCall(c)
case "Routes":
addMessagesFromRoutes(
c.Common().Value.Parent(),
args,
hdr.MessageNamesValue.Produced,
hdr.MessageNamesValue.Consumed,
)
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
137 changes: 136 additions & 1 deletion static/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<aggregate>",
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: "<process>",
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: "<projection>",
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: "<integration>",
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,
Expand Down
Loading

0 comments on commit d62751a

Please sign in to comment.