From 752dbdff28275255b49c61a882a98cf99ad37e71 Mon Sep 17 00:00:00 2001 From: James Harris Date: Wed, 23 Oct 2024 16:02:56 +1000 Subject: [PATCH] WIP [ci skip] --- config/staticconfig/analyze.go | 2 +- config/staticconfig/application.go | 63 +++----- config/staticconfig/configurer.go | 162 ------------------- config/staticconfig/context.go | 4 +- config/staticconfig/entity.go | 183 ++++++++++++++++++++++ config/staticconfig/handler.go | 113 ++++++------- config/staticconfig/identity.go | 28 ---- config/staticconfig/internal/ssax/flow.go | 8 + config/staticconfig/route.go | 14 +- config/staticconfig/type.go | 6 +- config/staticconfig/varargs.go | 8 +- 11 files changed, 281 insertions(+), 310 deletions(-) delete mode 100644 config/staticconfig/configurer.go create mode 100644 config/staticconfig/entity.go delete mode 100644 config/staticconfig/identity.go diff --git a/config/staticconfig/analyze.go b/config/staticconfig/analyze.go index fb9f7a3..087c33c 100644 --- a/config/staticconfig/analyze.go +++ b/config/staticconfig/analyze.go @@ -108,7 +108,7 @@ func Analyze(pkgs []*packages.Package) Analysis { }, } - if !findDogma(ctx) { + if !resolveDogmaPackage(ctx) { // If the dogma package is not found as an import, none of the packages // can possibly have types that implement [dogma.Application] because // doing so requires referring to [dogma.ApplicationConfigurer]. diff --git a/config/staticconfig/application.go b/config/staticconfig/application.go index 1c82f64..909899c 100644 --- a/config/staticconfig/application.go +++ b/config/staticconfig/application.go @@ -5,47 +5,36 @@ import ( "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/config/internal/configbuilder" - "github.com/dogmatiq/enginekit/internal/typename" ) // analyzeApplicationType analyzes t, which must be an implementation of // [dogma.Application]. func analyzeApplicationType(ctx *context, t types.Type) { - ctx.Analysis.Applications = append( - ctx.Analysis.Applications, - configbuilder.Application( - func(b *configbuilder.ApplicationBuilder) { - b.SetSourceTypeName(typename.OfStatic(t)) - - for call := range findConfigurerCalls(ctx, b, t) { - switch call.Method.Name() { - case "Identity": - analyzeIdentityCall(b, call) - case "RegisterAggregate": - b.Aggregate(func(b *configbuilder.AggregateBuilder) { - b.UpdateFidelity(call.Fidelity) - analyzeAggregate(ctx, b, call.Args[0]) - }) - case "RegisterProcess": - b.Process(func(b *configbuilder.ProcessBuilder) { - b.UpdateFidelity(call.Fidelity) - analyzeProcess(ctx, b, call.Args[0]) - }) - case "RegisterIntegration": - b.Integration(func(b *configbuilder.IntegrationBuilder) { - b.UpdateFidelity(call.Fidelity) - analyzeIntegration(ctx, b, call.Args[0]) - }) - case "RegisterProjection": - b.Projection(func(b *configbuilder.ProjectionBuilder) { - b.UpdateFidelity(call.Fidelity) - analyzeProjection(ctx, b, call.Args[0]) - }) - default: - b.UpdateFidelity(config.Incomplete) - } - } - }, - ), + app := configbuilder.Application( + func(b *configbuilder.ApplicationBuilder) { + analyzeEntity( + ctx, + t, + b, + analyzeApplicationConfigurerCall, + ) + }, ) + + ctx.Analysis.Applications = append(ctx.Analysis.Applications, app) +} + +func analyzeApplicationConfigurerCall(ctx *configurerCallContext[*configbuilder.ApplicationBuilder]) { + switch ctx.Method.Name() { + case "RegisterAggregate": + analyzeHandler(ctx, ctx.Builder.Aggregate, nil) + case "RegisterProcess": + analyzeHandler(ctx, ctx.Builder.Process, nil) + case "RegisterIntegration": + analyzeHandler(ctx, ctx.Builder.Integration, nil) + case "RegisterProjection": + analyzeHandler(ctx, ctx.Builder.Projection, analyzeProjectionConfigurerCall) + default: + ctx.Builder.UpdateFidelity(config.Incomplete) + } } diff --git a/config/staticconfig/configurer.go b/config/staticconfig/configurer.go deleted file mode 100644 index 07f0230..0000000 --- a/config/staticconfig/configurer.go +++ /dev/null @@ -1,162 +0,0 @@ -package staticconfig - -import ( - "go/types" - "iter" - - "github.com/dogmatiq/enginekit/config" - "github.com/dogmatiq/enginekit/config/internal/configbuilder" - "github.com/dogmatiq/enginekit/config/staticconfig/internal/ssax" - "golang.org/x/tools/go/ssa" -) - -// configureContext is a specialization of [context] that is used when analyzing -// a Configure() method. -type configureContext struct { - *context - - Func *ssa.Function - Builder configbuilder.EntityBuilder - ConfigurerIndices []int -} - -func (c *configureContext) IsConfigurer(v ssa.Value) bool { - for _, i := range c.ConfigurerIndices { - if v == c.Func.Params[i] { - return true - } - } - - return false -} - -type configurerCall struct { - *ssa.CallCommon - - Instruction ssa.CallInstruction - Fidelity config.Fidelity -} - -// analyzeConfigurerCalls analyzes the calls to the "configurer" that is passed -// to t's "Configure()" method. -// -// Any calls that are not recognized are yielded. -func findConfigurerCalls( - ctx *context, - b configbuilder.EntityBuilder, - t types.Type, -) iter.Seq[configurerCall] { - configure := ctx.LookupMethod(t, "Configure") - - return func(yield func(configurerCall) bool) { - emitConfigurerCallsInFunc( - &configureContext{ - context: ctx, - Func: configure, - Builder: b, - ConfigurerIndices: []int{1}, - }, - configure, - yield, - ) - } -} - -// emitConfigurerCallsInFunc yields all call to methods on the Dogma application -// or handler "configurer" within the given function. -// -// indices is a list of the positions of parameters to fn that are the -// configurer. -func emitConfigurerCallsInFunc( - ctx *configureContext, - fn *ssa.Function, - yield func(configurerCall) bool, -) bool { - if len(fn.Blocks) == 0 { - return true - } - - for b := range ssax.WalkDown(fn.Blocks[0]) { - for _, inst := range b.Instrs { - if !emitConfigurerCallsInInstruction(ctx, inst, yield) { - return false - } - } - } - - return true -} - -func emitConfigurerCallsInInstruction( - ctx *configureContext, - inst ssa.Instruction, - yield func(configurerCall) bool, -) bool { - switch inst := inst.(type) { - case ssa.CallInstruction: - return emitConfigurerCallsInCallInstruction(ctx, inst, yield) - default: - return true - } -} - -func emitConfigurerCallsInCallInstruction( - ctx *configureContext, - call ssa.CallInstruction, - yield func(configurerCall) bool, -) bool { - com := call.Common() - - if com.IsInvoke() && ctx.IsConfigurer(com.Value) { - // We've found a direct call to a method on the configurer. - var f config.Fidelity - if !ssax.IsUnconditional(call.Block()) { - f |= config.Speculative - } - - return yield(configurerCall{com, call, f}) - } - - // We've found a call to some function or method that does not belong to the - // configurer. If any of the arguments are the configurer we analyze the - // called function as well. - // - // This is an quite naive implementation. There are other ways that the - // callee could gain access to the configurer. For example, it could be - // passed inside a context, or assigned to a field within the entity struct. - // - // First, we build a list of the indices of arguments that are the - // configurer. It doesn't make much sense, but the configurer could be - // passed in multiple positions. - var indices []int - for i, arg := range com.Args { - if ctx.IsConfigurer(arg) { - indices = append(indices, i) - } - } - - // If none of the arguments are the configurer, we can skip analyzing the - // callee. This prevents us from analyzing the entire program. - if len(indices) == 0 { - return true - } - - // If we can't obtain the callee, this is a call to an interface method, or - // some other un-analyzable function. - fn := com.StaticCallee() - if fn == nil { - ctx.Builder.UpdateFidelity(config.Incomplete) - return true - } - - return emitConfigurerCallsInFunc( - &configureContext{ - context: ctx.context, - Func: fn, - Builder: ctx.Builder, - ConfigurerIndices: indices, - }, - fn, - yield, - ) -} diff --git a/config/staticconfig/context.go b/config/staticconfig/context.go index 09db393..abd475a 100644 --- a/config/staticconfig/context.go +++ b/config/staticconfig/context.go @@ -25,10 +25,10 @@ type context struct { Analysis *Analysis } -// findDogma updates ctx with information about the Dogma package. +// resolveDogmaPackage updates ctx with information about the Dogma package. // // It returns false if the Dogma package has not been imported. -func findDogma(ctx *context) bool { +func resolveDogmaPackage(ctx *context) bool { for _, pkg := range ctx.Program.AllPackages() { if pkg.Pkg.Path() != "github.com/dogmatiq/dogma" { continue diff --git a/config/staticconfig/entity.go b/config/staticconfig/entity.go new file mode 100644 index 0000000..4296754 --- /dev/null +++ b/config/staticconfig/entity.go @@ -0,0 +1,183 @@ +package staticconfig + +import ( + "go/types" + + "github.com/dogmatiq/enginekit/config" + "github.com/dogmatiq/enginekit/config/internal/configbuilder" + "github.com/dogmatiq/enginekit/config/staticconfig/internal/ssax" + "github.com/dogmatiq/enginekit/internal/typename" + "golang.org/x/tools/go/ssa" +) + +type entityContext[T configbuilder.EntityBuilder] struct { + *context + + EntityType types.Type + Builder T + ConfigureMethod *ssa.Function + FunctionUnderAnalysis *ssa.Function + ConfigurerParamIndices []int +} + +func (c *entityContext[T]) IsConfigurer(v ssa.Value) bool { + for _, i := range c.ConfigurerParamIndices { + if v == c.FunctionUnderAnalysis.Params[i] { + return true + } + } + return false +} + +type configurerCallContext[T configbuilder.EntityBuilder] struct { + *entityContext[T] + *ssa.CallCommon + + Instruction ssa.CallInstruction + Fidelity config.Fidelity +} + +// configurerCallAnalyzer is a function that analyzes a call to a method on an +// entity's configurer. +type configurerCallAnalyzer[T configbuilder.EntityBuilder] func(*configurerCallContext[T]) + +// analyzeEntity analyzes the Configure() method of the type t, which must be a +// Dogma application or handler. +// +// It calls the analyze function for each call to a method on the configurer, +// other than Identity() which is handled the same in all cases. +func analyzeEntity[T configbuilder.EntityBuilder]( + ctx *context, + t types.Type, + builder T, + analyze configurerCallAnalyzer[T], +) { + builder.SetSourceTypeName(typename.OfStatic(t)) + configure := ctx.LookupMethod(t, "Configure") + + analyzeConfigurerCallsInFunc( + &entityContext[T]{ + context: ctx, + EntityType: t, + Builder: builder, + ConfigureMethod: configure, + FunctionUnderAnalysis: configure, + ConfigurerParamIndices: []int{1}, + }, + func(ctx *configurerCallContext[T]) { + switch ctx.Method.Name() { + case "Identity": + analyzeIdentity(ctx) + default: + analyze(ctx) + } + }, + ) +} + +// analyzeConfigurerCallsInFunc analyzes calls to methods on the configurer in +// the function under analysis. +func analyzeConfigurerCallsInFunc[T configbuilder.EntityBuilder]( + ctx *entityContext[T], + analyze configurerCallAnalyzer[T], +) { + for b := range ssax.WalkFunc(ctx.FunctionUnderAnalysis) { + for _, inst := range b.Instrs { + if inst, ok := inst.(ssa.CallInstruction); ok { + analyzeConfigurerCallsInInstruction(ctx, inst, analyze) + } + } + } +} + +// analyzeConfigurerCallsInInstruction analyzes calls to methods on the +// configurer in the given instruction. +func analyzeConfigurerCallsInInstruction[T configbuilder.EntityBuilder]( + ctx *entityContext[T], + inst ssa.CallInstruction, + analyze configurerCallAnalyzer[T], +) { + com := inst.Common() + + if com.IsInvoke() && ctx.IsConfigurer(com.Value) { + // We've found a direct call to a method on the configurer. + var f config.Fidelity + if !ssax.IsUnconditional(inst.Block()) { + f |= config.Speculative + } + + analyze(&configurerCallContext[T]{ + entityContext: ctx, + CallCommon: com, + Instruction: inst, + Fidelity: f, + }) + + return + } + + // We've found a call to some function or method that does not belong to the + // configurer. If any of the arguments are the configurer we analyze the + // called function as well. + // + // This is an quite naive implementation. There are other ways that the + // callee could gain access to the configurer. For example, it could be + // passed inside a context, or assigned to a field within the entity struct. + // + // First, we build a list of the indices of arguments that are the + // configurer. It doesn't make much sense, but the configurer could be + // passed in multiple positions. + var indices []int + for i, arg := range com.Args { + if ctx.IsConfigurer(arg) { + indices = append(indices, i) + } + } + + // We don't analyze the callee if it is not passed the configurer. + if len(indices) == 0 { + return + } + + // If we can't obtain the callee this is a call to an interface method or + // some other un-analyzable function. + fn := com.StaticCallee() + if fn == nil { + ctx.Builder.UpdateFidelity(config.Incomplete) + return + } + + analyzeConfigurerCallsInFunc( + &entityContext[T]{ + context: ctx.context, + EntityType: ctx.EntityType, + Builder: ctx.Builder, + ConfigureMethod: ctx.ConfigureMethod, + FunctionUnderAnalysis: fn, + ConfigurerParamIndices: indices, + }, + analyze, + ) +} + +func analyzeIdentity[T configbuilder.EntityBuilder]( + ctx *configurerCallContext[T], +) { + ctx. + Builder. + Identity(func(b *configbuilder.IdentityBuilder) { + b.UpdateFidelity(ctx.Fidelity) + + if name, ok := ssax.AsString(ctx.Args[0]).TryGet(); ok { + b.SetName(name) + } else { + b.UpdateFidelity(config.Incomplete) + } + + if key, ok := ssax.AsString(ctx.Args[1]).TryGet(); ok { + b.SetKey(key) + } else { + b.UpdateFidelity(config.Incomplete) + } + }) +} diff --git a/config/staticconfig/handler.go b/config/staticconfig/handler.go index 4c1eff3..cec6448 100644 --- a/config/staticconfig/handler.go +++ b/config/staticconfig/handler.go @@ -1,87 +1,68 @@ package staticconfig import ( - "iter" - "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/config/internal/configbuilder" - "github.com/dogmatiq/enginekit/internal/typename" "golang.org/x/tools/go/ssa" ) -func analyzeHandler( - ctx *context, - b configbuilder.HandlerBuilder, - h ssa.Value, -) iter.Seq[configurerCall] { - return func(yield func(configurerCall) bool) { - switch inst := h.(type) { - default: - b.UpdateFidelity(config.Incomplete) - case *ssa.MakeInterface: - t := inst.X.Type() - b.SetSourceTypeName(typename.OfStatic(t)) +func analyzeHandler[T configbuilder.HandlerBuilder]( + ctx *configurerCallContext[*configbuilder.ApplicationBuilder], + build func(func(T)), + analyze configurerCallAnalyzer[T], +) { + build(func(b T) { + b.UpdateFidelity(ctx.Fidelity) - for call := range findConfigurerCalls(ctx, b, t) { - switch call.Method.Name() { - case "Identity": - analyzeIdentityCall(b, call) + inst, ok := ctx.Args[0].(*ssa.MakeInterface) + if !ok { + ctx.Builder.UpdateFidelity(config.Incomplete) + return + } + + analyzeEntity( + ctx.context, + inst.X.Type(), + b, + func(ctx *configurerCallContext[T]) { + switch ctx.Method.Name() { case "Routes": - analyzeRoutesCall(ctx, b, call) + analyzeRoutes(ctx) + case "Disable": - b.SetDisabled(true) + // TODO(jmalloc): f is lost in this case, so any handler + // that is _sometimes_ disabled will appear as always + // disabled, which is a bit non-sensical. + // + // It probably needs similar treatment to + // https://github.com/dogmatiq/enginekit/issues/55. + ctx.Builder.SetDisabled(true) + default: - if !yield(call) { - return + if analyze == nil { + ctx.Builder.UpdateFidelity(config.Incomplete) + } else { + analyze(ctx) } } - } + }, + ) - // If the handler wasn't disabled, and the configuration is NOT - // incomplete, we know that the handler is enabled. - if !b.IsDisabled().IsPresent() && b.Fidelity()&config.Incomplete == 0 { - b.SetDisabled(false) - } + // If the handler wasn't disabled, and the configuration is NOT + // incomplete, we know that the handler is enabled. + if !b.IsDisabled().IsPresent() && b.Fidelity()&config.Incomplete == 0 { + b.SetDisabled(false) } - } -} - -func analyzeAggregate( - ctx *context, - b *configbuilder.AggregateBuilder, - h ssa.Value, -) { - for call := range analyzeHandler(ctx, b, h) { - b.UpdateFidelity(call.Fidelity) - } -} - -func analyzeProcess( - ctx *context, - b *configbuilder.ProcessBuilder, - h ssa.Value, -) { - for call := range analyzeHandler(ctx, b, h) { - b.UpdateFidelity(call.Fidelity) - } -} - -func analyzeIntegration( - ctx *context, - b *configbuilder.IntegrationBuilder, - h ssa.Value, -) { - for call := range analyzeHandler(ctx, b, h) { - b.UpdateFidelity(call.Fidelity) - } + }) } -func analyzeProjection( - ctx *context, - b *configbuilder.ProjectionBuilder, - h ssa.Value, +func analyzeProjectionConfigurerCall( + ctx *configurerCallContext[*configbuilder.ProjectionBuilder], ) { - for call := range analyzeHandler(ctx, b, h) { - b.UpdateFidelity(call.Fidelity) + switch ctx.Method.Name() { + case "DeliveryPolicy": + panic("not implemented") // TODO + default: + ctx.Builder.UpdateFidelity(config.Incomplete) } } diff --git a/config/staticconfig/identity.go b/config/staticconfig/identity.go deleted file mode 100644 index 5d8c761..0000000 --- a/config/staticconfig/identity.go +++ /dev/null @@ -1,28 +0,0 @@ -package staticconfig - -import ( - "github.com/dogmatiq/enginekit/config" - "github.com/dogmatiq/enginekit/config/internal/configbuilder" - "github.com/dogmatiq/enginekit/config/staticconfig/internal/ssax" -) - -func analyzeIdentityCall( - b configbuilder.EntityBuilder, - call configurerCall, -) { - b.Identity(func(b *configbuilder.IdentityBuilder) { - b.UpdateFidelity(call.Fidelity) - - if name, ok := ssax.AsString(call.Args[0]).TryGet(); ok { - b.SetName(name) - } else { - b.UpdateFidelity(config.Incomplete) - } - - if key, ok := ssax.AsString(call.Args[1]).TryGet(); ok { - b.SetKey(key) - } else { - b.UpdateFidelity(config.Incomplete) - } - }) -} diff --git a/config/staticconfig/internal/ssax/flow.go b/config/staticconfig/internal/ssax/flow.go index 9a7f2ad..6eb81a7 100644 --- a/config/staticconfig/internal/ssax/flow.go +++ b/config/staticconfig/internal/ssax/flow.go @@ -6,6 +6,14 @@ import ( "golang.org/x/tools/go/ssa" ) +// WalkFunc recursively yields all reachable blocks in the given function. +func WalkFunc(fn *ssa.Function) iter.Seq[*ssa.BasicBlock] { + if len(fn.Blocks) == 0 { + return func(func(*ssa.BasicBlock) bool) {} + } + return WalkDown(fn.Blocks[0]) +} + // WalkDown recursively yields b and all reachable successor blocks of b. // // A block is considered reachable if there is a control flow path from b to diff --git a/config/staticconfig/route.go b/config/staticconfig/route.go index 2f33b64..50fb3cf 100644 --- a/config/staticconfig/route.go +++ b/config/staticconfig/route.go @@ -7,15 +7,13 @@ import ( "golang.org/x/tools/go/ssa" ) -func analyzeRoutesCall( - ctx *context, - b configbuilder.HandlerBuilder, - call configurerCall, +func analyzeRoutes[T configbuilder.HandlerBuilder]( + ctx *configurerCallContext[T], ) { - for r := range resolveVariadic(b, call) { - b.Route(func(b *configbuilder.RouteBuilder) { - b.UpdateFidelity(call.Fidelity) - analyzeRoute(ctx, b, r) + for r := range resolveVariadic(ctx.Builder, ctx.Instruction) { + ctx.Builder.Route(func(b *configbuilder.RouteBuilder) { + b.UpdateFidelity(ctx.Fidelity) // TODO: is this correct? + analyzeRoute(ctx.context, b, r) }) } } diff --git a/config/staticconfig/type.go b/config/staticconfig/type.go index 42d0791..142bb3c 100644 --- a/config/staticconfig/type.go +++ b/config/staticconfig/type.go @@ -26,9 +26,9 @@ func isAbstract(t types.Type) bool { // analyzeType analyzes a type that was discovered within a package. // -// THe currently implementation only looks for [dogma.Application] -// implementations; handler implementations are ignored unless they are actually -// used within an application. +// The current implementation only looks for types that implement the +// [dogma.Application] interface. Handler implementations are ignored unless +// they are actually used within an application. func analyzeType(ctx *context, t types.Type) { if isAbstract(t) { // We're only interested in concrete types; otherwise there's nothing to diff --git a/config/staticconfig/varargs.go b/config/staticconfig/varargs.go index d241f57..72568f4 100644 --- a/config/staticconfig/varargs.go +++ b/config/staticconfig/varargs.go @@ -45,9 +45,11 @@ func isIndexOfArray( func resolveVariadic( b configbuilder.EntityBuilder, - call configurerCall, + inst ssa.CallInstruction, ) iter.Seq[ssa.Value] { return func(yield func(ssa.Value) bool) { + call := inst.Common() + variadics := call.Args[len(call.Args)-1] if ssax.IsZeroValue(variadics) { return @@ -60,11 +62,11 @@ func resolveVariadic( } for b := range ssax.WalkDown(array.Block()) { - if !ssax.PathExists(b, call.Instruction.Block()) { + if !ssax.PathExists(b, inst.Block()) { continue } - for inst := range ssax.InstructionsBefore(b, call.Instruction) { + for inst := range ssax.InstructionsBefore(b, inst) { switch inst := inst.(type) { case *ssa.Store: if _, ok := isIndexOfArray(array, inst.Addr); ok {