From c7bf3eb54d519ecfd89ff774842769c1d12d3daa Mon Sep 17 00:00:00 2001 From: James Harris Date: Tue, 15 Oct 2024 19:46:39 +1000 Subject: [PATCH 01/38] Add `staticconfig` package. --- config/staticconfig/analyze.go | 131 +++++++++++ config/staticconfig/analyze_test.go | 220 +++++++++++++++++ config/staticconfig/doc.go | 3 + config/staticconfig/dogma.go | 31 +++ config/staticconfig/static_test.go | 222 ++++++++++++++++++ .../testdata/_pending/aliased-handlers.md | 60 +++++ .../conditional-routes-in-handlers.md | 67 ++++++ .../testdata/_pending/const-value-ident.md | 30 +++ .../_pending/dynamic-routes-in-handlers.md | 58 +++++ .../testdata/_pending/empty-app.md | 20 ++ .../testdata/_pending/generic-handler.md | 69 ++++++ .../testdata/_pending/handler-adaptor-test.md | 58 +++++ .../_pending/handler-constructor-test.md | 59 +++++ .../testdata/_pending/handler-from-field.md | 46 ++++ .../testdata/_pending/iface-configurer.md | 33 +++ .../testdata/_pending/invalid-syntax.md | 22 ++ .../testdata/_pending/literal-value-ident.md | 22 ++ .../_pending/multiple-apps-in-single-pkg.md | 36 +++ .../_pending/multiple-handlers-of-a-kind.md | 94 ++++++++ .../testdata/_pending/nil-handlers.md | 25 ++ .../_pending/nil-routes-in-handlers.md | 50 ++++ ...pointer-handlers-registered-as-pointers.md | 58 +++++ .../testdata/_pending/pointer-receiver-app.md | 25 ++ .../unregistered-routes-in-handlers.md | 58 +++++ .../testdata/_pending/var-value-ident.md | 24 ++ config/staticconfig/testdata/no-apps.md | 24 ++ go.mod | 5 + go.sum | 38 +-- 28 files changed, 1554 insertions(+), 34 deletions(-) create mode 100644 config/staticconfig/analyze.go create mode 100644 config/staticconfig/analyze_test.go create mode 100644 config/staticconfig/doc.go create mode 100644 config/staticconfig/dogma.go create mode 100644 config/staticconfig/static_test.go create mode 100644 config/staticconfig/testdata/_pending/aliased-handlers.md create mode 100644 config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md create mode 100644 config/staticconfig/testdata/_pending/const-value-ident.md create mode 100644 config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md create mode 100644 config/staticconfig/testdata/_pending/empty-app.md create mode 100644 config/staticconfig/testdata/_pending/generic-handler.md create mode 100644 config/staticconfig/testdata/_pending/handler-adaptor-test.md create mode 100644 config/staticconfig/testdata/_pending/handler-constructor-test.md create mode 100644 config/staticconfig/testdata/_pending/handler-from-field.md create mode 100644 config/staticconfig/testdata/_pending/iface-configurer.md create mode 100644 config/staticconfig/testdata/_pending/invalid-syntax.md create mode 100644 config/staticconfig/testdata/_pending/literal-value-ident.md create mode 100644 config/staticconfig/testdata/_pending/multiple-apps-in-single-pkg.md create mode 100644 config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md create mode 100644 config/staticconfig/testdata/_pending/nil-handlers.md create mode 100644 config/staticconfig/testdata/_pending/nil-routes-in-handlers.md create mode 100644 config/staticconfig/testdata/_pending/non-pointer-handlers-registered-as-pointers.md create mode 100644 config/staticconfig/testdata/_pending/pointer-receiver-app.md create mode 100644 config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md create mode 100644 config/staticconfig/testdata/_pending/var-value-ident.md create mode 100644 config/staticconfig/testdata/no-apps.md diff --git a/config/staticconfig/analyze.go b/config/staticconfig/analyze.go new file mode 100644 index 00000000..72bbe652 --- /dev/null +++ b/config/staticconfig/analyze.go @@ -0,0 +1,131 @@ +package staticconfig + +import ( + "cmp" + "slices" + + "github.com/dogmatiq/enginekit/config" + "golang.org/x/tools/go/packages" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +// PackagesLoadMode is the minimal [packages.LoadMode] required when loading +// packages for analysis by [FromPackages]. +const PackagesLoadMode = packages.NeedFiles | + packages.NeedCompiledGoFiles | + packages.NeedImports | + packages.NeedTypes | + packages.NeedSyntax | + packages.NeedTypesInfo | + packages.NeedDeps + + // FromDir returns the configurations of the [dogma.Application] in the Go +// package at the given directory, and its subdirectories. +// +// The configurations are built by statically analyzing the code; it is never +// executed. As a result, the returned configurations may be invalid or +// incomplete. See [config.Fidelity]. +func FromDir(dir string) Analysis { + pkgs, err := packages.Load( + &packages.Config{ + Mode: PackagesLoadMode, + Dir: dir, + }, + "./...", + ) + if err != nil { + // According to the documentation of [packages.Load], this error relates + // only to malformed patterns, which should never occur since it's + // hardcoded above. + panic(err) + } + + return FromPackages(pkgs) +} + +// FromPackages returns the configurations of the [dogma.Application] in the +// given Go packages. +// +// The configurations are built by statically analyzing the code; it is never +// executed. As a result, the returned configurations may be invalid or +// incomplete. See [config.Fidelity]. +// +// The packages must have be loaded from source syntax using the [packages.Load] +// function using [PackagesLoadMode], at a minimum. +func FromPackages(pkgs []*packages.Package) Analysis { + ctx := &analysisContext{ + Analysis: Analysis{ + Packages: pkgs, + }, + } + + ctx.SSAProgram, ctx.SSAPackages = ssautil.AllPackages( + ctx.Packages, + 0, + // ssa.SanityCheckFunctions, // TODO: document why this is necessary + // see.InstantiateGenerics // TODO: might this make some generic handling code easier? + ) + + ctx.SSAProgram.Build() + + if !lookupDogmaPackage(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]. + return ctx.Analysis + } + + // for _, pkg := range ctx.SSAPackages { + // if pkg == nil { + // // Any [packages.Package] that can not be built results in a nil + // // [ssa.Package]. We ignore any such packages so that we can still + // // obtain information about applications from other valid packages. + // continue + // } + + // for _, m := range pkg.Members { + // // The sequence of the if-blocks below is important as a type + // // implements an interface only if the methods in the interface's + // // method set have non-pointer receivers. Hence the implementation + // // check for the non-pointer type is made first. + // // + // // A pointer to the type, on the other hand, implements the + // // interface regardless of whether pointer receivers are used or + // // not. + // if types.Implements(m.Type(), dogmaPkg.Application) { + // apps = append(apps, analyzeApplication(prog, dogmaPkg, m.Type())) + // continue + // } + + // if p := types.NewPointer(m.Type()); types.Implements(p, dogmaPkg.Application) { + // apps = append(apps, analyzeApplication(prog, dogmaPkg, p)) + // } + // } + // } + + slices.SortFunc( + ctx.Analysis.Applications, + func(a, b *config.Application) int { + return cmp.Compare( + a.String(), + b.String(), + ) + }, + ) + + return ctx.Analysis +} + +// Analysis encapsulates the results of static analysis. +type Analysis struct { + Applications []*config.Application + Packages []*packages.Package + SSAProgram *ssa.Program + SSAPackages []*ssa.Package +} + +type analysisContext struct { + Analysis + Dogma dogmaPkg +} diff --git a/config/staticconfig/analyze_test.go b/config/staticconfig/analyze_test.go new file mode 100644 index 00000000..3e5d639a --- /dev/null +++ b/config/staticconfig/analyze_test.go @@ -0,0 +1,220 @@ +package staticconfig_test + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/dogmatiq/aureus" + . "github.com/dogmatiq/enginekit/config/staticconfig" + "golang.org/x/tools/go/packages" + // . "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +func TestAnalyzer(t *testing.T) { + aureus.Run( + t, + func(w io.Writer, in aureus.Content, out aureus.ContentMetaData) error { + pkg := strings.TrimSuffix( + filepath.Base(in.File), + filepath.Ext(in.File), + ) + + // Make a temporary directory to write the Go source code. + // + // The name is based on the input file name rather than using a + // random temporary directory, otherwise the test output would be + // non-deterministic. + // + // Additionally, creating the directory within the repository allows + // the test code to use this repo's go.mod file, ensuring the + // statically analyzed code uses the same versions of Dogma, etc. + dir := filepath.Join( + filepath.Dir(in.File), + pkg, + ) + if err := os.MkdirAll(dir, 0700); err != nil { + return err + } + defer os.RemoveAll(dir) + + if err := os.WriteFile( + filepath.Join(dir, "main.go"), + []byte(in.Data), + 0600, + ); err != nil { + return err + } + + defer func() { + if e := recover(); e != nil { + if _, err := io.WriteString( + w, + e.(packages.Error).Msg+"\n", + ); err != nil { + panic(err) + } + } + }() + + result := FromDir(dir) + + if len(result.Applications) == 0 { + _, err := io.WriteString(w, "(no applications found)\n") + return err + } + + // noise := []string{ + // "github.com/dogmatiq/configkit/static/testdata/" + pkg + ".", + // "github.com/dogmatiq/enginekit/enginetest/stubs.", + // } + + // for i, app := range apps { + // s := configkit.ToString(app) + // for _, p := range noise { + // s = strings.ReplaceAll(s, p, "") + // } + + // if i > 0 { + // s = "\n" + s + // } + // if _, err := io.WriteString(w, s); err != nil { + // return err + // } + // } + + return nil + }, + ) + + // t.Run("should parse multiple packages contain applications", func(t *testing.T) { + // apps := FromDir("testdata/multiple-apps-in-pkgs") + + // if len(apps) != 2 { + // t.Fatalf("expected 2 applications, got %d", len(apps)) + // } + + // if expected, actual := "", + // apps[0].Identity().Name; expected != actual { + // t.Fatalf( + // "unexpected application name: want %s, got %s", + // expected, + // actual, + // ) + // } + + // if expected, actual := "b754902b-47c8-48fc-84d2-d920c9cbdaec", + // apps[0].Identity().Key; expected != actual { + // t.Fatalf( + // "unexpected application key: want %s, got %s", + // expected, + // actual, + // ) + // } + + // if expected, actual := "", + // apps[1].Identity().Name; expected != actual { + // t.Fatalf( + // "unexpected application name: want %s, got %s", + // expected, + // actual, + // ) + // } + + // if expected, actual := "bfaf2a16-23a0-495d-8098-051d77635822", + // apps[1].Identity().Key; expected != actual { + // t.Fatalf( + // "unexpected application key: want %s, got %s", + // expected, + // actual, + // ) + // } + // }) + + // t.Run("should parse all application-level messages", func(t *testing.T) { + // apps := FromDir("testdata/app-level-messages") + + // if len(apps) != 1 { + // t.Fatalf("expected 1 application, got %d", len(apps)) + // } + + // contains := func( + // mn message.Name, + // mk message.Kind, + // iterator iter.Seq2[message.Name, message.Kind], + // ) bool { + // for k, v := range iterator { + // if k == mn && v == mk { + // return true + // } + // } + // return false + // } + + // if !contains( + // message.NameFor[CommandStub[TypeA]](), + // message.CommandKind, + // apps[0].MessageNames().Consumed(), + // ) { + // t.Fatal("expected consumed TypeA command message") + // } + + // if !contains( + // message.NameFor[EventStub[TypeA]](), + // message.EventKind, + // apps[0].MessageNames().Consumed(), + // ) { + // t.Fatal("expected consumed TypeA event message") + // } + + // if !contains( + // message.NameFor[EventStub[TypeC]](), + // message.EventKind, + // apps[0].MessageNames().Consumed(), + // ) { + // t.Fatal("expected consumed TypeC event message") + // } + + // if !contains( + // message.NameFor[TimeoutStub[TypeA]](), + // message.TimeoutKind, + // apps[0].MessageNames().Consumed(), + // ) { + // t.Fatal("expected consumed TypeA timeout message") + // } + + // if !contains( + // message.NameFor[EventStub[TypeA]](), + // message.EventKind, + // apps[0].MessageNames().Produced(), + // ) { + // t.Fatal("expected produced TypeA event message") + // } + + // if !contains( + // message.NameFor[CommandStub[TypeB]](), + // message.CommandKind, + // apps[0].MessageNames().Produced(), + // ) { + // t.Fatal("expected produced TypeB command message") + // } + + // if !contains( + // message.NameFor[TimeoutStub[TypeA]](), + // message.TimeoutKind, + // apps[0].MessageNames().Produced(), + // ) { + // t.Fatal("expected produced TypeA timeout message") + // } + + // if !contains( + // message.NameFor[EventStub[TypeB]](), + // message.EventKind, + // apps[0].MessageNames().Produced(), + // ) { + // t.Fatal("expected produced TypeB event message") + // } + // }) +} diff --git a/config/staticconfig/doc.go b/config/staticconfig/doc.go new file mode 100644 index 00000000..9ad9c693 --- /dev/null +++ b/config/staticconfig/doc.go @@ -0,0 +1,3 @@ +// Package staticconfig builds configuration by statically analyzing codebases +// that contain [dogma.Application] implementations. +package staticconfig diff --git a/config/staticconfig/dogma.go b/config/staticconfig/dogma.go new file mode 100644 index 00000000..4d14fc60 --- /dev/null +++ b/config/staticconfig/dogma.go @@ -0,0 +1,31 @@ +package staticconfig + +import ( + "go/types" +) + +const ( + // dogmaPkgPath is the full path of dogma package. + dogmaPkgPath = "github.com/dogmatiq/dogma" +) + +// dogma encapsulates information about the dogma package. +type dogmaPkg struct { + Package *types.Package +} + +// lookupDogmaPackage returns information about the dogma package. +// +// It returns false if the Dogma package has not been imported. +func lookupDogmaPackage(ctx *analysisContext) bool { + pkg := ctx.SSAProgram.ImportedPackage(dogmaPkgPath) + if pkg == nil { + return false + } + + ctx.Dogma = dogmaPkg{ + Package: pkg.Pkg, + } + + return true +} diff --git a/config/staticconfig/static_test.go b/config/staticconfig/static_test.go new file mode 100644 index 00000000..6a479323 --- /dev/null +++ b/config/staticconfig/static_test.go @@ -0,0 +1,222 @@ +package staticconfig_test + +// import ( +// "io" +// "iter" +// "os" +// "path/filepath" +// "strings" +// "testing" + +// "github.com/dogmatiq/aureus" +// "github.com/dogmatiq/configkit" +// "github.com/dogmatiq/configkit/message" +// . "github.com/dogmatiq/enginekit/enginetest/stubs" +// "golang.org/x/tools/go/packages" +// ) + +// func TestFromPackages(t *testing.T) { +// aureus.Run( +// t, +// func(w io.Writer, in aureus.Content, out aureus.ContentMetaData) error { +// pkg := strings.TrimSuffix( +// filepath.Base(in.File), +// filepath.Ext(in.File), +// ) + +// // Make a temporary directory to write the Go source code. +// // +// // The name is based on the input file name rather than using a +// // random temporary directory, otherwise the test output would be +// // non-deterministic. +// // +// // Additionally, creating the directory within the repository allows +// // the test code to use this repo's go.mod file, ensuring the +// // statically analyzed code uses the same versions of Dogma, etc. +// dir := filepath.Join( +// filepath.Dir(in.File), +// pkg, +// ) +// if err := os.MkdirAll(dir, 0700); err != nil { +// return err +// } +// defer os.RemoveAll(dir) + +// if err := os.WriteFile( +// filepath.Join(dir, "main.go"), +// []byte(in.Data), +// 0600, +// ); err != nil { +// return err +// } + +// defer func() { +// if e := recover(); e != nil { +// if _, err := io.WriteString( +// w, +// e.(packages.Error).Msg+"\n", +// ); err != nil { +// panic(err) +// } +// } +// }() + +// apps := FromDir(dir) + +// if len(apps) == 0 { +// _, err := io.WriteString(w, "(no applications found)\n") +// return err +// } + +// noise := []string{ +// "github.com/dogmatiq/configkit/static/testdata/" + pkg + ".", +// "github.com/dogmatiq/enginekit/enginetest/stubs.", +// } + +// for i, app := range apps { +// s := configkit.ToString(app) +// for _, p := range noise { +// s = strings.ReplaceAll(s, p, "") +// } + +// if i > 0 { +// s = "\n" + s +// } +// if _, err := io.WriteString(w, s); err != nil { +// return err +// } +// } + +// return nil +// }, +// ) + +// t.Run("should parse multiple packages contain applications", func(t *testing.T) { +// apps := FromDir("testdata/multiple-apps-in-pkgs") + +// if len(apps) != 2 { +// t.Fatalf("expected 2 applications, got %d", len(apps)) +// } + +// if expected, actual := "", +// apps[0].Identity().Name; expected != actual { +// t.Fatalf( +// "unexpected application name: want %s, got %s", +// expected, +// actual, +// ) +// } + +// if expected, actual := "b754902b-47c8-48fc-84d2-d920c9cbdaec", +// apps[0].Identity().Key; expected != actual { +// t.Fatalf( +// "unexpected application key: want %s, got %s", +// expected, +// actual, +// ) +// } + +// if expected, actual := "", +// apps[1].Identity().Name; expected != actual { +// t.Fatalf( +// "unexpected application name: want %s, got %s", +// expected, +// actual, +// ) +// } + +// if expected, actual := "bfaf2a16-23a0-495d-8098-051d77635822", +// apps[1].Identity().Key; expected != actual { +// t.Fatalf( +// "unexpected application key: want %s, got %s", +// expected, +// actual, +// ) +// } +// }) + +// t.Run("should parse all application-level messages", func(t *testing.T) { +// apps := FromDir("testdata/app-level-messages") + +// if len(apps) != 1 { +// t.Fatalf("expected 1 application, got %d", len(apps)) +// } + +// contains := func( +// mn message.Name, +// mk message.Kind, +// iterator iter.Seq2[message.Name, message.Kind], +// ) bool { +// for k, v := range iterator { +// if k == mn && v == mk { +// return true +// } +// } +// return false +// } + +// if !contains( +// message.NameFor[CommandStub[TypeA]](), +// message.CommandKind, +// apps[0].MessageNames().Consumed(), +// ) { +// t.Fatal("expected consumed TypeA command message") +// } + +// if !contains( +// message.NameFor[EventStub[TypeA]](), +// message.EventKind, +// apps[0].MessageNames().Consumed(), +// ) { +// t.Fatal("expected consumed TypeA event message") +// } + +// if !contains( +// message.NameFor[EventStub[TypeC]](), +// message.EventKind, +// apps[0].MessageNames().Consumed(), +// ) { +// t.Fatal("expected consumed TypeC event message") +// } + +// if !contains( +// message.NameFor[TimeoutStub[TypeA]](), +// message.TimeoutKind, +// apps[0].MessageNames().Consumed(), +// ) { +// t.Fatal("expected consumed TypeA timeout message") +// } + +// if !contains( +// message.NameFor[EventStub[TypeA]](), +// message.EventKind, +// apps[0].MessageNames().Produced(), +// ) { +// t.Fatal("expected produced TypeA event message") +// } + +// if !contains( +// message.NameFor[CommandStub[TypeB]](), +// message.CommandKind, +// apps[0].MessageNames().Produced(), +// ) { +// t.Fatal("expected produced TypeB command message") +// } + +// if !contains( +// message.NameFor[TimeoutStub[TypeA]](), +// message.TimeoutKind, +// apps[0].MessageNames().Produced(), +// ) { +// t.Fatal("expected produced TypeA timeout message") +// } + +// if !contains( +// message.NameFor[EventStub[TypeB]](), +// message.EventKind, +// apps[0].MessageNames().Produced(), +// ) { +// t.Fatal("expected produced TypeB event message") +// } +// }) +// } diff --git a/config/staticconfig/testdata/_pending/aliased-handlers.md b/config/staticconfig/testdata/_pending/aliased-handlers.md new file mode 100644 index 00000000..fe777c05 --- /dev/null +++ b/config/staticconfig/testdata/_pending/aliased-handlers.md @@ -0,0 +1,60 @@ +# Type Aliased Handlers + +This test verifies that static analysis can correctly parse handlers that are +declared as type aliases. + +```go au:input +package app + +import ( + "context" + . "github.com/dogmatiq/dogma" + "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +type ( + // IntegrationHandlerAlias is a test type alias of IntegrationHandler. + IntegrationHandlerAlias = IntegrationHandler +) + +// App implements Application interface. +type App struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (App) Configure(c ApplicationConfigurer) { + c.Identity("", "1b828a1c-eba1-4e4c-88b8-e49f78ad15c7") + + c.RegisterIntegration(IntegrationHandlerAlias{}) +} + +// IntegrationHandler is a test implementation of +// IntegrationMessageHandler. +type IntegrationHandler struct{} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (IntegrationHandler) Configure(c IntegrationConfigurer) { + c.Identity("", "4d8cd3f5-21dc-475b-a8dc-80138adde3f2") + + c.Routes( + HandlesCommand[stubs.CommandStub[stubs.TypeB]](), + ) +} + +// HandleCommand handles a command message that has been routed to this handler. +func (IntegrationHandler) HandleCommand( + context.Context, + IntegrationCommandScope, + Command, +) error { + return nil +} +``` + +```au:output +application (1b828a1c-eba1-4e4c-88b8-e49f78ad15c7) App + + - integration (4d8cd3f5-21dc-475b-a8dc-80138adde3f2) IntegrationHandlerAlias + handles CommandStub[TypeB]? +``` diff --git a/config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md b/config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md new file mode 100644 index 00000000..2a95694c --- /dev/null +++ b/config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md @@ -0,0 +1,67 @@ +# Conditional Routes in Dogma Application Handlers + +This test verifies that static analysis correctly parses handles that have +conditional routes within their bodies. + +```go au:input +package app + +import ( + "context" + "math/rand" + . "github.com/dogmatiq/dogma" + "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +// App implements Application interface. +type App struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (App) Configure(c ApplicationConfigurer) { + c.Identity("", "7e34538e-c407-4af8-8d3c-960e09cde98a") + c.RegisterIntegration(IntegrationHandler{}) +} + +// IntegrationHandler is a test implementation of +// IntegrationMessageHandler. +type IntegrationHandler struct{} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (IntegrationHandler) Configure(c IntegrationConfigurer) { + c.Identity("", "92cce461-8d30-409b-8d5a-406f656cef2d") + + if rand.Int() == 0 { + c.Routes( + HandlesCommand[stubs.CommandStub[stubs.TypeA]](), + RecordsEvent[stubs.EventStub[stubs.TypeA]](), + ) + } else { + c.Routes( + HandlesCommand[stubs.CommandStub[stubs.TypeB]](), + RecordsEvent[stubs.EventStub[stubs.TypeB]](), + ) + } +} + +// HandleCommand handles a command message that has been routed to this handler. +func (IntegrationHandler) HandleCommand( + context.Context, + IntegrationCommandScope, + Command, +) error { + return nil +} + +``` + +```au:output +application (7e34538e-c407-4af8-8d3c-960e09cde98a) App + + - integration (92cce461-8d30-409b-8d5a-406f656cef2d) IntegrationHandler + handles CommandStub[TypeA]? + handles CommandStub[TypeB]? + records EventStub[TypeA]! + records EventStub[TypeB]! +``` diff --git a/config/staticconfig/testdata/_pending/const-value-ident.md b/config/staticconfig/testdata/_pending/const-value-ident.md new file mode 100644 index 00000000..8ca40464 --- /dev/null +++ b/config/staticconfig/testdata/_pending/const-value-ident.md @@ -0,0 +1,30 @@ +# No applications + +This test verifies that the identity specified with constants is correctly +parsed. + +```go au:input +package app + +import . "github.com/dogmatiq/dogma" + +const ( + // AppName is the application name. + AppName = "" + // AppKey is the application key. + AppKey = "04e12cf2-3c66-4414-9203-e045ddbe02c7" +) + +// App implements Application interface. +type App struct{} + +// Configure sets the application identity using non-literal constant +// expressions. +func (App) Configure(c ApplicationConfigurer) { + c.Identity(AppName, AppKey) +} +``` + +```au:output +application (04e12cf2-3c66-4414-9203-e045ddbe02c7) App +``` diff --git a/config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md b/config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md new file mode 100644 index 00000000..77421c05 --- /dev/null +++ b/config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md @@ -0,0 +1,58 @@ +# Dynamic routes inside Dogma Application Handlers + +This test verifies that static analysis correctly parses routes in handles that +are dynamically populated. + +```go au:input +package app + +import ( + "context" + . "github.com/dogmatiq/dogma" + "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +// App implements Application interface. +type App struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (App) Configure(c ApplicationConfigurer) { + c.Identity("", "3bc3849b-abe0-4c4e-9db4-e48dc28c9a26") + c.RegisterIntegration(IntegrationHandler{}) +} + +// IntegrationHandler is a test implementation of +// IntegrationMessageHandler. +type IntegrationHandler struct{} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (IntegrationHandler) Configure(c IntegrationConfigurer) { + c.Identity("", "3a06b7da-1079-4e4b-a6a6-064c62241918") + + routes := []IntegrationRoute{ + HandlesCommand[stubs.CommandStub[stubs.TypeA]](), + RecordsEvent[stubs.EventStub[stubs.TypeA]](), + } + + c.Routes(routes...) +} + +// HandleCommand handles a command message that has been routed to this handler. +func (IntegrationHandler) HandleCommand( + context.Context, + IntegrationCommandScope, + Command, +) error { + return nil +} +``` + +```au:output +application (3bc3849b-abe0-4c4e-9db4-e48dc28c9a26) App + + - integration (3a06b7da-1079-4e4b-a6a6-064c62241918) IntegrationHandler + handles CommandStub[TypeA]? + records EventStub[TypeA]! +``` diff --git a/config/staticconfig/testdata/_pending/empty-app.md b/config/staticconfig/testdata/_pending/empty-app.md new file mode 100644 index 00000000..75433849 --- /dev/null +++ b/config/staticconfig/testdata/_pending/empty-app.md @@ -0,0 +1,20 @@ +# Empty application + +This test ensures that the static analyzer includes Dogma applications that have +no handlers. + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("", "8a6baab1-ee64-402e-a081-e43f4bebc243") +} +``` + +```au:output +application (8a6baab1-ee64-402e-a081-e43f4bebc243) App +``` diff --git a/config/staticconfig/testdata/_pending/generic-handler.md b/config/staticconfig/testdata/_pending/generic-handler.md new file mode 100644 index 00000000..a8cc8b03 --- /dev/null +++ b/config/staticconfig/testdata/_pending/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) *InstantiatedIntegration + handles CommandStub[TypeA]? +``` diff --git a/config/staticconfig/testdata/_pending/handler-adaptor-test.md b/config/staticconfig/testdata/_pending/handler-adaptor-test.md new file mode 100644 index 00000000..94c197fe --- /dev/null +++ b/config/staticconfig/testdata/_pending/handler-adaptor-test.md @@ -0,0 +1,58 @@ +# Handler Adaptors + +This test verifies that static analysis correctly parses handler adaptors. + +```go au:input +package app + +import ( + . "github.com/dogmatiq/dogma" + "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +// App implements Application interface. +type App struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (App) Configure(c ApplicationConfigurer) { + c.Identity("", "f610eae4-f5d0-4eea-a9c9-6cbbfa9b2060") + + c.RegisterIntegration(AdaptIntegration(IntegrationHandler{})) +} + +// IntegrationHandler is the type that provides the handler logic, but is not +// itself an implementation of IntegrationMessageHandler. +type IntegrationHandler struct{} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (IntegrationHandler) Configure(c IntegrationConfigurer) { + c.Identity("", "099b5b8d-9e04-422f-bcc3-bb0d451158c7") + + c.Routes( + HandlesCommand[stubs.CommandStub[stubs.TypeI]](), + RecordsEvent[stubs.EventStub[stubs.TypeI]](), + ) +} + +// PartialIntegrationMessageHandler is the subset of +// IntegrationMessageHandler that must be implemented for a type to be +// detected as a concrete implementation. +type PartialIntegrationMessageHandler interface { + Configure(c IntegrationConfigurer) +} + +// AdaptIntegration adapts the argument to the IntegrationMessageHandler interface. +func AdaptIntegration(PartialIntegrationMessageHandler) IntegrationMessageHandler { + panic("the implementation of this function is irrelevant to the analyzer") +} +``` + +```au:output +application (f610eae4-f5d0-4eea-a9c9-6cbbfa9b2060) App + + - integration (099b5b8d-9e04-422f-bcc3-bb0d451158c7) IntegrationHandler + handles CommandStub[TypeI]? + records EventStub[TypeI]! +``` diff --git a/config/staticconfig/testdata/_pending/handler-constructor-test.md b/config/staticconfig/testdata/_pending/handler-constructor-test.md new file mode 100644 index 00000000..b129a07e --- /dev/null +++ b/config/staticconfig/testdata/_pending/handler-constructor-test.md @@ -0,0 +1,59 @@ +# Handler constructors + +This test verifies that static analysis correctly parses handler constructors. + +```go au:input +package app + +import ( + "context" + . "github.com/dogmatiq/dogma" + "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +// App implements Application interface. +type App struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (App) Configure(c ApplicationConfigurer) { + c.Identity("", "3bc3849b-abe0-4c4e-9db4-e48dc28c9a26") + + c.RegisterIntegration(NewIntegrationHandler()) +} + +// IntegrationHandler is a test implementation of +// IntegrationMessageHandler. +type IntegrationHandler struct{} + +// NewIntegrationHandler returns a new IntegrationHandler. +func NewIntegrationHandler() IntegrationHandler { + panic("the implementation of this function is irrelevant to the analyzer") +} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (IntegrationHandler) Configure(c IntegrationConfigurer) { + c.Identity("", "099b5b8d-9e04-422f-bcc3-bb0d451158c7") + + c.Routes( + HandlesCommand[stubs.CommandStub[stubs.TypeB]](), + ) +} + +// HandleCommand handles a command message that has been routed to this handler. +func (IntegrationHandler) HandleCommand( + context.Context, + IntegrationCommandScope, + Command, +) error { + return nil +} +``` + +```au:output +application (3bc3849b-abe0-4c4e-9db4-e48dc28c9a26) App + + - integration (099b5b8d-9e04-422f-bcc3-bb0d451158c7) IntegrationHandler + handles CommandStub[TypeB]? +``` diff --git a/config/staticconfig/testdata/_pending/handler-from-field.md b/config/staticconfig/testdata/_pending/handler-from-field.md new file mode 100644 index 00000000..f6e0f0d1 --- /dev/null +++ b/config/staticconfig/testdata/_pending/handler-from-field.md @@ -0,0 +1,46 @@ +# Handler from struct field + +This test ensures that the static analyzer can recognized the type of a handler +when it is registered using the value of a struct field, rather than constructed +inline. + +```go au:input +package app + +import ( + "context" + . "github.com/dogmatiq/dogma" + . "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +type App struct { + Field Handler +} + +func (a App) Configure(c ApplicationConfigurer) { + c.Identity("", "7468a57f-20f0-4d11-9aad-48fcd553a908") + c.RegisterIntegration(a.Field) +} + +type Handler struct{} + +func (Handler) Configure(c IntegrationConfigurer) { + c.Identity("", "195ede4a-3f26-4d19-a8fe-41b2a5f92d06") + c.Routes(HandlesCommand[CommandStub[TypeA]]()) +} + +func (Handler) HandleCommand( + context.Context, + IntegrationCommandScope, + Command, +) error{ + return nil +} +``` + +```au:output +application (7468a57f-20f0-4d11-9aad-48fcd553a908) App + + - integration (195ede4a-3f26-4d19-a8fe-41b2a5f92d06) Handler + handles CommandStub[TypeA]? +``` diff --git a/config/staticconfig/testdata/_pending/iface-configurer.md b/config/staticconfig/testdata/_pending/iface-configurer.md new file mode 100644 index 00000000..ea0a5133 --- /dev/null +++ b/config/staticconfig/testdata/_pending/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 +``` diff --git a/config/staticconfig/testdata/_pending/invalid-syntax.md b/config/staticconfig/testdata/_pending/invalid-syntax.md new file mode 100644 index 00000000..c1e224dd --- /dev/null +++ b/config/staticconfig/testdata/_pending/invalid-syntax.md @@ -0,0 +1,22 @@ +# Type Aliased Handlers + +This test verifies that static analysis panics when it encounters incorrect +syntax. + +```go au:input +package app + +// Even though this file has invalid syntax the import statements are still +// parsed. This import necessary so that the test still considers it a +// possibility that this package has valid Dogma application implementations. +import "github.com/dogmatiq/dogma" + +// Below is the deliberate illegal Go syntax to test loading of the packages +// with errors. + + +``` + +```au:output +expected declaration, found '<' +``` diff --git a/config/staticconfig/testdata/_pending/literal-value-ident.md b/config/staticconfig/testdata/_pending/literal-value-ident.md new file mode 100644 index 00000000..8d6fc0c5 --- /dev/null +++ b/config/staticconfig/testdata/_pending/literal-value-ident.md @@ -0,0 +1,22 @@ +# No applications + +This test verifies that the identity specified with string literals is correctly +parsed. + +```go au:input +package app + +import . "github.com/dogmatiq/dogma" + +// App implements Application interface. +type App struct{} + +// Configure sets the application identity using literal string values. +func (App) Configure(c ApplicationConfigurer) { + c.Identity("", "9d0af85d-f506-4742-b676-ce87730bb1a0") +} +``` + +```au:output +application (9d0af85d-f506-4742-b676-ce87730bb1a0) App +``` diff --git a/config/staticconfig/testdata/_pending/multiple-apps-in-single-pkg.md b/config/staticconfig/testdata/_pending/multiple-apps-in-single-pkg.md new file mode 100644 index 00000000..114a5731 --- /dev/null +++ b/config/staticconfig/testdata/_pending/multiple-apps-in-single-pkg.md @@ -0,0 +1,36 @@ +# Multiple Dogma Apps in a Single Package + +This test verifies that static analysis correctly parses multiple Dogma +applications in a single Go package. + +```go au:input +package app + +import ( + . "github.com/dogmatiq/dogma" +) + +// AppFirst implements Application interface. +type AppFirst struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (AppFirst) Configure(c ApplicationConfigurer) { + c.Identity("", "4fec74a1-6ed4-46f4-8417-01e0910be8f1") +} + +// AppSecond implements Application interface. +type AppSecond struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (a AppSecond) Configure(c ApplicationConfigurer) { + c.Identity("", "6e97d403-3cb8-4a59-a7ec-74e8e219a7bc") +} +``` + +```au:output +application (4fec74a1-6ed4-46f4-8417-01e0910be8f1) AppFirst + +application (6e97d403-3cb8-4a59-a7ec-74e8e219a7bc) AppSecond +``` diff --git a/config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md b/config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md new file mode 100644 index 00000000..144b53cd --- /dev/null +++ b/config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md @@ -0,0 +1,94 @@ +# Multiple handlers of a same kind + +This test verifies that static analysis can correctly parse multiple handlers of +a same kind. + +```go au:input +package app + +import ( + "context" + . "github.com/dogmatiq/dogma" + "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +// App implements Application interface. +type App struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (App) Configure(c ApplicationConfigurer) { + c.Identity("", "8961f548-1afc-4996-894c-956835c83199") + + c.RegisterIntegration(FirstIntegrationHandler{}) + c.RegisterIntegration(SecondIntegrationHandler{}) +} + +// FirstIntegrationHandler is a test implementation of +// IntegrationMessageHandler. +type FirstIntegrationHandler struct{} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (FirstIntegrationHandler) Configure(c IntegrationConfigurer) { + c.Identity("", "14cf2812-eead-43b3-9c9c-10db5b469e94") + + c.Routes( + HandlesCommand[stubs.CommandStub[stubs.TypeC]](), + ) +} + +// RouteCommandToInstance returns the ID of the integration instance that is +// targetted by m. +func (FirstIntegrationHandler) RouteCommandToInstance(Command) string { + return "" +} + +// HandleCommand handles a command message that has been routed to this handler. +func (FirstIntegrationHandler) HandleCommand( + context.Context, + IntegrationCommandScope, + Command, +) error { + return nil +} + +// SecondIntegrationHandler is a test implementation of +// IntegrationMessageHandler. +type SecondIntegrationHandler struct{} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (SecondIntegrationHandler) Configure(c IntegrationConfigurer) { + c.Identity("", "6bed3fbc-30e2-44c7-9a5b-e440ffe370d9") + + c.Routes( + HandlesCommand[stubs.CommandStub[stubs.TypeD]](), + ) +} + +// RouteCommandToInstance returns the ID of the integration instance that is +// targetted by m. +func (SecondIntegrationHandler) RouteCommandToInstance(Command) string { + return "" +} + +// HandleCommand handles a command message that has been routed to this handler. +func (SecondIntegrationHandler) HandleCommand( + context.Context, + IntegrationCommandScope, + Command, +) error { + return nil +} +``` + +```au:output +application (8961f548-1afc-4996-894c-956835c83199) App + + - integration (14cf2812-eead-43b3-9c9c-10db5b469e94) FirstIntegrationHandler + handles CommandStub[TypeC]? + + - integration (6bed3fbc-30e2-44c7-9a5b-e440ffe370d9) SecondIntegrationHandler + handles CommandStub[TypeD]? +``` diff --git a/config/staticconfig/testdata/_pending/nil-handlers.md b/config/staticconfig/testdata/_pending/nil-handlers.md new file mode 100644 index 00000000..e69821d5 --- /dev/null +++ b/config/staticconfig/testdata/_pending/nil-handlers.md @@ -0,0 +1,25 @@ +# Nil-valued handlers + +This test ensures that the static analyzer ignores handlers that are `nil`, but +still includes the application itself. + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("", "0726ae0d-67e4-4a50-8a19-9f58eae38e51") + + c.RegisterAggregate(nil) + c.RegisterProcess(nil) + c.RegisterProjection(nil) + c.RegisterIntegration(nil) +} +``` + +```au:output +application (0726ae0d-67e4-4a50-8a19-9f58eae38e51) App +``` diff --git a/config/staticconfig/testdata/_pending/nil-routes-in-handlers.md b/config/staticconfig/testdata/_pending/nil-routes-in-handlers.md new file mode 100644 index 00000000..8dcc7461 --- /dev/null +++ b/config/staticconfig/testdata/_pending/nil-routes-in-handlers.md @@ -0,0 +1,50 @@ +# Nil Routes in Dogma Application Handlers + +This test verifies that static analysis correctly parses `nil` routes inside +Dogma Application handlers. + +```go au:input +package app + +import ( + "context" + . "github.com/dogmatiq/dogma" +) + +// App implements Application interface. +type App struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (App) Configure(c ApplicationConfigurer) { + c.Identity("", "c100edcc-6dcc-42ed-ac75-69eecb3d0ec4") + c.RegisterIntegration(IntegrationHandler{}) +} + + +// IntegrationHandler is a test implementation of +// IntegrationMessageHandler. +type IntegrationHandler struct{} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (IntegrationHandler) Configure(c IntegrationConfigurer) { + c.Identity("", "363039e5-2938-4b2c-9bec-dcb29dee2da1") + c.Routes(nil) +} + +// HandleCommand handles a command message that has been routed to this handler. +func (IntegrationHandler) HandleCommand( + context.Context, + IntegrationCommandScope, + Command, +) error { + return nil +} +``` + +```au:output +application (c100edcc-6dcc-42ed-ac75-69eecb3d0ec4) App + + - integration (363039e5-2938-4b2c-9bec-dcb29dee2da1) IntegrationHandler +``` diff --git a/config/staticconfig/testdata/_pending/non-pointer-handlers-registered-as-pointers.md b/config/staticconfig/testdata/_pending/non-pointer-handlers-registered-as-pointers.md new file mode 100644 index 00000000..4cf1dc6c --- /dev/null +++ b/config/staticconfig/testdata/_pending/non-pointer-handlers-registered-as-pointers.md @@ -0,0 +1,58 @@ +# Non-pointer Handlers Registered in a Dogma Application as Pointers. + +This test verifies that static analysis can correctly parse non-pointer handlers +registered in a dogma application as pointers using 'address-of' operator. + +```go au:input +package app + +import ( + "context" + . "github.com/dogmatiq/dogma" + "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +// App implements Application interface. +type App struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (App) Configure(c ApplicationConfigurer) { + c.Identity( + "", + "282653ad-9343-44f1-889e-a8b2b095b54b", + ) + + c.RegisterIntegration(&IntegrationHandler{}) +} + +// IntegrationHandler is a test implementation of +// IntegrationMessageHandler. +type IntegrationHandler struct{} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (IntegrationHandler) Configure(c IntegrationConfigurer) { + c.Identity("", "1425ca64-0448-4bfd-b18d-9fe63a95995f") + + c.Routes( + HandlesCommand[stubs.CommandStub[stubs.TypeB]](), + ) +} + +// HandleCommand handles a command message that has been routed to this handler. +func (IntegrationHandler) HandleCommand( + context.Context, + IntegrationCommandScope, + Command, +) error { + return nil +} +``` + +```au:output +application (282653ad-9343-44f1-889e-a8b2b095b54b) App + + - integration (1425ca64-0448-4bfd-b18d-9fe63a95995f) *IntegrationHandler + handles CommandStub[TypeB]? +``` diff --git a/config/staticconfig/testdata/_pending/pointer-receiver-app.md b/config/staticconfig/testdata/_pending/pointer-receiver-app.md new file mode 100644 index 00000000..fa8d263a --- /dev/null +++ b/config/staticconfig/testdata/_pending/pointer-receiver-app.md @@ -0,0 +1,25 @@ +# Pointer-Receiver Dogma App + +This test verifies that static analysis correctly parses Dogma applications +declared with pointer-receiver methods. + +```go au:input +package app + +import ( + . "github.com/dogmatiq/dogma" +) + +// App implements Application interface. +type App struct{} + +// Configure is implemented using a pointer receiver, such that the *App +// implements Application, and not App itself. +func (a *App) Configure(c ApplicationConfigurer) { + c.Identity("", "b754902b-47c8-48fc-84d2-d920c9cbdaec") +} +``` + +```au:output +application (b754902b-47c8-48fc-84d2-d920c9cbdaec) *App +``` diff --git a/config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md b/config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md new file mode 100644 index 00000000..459e8b03 --- /dev/null +++ b/config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md @@ -0,0 +1,58 @@ +# Unregistered Routes in Dogma Application Handlers + +This test verifies that static analysis ignores unregistered routes in Dogma +application handlers. + +```go au:input +package app + +import ( + "context" + . "github.com/dogmatiq/dogma" + "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +// App implements Application interface. +type App struct{} + +// Configure configures the behavior of the engine as it relates to this +// application. +func (App) Configure(c ApplicationConfigurer) { + c.Identity("", "f2c08525-623e-4c76-851c-3172953269e3") + c.RegisterIntegration(IntegrationHandler{}) +} + +// IntegrationHandler is a test implementation of +// IntegrationMessageHandler. +type IntegrationHandler struct{} + +// Configure configures the behavior of the engine as it relates to this +// handler. +func (IntegrationHandler) Configure(c IntegrationConfigurer) { + c.Identity("", "ac391765-da58-4e7c-a478-e4725eb2b0e9") + + // Create a route that is never passed to c.Routes(). + HandlesCommand[stubs.CommandStub[stubs.TypeX]]() + + // Ensure there is still _some_ call to Routes(). + c.Routes( + HandlesCommand[stubs.CommandStub[stubs.TypeA]](), + ) +} + +// HandleCommand handles a command message that has been routed to this handler. +func (IntegrationHandler) HandleCommand( + context.Context, + IntegrationCommandScope, + Command, +) error { + return nil +} +``` + +```au:output +application (f2c08525-623e-4c76-851c-3172953269e3) App + + - integration (ac391765-da58-4e7c-a478-e4725eb2b0e9) IntegrationHandler + handles CommandStub[TypeA]? +``` diff --git a/config/staticconfig/testdata/_pending/var-value-ident.md b/config/staticconfig/testdata/_pending/var-value-ident.md new file mode 100644 index 00000000..23ba596f --- /dev/null +++ b/config/staticconfig/testdata/_pending/var-value-ident.md @@ -0,0 +1,24 @@ +# No applications + +This test verifies that the identity specified with variables is correctly parsed. + +```go au:input +package app + +import . "github.com/dogmatiq/dogma" + +// App implements Application interface. +type App struct { + Name string + Key string +} + +// Configure sets the application identity using non-constant expressions. +func (a App) Configure(c ApplicationConfigurer) { + c.Identity(a.Name, a.Name) +} +``` + +```au:output +application () App +``` diff --git a/config/staticconfig/testdata/no-apps.md b/config/staticconfig/testdata/no-apps.md new file mode 100644 index 00000000..db8f9852 --- /dev/null +++ b/config/staticconfig/testdata/no-apps.md @@ -0,0 +1,24 @@ +# No applications + +This test ensures that static analyzer does not fail when the code does not +contain any Dogma applications. + +```go au:input +package app + +// App looks a lot like a [dogma.Application], but does not actually +// implement the Dogma interface. +type App struct{} + +func (a App) Configure(c ApplicationConfigurer) { + c.Identity("name", "ee6ca834-34a3-4e59-8c36-7aeb796401d7") +} + +type ApplicationConfigurer interface { + Identity(name, key string) +} +``` + +```au:output +(no applications found) +``` diff --git a/go.mod b/go.mod index 97dfe1a3..d3e4c886 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,12 @@ module github.com/dogmatiq/enginekit go 1.23 require ( + github.com/dogmatiq/aureus v0.1.0 github.com/dogmatiq/dapper v0.6.0 github.com/dogmatiq/dogma v0.15.0 github.com/dogmatiq/primo v0.3.1 github.com/google/go-cmp v0.6.0 + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 pgregory.net/rapid v1.1.0 @@ -14,7 +16,10 @@ require ( require ( github.com/dogmatiq/jumble v0.1.0 // indirect + github.com/yuin/goldmark v1.7.0 // indirect + golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect diff --git a/go.sum b/go.sum index d673679f..0c1344f6 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,5 @@ -cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y= -cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg= -github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= -github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= +github.com/dogmatiq/aureus v0.1.0 h1:BIUF1G4pdCiJ+WQ6GnTmbhaejbjtX35Z9w2somdgslA= +github.com/dogmatiq/aureus v0.1.0/go.mod h1:eTm6/WDfVI2tNjg1WCXiPt4fqjMhjO2kNM522ENa6mM= github.com/dogmatiq/dapper v0.6.0 h1:hnWUsjnt3nUiC9hmkPvuxrnMd7fYNz1i+/GS3gOx0Xs= github.com/dogmatiq/dapper v0.6.0/go.mod h1:ubRHWzt73s0MsPpGhWvnfW/Z/1YPnrkCsQv6CUOZVEw= github.com/dogmatiq/dogma v0.15.0 h1:aXOTd2K4wLvlwHc1D9OsFREp0BusNJ9o9KssxURftmg= @@ -18,42 +8,22 @@ github.com/dogmatiq/jumble v0.1.0 h1:Cb3ExfxY+AoUP4G9/sOwoOdYX8o+kOLK8+dhXAry+QA github.com/dogmatiq/jumble v0.1.0/go.mod h1:FCGV2ImXu8zvThxhd4QLstiEdu74vbIVw9bFJSBcKr4= github.com/dogmatiq/primo v0.3.1 h1:JSqiCh1ma9CbIVzPf8k1vhzQ2Zn/d/WupzElDoiYZw0= github.com/dogmatiq/primo v0.3.1/go.mod h1:z2DfWNz0YmwIKhUEwgJY4xyeWOw0He+9veRRMGQ21UI= -github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= -github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= -github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= -github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= -github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= -github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= +github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= -google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= From 5d0d4474efecfd238630585ad4ad50263a192c2f Mon Sep 17 00:00:00 2001 From: James Harris Date: Fri, 18 Oct 2024 06:38:08 +1000 Subject: [PATCH 02/38] Add support for static analysis of identities. --- config/staticconfig/analyze.go | 179 ++++++++++++------ config/staticconfig/analyze_test.go | 71 +++---- config/staticconfig/application.go | 81 ++++++++ config/staticconfig/context.go | 54 ++++++ config/staticconfig/dogma.go | 31 --- config/staticconfig/entity.go | 151 +++++++++++++++ .../testdata/_pending/const-value-ident.md | 30 --- .../testdata/_pending/empty-app.md | 20 -- .../_pending/multiple-apps-in-single-pkg.md | 36 ---- .../testdata/_pending/pointer-receiver-app.md | 45 +++-- .../testdata/_pending/var-value-ident.md | 24 --- .../testdata/identity-from-const.md | 26 +++ .../testdata/identity-from-var.md | 24 +++ config/staticconfig/testdata/multiple-apps.md | 31 +++ config/staticconfig/testdata/no-apps.md | 36 +++- config/staticconfig/testdata/no-handlers.md | 21 ++ go.sum | 36 ++++ 17 files changed, 631 insertions(+), 265 deletions(-) create mode 100644 config/staticconfig/application.go create mode 100644 config/staticconfig/context.go delete mode 100644 config/staticconfig/dogma.go create mode 100644 config/staticconfig/entity.go delete mode 100644 config/staticconfig/testdata/_pending/const-value-ident.md delete mode 100644 config/staticconfig/testdata/_pending/empty-app.md delete mode 100644 config/staticconfig/testdata/_pending/multiple-apps-in-single-pkg.md delete mode 100644 config/staticconfig/testdata/_pending/var-value-ident.md create mode 100644 config/staticconfig/testdata/identity-from-const.md create mode 100644 config/staticconfig/testdata/identity-from-var.md create mode 100644 config/staticconfig/testdata/multiple-apps.md create mode 100644 config/staticconfig/testdata/no-handlers.md diff --git a/config/staticconfig/analyze.go b/config/staticconfig/analyze.go index 72bbe652..2cffe2f7 100644 --- a/config/staticconfig/analyze.go +++ b/config/staticconfig/analyze.go @@ -2,6 +2,9 @@ package staticconfig import ( "cmp" + "fmt" + "go/types" + "iter" "slices" "github.com/dogmatiq/enginekit/config" @@ -10,8 +13,33 @@ import ( "golang.org/x/tools/go/ssa/ssautil" ) +// Analysis encapsulates the results of static analysis. +type Analysis struct { + Applications []*config.Application + + Artifacts struct { + Packages []*packages.Package + SSAProgram *ssa.Program + SSAPackages []*ssa.Package + } +} + +// Errors returns a sequence of errors that occurred during analysis, not +// including errors with the Dogma configuration itself. +func (a Analysis) Errors() iter.Seq[error] { + return func(yield func(error) bool) { + for _, pkg := range a.Artifacts.Packages { + for _, err := range pkg.Errors { + if !yield(err) { + return + } + } + } + } +} + // PackagesLoadMode is the minimal [packages.LoadMode] required when loading -// packages for analysis by [FromPackages]. +// packages for analysis by [Analyze]. const PackagesLoadMode = packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | @@ -20,13 +48,14 @@ const PackagesLoadMode = packages.NeedFiles | packages.NeedTypesInfo | packages.NeedDeps - // FromDir returns the configurations of the [dogma.Application] in the Go -// package at the given directory, and its subdirectories. +// LoadAndAnalyze returns the configurations of the [dogma.Application] +// implementations in the Go package at the given directory, and its +// subdirectories. // -// The configurations are built by statically analyzing the code; it is never +// The configurations are built by statically analyzing the code, which is never // executed. As a result, the returned configurations may be invalid or // incomplete. See [config.Fidelity]. -func FromDir(dir string) Analysis { +func LoadAndAnalyze(dir string) Analysis { pkgs, err := packages.Load( &packages.Config{ Mode: PackagesLoadMode, @@ -41,71 +70,68 @@ func FromDir(dir string) Analysis { panic(err) } - return FromPackages(pkgs) + return Analyze(pkgs) } -// FromPackages returns the configurations of the [dogma.Application] in the -// given Go packages. +// Analyze returns the configurations of the [dogma.Application] implementations +// in the given Go packages. // -// The configurations are built by statically analyzing the code; it is never +// The configurations are built by statically analyzing the code, which is never // executed. As a result, the returned configurations may be invalid or // incomplete. See [config.Fidelity]. // // The packages must have be loaded from source syntax using the [packages.Load] // function using [PackagesLoadMode], at a minimum. -func FromPackages(pkgs []*packages.Package) Analysis { - ctx := &analysisContext{ - Analysis: Analysis{ - Packages: pkgs, - }, - } - - ctx.SSAProgram, ctx.SSAPackages = ssautil.AllPackages( - ctx.Packages, +func Analyze(pkgs []*packages.Package) Analysis { + ctx := &context{} + ctx.Program, ctx.Packages = ssautil.AllPackages( + pkgs, 0, // ssa.SanityCheckFunctions, // TODO: document why this is necessary - // see.InstantiateGenerics // TODO: might this make some generic handling code easier? + // ssa.InstantiateGenerics // TODO: might this make some generic handling code easier? ) - ctx.SSAProgram.Build() + ctx.Program.Build() + + res := Analysis{ + Artifacts: struct { + Packages []*packages.Package + SSAProgram *ssa.Program + SSAPackages []*ssa.Package + }{ + pkgs, + ctx.Program, + ctx.Packages, + }, + } - if !lookupDogmaPackage(ctx) { + if !findDogma(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]. - return ctx.Analysis + return res + } + + for _, pkg := range ctx.Packages { + if pkg == nil { + // Any [packages.Package] that can not be built results in a nil + // [ssa.Package]. We ignore any such packages so that we can still + // obtain information about applications from other valid packages. + continue + } + + for _, m := range pkg.Members { + if t, ok := m.(*ssa.Type); ok { + analyzeType(ctx, t) + } + } } - // for _, pkg := range ctx.SSAPackages { - // if pkg == nil { - // // Any [packages.Package] that can not be built results in a nil - // // [ssa.Package]. We ignore any such packages so that we can still - // // obtain information about applications from other valid packages. - // continue - // } - - // for _, m := range pkg.Members { - // // The sequence of the if-blocks below is important as a type - // // implements an interface only if the methods in the interface's - // // method set have non-pointer receivers. Hence the implementation - // // check for the non-pointer type is made first. - // // - // // A pointer to the type, on the other hand, implements the - // // interface regardless of whether pointer receivers are used or - // // not. - // if types.Implements(m.Type(), dogmaPkg.Application) { - // apps = append(apps, analyzeApplication(prog, dogmaPkg, m.Type())) - // continue - // } - - // if p := types.NewPointer(m.Type()); types.Implements(p, dogmaPkg.Application) { - // apps = append(apps, analyzeApplication(prog, dogmaPkg, p)) - // } - // } - // } + res.Applications = ctx.Applications + // Ensure the applications are in a deterministic order. slices.SortFunc( - ctx.Analysis.Applications, + res.Applications, func(a, b *config.Application) int { return cmp.Compare( a.String(), @@ -114,18 +140,51 @@ func FromPackages(pkgs []*packages.Package) Analysis { }, ) - return ctx.Analysis + return res } -// Analysis encapsulates the results of static analysis. -type Analysis struct { - Applications []*config.Application - Packages []*packages.Package - SSAProgram *ssa.Program - SSAPackages []*ssa.Package +// packageOf returns the package in which t is declared. +// +// It panics if t is not a named type or a pointer to a named type. +func packageOf(t types.Type) *types.Package { + switch t := t.(type) { + case *types.Named: + return t.Obj().Pkg() + case *types.Alias: + return t.Obj().Pkg() + case *types.Pointer: + return packageOf(t.Elem()) + default: + panic(fmt.Sprintf("cannot determine package for anonymous or built-in type %v", t)) + } } -type analysisContext struct { - Analysis - Dogma dogmaPkg +func analyzeType(ctx *context, m *ssa.Type) { + t := m.Type() + + if types.IsInterface(t) { + // We're only interested in concrete types; otherwise there's nothing to + // analyze! + return + } + + // The sequence of the if-blocks below is important as a type + // implements an interface only if the methods in the interface's + // method set have non-pointer receivers. Hence the implementation + // check for the "raw" (non-pointer) type is made first. + // + // A pointer to the type, on the other hand, implements the + // interface regardless of whether pointer receivers are used or + // not. + + if types.Implements(t, ctx.Dogma.Application) { + analyzeApplication(ctx, t) + return + } + + p := types.NewPointer(t) + if types.Implements(p, ctx.Dogma.Application) { + analyzeApplication(ctx, p) + return + } } diff --git a/config/staticconfig/analyze_test.go b/config/staticconfig/analyze_test.go index 3e5d639a..575a5de9 100644 --- a/config/staticconfig/analyze_test.go +++ b/config/staticconfig/analyze_test.go @@ -4,40 +4,32 @@ import ( "io" "os" "path/filepath" - "strings" "testing" "github.com/dogmatiq/aureus" + "github.com/dogmatiq/enginekit/config" . "github.com/dogmatiq/enginekit/config/staticconfig" - "golang.org/x/tools/go/packages" - // . "github.com/dogmatiq/enginekit/enginetest/stubs" ) func TestAnalyzer(t *testing.T) { aureus.Run( t, func(w io.Writer, in aureus.Content, out aureus.ContentMetaData) error { - pkg := strings.TrimSuffix( - filepath.Base(in.File), - filepath.Ext(in.File), - ) - // Make a temporary directory to write the Go source code. // - // The name is based on the input file name rather than using a - // random temporary directory, otherwise the test output would be - // non-deterministic. + // The name is static so that the the test output is deterministic. // // Additionally, creating the directory within the repository allows // the test code to use this repo's go.mod file, ensuring the // statically analyzed code uses the same versions of Dogma, etc. dir := filepath.Join( filepath.Dir(in.File), - pkg, + "pkg", ) - if err := os.MkdirAll(dir, 0700); err != nil { + if err := os.Mkdir(dir, 0700); err != nil { return err } + defer os.RemoveAll(dir) if err := os.WriteFile( @@ -48,42 +40,31 @@ func TestAnalyzer(t *testing.T) { return err } - defer func() { - if e := recover(); e != nil { - if _, err := io.WriteString( - w, - e.(packages.Error).Msg+"\n", - ); err != nil { - panic(err) - } - } - }() - - result := FromDir(dir) + result := LoadAndAnalyze(dir) if len(result.Applications) == 0 { - _, err := io.WriteString(w, "(no applications found)\n") - return err + if _, err := io.WriteString(w, "(no applications found)\n"); err != nil { + return err + } } - // noise := []string{ - // "github.com/dogmatiq/configkit/static/testdata/" + pkg + ".", - // "github.com/dogmatiq/enginekit/enginetest/stubs.", - // } - - // for i, app := range apps { - // s := configkit.ToString(app) - // for _, p := range noise { - // s = strings.ReplaceAll(s, p, "") - // } - - // if i > 0 { - // s = "\n" + s - // } - // if _, err := io.WriteString(w, s); err != nil { - // return err - // } - // } + for err := range result.Errors() { + if _, err := io.WriteString(w, err.Error()+"\n"); err != nil { + return err + } + } + + for i, app := range result.Applications { + if i > 0 { + if _, err := io.WriteString(w, "\n"); err != nil { + return err + } + } + + if _, err := config.WriteDetails(w, app); err != nil { + return err + } + } return nil }, diff --git a/config/staticconfig/application.go b/config/staticconfig/application.go new file mode 100644 index 00000000..b0efb401 --- /dev/null +++ b/config/staticconfig/application.go @@ -0,0 +1,81 @@ +package staticconfig + +import ( + "go/types" + + "github.com/dogmatiq/enginekit/config" + "github.com/dogmatiq/enginekit/config/internal/configbuilder" + "github.com/dogmatiq/enginekit/internal/typename" +) + +// analyzeApplication analyzes t, which must be an implementation of +// [dogma.Application]. +func analyzeApplication(ctx *context, t types.Type) { + ctx.Applications = append( + ctx.Applications, + configbuilder.Application( + func(b *configbuilder.ApplicationBuilder) { + b.SetSourceTypeName(typename.OfStatic(t)) + + for call := range analyzeEntity(ctx, b, t) { + switch call.Method.Name() { + // // case "RegisterAggregate": + // // analyzeRegisterAggregateCall(ctx, c) + // // case "RegisterProcess": + // // analyzeRegisterProcessCall(ctx, c) + // // case "RegisterIntegration": + // // analyzeRegisterIntegrationCall(ctx, c) + // // case "RegisterProjection": + // // analyzeRegisterProjectionCall(ctx, c) + // // case "Handlers": + // // panic("not implemented") + default: + b.UpdateFidelity(config.Incomplete) + } + } + }, + ), + ) + + // switch c.Common().Method.Name() { + // case "Identity": + // app.IdentityValue = analyzeIdentityCall(c) + // case "RegisterAggregate": + // addHandlerFromArguments( + // prog, + // dogmaPkg, + // dogmaPkg.AggregateMessageHandler, + // args, + // app.HandlersValue, + // configkit.AggregateHandlerType, + // ) + // case "RegisterProcess": + // addHandlerFromArguments( + // prog, + // dogmaPkg, + // dogmaPkg.ProcessMessageHandler, + // args, + // app.HandlersValue, + // configkit.ProcessHandlerType, + // ) + // case "RegisterProjection": + // addHandlerFromArguments( + // prog, + // dogmaPkg, + // dogmaPkg.ProjectionMessageHandler, + // args, + // app.HandlersValue, + // configkit.ProjectionHandlerType, + // ) + // case "RegisterIntegration": + // addHandlerFromArguments( + // prog, + // dogmaPkg, + // dogmaPkg.IntegrationMessageHandler, + // args, + // app.HandlersValue, + // configkit.IntegrationHandlerType, + // ) + // } + // } +} diff --git a/config/staticconfig/context.go b/config/staticconfig/context.go new file mode 100644 index 00000000..bf450620 --- /dev/null +++ b/config/staticconfig/context.go @@ -0,0 +1,54 @@ +package staticconfig + +import ( + "go/types" + + "github.com/dogmatiq/enginekit/config" + "golang.org/x/tools/go/ssa" +) + +type context struct { + Program *ssa.Program + Packages []*ssa.Package + + Dogma struct { + Package *ssa.Package + Application *types.Interface + } + + Applications []*config.Application +} + +// findDogma updates ctx with information about the Dogma package. +// +// It returns false if the Dogma package has not been imported. +func findDogma(ctx *context) bool { + for _, pkg := range ctx.Program.AllPackages() { + if pkg.Pkg.Path() != "github.com/dogmatiq/dogma" { + continue + } + + iface := func(n string) *types.Interface { + return pkg.Pkg. + Scope(). + Lookup(n). + Type(). + Underlying().(*types.Interface) + } + + ctx.Dogma.Package = pkg + ctx.Dogma.Application = iface("Application") + + return true + } + + return false +} + +func (ctx *context) LookupMethod(t types.Type, name string) *ssa.Function { + fn := ctx.Program.LookupMethod(t, packageOf(t), name) + if fn == nil { + panic("method not found") + } + return fn +} diff --git a/config/staticconfig/dogma.go b/config/staticconfig/dogma.go deleted file mode 100644 index 4d14fc60..00000000 --- a/config/staticconfig/dogma.go +++ /dev/null @@ -1,31 +0,0 @@ -package staticconfig - -import ( - "go/types" -) - -const ( - // dogmaPkgPath is the full path of dogma package. - dogmaPkgPath = "github.com/dogmatiq/dogma" -) - -// dogma encapsulates information about the dogma package. -type dogmaPkg struct { - Package *types.Package -} - -// lookupDogmaPackage returns information about the dogma package. -// -// It returns false if the Dogma package has not been imported. -func lookupDogmaPackage(ctx *analysisContext) bool { - pkg := ctx.SSAProgram.ImportedPackage(dogmaPkgPath) - if pkg == nil { - return false - } - - ctx.Dogma = dogmaPkg{ - Package: pkg.Pkg, - } - - return true -} diff --git a/config/staticconfig/entity.go b/config/staticconfig/entity.go new file mode 100644 index 00000000..1a62f779 --- /dev/null +++ b/config/staticconfig/entity.go @@ -0,0 +1,151 @@ +package staticconfig + +import ( + "go/constant" + "go/types" + "iter" + + "github.com/dogmatiq/enginekit/config" + "github.com/dogmatiq/enginekit/config/internal/configbuilder" + "golang.org/x/tools/go/ssa" +) + +// analyzeConfigurerCalls analyzes the calls to the "configurer" that is passed +// to t's "Configure()" method. +// +// Any calls that are not recognized are yielded. +func analyzeEntity( + ctx *context, + b configbuilder.EntityBuilder, + t types.Type, +) iter.Seq[*ssa.CallCommon] { + return func(yield func(*ssa.CallCommon) bool) { + configure := ctx.LookupMethod(t, "Configure") + + for call := range findConfigurerCalls(configure, 1) { + switch call.Method.Name() { + case "Identity": + analyzeIdentityCall(b, call) + default: + if !yield(call) { + return + } + } + } + } +} + +func analyzeIdentityCall( + b configbuilder.EntityBuilder, + call *ssa.CallCommon, +) { + b.Identity(func(b *configbuilder.IdentityBuilder) { + if name, ok := call.Args[0].(*ssa.Const); ok { + b.SetName(constant.StringVal(name.Value)) + } else { + b.UpdateFidelity(config.Incomplete) + } + + if key, ok := call.Args[1].(*ssa.Const); ok { + b.SetKey(constant.StringVal(key.Value)) + } else { + b.UpdateFidelity(config.Incomplete) + } + }) +} + +// indices refers to the positions of arguments that are the configurer. If none +// are provided it defaults to [1]. This accounts for the most common case where +// 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. + +// findConfigurerCalls yields all call to methods on the Dogma application or +// handler "configurer" within the given function. +func findConfigurerCalls( + fn *ssa.Function, + indices ...int, +) iter.Seq[*ssa.CallCommon] { + isConfigurerCall := func(call *ssa.CallCommon) bool { + for _, i := range indices { + if call.Value == fn.Params[i] { + return true + } + } + return false + } + + return func(yield func(*ssa.CallCommon) bool) { + for _, block := range fn.Blocks { + for _, inst := range block.Instrs { + inst, ok := inst.(*ssa.Call) + if !ok { + continue + } + + call := inst.Common() + + if isConfigurerCall(call) { + // We've found a direct call to a method on the configurer. + if !yield(call) { + return + } + // } else { + // // We've found a call to some other function or method. We need + // // to analyse the instructions within *that* function to see if + // // *it* makes any calls to the configurer. + // if !e.yieldIndirectCalls(call, yield) { + // return false + // } + } + } + } + + } +} + +// func (e *entity) yieldIndirectCalls( +// call *ssa.CallCommon, +// yield func(*ssa.CallCommon) bool, +// ) bool { +// // com := call.Common() + +// // var indices []int +// // for i, arg := range com.Args { +// // if _, ok := configurers[arg]; ok { +// // indices = append(indices, i) +// // } +// // } + +// // if len(indices) == 0 { +// // 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 e.yieldCalls( +// call.StaticCallee(), +// yield, +// ) +// } diff --git a/config/staticconfig/testdata/_pending/const-value-ident.md b/config/staticconfig/testdata/_pending/const-value-ident.md deleted file mode 100644 index 8ca40464..00000000 --- a/config/staticconfig/testdata/_pending/const-value-ident.md +++ /dev/null @@ -1,30 +0,0 @@ -# No applications - -This test verifies that the identity specified with constants is correctly -parsed. - -```go au:input -package app - -import . "github.com/dogmatiq/dogma" - -const ( - // AppName is the application name. - AppName = "" - // AppKey is the application key. - AppKey = "04e12cf2-3c66-4414-9203-e045ddbe02c7" -) - -// App implements Application interface. -type App struct{} - -// Configure sets the application identity using non-literal constant -// expressions. -func (App) Configure(c ApplicationConfigurer) { - c.Identity(AppName, AppKey) -} -``` - -```au:output -application (04e12cf2-3c66-4414-9203-e045ddbe02c7) App -``` diff --git a/config/staticconfig/testdata/_pending/empty-app.md b/config/staticconfig/testdata/_pending/empty-app.md deleted file mode 100644 index 75433849..00000000 --- a/config/staticconfig/testdata/_pending/empty-app.md +++ /dev/null @@ -1,20 +0,0 @@ -# Empty application - -This test ensures that the static analyzer includes Dogma applications that have -no handlers. - -```go au:input -package app - -import "github.com/dogmatiq/dogma" - -type App struct{} - -func (App) Configure(c dogma.ApplicationConfigurer) { - c.Identity("", "8a6baab1-ee64-402e-a081-e43f4bebc243") -} -``` - -```au:output -application (8a6baab1-ee64-402e-a081-e43f4bebc243) App -``` diff --git a/config/staticconfig/testdata/_pending/multiple-apps-in-single-pkg.md b/config/staticconfig/testdata/_pending/multiple-apps-in-single-pkg.md deleted file mode 100644 index 114a5731..00000000 --- a/config/staticconfig/testdata/_pending/multiple-apps-in-single-pkg.md +++ /dev/null @@ -1,36 +0,0 @@ -# Multiple Dogma Apps in a Single Package - -This test verifies that static analysis correctly parses multiple Dogma -applications in a single Go package. - -```go au:input -package app - -import ( - . "github.com/dogmatiq/dogma" -) - -// AppFirst implements Application interface. -type AppFirst struct{} - -// Configure configures the behavior of the engine as it relates to this -// application. -func (AppFirst) Configure(c ApplicationConfigurer) { - c.Identity("", "4fec74a1-6ed4-46f4-8417-01e0910be8f1") -} - -// AppSecond implements Application interface. -type AppSecond struct{} - -// Configure configures the behavior of the engine as it relates to this -// application. -func (a AppSecond) Configure(c ApplicationConfigurer) { - c.Identity("", "6e97d403-3cb8-4a59-a7ec-74e8e219a7bc") -} -``` - -```au:output -application (4fec74a1-6ed4-46f4-8417-01e0910be8f1) AppFirst - -application (6e97d403-3cb8-4a59-a7ec-74e8e219a7bc) AppSecond -``` diff --git a/config/staticconfig/testdata/_pending/pointer-receiver-app.md b/config/staticconfig/testdata/_pending/pointer-receiver-app.md index fa8d263a..b865d38d 100644 --- a/config/staticconfig/testdata/_pending/pointer-receiver-app.md +++ b/config/staticconfig/testdata/_pending/pointer-receiver-app.md @@ -1,25 +1,42 @@ -# Pointer-Receiver Dogma App +# Applications with no handlers -This test verifies that static analysis correctly parses Dogma applications -declared with pointer-receiver methods. +This test ensures that the static analyzer includes Dogma applications that have +no handlers. -```go au:input +## With non-pointer receiver + +```au:output au:group="non-pointer" +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) + - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 +``` + +```go au:input au:group="non-pointer" package app -import ( - . "github.com/dogmatiq/dogma" -) +import "github.com/dogmatiq/dogma" -// App implements Application interface. type App struct{} -// Configure is implemented using a pointer receiver, such that the *App -// implements Application, and not App itself. -func (a *App) Configure(c ApplicationConfigurer) { - c.Identity("", "b754902b-47c8-48fc-84d2-d920c9cbdaec") +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "8a6baab1-ee64-402e-a081-e43f4bebc243") } ``` -```au:output -application (b754902b-47c8-48fc-84d2-d920c9cbdaec) *App +## With pointer receiver + +```au:output au:group="pointer" +valid application *github.com/dogmatiq/enginekit/config/staticconfig/testdata/empty-app.App (runtime type unavailable) + - valid identity app/d196eb7a-bad4-4826-8763-db1111882fbd +``` + +```go au:input au:group="pointer" +package app + +import "github.com/dogmatiq/dogma" + +type App struct{} + +func (*App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "d196eb7a-bad4-4826-8763-db1111882fbd") +} ``` diff --git a/config/staticconfig/testdata/_pending/var-value-ident.md b/config/staticconfig/testdata/_pending/var-value-ident.md deleted file mode 100644 index 23ba596f..00000000 --- a/config/staticconfig/testdata/_pending/var-value-ident.md +++ /dev/null @@ -1,24 +0,0 @@ -# No applications - -This test verifies that the identity specified with variables is correctly parsed. - -```go au:input -package app - -import . "github.com/dogmatiq/dogma" - -// App implements Application interface. -type App struct { - Name string - Key string -} - -// Configure sets the application identity using non-constant expressions. -func (a App) Configure(c ApplicationConfigurer) { - c.Identity(a.Name, a.Name) -} -``` - -```au:output -application () App -``` diff --git a/config/staticconfig/testdata/identity-from-const.md b/config/staticconfig/testdata/identity-from-const.md new file mode 100644 index 00000000..25975a13 --- /dev/null +++ b/config/staticconfig/testdata/identity-from-const.md @@ -0,0 +1,26 @@ +# Identity built from constants + +This test verifies that the static analyzer can discover the values within an +entity's identity when they are sourced from non-literal constant expressions. + +```au:output +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) + - valid identity app/d0de39ba-aaaf-43fd-8e8f-7c4e3be309ec +``` + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +const ( + Name = "app" + Key = "d0de39ba-aaaf-43fd-8e8f-7c4e3be309ec" +) + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity(Name, Key) +} +``` diff --git a/config/staticconfig/testdata/identity-from-var.md b/config/staticconfig/testdata/identity-from-var.md new file mode 100644 index 00000000..c026824c --- /dev/null +++ b/config/staticconfig/testdata/identity-from-var.md @@ -0,0 +1,24 @@ +# Identity built from variables + +This test verifies that the static analyzer includes an entity's identity, even +if it cannot determine the values used. + +```au:output +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) + - incomplete identity ?/? +``` + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct { + Name string + Key string +} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + c.Identity(a.Name, a.Name) +} +``` diff --git a/config/staticconfig/testdata/multiple-apps.md b/config/staticconfig/testdata/multiple-apps.md new file mode 100644 index 00000000..7224ea62 --- /dev/null +++ b/config/staticconfig/testdata/multiple-apps.md @@ -0,0 +1,31 @@ +# Multiple applications in a single package + +This test verifies that the static analyzer discovers multiple Dogma application +types defined within the same Go package. + +```au:output +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.One (runtime type unavailable) + - valid identity one/4fec74a1-6ed4-46f4-8417-01e0910be8f1 + +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.Two (runtime type unavailable) + - valid identity two/6e97d403-3cb8-4a59-a7ec-74e8e219a7bc +``` + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type ( + One struct{} + Two struct{} +) + +func (One) Configure(c dogma.ApplicationConfigurer) { + c.Identity("one", "4fec74a1-6ed4-46f4-8417-01e0910be8f1") +} + +func (Two) Configure(c dogma.ApplicationConfigurer) { + c.Identity("two", "6e97d403-3cb8-4a59-a7ec-74e8e219a7bc") +} +``` diff --git a/config/staticconfig/testdata/no-apps.md b/config/staticconfig/testdata/no-apps.md index db8f9852..c623a07b 100644 --- a/config/staticconfig/testdata/no-apps.md +++ b/config/staticconfig/testdata/no-apps.md @@ -1,13 +1,29 @@ # No applications -This test ensures that static analyzer does not fail when the code does not -contain any Dogma applications. +This test ensures that the static analyzer does not fail when the analyzed code +does not contain any Dogma applications. + +```au:output +(no applications found) +``` + +## Empty package + +```go au:input +package app +``` + +## Concrete type with similar structure to a dogma.Application ```go au:input package app +import _ "github.com/dogmatiq/dogma" + // App looks a lot like a [dogma.Application], but does not actually -// implement the Dogma interface. +// implement the Dogma interface because the local [ApplicationConfigurer] +// interface is not the same type as [dogma.ApplicationConfigurer], even though +// it's compatible. type App struct{} func (a App) Configure(c ApplicationConfigurer) { @@ -19,6 +35,16 @@ type ApplicationConfigurer interface { } ``` -```au:output -(no applications found) +## Interface that is compatible with dogma.Application + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +// App does implement [dogma.Application], but it is not a concrete type so +// there's nothing to analyze. +type App interface { + Configure(c dogma.ApplicationConfigurer) +} ``` diff --git a/config/staticconfig/testdata/no-handlers.md b/config/staticconfig/testdata/no-handlers.md new file mode 100644 index 00000000..83dd2959 --- /dev/null +++ b/config/staticconfig/testdata/no-handlers.md @@ -0,0 +1,21 @@ +# Applications with no handlers + +This test ensures that the static analyzer includes Dogma applications that have +no handlers. + +```au:output au:group="non-pointer" +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) + - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 +``` + +```go au:input au:group="non-pointer" +package app + +import "github.com/dogmatiq/dogma" + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "8a6baab1-ee64-402e-a081-e43f4bebc243") +} +``` diff --git a/go.sum b/go.sum index 0c1344f6..4f1c84ef 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,15 @@ +cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y= +cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= +github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/dogmatiq/aureus v0.1.0 h1:BIUF1G4pdCiJ+WQ6GnTmbhaejbjtX35Z9w2somdgslA= github.com/dogmatiq/aureus v0.1.0/go.mod h1:eTm6/WDfVI2tNjg1WCXiPt4fqjMhjO2kNM522ENa6mM= github.com/dogmatiq/dapper v0.6.0 h1:hnWUsjnt3nUiC9hmkPvuxrnMd7fYNz1i+/GS3gOx0Xs= @@ -8,22 +20,46 @@ github.com/dogmatiq/jumble v0.1.0 h1:Cb3ExfxY+AoUP4G9/sOwoOdYX8o+kOLK8+dhXAry+QA github.com/dogmatiq/jumble v0.1.0/go.mod h1:FCGV2ImXu8zvThxhd4QLstiEdu74vbIVw9bFJSBcKr4= github.com/dogmatiq/primo v0.3.1 h1:JSqiCh1ma9CbIVzPf8k1vhzQ2Zn/d/WupzElDoiYZw0= github.com/dogmatiq/primo v0.3.1/go.mod h1:z2DfWNz0YmwIKhUEwgJY4xyeWOw0He+9veRRMGQ21UI= +github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= +github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= From 96ae9f1f89f3e8db068c30904596065a3e54eac8 Mon Sep 17 00:00:00 2001 From: James Harris Date: Fri, 18 Oct 2024 11:59:35 +1000 Subject: [PATCH 03/38] Remove `analyzeEntity`. --- config/staticconfig/analyze.go | 4 +- config/staticconfig/application.go | 4 +- config/staticconfig/entity.go | 26 +--- config/staticconfig/static_test.go | 222 ----------------------------- 4 files changed, 9 insertions(+), 247 deletions(-) delete mode 100644 config/staticconfig/static_test.go diff --git a/config/staticconfig/analyze.go b/config/staticconfig/analyze.go index 2cffe2f7..95476778 100644 --- a/config/staticconfig/analyze.go +++ b/config/staticconfig/analyze.go @@ -86,9 +86,7 @@ func Analyze(pkgs []*packages.Package) Analysis { ctx := &context{} ctx.Program, ctx.Packages = ssautil.AllPackages( pkgs, - 0, - // ssa.SanityCheckFunctions, // TODO: document why this is necessary - // ssa.InstantiateGenerics // TODO: might this make some generic handling code easier? + ssa.InstantiateGenerics, // | ssa.SanityCheckFunctions, // TODO: document why this is necessary ) ctx.Program.Build() diff --git a/config/staticconfig/application.go b/config/staticconfig/application.go index b0efb401..ac9e16ce 100644 --- a/config/staticconfig/application.go +++ b/config/staticconfig/application.go @@ -17,8 +17,10 @@ func analyzeApplication(ctx *context, t types.Type) { func(b *configbuilder.ApplicationBuilder) { b.SetSourceTypeName(typename.OfStatic(t)) - for call := range analyzeEntity(ctx, b, t) { + for call := range findConfigurerCalls(ctx, t) { switch call.Method.Name() { + case "Identity": + analyzeIdentityCall(b, call) // // case "RegisterAggregate": // // analyzeRegisterAggregateCall(ctx, c) // // case "RegisterProcess": diff --git a/config/staticconfig/entity.go b/config/staticconfig/entity.go index 1a62f779..8c46df73 100644 --- a/config/staticconfig/entity.go +++ b/config/staticconfig/entity.go @@ -14,25 +14,9 @@ import ( // to t's "Configure()" method. // // Any calls that are not recognized are yielded. -func analyzeEntity( - ctx *context, - b configbuilder.EntityBuilder, - t types.Type, -) iter.Seq[*ssa.CallCommon] { - return func(yield func(*ssa.CallCommon) bool) { - configure := ctx.LookupMethod(t, "Configure") - - for call := range findConfigurerCalls(configure, 1) { - switch call.Method.Name() { - case "Identity": - analyzeIdentityCall(b, call) - default: - if !yield(call) { - return - } - } - } - } +func findConfigurerCalls(ctx *context, t types.Type) iter.Seq[*ssa.CallCommon] { + configure := ctx.LookupMethod(t, "Configure") + return findConfigurerCallsInFunc(configure, 1) } func analyzeIdentityCall( @@ -67,9 +51,9 @@ func analyzeIdentityCall( // names of the type parameters and the values are the concrete types that have // been instantiated. -// findConfigurerCalls yields all call to methods on the Dogma application or +// findConfigurerCallsInFunc yields all call to methods on the Dogma application or // handler "configurer" within the given function. -func findConfigurerCalls( +func findConfigurerCallsInFunc( fn *ssa.Function, indices ...int, ) iter.Seq[*ssa.CallCommon] { diff --git a/config/staticconfig/static_test.go b/config/staticconfig/static_test.go deleted file mode 100644 index 6a479323..00000000 --- a/config/staticconfig/static_test.go +++ /dev/null @@ -1,222 +0,0 @@ -package staticconfig_test - -// import ( -// "io" -// "iter" -// "os" -// "path/filepath" -// "strings" -// "testing" - -// "github.com/dogmatiq/aureus" -// "github.com/dogmatiq/configkit" -// "github.com/dogmatiq/configkit/message" -// . "github.com/dogmatiq/enginekit/enginetest/stubs" -// "golang.org/x/tools/go/packages" -// ) - -// func TestFromPackages(t *testing.T) { -// aureus.Run( -// t, -// func(w io.Writer, in aureus.Content, out aureus.ContentMetaData) error { -// pkg := strings.TrimSuffix( -// filepath.Base(in.File), -// filepath.Ext(in.File), -// ) - -// // Make a temporary directory to write the Go source code. -// // -// // The name is based on the input file name rather than using a -// // random temporary directory, otherwise the test output would be -// // non-deterministic. -// // -// // Additionally, creating the directory within the repository allows -// // the test code to use this repo's go.mod file, ensuring the -// // statically analyzed code uses the same versions of Dogma, etc. -// dir := filepath.Join( -// filepath.Dir(in.File), -// pkg, -// ) -// if err := os.MkdirAll(dir, 0700); err != nil { -// return err -// } -// defer os.RemoveAll(dir) - -// if err := os.WriteFile( -// filepath.Join(dir, "main.go"), -// []byte(in.Data), -// 0600, -// ); err != nil { -// return err -// } - -// defer func() { -// if e := recover(); e != nil { -// if _, err := io.WriteString( -// w, -// e.(packages.Error).Msg+"\n", -// ); err != nil { -// panic(err) -// } -// } -// }() - -// apps := FromDir(dir) - -// if len(apps) == 0 { -// _, err := io.WriteString(w, "(no applications found)\n") -// return err -// } - -// noise := []string{ -// "github.com/dogmatiq/configkit/static/testdata/" + pkg + ".", -// "github.com/dogmatiq/enginekit/enginetest/stubs.", -// } - -// for i, app := range apps { -// s := configkit.ToString(app) -// for _, p := range noise { -// s = strings.ReplaceAll(s, p, "") -// } - -// if i > 0 { -// s = "\n" + s -// } -// if _, err := io.WriteString(w, s); err != nil { -// return err -// } -// } - -// return nil -// }, -// ) - -// t.Run("should parse multiple packages contain applications", func(t *testing.T) { -// apps := FromDir("testdata/multiple-apps-in-pkgs") - -// if len(apps) != 2 { -// t.Fatalf("expected 2 applications, got %d", len(apps)) -// } - -// if expected, actual := "", -// apps[0].Identity().Name; expected != actual { -// t.Fatalf( -// "unexpected application name: want %s, got %s", -// expected, -// actual, -// ) -// } - -// if expected, actual := "b754902b-47c8-48fc-84d2-d920c9cbdaec", -// apps[0].Identity().Key; expected != actual { -// t.Fatalf( -// "unexpected application key: want %s, got %s", -// expected, -// actual, -// ) -// } - -// if expected, actual := "", -// apps[1].Identity().Name; expected != actual { -// t.Fatalf( -// "unexpected application name: want %s, got %s", -// expected, -// actual, -// ) -// } - -// if expected, actual := "bfaf2a16-23a0-495d-8098-051d77635822", -// apps[1].Identity().Key; expected != actual { -// t.Fatalf( -// "unexpected application key: want %s, got %s", -// expected, -// actual, -// ) -// } -// }) - -// t.Run("should parse all application-level messages", func(t *testing.T) { -// apps := FromDir("testdata/app-level-messages") - -// if len(apps) != 1 { -// t.Fatalf("expected 1 application, got %d", len(apps)) -// } - -// contains := func( -// mn message.Name, -// mk message.Kind, -// iterator iter.Seq2[message.Name, message.Kind], -// ) bool { -// for k, v := range iterator { -// if k == mn && v == mk { -// return true -// } -// } -// return false -// } - -// if !contains( -// message.NameFor[CommandStub[TypeA]](), -// message.CommandKind, -// apps[0].MessageNames().Consumed(), -// ) { -// t.Fatal("expected consumed TypeA command message") -// } - -// if !contains( -// message.NameFor[EventStub[TypeA]](), -// message.EventKind, -// apps[0].MessageNames().Consumed(), -// ) { -// t.Fatal("expected consumed TypeA event message") -// } - -// if !contains( -// message.NameFor[EventStub[TypeC]](), -// message.EventKind, -// apps[0].MessageNames().Consumed(), -// ) { -// t.Fatal("expected consumed TypeC event message") -// } - -// if !contains( -// message.NameFor[TimeoutStub[TypeA]](), -// message.TimeoutKind, -// apps[0].MessageNames().Consumed(), -// ) { -// t.Fatal("expected consumed TypeA timeout message") -// } - -// if !contains( -// message.NameFor[EventStub[TypeA]](), -// message.EventKind, -// apps[0].MessageNames().Produced(), -// ) { -// t.Fatal("expected produced TypeA event message") -// } - -// if !contains( -// message.NameFor[CommandStub[TypeB]](), -// message.CommandKind, -// apps[0].MessageNames().Produced(), -// ) { -// t.Fatal("expected produced TypeB command message") -// } - -// if !contains( -// message.NameFor[TimeoutStub[TypeA]](), -// message.TimeoutKind, -// apps[0].MessageNames().Produced(), -// ) { -// t.Fatal("expected produced TypeA timeout message") -// } - -// if !contains( -// message.NameFor[EventStub[TypeB]](), -// message.EventKind, -// apps[0].MessageNames().Produced(), -// ) { -// t.Fatal("expected produced TypeB event message") -// } -// }) -// } From a47227b449e7dbd566943ee4b913bbaeeb99e6b6 Mon Sep 17 00:00:00 2001 From: James Harris Date: Fri, 18 Oct 2024 19:15:09 +1000 Subject: [PATCH 04/38] Detect speculative/conditional blocks. --- config/staticconfig/analyze_test.go | 9 +- .../staticconfig/{entity.go => configurer.go} | 98 +++++++------ config/staticconfig/identity.go | 30 ++++ config/staticconfig/testdata/.gitignore | 1 + .../testdata/identity-outside-conditional.md | 105 ++++++++++++++ .../testdata/identity-within-conditional.md | 137 ++++++++++++++++++ 6 files changed, 331 insertions(+), 49 deletions(-) rename config/staticconfig/{entity.go => configurer.go} (52%) create mode 100644 config/staticconfig/identity.go create mode 100644 config/staticconfig/testdata/.gitignore create mode 100644 config/staticconfig/testdata/identity-outside-conditional.md create mode 100644 config/staticconfig/testdata/identity-within-conditional.md diff --git a/config/staticconfig/analyze_test.go b/config/staticconfig/analyze_test.go index 575a5de9..21f1ab60 100644 --- a/config/staticconfig/analyze_test.go +++ b/config/staticconfig/analyze_test.go @@ -26,11 +26,16 @@ func TestAnalyzer(t *testing.T) { filepath.Dir(in.File), "pkg", ) - if err := os.Mkdir(dir, 0700); err != nil { + if err := os.MkdirAll(dir, 0700); err != nil { return err } - defer os.RemoveAll(dir) + defer func() { + err := os.RemoveAll(dir) + if err != nil { + t.Logf("failed to remove temporary directory: %v", err) + } + }() if err := os.WriteFile( filepath.Join(dir, "main.go"), diff --git a/config/staticconfig/entity.go b/config/staticconfig/configurer.go similarity index 52% rename from config/staticconfig/entity.go rename to config/staticconfig/configurer.go index 8c46df73..19846cc3 100644 --- a/config/staticconfig/entity.go +++ b/config/staticconfig/configurer.go @@ -1,62 +1,38 @@ package staticconfig import ( - "go/constant" "go/types" "iter" + "os" "github.com/dogmatiq/enginekit/config" - "github.com/dogmatiq/enginekit/config/internal/configbuilder" "golang.org/x/tools/go/ssa" ) +type configurerCall struct { + *ssa.CallCommon + + 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, t types.Type) iter.Seq[*ssa.CallCommon] { +func findConfigurerCalls(ctx *context, t types.Type) iter.Seq[configurerCall] { configure := ctx.LookupMethod(t, "Configure") return findConfigurerCallsInFunc(configure, 1) } -func analyzeIdentityCall( - b configbuilder.EntityBuilder, - call *ssa.CallCommon, -) { - b.Identity(func(b *configbuilder.IdentityBuilder) { - if name, ok := call.Args[0].(*ssa.Const); ok { - b.SetName(constant.StringVal(name.Value)) - } else { - b.UpdateFidelity(config.Incomplete) - } - - if key, ok := call.Args[1].(*ssa.Const); ok { - b.SetKey(constant.StringVal(key.Value)) - } else { - b.UpdateFidelity(config.Incomplete) - } - }) -} - -// indices refers to the positions of arguments that are the configurer. If none -// are provided it defaults to [1]. This accounts for the most common case where -// 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. - -// findConfigurerCallsInFunc yields all call to methods on the Dogma application or -// handler "configurer" within the given function. +// findConfigurerCallsInFunc 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 findConfigurerCallsInFunc( fn *ssa.Function, indices ...int, -) iter.Seq[*ssa.CallCommon] { +) iter.Seq[configurerCall] { isConfigurerCall := func(call *ssa.CallCommon) bool { for _, i := range indices { if call.Value == fn.Params[i] { @@ -66,8 +42,15 @@ func findConfigurerCallsInFunc( return false } - return func(yield func(*ssa.CallCommon) bool) { + fn.WriteTo(os.Stdout) + + return func(yield func(configurerCall) bool) { for _, block := range fn.Blocks { + var f config.Fidelity + if isConditional(fn, block) { + f |= config.Speculative + } + for _, inst := range block.Instrs { inst, ok := inst.(*ssa.Call) if !ok { @@ -78,21 +61,42 @@ func findConfigurerCallsInFunc( if isConfigurerCall(call) { // We've found a direct call to a method on the configurer. - if !yield(call) { + if !yield(configurerCall{call, f}) { return } - // } else { - // // We've found a call to some other function or method. We need - // // to analyse the instructions within *that* function to see if - // // *it* makes any calls to the configurer. - // if !e.yieldIndirectCalls(call, yield) { - // return false - // } } } } + } +} + +// isConditional returns true if there is any control flow path through fn that +// does NOT pass through b. +func isConditional(fn *ssa.Function, b *ssa.BasicBlock) bool { + return !isInevitable(fn.Blocks[0], b) +} + +// isInevitable returns true if all paths out of "from" pass through "to". +func isInevitable(from, to *ssa.BasicBlock) bool { + if from == to { + return true + } + + if len(from.Succs) == 0 { + return false + } + + for _, succ := range from.Succs { + if succ == from { + continue + } + if !isInevitable(succ, to) { + return false + } } + + return true } // func (e *entity) yieldIndirectCalls( diff --git a/config/staticconfig/identity.go b/config/staticconfig/identity.go new file mode 100644 index 00000000..beac1af8 --- /dev/null +++ b/config/staticconfig/identity.go @@ -0,0 +1,30 @@ +package staticconfig + +import ( + "go/constant" + + "github.com/dogmatiq/enginekit/config" + "github.com/dogmatiq/enginekit/config/internal/configbuilder" + "golang.org/x/tools/go/ssa" +) + +func analyzeIdentityCall( + b configbuilder.EntityBuilder, + call configurerCall, +) { + b.Identity(func(b *configbuilder.IdentityBuilder) { + b.UpdateFidelity(call.Fidelity) + + if name, ok := call.Args[0].(*ssa.Const); ok { + b.SetName(constant.StringVal(name.Value)) + } else { + b.UpdateFidelity(config.Incomplete) + } + + if key, ok := call.Args[1].(*ssa.Const); ok { + b.SetKey(constant.StringVal(key.Value)) + } else { + b.UpdateFidelity(config.Incomplete) + } + }) +} diff --git a/config/staticconfig/testdata/.gitignore b/config/staticconfig/testdata/.gitignore new file mode 100644 index 00000000..01d0a084 --- /dev/null +++ b/config/staticconfig/testdata/.gitignore @@ -0,0 +1 @@ +pkg/ diff --git a/config/staticconfig/testdata/identity-outside-conditional.md b/config/staticconfig/testdata/identity-outside-conditional.md new file mode 100644 index 00000000..ced8d432 --- /dev/null +++ b/config/staticconfig/testdata/identity-outside-conditional.md @@ -0,0 +1,105 @@ +# Conditional identity + +This test verifies that the static analyzer includes information about an +entity's identity even if it appears after (but not within) a conditional +statement. + +```au:output +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) + - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 +``` + +## If statement + +```go au:input +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if rand.Int() == 0 { + } + + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + +## Else statement + +```go au:input +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if rand.Int() == 0 { + } else { + } + + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + +## Switch statement + +```go au:input +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + switch rand.Int() { + case 0: + } + + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + +## For statement + +```go au:input +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + for range rand.Int() { + } + + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + +## Select statement + +```go au:input +package app + +import "math/rand" +import "time" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + select { + case <-time.After(time.Duration(rand.Int())): + default: + } + + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` diff --git a/config/staticconfig/testdata/identity-within-conditional.md b/config/staticconfig/testdata/identity-within-conditional.md new file mode 100644 index 00000000..6b96e0a4 --- /dev/null +++ b/config/staticconfig/testdata/identity-within-conditional.md @@ -0,0 +1,137 @@ +# Conditional identity + +This test verifies that the static analyzer includes information about an +entity's identity when it is defined within a conditional statement. + +```au:output +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) + - valid speculative identity app/de142370-93ee-409c-9336-5084d9c5e285 +``` + +## If statement + +```go au:input +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if rand.Int() == 0 { + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + } +} +``` + +## Else statement + +```go au:input +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if rand.Int() == 0 { + } else { + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + } +} +``` + +## After conditional return + +```go au:input +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if rand.Int() == 0 { + return + } + + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + +## After conditonal panic + +```go au:input +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if rand.Int() == 0 { + panic("oh no") + } + + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + +## Switch statement + +```go au:input +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + switch rand.Int() { + case 0: + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + } +} +``` + +## For statement + +```go au:input +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + for range rand.Int() { + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + } +} +``` + +## Select statement + +```go au:input +package app + +import "math/rand" +import "time" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + select { + case <-time.After(time.Duration(rand.Int())): + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + default: + } +} +``` From 16bafc2f86f2fdacefdb92109dbb9a3da5d6ef11 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sat, 19 Oct 2024 19:49:04 +1000 Subject: [PATCH 05/38] Add dead code elimination. --- config/staticconfig/block.go | 89 +++++++++++++++++++ config/staticconfig/configurer.go | 34 +------ .../conditional-excluded-by-const-expr.md | 43 +++++++++ .../conditional-included-by-const-expr.md | 44 +++++++++ ...al.md => conditional-present-in-method.md} | 2 +- ...y-within-conditional.md => conditional.md} | 0 go.sum | 36 -------- 7 files changed, 178 insertions(+), 70 deletions(-) create mode 100644 config/staticconfig/block.go create mode 100644 config/staticconfig/testdata/conditional-excluded-by-const-expr.md create mode 100644 config/staticconfig/testdata/conditional-included-by-const-expr.md rename config/staticconfig/testdata/{identity-outside-conditional.md => conditional-present-in-method.md} (97%) rename config/staticconfig/testdata/{identity-within-conditional.md => conditional.md} (100%) diff --git a/config/staticconfig/block.go b/config/staticconfig/block.go new file mode 100644 index 00000000..e159e24f --- /dev/null +++ b/config/staticconfig/block.go @@ -0,0 +1,89 @@ +package staticconfig + +import ( + "go/constant" + "iter" + + "golang.org/x/tools/go/ssa" +) + +// walkReachable yields all blocks reachable from b. +func walkReachable(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { + return func(yield func(*ssa.BasicBlock) bool) { + yielded := map[*ssa.BasicBlock]struct{}{} + + var walk func(*ssa.BasicBlock) bool + walk = func(b *ssa.BasicBlock) bool { + if _, ok := yielded[b]; ok { + return true + } + yielded[b] = struct{}{} + + if !yield(b) { + return false + } + + for succ := range reachableSuccessors(b) { + if !walk(succ) { + return false + } + } + + return true + } + + walk(b) + } +} + +// reachableSuccessors yields the successors of b that are actually reachable. +func reachableSuccessors(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { + return func(yield func(*ssa.BasicBlock) bool) { + if branch, ok := b.Instrs[len(b.Instrs)-1].(*ssa.If); ok { + if v, ok := branch.Cond.(*ssa.Const); ok { + if constant.BoolVal(v.Value) { + yield(b.Succs[0]) + } else { + yield(b.Succs[1]) + } + + return + } + } + + for _, succ := range b.Succs { + if !yield(succ) { + return + } + } + } +} + +// isConditional returns true if there is any control flow path through fn that +// does NOT pass through b. +func isConditional(fn *ssa.Function, b *ssa.BasicBlock) bool { + return !isInevitable(fn.Blocks[0], b) +} + +// isInevitable returns true if all paths out of "from" pass through "to". +func isInevitable(from, to *ssa.BasicBlock) bool { + if from == to { + return true + } + + if len(from.Succs) == 0 { + return false + } + + for succ := range reachableSuccessors(from) { + if succ == from { + continue + } + + if !isInevitable(succ, to) { + return false + } + } + + return true +} diff --git a/config/staticconfig/configurer.go b/config/staticconfig/configurer.go index 19846cc3..8edd36f7 100644 --- a/config/staticconfig/configurer.go +++ b/config/staticconfig/configurer.go @@ -3,7 +3,6 @@ package staticconfig import ( "go/types" "iter" - "os" "github.com/dogmatiq/enginekit/config" "golang.org/x/tools/go/ssa" @@ -42,10 +41,8 @@ func findConfigurerCallsInFunc( return false } - fn.WriteTo(os.Stdout) - return func(yield func(configurerCall) bool) { - for _, block := range fn.Blocks { + for block := range walkReachable(fn.Blocks[0]) { var f config.Fidelity if isConditional(fn, block) { f |= config.Speculative @@ -70,35 +67,6 @@ func findConfigurerCallsInFunc( } } -// isConditional returns true if there is any control flow path through fn that -// does NOT pass through b. -func isConditional(fn *ssa.Function, b *ssa.BasicBlock) bool { - return !isInevitable(fn.Blocks[0], b) -} - -// isInevitable returns true if all paths out of "from" pass through "to". -func isInevitable(from, to *ssa.BasicBlock) bool { - if from == to { - return true - } - - if len(from.Succs) == 0 { - return false - } - - for _, succ := range from.Succs { - if succ == from { - continue - } - - if !isInevitable(succ, to) { - return false - } - } - - return true -} - // func (e *entity) yieldIndirectCalls( // call *ssa.CallCommon, // yield func(*ssa.CallCommon) bool, diff --git a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md new file mode 100644 index 00000000..a50c3e19 --- /dev/null +++ b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md @@ -0,0 +1,43 @@ +# Conditional w/ constant expression that excludes configuration + +This test verifies that the static analyzer excludes information about an +entity's identity if it appears in an unreachable branch. + +```au:output +invalid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) + - no identity is configured +``` + +## After conditional return + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if true { + return + } + + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + +## Within conditional block + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if false { + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + } +} +``` diff --git a/config/staticconfig/testdata/conditional-included-by-const-expr.md b/config/staticconfig/testdata/conditional-included-by-const-expr.md new file mode 100644 index 00000000..c5439943 --- /dev/null +++ b/config/staticconfig/testdata/conditional-included-by-const-expr.md @@ -0,0 +1,44 @@ +# Conditional w/ constant expression that includes configuration + +This test verifies that the static analyzer includes information about an +entity's identity if it appears in a conditional block that is always executed. +Note that the identity is not marked as "speculative". + +```au:output +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) + - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 +``` + +## After conditional return + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if false { + return + } + + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + +## Within conditional block + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if true { + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + } +} +``` diff --git a/config/staticconfig/testdata/identity-outside-conditional.md b/config/staticconfig/testdata/conditional-present-in-method.md similarity index 97% rename from config/staticconfig/testdata/identity-outside-conditional.md rename to config/staticconfig/testdata/conditional-present-in-method.md index ced8d432..8729d8b2 100644 --- a/config/staticconfig/testdata/identity-outside-conditional.md +++ b/config/staticconfig/testdata/conditional-present-in-method.md @@ -1,4 +1,4 @@ -# Conditional identity +# Unconditional identity after conditional statement This test verifies that the static analyzer includes information about an entity's identity even if it appears after (but not within) a conditional diff --git a/config/staticconfig/testdata/identity-within-conditional.md b/config/staticconfig/testdata/conditional.md similarity index 100% rename from config/staticconfig/testdata/identity-within-conditional.md rename to config/staticconfig/testdata/conditional.md diff --git a/go.sum b/go.sum index 4f1c84ef..0c1344f6 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,3 @@ -cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y= -cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= -github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg= -github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= -github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/dogmatiq/aureus v0.1.0 h1:BIUF1G4pdCiJ+WQ6GnTmbhaejbjtX35Z9w2somdgslA= github.com/dogmatiq/aureus v0.1.0/go.mod h1:eTm6/WDfVI2tNjg1WCXiPt4fqjMhjO2kNM522ENa6mM= github.com/dogmatiq/dapper v0.6.0 h1:hnWUsjnt3nUiC9hmkPvuxrnMd7fYNz1i+/GS3gOx0Xs= @@ -20,46 +8,22 @@ github.com/dogmatiq/jumble v0.1.0 h1:Cb3ExfxY+AoUP4G9/sOwoOdYX8o+kOLK8+dhXAry+QA github.com/dogmatiq/jumble v0.1.0/go.mod h1:FCGV2ImXu8zvThxhd4QLstiEdu74vbIVw9bFJSBcKr4= github.com/dogmatiq/primo v0.3.1 h1:JSqiCh1ma9CbIVzPf8k1vhzQ2Zn/d/WupzElDoiYZw0= github.com/dogmatiq/primo v0.3.1/go.mod h1:z2DfWNz0YmwIKhUEwgJY4xyeWOw0He+9veRRMGQ21UI= -github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= -github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= -github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= -github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= -github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= -github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= -golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= -golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= -google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= From 5008892a75a90ecca77c282f0b8708aca49192be Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Oct 2024 07:06:27 +1000 Subject: [PATCH 06/38] Add support for "indirect" calls to the configurer. --- config/staticconfig/analyze.go | 49 +++--- config/staticconfig/analyze_test.go | 39 ++--- config/staticconfig/application.go | 4 +- config/staticconfig/block.go | 9 +- config/staticconfig/configurer.go | 164 ++++++++++-------- config/staticconfig/context.go | 30 +++- config/staticconfig/testdata/.gitignore | 2 +- .../testdata/_pending/literal-value-ident.md | 22 --- .../testdata/_pending/pointer-receiver-app.md | 2 +- .../conditional-excluded-by-const-expr.md | 2 +- .../conditional-included-by-const-expr.md | 2 +- .../testdata/conditional-present-in-method.md | 2 +- config/staticconfig/testdata/conditional.md | 2 +- .../testdata/identity-from-const.md | 2 +- .../testdata/identity-from-var.md | 2 +- config/staticconfig/testdata/indirect.md | 78 +++++++++ config/staticconfig/testdata/multiple-apps.md | 4 +- config/staticconfig/testdata/no-handlers.md | 2 +- 18 files changed, 260 insertions(+), 157 deletions(-) delete mode 100644 config/staticconfig/testdata/_pending/literal-value-ident.md create mode 100644 config/staticconfig/testdata/indirect.md diff --git a/config/staticconfig/analyze.go b/config/staticconfig/analyze.go index 95476778..2087a03f 100644 --- a/config/staticconfig/analyze.go +++ b/config/staticconfig/analyze.go @@ -16,12 +16,14 @@ import ( // Analysis encapsulates the results of static analysis. type Analysis struct { Applications []*config.Application + Artifacts Artifacts +} - Artifacts struct { - Packages []*packages.Package - SSAProgram *ssa.Program - SSAPackages []*ssa.Package - } +// Artifacts contains the intermediate results of the analysis. +type Artifacts struct { + Packages []*packages.Package + SSAProgram *ssa.Program + SSAPackages []*ssa.Package } // Errors returns a sequence of errors that occurred during analysis, not @@ -83,23 +85,26 @@ func LoadAndAnalyze(dir string) Analysis { // The packages must have be loaded from source syntax using the [packages.Load] // function using [PackagesLoadMode], at a minimum. func Analyze(pkgs []*packages.Package) Analysis { - ctx := &context{} - ctx.Program, ctx.Packages = ssautil.AllPackages( + prog, ssaPackages := ssautil.AllPackages( pkgs, ssa.InstantiateGenerics, // | ssa.SanityCheckFunctions, // TODO: document why this is necessary ) - ctx.Program.Build() - - res := Analysis{ - Artifacts: struct { - Packages []*packages.Package - SSAProgram *ssa.Program - SSAPackages []*ssa.Package - }{ - pkgs, - ctx.Program, - ctx.Packages, + prog.Build() + + ctx := &context{ + Program: prog, + Packages: ssaPackages, + Analysis: &Analysis{ + Artifacts: struct { + Packages []*packages.Package + SSAProgram *ssa.Program + SSAPackages []*ssa.Package + }{ + pkgs, + prog, + ssaPackages, + }, }, } @@ -107,7 +112,7 @@ func Analyze(pkgs []*packages.Package) Analysis { // 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]. - return res + return *ctx.Analysis } for _, pkg := range ctx.Packages { @@ -125,11 +130,9 @@ func Analyze(pkgs []*packages.Package) Analysis { } } - res.Applications = ctx.Applications - // Ensure the applications are in a deterministic order. slices.SortFunc( - res.Applications, + ctx.Analysis.Applications, func(a, b *config.Application) int { return cmp.Compare( a.String(), @@ -138,7 +141,7 @@ func Analyze(pkgs []*packages.Package) Analysis { }, ) - return res + return *ctx.Analysis } // packageOf returns the package in which t is declared. diff --git a/config/staticconfig/analyze_test.go b/config/staticconfig/analyze_test.go index 21f1ab60..54f48cec 100644 --- a/config/staticconfig/analyze_test.go +++ b/config/staticconfig/analyze_test.go @@ -4,6 +4,7 @@ import ( "io" "os" "path/filepath" + "strings" "testing" "github.com/dogmatiq/aureus" @@ -15,27 +16,14 @@ func TestAnalyzer(t *testing.T) { aureus.Run( t, func(w io.Writer, in aureus.Content, out aureus.ContentMetaData) error { - // Make a temporary directory to write the Go source code. - // - // The name is static so that the the test output is deterministic. - // - // Additionally, creating the directory within the repository allows - // the test code to use this repo's go.mod file, ensuring the - // statically analyzed code uses the same versions of Dogma, etc. - dir := filepath.Join( - filepath.Dir(in.File), - "pkg", - ) - if err := os.MkdirAll(dir, 0700); err != nil { + // Create a temporary directory to write the Go source code, but + // create it within this Go module so that it uses the same version + // of Dogma, etc. + dir, err := os.MkdirTemp(filepath.Dir(in.File), "aureus-") + if err != nil { return err } - - defer func() { - err := os.RemoveAll(dir) - if err != nil { - t.Logf("failed to remove temporary directory: %v", err) - } - }() + defer os.RemoveAll(dir) if err := os.WriteFile( filepath.Join(dir, "main.go"), @@ -66,7 +54,18 @@ func TestAnalyzer(t *testing.T) { } } - if _, err := config.WriteDetails(w, app); err != nil { + // Render the details of the application. + details := config.RenderDetails(app) + + // Remove the random portion of the temporary directory name + // so that the test output is deterministic. + details = strings.ReplaceAll( + details, + "/"+filepath.Base(dir)+".", + ".", + ) + + if _, err := io.WriteString(w, details); err != nil { return err } } diff --git a/config/staticconfig/application.go b/config/staticconfig/application.go index ac9e16ce..672d8d20 100644 --- a/config/staticconfig/application.go +++ b/config/staticconfig/application.go @@ -11,8 +11,8 @@ import ( // analyzeApplication analyzes t, which must be an implementation of // [dogma.Application]. func analyzeApplication(ctx *context, t types.Type) { - ctx.Applications = append( - ctx.Applications, + ctx.Analysis.Applications = append( + ctx.Analysis.Applications, configbuilder.Application( func(b *configbuilder.ApplicationBuilder) { b.SetSourceTypeName(typename.OfStatic(t)) diff --git a/config/staticconfig/block.go b/config/staticconfig/block.go index e159e24f..7f5116f3 100644 --- a/config/staticconfig/block.go +++ b/config/staticconfig/block.go @@ -17,6 +17,7 @@ func walkReachable(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { if _, ok := yielded[b]; ok { return true } + yielded[b] = struct{}{} if !yield(b) { @@ -59,10 +60,10 @@ func reachableSuccessors(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { } } -// isConditional returns true if there is any control flow path through fn that -// does NOT pass through b. -func isConditional(fn *ssa.Function, b *ssa.BasicBlock) bool { - return !isInevitable(fn.Blocks[0], b) +// isConditional returns true if there is any control flow path through the +// function that does NOT pass through b. +func isConditional(b *ssa.BasicBlock) bool { + return !isInevitable(b.Parent().Blocks[0], b) } // isInevitable returns true if all paths out of "from" pass through "to". diff --git a/config/staticconfig/configurer.go b/config/staticconfig/configurer.go index 8edd36f7..7eba528d 100644 --- a/config/staticconfig/configurer.go +++ b/config/staticconfig/configurer.go @@ -20,88 +20,116 @@ type configurerCall struct { // Any calls that are not recognized are yielded. func findConfigurerCalls(ctx *context, t types.Type) iter.Seq[configurerCall] { configure := ctx.LookupMethod(t, "Configure") - return findConfigurerCallsInFunc(configure, 1) + + ctx = ctx.NewChild( + func(v ssa.Value) bool { + return v == configure.Params[1] + }, + ) + + return func(yield func(configurerCall) bool) { + emitConfigurerCallsInFunc(ctx, configure, yield) + } } -// findConfigurerCallsInFunc yields all call to methods on the Dogma application +// 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 findConfigurerCallsInFunc( +func emitConfigurerCallsInFunc( + ctx *context, fn *ssa.Function, - indices ...int, -) iter.Seq[configurerCall] { - isConfigurerCall := func(call *ssa.CallCommon) bool { - for _, i := range indices { - if call.Value == fn.Params[i] { - return true + yield func(configurerCall) bool, +) bool { + if len(fn.Blocks) == 0 { + return true + } + + for block := range walkReachable(fn.Blocks[0]) { + for _, inst := range block.Instrs { + if !emitConfigurerCallsInInstruction(ctx, inst, yield) { + return false } } - return false } - return func(yield func(configurerCall) bool) { - for block := range walkReachable(fn.Blocks[0]) { - var f config.Fidelity - if isConditional(fn, block) { - f |= config.Speculative - } + return true +} - for _, inst := range block.Instrs { - inst, ok := inst.(*ssa.Call) - if !ok { - continue - } +func emitConfigurerCallsInInstruction( + ctx *context, + inst ssa.Instruction, + yield func(configurerCall) bool, +) bool { + switch inst := inst.(type) { + case ssa.CallInstruction: + return emitConfigurerCallsInCallInstruction(ctx, inst, yield) + default: + return true + } +} - call := inst.Common() +func emitConfigurerCallsInCallInstruction( + ctx *context, + call ssa.CallInstruction, + yield func(configurerCall) bool, +) bool { + com := call.Common() + + if com.IsInvoke() { + // We're invoking a method on an interface, that is, we don't know the + // concrete type. If it's not a call to a method on the configurer, + // there's nothing more we can analyze. + if !ctx.IsConfigurer(com.Value) { + return true + } - if isConfigurerCall(call) { - // We've found a direct call to a method on the configurer. - if !yield(configurerCall{call, f}) { - return - } - } - } + // We've found a direct call to a method on the configurer. + var f config.Fidelity + if isConditional(call.Block()) { + f |= config.Speculative + } + + return yield(configurerCall{com, f}) + } + + // We've found a call to some other function or method. + // + // If any of the parameters refer to the configurer, we need to analyze + // _that_ function. + // + // This is an native implementation. There are other ways that this function + // could gain access to the configurer. For example, it could be passed + // inside a context, or assigned to a field within the entity struct. + fn := com.StaticCallee() + + // Check at which argument indices the configurer is passed to the function. + var indices []int + for i, arg := range com.Args { + if ctx.IsConfigurer(arg) { + indices = append(indices, i) } } -} -// func (e *entity) yieldIndirectCalls( -// call *ssa.CallCommon, -// yield func(*ssa.CallCommon) bool, -// ) bool { -// // com := call.Common() - -// // var indices []int -// // for i, arg := range com.Args { -// // if _, ok := configurers[arg]; ok { -// // indices = append(indices, i) -// // } -// // } - -// // if len(indices) == 0 { -// // 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 e.yieldCalls( -// call.StaticCallee(), -// yield, -// ) -// } + // Don't analyze fn if the configurer is not passed as an argument. + if len(indices) == 0 { + return true + } + + return emitConfigurerCallsInFunc( + ctx.NewChild( + func(v ssa.Value) bool { + for _, i := range indices { + if v == fn.Params[i] { + return true + } + } + + return false + }, + ), + fn, + yield, + ) +} diff --git a/config/staticconfig/context.go b/config/staticconfig/context.go index bf450620..67b04d14 100644 --- a/config/staticconfig/context.go +++ b/config/staticconfig/context.go @@ -1,9 +1,9 @@ package staticconfig import ( + "fmt" "go/types" - "github.com/dogmatiq/enginekit/config" "golang.org/x/tools/go/ssa" ) @@ -12,11 +12,14 @@ type context struct { Packages []*ssa.Package Dogma struct { - Package *ssa.Package - Application *types.Interface + Package *ssa.Package + Application *types.Interface + ApplicationConfigurer *types.Interface } - Applications []*config.Application + Analysis *Analysis + + IsConfigurer func(ssa.Value) bool } // findDogma updates ctx with information about the Dogma package. @@ -38,6 +41,7 @@ func findDogma(ctx *context) bool { ctx.Dogma.Package = pkg ctx.Dogma.Application = iface("Application") + ctx.Dogma.ApplicationConfigurer = iface("ApplicationConfigurer") return true } @@ -45,10 +49,22 @@ func findDogma(ctx *context) bool { return false } -func (ctx *context) LookupMethod(t types.Type, name string) *ssa.Function { - fn := ctx.Program.LookupMethod(t, packageOf(t), name) +func (c *context) LookupMethod(t types.Type, name string) *ssa.Function { + fn := c.Program.LookupMethod(t, packageOf(t), name) if fn == nil { - panic("method not found") + panic(fmt.Sprintf("method not found: %s.%s", t, name)) } return fn } + +func (c *context) NewChild( + isConfigurer func(ssa.Value) bool, +) *context { + return &context{ + Program: c.Program, + Packages: c.Packages, + Dogma: c.Dogma, + Analysis: c.Analysis, + IsConfigurer: isConfigurer, + } +} diff --git a/config/staticconfig/testdata/.gitignore b/config/staticconfig/testdata/.gitignore index 01d0a084..e12637ed 100644 --- a/config/staticconfig/testdata/.gitignore +++ b/config/staticconfig/testdata/.gitignore @@ -1 +1 @@ -pkg/ +aureus-*/ diff --git a/config/staticconfig/testdata/_pending/literal-value-ident.md b/config/staticconfig/testdata/_pending/literal-value-ident.md deleted file mode 100644 index 8d6fc0c5..00000000 --- a/config/staticconfig/testdata/_pending/literal-value-ident.md +++ /dev/null @@ -1,22 +0,0 @@ -# No applications - -This test verifies that the identity specified with string literals is correctly -parsed. - -```go au:input -package app - -import . "github.com/dogmatiq/dogma" - -// App implements Application interface. -type App struct{} - -// Configure sets the application identity using literal string values. -func (App) Configure(c ApplicationConfigurer) { - c.Identity("", "9d0af85d-f506-4742-b676-ce87730bb1a0") -} -``` - -```au:output -application (9d0af85d-f506-4742-b676-ce87730bb1a0) App -``` diff --git a/config/staticconfig/testdata/_pending/pointer-receiver-app.md b/config/staticconfig/testdata/_pending/pointer-receiver-app.md index b865d38d..bf735697 100644 --- a/config/staticconfig/testdata/_pending/pointer-receiver-app.md +++ b/config/staticconfig/testdata/_pending/pointer-receiver-app.md @@ -6,7 +6,7 @@ no handlers. ## With non-pointer receiver ```au:output au:group="non-pointer" -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 ``` diff --git a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md index a50c3e19..6acdafa0 100644 --- a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer excludes information about an entity's identity if it appears in an unreachable branch. ```au:output -invalid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) +invalid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - no identity is configured ``` diff --git a/config/staticconfig/testdata/conditional-included-by-const-expr.md b/config/staticconfig/testdata/conditional-included-by-const-expr.md index c5439943..3f1c7c28 100644 --- a/config/staticconfig/testdata/conditional-included-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-included-by-const-expr.md @@ -5,7 +5,7 @@ entity's identity if it appears in a conditional block that is always executed. Note that the identity is not marked as "speculative". ```au:output -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/conditional-present-in-method.md b/config/staticconfig/testdata/conditional-present-in-method.md index 8729d8b2..9374db01 100644 --- a/config/staticconfig/testdata/conditional-present-in-method.md +++ b/config/staticconfig/testdata/conditional-present-in-method.md @@ -5,7 +5,7 @@ entity's identity even if it appears after (but not within) a conditional statement. ```au:output -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/conditional.md b/config/staticconfig/testdata/conditional.md index 6b96e0a4..ef5b04ab 100644 --- a/config/staticconfig/testdata/conditional.md +++ b/config/staticconfig/testdata/conditional.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer includes information about an entity's identity when it is defined within a conditional statement. ```au:output -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid speculative identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/identity-from-const.md b/config/staticconfig/testdata/identity-from-const.md index 25975a13..9fc34db5 100644 --- a/config/staticconfig/testdata/identity-from-const.md +++ b/config/staticconfig/testdata/identity-from-const.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer can discover the values within an entity's identity when they are sourced from non-literal constant expressions. ```au:output -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/d0de39ba-aaaf-43fd-8e8f-7c4e3be309ec ``` diff --git a/config/staticconfig/testdata/identity-from-var.md b/config/staticconfig/testdata/identity-from-var.md index c026824c..30cd3e8a 100644 --- a/config/staticconfig/testdata/identity-from-var.md +++ b/config/staticconfig/testdata/identity-from-var.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer includes an entity's identity, even if it cannot determine the values used. ```au:output -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - incomplete identity ?/? ``` diff --git a/config/staticconfig/testdata/indirect.md b/config/staticconfig/testdata/indirect.md new file mode 100644 index 00000000..4b184a87 --- /dev/null +++ b/config/staticconfig/testdata/indirect.md @@ -0,0 +1,78 @@ +# Indirect configuration + +This test verifies that the static analyzer traverses into code called from the +`Configure()` method if that method is given access to the +`ApplicationConfigurer` interface. + +```au:output +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) + - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 +``` + +## Method call + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + a.setup(c) +} + +func (a App) setup(c dogma.ApplicationConfigurer) { + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + +## Function call + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + setup(c) +} + +func setup(c dogma.ApplicationConfigurer) { + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + +## Deferred + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + defer c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + +## Separate goroutine + +This test guarantees that the identity configured in a separate goroutine is +detected by the static analyzer, but this usage would like introduce a race +condition in any real `ApplicationConfigurer` implementation. + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + go c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` diff --git a/config/staticconfig/testdata/multiple-apps.md b/config/staticconfig/testdata/multiple-apps.md index 7224ea62..e3564558 100644 --- a/config/staticconfig/testdata/multiple-apps.md +++ b/config/staticconfig/testdata/multiple-apps.md @@ -4,10 +4,10 @@ This test verifies that the static analyzer discovers multiple Dogma application types defined within the same Go package. ```au:output -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.One (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.One (runtime type unavailable) - valid identity one/4fec74a1-6ed4-46f4-8417-01e0910be8f1 -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.Two (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.Two (runtime type unavailable) - valid identity two/6e97d403-3cb8-4a59-a7ec-74e8e219a7bc ``` diff --git a/config/staticconfig/testdata/no-handlers.md b/config/staticconfig/testdata/no-handlers.md index 83dd2959..a046cfdb 100644 --- a/config/staticconfig/testdata/no-handlers.md +++ b/config/staticconfig/testdata/no-handlers.md @@ -4,7 +4,7 @@ This test ensures that the static analyzer includes Dogma applications that have no handlers. ```au:output au:group="non-pointer" -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata/pkg.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 ``` From 98265eb998b15cee23755cdad5a112acc045acf5 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Oct 2024 07:11:50 +1000 Subject: [PATCH 07/38] Add tests for conditional defers. --- .../conditional-excluded-by-const-expr.md | 15 +++++++++++++++ .../conditional-included-by-const-expr.md | 16 ++++++++++++++++ config/staticconfig/testdata/conditional.md | 17 +++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md index 6acdafa0..a2b0bd8d 100644 --- a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md @@ -41,3 +41,18 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { } } ``` + +## In defer that is never scheduled + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + panic("prevent defer") + defer c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` diff --git a/config/staticconfig/testdata/conditional-included-by-const-expr.md b/config/staticconfig/testdata/conditional-included-by-const-expr.md index 3f1c7c28..cb981fc3 100644 --- a/config/staticconfig/testdata/conditional-included-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-included-by-const-expr.md @@ -42,3 +42,19 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { } } ``` + +## In defer that is scheduled conditionally + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if true { + defer c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + } +} +``` diff --git a/config/staticconfig/testdata/conditional.md b/config/staticconfig/testdata/conditional.md index ef5b04ab..7b773762 100644 --- a/config/staticconfig/testdata/conditional.md +++ b/config/staticconfig/testdata/conditional.md @@ -135,3 +135,20 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { } } ``` + +## Deferred + +```go au:input +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if rand.Int() == 0 { + defer c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + } +} +``` From e3b85d1d83b14d96dbe95c3e8cd277532f817c2f Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Oct 2024 08:37:26 +1000 Subject: [PATCH 08/38] Add some basic tests for generic types and functions. --- config/staticconfig/analyze.go | 2 +- config/staticconfig/testdata/indirect.md | 18 ++++++++++++++++++ config/staticconfig/testdata/no-apps.md | 20 ++++++++++++++++++++ config/staticconfig/testdata/no-handlers.md | 4 ++-- config/staticconfig/type.go | 20 ++++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 config/staticconfig/type.go diff --git a/config/staticconfig/analyze.go b/config/staticconfig/analyze.go index 2087a03f..002b3060 100644 --- a/config/staticconfig/analyze.go +++ b/config/staticconfig/analyze.go @@ -163,7 +163,7 @@ func packageOf(t types.Type) *types.Package { func analyzeType(ctx *context, m *ssa.Type) { t := m.Type() - if types.IsInterface(t) { + if isAbstract(t) { // We're only interested in concrete types; otherwise there's nothing to // analyze! return diff --git a/config/staticconfig/testdata/indirect.md b/config/staticconfig/testdata/indirect.md index 4b184a87..a40b6f82 100644 --- a/config/staticconfig/testdata/indirect.md +++ b/config/staticconfig/testdata/indirect.md @@ -45,6 +45,24 @@ func setup(c dogma.ApplicationConfigurer) { } ``` +## Generic function call + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + setup(c) +} + +func setup[T dogma.ApplicationConfigurer](c T) { + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") +} +``` + ## Deferred ```go au:input diff --git a/config/staticconfig/testdata/no-apps.md b/config/staticconfig/testdata/no-apps.md index c623a07b..9d66f639 100644 --- a/config/staticconfig/testdata/no-apps.md +++ b/config/staticconfig/testdata/no-apps.md @@ -48,3 +48,23 @@ type App interface { Configure(c dogma.ApplicationConfigurer) } ``` + +## Uninstantiated generic application + +We can't analyze this code because the application is generic and not +instantiated, meaning that we have no concrete type for `T`. We _could_ chose a +compatible type for `T` and analyze the result of instantiating the generic +type, but the assumption is that the reason the type is generic is because the +application is intended to be used with multiple types. + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App[T any] struct{} + +func (App[T]) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "8a6baab1-ee64-402e-a081-e43f4bebc243") +} +``` diff --git a/config/staticconfig/testdata/no-handlers.md b/config/staticconfig/testdata/no-handlers.md index a046cfdb..592966a3 100644 --- a/config/staticconfig/testdata/no-handlers.md +++ b/config/staticconfig/testdata/no-handlers.md @@ -3,12 +3,12 @@ This test ensures that the static analyzer includes Dogma applications that have no handlers. -```au:output au:group="non-pointer" +```au:output valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 ``` -```go au:input au:group="non-pointer" +```go au:input package app import "github.com/dogmatiq/dogma" diff --git a/config/staticconfig/type.go b/config/staticconfig/type.go new file mode 100644 index 00000000..12c8ae51 --- /dev/null +++ b/config/staticconfig/type.go @@ -0,0 +1,20 @@ +package staticconfig + +import "go/types" + +func isAbstract(t types.Type) bool { + if types.IsInterface(t) { + return true + } + + // Check if the type is a generic type that has not been instantiated + // (meaning that it has no concrete values for its type parameters). + switch t := t.(type) { + case *types.Named: + return t.Origin() == t && t.TypeParams().Len() != 0 + case *types.Alias: + return t.Origin() == t && t.TypeParams().Len() != 0 + } + + return false +} From a543e56784d603b2ab3442122251854ed48ed282 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Oct 2024 09:53:58 +1000 Subject: [PATCH 09/38] Add tests for incomplete / unreachable code. --- config/staticconfig/application.go | 2 +- config/staticconfig/configurer.go | 72 +++++++++++++------ config/staticconfig/context.go | 14 ---- .../testdata/_pending/_alias-for-generic.md | 22 ++++++ config/staticconfig/testdata/incomplete.md | 45 ++++++++++++ 5 files changed, 118 insertions(+), 37 deletions(-) create mode 100644 config/staticconfig/testdata/_pending/_alias-for-generic.md create mode 100644 config/staticconfig/testdata/incomplete.md diff --git a/config/staticconfig/application.go b/config/staticconfig/application.go index 672d8d20..c6ac8f94 100644 --- a/config/staticconfig/application.go +++ b/config/staticconfig/application.go @@ -17,7 +17,7 @@ func analyzeApplication(ctx *context, t types.Type) { func(b *configbuilder.ApplicationBuilder) { b.SetSourceTypeName(typename.OfStatic(t)) - for call := range findConfigurerCalls(ctx, t) { + for call := range findConfigurerCalls(ctx, b, t) { switch call.Method.Name() { case "Identity": analyzeIdentityCall(b, call) diff --git a/config/staticconfig/configurer.go b/config/staticconfig/configurer.go index 7eba528d..66e06af2 100644 --- a/config/staticconfig/configurer.go +++ b/config/staticconfig/configurer.go @@ -5,9 +5,31 @@ import ( "iter" "github.com/dogmatiq/enginekit/config" + "github.com/dogmatiq/enginekit/config/internal/configbuilder" "golang.org/x/tools/go/ssa" ) +// configureContext is a specialization of [context] that is used when analyzing +// a Configure() method. +type configureContext struct { + *context + + Builder configbuilder.EntityBuilder + ConfigurerIndices []int +} + +func (c *configureContext) IsConfigurer(v ssa.Value) bool { + params := v.Parent().Params + + for _, i := range c.ConfigurerIndices { + if v == params[i] { + return true + } + } + + return false +} + type configurerCall struct { *ssa.CallCommon @@ -18,17 +40,23 @@ type configurerCall struct { // to t's "Configure()" method. // // Any calls that are not recognized are yielded. -func findConfigurerCalls(ctx *context, t types.Type) iter.Seq[configurerCall] { +func findConfigurerCalls( + ctx *context, + b configbuilder.EntityBuilder, + t types.Type, +) iter.Seq[configurerCall] { configure := ctx.LookupMethod(t, "Configure") - ctx = ctx.NewChild( - func(v ssa.Value) bool { - return v == configure.Params[1] - }, - ) - return func(yield func(configurerCall) bool) { - emitConfigurerCallsInFunc(ctx, configure, yield) + emitConfigurerCallsInFunc( + &configureContext{ + context: ctx, + Builder: b, + ConfigurerIndices: []int{1}, + }, + configure, + yield, + ) } } @@ -38,7 +66,7 @@ func findConfigurerCalls(ctx *context, t types.Type) iter.Seq[configurerCall] { // indices is a list of the positions of parameters to fn that are the // configurer. func emitConfigurerCallsInFunc( - ctx *context, + ctx *configureContext, fn *ssa.Function, yield func(configurerCall) bool, ) bool { @@ -58,7 +86,7 @@ func emitConfigurerCallsInFunc( } func emitConfigurerCallsInInstruction( - ctx *context, + ctx *configureContext, inst ssa.Instruction, yield func(configurerCall) bool, ) bool { @@ -71,7 +99,7 @@ func emitConfigurerCallsInInstruction( } func emitConfigurerCallsInCallInstruction( - ctx *context, + ctx *configureContext, call ssa.CallInstruction, yield func(configurerCall) bool, ) bool { @@ -82,6 +110,7 @@ func emitConfigurerCallsInCallInstruction( // concrete type. If it's not a call to a method on the configurer, // there's nothing more we can analyze. if !ctx.IsConfigurer(com.Value) { + ctx.Builder.UpdateFidelity(config.Incomplete) return true } @@ -104,6 +133,11 @@ func emitConfigurerCallsInCallInstruction( // inside a context, or assigned to a field within the entity struct. fn := com.StaticCallee() + if fn == nil { + ctx.Builder.UpdateFidelity(config.Incomplete) + return true + } + // Check at which argument indices the configurer is passed to the function. var indices []int for i, arg := range com.Args { @@ -118,17 +152,11 @@ func emitConfigurerCallsInCallInstruction( } return emitConfigurerCallsInFunc( - ctx.NewChild( - func(v ssa.Value) bool { - for _, i := range indices { - if v == fn.Params[i] { - return true - } - } - - return false - }, - ), + &configureContext{ + context: ctx.context, + Builder: ctx.Builder, + ConfigurerIndices: indices, + }, fn, yield, ) diff --git a/config/staticconfig/context.go b/config/staticconfig/context.go index 67b04d14..93517176 100644 --- a/config/staticconfig/context.go +++ b/config/staticconfig/context.go @@ -18,8 +18,6 @@ type context struct { } Analysis *Analysis - - IsConfigurer func(ssa.Value) bool } // findDogma updates ctx with information about the Dogma package. @@ -56,15 +54,3 @@ func (c *context) LookupMethod(t types.Type, name string) *ssa.Function { } return fn } - -func (c *context) NewChild( - isConfigurer func(ssa.Value) bool, -) *context { - return &context{ - Program: c.Program, - Packages: c.Packages, - Dogma: c.Dogma, - Analysis: c.Analysis, - IsConfigurer: isConfigurer, - } -} diff --git a/config/staticconfig/testdata/_pending/_alias-for-generic.md b/config/staticconfig/testdata/_pending/_alias-for-generic.md new file mode 100644 index 00000000..499361a8 --- /dev/null +++ b/config/staticconfig/testdata/_pending/_alias-for-generic.md @@ -0,0 +1,22 @@ +# Uninstatiated generic application + +This test ensures that the static analyzer does not fail if it encounters a +generic type that implements the `dogma.Application` interface. + +```au:output +(no applications found) +``` + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App[T any] struct{} + +func (App[T]) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "8a6baab1-ee64-402e-a081-e43f4bebc243") +} + +type Alias = App[int] +``` diff --git a/config/staticconfig/testdata/incomplete.md b/config/staticconfig/testdata/incomplete.md new file mode 100644 index 00000000..d7b0ff88 --- /dev/null +++ b/config/staticconfig/testdata/incomplete.md @@ -0,0 +1,45 @@ +# Incomplete configuration + +This test verifies that the static analyzer marks configuration as incomplete if +the `Configure()` method calls into code that is unable to be analyzed. + +```au:output +incomplete application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) + - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 +``` + +## Function call + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct { + setup func(dogma.ApplicationConfigurer) +} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + a.setup(c) +} +``` + +## Method call on interface + +```go au:input +package app + +import "github.com/dogmatiq/dogma" + +type App struct { + iface interface { + setup(dogma.ApplicationConfigurer) + } +} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + a.iface.setup(c) +} +``` From 23b82dd0cae775164bb361c5c67d89e4ee67c277 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Oct 2024 14:41:30 +1000 Subject: [PATCH 10/38] Improve resolution of indirect constant values. --- config/staticconfig/analyze_test.go | 30 +++--- config/staticconfig/configurer.go | 44 ++++----- config/staticconfig/identity.go | 94 ++++++++++++++++++- .../testdata/_pending/_alias-for-generic.md | 4 +- .../testdata/_pending/aliased-handlers.md | 4 +- .../conditional-routes-in-handlers.md | 4 +- .../_pending/dynamic-routes-in-handlers.md | 4 +- .../testdata/_pending/generic-handler.md | 4 +- .../testdata/_pending/handler-adaptor-test.md | 4 +- .../_pending/handler-constructor-test.md | 4 +- .../testdata/_pending/handler-from-field.md | 4 +- .../testdata/_pending/iface-configurer.md | 4 +- .../testdata/_pending/invalid-syntax.md | 4 +- .../_pending/multiple-handlers-of-a-kind.md | 4 +- .../testdata/_pending/nil-handlers.md | 4 +- .../_pending/nil-routes-in-handlers.md | 4 +- ...pointer-handlers-registered-as-pointers.md | 4 +- .../testdata/_pending/pointer-receiver-app.md | 8 +- .../unregistered-routes-in-handlers.md | 4 +- .../conditional-excluded-by-const-expr.md | 10 +- .../conditional-included-by-const-expr.md | 10 +- .../testdata/conditional-present-in-method.md | 12 +-- config/staticconfig/testdata/conditional.md | 18 ++-- .../testdata/identity-from-const.md | 4 +- .../identity-from-non-const-static.md | 69 ++++++++++++++ .../testdata/identity-from-non-const.md | 65 +++++++++++++ .../testdata/identity-from-var.md | 24 ----- .../{incomplete.md => incomplete-entity.md} | 17 ++-- config/staticconfig/testdata/indirect.md | 12 +-- config/staticconfig/testdata/multiple-apps.md | 4 +- config/staticconfig/testdata/no-apps.md | 10 +- config/staticconfig/testdata/no-handlers.md | 4 +- go.mod | 14 +-- go.sum | 58 ++++++++++++ 34 files changed, 413 insertions(+), 154 deletions(-) create mode 100644 config/staticconfig/testdata/identity-from-non-const-static.md create mode 100644 config/staticconfig/testdata/identity-from-non-const.md delete mode 100644 config/staticconfig/testdata/identity-from-var.md rename config/staticconfig/testdata/{incomplete.md => incomplete-entity.md} (73%) diff --git a/config/staticconfig/analyze_test.go b/config/staticconfig/analyze_test.go index 54f48cec..773c2eee 100644 --- a/config/staticconfig/analyze_test.go +++ b/config/staticconfig/analyze_test.go @@ -15,41 +15,49 @@ import ( func TestAnalyzer(t *testing.T) { aureus.Run( t, - func(w io.Writer, in aureus.Content, out aureus.ContentMetaData) error { + func(t *testing.T, in aureus.Input, out aureus.Output) error { + t.Parallel() + // Create a temporary directory to write the Go source code, but // create it within this Go module so that it uses the same version // of Dogma, etc. - dir, err := os.MkdirTemp(filepath.Dir(in.File), "aureus-") + dir, err := os.MkdirTemp("testdata", "aureus-") if err != nil { return err } defer os.RemoveAll(dir) - if err := os.WriteFile( - filepath.Join(dir, "main.go"), - []byte(in.Data), - 0600, - ); err != nil { + f, err := os.Create(filepath.Join(dir, "main.go")) + if err != nil { + return err + } + defer f.Close() + + if _, err := io.Copy(f, in); err != nil { + return err + } + + if err := f.Close(); err != nil { return err } result := LoadAndAnalyze(dir) if len(result.Applications) == 0 { - if _, err := io.WriteString(w, "(no applications found)\n"); err != nil { + if _, err := io.WriteString(out, "(no applications found)\n"); err != nil { return err } } for err := range result.Errors() { - if _, err := io.WriteString(w, err.Error()+"\n"); err != nil { + if _, err := io.WriteString(out, err.Error()+"\n"); err != nil { return err } } for i, app := range result.Applications { if i > 0 { - if _, err := io.WriteString(w, "\n"); err != nil { + if _, err := io.WriteString(out, "\n"); err != nil { return err } } @@ -65,7 +73,7 @@ func TestAnalyzer(t *testing.T) { ".", ) - if _, err := io.WriteString(w, details); err != nil { + if _, err := io.WriteString(out, details); err != nil { return err } } diff --git a/config/staticconfig/configurer.go b/config/staticconfig/configurer.go index 66e06af2..be14f33b 100644 --- a/config/staticconfig/configurer.go +++ b/config/staticconfig/configurer.go @@ -105,15 +105,7 @@ func emitConfigurerCallsInCallInstruction( ) bool { com := call.Common() - if com.IsInvoke() { - // We're invoking a method on an interface, that is, we don't know the - // concrete type. If it's not a call to a method on the configurer, - // there's nothing more we can analyze. - if !ctx.IsConfigurer(com.Value) { - ctx.Builder.UpdateFidelity(config.Incomplete) - return true - } - + if com.IsInvoke() && ctx.IsConfigurer(com.Value) { // We've found a direct call to a method on the configurer. var f config.Fidelity if isConditional(call.Block()) { @@ -123,22 +115,17 @@ func emitConfigurerCallsInCallInstruction( return yield(configurerCall{com, f}) } - // We've found a call to some other function or method. + // 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. // - // If any of the parameters refer to the configurer, we need to analyze - // _that_ function. + // 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. // - // This is an native implementation. There are other ways that this function - // could gain access to the configurer. For example, it could be passed - // inside a context, or assigned to a field within the entity struct. - fn := com.StaticCallee() - - if fn == nil { - ctx.Builder.UpdateFidelity(config.Incomplete) - return true - } - - // Check at which argument indices the configurer is passed to the function. + // 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) { @@ -146,11 +133,20 @@ func emitConfigurerCallsInCallInstruction( } } - // Don't analyze fn if the configurer is not passed as an argument. + // 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, diff --git a/config/staticconfig/identity.go b/config/staticconfig/identity.go index beac1af8..009a5ac1 100644 --- a/config/staticconfig/identity.go +++ b/config/staticconfig/identity.go @@ -2,6 +2,7 @@ package staticconfig import ( "go/constant" + "go/token" "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/config/internal/configbuilder" @@ -15,16 +16,101 @@ func analyzeIdentityCall( b.Identity(func(b *configbuilder.IdentityBuilder) { b.UpdateFidelity(call.Fidelity) - if name, ok := call.Args[0].(*ssa.Const); ok { - b.SetName(constant.StringVal(name.Value)) + if name, ok := resolveValue(call.Args[0]); ok { + b.SetName(constant.StringVal(name)) } else { b.UpdateFidelity(config.Incomplete) } - if key, ok := call.Args[1].(*ssa.Const); ok { - b.SetKey(constant.StringVal(key.Value)) + if key, ok := resolveValue(call.Args[1]); ok { + b.SetKey(constant.StringVal(key)) } else { b.UpdateFidelity(config.Incomplete) } }) } + +func resolveValue( + v ssa.Value, +) (constant.Value, bool) { + switch v := v.(type) { + case *ssa.Const: + return v.Value, true + case ssa.Instruction: + values := resolveExpr(v) + switch len(values) { + case 0: + return nil, false + case 1: + return values[0], values[0] != nil + default: + panic("did not expect multiple values") + } + default: + return nil, false + } +} + +func resolveExpr( + inst ssa.Instruction, +) []constant.Value { + switch inst := inst.(type) { + case *ssa.Call: + return resolveReturnValues(inst.Common()) + case *ssa.Extract: + if expr, ok := inst.Tuple.(ssa.Instruction); ok { + values := resolveExpr(expr) + return values[inst.Index : inst.Index+1] + } + return nil + default: + return nil + } +} + +func resolveReturnValues(call *ssa.CallCommon) []constant.Value { + fn := call.StaticCallee() + if fn == nil { + return nil + } + + if len(fn.Blocks) == 0 { + return nil + } + + if fn.Signature.Results().Len() == 0 { + return nil + } + + var results []constant.Value + + for b := range walkReachable(fn.Blocks[0]) { + inst, ok := b.Instrs[len(b.Instrs)-1].(*ssa.Return) + if !ok { + continue + } + + var values []constant.Value + + for _, v := range inst.Results { + if x, ok := resolveValue(v); ok { + values = append(values, x) + } else { + values = append(values, nil) + } + } + + if results == nil { + results = values + } else { + for i, a := range values { + b := results[i] + if constant.Compare(a, token.NEQ, b) { + results[i] = nil + } + } + } + } + + return results +} diff --git a/config/staticconfig/testdata/_pending/_alias-for-generic.md b/config/staticconfig/testdata/_pending/_alias-for-generic.md index 499361a8..3c9614ed 100644 --- a/config/staticconfig/testdata/_pending/_alias-for-generic.md +++ b/config/staticconfig/testdata/_pending/_alias-for-generic.md @@ -3,11 +3,11 @@ This test ensures that the static analyzer does not fail if it encounters a generic type that implements the `dogma.Application` interface. -```au:output +```au:output au:group=matrix (no applications found) ``` -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" diff --git a/config/staticconfig/testdata/_pending/aliased-handlers.md b/config/staticconfig/testdata/_pending/aliased-handlers.md index fe777c05..8de00e09 100644 --- a/config/staticconfig/testdata/_pending/aliased-handlers.md +++ b/config/staticconfig/testdata/_pending/aliased-handlers.md @@ -3,7 +3,7 @@ This test verifies that static analysis can correctly parse handlers that are declared as type aliases. -```go au:input +```go au:input au:group=matrix package app import ( @@ -52,7 +52,7 @@ func (IntegrationHandler) HandleCommand( } ``` -```au:output +```au:output au:group=matrix application (1b828a1c-eba1-4e4c-88b8-e49f78ad15c7) App - integration (4d8cd3f5-21dc-475b-a8dc-80138adde3f2) IntegrationHandlerAlias diff --git a/config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md b/config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md index 2a95694c..98cda2f1 100644 --- a/config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md +++ b/config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md @@ -3,7 +3,7 @@ This test verifies that static analysis correctly parses handles that have conditional routes within their bodies. -```go au:input +```go au:input au:group=matrix package app import ( @@ -56,7 +56,7 @@ func (IntegrationHandler) HandleCommand( ``` -```au:output +```au:output au:group=matrix application (7e34538e-c407-4af8-8d3c-960e09cde98a) App - integration (92cce461-8d30-409b-8d5a-406f656cef2d) IntegrationHandler diff --git a/config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md b/config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md index 77421c05..d91ab685 100644 --- a/config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md +++ b/config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md @@ -3,7 +3,7 @@ This test verifies that static analysis correctly parses routes in handles that are dynamically populated. -```go au:input +```go au:input au:group=matrix package app import ( @@ -49,7 +49,7 @@ func (IntegrationHandler) HandleCommand( } ``` -```au:output +```au:output au:group=matrix application (3bc3849b-abe0-4c4e-9db4-e48dc28c9a26) App - integration (3a06b7da-1079-4e4b-a6a6-064c62241918) IntegrationHandler diff --git a/config/staticconfig/testdata/_pending/generic-handler.md b/config/staticconfig/testdata/_pending/generic-handler.md index a8cc8b03..291dfa1f 100644 --- a/config/staticconfig/testdata/_pending/generic-handler.md +++ b/config/staticconfig/testdata/_pending/generic-handler.md @@ -3,7 +3,7 @@ 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 +```go au:input au:group=matrix package app import ( @@ -61,7 +61,7 @@ func (a App) Configure(c ApplicationConfigurer) { ``` -```au:output +```au:output au:group=matrix application (e522c782-48d2-4c47-a4c9-81e0d7cdeba0) App - integration (abc7c329-c9da-4161-a8e2-6ab45be2dd83) *InstantiatedIntegration diff --git a/config/staticconfig/testdata/_pending/handler-adaptor-test.md b/config/staticconfig/testdata/_pending/handler-adaptor-test.md index 94c197fe..ce495420 100644 --- a/config/staticconfig/testdata/_pending/handler-adaptor-test.md +++ b/config/staticconfig/testdata/_pending/handler-adaptor-test.md @@ -2,7 +2,7 @@ This test verifies that static analysis correctly parses handler adaptors. -```go au:input +```go au:input au:group=matrix package app import ( @@ -49,7 +49,7 @@ func AdaptIntegration(PartialIntegrationMessageHandler) IntegrationMessageHandle } ``` -```au:output +```au:output au:group=matrix application (f610eae4-f5d0-4eea-a9c9-6cbbfa9b2060) App - integration (099b5b8d-9e04-422f-bcc3-bb0d451158c7) IntegrationHandler diff --git a/config/staticconfig/testdata/_pending/handler-constructor-test.md b/config/staticconfig/testdata/_pending/handler-constructor-test.md index b129a07e..4ef88110 100644 --- a/config/staticconfig/testdata/_pending/handler-constructor-test.md +++ b/config/staticconfig/testdata/_pending/handler-constructor-test.md @@ -2,7 +2,7 @@ This test verifies that static analysis correctly parses handler constructors. -```go au:input +```go au:input au:group=matrix package app import ( @@ -51,7 +51,7 @@ func (IntegrationHandler) HandleCommand( } ``` -```au:output +```au:output au:group=matrix application (3bc3849b-abe0-4c4e-9db4-e48dc28c9a26) App - integration (099b5b8d-9e04-422f-bcc3-bb0d451158c7) IntegrationHandler diff --git a/config/staticconfig/testdata/_pending/handler-from-field.md b/config/staticconfig/testdata/_pending/handler-from-field.md index f6e0f0d1..118b4ccf 100644 --- a/config/staticconfig/testdata/_pending/handler-from-field.md +++ b/config/staticconfig/testdata/_pending/handler-from-field.md @@ -4,7 +4,7 @@ This test ensures that the static analyzer can recognized the type of a handler when it is registered using the value of a struct field, rather than constructed inline. -```go au:input +```go au:input au:group=matrix package app import ( @@ -38,7 +38,7 @@ func (Handler) HandleCommand( } ``` -```au:output +```au:output au:group=matrix application (7468a57f-20f0-4d11-9aad-48fcd553a908) App - integration (195ede4a-3f26-4d19-a8fe-41b2a5f92d06) Handler diff --git a/config/staticconfig/testdata/_pending/iface-configurer.md b/config/staticconfig/testdata/_pending/iface-configurer.md index ea0a5133..3dbff468 100644 --- a/config/staticconfig/testdata/_pending/iface-configurer.md +++ b/config/staticconfig/testdata/_pending/iface-configurer.md @@ -5,7 +5,7 @@ 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 +```go au:input au:group=matrix package app import ( @@ -28,6 +28,6 @@ func (a App) Configure(c ApplicationConfigurer) { ``` -```au:output +```au:output au:group=matrix application (7468a57f-20f0-4d11-9aad-48fcd553a908) App ``` diff --git a/config/staticconfig/testdata/_pending/invalid-syntax.md b/config/staticconfig/testdata/_pending/invalid-syntax.md index c1e224dd..f0f745cd 100644 --- a/config/staticconfig/testdata/_pending/invalid-syntax.md +++ b/config/staticconfig/testdata/_pending/invalid-syntax.md @@ -3,7 +3,7 @@ This test verifies that static analysis panics when it encounters incorrect syntax. -```go au:input +```go au:input au:group=matrix package app // Even though this file has invalid syntax the import statements are still @@ -17,6 +17,6 @@ import "github.com/dogmatiq/dogma" ``` -```au:output +```au:output au:group=matrix expected declaration, found '<' ``` diff --git a/config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md b/config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md index 144b53cd..61165024 100644 --- a/config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md +++ b/config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md @@ -3,7 +3,7 @@ This test verifies that static analysis can correctly parse multiple handlers of a same kind. -```go au:input +```go au:input au:group=matrix package app import ( @@ -83,7 +83,7 @@ func (SecondIntegrationHandler) HandleCommand( } ``` -```au:output +```au:output au:group=matrix application (8961f548-1afc-4996-894c-956835c83199) App - integration (14cf2812-eead-43b3-9c9c-10db5b469e94) FirstIntegrationHandler diff --git a/config/staticconfig/testdata/_pending/nil-handlers.md b/config/staticconfig/testdata/_pending/nil-handlers.md index e69821d5..5757e535 100644 --- a/config/staticconfig/testdata/_pending/nil-handlers.md +++ b/config/staticconfig/testdata/_pending/nil-handlers.md @@ -3,7 +3,7 @@ This test ensures that the static analyzer ignores handlers that are `nil`, but still includes the application itself. -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" @@ -20,6 +20,6 @@ func (App) Configure(c dogma.ApplicationConfigurer) { } ``` -```au:output +```au:output au:group=matrix application (0726ae0d-67e4-4a50-8a19-9f58eae38e51) App ``` diff --git a/config/staticconfig/testdata/_pending/nil-routes-in-handlers.md b/config/staticconfig/testdata/_pending/nil-routes-in-handlers.md index 8dcc7461..7f02a313 100644 --- a/config/staticconfig/testdata/_pending/nil-routes-in-handlers.md +++ b/config/staticconfig/testdata/_pending/nil-routes-in-handlers.md @@ -3,7 +3,7 @@ This test verifies that static analysis correctly parses `nil` routes inside Dogma Application handlers. -```go au:input +```go au:input au:group=matrix package app import ( @@ -43,7 +43,7 @@ func (IntegrationHandler) HandleCommand( } ``` -```au:output +```au:output au:group=matrix application (c100edcc-6dcc-42ed-ac75-69eecb3d0ec4) App - integration (363039e5-2938-4b2c-9bec-dcb29dee2da1) IntegrationHandler diff --git a/config/staticconfig/testdata/_pending/non-pointer-handlers-registered-as-pointers.md b/config/staticconfig/testdata/_pending/non-pointer-handlers-registered-as-pointers.md index 4cf1dc6c..260397ac 100644 --- a/config/staticconfig/testdata/_pending/non-pointer-handlers-registered-as-pointers.md +++ b/config/staticconfig/testdata/_pending/non-pointer-handlers-registered-as-pointers.md @@ -3,7 +3,7 @@ This test verifies that static analysis can correctly parse non-pointer handlers registered in a dogma application as pointers using 'address-of' operator. -```go au:input +```go au:input au:group=matrix package app import ( @@ -50,7 +50,7 @@ func (IntegrationHandler) HandleCommand( } ``` -```au:output +```au:output au:group=matrix application (282653ad-9343-44f1-889e-a8b2b095b54b) App - integration (1425ca64-0448-4bfd-b18d-9fe63a95995f) *IntegrationHandler diff --git a/config/staticconfig/testdata/_pending/pointer-receiver-app.md b/config/staticconfig/testdata/_pending/pointer-receiver-app.md index bf735697..812045ca 100644 --- a/config/staticconfig/testdata/_pending/pointer-receiver-app.md +++ b/config/staticconfig/testdata/_pending/pointer-receiver-app.md @@ -5,12 +5,12 @@ no handlers. ## With non-pointer receiver -```au:output au:group="non-pointer" +```au:output au:group=matrix au:group="non-pointer" valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 ``` -```go au:input au:group="non-pointer" +```go au:input au:group=matrix au:group="non-pointer" package app import "github.com/dogmatiq/dogma" @@ -24,12 +24,12 @@ func (App) Configure(c dogma.ApplicationConfigurer) { ## With pointer receiver -```au:output au:group="pointer" +```au:output au:group=matrix au:group="pointer" valid application *github.com/dogmatiq/enginekit/config/staticconfig/testdata/empty-app.App (runtime type unavailable) - valid identity app/d196eb7a-bad4-4826-8763-db1111882fbd ``` -```go au:input au:group="pointer" +```go au:input au:group=matrix au:group="pointer" package app import "github.com/dogmatiq/dogma" diff --git a/config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md b/config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md index 459e8b03..34d04ddd 100644 --- a/config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md +++ b/config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md @@ -3,7 +3,7 @@ This test verifies that static analysis ignores unregistered routes in Dogma application handlers. -```go au:input +```go au:input au:group=matrix package app import ( @@ -50,7 +50,7 @@ func (IntegrationHandler) HandleCommand( } ``` -```au:output +```au:output au:group=matrix application (f2c08525-623e-4c76-851c-3172953269e3) App - integration (ac391765-da58-4e7c-a478-e4725eb2b0e9) IntegrationHandler diff --git a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md index a2b0bd8d..93c386ae 100644 --- a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md @@ -1,16 +1,16 @@ -# Conditional w/ constant expression that excludes configuration +# Conditional with constant expression that excludes configuration This test verifies that the static analyzer excludes information about an entity's identity if it appears in an unreachable branch. -```au:output +```au:output au:group=matrix invalid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - no identity is configured ``` ## After conditional return -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" @@ -28,7 +28,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## Within conditional block -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" @@ -44,7 +44,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## In defer that is never scheduled -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" diff --git a/config/staticconfig/testdata/conditional-included-by-const-expr.md b/config/staticconfig/testdata/conditional-included-by-const-expr.md index cb981fc3..ae072634 100644 --- a/config/staticconfig/testdata/conditional-included-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-included-by-const-expr.md @@ -1,17 +1,17 @@ -# Conditional w/ constant expression that includes configuration +# Conditional with constant expression that includes configuration This test verifies that the static analyzer includes information about an entity's identity if it appears in a conditional block that is always executed. Note that the identity is not marked as "speculative". -```au:output +```au:output au:group=matrix valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` ## After conditional return -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" @@ -29,7 +29,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## Within conditional block -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" @@ -45,7 +45,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## In defer that is scheduled conditionally -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" diff --git a/config/staticconfig/testdata/conditional-present-in-method.md b/config/staticconfig/testdata/conditional-present-in-method.md index 9374db01..c6a75e63 100644 --- a/config/staticconfig/testdata/conditional-present-in-method.md +++ b/config/staticconfig/testdata/conditional-present-in-method.md @@ -4,14 +4,14 @@ This test verifies that the static analyzer includes information about an entity's identity even if it appears after (but not within) a conditional statement. -```au:output +```au:output au:group=matrix valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` ## If statement -```go au:input +```go au:input au:group=matrix package app import "math/rand" @@ -29,7 +29,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## Else statement -```go au:input +```go au:input au:group=matrix package app import "math/rand" @@ -48,7 +48,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## Switch statement -```go au:input +```go au:input au:group=matrix package app import "math/rand" @@ -67,7 +67,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## For statement -```go au:input +```go au:input au:group=matrix package app import "math/rand" @@ -85,7 +85,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## Select statement -```go au:input +```go au:input au:group=matrix package app import "math/rand" diff --git a/config/staticconfig/testdata/conditional.md b/config/staticconfig/testdata/conditional.md index 7b773762..73c661e8 100644 --- a/config/staticconfig/testdata/conditional.md +++ b/config/staticconfig/testdata/conditional.md @@ -3,14 +3,14 @@ This test verifies that the static analyzer includes information about an entity's identity when it is defined within a conditional statement. -```au:output +```au:output au:group=matrix valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid speculative identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` ## If statement -```go au:input +```go au:input au:group=matrix package app import "math/rand" @@ -27,7 +27,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## Else statement -```go au:input +```go au:input au:group=matrix package app import "math/rand" @@ -45,7 +45,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## After conditional return -```go au:input +```go au:input au:group=matrix package app import "math/rand" @@ -64,7 +64,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## After conditonal panic -```go au:input +```go au:input au:group=matrix package app import "math/rand" @@ -83,7 +83,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## Switch statement -```go au:input +```go au:input au:group=matrix package app import "math/rand" @@ -101,7 +101,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## For statement -```go au:input +```go au:input au:group=matrix package app import "math/rand" @@ -118,7 +118,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## Select statement -```go au:input +```go au:input au:group=matrix package app import "math/rand" @@ -138,7 +138,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { ## Deferred -```go au:input +```go au:input au:group=matrix package app import "math/rand" diff --git a/config/staticconfig/testdata/identity-from-const.md b/config/staticconfig/testdata/identity-from-const.md index 9fc34db5..e3570b38 100644 --- a/config/staticconfig/testdata/identity-from-const.md +++ b/config/staticconfig/testdata/identity-from-const.md @@ -3,12 +3,12 @@ This test verifies that the static analyzer can discover the values within an entity's identity when they are sourced from non-literal constant expressions. -```au:output +```au:output au:group=matrix valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/d0de39ba-aaaf-43fd-8e8f-7c4e3be309ec ``` -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" diff --git a/config/staticconfig/testdata/identity-from-non-const-static.md b/config/staticconfig/testdata/identity-from-non-const-static.md new file mode 100644 index 00000000..a7aebcb3 --- /dev/null +++ b/config/staticconfig/testdata/identity-from-non-const-static.md @@ -0,0 +1,69 @@ +# Identity built from non-constant values that can be resolved statically + +This test verifies that the static analyzer includes an entity's identity, even +if it cannot determine the values used. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) + - valid identity app/a0a0edb7-ce45-4eb4-940c-0f77459ae2a0 +``` + +## Function call + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" + +type App struct { +} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + c.Identity(name(), "a0a0edb7-ce45-4eb4-940c-0f77459ae2a0") +} + +func name() string { + return "app" +} +``` + +## Function call with tuple extraction + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" + +type App struct { +} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + c.Identity(ident()) +} + +func ident() (string,string) { + return "app", "a0a0edb7-ce45-4eb4-940c-0f77459ae2a0" +} +``` + +## Function call with multiple branches that return the same values + +```go au:input au:group=matrix +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + c.Identity(ident()) +} + +func ident() (string, string) { + if rand.Int() == 0 { + return "app", "a0a0edb7-ce45-4eb4-940c-0f77459ae2a0" + } + return "app", "a0a0edb7-ce45-4eb4-940c-0f77459ae2a0" +} +``` diff --git a/config/staticconfig/testdata/identity-from-non-const.md b/config/staticconfig/testdata/identity-from-non-const.md new file mode 100644 index 00000000..def26d84 --- /dev/null +++ b/config/staticconfig/testdata/identity-from-non-const.md @@ -0,0 +1,65 @@ +# Identity built from non-constant values + +This test verifies that the static analyzer includes an entity's identity, even +if it cannot determine the values used. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) + - incomplete identity ?/? +``` + +## Variables + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" + +type App struct { + Name string + Key string +} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + c.Identity(a.Name, a.Key) +} +``` + +## Function call + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" + +type App struct { + name func() string + key func() string +} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + c.Identity(a.name(), a.key()) +} +``` + +## Function call with a non-deterministic return value + +```go au:input au:group=matrix +package app + +import "math/rand" +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + c.Identity(ident()) +} + +func ident() (string, string) { + if rand.Int() == 0 { + return "app1", "a0a0edb7-ce45-4eb4-940c-0f77459ae2a0" + } + return "app2", "08905dce-9059-4601-a48f-f449c6fba70b" +} +``` diff --git a/config/staticconfig/testdata/identity-from-var.md b/config/staticconfig/testdata/identity-from-var.md deleted file mode 100644 index 30cd3e8a..00000000 --- a/config/staticconfig/testdata/identity-from-var.md +++ /dev/null @@ -1,24 +0,0 @@ -# Identity built from variables - -This test verifies that the static analyzer includes an entity's identity, even -if it cannot determine the values used. - -```au:output -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - - incomplete identity ?/? -``` - -```go au:input -package app - -import "github.com/dogmatiq/dogma" - -type App struct { - Name string - Key string -} - -func (a App) Configure(c dogma.ApplicationConfigurer) { - c.Identity(a.Name, a.Name) -} -``` diff --git a/config/staticconfig/testdata/incomplete.md b/config/staticconfig/testdata/incomplete-entity.md similarity index 73% rename from config/staticconfig/testdata/incomplete.md rename to config/staticconfig/testdata/incomplete-entity.md index d7b0ff88..6d610509 100644 --- a/config/staticconfig/testdata/incomplete.md +++ b/config/staticconfig/testdata/incomplete-entity.md @@ -1,16 +1,17 @@ -# Incomplete configuration +# Incomplete entity configuration -This test verifies that the static analyzer marks configuration as incomplete if -the `Configure()` method calls into code that is unable to be analyzed. +This test verifies that the static analyzer marks configuration of an entity as +incomplete if the `Configure()` method calls into code that is unable to be +analyzed. -```au:output +```au:output au:group=matrix incomplete application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` -## Function call +## Call to closure -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" @@ -25,9 +26,9 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { } ``` -## Method call on interface +## Call to method on interface -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" diff --git a/config/staticconfig/testdata/indirect.md b/config/staticconfig/testdata/indirect.md index a40b6f82..062799e5 100644 --- a/config/staticconfig/testdata/indirect.md +++ b/config/staticconfig/testdata/indirect.md @@ -4,14 +4,14 @@ This test verifies that the static analyzer traverses into code called from the `Configure()` method if that method is given access to the `ApplicationConfigurer` interface. -```au:output +```au:output au:group=matrix valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` ## Method call -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" @@ -29,7 +29,7 @@ func (a App) setup(c dogma.ApplicationConfigurer) { ## Function call -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" @@ -47,7 +47,7 @@ func setup(c dogma.ApplicationConfigurer) { ## Generic function call -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" @@ -65,7 +65,7 @@ func setup[T dogma.ApplicationConfigurer](c T) { ## Deferred -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" @@ -83,7 +83,7 @@ This test guarantees that the identity configured in a separate goroutine is detected by the static analyzer, but this usage would like introduce a race condition in any real `ApplicationConfigurer` implementation. -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" diff --git a/config/staticconfig/testdata/multiple-apps.md b/config/staticconfig/testdata/multiple-apps.md index e3564558..a1272a64 100644 --- a/config/staticconfig/testdata/multiple-apps.md +++ b/config/staticconfig/testdata/multiple-apps.md @@ -3,7 +3,7 @@ This test verifies that the static analyzer discovers multiple Dogma application types defined within the same Go package. -```au:output +```au:output au:group=matrix valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.One (runtime type unavailable) - valid identity one/4fec74a1-6ed4-46f4-8417-01e0910be8f1 @@ -11,7 +11,7 @@ valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.Two - valid identity two/6e97d403-3cb8-4a59-a7ec-74e8e219a7bc ``` -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" diff --git a/config/staticconfig/testdata/no-apps.md b/config/staticconfig/testdata/no-apps.md index 9d66f639..2d7cd2d0 100644 --- a/config/staticconfig/testdata/no-apps.md +++ b/config/staticconfig/testdata/no-apps.md @@ -3,19 +3,19 @@ This test ensures that the static analyzer does not fail when the analyzed code does not contain any Dogma applications. -```au:output +```au:output au:group=matrix (no applications found) ``` ## Empty package -```go au:input +```go au:input au:group=matrix package app ``` ## Concrete type with similar structure to a dogma.Application -```go au:input +```go au:input au:group=matrix package app import _ "github.com/dogmatiq/dogma" @@ -37,7 +37,7 @@ type ApplicationConfigurer interface { ## Interface that is compatible with dogma.Application -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" @@ -57,7 +57,7 @@ compatible type for `T` and analyze the result of instantiating the generic type, but the assumption is that the reason the type is generic is because the application is intended to be used with multiple types. -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" diff --git a/config/staticconfig/testdata/no-handlers.md b/config/staticconfig/testdata/no-handlers.md index 592966a3..d5d0267c 100644 --- a/config/staticconfig/testdata/no-handlers.md +++ b/config/staticconfig/testdata/no-handlers.md @@ -3,12 +3,12 @@ This test ensures that the static analyzer includes Dogma applications that have no handlers. -```au:output +```au:output au:group=matrix valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 ``` -```go au:input +```go au:input au:group=matrix package app import "github.com/dogmatiq/dogma" diff --git a/go.mod b/go.mod index d3e4c886..528ee1ea 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module github.com/dogmatiq/enginekit go 1.23 require ( - github.com/dogmatiq/aureus v0.1.0 + github.com/dogmatiq/aureus v0.2.1 github.com/dogmatiq/dapper v0.6.0 github.com/dogmatiq/dogma v0.15.0 github.com/dogmatiq/primo v0.3.1 github.com/google/go-cmp v0.6.0 - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d + golang.org/x/tools v0.26.0 google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.1 pgregory.net/rapid v1.1.0 @@ -16,11 +16,11 @@ require ( require ( github.com/dogmatiq/jumble v0.1.0 // indirect - github.com/yuin/goldmark v1.7.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.28.0 // indirect + github.com/yuin/goldmark v1.7.4 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect ) diff --git a/go.sum b/go.sum index 0c1344f6..6e7c1c0f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,27 @@ +cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y= +cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0TxYVST9h4Ie192jJWpHvthBBgg= +github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= +github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/dogmatiq/aureus v0.1.0 h1:BIUF1G4pdCiJ+WQ6GnTmbhaejbjtX35Z9w2somdgslA= github.com/dogmatiq/aureus v0.1.0/go.mod h1:eTm6/WDfVI2tNjg1WCXiPt4fqjMhjO2kNM522ENa6mM= +github.com/dogmatiq/aureus v0.1.1-0.20241007045608-56b32324299c h1:Z00tDp3KdWeTGVpqPEn4OR/gJAfEsXcCGVDm7KFovRY= +github.com/dogmatiq/aureus v0.1.1-0.20241007045608-56b32324299c/go.mod h1:oyg9b8Y10wH8M5OngzHapHFHMOobqLrkbky6hhdeATU= +github.com/dogmatiq/aureus v0.2.0 h1:YKRTm9caNUxC9TZafhchm8ZIXycGDSDKU6lRv74aisQ= +github.com/dogmatiq/aureus v0.2.0/go.mod h1:oyg9b8Y10wH8M5OngzHapHFHMOobqLrkbky6hhdeATU= +github.com/dogmatiq/aureus v0.2.1-0.20241020043618-02979f76b5b4 h1:cBEW1TdIoVSIyUpgz5ow5DUpxuXmom0w5Rm7uvnLu8k= +github.com/dogmatiq/aureus v0.2.1-0.20241020043618-02979f76b5b4/go.mod h1:oyg9b8Y10wH8M5OngzHapHFHMOobqLrkbky6hhdeATU= +github.com/dogmatiq/aureus v0.2.1-0.20241020043859-36cdbb3a14fe h1:fQxBi/snwSeZPl3w3pZvI23K+/8YA3iSLj0/aUBDenQ= +github.com/dogmatiq/aureus v0.2.1-0.20241020043859-36cdbb3a14fe/go.mod h1:oyg9b8Y10wH8M5OngzHapHFHMOobqLrkbky6hhdeATU= +github.com/dogmatiq/aureus v0.2.1 h1:cy89t0u3EpnPBhp9Z4L5vkG9f4cVMhG4zQkDtFZv7p0= +github.com/dogmatiq/aureus v0.2.1/go.mod h1:oyg9b8Y10wH8M5OngzHapHFHMOobqLrkbky6hhdeATU= github.com/dogmatiq/dapper v0.6.0 h1:hnWUsjnt3nUiC9hmkPvuxrnMd7fYNz1i+/GS3gOx0Xs= github.com/dogmatiq/dapper v0.6.0/go.mod h1:ubRHWzt73s0MsPpGhWvnfW/Z/1YPnrkCsQv6CUOZVEw= github.com/dogmatiq/dogma v0.15.0 h1:aXOTd2K4wLvlwHc1D9OsFREp0BusNJ9o9KssxURftmg= @@ -8,22 +30,58 @@ github.com/dogmatiq/jumble v0.1.0 h1:Cb3ExfxY+AoUP4G9/sOwoOdYX8o+kOLK8+dhXAry+QA github.com/dogmatiq/jumble v0.1.0/go.mod h1:FCGV2ImXu8zvThxhd4QLstiEdu74vbIVw9bFJSBcKr4= github.com/dogmatiq/primo v0.3.1 h1:JSqiCh1ma9CbIVzPf8k1vhzQ2Zn/d/WupzElDoiYZw0= github.com/dogmatiq/primo v0.3.1/go.mod h1:z2DfWNz0YmwIKhUEwgJY4xyeWOw0He+9veRRMGQ21UI= +github.com/envoyproxy/go-control-plane v0.13.0 h1:HzkeUz1Knt+3bK+8LG1bxOO/jzWZmdxpwC51i202les= +github.com/envoyproxy/go-control-plane v0.13.0/go.mod h1:GRaKG3dwvFoTg4nj7aXdZnvMg4d7nvT/wl9WgVXn3Q8= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= +github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= From 2ef83c772d996a769a34fc313a6e29a4523148d3 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Oct 2024 20:00:06 +1000 Subject: [PATCH 11/38] Add support for aliases. --- .../testdata/_pending/_alias-for-generic.md | 22 ------------------ .../testdata/alias-for-generic.md | 23 +++++++++++++++++++ internal/typename/typename.go | 2 ++ 3 files changed, 25 insertions(+), 22 deletions(-) delete mode 100644 config/staticconfig/testdata/_pending/_alias-for-generic.md create mode 100644 config/staticconfig/testdata/alias-for-generic.md diff --git a/config/staticconfig/testdata/_pending/_alias-for-generic.md b/config/staticconfig/testdata/_pending/_alias-for-generic.md deleted file mode 100644 index 3c9614ed..00000000 --- a/config/staticconfig/testdata/_pending/_alias-for-generic.md +++ /dev/null @@ -1,22 +0,0 @@ -# Uninstatiated generic application - -This test ensures that the static analyzer does not fail if it encounters a -generic type that implements the `dogma.Application` interface. - -```au:output au:group=matrix -(no applications found) -``` - -```go au:input au:group=matrix -package app - -import "github.com/dogmatiq/dogma" - -type App[T any] struct{} - -func (App[T]) Configure(c dogma.ApplicationConfigurer) { - c.Identity("app", "8a6baab1-ee64-402e-a081-e43f4bebc243") -} - -type Alias = App[int] -``` diff --git a/config/staticconfig/testdata/alias-for-generic.md b/config/staticconfig/testdata/alias-for-generic.md new file mode 100644 index 00000000..3826f7f9 --- /dev/null +++ b/config/staticconfig/testdata/alias-for-generic.md @@ -0,0 +1,23 @@ +# Iinstatiated generic application + +This test ensures that the static analyzer finds an instantiated generic type +that implements the `dogma.Application` interface. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.Alias (runtime type unavailable) + - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 +``` + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" + +type App[T any] struct{} + +func (App[T]) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "8a6baab1-ee64-402e-a081-e43f4bebc243") +} + +type Alias = App[int] +``` diff --git a/internal/typename/typename.go b/internal/typename/typename.go index 02970bb8..52f20fe3 100644 --- a/internal/typename/typename.go +++ b/internal/typename/typename.go @@ -22,6 +22,8 @@ func OfStatic(t types.Type) string { switch t := t.(type) { case *types.Named: return t.String() + case *types.Alias: + return t.String() case *types.Pointer: return "*" + OfStatic(t.Elem()) default: From 6096e7642eed13388e1d1003223b3ec89717bf8a Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Oct 2024 20:14:29 +1000 Subject: [PATCH 12/38] Add test for invalid syntax. --- config/staticconfig/analyze_test.go | 19 ++++++--- .../testdata/_pending/iface-configurer.md | 33 --------------- .../testdata/_pending/pointer-receiver-app.md | 42 ------------------- .../testdata/{_pending => }/invalid-syntax.md | 17 ++++---- 4 files changed, 23 insertions(+), 88 deletions(-) delete mode 100644 config/staticconfig/testdata/_pending/iface-configurer.md delete mode 100644 config/staticconfig/testdata/_pending/pointer-receiver-app.md rename config/staticconfig/testdata/{_pending => }/invalid-syntax.md (64%) diff --git a/config/staticconfig/analyze_test.go b/config/staticconfig/analyze_test.go index 773c2eee..70778741 100644 --- a/config/staticconfig/analyze_test.go +++ b/config/staticconfig/analyze_test.go @@ -18,10 +18,15 @@ func TestAnalyzer(t *testing.T) { func(t *testing.T, in aureus.Input, out aureus.Output) error { t.Parallel() + cwd, _ := os.Getwd() + // Create a temporary directory to write the Go source code, but // create it within this Go module so that it uses the same version // of Dogma, etc. - dir, err := os.MkdirTemp("testdata", "aureus-") + dir, err := os.MkdirTemp( + filepath.Join(cwd, "testdata"), + "aureus-", + ) if err != nil { return err } @@ -43,14 +48,18 @@ func TestAnalyzer(t *testing.T) { result := LoadAndAnalyze(dir) - if len(result.Applications) == 0 { - if _, err := io.WriteString(out, "(no applications found)\n"); err != nil { + for err := range result.Errors() { + message := err.Error() + message = strings.ReplaceAll(message, dir+"/", "") + message = strings.ReplaceAll(message, dir, "") + + if _, err := io.WriteString(out, "ERROR: "+message+"\n"); err != nil { return err } } - for err := range result.Errors() { - if _, err := io.WriteString(out, err.Error()+"\n"); err != nil { + if len(result.Applications) == 0 { + if _, err := io.WriteString(out, "(no applications found)\n"); err != nil { return err } } diff --git a/config/staticconfig/testdata/_pending/iface-configurer.md b/config/staticconfig/testdata/_pending/iface-configurer.md deleted file mode 100644 index 3dbff468..00000000 --- a/config/staticconfig/testdata/_pending/iface-configurer.md +++ /dev/null @@ -1,33 +0,0 @@ -# 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 au:group=matrix -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 au:group=matrix -application (7468a57f-20f0-4d11-9aad-48fcd553a908) App -``` diff --git a/config/staticconfig/testdata/_pending/pointer-receiver-app.md b/config/staticconfig/testdata/_pending/pointer-receiver-app.md deleted file mode 100644 index 812045ca..00000000 --- a/config/staticconfig/testdata/_pending/pointer-receiver-app.md +++ /dev/null @@ -1,42 +0,0 @@ -# Applications with no handlers - -This test ensures that the static analyzer includes Dogma applications that have -no handlers. - -## With non-pointer receiver - -```au:output au:group=matrix au:group="non-pointer" -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) - - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 -``` - -```go au:input au:group=matrix au:group="non-pointer" -package app - -import "github.com/dogmatiq/dogma" - -type App struct{} - -func (App) Configure(c dogma.ApplicationConfigurer) { - c.Identity("app", "8a6baab1-ee64-402e-a081-e43f4bebc243") -} -``` - -## With pointer receiver - -```au:output au:group=matrix au:group="pointer" -valid application *github.com/dogmatiq/enginekit/config/staticconfig/testdata/empty-app.App (runtime type unavailable) - - valid identity app/d196eb7a-bad4-4826-8763-db1111882fbd -``` - -```go au:input au:group=matrix au:group="pointer" -package app - -import "github.com/dogmatiq/dogma" - -type App struct{} - -func (*App) Configure(c dogma.ApplicationConfigurer) { - c.Identity("app", "d196eb7a-bad4-4826-8763-db1111882fbd") -} -``` diff --git a/config/staticconfig/testdata/_pending/invalid-syntax.md b/config/staticconfig/testdata/invalid-syntax.md similarity index 64% rename from config/staticconfig/testdata/_pending/invalid-syntax.md rename to config/staticconfig/testdata/invalid-syntax.md index f0f745cd..c8bf3cd6 100644 --- a/config/staticconfig/testdata/_pending/invalid-syntax.md +++ b/config/staticconfig/testdata/invalid-syntax.md @@ -1,7 +1,12 @@ -# Type Aliased Handlers +# Invalid syntax -This test verifies that static analysis panics when it encounters incorrect -syntax. +This test verifies that static analyzer does not fail catastrophically when the +analyzed code does not compile. + +```au:output au:group=matrix +ERROR: main.go:10:1: expected declaration, found '<' +(no applications found) +``` ```go au:input au:group=matrix package app @@ -9,14 +14,10 @@ package app // Even though this file has invalid syntax the import statements are still // parsed. This import necessary so that the test still considers it a // possibility that this package has valid Dogma application implementations. -import "github.com/dogmatiq/dogma" +import _ "github.com/dogmatiq/dogma" // Below is the deliberate illegal Go syntax to test loading of the packages // with errors. ``` - -```au:output au:group=matrix -expected declaration, found '<' -``` From 3b01e290982c20488350bd4d821082e2e5d36841 Mon Sep 17 00:00:00 2001 From: James Harris Date: Sun, 20 Oct 2024 20:24:28 +1000 Subject: [PATCH 13/38] Add tests for registring nil handlers. --- config/staticconfig/application.go | 94 +++++++++---------- .../testdata/_pending/nil-handlers.md | 25 ----- config/staticconfig/testdata/nil-handlers.md | 40 ++++++++ 3 files changed, 83 insertions(+), 76 deletions(-) delete mode 100644 config/staticconfig/testdata/_pending/nil-handlers.md create mode 100644 config/staticconfig/testdata/nil-handlers.md diff --git a/config/staticconfig/application.go b/config/staticconfig/application.go index c6ac8f94..7019004e 100644 --- a/config/staticconfig/application.go +++ b/config/staticconfig/application.go @@ -21,16 +21,14 @@ func analyzeApplication(ctx *context, t types.Type) { switch call.Method.Name() { case "Identity": analyzeIdentityCall(b, call) - // // case "RegisterAggregate": - // // analyzeRegisterAggregateCall(ctx, c) - // // case "RegisterProcess": - // // analyzeRegisterProcessCall(ctx, c) - // // case "RegisterIntegration": - // // analyzeRegisterIntegrationCall(ctx, c) - // // case "RegisterProjection": - // // analyzeRegisterProjectionCall(ctx, c) - // // case "Handlers": - // // panic("not implemented") + case "RegisterAggregate": + analyzeRegisterAggregateCall(b, call) + case "RegisterProcess": + analyzeRegisterProcessCall(b, call) + case "RegisterIntegration": + analyzeRegisterIntegrationCall(b, call) + case "RegisterProjection": + analyzeRegisterProjectionCall(b, call) default: b.UpdateFidelity(config.Incomplete) } @@ -38,46 +36,40 @@ func analyzeApplication(ctx *context, t types.Type) { }, ), ) +} + +func analyzeRegisterAggregateCall( + b *configbuilder.ApplicationBuilder, + _ configurerCall, +) { + b.Aggregate(func(b *configbuilder.AggregateBuilder) { + b.UpdateFidelity(config.Incomplete) + }) +} + +func analyzeRegisterProcessCall( + b *configbuilder.ApplicationBuilder, + _ configurerCall, +) { + b.Process(func(b *configbuilder.ProcessBuilder) { + b.UpdateFidelity(config.Incomplete) + }) +} + +func analyzeRegisterIntegrationCall( + b *configbuilder.ApplicationBuilder, + _ configurerCall, +) { + b.Integration(func(b *configbuilder.IntegrationBuilder) { + b.UpdateFidelity(config.Incomplete) + }) +} - // switch c.Common().Method.Name() { - // case "Identity": - // app.IdentityValue = analyzeIdentityCall(c) - // case "RegisterAggregate": - // addHandlerFromArguments( - // prog, - // dogmaPkg, - // dogmaPkg.AggregateMessageHandler, - // args, - // app.HandlersValue, - // configkit.AggregateHandlerType, - // ) - // case "RegisterProcess": - // addHandlerFromArguments( - // prog, - // dogmaPkg, - // dogmaPkg.ProcessMessageHandler, - // args, - // app.HandlersValue, - // configkit.ProcessHandlerType, - // ) - // case "RegisterProjection": - // addHandlerFromArguments( - // prog, - // dogmaPkg, - // dogmaPkg.ProjectionMessageHandler, - // args, - // app.HandlersValue, - // configkit.ProjectionHandlerType, - // ) - // case "RegisterIntegration": - // addHandlerFromArguments( - // prog, - // dogmaPkg, - // dogmaPkg.IntegrationMessageHandler, - // args, - // app.HandlersValue, - // configkit.IntegrationHandlerType, - // ) - // } - // } +func analyzeRegisterProjectionCall( + b *configbuilder.ApplicationBuilder, + _ configurerCall, +) { + b.Projection(func(b *configbuilder.ProjectionBuilder) { + b.UpdateFidelity(config.Incomplete) + }) } diff --git a/config/staticconfig/testdata/_pending/nil-handlers.md b/config/staticconfig/testdata/_pending/nil-handlers.md deleted file mode 100644 index 5757e535..00000000 --- a/config/staticconfig/testdata/_pending/nil-handlers.md +++ /dev/null @@ -1,25 +0,0 @@ -# Nil-valued handlers - -This test ensures that the static analyzer ignores handlers that are `nil`, but -still includes the application itself. - -```go au:input au:group=matrix -package app - -import "github.com/dogmatiq/dogma" - -type App struct{} - -func (App) Configure(c dogma.ApplicationConfigurer) { - c.Identity("", "0726ae0d-67e4-4a50-8a19-9f58eae38e51") - - c.RegisterAggregate(nil) - c.RegisterProcess(nil) - c.RegisterProjection(nil) - c.RegisterIntegration(nil) -} -``` - -```au:output au:group=matrix -application (0726ae0d-67e4-4a50-8a19-9f58eae38e51) App -``` diff --git a/config/staticconfig/testdata/nil-handlers.md b/config/staticconfig/testdata/nil-handlers.md new file mode 100644 index 00000000..a1e8a71f --- /dev/null +++ b/config/staticconfig/testdata/nil-handlers.md @@ -0,0 +1,40 @@ +# Nil handlers + +This test ensures that the static analyzer includes basic information about the +presence of `nil` handlers. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) + - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 + - incomplete aggregate + - no identity is configured + - no "handles-command" routes are configured + - no "records-event" routes are configured + - incomplete process + - no identity is configured + - no "handles-event" routes are configured + - no "executes-command" routes are configured + - incomplete integration + - no identity is configured + - no "handles-command" routes are configured + - incomplete projection + - no identity is configured + - no "handles-event" routes are configured +``` + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "0726ae0d-67e4-4a50-8a19-9f58eae38e51") + + c.RegisterAggregate(nil) + c.RegisterProcess(nil) + c.RegisterIntegration(nil) + c.RegisterProjection(nil) +} +``` From 2311dd34a966670d984f995a4b5b81b0010b5d98 Mon Sep 17 00:00:00 2001 From: James Harris Date: Mon, 21 Oct 2024 12:13:10 +1000 Subject: [PATCH 14/38] Move generated test code under a subdirectory to avoid UI shift in IDE. --- config/staticconfig/analyze_test.go | 38 ++++++++++++++++--------- config/staticconfig/testdata/.gitignore | 2 +- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/config/staticconfig/analyze_test.go b/config/staticconfig/analyze_test.go index 70778741..48ccd980 100644 --- a/config/staticconfig/analyze_test.go +++ b/config/staticconfig/analyze_test.go @@ -13,20 +13,28 @@ import ( ) func TestAnalyzer(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + // Create a single directory for the Go source code used as Aureus test + // inputs. + // + // Since it's under the testdata directory it is ignored by Go's tooling, + // but it is still subject to the same go.mod file, and hence the same + // version of Dogma, etc. + outputDir := filepath.Join(cwd, "testdata", ".aureus") + if err := os.MkdirAll(outputDir, 0700); err != nil { + t.Fatal(err) + } + aureus.Run( t, func(t *testing.T, in aureus.Input, out aureus.Output) error { t.Parallel() - cwd, _ := os.Getwd() - - // Create a temporary directory to write the Go source code, but - // create it within this Go module so that it uses the same version - // of Dogma, etc. - dir, err := os.MkdirTemp( - filepath.Join(cwd, "testdata"), - "aureus-", - ) + dir, err := os.MkdirTemp(outputDir, "aureus-") if err != nil { return err } @@ -48,20 +56,22 @@ func TestAnalyzer(t *testing.T) { result := LoadAndAnalyze(dir) + hasErrors := false for err := range result.Errors() { + hasErrors = true + message := err.Error() message = strings.ReplaceAll(message, dir+"/", "") message = strings.ReplaceAll(message, dir, "") - if _, err := io.WriteString(out, "ERROR: "+message+"\n"); err != nil { + if _, err := io.WriteString(out, message+"\n"); err != nil { return err } } - if len(result.Applications) == 0 { - if _, err := io.WriteString(out, "(no applications found)\n"); err != nil { - return err - } + if !hasErrors && len(result.Applications) == 0 { + _, err := io.WriteString(out, "(no applications found)\n") + return err } for i, app := range result.Applications { diff --git a/config/staticconfig/testdata/.gitignore b/config/staticconfig/testdata/.gitignore index e12637ed..ce4c89b6 100644 --- a/config/staticconfig/testdata/.gitignore +++ b/config/staticconfig/testdata/.gitignore @@ -1 +1 @@ -aureus-*/ +.aureus/ From 98c699c0ca2aafebf09706685379eb038b17b96e Mon Sep 17 00:00:00 2001 From: James Harris Date: Mon, 21 Oct 2024 12:14:57 +1000 Subject: [PATCH 15/38] Add documentation to internal static value resolution code. --- config/staticconfig/analyze.go | 54 +--------- config/staticconfig/application.go | 4 +- config/staticconfig/identity.go | 91 +--------------- .../staticconfig/testdata/invalid-syntax.md | 3 +- config/staticconfig/type.go | 56 +++++++++- config/staticconfig/value.go | 101 ++++++++++++++++++ 6 files changed, 165 insertions(+), 144 deletions(-) create mode 100644 config/staticconfig/value.go diff --git a/config/staticconfig/analyze.go b/config/staticconfig/analyze.go index 002b3060..fb9f7a3e 100644 --- a/config/staticconfig/analyze.go +++ b/config/staticconfig/analyze.go @@ -2,8 +2,6 @@ package staticconfig import ( "cmp" - "fmt" - "go/types" "iter" "slices" @@ -87,7 +85,9 @@ func LoadAndAnalyze(dir string) Analysis { func Analyze(pkgs []*packages.Package) Analysis { prog, ssaPackages := ssautil.AllPackages( pkgs, - ssa.InstantiateGenerics, // | ssa.SanityCheckFunctions, // TODO: document why this is necessary + ssa.InstantiateGenerics| // Instantiate generic types so that we can analyze them. + ssa.SanityCheckFunctions, // TODO: document why this is necessary + ) prog.Build() @@ -125,7 +125,7 @@ func Analyze(pkgs []*packages.Package) Analysis { for _, m := range pkg.Members { if t, ok := m.(*ssa.Type); ok { - analyzeType(ctx, t) + analyzeType(ctx, t.Type()) } } } @@ -143,49 +143,3 @@ func Analyze(pkgs []*packages.Package) Analysis { return *ctx.Analysis } - -// packageOf returns the package in which t is declared. -// -// It panics if t is not a named type or a pointer to a named type. -func packageOf(t types.Type) *types.Package { - switch t := t.(type) { - case *types.Named: - return t.Obj().Pkg() - case *types.Alias: - return t.Obj().Pkg() - case *types.Pointer: - return packageOf(t.Elem()) - default: - panic(fmt.Sprintf("cannot determine package for anonymous or built-in type %v", t)) - } -} - -func analyzeType(ctx *context, m *ssa.Type) { - t := m.Type() - - if isAbstract(t) { - // We're only interested in concrete types; otherwise there's nothing to - // analyze! - return - } - - // The sequence of the if-blocks below is important as a type - // implements an interface only if the methods in the interface's - // method set have non-pointer receivers. Hence the implementation - // check for the "raw" (non-pointer) type is made first. - // - // A pointer to the type, on the other hand, implements the - // interface regardless of whether pointer receivers are used or - // not. - - if types.Implements(t, ctx.Dogma.Application) { - analyzeApplication(ctx, t) - return - } - - p := types.NewPointer(t) - if types.Implements(p, ctx.Dogma.Application) { - analyzeApplication(ctx, p) - return - } -} diff --git a/config/staticconfig/application.go b/config/staticconfig/application.go index 7019004e..402a07be 100644 --- a/config/staticconfig/application.go +++ b/config/staticconfig/application.go @@ -8,9 +8,9 @@ import ( "github.com/dogmatiq/enginekit/internal/typename" ) -// analyzeApplication analyzes t, which must be an implementation of +// analyzeApplicationType analyzes t, which must be an implementation of // [dogma.Application]. -func analyzeApplication(ctx *context, t types.Type) { +func analyzeApplicationType(ctx *context, t types.Type) { ctx.Analysis.Applications = append( ctx.Analysis.Applications, configbuilder.Application( diff --git a/config/staticconfig/identity.go b/config/staticconfig/identity.go index 009a5ac1..3076416c 100644 --- a/config/staticconfig/identity.go +++ b/config/staticconfig/identity.go @@ -2,11 +2,9 @@ package staticconfig import ( "go/constant" - "go/token" "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/config/internal/configbuilder" - "golang.org/x/tools/go/ssa" ) func analyzeIdentityCall( @@ -16,101 +14,16 @@ func analyzeIdentityCall( b.Identity(func(b *configbuilder.IdentityBuilder) { b.UpdateFidelity(call.Fidelity) - if name, ok := resolveValue(call.Args[0]); ok { + if name := staticValue(call.Args[0]); name != nil { b.SetName(constant.StringVal(name)) } else { b.UpdateFidelity(config.Incomplete) } - if key, ok := resolveValue(call.Args[1]); ok { + if key := staticValue(call.Args[1]); key != nil { b.SetKey(constant.StringVal(key)) } else { b.UpdateFidelity(config.Incomplete) } }) } - -func resolveValue( - v ssa.Value, -) (constant.Value, bool) { - switch v := v.(type) { - case *ssa.Const: - return v.Value, true - case ssa.Instruction: - values := resolveExpr(v) - switch len(values) { - case 0: - return nil, false - case 1: - return values[0], values[0] != nil - default: - panic("did not expect multiple values") - } - default: - return nil, false - } -} - -func resolveExpr( - inst ssa.Instruction, -) []constant.Value { - switch inst := inst.(type) { - case *ssa.Call: - return resolveReturnValues(inst.Common()) - case *ssa.Extract: - if expr, ok := inst.Tuple.(ssa.Instruction); ok { - values := resolveExpr(expr) - return values[inst.Index : inst.Index+1] - } - return nil - default: - return nil - } -} - -func resolveReturnValues(call *ssa.CallCommon) []constant.Value { - fn := call.StaticCallee() - if fn == nil { - return nil - } - - if len(fn.Blocks) == 0 { - return nil - } - - if fn.Signature.Results().Len() == 0 { - return nil - } - - var results []constant.Value - - for b := range walkReachable(fn.Blocks[0]) { - inst, ok := b.Instrs[len(b.Instrs)-1].(*ssa.Return) - if !ok { - continue - } - - var values []constant.Value - - for _, v := range inst.Results { - if x, ok := resolveValue(v); ok { - values = append(values, x) - } else { - values = append(values, nil) - } - } - - if results == nil { - results = values - } else { - for i, a := range values { - b := results[i] - if constant.Compare(a, token.NEQ, b) { - results[i] = nil - } - } - } - } - - return results -} diff --git a/config/staticconfig/testdata/invalid-syntax.md b/config/staticconfig/testdata/invalid-syntax.md index c8bf3cd6..9acac8f9 100644 --- a/config/staticconfig/testdata/invalid-syntax.md +++ b/config/staticconfig/testdata/invalid-syntax.md @@ -4,8 +4,7 @@ This test verifies that static analyzer does not fail catastrophically when the analyzed code does not compile. ```au:output au:group=matrix -ERROR: main.go:10:1: expected declaration, found '<' -(no applications found) +main.go:10:1: expected declaration, found '<' ``` ```go au:input au:group=matrix diff --git a/config/staticconfig/type.go b/config/staticconfig/type.go index 12c8ae51..42d07914 100644 --- a/config/staticconfig/type.go +++ b/config/staticconfig/type.go @@ -1,7 +1,12 @@ package staticconfig -import "go/types" +import ( + "fmt" + "go/types" +) +// isAbstract returns true if t is abstract, either because it refers to an +// interface or because it is a generic type that has not been instantiated. func isAbstract(t types.Type) bool { if types.IsInterface(t) { return true @@ -18,3 +23,52 @@ func isAbstract(t types.Type) bool { return false } + +// 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. +func analyzeType(ctx *context, t types.Type) { + if isAbstract(t) { + // We're only interested in concrete types; otherwise there's nothing to + // analyze! + return + } + + // The sequence of the if-blocks below is important as a type + // implements an interface only if the methods in the interface's + // method set have non-pointer receivers. Hence the implementation + // check for the "raw" (non-pointer) type is made first. + // + // A pointer to the type, on the other hand, implements the + // interface regardless of whether pointer receivers are used or + // not. + + if types.Implements(t, ctx.Dogma.Application) { + analyzeApplicationType(ctx, t) + return + } + + p := types.NewPointer(t) + if types.Implements(p, ctx.Dogma.Application) { + analyzeApplicationType(ctx, p) + return + } +} + +// packageOf returns the package in which t is declared. +// +// It panics if t is not a named type or a pointer to a named type. +func packageOf(t types.Type) *types.Package { + switch t := t.(type) { + case *types.Named: + return t.Obj().Pkg() + case *types.Alias: + return t.Obj().Pkg() + case *types.Pointer: + return packageOf(t.Elem()) + default: + panic(fmt.Sprintf("cannot determine package for anonymous or built-in type %v", t)) + } +} diff --git a/config/staticconfig/value.go b/config/staticconfig/value.go new file mode 100644 index 00000000..daa2663a --- /dev/null +++ b/config/staticconfig/value.go @@ -0,0 +1,101 @@ +package staticconfig + +import ( + "go/constant" + "go/token" + + "golang.org/x/tools/go/ssa" +) + +// staticValue returns the constant value of v, if it's possible to obtain; +// otherwise, it returns nil. +func staticValue(v ssa.Value) constant.Value { + switch v := v.(type) { + case *ssa.Const: + return v.Value + case ssa.Instruction: + values := staticValueOfInstruction(v) + switch len(values) { + case 0: + return nil + case 1: + return values[0] + default: + panic("did not expect multiple values") + } + default: + return nil + } +} + +// staticValueOfInstruction returns the constant value(s) of an instruction. +// +// If an individual value within the expression cannot be resolved, it is +// represented as a nil value in the returned slice. +// +// It returns an empty slice if the expression itself cannot be resolved. +func staticValueOfInstruction(inst ssa.Instruction) []constant.Value { + switch inst := inst.(type) { + case *ssa.Call: + return staticReturnValues(inst.Common()) + case *ssa.Extract: + if expr, ok := inst.Tuple.(ssa.Instruction); ok { + values := staticValueOfInstruction(expr) + return values[inst.Index : inst.Index+1] + } + return nil + default: + return nil + } +} + +// staticReturnValues returns the constant values returned by a function. +// +// If an invividual value cannot be resolved, it is represented as a nil value +// in the returned slice. The function must return the same value on all control +// paths for a value to be considered constant. +// +// It returns nil if the function itself cannot be resolved. For example, if it +// is a dynamic call to an interface method. +func staticReturnValues(call *ssa.CallCommon) []constant.Value { + fn := call.StaticCallee() + if fn == nil { + return nil + } + + if len(fn.Blocks) == 0 { + return nil + } + + if fn.Signature.Results().Len() == 0 { + return nil + } + + var results []constant.Value + + for b := range walkReachable(fn.Blocks[0]) { + inst, ok := b.Instrs[len(b.Instrs)-1].(*ssa.Return) + if !ok { + continue + } + + var values []constant.Value + + for _, v := range inst.Results { + values = append(values, staticValue(v)) + } + + if results == nil { + results = values + } else { + for i, a := range values { + b := results[i] + if constant.Compare(a, token.NEQ, b) { + results[i] = nil + } + } + } + } + + return results +} From 836bf858f595d159123815b0d2e8c3c2817ebcf7 Mon Sep 17 00:00:00 2001 From: James Harris Date: Mon, 21 Oct 2024 18:02:08 +1000 Subject: [PATCH 16/38] Fix directory trimming. --- config/staticconfig/analyze_test.go | 3 +- config/staticconfig/testdata/_aggregate.md | 42 ++++++++++++++ .../_handler-with-unregistered-routes.md | 39 +++++++++++++ .../unregistered-routes-in-handlers.md | 58 ------------------- .../testdata/alias-for-generic.md | 2 +- .../conditional-excluded-by-const-expr.md | 2 +- .../conditional-included-by-const-expr.md | 2 +- .../testdata/conditional-present-in-method.md | 2 +- config/staticconfig/testdata/conditional.md | 2 +- .../testdata/identity-from-const.md | 2 +- .../identity-from-non-const-static.md | 2 +- .../testdata/identity-from-non-const.md | 2 +- .../testdata/incomplete-entity.md | 2 +- config/staticconfig/testdata/indirect.md | 2 +- config/staticconfig/testdata/multiple-apps.md | 4 +- config/staticconfig/testdata/nil-handlers.md | 2 +- config/staticconfig/testdata/no-handlers.md | 2 +- 17 files changed, 97 insertions(+), 73 deletions(-) create mode 100644 config/staticconfig/testdata/_aggregate.md create mode 100644 config/staticconfig/testdata/_handler-with-unregistered-routes.md delete mode 100644 config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md diff --git a/config/staticconfig/analyze_test.go b/config/staticconfig/analyze_test.go index 48ccd980..147312d1 100644 --- a/config/staticconfig/analyze_test.go +++ b/config/staticconfig/analyze_test.go @@ -86,9 +86,10 @@ func TestAnalyzer(t *testing.T) { // Remove the random portion of the temporary directory name // so that the test output is deterministic. + rel, _ := filepath.Rel(cwd, dir) details = strings.ReplaceAll( details, - "/"+filepath.Base(dir)+".", + "/"+rel+".", ".", ) diff --git a/config/staticconfig/testdata/_aggregate.md b/config/staticconfig/testdata/_aggregate.md new file mode 100644 index 00000000..2bfec28f --- /dev/null +++ b/config/staticconfig/testdata/_aggregate.md @@ -0,0 +1,42 @@ +# Aggregate + +This test ensures that the static analyzer supports all aspects of configuring +an aggregate. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) + - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 + - disabled valid aggregate github.com/dogmatiq/enginekit/config/staticconfig.Aggregate (runtime type unavailable) + - valid identity aggregate/916e5e95-70c4-4823-9de2-0f7389d18b4f + - incomplete route + - incomplete route +``` + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Aggregate struct {} + +func (Aggregate) Configure(c dogma.AggregateConfigurer) { + c.Identity("aggregate", "916e5e95-70c4-4823-9de2-0f7389d18b4f") + c.Routes( + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]](), + dogma.RecordsEvent[stubs.EventStub[stubs.TypeA]](), + ) + c.Disable() +} + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "0726ae0d-67e4-4a50-8a19-9f58eae38e51") + c.RegisterAggregate(Aggregate{}) +} + +func (Aggregate) New() dogma.AggregateRoot { return nil } +func (Aggregate) RouteCommandToInstance(dogma.Command) string { return "" } +func (Aggregate) HandleCommand(dogma.AggregateRoot, dogma.AggregateCommandScope, dogma.Command) {} +``` diff --git a/config/staticconfig/testdata/_handler-with-unregistered-routes.md b/config/staticconfig/testdata/_handler-with-unregistered-routes.md new file mode 100644 index 00000000..f5d064db --- /dev/null +++ b/config/staticconfig/testdata/_handler-with-unregistered-routes.md @@ -0,0 +1,39 @@ +# Handler with unregistered routes + +This test verifies that static analyzer does not include information about +routes that are constructed but never passed to the configurer's `Routes()` +method. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) + - valid identity app/f2c08525-623e-4c76-851c-3172953269e3 + - invalid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (runtime type unavailable) + - no "handles-command" routes are configured + - valid identity handler/ac391765-da58-4e7c-a478-e4725eb2b0e9 +``` + +```go au:input au:group=matrix +package app + +import ( + "context" + "github.com/dogmatiq/dogma" + "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "ac391765-da58-4e7c-a478-e4725eb2b0e9") + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeX]]() +} + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "f2c08525-623e-4c76-851c-3172953269e3") + c.RegisterIntegration(Integration{}) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } +``` diff --git a/config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md b/config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md deleted file mode 100644 index 34d04ddd..00000000 --- a/config/staticconfig/testdata/_pending/unregistered-routes-in-handlers.md +++ /dev/null @@ -1,58 +0,0 @@ -# Unregistered Routes in Dogma Application Handlers - -This test verifies that static analysis ignores unregistered routes in Dogma -application handlers. - -```go au:input au:group=matrix -package app - -import ( - "context" - . "github.com/dogmatiq/dogma" - "github.com/dogmatiq/enginekit/enginetest/stubs" -) - -// App implements Application interface. -type App struct{} - -// Configure configures the behavior of the engine as it relates to this -// application. -func (App) Configure(c ApplicationConfigurer) { - c.Identity("", "f2c08525-623e-4c76-851c-3172953269e3") - c.RegisterIntegration(IntegrationHandler{}) -} - -// IntegrationHandler is a test implementation of -// IntegrationMessageHandler. -type IntegrationHandler struct{} - -// Configure configures the behavior of the engine as it relates to this -// handler. -func (IntegrationHandler) Configure(c IntegrationConfigurer) { - c.Identity("", "ac391765-da58-4e7c-a478-e4725eb2b0e9") - - // Create a route that is never passed to c.Routes(). - HandlesCommand[stubs.CommandStub[stubs.TypeX]]() - - // Ensure there is still _some_ call to Routes(). - c.Routes( - HandlesCommand[stubs.CommandStub[stubs.TypeA]](), - ) -} - -// HandleCommand handles a command message that has been routed to this handler. -func (IntegrationHandler) HandleCommand( - context.Context, - IntegrationCommandScope, - Command, -) error { - return nil -} -``` - -```au:output au:group=matrix -application (f2c08525-623e-4c76-851c-3172953269e3) App - - - integration (ac391765-da58-4e7c-a478-e4725eb2b0e9) IntegrationHandler - handles CommandStub[TypeA]? -``` diff --git a/config/staticconfig/testdata/alias-for-generic.md b/config/staticconfig/testdata/alias-for-generic.md index 3826f7f9..14c67d32 100644 --- a/config/staticconfig/testdata/alias-for-generic.md +++ b/config/staticconfig/testdata/alias-for-generic.md @@ -4,7 +4,7 @@ This test ensures that the static analyzer finds an instantiated generic type that implements the `dogma.Application` interface. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.Alias (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.Alias (runtime type unavailable) - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 ``` diff --git a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md index 93c386ae..3b16d7c7 100644 --- a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer excludes information about an entity's identity if it appears in an unreachable branch. ```au:output au:group=matrix -invalid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) +invalid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - no identity is configured ``` diff --git a/config/staticconfig/testdata/conditional-included-by-const-expr.md b/config/staticconfig/testdata/conditional-included-by-const-expr.md index ae072634..5857952d 100644 --- a/config/staticconfig/testdata/conditional-included-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-included-by-const-expr.md @@ -5,7 +5,7 @@ entity's identity if it appears in a conditional block that is always executed. Note that the identity is not marked as "speculative". ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/conditional-present-in-method.md b/config/staticconfig/testdata/conditional-present-in-method.md index c6a75e63..620a4876 100644 --- a/config/staticconfig/testdata/conditional-present-in-method.md +++ b/config/staticconfig/testdata/conditional-present-in-method.md @@ -5,7 +5,7 @@ entity's identity even if it appears after (but not within) a conditional statement. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/conditional.md b/config/staticconfig/testdata/conditional.md index 73c661e8..f2f88534 100644 --- a/config/staticconfig/testdata/conditional.md +++ b/config/staticconfig/testdata/conditional.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer includes information about an entity's identity when it is defined within a conditional statement. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - valid speculative identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/identity-from-const.md b/config/staticconfig/testdata/identity-from-const.md index e3570b38..06e91920 100644 --- a/config/staticconfig/testdata/identity-from-const.md +++ b/config/staticconfig/testdata/identity-from-const.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer can discover the values within an entity's identity when they are sourced from non-literal constant expressions. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - valid identity app/d0de39ba-aaaf-43fd-8e8f-7c4e3be309ec ``` diff --git a/config/staticconfig/testdata/identity-from-non-const-static.md b/config/staticconfig/testdata/identity-from-non-const-static.md index a7aebcb3..480cdb51 100644 --- a/config/staticconfig/testdata/identity-from-non-const-static.md +++ b/config/staticconfig/testdata/identity-from-non-const-static.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer includes an entity's identity, even if it cannot determine the values used. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - valid identity app/a0a0edb7-ce45-4eb4-940c-0f77459ae2a0 ``` diff --git a/config/staticconfig/testdata/identity-from-non-const.md b/config/staticconfig/testdata/identity-from-non-const.md index def26d84..e6a9ec2a 100644 --- a/config/staticconfig/testdata/identity-from-non-const.md +++ b/config/staticconfig/testdata/identity-from-non-const.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer includes an entity's identity, even if it cannot determine the values used. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - incomplete identity ?/? ``` diff --git a/config/staticconfig/testdata/incomplete-entity.md b/config/staticconfig/testdata/incomplete-entity.md index 6d610509..e17d8ec8 100644 --- a/config/staticconfig/testdata/incomplete-entity.md +++ b/config/staticconfig/testdata/incomplete-entity.md @@ -5,7 +5,7 @@ incomplete if the `Configure()` method calls into code that is unable to be analyzed. ```au:output au:group=matrix -incomplete application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) +incomplete application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/indirect.md b/config/staticconfig/testdata/indirect.md index 062799e5..b77a3ef2 100644 --- a/config/staticconfig/testdata/indirect.md +++ b/config/staticconfig/testdata/indirect.md @@ -5,7 +5,7 @@ This test verifies that the static analyzer traverses into code called from the `ApplicationConfigurer` interface. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/multiple-apps.md b/config/staticconfig/testdata/multiple-apps.md index a1272a64..0ab23033 100644 --- a/config/staticconfig/testdata/multiple-apps.md +++ b/config/staticconfig/testdata/multiple-apps.md @@ -4,10 +4,10 @@ This test verifies that the static analyzer discovers multiple Dogma application types defined within the same Go package. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.One (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.One (runtime type unavailable) - valid identity one/4fec74a1-6ed4-46f4-8417-01e0910be8f1 -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.Two (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.Two (runtime type unavailable) - valid identity two/6e97d403-3cb8-4a59-a7ec-74e8e219a7bc ``` diff --git a/config/staticconfig/testdata/nil-handlers.md b/config/staticconfig/testdata/nil-handlers.md index a1e8a71f..edeb1bb6 100644 --- a/config/staticconfig/testdata/nil-handlers.md +++ b/config/staticconfig/testdata/nil-handlers.md @@ -4,7 +4,7 @@ This test ensures that the static analyzer includes basic information about the presence of `nil` handlers. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 - incomplete aggregate - no identity is configured diff --git a/config/staticconfig/testdata/no-handlers.md b/config/staticconfig/testdata/no-handlers.md index d5d0267c..e129ba0d 100644 --- a/config/staticconfig/testdata/no-handlers.md +++ b/config/staticconfig/testdata/no-handlers.md @@ -4,7 +4,7 @@ This test ensures that the static analyzer includes Dogma applications that have no handlers. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig/testdata.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 ``` From 0cead0cc92c456330d694d2cd46f82eb1f1b3cc4 Mon Sep 17 00:00:00 2001 From: James Harris Date: Mon, 21 Oct 2024 18:02:29 +1000 Subject: [PATCH 17/38] Add `configureContext.Func`. --- config/staticconfig/configurer.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/config/staticconfig/configurer.go b/config/staticconfig/configurer.go index be14f33b..e662dcdc 100644 --- a/config/staticconfig/configurer.go +++ b/config/staticconfig/configurer.go @@ -14,15 +14,14 @@ import ( type configureContext struct { *context + Func *ssa.Function Builder configbuilder.EntityBuilder ConfigurerIndices []int } func (c *configureContext) IsConfigurer(v ssa.Value) bool { - params := v.Parent().Params - for _, i := range c.ConfigurerIndices { - if v == params[i] { + if v == c.Func.Params[i] { return true } } @@ -51,6 +50,7 @@ func findConfigurerCalls( emitConfigurerCallsInFunc( &configureContext{ context: ctx, + Func: configure, Builder: b, ConfigurerIndices: []int{1}, }, @@ -150,6 +150,7 @@ func emitConfigurerCallsInCallInstruction( return emitConfigurerCallsInFunc( &configureContext{ context: ctx.context, + Func: fn, Builder: ctx.Builder, ConfigurerIndices: indices, }, From 98718c1d37324abd1332710a839fe1208e9e3e72 Mon Sep 17 00:00:00 2001 From: James Harris Date: Mon, 21 Oct 2024 18:07:04 +1000 Subject: [PATCH 18/38] Use `staticValue()` to resolve conditionals. --- config/staticconfig/block.go | 4 ++-- .../conditional-included-by-const-expr.md | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/config/staticconfig/block.go b/config/staticconfig/block.go index 7f5116f3..f1ea5cfd 100644 --- a/config/staticconfig/block.go +++ b/config/staticconfig/block.go @@ -41,8 +41,8 @@ func walkReachable(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { func reachableSuccessors(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { return func(yield func(*ssa.BasicBlock) bool) { if branch, ok := b.Instrs[len(b.Instrs)-1].(*ssa.If); ok { - if v, ok := branch.Cond.(*ssa.Const); ok { - if constant.BoolVal(v.Value) { + if v := staticValue(branch.Cond); v != nil { + if constant.BoolVal(v) { yield(b.Succs[0]) } else { yield(b.Succs[1]) diff --git a/config/staticconfig/testdata/conditional-included-by-const-expr.md b/config/staticconfig/testdata/conditional-included-by-const-expr.md index 5857952d..a100a6a5 100644 --- a/config/staticconfig/testdata/conditional-included-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-included-by-const-expr.md @@ -58,3 +58,23 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { } } ``` + +## If statement with non-const static condition + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" + +type App struct {} + +func (a App) Configure(c dogma.ApplicationConfigurer) { + if cond() { + c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") + } +} + +func cond() bool { + return true +} +``` From d1eae20469ae692c774f08910eae37914bcc1ffe Mon Sep 17 00:00:00 2001 From: James Harris Date: Mon, 21 Oct 2024 18:14:11 +1000 Subject: [PATCH 19/38] Add `transferOfControl()`. --- config/staticconfig/block.go | 14 ++++++++++++-- config/staticconfig/value.go | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/config/staticconfig/block.go b/config/staticconfig/block.go index f1ea5cfd..cd8bcc1f 100644 --- a/config/staticconfig/block.go +++ b/config/staticconfig/block.go @@ -40,8 +40,8 @@ func walkReachable(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { // reachableSuccessors yields the successors of b that are actually reachable. func reachableSuccessors(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { return func(yield func(*ssa.BasicBlock) bool) { - if branch, ok := b.Instrs[len(b.Instrs)-1].(*ssa.If); ok { - if v := staticValue(branch.Cond); v != nil { + if inst, ok := transferOfControl[*ssa.If](b); ok { + if v := staticValue(inst.Cond); v != nil { if constant.BoolVal(v) { yield(b.Succs[0]) } else { @@ -88,3 +88,13 @@ func isInevitable(from, to *ssa.BasicBlock) bool { return true } + +// transferOfControl returns the last instruction in b, if it is of type T. +func transferOfControl[T ssa.Instruction](b *ssa.BasicBlock) (inst T, ok bool) { + if len(b.Instrs) == 0 { + return inst, false + } + + inst, ok = b.Instrs[len(b.Instrs)-1].(T) + return inst, ok +} diff --git a/config/staticconfig/value.go b/config/staticconfig/value.go index daa2663a..e2caa102 100644 --- a/config/staticconfig/value.go +++ b/config/staticconfig/value.go @@ -74,7 +74,7 @@ func staticReturnValues(call *ssa.CallCommon) []constant.Value { var results []constant.Value for b := range walkReachable(fn.Blocks[0]) { - inst, ok := b.Instrs[len(b.Instrs)-1].(*ssa.Return) + inst, ok := transferOfControl[*ssa.Return](b) if !ok { continue } From 9627c5e22d5ef4105695387b88562846e5947857 Mon Sep 17 00:00:00 2001 From: James Harris Date: Tue, 22 Oct 2024 10:52:38 +1000 Subject: [PATCH 20/38] Add `analyzeHandler()`. --- config/staticconfig/application.go | 56 ++++++------------- config/staticconfig/handler.go | 87 ++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 40 deletions(-) create mode 100644 config/staticconfig/handler.go diff --git a/config/staticconfig/application.go b/config/staticconfig/application.go index 402a07be..1c82f646 100644 --- a/config/staticconfig/application.go +++ b/config/staticconfig/application.go @@ -22,13 +22,25 @@ func analyzeApplicationType(ctx *context, t types.Type) { case "Identity": analyzeIdentityCall(b, call) case "RegisterAggregate": - analyzeRegisterAggregateCall(b, call) + b.Aggregate(func(b *configbuilder.AggregateBuilder) { + b.UpdateFidelity(call.Fidelity) + analyzeAggregate(ctx, b, call.Args[0]) + }) case "RegisterProcess": - analyzeRegisterProcessCall(b, call) + b.Process(func(b *configbuilder.ProcessBuilder) { + b.UpdateFidelity(call.Fidelity) + analyzeProcess(ctx, b, call.Args[0]) + }) case "RegisterIntegration": - analyzeRegisterIntegrationCall(b, call) + b.Integration(func(b *configbuilder.IntegrationBuilder) { + b.UpdateFidelity(call.Fidelity) + analyzeIntegration(ctx, b, call.Args[0]) + }) case "RegisterProjection": - analyzeRegisterProjectionCall(b, call) + b.Projection(func(b *configbuilder.ProjectionBuilder) { + b.UpdateFidelity(call.Fidelity) + analyzeProjection(ctx, b, call.Args[0]) + }) default: b.UpdateFidelity(config.Incomplete) } @@ -37,39 +49,3 @@ func analyzeApplicationType(ctx *context, t types.Type) { ), ) } - -func analyzeRegisterAggregateCall( - b *configbuilder.ApplicationBuilder, - _ configurerCall, -) { - b.Aggregate(func(b *configbuilder.AggregateBuilder) { - b.UpdateFidelity(config.Incomplete) - }) -} - -func analyzeRegisterProcessCall( - b *configbuilder.ApplicationBuilder, - _ configurerCall, -) { - b.Process(func(b *configbuilder.ProcessBuilder) { - b.UpdateFidelity(config.Incomplete) - }) -} - -func analyzeRegisterIntegrationCall( - b *configbuilder.ApplicationBuilder, - _ configurerCall, -) { - b.Integration(func(b *configbuilder.IntegrationBuilder) { - b.UpdateFidelity(config.Incomplete) - }) -} - -func analyzeRegisterProjectionCall( - b *configbuilder.ApplicationBuilder, - _ configurerCall, -) { - b.Projection(func(b *configbuilder.ProjectionBuilder) { - b.UpdateFidelity(config.Incomplete) - }) -} diff --git a/config/staticconfig/handler.go b/config/staticconfig/handler.go new file mode 100644 index 00000000..adac424f --- /dev/null +++ b/config/staticconfig/handler.go @@ -0,0 +1,87 @@ +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)) + + for call := range findConfigurerCalls(ctx, b, t) { + switch call.Method.Name() { + case "Identity": + analyzeIdentityCall(b, call) + case "Routes": + // analyzeRoutesCall(ctx, b, call) + case "Disable": + b.SetDisabled(true) + default: + if !yield(call) { + return + } + } + } + + // 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, +) { + for call := range analyzeHandler(ctx, b, h) { + b.UpdateFidelity(call.Fidelity) + } +} From 6f1489585fb984c687beb70eec6f7290552d1aeb Mon Sep 17 00:00:00 2001 From: James Harris Date: Tue, 22 Oct 2024 12:57:26 +1000 Subject: [PATCH 21/38] Add `ssax` package. --- config/staticconfig/block.go | 100 --------------- config/staticconfig/configurer.go | 7 +- config/staticconfig/handler.go | 1 + config/staticconfig/identity.go | 11 +- config/staticconfig/internal/ssax/block.go | 37 ++++++ config/staticconfig/internal/ssax/const.go | 63 ++++++++++ config/staticconfig/internal/ssax/doc.go | 2 + config/staticconfig/internal/ssax/flow.go | 130 +++++++++++++++++++ config/staticconfig/internal/ssax/value.go | 140 +++++++++++++++++++++ config/staticconfig/value.go | 101 --------------- 10 files changed, 382 insertions(+), 210 deletions(-) delete mode 100644 config/staticconfig/block.go create mode 100644 config/staticconfig/internal/ssax/block.go create mode 100644 config/staticconfig/internal/ssax/const.go create mode 100644 config/staticconfig/internal/ssax/doc.go create mode 100644 config/staticconfig/internal/ssax/flow.go create mode 100644 config/staticconfig/internal/ssax/value.go delete mode 100644 config/staticconfig/value.go diff --git a/config/staticconfig/block.go b/config/staticconfig/block.go deleted file mode 100644 index cd8bcc1f..00000000 --- a/config/staticconfig/block.go +++ /dev/null @@ -1,100 +0,0 @@ -package staticconfig - -import ( - "go/constant" - "iter" - - "golang.org/x/tools/go/ssa" -) - -// walkReachable yields all blocks reachable from b. -func walkReachable(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { - return func(yield func(*ssa.BasicBlock) bool) { - yielded := map[*ssa.BasicBlock]struct{}{} - - var walk func(*ssa.BasicBlock) bool - walk = func(b *ssa.BasicBlock) bool { - if _, ok := yielded[b]; ok { - return true - } - - yielded[b] = struct{}{} - - if !yield(b) { - return false - } - - for succ := range reachableSuccessors(b) { - if !walk(succ) { - return false - } - } - - return true - } - - walk(b) - } -} - -// reachableSuccessors yields the successors of b that are actually reachable. -func reachableSuccessors(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { - return func(yield func(*ssa.BasicBlock) bool) { - if inst, ok := transferOfControl[*ssa.If](b); ok { - if v := staticValue(inst.Cond); v != nil { - if constant.BoolVal(v) { - yield(b.Succs[0]) - } else { - yield(b.Succs[1]) - } - - return - } - } - - for _, succ := range b.Succs { - if !yield(succ) { - return - } - } - } -} - -// isConditional returns true if there is any control flow path through the -// function that does NOT pass through b. -func isConditional(b *ssa.BasicBlock) bool { - return !isInevitable(b.Parent().Blocks[0], b) -} - -// isInevitable returns true if all paths out of "from" pass through "to". -func isInevitable(from, to *ssa.BasicBlock) bool { - if from == to { - return true - } - - if len(from.Succs) == 0 { - return false - } - - for succ := range reachableSuccessors(from) { - if succ == from { - continue - } - - if !isInevitable(succ, to) { - return false - } - } - - return true -} - -// transferOfControl returns the last instruction in b, if it is of type T. -func transferOfControl[T ssa.Instruction](b *ssa.BasicBlock) (inst T, ok bool) { - if len(b.Instrs) == 0 { - return inst, false - } - - inst, ok = b.Instrs[len(b.Instrs)-1].(T) - return inst, ok -} diff --git a/config/staticconfig/configurer.go b/config/staticconfig/configurer.go index e662dcdc..791870bb 100644 --- a/config/staticconfig/configurer.go +++ b/config/staticconfig/configurer.go @@ -6,6 +6,7 @@ import ( "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" ) @@ -74,8 +75,8 @@ func emitConfigurerCallsInFunc( return true } - for block := range walkReachable(fn.Blocks[0]) { - for _, inst := range block.Instrs { + for b := range ssax.WalkDown(fn.Blocks[0]) { + for _, inst := range b.Instrs { if !emitConfigurerCallsInInstruction(ctx, inst, yield) { return false } @@ -108,7 +109,7 @@ func emitConfigurerCallsInCallInstruction( if com.IsInvoke() && ctx.IsConfigurer(com.Value) { // We've found a direct call to a method on the configurer. var f config.Fidelity - if isConditional(call.Block()) { + if !ssax.IsUnconditional(call.Block()) { f |= config.Speculative } diff --git a/config/staticconfig/handler.go b/config/staticconfig/handler.go index adac424f..83e94211 100644 --- a/config/staticconfig/handler.go +++ b/config/staticconfig/handler.go @@ -5,6 +5,7 @@ import ( "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" ) diff --git a/config/staticconfig/identity.go b/config/staticconfig/identity.go index 3076416c..5d8c761f 100644 --- a/config/staticconfig/identity.go +++ b/config/staticconfig/identity.go @@ -1,10 +1,9 @@ package staticconfig import ( - "go/constant" - "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/config/internal/configbuilder" + "github.com/dogmatiq/enginekit/config/staticconfig/internal/ssax" ) func analyzeIdentityCall( @@ -14,14 +13,14 @@ func analyzeIdentityCall( b.Identity(func(b *configbuilder.IdentityBuilder) { b.UpdateFidelity(call.Fidelity) - if name := staticValue(call.Args[0]); name != nil { - b.SetName(constant.StringVal(name)) + if name, ok := ssax.AsString(call.Args[0]).TryGet(); ok { + b.SetName(name) } else { b.UpdateFidelity(config.Incomplete) } - if key := staticValue(call.Args[1]); key != nil { - b.SetKey(constant.StringVal(key)) + 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/block.go b/config/staticconfig/internal/ssax/block.go new file mode 100644 index 00000000..bb4786b2 --- /dev/null +++ b/config/staticconfig/internal/ssax/block.go @@ -0,0 +1,37 @@ +package ssax + +import ( + "iter" + + "github.com/dogmatiq/enginekit/optional" + "golang.org/x/tools/go/ssa" +) + +// Terminator returns the final "transfer of control" instruction in the given +// block. +// +// If the block does not contain any instructions (as is the case for external +// functions), or the terminator instruction is not of type T, ok is false. +// +// The instruction is always [ssa.If], [ssa.Jump], [ssa.Return], or [ssa.Panic]. +func Terminator[T ssa.Instruction](b *ssa.BasicBlock) optional.Optional[T] { + return optional.As[T](optional.Last(b.Instrs)) +} + +// InstructionsBefore yields all instructions in the block that precede the +// given instruction. +// +// It yields all instructions if inst is not in b. +func InstructionsBefore(b *ssa.BasicBlock, inst ssa.Instruction) iter.Seq[ssa.Instruction] { + return func(yield func(ssa.Instruction) bool) { + for _, x := range b.Instrs { + if x == inst { + return + } + + if !yield(x) { + return + } + } + } +} diff --git a/config/staticconfig/internal/ssax/const.go b/config/staticconfig/internal/ssax/const.go new file mode 100644 index 00000000..608afc22 --- /dev/null +++ b/config/staticconfig/internal/ssax/const.go @@ -0,0 +1,63 @@ +package ssax + +import ( + "go/constant" + "math" + + "github.com/dogmatiq/enginekit/optional" + "golang.org/x/tools/go/ssa" +) + +// Const returns the singlar constant value of v if possible. +func Const(v ssa.Value) optional.Optional[constant.Value] { + return optional.TryTransform( + StaticValue(v), + func(v ssa.Value) (constant.Value, bool) { + if c, ok := v.(*ssa.Const); ok { + return c.Value, true + } + return nil, false + }, + ) +} + +// AsString returns the singular constant string value of v if possible. +func AsString(v ssa.Value) optional.Optional[string] { + return constAs(constant.StringVal, v) +} + +// AsBool returns the singular constant boolean value of v if possible. +func AsBool(v ssa.Value) optional.Optional[bool] { + return constAs(constant.BoolVal, v) +} + +// AsInt returns the singular constant integer value of v if possible. +func AsInt(v ssa.Value) optional.Optional[int] { + return optional.TryTransform( + Const(v), + func(c constant.Value) (_ int, ok bool) { + i, ok := constant.Int64Val(c) + return int(i), ok && i >= math.MinInt && i <= math.MaxInt + }, + ) +} + +// constAsX returns the constant value of v, converted to type T by fn. +func constAs[T any]( + fn func(constant.Value) T, + v ssa.Value, +) optional.Optional[T] { + return optional.TryTransform( + Const(v), + func(c constant.Value) (_ T, ok bool) { + defer func() { + if recover() != nil { + // ignore panics about type conversion + ok = false + } + }() + + return fn(c), true + }, + ) +} diff --git a/config/staticconfig/internal/ssax/doc.go b/config/staticconfig/internal/ssax/doc.go new file mode 100644 index 00000000..c59a7135 --- /dev/null +++ b/config/staticconfig/internal/ssax/doc.go @@ -0,0 +1,2 @@ +// Package ssax contains general SSA-related utilities. +package ssax diff --git a/config/staticconfig/internal/ssax/flow.go b/config/staticconfig/internal/ssax/flow.go new file mode 100644 index 00000000..9a7f2ad6 --- /dev/null +++ b/config/staticconfig/internal/ssax/flow.go @@ -0,0 +1,130 @@ +package ssax + +import ( + "iter" + + "golang.org/x/tools/go/ssa" +) + +// 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 +// that block that does not depend on a condition that is known to be false at +// compile-time. +func WalkDown(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { + return walk(b, DirectSuccessors) +} + +// DirectSuccessors yields the reachable direct successors of b. +func DirectSuccessors(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { + return func(yield func(*ssa.BasicBlock) bool) { + successors := b.Succs + + if inst, ok := Terminator[*ssa.If](b).TryGet(); ok { + if cond, ok := AsBool(inst.Cond).TryGet(); ok { + if cond { + successors = b.Succs[:1] + } else { + successors = b.Succs[1:] + } + } + } + + for _, s := range successors { + if !yield(s) { + return + } + } + } +} + +// IsUnconditional returns true if all control-flow paths through the function +// containing b pass through b at some point. +func IsUnconditional(b *ssa.BasicBlock) bool { + return UnconditionalPathExists(b.Parent().Blocks[0], b) +} + +// PathExists returns true if, after dead-code elimnation, it's possible to +// traverse the control-flow graph from one specific node to another. +func PathExists(from, to *ssa.BasicBlock) bool { + if from.Parent() != to.Parent() { + panic("blocks are not in the same function") + } + + for b := range WalkDown(from) { + if b == to { + return true + } + } + + return false +} + +// UnconditionalPathExists returns true if, after dead-code elimination, all +// control flow paths from one node always lead to another. +func UnconditionalPathExists(from, to *ssa.BasicBlock) bool { + if from.Parent() != to.Parent() { + panic("blocks are not in the same function") + } + + seen := map[*ssa.BasicBlock]struct{}{} + + var exists func(*ssa.BasicBlock) bool + exists = func(from *ssa.BasicBlock) bool { + if _, ok := seen[from]; ok { + return true + } + seen[from] = struct{}{} + + if from == to { + return true + } + + if len(from.Succs) == 0 { + return false + } + + for s := range DirectSuccessors(from) { + if !exists(s) { + return false + } + } + + return true + } + + return exists(from) +} + +// walk recursively yields b, and the blocks yielded by next(b). It stops +// recursing when a cycle is detected. +func walk( + b *ssa.BasicBlock, + next func(*ssa.BasicBlock) iter.Seq[*ssa.BasicBlock], +) iter.Seq[*ssa.BasicBlock] { + return func(yield func(*ssa.BasicBlock) bool) { + seen := map[*ssa.BasicBlock]struct{}{} + + var emit func(*ssa.BasicBlock) bool + emit = func(b *ssa.BasicBlock) bool { + if _, ok := seen[b]; ok { + return true + } + + seen[b] = struct{}{} + if !yield(b) { + return false + } + + for n := range next(b) { + if !emit(n) { + return false + } + } + + return true + } + + emit(b) + } +} diff --git a/config/staticconfig/internal/ssax/value.go b/config/staticconfig/internal/ssax/value.go new file mode 100644 index 00000000..95cdeb97 --- /dev/null +++ b/config/staticconfig/internal/ssax/value.go @@ -0,0 +1,140 @@ +package ssax + +import ( + "fmt" + "go/constant" + "go/token" + + "github.com/dogmatiq/enginekit/optional" + "golang.org/x/tools/go/ssa" +) + +// StaticValue returns the singular value of v. +// +// If v cannot be resolved to a single value, it returns an empty optional. +func StaticValue(v ssa.Value) optional.Optional[ssa.Value] { + switch v := v.(type) { + case *ssa.Const: + return optional.Some[ssa.Value](v) + + case ssa.Instruction: + values := staticValuesFromInstruction(v) + + if len(values) == 1 { + fmt.Println("RESOLVED", values[0]) + return values[0] + } + + if len(values) > 1 { + panic("did not expect multiple values") + } + } + + // TODO(jmalloc): This implementation is incomplete. + return optional.None[ssa.Value]() +} + +// staticValuesFromInstruction returns the static value(s) that result from +// evaluating the given instruction. +// +// If an individual value within the expression cannot be resolved to a singular +// static value, it is represented as a nil value in the returned slice. +// +// It returns an empty slice if the expression itself cannot be resolved. +func staticValuesFromInstruction(inst ssa.Instruction) []optional.Optional[ssa.Value] { + switch inst := inst.(type) { + case *ssa.Call: + return staticValuesFromCall(inst.Common()) + + case *ssa.Extract: + if expr, ok := inst.Tuple.(ssa.Instruction); ok { + values := staticValuesFromInstruction(expr) + return values[inst.Index : inst.Index+1] + } + } + + // TODO(jmalloc): This implementation is incomplete. + return nil +} + +// staticValuesFromCall returns the static value(s) that result from evaluating +// a call to a function. +// +// If an individual value within the expression cannot be resolved to a singular +// static value, it is represented as a nil value in the returned slice. +// +// It returns an empty slice if the function itself cannot be resolved. For +// example, if it is a dynamic call to an interface method. +func staticValuesFromCall( + call *ssa.CallCommon, +) []optional.Optional[ssa.Value] { + // TODO: we could use StaticValue or some variant thereof to resolve the + // callee in more cases. + fn := call.StaticCallee() + if fn == nil { + // A call to an interface method. + return nil + } + + if len(fn.Blocks) == 0 { + // Probably an external C function. + return nil + } + + n := fn.Signature.Results().Len() + + if n == 0 { + // The function does not have any output parameters. + return nil + } + + outputs := make([]optional.Optional[ssa.Value], n) + conflicting := make([]bool, n) + + for b := range WalkDown(fn.Blocks[0]) { + ret, ok := Terminator[*ssa.Return](b).TryGet() + if !ok { + continue + } + + for i, v := range ret.Results { + if conflicting[i] { + continue + } + + v := StaticValue(v) + if !v.IsPresent() { + continue + } + + x := outputs[i] + if !x.IsPresent() { + outputs[i] = v + continue + } + + if !equal(x.Get(), v.Get()) { + conflicting[i] = true + outputs[i] = optional.None[ssa.Value]() + } + } + } + + return outputs +} + +// equal returns true if a and b refer to the same value, or are equal constant +// values. +func equal(a, b ssa.Value) bool { + if a == b { + return true + } + + if a, ok := a.(*ssa.Const); ok { + if b, ok := b.(*ssa.Const); ok { + return constant.Compare(a.Value, token.EQL, b.Value) + } + } + + return false +} diff --git a/config/staticconfig/value.go b/config/staticconfig/value.go deleted file mode 100644 index e2caa102..00000000 --- a/config/staticconfig/value.go +++ /dev/null @@ -1,101 +0,0 @@ -package staticconfig - -import ( - "go/constant" - "go/token" - - "golang.org/x/tools/go/ssa" -) - -// staticValue returns the constant value of v, if it's possible to obtain; -// otherwise, it returns nil. -func staticValue(v ssa.Value) constant.Value { - switch v := v.(type) { - case *ssa.Const: - return v.Value - case ssa.Instruction: - values := staticValueOfInstruction(v) - switch len(values) { - case 0: - return nil - case 1: - return values[0] - default: - panic("did not expect multiple values") - } - default: - return nil - } -} - -// staticValueOfInstruction returns the constant value(s) of an instruction. -// -// If an individual value within the expression cannot be resolved, it is -// represented as a nil value in the returned slice. -// -// It returns an empty slice if the expression itself cannot be resolved. -func staticValueOfInstruction(inst ssa.Instruction) []constant.Value { - switch inst := inst.(type) { - case *ssa.Call: - return staticReturnValues(inst.Common()) - case *ssa.Extract: - if expr, ok := inst.Tuple.(ssa.Instruction); ok { - values := staticValueOfInstruction(expr) - return values[inst.Index : inst.Index+1] - } - return nil - default: - return nil - } -} - -// staticReturnValues returns the constant values returned by a function. -// -// If an invividual value cannot be resolved, it is represented as a nil value -// in the returned slice. The function must return the same value on all control -// paths for a value to be considered constant. -// -// It returns nil if the function itself cannot be resolved. For example, if it -// is a dynamic call to an interface method. -func staticReturnValues(call *ssa.CallCommon) []constant.Value { - fn := call.StaticCallee() - if fn == nil { - return nil - } - - if len(fn.Blocks) == 0 { - return nil - } - - if fn.Signature.Results().Len() == 0 { - return nil - } - - var results []constant.Value - - for b := range walkReachable(fn.Blocks[0]) { - inst, ok := transferOfControl[*ssa.Return](b) - if !ok { - continue - } - - var values []constant.Value - - for _, v := range inst.Results { - values = append(values, staticValue(v)) - } - - if results == nil { - results = values - } else { - for i, a := range values { - b := results[i] - if constant.Compare(a, token.NEQ, b) { - results[i] = nil - } - } - } - } - - return results -} From c419faaad44f54e557c0b7aee88240ec0a8f6402 Mon Sep 17 00:00:00 2001 From: James Harris Date: Tue, 22 Oct 2024 13:51:57 +1000 Subject: [PATCH 22/38] Add primitive support for `Routes()`. --- config/staticconfig/configurer.go | 6 +- config/staticconfig/context.go | 24 +++++- config/staticconfig/handler.go | 3 +- config/staticconfig/route.go | 80 +++++++++++++++++++ .../_handler-with-unregistered-routes.md | 39 --------- .../testdata/{_aggregate.md => aggregate.md} | 4 +- config/staticconfig/varargs.go | 78 ++++++++++++++++++ 7 files changed, 184 insertions(+), 50 deletions(-) create mode 100644 config/staticconfig/route.go delete mode 100644 config/staticconfig/testdata/_handler-with-unregistered-routes.md rename config/staticconfig/testdata/{_aggregate.md => aggregate.md} (79%) create mode 100644 config/staticconfig/varargs.go diff --git a/config/staticconfig/configurer.go b/config/staticconfig/configurer.go index 791870bb..833901e6 100644 --- a/config/staticconfig/configurer.go +++ b/config/staticconfig/configurer.go @@ -32,8 +32,8 @@ func (c *configureContext) IsConfigurer(v ssa.Value) bool { type configurerCall struct { *ssa.CallCommon - - Fidelity config.Fidelity + Instruction ssa.CallInstruction + Fidelity config.Fidelity } // analyzeConfigurerCalls analyzes the calls to the "configurer" that is passed @@ -113,7 +113,7 @@ func emitConfigurerCallsInCallInstruction( f |= config.Speculative } - return yield(configurerCall{com, f}) + return yield(configurerCall{com, call, f}) } // We've found a call to some function or method that does not belong to the diff --git a/config/staticconfig/context.go b/config/staticconfig/context.go index 93517176..09db393c 100644 --- a/config/staticconfig/context.go +++ b/config/staticconfig/context.go @@ -12,9 +12,14 @@ type context struct { Packages []*ssa.Package Dogma struct { - Package *ssa.Package - Application *types.Interface - ApplicationConfigurer *types.Interface + Package *ssa.Package + Application *types.Interface + + HandlesCommand *types.Func + ExecutesCommand *types.Func + HandlesEvent *types.Func + RecordsEvent *types.Func + SchedulesTimeout *types.Func } Analysis *Analysis @@ -37,9 +42,20 @@ func findDogma(ctx *context) bool { Underlying().(*types.Interface) } + fn := func(n string) *types.Func { + return pkg.Pkg. + Scope(). + Lookup(n).(*types.Func) + } + ctx.Dogma.Package = pkg ctx.Dogma.Application = iface("Application") - ctx.Dogma.ApplicationConfigurer = iface("ApplicationConfigurer") + + ctx.Dogma.HandlesCommand = fn("HandlesCommand") + ctx.Dogma.ExecutesCommand = fn("ExecutesCommand") + ctx.Dogma.HandlesEvent = fn("HandlesEvent") + ctx.Dogma.RecordsEvent = fn("RecordsEvent") + ctx.Dogma.SchedulesTimeout = fn("SchedulesTimeout") return true } diff --git a/config/staticconfig/handler.go b/config/staticconfig/handler.go index 83e94211..4c1eff32 100644 --- a/config/staticconfig/handler.go +++ b/config/staticconfig/handler.go @@ -5,7 +5,6 @@ import ( "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" ) @@ -28,7 +27,7 @@ func analyzeHandler( case "Identity": analyzeIdentityCall(b, call) case "Routes": - // analyzeRoutesCall(ctx, b, call) + analyzeRoutesCall(ctx, b, call) case "Disable": b.SetDisabled(true) default: diff --git a/config/staticconfig/route.go b/config/staticconfig/route.go new file mode 100644 index 00000000..0487afee --- /dev/null +++ b/config/staticconfig/route.go @@ -0,0 +1,80 @@ +package staticconfig + +import ( + "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 analyzeRoutesCall( + ctx *context, + b configbuilder.HandlerBuilder, + call configurerCall, +) { + routes, ok := resolveVariadic(b, call) + if !ok { + b.UpdateFidelity(config.Incomplete) + return + } + + for _, r := range routes { + b.Route(func(b *configbuilder.RouteBuilder) { + b.UpdateFidelity(call.Fidelity) + analyzeRoute(ctx, b, r) + }) + } +} + +func analyzeRoute( + ctx *context, + b *configbuilder.RouteBuilder, + r ssa.Value, +) { + call, ok := findRouteCall(ctx, r) + if !ok { + b.UpdateFidelity(config.Incomplete) + return + } + + fn := call.Common().StaticCallee() + + switch fn.Object() { + case ctx.Dogma.HandlesCommand: + b.SetRouteType(config.HandlesCommandRouteType) + case ctx.Dogma.HandlesEvent: + b.SetRouteType(config.HandlesEventRouteType) + case ctx.Dogma.ExecutesCommand: + b.SetRouteType(config.ExecutesCommandRouteType) + case ctx.Dogma.RecordsEvent: + b.SetRouteType(config.RecordsEventRouteType) + case ctx.Dogma.SchedulesTimeout: + b.SetRouteType(config.SchedulesTimeoutRouteType) + } + + b.SetMessageTypeName(typename.OfStatic(fn.TypeArgs()[0])) +} + +func findRouteCall( + ctx *context, + v ssa.Value, +) (*ssa.Call, bool) { + switch v := v.(type) { + case *ssa.Call: + fn := v.Common().StaticCallee() + if fn != nil { + switch fn.Object() { + case ctx.Dogma.HandlesCommand, + ctx.Dogma.HandlesEvent, + ctx.Dogma.ExecutesCommand, + ctx.Dogma.RecordsEvent, + ctx.Dogma.SchedulesTimeout: + return v, true + } + } + case *ssa.MakeInterface: + return findRouteCall(ctx, v.X) + } + + return nil, false +} diff --git a/config/staticconfig/testdata/_handler-with-unregistered-routes.md b/config/staticconfig/testdata/_handler-with-unregistered-routes.md deleted file mode 100644 index f5d064db..00000000 --- a/config/staticconfig/testdata/_handler-with-unregistered-routes.md +++ /dev/null @@ -1,39 +0,0 @@ -# Handler with unregistered routes - -This test verifies that static analyzer does not include information about -routes that are constructed but never passed to the configurer's `Routes()` -method. - -```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - - valid identity app/f2c08525-623e-4c76-851c-3172953269e3 - - invalid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (runtime type unavailable) - - no "handles-command" routes are configured - - valid identity handler/ac391765-da58-4e7c-a478-e4725eb2b0e9 -``` - -```go au:input au:group=matrix -package app - -import ( - "context" - "github.com/dogmatiq/dogma" - "github.com/dogmatiq/enginekit/enginetest/stubs" -) - -type Integration struct{} - -func (Integration) Configure(c dogma.IntegrationConfigurer) { - c.Identity("handler", "ac391765-da58-4e7c-a478-e4725eb2b0e9") - dogma.HandlesCommand[stubs.CommandStub[stubs.TypeX]]() -} - -type App struct{} - -func (App) Configure(c dogma.ApplicationConfigurer) { - c.Identity("app", "f2c08525-623e-4c76-851c-3172953269e3") - c.RegisterIntegration(Integration{}) -} - -func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } -``` diff --git a/config/staticconfig/testdata/_aggregate.md b/config/staticconfig/testdata/aggregate.md similarity index 79% rename from config/staticconfig/testdata/_aggregate.md rename to config/staticconfig/testdata/aggregate.md index 2bfec28f..a97dde50 100644 --- a/config/staticconfig/testdata/_aggregate.md +++ b/config/staticconfig/testdata/aggregate.md @@ -8,8 +8,8 @@ valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 - disabled valid aggregate github.com/dogmatiq/enginekit/config/staticconfig.Aggregate (runtime type unavailable) - valid identity aggregate/916e5e95-70c4-4823-9de2-0f7389d18b4f - - incomplete route - - incomplete route + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) + - valid records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) ``` ```go au:input au:group=matrix diff --git a/config/staticconfig/varargs.go b/config/staticconfig/varargs.go new file mode 100644 index 00000000..ac45b882 --- /dev/null +++ b/config/staticconfig/varargs.go @@ -0,0 +1,78 @@ +package staticconfig + +import ( + "go/token" + "go/types" + + "github.com/dogmatiq/enginekit/config/internal/configbuilder" + "github.com/dogmatiq/enginekit/config/staticconfig/internal/ssax" + "golang.org/x/tools/go/ssa" +) + +func findAllocation(v ssa.Value) (*ssa.Alloc, bool) { + // fmt.Println("***", v, reflect.TypeOf(v)) + switch v := v.(type) { + case *ssa.Alloc: + return v, true + + case *ssa.Slice: + return findAllocation(v.X) + + case *ssa.UnOp: + if v.Op == token.MUL { // pointer de-reference + return findAllocation(v.X) + } + return nil, false + + default: + return nil, false + } +} + +func isIndexOfArray( + array *ssa.Alloc, + v ssa.Value, +) (int, bool) { + switch v := v.(type) { + case *ssa.IndexAddr: + if v.X != array { + return 0, false + } + return ssax.AsInt(v.Index).TryGet() + } + + return 0, false +} + +func resolveVariadic( + _ configbuilder.EntityBuilder, + call configurerCall, +) ([]ssa.Value, bool) { + n := len(call.Args) - 1 + variadics := call.Args[n] + + array, ok := findAllocation(variadics) + if !ok { + return nil, false + } + + size := array.Type().Underlying().(*types.Pointer).Elem().(*types.Array).Len() + result := make([]ssa.Value, size) + + for b := range ssax.WalkDown(array.Block()) { + if !ssax.PathExists(b, call.Instruction.Block()) { + continue + } + + for inst := range ssax.InstructionsBefore(b, call.Instruction) { + switch inst := inst.(type) { + case *ssa.Store: + if i, ok := isIndexOfArray(array, inst.Addr); ok { + result[i] = inst.Val + } + } + } + } + + return result, true +} From 3b5f189385934afc69fc990fa8ecfc5e43e7e9f4 Mon Sep 17 00:00:00 2001 From: James Harris Date: Tue, 22 Oct 2024 17:44:21 +1000 Subject: [PATCH 23/38] Add basic tests for remaining handler and route types. --- config/staticconfig/analyze_test.go | 4 +- config/staticconfig/configurer.go | 1 + config/staticconfig/internal/ssax/const.go | 9 +++ config/staticconfig/internal/ssax/value.go | 9 +-- config/staticconfig/route.go | 8 +- .../{aggregate.md => handler-aggregate.md} | 4 +- .../testdata/handler-integration.md | 41 ++++++++++ .../staticconfig/testdata/handler-process.md | 46 +++++++++++ .../testdata/handler-projection.md | 45 +++++++++++ .../handler-with-unregistered-routes.md | 79 +++++++++++++++++++ config/staticconfig/varargs.go | 51 ++++++------ 11 files changed, 256 insertions(+), 41 deletions(-) rename config/staticconfig/testdata/{aggregate.md => handler-aggregate.md} (97%) create mode 100644 config/staticconfig/testdata/handler-integration.md create mode 100644 config/staticconfig/testdata/handler-process.md create mode 100644 config/staticconfig/testdata/handler-projection.md create mode 100644 config/staticconfig/testdata/handler-with-unregistered-routes.md diff --git a/config/staticconfig/analyze_test.go b/config/staticconfig/analyze_test.go index 147312d1..745d1f64 100644 --- a/config/staticconfig/analyze_test.go +++ b/config/staticconfig/analyze_test.go @@ -38,7 +38,9 @@ func TestAnalyzer(t *testing.T) { if err != nil { return err } - defer os.RemoveAll(dir) + t.Cleanup(func() { + os.RemoveAll(dir) + }) f, err := os.Create(filepath.Join(dir, "main.go")) if err != nil { diff --git a/config/staticconfig/configurer.go b/config/staticconfig/configurer.go index 833901e6..07f02309 100644 --- a/config/staticconfig/configurer.go +++ b/config/staticconfig/configurer.go @@ -32,6 +32,7 @@ func (c *configureContext) IsConfigurer(v ssa.Value) bool { type configurerCall struct { *ssa.CallCommon + Instruction ssa.CallInstruction Fidelity config.Fidelity } diff --git a/config/staticconfig/internal/ssax/const.go b/config/staticconfig/internal/ssax/const.go index 608afc22..673f9c58 100644 --- a/config/staticconfig/internal/ssax/const.go +++ b/config/staticconfig/internal/ssax/const.go @@ -8,6 +8,15 @@ import ( "golang.org/x/tools/go/ssa" ) +// IsZeroValue returns true if v is a constant value that represents the zero +// value for its type. +func IsZeroValue(v ssa.Value) bool { + if c, ok := Const(v).TryGet(); ok { + return c == nil + } + return false +} + // Const returns the singlar constant value of v if possible. func Const(v ssa.Value) optional.Optional[constant.Value] { return optional.TryTransform( diff --git a/config/staticconfig/internal/ssax/value.go b/config/staticconfig/internal/ssax/value.go index 95cdeb97..2923c974 100644 --- a/config/staticconfig/internal/ssax/value.go +++ b/config/staticconfig/internal/ssax/value.go @@ -1,7 +1,6 @@ package ssax import ( - "fmt" "go/constant" "go/token" @@ -19,15 +18,13 @@ func StaticValue(v ssa.Value) optional.Optional[ssa.Value] { case ssa.Instruction: values := staticValuesFromInstruction(v) + if len(values) > 1 { + panic("did not expect multiple values") + } if len(values) == 1 { - fmt.Println("RESOLVED", values[0]) return values[0] } - - if len(values) > 1 { - panic("did not expect multiple values") - } } // TODO(jmalloc): This implementation is incomplete. diff --git a/config/staticconfig/route.go b/config/staticconfig/route.go index 0487afee..2f33b644 100644 --- a/config/staticconfig/route.go +++ b/config/staticconfig/route.go @@ -12,13 +12,7 @@ func analyzeRoutesCall( b configbuilder.HandlerBuilder, call configurerCall, ) { - routes, ok := resolveVariadic(b, call) - if !ok { - b.UpdateFidelity(config.Incomplete) - return - } - - for _, r := range routes { + for r := range resolveVariadic(b, call) { b.Route(func(b *configbuilder.RouteBuilder) { b.UpdateFidelity(call.Fidelity) analyzeRoute(ctx, b, r) diff --git a/config/staticconfig/testdata/aggregate.md b/config/staticconfig/testdata/handler-aggregate.md similarity index 97% rename from config/staticconfig/testdata/aggregate.md rename to config/staticconfig/testdata/handler-aggregate.md index a97dde50..d84ecb35 100644 --- a/config/staticconfig/testdata/aggregate.md +++ b/config/staticconfig/testdata/handler-aggregate.md @@ -1,7 +1,7 @@ -# Aggregate +# Aggregate message handler This test ensures that the static analyzer supports all aspects of configuring -an aggregate. +an aggregate handler. ```au:output au:group=matrix valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) diff --git a/config/staticconfig/testdata/handler-integration.md b/config/staticconfig/testdata/handler-integration.md new file mode 100644 index 00000000..8786aa7b --- /dev/null +++ b/config/staticconfig/testdata/handler-integration.md @@ -0,0 +1,41 @@ +# Integration message handler + +This test ensures that the static analyzer supports all aspects of configuring +an integration handler. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) + - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 + - disabled valid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (runtime type unavailable) + - valid identity integration/b92431e6-3a7d-4235-a76f-541622c487ee + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) + - valid records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) +``` + +```go au:input au:group=matrix +package app + +import "context" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Integration struct {} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("integration", "b92431e6-3a7d-4235-a76f-541622c487ee") + c.Routes( + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]](), + dogma.RecordsEvent[stubs.EventStub[stubs.TypeA]](), + ) + c.Disable() +} + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "0726ae0d-67e4-4a50-8a19-9f58eae38e51") + c.RegisterIntegration(Integration{}) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } +``` diff --git a/config/staticconfig/testdata/handler-process.md b/config/staticconfig/testdata/handler-process.md new file mode 100644 index 00000000..4e9f03c3 --- /dev/null +++ b/config/staticconfig/testdata/handler-process.md @@ -0,0 +1,46 @@ +# Process message handler + +This test ensures that the static analyzer supports all aspects of configuring +a process handler. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) + - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 + - disabled valid process github.com/dogmatiq/enginekit/config/staticconfig.Process (runtime type unavailable) + - valid identity process/4ff1b1c1-5c64-49bc-a547-c13f5bafad7d + - valid handles-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) + - valid executes-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) + - valid schedules-timeout route for github.com/dogmatiq/enginekit/enginetest/stubs.TimeoutStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) +``` + +```go au:input au:group=matrix +package app + +import "context" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Process struct {} + +func (Process) Configure(c dogma.ProcessConfigurer) { + c.Identity("process", "4ff1b1c1-5c64-49bc-a547-c13f5bafad7d") + c.Routes( + dogma.HandlesEvent[stubs.EventStub[stubs.TypeA]](), + dogma.ExecutesCommand[stubs.CommandStub[stubs.TypeA]](), + dogma.SchedulesTimeout[stubs.TimeoutStub[stubs.TypeA]](), + ) + c.Disable() +} + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "0726ae0d-67e4-4a50-8a19-9f58eae38e51") + c.RegisterProcess(Process{}) +} + +func (Process) New() dogma.ProcessRoot { return nil } +func (Process) RouteEventToInstance(context.Context, dogma.Event) (string, bool, error) { return "", false, nil } +func (Process) HandleEvent(context.Context, dogma.ProcessRoot, dogma.ProcessEventScope, dogma.Event) error { return nil } +func (Process) HandleTimeout(context.Context, dogma.ProcessRoot, dogma.ProcessTimeoutScope, dogma.Timeout) error { return nil } +``` diff --git a/config/staticconfig/testdata/handler-projection.md b/config/staticconfig/testdata/handler-projection.md new file mode 100644 index 00000000..6124cd53 --- /dev/null +++ b/config/staticconfig/testdata/handler-projection.md @@ -0,0 +1,45 @@ +# Projection message handler + +This test ensures that the static analyzer supports all aspects of configuring +a projection handler. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) + - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 + - disabled valid projection github.com/dogmatiq/enginekit/config/staticconfig.Projection (runtime type unavailable) + - valid identity projection/238d7498-515b-44b5-b6a8-914a08762ecc + - valid handles-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) +``` + +```go au:input au:group=matrix +package app + +import "context" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Projection struct {} + +func (Projection) Configure(c dogma.ProjectionConfigurer) { + c.Identity("projection", "238d7498-515b-44b5-b6a8-914a08762ecc") + c.Routes( + dogma.HandlesEvent[stubs.EventStub[stubs.TypeA]](), + ) + // c.DeliveryPolicy(dogma.BroadcastProjectionDeliveryPolicy{ + // PrimaryFirst: true, + // }) + c.Disable() +} + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "0726ae0d-67e4-4a50-8a19-9f58eae38e51") + c.RegisterProjection(Projection{}) +} + +func (Projection) HandleEvent(context.Context, []byte, []byte, []byte, dogma.ProjectionEventScope, dogma.Event) (bool, error) { return false, nil } +func (Projection) Compact(context.Context, dogma.ProjectionCompactScope) error { return nil } +func (Projection) ResourceVersion(context.Context, []byte) ([]byte, error) +func (Projection) CloseResource(context.Context, []byte) error +``` diff --git a/config/staticconfig/testdata/handler-with-unregistered-routes.md b/config/staticconfig/testdata/handler-with-unregistered-routes.md new file mode 100644 index 00000000..eb502080 --- /dev/null +++ b/config/staticconfig/testdata/handler-with-unregistered-routes.md @@ -0,0 +1,79 @@ +# Handler with unregistered routes + +This test verifies that static analyzer does not include information about +routes that are constructed but never passed to the configurer's `Routes()` +method. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) + - valid identity app/f2c08525-623e-4c76-851c-3172953269e3 + - invalid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (runtime type unavailable) + - no "handles-command" routes are configured + - valid identity handler/ac391765-da58-4e7c-a478-e4725eb2b0e9 +``` + +## No call to Routes() + +```go au:input au:group=matrix +package app + +import ( + "context" + "github.com/dogmatiq/dogma" + "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "ac391765-da58-4e7c-a478-e4725eb2b0e9") + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeX]]() +} + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "f2c08525-623e-4c76-851c-3172953269e3") + c.RegisterIntegration(Integration{}) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } +``` + +## Route appended to slice after calling Routes() + +```go au:input au:group=matrix +package app + +import ( + "context" + "github.com/dogmatiq/dogma" + "github.com/dogmatiq/enginekit/enginetest/stubs" +) + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "ac391765-da58-4e7c-a478-e4725eb2b0e9") + + var routes []dogma.IntegrationRoute + + c.Routes(routes...) + + routes = append( + routes, + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeX]](), + ) +} + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "f2c08525-623e-4c76-851c-3172953269e3") + + + c.RegisterIntegration(Integration{}) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } +``` diff --git a/config/staticconfig/varargs.go b/config/staticconfig/varargs.go index ac45b882..d241f576 100644 --- a/config/staticconfig/varargs.go +++ b/config/staticconfig/varargs.go @@ -2,15 +2,15 @@ package staticconfig import ( "go/token" - "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" ) func findAllocation(v ssa.Value) (*ssa.Alloc, bool) { - // fmt.Println("***", v, reflect.TypeOf(v)) switch v := v.(type) { case *ssa.Alloc: return v, true @@ -40,39 +40,40 @@ func isIndexOfArray( } return ssax.AsInt(v.Index).TryGet() } - return 0, false } func resolveVariadic( - _ configbuilder.EntityBuilder, + b configbuilder.EntityBuilder, call configurerCall, -) ([]ssa.Value, bool) { - n := len(call.Args) - 1 - variadics := call.Args[n] - - array, ok := findAllocation(variadics) - if !ok { - return nil, false - } - - size := array.Type().Underlying().(*types.Pointer).Elem().(*types.Array).Len() - result := make([]ssa.Value, size) +) iter.Seq[ssa.Value] { + return func(yield func(ssa.Value) bool) { + variadics := call.Args[len(call.Args)-1] + if ssax.IsZeroValue(variadics) { + return + } - for b := range ssax.WalkDown(array.Block()) { - if !ssax.PathExists(b, call.Instruction.Block()) { - continue + array, ok := findAllocation(variadics) + if !ok { + b.UpdateFidelity(config.Incomplete) + return } - for inst := range ssax.InstructionsBefore(b, call.Instruction) { - switch inst := inst.(type) { - case *ssa.Store: - if i, ok := isIndexOfArray(array, inst.Addr); ok { - result[i] = inst.Val + for b := range ssax.WalkDown(array.Block()) { + if !ssax.PathExists(b, call.Instruction.Block()) { + continue + } + + for inst := range ssax.InstructionsBefore(b, call.Instruction) { + switch inst := inst.(type) { + case *ssa.Store: + if _, ok := isIndexOfArray(array, inst.Addr); ok { + if !yield(inst.Val) { + return + } + } } } } } - - return result, true } From dcbd1d160c43d6eeb0dd54157ad38838a5aa7207 Mon Sep 17 00:00:00 2001 From: James Harris Date: Wed, 23 Oct 2024 13:13:44 +1000 Subject: [PATCH 24/38] Write temporary files to directory under PID. --- config/staticconfig/analyze_test.go | 140 ++-------------------------- go.mod | 4 +- go.sum | 30 +----- 3 files changed, 16 insertions(+), 158 deletions(-) diff --git a/config/staticconfig/analyze_test.go b/config/staticconfig/analyze_test.go index 745d1f64..c0c53928 100644 --- a/config/staticconfig/analyze_test.go +++ b/config/staticconfig/analyze_test.go @@ -4,6 +4,7 @@ import ( "io" "os" "path/filepath" + "strconv" "strings" "testing" @@ -24,10 +25,18 @@ func TestAnalyzer(t *testing.T) { // Since it's under the testdata directory it is ignored by Go's tooling, // but it is still subject to the same go.mod file, and hence the same // version of Dogma, etc. - outputDir := filepath.Join(cwd, "testdata", ".aureus") + outputDir := filepath.Join( + cwd, + "testdata", + ".aureus", + strconv.Itoa(os.Getpid()), + ) if err := os.MkdirAll(outputDir, 0700); err != nil { t.Fatal(err) } + t.Cleanup(func() { + os.RemoveAll(outputDir) + }) aureus.Run( t, @@ -103,133 +112,4 @@ func TestAnalyzer(t *testing.T) { return nil }, ) - - // t.Run("should parse multiple packages contain applications", func(t *testing.T) { - // apps := FromDir("testdata/multiple-apps-in-pkgs") - - // if len(apps) != 2 { - // t.Fatalf("expected 2 applications, got %d", len(apps)) - // } - - // if expected, actual := "", - // apps[0].Identity().Name; expected != actual { - // t.Fatalf( - // "unexpected application name: want %s, got %s", - // expected, - // actual, - // ) - // } - - // if expected, actual := "b754902b-47c8-48fc-84d2-d920c9cbdaec", - // apps[0].Identity().Key; expected != actual { - // t.Fatalf( - // "unexpected application key: want %s, got %s", - // expected, - // actual, - // ) - // } - - // if expected, actual := "", - // apps[1].Identity().Name; expected != actual { - // t.Fatalf( - // "unexpected application name: want %s, got %s", - // expected, - // actual, - // ) - // } - - // if expected, actual := "bfaf2a16-23a0-495d-8098-051d77635822", - // apps[1].Identity().Key; expected != actual { - // t.Fatalf( - // "unexpected application key: want %s, got %s", - // expected, - // actual, - // ) - // } - // }) - - // t.Run("should parse all application-level messages", func(t *testing.T) { - // apps := FromDir("testdata/app-level-messages") - - // if len(apps) != 1 { - // t.Fatalf("expected 1 application, got %d", len(apps)) - // } - - // contains := func( - // mn message.Name, - // mk message.Kind, - // iterator iter.Seq2[message.Name, message.Kind], - // ) bool { - // for k, v := range iterator { - // if k == mn && v == mk { - // return true - // } - // } - // return false - // } - - // if !contains( - // message.NameFor[CommandStub[TypeA]](), - // message.CommandKind, - // apps[0].MessageNames().Consumed(), - // ) { - // t.Fatal("expected consumed TypeA command message") - // } - - // if !contains( - // message.NameFor[EventStub[TypeA]](), - // message.EventKind, - // apps[0].MessageNames().Consumed(), - // ) { - // t.Fatal("expected consumed TypeA event message") - // } - - // if !contains( - // message.NameFor[EventStub[TypeC]](), - // message.EventKind, - // apps[0].MessageNames().Consumed(), - // ) { - // t.Fatal("expected consumed TypeC event message") - // } - - // if !contains( - // message.NameFor[TimeoutStub[TypeA]](), - // message.TimeoutKind, - // apps[0].MessageNames().Consumed(), - // ) { - // t.Fatal("expected consumed TypeA timeout message") - // } - - // if !contains( - // message.NameFor[EventStub[TypeA]](), - // message.EventKind, - // apps[0].MessageNames().Produced(), - // ) { - // t.Fatal("expected produced TypeA event message") - // } - - // if !contains( - // message.NameFor[CommandStub[TypeB]](), - // message.CommandKind, - // apps[0].MessageNames().Produced(), - // ) { - // t.Fatal("expected produced TypeB command message") - // } - - // if !contains( - // message.NameFor[TimeoutStub[TypeA]](), - // message.TimeoutKind, - // apps[0].MessageNames().Produced(), - // ) { - // t.Fatal("expected produced TypeA timeout message") - // } - - // if !contains( - // message.NameFor[EventStub[TypeB]](), - // message.EventKind, - // apps[0].MessageNames().Produced(), - // ) { - // t.Fatal("expected produced TypeB event message") - // } - // }) } diff --git a/go.mod b/go.mod index 528ee1ea..4fca7b08 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/dogmatiq/enginekit go 1.23 require ( - github.com/dogmatiq/aureus v0.2.1 + github.com/dogmatiq/aureus v0.2.2 github.com/dogmatiq/dapper v0.6.0 github.com/dogmatiq/dogma v0.15.0 github.com/dogmatiq/primo v0.3.1 @@ -16,7 +16,7 @@ require ( require ( github.com/dogmatiq/jumble v0.1.0 // indirect - github.com/yuin/goldmark v1.7.4 // indirect + github.com/yuin/goldmark v1.7.8 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect diff --git a/go.sum b/go.sum index 6e7c1c0f..dfc26aa7 100644 --- a/go.sum +++ b/go.sum @@ -10,18 +10,8 @@ github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 h1:N+3sFI5GUjRKBi+i0Tx github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= -github.com/dogmatiq/aureus v0.1.0 h1:BIUF1G4pdCiJ+WQ6GnTmbhaejbjtX35Z9w2somdgslA= -github.com/dogmatiq/aureus v0.1.0/go.mod h1:eTm6/WDfVI2tNjg1WCXiPt4fqjMhjO2kNM522ENa6mM= -github.com/dogmatiq/aureus v0.1.1-0.20241007045608-56b32324299c h1:Z00tDp3KdWeTGVpqPEn4OR/gJAfEsXcCGVDm7KFovRY= -github.com/dogmatiq/aureus v0.1.1-0.20241007045608-56b32324299c/go.mod h1:oyg9b8Y10wH8M5OngzHapHFHMOobqLrkbky6hhdeATU= -github.com/dogmatiq/aureus v0.2.0 h1:YKRTm9caNUxC9TZafhchm8ZIXycGDSDKU6lRv74aisQ= -github.com/dogmatiq/aureus v0.2.0/go.mod h1:oyg9b8Y10wH8M5OngzHapHFHMOobqLrkbky6hhdeATU= -github.com/dogmatiq/aureus v0.2.1-0.20241020043618-02979f76b5b4 h1:cBEW1TdIoVSIyUpgz5ow5DUpxuXmom0w5Rm7uvnLu8k= -github.com/dogmatiq/aureus v0.2.1-0.20241020043618-02979f76b5b4/go.mod h1:oyg9b8Y10wH8M5OngzHapHFHMOobqLrkbky6hhdeATU= -github.com/dogmatiq/aureus v0.2.1-0.20241020043859-36cdbb3a14fe h1:fQxBi/snwSeZPl3w3pZvI23K+/8YA3iSLj0/aUBDenQ= -github.com/dogmatiq/aureus v0.2.1-0.20241020043859-36cdbb3a14fe/go.mod h1:oyg9b8Y10wH8M5OngzHapHFHMOobqLrkbky6hhdeATU= -github.com/dogmatiq/aureus v0.2.1 h1:cy89t0u3EpnPBhp9Z4L5vkG9f4cVMhG4zQkDtFZv7p0= -github.com/dogmatiq/aureus v0.2.1/go.mod h1:oyg9b8Y10wH8M5OngzHapHFHMOobqLrkbky6hhdeATU= +github.com/dogmatiq/aureus v0.2.2 h1:3SMF+GMntHyO5Y3cSbKVgX1w4I0+B6llV0zU8p+D6QQ= +github.com/dogmatiq/aureus v0.2.2/go.mod h1:ZHusLaF9NnCPt2nZOBjYCRcJpZ66PTMpZkpCfbEsRdU= github.com/dogmatiq/dapper v0.6.0 h1:hnWUsjnt3nUiC9hmkPvuxrnMd7fYNz1i+/GS3gOx0Xs= github.com/dogmatiq/dapper v0.6.0/go.mod h1:ubRHWzt73s0MsPpGhWvnfW/Z/1YPnrkCsQv6CUOZVEw= github.com/dogmatiq/dogma v0.15.0 h1:aXOTd2K4wLvlwHc1D9OsFREp0BusNJ9o9KssxURftmg= @@ -44,38 +34,26 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= -github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA= -github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= -github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= +github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= From 42fd0f0187f5dc4c00942b5c2eb521fc646af6ad Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 24 Oct 2024 07:17:45 +1000 Subject: [PATCH 25/38] Refactor analysis code to use callbacks instead of iterators. --- 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 | 111 ++++++------- 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, 280 insertions(+), 309 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 fb9f7a3e..087c33c1 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 1c82f646..909899c7 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 07f02309..00000000 --- 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 09db393c..abd475a7 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 00000000..42967546 --- /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 4c1eff32..29c080d6 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: +func analyzeHandler[T configbuilder.HandlerBuilder]( + ctx *configurerCallContext[*configbuilder.ApplicationBuilder], + build func(func(T)), + analyze configurerCallAnalyzer[T], +) { + build(func(b T) { + b.UpdateFidelity(ctx.Fidelity) + + inst, ok := ctx.Args[0].(*ssa.MakeInterface) + if !ok { b.UpdateFidelity(config.Incomplete) - case *ssa.MakeInterface: - t := inst.X.Type() - b.SetSourceTypeName(typename.OfStatic(t)) + return + } - for call := range findConfigurerCalls(ctx, b, t) { - switch call.Method.Name() { - case "Identity": - analyzeIdentityCall(b, call) + 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 5d8c761f..00000000 --- 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 9a7f2ad6..6eb81a79 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 2f33b644..50fb3cf5 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 42d07914..142bb3c4 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 d241f576..72568f40 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 { From 14c0a181bc078fdd99a53d315430ce6043482f31 Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 24 Oct 2024 07:31:46 +1000 Subject: [PATCH 26/38] Fix test title. --- config/staticconfig/testdata/alias-for-generic.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/staticconfig/testdata/alias-for-generic.md b/config/staticconfig/testdata/alias-for-generic.md index 14c67d32..dc02eb0e 100644 --- a/config/staticconfig/testdata/alias-for-generic.md +++ b/config/staticconfig/testdata/alias-for-generic.md @@ -1,4 +1,4 @@ -# Iinstatiated generic application +# Generic application instantiated via an alias This test ensures that the static analyzer finds an instantiated generic type that implements the `dogma.Application` interface. From b279e59d7f4e258e06281ad716293a3d1cf21c19 Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 24 Oct 2024 09:24:39 +1000 Subject: [PATCH 27/38] Move generic package/type/value analysis code to `ssax`. --- config/staticconfig/analyze.go | 7 +- config/staticconfig/context.go | 3 +- config/staticconfig/handler.go | 9 +- config/staticconfig/internal/ssax/type.go | 122 ++++++++++++++++++++++ config/staticconfig/type.go | 74 ------------- 5 files changed, 135 insertions(+), 80 deletions(-) create mode 100644 config/staticconfig/internal/ssax/type.go delete mode 100644 config/staticconfig/type.go diff --git a/config/staticconfig/analyze.go b/config/staticconfig/analyze.go index 087c33c1..06ed95cf 100644 --- a/config/staticconfig/analyze.go +++ b/config/staticconfig/analyze.go @@ -6,6 +6,7 @@ import ( "slices" "github.com/dogmatiq/enginekit/config" + "github.com/dogmatiq/enginekit/config/staticconfig/internal/ssax" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" @@ -123,9 +124,13 @@ func Analyze(pkgs []*packages.Package) Analysis { continue } + // Search through all members of the package to find types that + // implement [dogma.Application]. for _, m := range pkg.Members { if t, ok := m.(*ssa.Type); ok { - analyzeType(ctx, t.Type()) + if r, ok := ssax.Implements(t, ctx.Dogma.Application); ok { + analyzeApplicationType(ctx, r) + } } } } diff --git a/config/staticconfig/context.go b/config/staticconfig/context.go index abd475a7..dc715193 100644 --- a/config/staticconfig/context.go +++ b/config/staticconfig/context.go @@ -4,6 +4,7 @@ import ( "fmt" "go/types" + "github.com/dogmatiq/enginekit/config/staticconfig/internal/ssax" "golang.org/x/tools/go/ssa" ) @@ -64,7 +65,7 @@ func resolveDogmaPackage(ctx *context) bool { } func (c *context) LookupMethod(t types.Type, name string) *ssa.Function { - fn := c.Program.LookupMethod(t, packageOf(t), name) + fn := c.Program.LookupMethod(t, ssax.Package(t), name) if fn == nil { panic(fmt.Sprintf("method not found: %s.%s", t, name)) } diff --git a/config/staticconfig/handler.go b/config/staticconfig/handler.go index 29c080d6..33dd5924 100644 --- a/config/staticconfig/handler.go +++ b/config/staticconfig/handler.go @@ -3,7 +3,7 @@ package staticconfig import ( "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/config/internal/configbuilder" - "golang.org/x/tools/go/ssa" + "github.com/dogmatiq/enginekit/config/staticconfig/internal/ssax" ) func analyzeHandler[T configbuilder.HandlerBuilder]( @@ -14,15 +14,16 @@ func analyzeHandler[T configbuilder.HandlerBuilder]( build(func(b T) { b.UpdateFidelity(ctx.Fidelity) - inst, ok := ctx.Args[0].(*ssa.MakeInterface) - if !ok { + t := ssax.ConcreteType(ctx.Args[0]) + + if !t.IsPresent() { b.UpdateFidelity(config.Incomplete) return } analyzeEntity( ctx.context, - inst.X.Type(), + t.Get(), b, func(ctx *configurerCallContext[T]) { switch ctx.Method.Name() { diff --git a/config/staticconfig/internal/ssax/type.go b/config/staticconfig/internal/ssax/type.go new file mode 100644 index 00000000..792f9141 --- /dev/null +++ b/config/staticconfig/internal/ssax/type.go @@ -0,0 +1,122 @@ +package ssax + +import ( + "fmt" + "go/types" + + "github.com/dogmatiq/enginekit/internal/typename" + "github.com/dogmatiq/enginekit/optional" + "golang.org/x/tools/go/ssa" +) + +// Implements reports whether t implements i, regardless of whether it +// uses pointer or non-pointer method receivers. +// +// If ok is true, r is the receiver type that implements the interface, which +// may be either t or *t. +func Implements(t *ssa.Type, i *types.Interface) (r types.Type, ok bool) { + r = t.Type() + + if IsAbstract(r) { + return nil, false + } + + if types.Implements(r, i) { + return r, true + } + + r = types.NewPointer(r) + if types.Implements(r, i) { + return r, true + } + + return nil, false +} + +// IsAbstract returns true if t is abstract, either because it refers to an +// interface or because it is a generic type that has not been instantiated. +func IsAbstract(t types.Type) bool { + if types.IsInterface(t) { + return true + } + + // Check if the type is a generic type that has not been instantiated + // (meaning that it has no concrete values for its type parameters). + switch t := t.(type) { + case *types.Named: + return t.Origin() == t && t.TypeParams().Len() != 0 + case *types.Alias: + return t.Origin() == t && t.TypeParams().Len() != 0 + } + + return false +} + +// Package returns the package in which the elemental type of t is declared. +func Package(t types.Type) *types.Package { + switch t := t.(type) { + case *types.Named: + return t.Obj().Pkg() + case *types.Alias: + return t.Obj().Pkg() + case *types.Pointer: + return Package(t.Elem()) + default: + panic(fmt.Sprintf("cannot determine package for anonymous or built-in type %v", t)) + } +} + +// ConcreteType returns the concrete type of v, if it can be determined at +// compile-time. +func ConcreteType(v ssa.Value) optional.Optional[types.Type] { + t := v.Type() + + if !IsAbstract(t) { + return optional.Some(t) + } + + switch v := v.(type) { + case *ssa.Alloc: + case *ssa.BinOp: + case *ssa.Builtin: + case *ssa.Call: + case *ssa.ChangeInterface: + case *ssa.ChangeType: + case *ssa.Const: + // We made it past the IsAbstract() check so we know this is a constant + // nil value for an interface, and hence no type information is present. + return optional.None[types.Type]() + case *ssa.Convert: + case *ssa.Extract: + case *ssa.Field: + case *ssa.FieldAddr: + case *ssa.FreeVar: + case *ssa.Function: + case *ssa.Global: + case *ssa.Index: + case *ssa.IndexAddr: + case *ssa.Lookup: + case *ssa.MakeChan: + case *ssa.MakeClosure: + case *ssa.MakeInterface: + return ConcreteType(v.X) + case *ssa.MakeMap: + case *ssa.MakeSlice: + case *ssa.MultiConvert: + case *ssa.Next: + case *ssa.Parameter: + case *ssa.Phi: + case *ssa.Slice: + case *ssa.SliceToArrayPointer: + case *ssa.TypeAssert: + case *ssa.UnOp: + _ = v + + case *ssa.Range, *ssa.Select: + // These types implement ssa.Value, but they can not actually be used as + // expressions in Go. + return optional.None[types.Type]() + } + + panic(fmt.Sprintf("unhandled %T of type %s", v, typename.OfStatic(t))) +} diff --git a/config/staticconfig/type.go b/config/staticconfig/type.go deleted file mode 100644 index 142bb3c4..00000000 --- a/config/staticconfig/type.go +++ /dev/null @@ -1,74 +0,0 @@ -package staticconfig - -import ( - "fmt" - "go/types" -) - -// isAbstract returns true if t is abstract, either because it refers to an -// interface or because it is a generic type that has not been instantiated. -func isAbstract(t types.Type) bool { - if types.IsInterface(t) { - return true - } - - // Check if the type is a generic type that has not been instantiated - // (meaning that it has no concrete values for its type parameters). - switch t := t.(type) { - case *types.Named: - return t.Origin() == t && t.TypeParams().Len() != 0 - case *types.Alias: - return t.Origin() == t && t.TypeParams().Len() != 0 - } - - return false -} - -// analyzeType analyzes a type that was discovered within a package. -// -// 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 - // analyze! - return - } - - // The sequence of the if-blocks below is important as a type - // implements an interface only if the methods in the interface's - // method set have non-pointer receivers. Hence the implementation - // check for the "raw" (non-pointer) type is made first. - // - // A pointer to the type, on the other hand, implements the - // interface regardless of whether pointer receivers are used or - // not. - - if types.Implements(t, ctx.Dogma.Application) { - analyzeApplicationType(ctx, t) - return - } - - p := types.NewPointer(t) - if types.Implements(p, ctx.Dogma.Application) { - analyzeApplicationType(ctx, p) - return - } -} - -// packageOf returns the package in which t is declared. -// -// It panics if t is not a named type or a pointer to a named type. -func packageOf(t types.Type) *types.Package { - switch t := t.(type) { - case *types.Named: - return t.Obj().Pkg() - case *types.Alias: - return t.Obj().Pkg() - case *types.Pointer: - return packageOf(t.Elem()) - default: - panic(fmt.Sprintf("cannot determine package for anonymous or built-in type %v", t)) - } -} From 85a20c8403c763b047ef5deedf22a81b8b1ccf03 Mon Sep 17 00:00:00 2001 From: James Harris Date: Thu, 24 Oct 2024 09:28:52 +1000 Subject: [PATCH 28/38] Rename `WalkDown()` to `WalkBlock()`. --- config/staticconfig/internal/ssax/flow.go | 17 +++++++++++------ config/staticconfig/internal/ssax/value.go | 2 +- config/staticconfig/varargs.go | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/config/staticconfig/internal/ssax/flow.go b/config/staticconfig/internal/ssax/flow.go index 6eb81a79..41fbf6aa 100644 --- a/config/staticconfig/internal/ssax/flow.go +++ b/config/staticconfig/internal/ssax/flow.go @@ -8,18 +8,23 @@ import ( // 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 func(yield func(*ssa.BasicBlock) bool) { + if len(fn.Blocks) != 0 { + for b := range WalkBlock(fn.Blocks[0]) { + if !yield(b) { + return + } + } + } } - return WalkDown(fn.Blocks[0]) } -// WalkDown recursively yields b and all reachable successor blocks of b. +// WalkBlock 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 // that block that does not depend on a condition that is known to be false at // compile-time. -func WalkDown(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { +func WalkBlock(b *ssa.BasicBlock) iter.Seq[*ssa.BasicBlock] { return walk(b, DirectSuccessors) } @@ -59,7 +64,7 @@ func PathExists(from, to *ssa.BasicBlock) bool { panic("blocks are not in the same function") } - for b := range WalkDown(from) { + for b := range WalkBlock(from) { if b == to { return true } diff --git a/config/staticconfig/internal/ssax/value.go b/config/staticconfig/internal/ssax/value.go index 2923c974..6cdf4cdd 100644 --- a/config/staticconfig/internal/ssax/value.go +++ b/config/staticconfig/internal/ssax/value.go @@ -88,7 +88,7 @@ func staticValuesFromCall( outputs := make([]optional.Optional[ssa.Value], n) conflicting := make([]bool, n) - for b := range WalkDown(fn.Blocks[0]) { + for b := range WalkBlock(fn.Blocks[0]) { ret, ok := Terminator[*ssa.Return](b).TryGet() if !ok { continue diff --git a/config/staticconfig/varargs.go b/config/staticconfig/varargs.go index 72568f40..0f0ea06c 100644 --- a/config/staticconfig/varargs.go +++ b/config/staticconfig/varargs.go @@ -61,7 +61,7 @@ func resolveVariadic( return } - for b := range ssax.WalkDown(array.Block()) { + for b := range ssax.WalkBlock(array.Block()) { if !ssax.PathExists(b, inst.Block()) { continue } From cb4644c28da0310c1e1b5b02377c92001e357b68 Mon Sep 17 00:00:00 2001 From: James Harris Date: Fri, 1 Nov 2024 12:55:21 +1000 Subject: [PATCH 29/38] Update tests to work with new message formats. --- config/staticconfig/analyze_test.go | 12 +- config/staticconfig/application.go | 7 +- config/staticconfig/entity.go | 131 ++++++++++-------- config/staticconfig/handler.go | 50 +++---- config/staticconfig/route.go | 26 ++-- .../testdata/alias-for-generic.md | 2 +- .../conditional-excluded-by-const-expr.md | 4 +- .../conditional-included-by-const-expr.md | 2 +- .../testdata/conditional-present-in-method.md | 2 +- config/staticconfig/testdata/conditional.md | 2 +- .../testdata/handler-aggregate.md | 9 +- .../testdata/handler-integration.md | 9 +- .../staticconfig/testdata/handler-process.md | 11 +- .../testdata/handler-projection.md | 7 +- .../handler-with-unregistered-routes.md | 6 +- .../testdata/identity-from-const.md | 2 +- .../identity-from-non-const-static.md | 2 +- .../testdata/identity-from-non-const.md | 6 +- .../testdata/incomplete-entity.md | 3 +- config/staticconfig/testdata/indirect.md | 2 +- config/staticconfig/testdata/multiple-apps.md | 4 +- config/staticconfig/testdata/nil-handlers.md | 26 ++-- config/staticconfig/testdata/no-handlers.md | 2 +- config/staticconfig/varargs.go | 10 +- 24 files changed, 195 insertions(+), 142 deletions(-) diff --git a/config/staticconfig/analyze_test.go b/config/staticconfig/analyze_test.go index c0c53928..9ae685b9 100644 --- a/config/staticconfig/analyze_test.go +++ b/config/staticconfig/analyze_test.go @@ -93,18 +93,22 @@ func TestAnalyzer(t *testing.T) { } // Render the details of the application. - details := config.RenderDetails(app) + err := config.Validate(app) + desc := config.Description( + app, + config.WithValidationResult(err), + ) // Remove the random portion of the temporary directory name // so that the test output is deterministic. rel, _ := filepath.Rel(cwd, dir) - details = strings.ReplaceAll( - details, + desc = strings.ReplaceAll( + desc, "/"+rel+".", ".", ) - if _, err := io.WriteString(out, details); err != nil { + if _, err := io.WriteString(out, desc); err != nil { return err } } diff --git a/config/staticconfig/application.go b/config/staticconfig/application.go index 909899c7..984fb21e 100644 --- a/config/staticconfig/application.go +++ b/config/staticconfig/application.go @@ -3,6 +3,7 @@ package staticconfig import ( "go/types" + "github.com/dogmatiq/dogma" "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/config/internal/configbuilder" ) @@ -24,7 +25,9 @@ func analyzeApplicationType(ctx *context, t types.Type) { ctx.Analysis.Applications = append(ctx.Analysis.Applications, app) } -func analyzeApplicationConfigurerCall(ctx *configurerCallContext[*configbuilder.ApplicationBuilder]) { +func analyzeApplicationConfigurerCall( + ctx *configurerCallContext[*config.Application, dogma.Application, *configbuilder.ApplicationBuilder], +) { switch ctx.Method.Name() { case "RegisterAggregate": analyzeHandler(ctx, ctx.Builder.Aggregate, nil) @@ -35,6 +38,6 @@ func analyzeApplicationConfigurerCall(ctx *configurerCallContext[*configbuilder. case "RegisterProjection": analyzeHandler(ctx, ctx.Builder.Projection, analyzeProjectionConfigurerCall) default: - ctx.Builder.UpdateFidelity(config.Incomplete) + ctx.Builder.Partial() } } diff --git a/config/staticconfig/entity.go b/config/staticconfig/entity.go index 42967546..e02d449e 100644 --- a/config/staticconfig/entity.go +++ b/config/staticconfig/entity.go @@ -10,17 +10,21 @@ import ( "golang.org/x/tools/go/ssa" ) -type entityContext[T configbuilder.EntityBuilder] struct { +type entityContext[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], +] struct { *context EntityType types.Type - Builder T + Builder B ConfigureMethod *ssa.Function FunctionUnderAnalysis *ssa.Function ConfigurerParamIndices []int } -func (c *entityContext[T]) IsConfigurer(v ssa.Value) bool { +func (c *entityContext[T, E, B]) IsConfigurer(v ssa.Value) bool { for _, i := range c.ConfigurerParamIndices { if v == c.FunctionUnderAnalysis.Params[i] { return true @@ -29,57 +33,77 @@ func (c *entityContext[T]) IsConfigurer(v ssa.Value) bool { return false } -type configurerCallContext[T configbuilder.EntityBuilder] struct { - *entityContext[T] +type configurerCallContext[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], +] struct { + *entityContext[T, E, B] *ssa.CallCommon - Instruction ssa.CallInstruction - Fidelity config.Fidelity + Instruction ssa.CallInstruction + IsSpeculative bool } // configurerCallAnalyzer is a function that analyzes a call to a method on an // entity's configurer. -type configurerCallAnalyzer[T configbuilder.EntityBuilder] func(*configurerCallContext[T]) +type configurerCallAnalyzer[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], +] func(*configurerCallContext[T, E, B]) // 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]( +func analyzeEntity[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], +]( ctx *context, t types.Type, - builder T, - analyze configurerCallAnalyzer[T], + builder B, + analyze configurerCallAnalyzer[T, E, B], ) { - builder.SetSourceTypeName(typename.OfStatic(t)) + builder.TypeName(typename.OfStatic(t)) configure := ctx.LookupMethod(t, "Configure") + ectx := &entityContext[T, E, B]{ + context: ctx, + EntityType: t, + Builder: builder, + ConfigureMethod: configure, + FunctionUnderAnalysis: configure, + ConfigurerParamIndices: []int{1}, + } + + fn := func(ctx *configurerCallContext[T, E, B]) { + switch ctx.Method.Name() { + case "Identity": + analyzeIdentity(ctx) + default: + analyze(ctx) + } + } + 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) - } - }, + ectx, + fn, ) } // analyzeConfigurerCallsInFunc analyzes calls to methods on the configurer in // the function under analysis. -func analyzeConfigurerCallsInFunc[T configbuilder.EntityBuilder]( - ctx *entityContext[T], - analyze configurerCallAnalyzer[T], +func analyzeConfigurerCallsInFunc[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], +]( + ctx *entityContext[T, E, B], + analyze configurerCallAnalyzer[T, E, B], ) { for b := range ssax.WalkFunc(ctx.FunctionUnderAnalysis) { for _, inst := range b.Instrs { @@ -92,27 +116,24 @@ func analyzeConfigurerCallsInFunc[T configbuilder.EntityBuilder]( // analyzeConfigurerCallsInInstruction analyzes calls to methods on the // configurer in the given instruction. -func analyzeConfigurerCallsInInstruction[T configbuilder.EntityBuilder]( - ctx *entityContext[T], +func analyzeConfigurerCallsInInstruction[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], +]( + ctx *entityContext[T, E, B], inst ssa.CallInstruction, - analyze configurerCallAnalyzer[T], + analyze configurerCallAnalyzer[T, E, B], ) { 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]{ + analyze(&configurerCallContext[T, E, B]{ entityContext: ctx, CallCommon: com, Instruction: inst, - Fidelity: f, + IsSpeculative: !ssax.IsUnconditional(inst.Block()), }) - return } @@ -143,12 +164,12 @@ func analyzeConfigurerCallsInInstruction[T configbuilder.EntityBuilder]( // some other un-analyzable function. fn := com.StaticCallee() if fn == nil { - ctx.Builder.UpdateFidelity(config.Incomplete) + ctx.Builder.Partial() return } analyzeConfigurerCallsInFunc( - &entityContext[T]{ + &entityContext[T, E, B]{ context: ctx.context, EntityType: ctx.EntityType, Builder: ctx.Builder, @@ -160,24 +181,26 @@ func analyzeConfigurerCallsInInstruction[T configbuilder.EntityBuilder]( ) } -func analyzeIdentity[T configbuilder.EntityBuilder]( - ctx *configurerCallContext[T], +func analyzeIdentity[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], +]( + ctx *configurerCallContext[T, E, B], ) { ctx. Builder. Identity(func(b *configbuilder.IdentityBuilder) { - b.UpdateFidelity(ctx.Fidelity) + if ctx.IsSpeculative { + b.Speculative() + } if name, ok := ssax.AsString(ctx.Args[0]).TryGet(); ok { - b.SetName(name) - } else { - b.UpdateFidelity(config.Incomplete) + b.Name(name) } if key, ok := ssax.AsString(ctx.Args[1]).TryGet(); ok { - b.SetKey(key) - } else { - b.UpdateFidelity(config.Incomplete) + b.Key(key) } }) } diff --git a/config/staticconfig/handler.go b/config/staticconfig/handler.go index 33dd5924..e4b85683 100644 --- a/config/staticconfig/handler.go +++ b/config/staticconfig/handler.go @@ -1,23 +1,30 @@ package staticconfig import ( + "github.com/dogmatiq/dogma" "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/config/internal/configbuilder" "github.com/dogmatiq/enginekit/config/staticconfig/internal/ssax" ) -func analyzeHandler[T configbuilder.HandlerBuilder]( - ctx *configurerCallContext[*configbuilder.ApplicationBuilder], - build func(func(T)), - analyze configurerCallAnalyzer[T], +func analyzeHandler[ + T config.Handler, + H any, + B configbuilder.HandlerBuilder[T, H], +]( + ctx *configurerCallContext[*config.Application, dogma.Application, *configbuilder.ApplicationBuilder], + build func(func(B)), + analyze configurerCallAnalyzer[T, H, B], ) { - build(func(b T) { - b.UpdateFidelity(ctx.Fidelity) + build(func(b B) { + if ctx.IsSpeculative { + b.Speculative() + } t := ssax.ConcreteType(ctx.Args[0]) if !t.IsPresent() { - b.UpdateFidelity(config.Incomplete) + b.Partial() return } @@ -25,45 +32,40 @@ func analyzeHandler[T configbuilder.HandlerBuilder]( ctx.context, t.Get(), b, - func(ctx *configurerCallContext[T]) { + func(ctx *configurerCallContext[T, H, B]) { switch ctx.Method.Name() { case "Routes": analyzeRoutes(ctx) case "Disable": - // 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) + ctx.Builder.Disabled( + func(b *configbuilder.FlagBuilder[config.Disabled]) { + if ctx.IsSpeculative { + b.Speculative() + } + b.Value(true) + }, + ) default: if analyze == nil { - ctx.Builder.UpdateFidelity(config.Incomplete) + ctx.Builder.Partial() } 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) - } }) } func analyzeProjectionConfigurerCall( - ctx *configurerCallContext[*configbuilder.ProjectionBuilder], + ctx *configurerCallContext[*config.Projection, dogma.ProjectionMessageHandler, *configbuilder.ProjectionBuilder], ) { switch ctx.Method.Name() { case "DeliveryPolicy": panic("not implemented") // TODO default: - ctx.Builder.UpdateFidelity(config.Incomplete) + ctx.Builder.Partial() } } diff --git a/config/staticconfig/route.go b/config/staticconfig/route.go index 50fb3cf5..54d06885 100644 --- a/config/staticconfig/route.go +++ b/config/staticconfig/route.go @@ -7,12 +7,18 @@ import ( "golang.org/x/tools/go/ssa" ) -func analyzeRoutes[T configbuilder.HandlerBuilder]( - ctx *configurerCallContext[T], +func analyzeRoutes[ + T config.Handler, + H any, + B configbuilder.HandlerBuilder[T, H], +]( + ctx *configurerCallContext[T, H, B], ) { for r := range resolveVariadic(ctx.Builder, ctx.Instruction) { ctx.Builder.Route(func(b *configbuilder.RouteBuilder) { - b.UpdateFidelity(ctx.Fidelity) // TODO: is this correct? + if ctx.IsSpeculative { + b.Speculative() // TODO: is this correct? + } analyzeRoute(ctx.context, b, r) }) } @@ -25,7 +31,7 @@ func analyzeRoute( ) { call, ok := findRouteCall(ctx, r) if !ok { - b.UpdateFidelity(config.Incomplete) + b.Partial() return } @@ -33,18 +39,18 @@ func analyzeRoute( switch fn.Object() { case ctx.Dogma.HandlesCommand: - b.SetRouteType(config.HandlesCommandRouteType) + b.RouteType(config.HandlesCommandRouteType) case ctx.Dogma.HandlesEvent: - b.SetRouteType(config.HandlesEventRouteType) + b.RouteType(config.HandlesEventRouteType) case ctx.Dogma.ExecutesCommand: - b.SetRouteType(config.ExecutesCommandRouteType) + b.RouteType(config.ExecutesCommandRouteType) case ctx.Dogma.RecordsEvent: - b.SetRouteType(config.RecordsEventRouteType) + b.RouteType(config.RecordsEventRouteType) case ctx.Dogma.SchedulesTimeout: - b.SetRouteType(config.SchedulesTimeoutRouteType) + b.RouteType(config.SchedulesTimeoutRouteType) } - b.SetMessageTypeName(typename.OfStatic(fn.TypeArgs()[0])) + b.MessageTypeName(typename.OfStatic(fn.TypeArgs()[0])) } func findRouteCall( diff --git a/config/staticconfig/testdata/alias-for-generic.md b/config/staticconfig/testdata/alias-for-generic.md index dc02eb0e..3bcba295 100644 --- a/config/staticconfig/testdata/alias-for-generic.md +++ b/config/staticconfig/testdata/alias-for-generic.md @@ -4,7 +4,7 @@ This test ensures that the static analyzer finds an instantiated generic type that implements the `dogma.Application` interface. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.Alias (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.Alias (value unavailable) - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 ``` diff --git a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md index 3b16d7c7..45f9f944 100644 --- a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md @@ -4,8 +4,8 @@ This test verifies that the static analyzer excludes information about an entity's identity if it appears in an unreachable branch. ```au:output au:group=matrix -invalid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - - no identity is configured +invalid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - no identity ``` ## After conditional return diff --git a/config/staticconfig/testdata/conditional-included-by-const-expr.md b/config/staticconfig/testdata/conditional-included-by-const-expr.md index a100a6a5..06211e7e 100644 --- a/config/staticconfig/testdata/conditional-included-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-included-by-const-expr.md @@ -5,7 +5,7 @@ entity's identity if it appears in a conditional block that is always executed. Note that the identity is not marked as "speculative". ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/conditional-present-in-method.md b/config/staticconfig/testdata/conditional-present-in-method.md index 620a4876..f3ffeacc 100644 --- a/config/staticconfig/testdata/conditional-present-in-method.md +++ b/config/staticconfig/testdata/conditional-present-in-method.md @@ -5,7 +5,7 @@ entity's identity even if it appears after (but not within) a conditional statement. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/conditional.md b/config/staticconfig/testdata/conditional.md index f2f88534..dd0ebac2 100644 --- a/config/staticconfig/testdata/conditional.md +++ b/config/staticconfig/testdata/conditional.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer includes information about an entity's identity when it is defined within a conditional statement. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid speculative identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/handler-aggregate.md b/config/staticconfig/testdata/handler-aggregate.md index d84ecb35..0faa880a 100644 --- a/config/staticconfig/testdata/handler-aggregate.md +++ b/config/staticconfig/testdata/handler-aggregate.md @@ -4,12 +4,13 @@ This test ensures that the static analyzer supports all aspects of configuring an aggregate handler. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 - - disabled valid aggregate github.com/dogmatiq/enginekit/config/staticconfig.Aggregate (runtime type unavailable) + - valid aggregate github.com/dogmatiq/enginekit/config/staticconfig.Aggregate (value unavailable) - valid identity aggregate/916e5e95-70c4-4823-9de2-0f7389d18b4f - - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) - - valid records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid disabled flag, set to true ``` ```go au:input au:group=matrix diff --git a/config/staticconfig/testdata/handler-integration.md b/config/staticconfig/testdata/handler-integration.md index 8786aa7b..0baa37a3 100644 --- a/config/staticconfig/testdata/handler-integration.md +++ b/config/staticconfig/testdata/handler-integration.md @@ -4,12 +4,13 @@ This test ensures that the static analyzer supports all aspects of configuring an integration handler. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 - - disabled valid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (runtime type unavailable) + - valid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (value unavailable) - valid identity integration/b92431e6-3a7d-4235-a76f-541622c487ee - - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) - - valid records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid disabled flag, set to true ``` ```go au:input au:group=matrix diff --git a/config/staticconfig/testdata/handler-process.md b/config/staticconfig/testdata/handler-process.md index 4e9f03c3..adcd082b 100644 --- a/config/staticconfig/testdata/handler-process.md +++ b/config/staticconfig/testdata/handler-process.md @@ -4,13 +4,14 @@ This test ensures that the static analyzer supports all aspects of configuring a process handler. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 - - disabled valid process github.com/dogmatiq/enginekit/config/staticconfig.Process (runtime type unavailable) + - valid process github.com/dogmatiq/enginekit/config/staticconfig.Process (value unavailable) - valid identity process/4ff1b1c1-5c64-49bc-a547-c13f5bafad7d - - valid handles-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) - - valid executes-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) - - valid schedules-timeout route for github.com/dogmatiq/enginekit/enginetest/stubs.TimeoutStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) + - valid handles-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid executes-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid schedules-timeout route for github.com/dogmatiq/enginekit/enginetest/stubs.TimeoutStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid disabled flag, set to true ``` ```go au:input au:group=matrix diff --git a/config/staticconfig/testdata/handler-projection.md b/config/staticconfig/testdata/handler-projection.md index 6124cd53..49025093 100644 --- a/config/staticconfig/testdata/handler-projection.md +++ b/config/staticconfig/testdata/handler-projection.md @@ -4,11 +4,12 @@ This test ensures that the static analyzer supports all aspects of configuring a projection handler. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 - - disabled valid projection github.com/dogmatiq/enginekit/config/staticconfig.Projection (runtime type unavailable) + - valid projection github.com/dogmatiq/enginekit/config/staticconfig.Projection (value unavailable) - valid identity projection/238d7498-515b-44b5-b6a8-914a08762ecc - - valid handles-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (runtime type unavailable) + - valid handles-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid disabled flag, set to true ``` ```go au:input au:group=matrix diff --git a/config/staticconfig/testdata/handler-with-unregistered-routes.md b/config/staticconfig/testdata/handler-with-unregistered-routes.md index eb502080..8f87c457 100644 --- a/config/staticconfig/testdata/handler-with-unregistered-routes.md +++ b/config/staticconfig/testdata/handler-with-unregistered-routes.md @@ -5,10 +5,10 @@ routes that are constructed but never passed to the configurer's `Routes()` method. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/f2c08525-623e-4c76-851c-3172953269e3 - - invalid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (runtime type unavailable) - - no "handles-command" routes are configured + - invalid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (value unavailable) + - no handles-command routes - valid identity handler/ac391765-da58-4e7c-a478-e4725eb2b0e9 ``` diff --git a/config/staticconfig/testdata/identity-from-const.md b/config/staticconfig/testdata/identity-from-const.md index 06e91920..e4255e38 100644 --- a/config/staticconfig/testdata/identity-from-const.md +++ b/config/staticconfig/testdata/identity-from-const.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer can discover the values within an entity's identity when they are sourced from non-literal constant expressions. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/d0de39ba-aaaf-43fd-8e8f-7c4e3be309ec ``` diff --git a/config/staticconfig/testdata/identity-from-non-const-static.md b/config/staticconfig/testdata/identity-from-non-const-static.md index 480cdb51..84c2648a 100644 --- a/config/staticconfig/testdata/identity-from-non-const-static.md +++ b/config/staticconfig/testdata/identity-from-non-const-static.md @@ -4,7 +4,7 @@ This test verifies that the static analyzer includes an entity's identity, even if it cannot determine the values used. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/a0a0edb7-ce45-4eb4-940c-0f77459ae2a0 ``` diff --git a/config/staticconfig/testdata/identity-from-non-const.md b/config/staticconfig/testdata/identity-from-non-const.md index e6a9ec2a..3a2f80f5 100644 --- a/config/staticconfig/testdata/identity-from-non-const.md +++ b/config/staticconfig/testdata/identity-from-non-const.md @@ -4,8 +4,10 @@ This test verifies that the static analyzer includes an entity's identity, even if it cannot determine the values used. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) - - incomplete identity ?/? +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - incomplete identity + - name is unavailable + - key is unavailable ``` ## Variables diff --git a/config/staticconfig/testdata/incomplete-entity.md b/config/staticconfig/testdata/incomplete-entity.md index e17d8ec8..116421e8 100644 --- a/config/staticconfig/testdata/incomplete-entity.md +++ b/config/staticconfig/testdata/incomplete-entity.md @@ -5,7 +5,8 @@ incomplete if the `Configure()` method calls into code that is unable to be analyzed. ```au:output au:group=matrix -incomplete application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +incomplete application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - could not evaluate entire configuration - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/indirect.md b/config/staticconfig/testdata/indirect.md index b77a3ef2..2425f3b0 100644 --- a/config/staticconfig/testdata/indirect.md +++ b/config/staticconfig/testdata/indirect.md @@ -5,7 +5,7 @@ This test verifies that the static analyzer traverses into code called from the `ApplicationConfigurer` interface. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/multiple-apps.md b/config/staticconfig/testdata/multiple-apps.md index 0ab23033..792cf40b 100644 --- a/config/staticconfig/testdata/multiple-apps.md +++ b/config/staticconfig/testdata/multiple-apps.md @@ -4,10 +4,10 @@ This test verifies that the static analyzer discovers multiple Dogma application types defined within the same Go package. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.One (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.One (value unavailable) - valid identity one/4fec74a1-6ed4-46f4-8417-01e0910be8f1 -valid application github.com/dogmatiq/enginekit/config/staticconfig.Two (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.Two (value unavailable) - valid identity two/6e97d403-3cb8-4a59-a7ec-74e8e219a7bc ``` diff --git a/config/staticconfig/testdata/nil-handlers.md b/config/staticconfig/testdata/nil-handlers.md index edeb1bb6..f6172e30 100644 --- a/config/staticconfig/testdata/nil-handlers.md +++ b/config/staticconfig/testdata/nil-handlers.md @@ -4,22 +4,26 @@ This test ensures that the static analyzer includes basic information about the presence of `nil` handlers. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 - incomplete aggregate - - no identity is configured - - no "handles-command" routes are configured - - no "records-event" routes are configured + - could not evaluate entire configuration + - no identity + - no handles-command routes + - no records-event routes - incomplete process - - no identity is configured - - no "handles-event" routes are configured - - no "executes-command" routes are configured + - could not evaluate entire configuration + - no identity + - no handles-event routes + - no executes-command routes - incomplete integration - - no identity is configured - - no "handles-command" routes are configured + - could not evaluate entire configuration + - no identity + - no handles-command routes - incomplete projection - - no identity is configured - - no "handles-event" routes are configured + - could not evaluate entire configuration + - no identity + - no handles-event routes ``` ```go au:input au:group=matrix diff --git a/config/staticconfig/testdata/no-handlers.md b/config/staticconfig/testdata/no-handlers.md index e129ba0d..1fb8888d 100644 --- a/config/staticconfig/testdata/no-handlers.md +++ b/config/staticconfig/testdata/no-handlers.md @@ -4,7 +4,7 @@ This test ensures that the static analyzer includes Dogma applications that have no handlers. ```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (runtime type unavailable) +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 ``` diff --git a/config/staticconfig/varargs.go b/config/staticconfig/varargs.go index 0f0ea06c..0bf7a550 100644 --- a/config/staticconfig/varargs.go +++ b/config/staticconfig/varargs.go @@ -43,8 +43,12 @@ func isIndexOfArray( return 0, false } -func resolveVariadic( - b configbuilder.EntityBuilder, +func resolveVariadic[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], +]( + b B, inst ssa.CallInstruction, ) iter.Seq[ssa.Value] { return func(yield func(ssa.Value) bool) { @@ -57,7 +61,7 @@ func resolveVariadic( array, ok := findAllocation(variadics) if !ok { - b.UpdateFidelity(config.Incomplete) + b.Partial() return } From 853be79a80cd64bbf73aa5ce17de2aedfc0b5749 Mon Sep 17 00:00:00 2001 From: James Harris Date: Tue, 5 Nov 2024 06:54:53 +1000 Subject: [PATCH 30/38] Genericize static analysis of variadic arguments. --- config/staticconfig/internal/ssax/value.go | 55 ++++----- config/staticconfig/route.go | 13 +-- config/staticconfig/varargs.go | 129 +++++++++++---------- optional/container.go | 9 ++ 4 files changed, 111 insertions(+), 95 deletions(-) diff --git a/config/staticconfig/internal/ssax/value.go b/config/staticconfig/internal/ssax/value.go index 6cdf4cdd..eb62e3c1 100644 --- a/config/staticconfig/internal/ssax/value.go +++ b/config/staticconfig/internal/ssax/value.go @@ -12,41 +12,46 @@ import ( // // If v cannot be resolved to a single value, it returns an empty optional. func StaticValue(v ssa.Value) optional.Optional[ssa.Value] { - switch v := v.(type) { - case *ssa.Const: - return optional.Some[ssa.Value](v) - - case ssa.Instruction: - values := staticValuesFromInstruction(v) - if len(values) > 1 { - panic("did not expect multiple values") - } + values := staticValues(v) + if len(values) > 1 { + panic("did not expect multiple values") + } - if len(values) == 1 { - return values[0] - } + if len(values) == 1 { + return values[0] } - // TODO(jmalloc): This implementation is incomplete. return optional.None[ssa.Value]() } -// staticValuesFromInstruction returns the static value(s) that result from -// evaluating the given instruction. +// staticValues returns the static value(s) that result from evaluating the +// given node. // // If an individual value within the expression cannot be resolved to a singular -// static value, it is represented as a nil value in the returned slice. +// static value, it is represented as an empty optional in the returned slice. // // It returns an empty slice if the expression itself cannot be resolved. -func staticValuesFromInstruction(inst ssa.Instruction) []optional.Optional[ssa.Value] { - switch inst := inst.(type) { +func staticValues(v ssa.Value) []optional.Optional[ssa.Value] { + switch v := v.(type) { + case *ssa.Const: + return optional.Slice[ssa.Value](v) + case *ssa.Call: - return staticValuesFromCall(inst.Common()) + return staticValuesFromCall(v.Common()) case *ssa.Extract: - if expr, ok := inst.Tuple.(ssa.Instruction); ok { - values := staticValuesFromInstruction(expr) - return values[inst.Index : inst.Index+1] + values := staticValues(v.Tuple) + if len(values) <= v.Index { + return nil + } + return values[v.Index : v.Index+1] + + case *ssa.MakeInterface: + return staticValues(v.X) + + case *ssa.UnOp: + if v.Op == token.MUL { // pointer de-reference + return staticValues(v.X) } } @@ -58,13 +63,11 @@ func staticValuesFromInstruction(inst ssa.Instruction) []optional.Optional[ssa.V // a call to a function. // // If an individual value within the expression cannot be resolved to a singular -// static value, it is represented as a nil value in the returned slice. +// static value, it is represented as an empty value in the returned slice. // // It returns an empty slice if the function itself cannot be resolved. For // example, if it is a dynamic call to an interface method. -func staticValuesFromCall( - call *ssa.CallCommon, -) []optional.Optional[ssa.Value] { +func staticValuesFromCall(call *ssa.CallCommon) []optional.Optional[ssa.Value] { // TODO: we could use StaticValue or some variant thereof to resolve the // callee in more cases. fn := call.StaticCallee() diff --git a/config/staticconfig/route.go b/config/staticconfig/route.go index 54d06885..ca0a6c50 100644 --- a/config/staticconfig/route.go +++ b/config/staticconfig/route.go @@ -14,14 +14,11 @@ func analyzeRoutes[ ]( ctx *configurerCallContext[T, H, B], ) { - for r := range resolveVariadic(ctx.Builder, ctx.Instruction) { - ctx.Builder.Route(func(b *configbuilder.RouteBuilder) { - if ctx.IsSpeculative { - b.Speculative() // TODO: is this correct? - } - analyzeRoute(ctx.context, b, r) - }) - } + analyzeVariadicArguments( + ctx, + ctx.Builder.Route, + analyzeRoute, + ) } func analyzeRoute( diff --git a/config/staticconfig/varargs.go b/config/staticconfig/varargs.go index 0bf7a550..219889b9 100644 --- a/config/staticconfig/varargs.go +++ b/config/staticconfig/varargs.go @@ -1,85 +1,92 @@ package staticconfig import ( - "go/token" - "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" ) -func findAllocation(v ssa.Value) (*ssa.Alloc, bool) { - switch v := v.(type) { - case *ssa.Alloc: - return v, true - - case *ssa.Slice: - return findAllocation(v.X) - - case *ssa.UnOp: - if v.Op == token.MUL { // pointer de-reference - return findAllocation(v.X) - } - return nil, false - - default: - return nil, false - } -} - -func isIndexOfArray( - array *ssa.Alloc, - v ssa.Value, -) (int, bool) { - switch v := v.(type) { - case *ssa.IndexAddr: - if v.X != array { - return 0, false - } - return ssax.AsInt(v.Index).TryGet() - } - return 0, false -} - -func resolveVariadic[ +func analyzeVariadicArguments[ T config.Entity, E any, B configbuilder.EntityBuilder[T, E], + TChild config.Component, + BChild configbuilder.ComponentBuilder[TChild], ]( - b B, - inst ssa.CallInstruction, -) iter.Seq[ssa.Value] { - return func(yield func(ssa.Value) bool) { - call := inst.Common() + ctx *configurerCallContext[T, E, B], + child func(func(BChild)), + analyze func(*context, BChild, ssa.Value), +) { + // The variadic slice parameter is always the last argument. + varargs := ctx.Args[len(ctx.Args)-1] - variadics := call.Args[len(call.Args)-1] - if ssax.IsZeroValue(variadics) { - return - } + if ssax.IsZeroValue(varargs) { + return + } + + array, ok := findSliceArrayAllocation(varargs) + if !ok { + ctx.Builder.Partial() + return + } - array, ok := findAllocation(variadics) - if !ok { - b.Partial() - return + buildersByIndex := map[int][]BChild{} + + for block := range ssax.WalkBlock(array.Block()) { + // If there's no path from this block to the call instruction, we can + // safely ignore it, even if it modifies the underlying array. + if !ssax.PathExists(block, ctx.Instruction.Block()) { + continue } - for b := range ssax.WalkBlock(array.Block()) { - if !ssax.PathExists(b, inst.Block()) { - continue - } + for inst := range ssax.InstructionsBefore(block, ctx.Instruction) { + switch inst := inst.(type) { + case *ssa.Store: + if addr, ok := inst.Addr.(*ssa.IndexAddr); ok && addr.X == array { + child(func(b BChild) { + if index, ok := ssax.AsInt(addr.Index).TryGet(); ok { + // If there are multiple writes to the same index, + // we mark them all as speculative. + // + // TODO: Could we handle this more intelligently by + // using the value of the store instruction closest + // to the call instruction? + conflicting := buildersByIndex[index] + if len(conflicting) == 1 { + conflicting[0].Speculative() + } + if len(conflicting) != 0 { + b.Speculative() + } + buildersByIndex[index] = append(conflicting, b) + } else { + // If we can't resolve the index we assume the child + // is speculative because we can't tell if it is + // ever overwritten with a different value. + b.Speculative() + } - for inst := range ssax.InstructionsBefore(b, inst) { - switch inst := inst.(type) { - case *ssa.Store: - if _, ok := isIndexOfArray(array, inst.Addr); ok { - if !yield(inst.Val) { - return + if ctx.IsSpeculative { + b.Speculative() } - } + + analyze(ctx.context, b, inst.Val) + }) } } } } } + +// findSliceArrayAllocation returns the underlying array allocation of a slice. +func findSliceArrayAllocation(v ssa.Value) (*ssa.Alloc, bool) { + switch v := v.(type) { + case *ssa.Alloc: + return v, true + case *ssa.Slice: + return findSliceArrayAllocation(v.X) + default: + return nil, false + } +} diff --git a/optional/container.go b/optional/container.go index 90b2fffc..ab5abc4b 100644 --- a/optional/container.go +++ b/optional/container.go @@ -32,3 +32,12 @@ func Key[K comparable, V any, M ~map[K]V](m M, k K) Optional[V] { } return None[V]() } + +// Slice returns a slice of Optional[T] values. +func Slice[T any](elems ...T) []Optional[T] { + slice := make([]Optional[T], len(elems)) + for i, elem := range elems { + slice[i] = Some(elem) + } + return slice +} From 0a6c2ffe8383f6d1598e82394732324ecb30097f Mon Sep 17 00:00:00 2001 From: James Harris Date: Wed, 6 Nov 2024 08:07:48 +1000 Subject: [PATCH 31/38] Record the reasons that a configuration is partially loaded. --- config/aggregate_test.go | 2 +- config/application_test.go | 2 +- config/component.go | 42 +++-- config/describe.go | 2 +- config/entity_test.go | 4 +- config/handler_test.go | 4 +- config/identity_test.go | 4 +- config/integration_test.go | 2 +- config/internal/configbuilder/aggregate.go | 6 +- config/internal/configbuilder/application.go | 6 +- config/internal/configbuilder/builder.go | 10 +- config/internal/configbuilder/flag.go | 6 +- config/internal/configbuilder/identity.go | 6 +- config/internal/configbuilder/integration.go | 6 +- config/internal/configbuilder/process.go | 6 +- config/internal/configbuilder/projection.go | 6 +- .../configbuilder/projectiondeliverypolicy.go | 6 +- config/internal/configbuilder/route.go | 5 +- config/process_test.go | 2 +- config/projection_test.go | 6 +- config/runtimeconfig/aggregate.go | 2 +- config/runtimeconfig/aggregate_test.go | 2 +- config/runtimeconfig/application.go | 2 +- config/runtimeconfig/application_test.go | 2 +- config/runtimeconfig/integration.go | 2 +- config/runtimeconfig/integration_test.go | 2 +- config/runtimeconfig/process.go | 2 +- config/runtimeconfig/process_test.go | 2 +- config/runtimeconfig/projection.go | 2 +- config/runtimeconfig/projection_test.go | 2 +- config/staticconfig/application.go | 2 +- config/staticconfig/entity.go | 65 +++++-- config/staticconfig/handler.go | 14 +- config/staticconfig/route.go | 75 ++++----- .../conditional-routes-in-handlers.md | 67 -------- .../testdata/handler-with-appended-routes.md | 44 +++++ .../handler-with-conditional-routes.md | 143 ++++++++++++++++ .../testdata/incomplete-entity.md | 2 +- config/staticconfig/testdata/nil-handlers.md | 8 +- config/staticconfig/varargs.go | 159 +++++++++++------- config/validate.go | 6 +- 41 files changed, 483 insertions(+), 255 deletions(-) delete mode 100644 config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md create mode 100644 config/staticconfig/testdata/handler-with-appended-routes.md create mode 100644 config/staticconfig/testdata/handler-with-conditional-routes.md diff --git a/config/aggregate_test.go b/config/aggregate_test.go index 43b7d03f..ded6476e 100644 --- a/config/aggregate_test.go +++ b/config/aggregate_test.go @@ -105,7 +105,7 @@ func TestAggregate(t *testing.T) { Name: "nil aggregate", Error: multiline( `aggregate is invalid:`, - ` - could not evaluate entire configuration`, + ` - could not evaluate entire configuration: handler is nil`, ` - no identity`, ` - no handles-command routes`, ` - no records-event routes`, diff --git a/config/application_test.go b/config/application_test.go index d10574ab..c369d526 100644 --- a/config/application_test.go +++ b/config/application_test.go @@ -76,7 +76,7 @@ func TestApplication(t *testing.T) { Name: "nil application", Error: multiline( `application is invalid:`, - ` - could not evaluate entire configuration`, + ` - could not evaluate entire configuration: application is nil`, ` - no identity`, ), Component: runtimeconfig.FromApplication(nil), diff --git a/config/component.go b/config/component.go index b985b6b5..f59a53a6 100644 --- a/config/component.go +++ b/config/component.go @@ -2,6 +2,9 @@ package config import ( "fmt" + "strings" + + "github.com/dogmatiq/enginekit/config/internal/renderer" ) // Component is the "top-level" interface for the individual elements that form @@ -24,12 +27,12 @@ type ComponentCommon struct { // not be evaluated at configuration time. IsSpeculative bool - // IsPartial indicates that the configuration could not be loaded in its - // entirety. The configuration may be valid, but cannot be safely used to - // execute an application. + // IsPartialReasons is a list of reasons that the configuration could not be + // loaded in its entirety. The configuration may be valid, but cannot be + // safely used to execute an application. // - // A value of false does not imply a complete configuration. - IsPartial bool + // An empty slice does not imply a complete or valid configuration. + IsPartialReasons []string } // ComponentProperties returns the properties common to all [Component] types. @@ -40,8 +43,8 @@ func (p *ComponentCommon) ComponentProperties() *ComponentCommon { func validateComponent(ctx *validateContext) { p := ctx.Component.ComponentProperties() - if p.IsPartial { - ctx.Invalid(PartialConfigurationError{}) + if len(p.IsPartialReasons) != 0 { + ctx.Invalid(PartialConfigurationError{p.IsPartialReasons}) } if ctx.Options.ForExecution && p.IsSpeculative { @@ -63,16 +66,35 @@ func (e ConfigurationUnavailableError) Error() string { // PartialConfigurationError indicates that a [Component]'s configuration could // not be loaded in its entirety. -type PartialConfigurationError struct{} +type PartialConfigurationError struct { + Reasons []string +} func (e PartialConfigurationError) Error() string { - return "could not evaluate entire configuration" + w := &strings.Builder{} + r := &renderer.Renderer{Target: w} + + r.Print("could not evaluate entire configuration:") + + if len(e.Reasons) == 1 { + r.Print(" ", e.Reasons[0]) + } else if len(e.Reasons) > 1 { + for _, reason := range e.Reasons { + r.Print("\n") + r.StartChild() + r.Print(reason) + r.EndChild() + } + } + + return w.String() } // SpeculativeConfigurationError indicates that a [Component]'s inclusion in the // configuration is subject to some condition that could not be evaluated at the // time the configuration was built. -type SpeculativeConfigurationError struct{} +type SpeculativeConfigurationError struct { +} func (e SpeculativeConfigurationError) Error() string { return "conditions for the component's inclusion in the configuration could not be evaluated" diff --git a/config/describe.go b/config/describe.go index d9e86a83..4829ebb8 100644 --- a/config/describe.go +++ b/config/describe.go @@ -85,7 +85,7 @@ func hasError[T error](ctx *describeContext) bool { func (ctx *describeContext) DescribeFidelity() { p := ctx.Component.ComponentProperties() - if p.IsPartial || hasError[PartialConfigurationError](ctx) || hasError[ConfigurationUnavailableError](ctx) { + if len(p.IsPartialReasons) != 0 || hasError[PartialConfigurationError](ctx) || hasError[ConfigurationUnavailableError](ctx) { ctx.Print("incomplete ") } else if !ctx.options.ValidationResult.IsPresent() { ctx.Print("unvalidated ") diff --git a/config/entity_test.go b/config/entity_test.go index 30f668a2..740053a0 100644 --- a/config/entity_test.go +++ b/config/entity_test.go @@ -53,7 +53,7 @@ func testEntity[ t.Run("it panics if the entity is partially configured", func(t *testing.T) { entity := build( func(b B) { - b.Partial() + b.Partial("") b.Identity( func(b *configbuilder.IdentityBuilder) { b.Name("name") @@ -65,7 +65,7 @@ func testEntity[ test.ExpectPanic( t, - "could not evaluate entire configuration", + "could not evaluate entire configuration: ", func() { entity.Identity() }, diff --git a/config/handler_test.go b/config/handler_test.go index 1f2951a1..825059cd 100644 --- a/config/handler_test.go +++ b/config/handler_test.go @@ -65,14 +65,14 @@ func testHandler[ handler := build(func(b B) { b.Disabled( func(b *configbuilder.FlagBuilder[Disabled]) { - b.Partial() + b.Partial("") }, ) }) test.ExpectPanic( t, - `flag:disabled is invalid: could not evaluate entire configuration`, + `flag:disabled is invalid: could not evaluate entire configuration: `, func() { handler.IsDisabled() }, diff --git a/config/identity_test.go b/config/identity_test.go index 8cd4e603..46a83e66 100644 --- a/config/identity_test.go +++ b/config/identity_test.go @@ -63,14 +63,14 @@ func TestIdentity(t *testing.T) { }, { Name: "partial", - Error: `identity:name/e6b691dd-731c-4c14-8e1c-1622381202dc is invalid: could not evaluate entire configuration`, + Error: `identity:name/e6b691dd-731c-4c14-8e1c-1622381202dc is invalid: could not evaluate entire configuration: `, Component: &Identity{ // It's possibly non-sensical to have an identity that contains // both it's name and key be considered incomplete, but this // allows us to represent a case where the name and key are // build dynamically and we don't have the _entire_ string. ComponentCommon: ComponentCommon{ - IsPartial: true, + IsPartialReasons: []string{""}, }, Name: optional.Some("name"), Key: optional.Some("e6b691dd-731c-4c14-8e1c-1622381202dc"), diff --git a/config/integration_test.go b/config/integration_test.go index f7faa379..5039ccd0 100644 --- a/config/integration_test.go +++ b/config/integration_test.go @@ -92,7 +92,7 @@ func TestIntegration(t *testing.T) { Name: "nil integration", Error: multiline( `integration is invalid:`, - ` - could not evaluate entire configuration`, + ` - could not evaluate entire configuration: handler is nil`, ` - no identity`, ` - no handles-command routes`, ), diff --git a/config/internal/configbuilder/aggregate.go b/config/internal/configbuilder/aggregate.go index 2be1fd8d..374e024f 100644 --- a/config/internal/configbuilder/aggregate.go +++ b/config/internal/configbuilder/aggregate.go @@ -1,6 +1,8 @@ package configbuilder import ( + "fmt" + "github.com/dogmatiq/dogma" "github.com/dogmatiq/enginekit/config" ) @@ -47,8 +49,8 @@ func (b *AggregateBuilder) Disabled(fn func(*FlagBuilder[config.Disabled])) { } // Partial marks the compomnent as partially configured. -func (b *AggregateBuilder) Partial() { - b.target.IsPartial = true +func (b *AggregateBuilder) Partial(format string, args ...any) { + b.target.IsPartialReasons = append(b.target.IsPartialReasons, fmt.Sprintf(format, args...)) } // Speculative marks the component as speculative. diff --git a/config/internal/configbuilder/application.go b/config/internal/configbuilder/application.go index 07030957..b18107b1 100644 --- a/config/internal/configbuilder/application.go +++ b/config/internal/configbuilder/application.go @@ -1,6 +1,8 @@ package configbuilder import ( + "fmt" + "github.com/dogmatiq/dogma" "github.com/dogmatiq/enginekit/config" ) @@ -59,8 +61,8 @@ func (b *ApplicationBuilder) Projection(fn func(*ProjectionBuilder)) { } // Partial marks the compomnent as partially configured. -func (b *ApplicationBuilder) Partial() { - b.target.IsPartial = true +func (b *ApplicationBuilder) Partial(format string, args ...any) { + b.target.IsPartialReasons = append(b.target.IsPartialReasons, fmt.Sprintf(format, args...)) } // Speculative marks the component as speculative. diff --git a/config/internal/configbuilder/builder.go b/config/internal/configbuilder/builder.go index 3f9a0dc3..b72eb1e1 100644 --- a/config/internal/configbuilder/builder.go +++ b/config/internal/configbuilder/builder.go @@ -7,10 +7,16 @@ import ( "github.com/dogmatiq/enginekit/optional" ) +// UntypedComponentBuilder is the interface for builders of some unknown +// [config.Component] type. +type UntypedComponentBuilder interface { + Partial(format string, args ...any) + Speculative() +} + // ComponentBuilder an interface for builders that produce a [config.Component]. type ComponentBuilder[T config.Component] interface { - Partial() - Speculative() + UntypedComponentBuilder Done() T } diff --git a/config/internal/configbuilder/flag.go b/config/internal/configbuilder/flag.go index 47e6468c..b2d7efbf 100644 --- a/config/internal/configbuilder/flag.go +++ b/config/internal/configbuilder/flag.go @@ -1,6 +1,8 @@ package configbuilder import ( + "fmt" + "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/optional" ) @@ -23,8 +25,8 @@ func (b *FlagBuilder[S]) Value(v bool) { } // Partial marks the compomnent as partially configured. -func (b *FlagBuilder[S]) Partial() { - b.target.IsPartial = true +func (b *FlagBuilder[S]) Partial(format string, args ...any) { + b.target.IsPartialReasons = append(b.target.IsPartialReasons, fmt.Sprintf(format, args...)) } // Speculative marks the component as speculative. diff --git a/config/internal/configbuilder/identity.go b/config/internal/configbuilder/identity.go index 50cd230c..5e64fa12 100644 --- a/config/internal/configbuilder/identity.go +++ b/config/internal/configbuilder/identity.go @@ -1,6 +1,8 @@ package configbuilder import ( + "fmt" + "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/optional" ) @@ -33,8 +35,8 @@ func (b *IdentityBuilder) Key(key string) { } // Partial marks the compomnent as partially configured. -func (b *IdentityBuilder) Partial() { - b.target.IsPartial = true +func (b *IdentityBuilder) Partial(format string, args ...any) { + b.target.IsPartialReasons = append(b.target.IsPartialReasons, fmt.Sprintf(format, args...)) } // Speculative marks the component as speculative. diff --git a/config/internal/configbuilder/integration.go b/config/internal/configbuilder/integration.go index 696795a4..ba6cb9d3 100644 --- a/config/internal/configbuilder/integration.go +++ b/config/internal/configbuilder/integration.go @@ -1,6 +1,8 @@ package configbuilder import ( + "fmt" + "github.com/dogmatiq/dogma" "github.com/dogmatiq/enginekit/config" ) @@ -47,8 +49,8 @@ func (b *IntegrationBuilder) Disabled(fn func(*FlagBuilder[config.Disabled])) { } // Partial marks the compomnent as partially configured. -func (b *IntegrationBuilder) Partial() { - b.target.IsPartial = true +func (b *IntegrationBuilder) Partial(format string, args ...any) { + b.target.IsPartialReasons = append(b.target.IsPartialReasons, fmt.Sprintf(format, args...)) } // Speculative marks the component as speculative. diff --git a/config/internal/configbuilder/process.go b/config/internal/configbuilder/process.go index e61f532f..f8c206ba 100644 --- a/config/internal/configbuilder/process.go +++ b/config/internal/configbuilder/process.go @@ -1,6 +1,8 @@ package configbuilder import ( + "fmt" + "github.com/dogmatiq/dogma" "github.com/dogmatiq/enginekit/config" ) @@ -47,8 +49,8 @@ func (b *ProcessBuilder) Disabled(fn func(*FlagBuilder[config.Disabled])) { } // Partial marks the compomnent as partially configured. -func (b *ProcessBuilder) Partial() { - b.target.IsPartial = true +func (b *ProcessBuilder) Partial(format string, args ...any) { + b.target.IsPartialReasons = append(b.target.IsPartialReasons, fmt.Sprintf(format, args...)) } // Speculative marks the component as speculative. diff --git a/config/internal/configbuilder/projection.go b/config/internal/configbuilder/projection.go index ce5c7711..4647f311 100644 --- a/config/internal/configbuilder/projection.go +++ b/config/internal/configbuilder/projection.go @@ -1,6 +1,8 @@ package configbuilder import ( + "fmt" + "github.com/dogmatiq/dogma" "github.com/dogmatiq/enginekit/config" ) @@ -53,8 +55,8 @@ func (b *ProjectionBuilder) DeliveryPolicy(fn func(*ProjectionDeliveryPolicyBuil } // Partial marks the compomnent as partially configured. -func (b *ProjectionBuilder) Partial() { - b.target.IsPartial = true +func (b *ProjectionBuilder) Partial(format string, args ...any) { + b.target.IsPartialReasons = append(b.target.IsPartialReasons, fmt.Sprintf(format, args...)) } // Speculative marks the component as speculative. diff --git a/config/internal/configbuilder/projectiondeliverypolicy.go b/config/internal/configbuilder/projectiondeliverypolicy.go index 41f8f962..d4cdc16b 100644 --- a/config/internal/configbuilder/projectiondeliverypolicy.go +++ b/config/internal/configbuilder/projectiondeliverypolicy.go @@ -1,6 +1,8 @@ package configbuilder import ( + "fmt" + "github.com/dogmatiq/dogma" "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/optional" @@ -46,8 +48,8 @@ func (b *ProjectionDeliveryPolicyBuilder) BroadcastToPrimaryFirst(v bool) { } // Partial marks the compomnent as partially configured. -func (b *ProjectionDeliveryPolicyBuilder) Partial() { - b.target.IsPartial = true +func (b *ProjectionDeliveryPolicyBuilder) Partial(format string, args ...any) { + b.target.IsPartialReasons = append(b.target.IsPartialReasons, fmt.Sprintf(format, args...)) } // Speculative marks the component as speculative. diff --git a/config/internal/configbuilder/route.go b/config/internal/configbuilder/route.go index 69ef3bb3..861299dc 100644 --- a/config/internal/configbuilder/route.go +++ b/config/internal/configbuilder/route.go @@ -1,6 +1,7 @@ package configbuilder import ( + "fmt" "reflect" "github.com/dogmatiq/dogma" @@ -67,8 +68,8 @@ func (b *RouteBuilder) MessageType(t message.Type) { } // Partial marks the compomnent as partially configured. -func (b *RouteBuilder) Partial() { - b.target.IsPartial = true +func (b *RouteBuilder) Partial(format string, args ...any) { + b.target.IsPartialReasons = append(b.target.IsPartialReasons, fmt.Sprintf(format, args...)) } // Speculative marks the component as speculative. diff --git a/config/process_test.go b/config/process_test.go index 8522422d..d5c980bb 100644 --- a/config/process_test.go +++ b/config/process_test.go @@ -106,7 +106,7 @@ func TestProcess(t *testing.T) { Name: "nil process", Error: multiline( `process is invalid:`, - ` - could not evaluate entire configuration`, + ` - could not evaluate entire configuration: handler is nil`, ` - no identity`, ` - no handles-event routes`, ` - no executes-command routes`, diff --git a/config/projection_test.go b/config/projection_test.go index 6e1dd362..12ad2495 100644 --- a/config/projection_test.go +++ b/config/projection_test.go @@ -103,7 +103,7 @@ func TestProjection(t *testing.T) { Name: "nil projection", Error: multiline( `projection is invalid:`, - ` - could not evaluate entire configuration`, + ` - could not evaluate entire configuration: handler is nil`, ` - no identity`, ` - no handles-event routes`, ), @@ -426,13 +426,13 @@ func TestProjection(t *testing.T) { t.Run("it panics if the handler is partially configured", func(t *testing.T) { handler := configbuilder.Projection( func(b *configbuilder.ProjectionBuilder) { - b.Partial() + b.Partial("") }, ) test.ExpectPanic( t, - "could not evaluate entire configuration", + `could not evaluate entire configuration: `, func() { handler.DeliveryPolicy() }, diff --git a/config/runtimeconfig/aggregate.go b/config/runtimeconfig/aggregate.go index 73db3fa3..f037abda 100644 --- a/config/runtimeconfig/aggregate.go +++ b/config/runtimeconfig/aggregate.go @@ -18,7 +18,7 @@ func FromAggregate(h dogma.AggregateMessageHandler) *config.Aggregate { func buildAggregate(b *configbuilder.AggregateBuilder, h dogma.AggregateMessageHandler) { if h == nil { - b.Partial() + b.Partial("handler is nil") } else { b.Source(h) h.Configure(newHandlerConfigurer[dogma.AggregateRoute](b)) diff --git a/config/runtimeconfig/aggregate_test.go b/config/runtimeconfig/aggregate_test.go index 9e43bfaf..f487e18b 100644 --- a/config/runtimeconfig/aggregate_test.go +++ b/config/runtimeconfig/aggregate_test.go @@ -28,7 +28,7 @@ func TestFromAggregate(t *testing.T) { HandlerCommon: config.HandlerCommon{ EntityCommon: config.EntityCommon{ ComponentCommon: config.ComponentCommon{ - IsPartial: true, + IsPartialReasons: []string{"handler is nil"}, }, }, }, diff --git a/config/runtimeconfig/application.go b/config/runtimeconfig/application.go index 29525657..a27021a0 100644 --- a/config/runtimeconfig/application.go +++ b/config/runtimeconfig/application.go @@ -12,7 +12,7 @@ func FromApplication(app dogma.Application) *config.Application { return configbuilder.Application( func(b *configbuilder.ApplicationBuilder) { if app == nil { - b.Partial() + b.Partial("application is nil") } else { b.Source(app) app.Configure(&applicationConfigurer{b}) diff --git a/config/runtimeconfig/application_test.go b/config/runtimeconfig/application_test.go index fa263639..1a0df120 100644 --- a/config/runtimeconfig/application_test.go +++ b/config/runtimeconfig/application_test.go @@ -31,7 +31,7 @@ func TestFromApplication(t *testing.T) { return &config.Application{ EntityCommon: config.EntityCommon{ ComponentCommon: config.ComponentCommon{ - IsPartial: true, + IsPartialReasons: []string{"application is nil"}, }, }, } diff --git a/config/runtimeconfig/integration.go b/config/runtimeconfig/integration.go index d3bf7adf..ed273306 100644 --- a/config/runtimeconfig/integration.go +++ b/config/runtimeconfig/integration.go @@ -18,7 +18,7 @@ func FromIntegration(h dogma.IntegrationMessageHandler) *config.Integration { func buildIntegration(b *configbuilder.IntegrationBuilder, h dogma.IntegrationMessageHandler) { if h == nil { - b.Partial() + b.Partial("handler is nil") } else { b.Source(h) h.Configure(newHandlerConfigurer[dogma.IntegrationRoute](b)) diff --git a/config/runtimeconfig/integration_test.go b/config/runtimeconfig/integration_test.go index 9068eac7..4bb0f859 100644 --- a/config/runtimeconfig/integration_test.go +++ b/config/runtimeconfig/integration_test.go @@ -26,7 +26,7 @@ func TestFromIntegration(t *testing.T) { HandlerCommon: config.HandlerCommon{ EntityCommon: config.EntityCommon{ ComponentCommon: config.ComponentCommon{ - IsPartial: true, + IsPartialReasons: []string{"handler is nil"}, }, }, }, diff --git a/config/runtimeconfig/process.go b/config/runtimeconfig/process.go index bfdcfe5b..bf1bd6aa 100644 --- a/config/runtimeconfig/process.go +++ b/config/runtimeconfig/process.go @@ -18,7 +18,7 @@ func FromProcess(h dogma.ProcessMessageHandler) *config.Process { func buildProcess(b *configbuilder.ProcessBuilder, h dogma.ProcessMessageHandler) { if h == nil { - b.Partial() + b.Partial("handler is nil") } else { b.Source(h) h.Configure(newHandlerConfigurer[dogma.ProcessRoute](b)) diff --git a/config/runtimeconfig/process_test.go b/config/runtimeconfig/process_test.go index b77c60c6..9d0a82f6 100644 --- a/config/runtimeconfig/process_test.go +++ b/config/runtimeconfig/process_test.go @@ -26,7 +26,7 @@ func TestFromProcess(t *testing.T) { HandlerCommon: config.HandlerCommon{ EntityCommon: config.EntityCommon{ ComponentCommon: config.ComponentCommon{ - IsPartial: true, + IsPartialReasons: []string{"handler is nil"}, }, }, }, diff --git a/config/runtimeconfig/projection.go b/config/runtimeconfig/projection.go index 4e303e18..832a552a 100644 --- a/config/runtimeconfig/projection.go +++ b/config/runtimeconfig/projection.go @@ -16,7 +16,7 @@ func FromProjection(h dogma.ProjectionMessageHandler) *config.Projection { func buildProjection(b *configbuilder.ProjectionBuilder, h dogma.ProjectionMessageHandler) { if h == nil { - b.Partial() + b.Partial("handler is nil") } else { b.Source(h) h.Configure(&projectionConfigurer{ diff --git a/config/runtimeconfig/projection_test.go b/config/runtimeconfig/projection_test.go index 5bc11e97..74e5b559 100644 --- a/config/runtimeconfig/projection_test.go +++ b/config/runtimeconfig/projection_test.go @@ -26,7 +26,7 @@ func TestFromProjection(t *testing.T) { HandlerCommon: config.HandlerCommon{ EntityCommon: config.EntityCommon{ ComponentCommon: config.ComponentCommon{ - IsPartial: true, + IsPartialReasons: []string{"handler is nil"}, }, }, }, diff --git a/config/staticconfig/application.go b/config/staticconfig/application.go index 984fb21e..c35db7e2 100644 --- a/config/staticconfig/application.go +++ b/config/staticconfig/application.go @@ -38,6 +38,6 @@ func analyzeApplicationConfigurerCall( case "RegisterProjection": analyzeHandler(ctx, ctx.Builder.Projection, analyzeProjectionConfigurerCall) default: - ctx.Builder.Partial() + cannotAnalyzeUnrecognizedConfigurerMethod(ctx) } } diff --git a/config/staticconfig/entity.go b/config/staticconfig/entity.go index e02d449e..fdd16d53 100644 --- a/config/staticconfig/entity.go +++ b/config/staticconfig/entity.go @@ -1,7 +1,10 @@ package staticconfig import ( + "fmt" "go/types" + "os" + "runtime" "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/config/internal/configbuilder" @@ -41,8 +44,15 @@ type configurerCallContext[ *entityContext[T, E, B] *ssa.CallCommon - Instruction ssa.CallInstruction - IsSpeculative bool + Instruction ssa.CallInstruction + IsUnconditional bool +} + +// Apply configures b with any properties that are inferred from the context. +func (c *configurerCallContext[T, E, B]) Apply(b configbuilder.UntypedComponentBuilder) { + if !c.IsUnconditional { + b.Speculative() + } } // configurerCallAnalyzer is a function that analyzes a call to a method on an @@ -71,6 +81,9 @@ func analyzeEntity[ builder.TypeName(typename.OfStatic(t)) configure := ctx.LookupMethod(t, "Configure") + fmt.Println("===========================================") + configure.WriteTo(os.Stderr) + ectx := &entityContext[T, E, B]{ context: ctx, EntityType: t, @@ -125,14 +138,14 @@ func analyzeConfigurerCallsInInstruction[ inst ssa.CallInstruction, analyze configurerCallAnalyzer[T, E, B], ) { - com := inst.Common() + call := inst.Common() - if com.IsInvoke() && ctx.IsConfigurer(com.Value) { + if call.IsInvoke() && ctx.IsConfigurer(call.Value) { analyze(&configurerCallContext[T, E, B]{ - entityContext: ctx, - CallCommon: com, - Instruction: inst, - IsSpeculative: !ssax.IsUnconditional(inst.Block()), + entityContext: ctx, + CallCommon: call, + Instruction: inst, + IsUnconditional: ssax.IsUnconditional(inst.Block()), }) return } @@ -149,7 +162,7 @@ func analyzeConfigurerCallsInInstruction[ // 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 { + for i, arg := range call.Args { if ctx.IsConfigurer(arg) { indices = append(indices, i) } @@ -162,9 +175,9 @@ func analyzeConfigurerCallsInInstruction[ // If we can't obtain the callee this is a call to an interface method or // some other un-analyzable function. - fn := com.StaticCallee() + fn := call.StaticCallee() if fn == nil { - ctx.Builder.Partial() + cannotAnalyzeNonStaticCall(ctx.Builder) return } @@ -191,9 +204,7 @@ func analyzeIdentity[ ctx. Builder. Identity(func(b *configbuilder.IdentityBuilder) { - if ctx.IsSpeculative { - b.Speculative() - } + ctx.Apply(b) if name, ok := ssax.AsString(ctx.Args[0]).TryGet(); ok { b.Name(name) @@ -204,3 +215,29 @@ func analyzeIdentity[ } }) } + +func cannotAnalyzeUnrecognizedConfigurerMethod[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], +]( + ctx *configurerCallContext[T, E, B], +) { + ctx.Builder.Partial( + "configuration uses %s.%s(), which is not recognized", + ctx.Value.Type(), + ctx.Method.Name(), + ) +} + +func cannotAnalyzeNonStaticCall(b configbuilder.UntypedComponentBuilder) { + b.Partial("analysis of non-static function call is not possible") +} + +func unimplementedAnalysis(b configbuilder.UntypedComponentBuilder, node any) { + if _, file, line, ok := runtime.Caller(1); ok { + b.Partial("static analysis of %T is not implemented at %s:%d", node, file, line) + } else { + b.Partial("static analysis of %T is not implemented", node) + } +} diff --git a/config/staticconfig/handler.go b/config/staticconfig/handler.go index e4b85683..884024bf 100644 --- a/config/staticconfig/handler.go +++ b/config/staticconfig/handler.go @@ -17,14 +17,12 @@ func analyzeHandler[ analyze configurerCallAnalyzer[T, H, B], ) { build(func(b B) { - if ctx.IsSpeculative { - b.Speculative() - } + ctx.Apply(b) t := ssax.ConcreteType(ctx.Args[0]) if !t.IsPresent() { - b.Partial() + b.Partial("the handler's type is unknown") return } @@ -40,16 +38,14 @@ func analyzeHandler[ case "Disable": ctx.Builder.Disabled( func(b *configbuilder.FlagBuilder[config.Disabled]) { - if ctx.IsSpeculative { - b.Speculative() - } + ctx.Apply(b) b.Value(true) }, ) default: if analyze == nil { - ctx.Builder.Partial() + cannotAnalyzeUnrecognizedConfigurerMethod(ctx) } else { analyze(ctx) } @@ -66,6 +62,6 @@ func analyzeProjectionConfigurerCall( case "DeliveryPolicy": panic("not implemented") // TODO default: - ctx.Builder.Partial() + cannotAnalyzeUnrecognizedConfigurerMethod(ctx) } } diff --git a/config/staticconfig/route.go b/config/staticconfig/route.go index ca0a6c50..fa66d314 100644 --- a/config/staticconfig/route.go +++ b/config/staticconfig/route.go @@ -21,55 +21,44 @@ func analyzeRoutes[ ) } -func analyzeRoute( - ctx *context, +func analyzeRoute[ + T config.Handler, + H any, + B configbuilder.HandlerBuilder[T, H], +]( + ctx *configurerCallContext[T, H, B], b *configbuilder.RouteBuilder, r ssa.Value, ) { - call, ok := findRouteCall(ctx, r) - if !ok { - b.Partial() - return - } - - fn := call.Common().StaticCallee() + switch r := r.(type) { + case *ssa.MakeInterface: + analyzeRoute(ctx, b, r.X) - switch fn.Object() { - case ctx.Dogma.HandlesCommand: - b.RouteType(config.HandlesCommandRouteType) - case ctx.Dogma.HandlesEvent: - b.RouteType(config.HandlesEventRouteType) - case ctx.Dogma.ExecutesCommand: - b.RouteType(config.ExecutesCommandRouteType) - case ctx.Dogma.RecordsEvent: - b.RouteType(config.RecordsEventRouteType) - case ctx.Dogma.SchedulesTimeout: - b.RouteType(config.SchedulesTimeoutRouteType) - } + case *ssa.Call: + call := r.Common() + fn := call.StaticCallee() - b.MessageTypeName(typename.OfStatic(fn.TypeArgs()[0])) -} + if fn == nil { + cannotAnalyzeNonStaticCall(b) + return + } -func findRouteCall( - ctx *context, - v ssa.Value, -) (*ssa.Call, bool) { - switch v := v.(type) { - case *ssa.Call: - fn := v.Common().StaticCallee() - if fn != nil { - switch fn.Object() { - case ctx.Dogma.HandlesCommand, - ctx.Dogma.HandlesEvent, - ctx.Dogma.ExecutesCommand, - ctx.Dogma.RecordsEvent, - ctx.Dogma.SchedulesTimeout: - return v, true - } + switch fn.Object() { + case ctx.Dogma.HandlesCommand: + b.RouteType(config.HandlesCommandRouteType) + case ctx.Dogma.HandlesEvent: + b.RouteType(config.HandlesEventRouteType) + case ctx.Dogma.ExecutesCommand: + b.RouteType(config.ExecutesCommandRouteType) + case ctx.Dogma.RecordsEvent: + b.RouteType(config.RecordsEventRouteType) + case ctx.Dogma.SchedulesTimeout: + b.RouteType(config.SchedulesTimeoutRouteType) } - case *ssa.MakeInterface: - return findRouteCall(ctx, v.X) - } - return nil, false + b.MessageTypeName(typename.OfStatic(fn.TypeArgs()[0])) + + default: + unimplementedAnalysis(b, r) + } } diff --git a/config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md b/config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md deleted file mode 100644 index 98cda2f1..00000000 --- a/config/staticconfig/testdata/_pending/conditional-routes-in-handlers.md +++ /dev/null @@ -1,67 +0,0 @@ -# Conditional Routes in Dogma Application Handlers - -This test verifies that static analysis correctly parses handles that have -conditional routes within their bodies. - -```go au:input au:group=matrix -package app - -import ( - "context" - "math/rand" - . "github.com/dogmatiq/dogma" - "github.com/dogmatiq/enginekit/enginetest/stubs" -) - -// App implements Application interface. -type App struct{} - -// Configure configures the behavior of the engine as it relates to this -// application. -func (App) Configure(c ApplicationConfigurer) { - c.Identity("", "7e34538e-c407-4af8-8d3c-960e09cde98a") - c.RegisterIntegration(IntegrationHandler{}) -} - -// IntegrationHandler is a test implementation of -// IntegrationMessageHandler. -type IntegrationHandler struct{} - -// Configure configures the behavior of the engine as it relates to this -// handler. -func (IntegrationHandler) Configure(c IntegrationConfigurer) { - c.Identity("", "92cce461-8d30-409b-8d5a-406f656cef2d") - - if rand.Int() == 0 { - c.Routes( - HandlesCommand[stubs.CommandStub[stubs.TypeA]](), - RecordsEvent[stubs.EventStub[stubs.TypeA]](), - ) - } else { - c.Routes( - HandlesCommand[stubs.CommandStub[stubs.TypeB]](), - RecordsEvent[stubs.EventStub[stubs.TypeB]](), - ) - } -} - -// HandleCommand handles a command message that has been routed to this handler. -func (IntegrationHandler) HandleCommand( - context.Context, - IntegrationCommandScope, - Command, -) error { - return nil -} - -``` - -```au:output au:group=matrix -application (7e34538e-c407-4af8-8d3c-960e09cde98a) App - - - integration (92cce461-8d30-409b-8d5a-406f656cef2d) IntegrationHandler - handles CommandStub[TypeA]? - handles CommandStub[TypeB]? - records EventStub[TypeA]! - records EventStub[TypeB]! -``` diff --git a/config/staticconfig/testdata/handler-with-appended-routes.md b/config/staticconfig/testdata/handler-with-appended-routes.md new file mode 100644 index 00000000..8c2dbeac --- /dev/null +++ b/config/staticconfig/testdata/handler-with-appended-routes.md @@ -0,0 +1,44 @@ +# Handler with appended routes + +This test verifies that the static analyzer correctly identifies routes that are +appended to a slice before being passed to the `Routes()` method. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - valid identity app/e4183527-c234-42d5-8709-3dc8b9d5caa4 + - valid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (value unavailable) + - valid identity handler/5752bb84-0b65-4a7f-b2fa-bfb77a53a97f + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) +``` + +```go au:input au:group=matrix +package app + +import "context" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "5752bb84-0b65-4a7f-b2fa-bfb77a53a97f") + + var routes []dogma.IntegrationRoute + + routes = append( + routes, + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]](), + ) + + c.Routes(routes...) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "e4183527-c234-42d5-8709-3dc8b9d5caa4") + c.RegisterIntegration(Integration{}) +} +``` diff --git a/config/staticconfig/testdata/handler-with-conditional-routes.md b/config/staticconfig/testdata/handler-with-conditional-routes.md new file mode 100644 index 00000000..d00ee0ad --- /dev/null +++ b/config/staticconfig/testdata/handler-with-conditional-routes.md @@ -0,0 +1,143 @@ +# Handler with conditional routes + +This test verifies that the static analyzer correctly identifies when routes are +added to a handler conditionally. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - valid identity app/e4183527-c234-42d5-8709-3dc8b9d5caa4 + - valid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (value unavailable) + - valid identity handler/5752bb84-0b65-4a7f-b2fa-bfb77a53a97f + - valid speculative handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid speculative records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid speculative handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeB] (type unavailable) + - valid speculative records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeB] (type unavailable) +``` + +## Routes() call within conditional block + +```go au:input au:group=matrix +package app + +import "context" +import "math/rand" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "5752bb84-0b65-4a7f-b2fa-bfb77a53a97f") + + if rand.Int() == 0 { + c.Routes( + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]](), + dogma.RecordsEvent[stubs.EventStub[stubs.TypeA]](), + ) + } else { + c.Routes( + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeB]](), + dogma.RecordsEvent[stubs.EventStub[stubs.TypeB]](), + ) + } +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "e4183527-c234-42d5-8709-3dc8b9d5caa4") + c.RegisterIntegration(Integration{}) +} +``` + +## Slice built within conditional block + +```go au:input au:group=matrix +package app + +import "context" +import "math/rand" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "5752bb84-0b65-4a7f-b2fa-bfb77a53a97f") + + var routes []dogma.IntegrationRoute + + if rand.Int() == 0 { + routes = append( + routes, + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]](), + dogma.RecordsEvent[stubs.EventStub[stubs.TypeA]](), + ) + } else { + routes = append( + routes, + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeB]](), + dogma.RecordsEvent[stubs.EventStub[stubs.TypeB]](), + ) + } + + c.Routes(routes...) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "e4183527-c234-42d5-8709-3dc8b9d5caa4") + c.RegisterIntegration(Integration{}) +} +``` + +## Slice built within multiple conditional blocks + +```go au:input au:group=matrix +package app + +import "context" +import "math/rand" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "5752bb84-0b65-4a7f-b2fa-bfb77a53a97f") + + var routes []dogma.IntegrationRoute + + if rand.Int() == 0 { + routes = append( + routes, + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]](), + dogma.RecordsEvent[stubs.EventStub[stubs.TypeA]](), + ) + } + + if rand.Int() == 0 { + routes = append( + routes, + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeB]](), + dogma.RecordsEvent[stubs.EventStub[stubs.TypeB]](), + ) + } + + c.Routes(routes...) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "e4183527-c234-42d5-8709-3dc8b9d5caa4") + c.RegisterIntegration(Integration{}) +} +``` diff --git a/config/staticconfig/testdata/incomplete-entity.md b/config/staticconfig/testdata/incomplete-entity.md index 116421e8..b1fab971 100644 --- a/config/staticconfig/testdata/incomplete-entity.md +++ b/config/staticconfig/testdata/incomplete-entity.md @@ -6,7 +6,7 @@ analyzed. ```au:output au:group=matrix incomplete application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - - could not evaluate entire configuration + - could not evaluate entire configuration: analysis of non-static function call is not possible - valid identity app/de142370-93ee-409c-9336-5084d9c5e285 ``` diff --git a/config/staticconfig/testdata/nil-handlers.md b/config/staticconfig/testdata/nil-handlers.md index f6172e30..6d885377 100644 --- a/config/staticconfig/testdata/nil-handlers.md +++ b/config/staticconfig/testdata/nil-handlers.md @@ -7,21 +7,21 @@ presence of `nil` handlers. valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - valid identity app/0726ae0d-67e4-4a50-8a19-9f58eae38e51 - incomplete aggregate - - could not evaluate entire configuration + - could not evaluate entire configuration: the handler's type is unknown - no identity - no handles-command routes - no records-event routes - incomplete process - - could not evaluate entire configuration + - could not evaluate entire configuration: the handler's type is unknown - no identity - no handles-event routes - no executes-command routes - incomplete integration - - could not evaluate entire configuration + - could not evaluate entire configuration: the handler's type is unknown - no identity - no handles-command routes - incomplete projection - - could not evaluate entire configuration + - could not evaluate entire configuration: the handler's type is unknown - no identity - no handles-event routes ``` diff --git a/config/staticconfig/varargs.go b/config/staticconfig/varargs.go index 219889b9..9f8a3ebe 100644 --- a/config/staticconfig/varargs.go +++ b/config/staticconfig/varargs.go @@ -7,86 +7,125 @@ import ( "golang.org/x/tools/go/ssa" ) +type variadicConfigurerCallContext[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], + TC config.Component, + BC configbuilder.ComponentBuilder[TC], +] struct { + *configurerCallContext[T, E, B] + + BuildChild func(func(BC)) + AnalyzeChild func(*configurerCallContext[T, E, B], BC, ssa.Value) + + seen map[ssa.Value]struct{} +} + +// analyzeVariadicArguments analyzes the variadic arguments of a method call. func analyzeVariadicArguments[ T config.Entity, E any, B configbuilder.EntityBuilder[T, E], - TChild config.Component, - BChild configbuilder.ComponentBuilder[TChild], + TC config.Component, + BC configbuilder.ComponentBuilder[TC], ]( ctx *configurerCallContext[T, E, B], - child func(func(BChild)), - analyze func(*context, BChild, ssa.Value), + buildChild func(func(BC)), + analyzeChild func(*configurerCallContext[T, E, B], BC, ssa.Value), ) { - // The variadic slice parameter is always the last argument. - varargs := ctx.Args[len(ctx.Args)-1] + walkUpVariadic( + &variadicConfigurerCallContext[T, E, B, TC, BC]{ + configurerCallContext: ctx, + BuildChild: buildChild, + AnalyzeChild: analyzeChild, + seen: map[ssa.Value]struct{}{}, + }, + ctx.Args[len(ctx.Args)-1], // varadic slice is always the last argument + ) +} - if ssax.IsZeroValue(varargs) { +func walkUpVariadic[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], + TC config.Component, + BC configbuilder.ComponentBuilder[TC], +]( + ctx *variadicConfigurerCallContext[T, E, B, TC, BC], + v ssa.Value, +) { + if _, ok := ctx.seen[v]; ok { return } + ctx.seen[v] = struct{}{} - array, ok := findSliceArrayAllocation(varargs) - if !ok { - ctx.Builder.Partial() - return - } + switch v := v.(type) { + default: + unimplementedAnalysis(ctx.Builder, v) - buildersByIndex := map[int][]BChild{} + case *ssa.Const: + // We've found a nil slice. - for block := range ssax.WalkBlock(array.Block()) { - // If there's no path from this block to the call instruction, we can - // safely ignore it, even if it modifies the underlying array. - if !ssax.PathExists(block, ctx.Instruction.Block()) { - continue + case *ssa.Phi: + for _, edge := range v.Edges { + walkUpVariadic(ctx, edge) } - for inst := range ssax.InstructionsBefore(block, ctx.Instruction) { - switch inst := inst.(type) { - case *ssa.Store: - if addr, ok := inst.Addr.(*ssa.IndexAddr); ok && addr.X == array { - child(func(b BChild) { - if index, ok := ssax.AsInt(addr.Index).TryGet(); ok { - // If there are multiple writes to the same index, - // we mark them all as speculative. - // - // TODO: Could we handle this more intelligently by - // using the value of the store instruction closest - // to the call instruction? - conflicting := buildersByIndex[index] - if len(conflicting) == 1 { - conflicting[0].Speculative() - } - if len(conflicting) != 0 { - b.Speculative() - } - buildersByIndex[index] = append(conflicting, b) - } else { - // If we can't resolve the index we assume the child - // is speculative because we can't tell if it is - // ever overwritten with a different value. - b.Speculative() - } - - if ctx.IsSpeculative { - b.Speculative() - } - - analyze(ctx.context, b, inst.Val) - }) + case *ssa.Alloc: + walkDownVariadic(ctx, v) + + case *ssa.Slice: + walkUpVariadic(ctx, v.X) + + case *ssa.Call: + call := v.Common() + + if fn, ok := call.Value.(*ssa.Builtin); ok { + if fn.Name() == "append" { + for _, arg := range call.Args { + walkUpVariadic(ctx, arg) } } } } } -// findSliceArrayAllocation returns the underlying array allocation of a slice. -func findSliceArrayAllocation(v ssa.Value) (*ssa.Alloc, bool) { - switch v := v.(type) { - case *ssa.Alloc: - return v, true - case *ssa.Slice: - return findSliceArrayAllocation(v.X) - default: - return nil, false +func walkDownVariadic[ + T config.Entity, + E any, + B configbuilder.EntityBuilder[T, E], + TC config.Component, + BC configbuilder.ComponentBuilder[TC], +]( + ctx *variadicConfigurerCallContext[T, E, B, TC, BC], + alloc *ssa.Alloc, +) { + for block := range ssax.WalkBlock(alloc.Block()) { + unconditional := ssax.IsUnconditional(block) + + for inst := range ssax.InstructionsBefore(block, ctx.Instruction) { + switch inst := inst.(type) { + case *ssa.Store: + addr, ok := inst.Addr.(*ssa.IndexAddr) + if !ok { + continue + } + + if addr.X != alloc { + continue + } + + ctx.BuildChild(func(b BC) { + ctx.Apply(b) + + if !unconditional { + b.Speculative() + } + + ctx.AnalyzeChild(ctx.configurerCallContext, b, inst.Val) + }) + } + } } } diff --git a/config/validate.go b/config/validate.go index 46c043e2..d63b4eab 100644 --- a/config/validate.go +++ b/config/validate.go @@ -138,8 +138,10 @@ func newResolutionContext(c Component, allowPartial bool) *validateContext { }, } - if !allowPartial && c.ComponentProperties().IsPartial { - ctx.Invalid(PartialConfigurationError{}) + p := c.ComponentProperties() + + if !allowPartial && len(p.IsPartialReasons) > 0 { + ctx.Invalid(PartialConfigurationError{p.IsPartialReasons}) } return ctx From 13f27ead5746d986e27883c571a46979c25041e8 Mon Sep 17 00:00:00 2001 From: James Harris Date: Wed, 6 Nov 2024 08:10:14 +1000 Subject: [PATCH 32/38] Remove debug code. --- config/staticconfig/entity.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/config/staticconfig/entity.go b/config/staticconfig/entity.go index fdd16d53..41f1cd9e 100644 --- a/config/staticconfig/entity.go +++ b/config/staticconfig/entity.go @@ -1,9 +1,7 @@ package staticconfig import ( - "fmt" "go/types" - "os" "runtime" "github.com/dogmatiq/enginekit/config" @@ -81,9 +79,6 @@ func analyzeEntity[ builder.TypeName(typename.OfStatic(t)) configure := ctx.LookupMethod(t, "Configure") - fmt.Println("===========================================") - configure.WriteTo(os.Stderr) - ectx := &entityContext[T, E, B]{ context: ctx, EntityType: t, From 04e17c43a9597e703c705fc74609e1f50fa43a16 Mon Sep 17 00:00:00 2001 From: James Harris Date: Wed, 6 Nov 2024 09:25:39 +1000 Subject: [PATCH 33/38] Improve "speculative" detection of dynamically constructed variadics. --- config/staticconfig/entity.go | 5 + .../testdata/handler-with-appended-routes.md | 44 ----- .../handler-with-speculative-routes.md | 83 +++++++++ .../testdata/handler-with-variadic-routes.md | 111 +++++++++++ config/staticconfig/varargs.go | 175 ++++++++++-------- optional/numeric.go | 25 +++ 6 files changed, 320 insertions(+), 123 deletions(-) delete mode 100644 config/staticconfig/testdata/handler-with-appended-routes.md create mode 100644 config/staticconfig/testdata/handler-with-speculative-routes.md create mode 100644 config/staticconfig/testdata/handler-with-variadic-routes.md create mode 100644 optional/numeric.go diff --git a/config/staticconfig/entity.go b/config/staticconfig/entity.go index 41f1cd9e..fdd16d53 100644 --- a/config/staticconfig/entity.go +++ b/config/staticconfig/entity.go @@ -1,7 +1,9 @@ package staticconfig import ( + "fmt" "go/types" + "os" "runtime" "github.com/dogmatiq/enginekit/config" @@ -79,6 +81,9 @@ func analyzeEntity[ builder.TypeName(typename.OfStatic(t)) configure := ctx.LookupMethod(t, "Configure") + fmt.Println("===========================================") + configure.WriteTo(os.Stderr) + ectx := &entityContext[T, E, B]{ context: ctx, EntityType: t, diff --git a/config/staticconfig/testdata/handler-with-appended-routes.md b/config/staticconfig/testdata/handler-with-appended-routes.md deleted file mode 100644 index 8c2dbeac..00000000 --- a/config/staticconfig/testdata/handler-with-appended-routes.md +++ /dev/null @@ -1,44 +0,0 @@ -# Handler with appended routes - -This test verifies that the static analyzer correctly identifies routes that are -appended to a slice before being passed to the `Routes()` method. - -```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) - - valid identity app/e4183527-c234-42d5-8709-3dc8b9d5caa4 - - valid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (value unavailable) - - valid identity handler/5752bb84-0b65-4a7f-b2fa-bfb77a53a97f - - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) -``` - -```go au:input au:group=matrix -package app - -import "context" -import "github.com/dogmatiq/dogma" -import "github.com/dogmatiq/enginekit/enginetest/stubs" - -type Integration struct{} - -func (Integration) Configure(c dogma.IntegrationConfigurer) { - c.Identity("handler", "5752bb84-0b65-4a7f-b2fa-bfb77a53a97f") - - var routes []dogma.IntegrationRoute - - routes = append( - routes, - dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]](), - ) - - c.Routes(routes...) -} - -func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } - -type App struct{} - -func (App) Configure(c dogma.ApplicationConfigurer) { - c.Identity("app", "e4183527-c234-42d5-8709-3dc8b9d5caa4") - c.RegisterIntegration(Integration{}) -} -``` diff --git a/config/staticconfig/testdata/handler-with-speculative-routes.md b/config/staticconfig/testdata/handler-with-speculative-routes.md new file mode 100644 index 00000000..442e5c0d --- /dev/null +++ b/config/staticconfig/testdata/handler-with-speculative-routes.md @@ -0,0 +1,83 @@ +# Handler with speculative routes + +This test verifies that the static analyzer correctly identifies routes as +"speculative" under various complex conditions. + +Some of these scenarios could be improved, potentially avoiding false positives +for the "speculative" flag. In general, however, it is preferred that the +analyzer errs on the side of caution and marks routes as "speculative" when it +is unsure. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - valid identity app/e4183527-c234-42d5-8709-3dc8b9d5caa4 + - valid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (value unavailable) + - valid identity handler/5752bb84-0b65-4a7f-b2fa-bfb77a53a97f + - valid speculative handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid speculative records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) +``` + +## Random index + +```go au:input au:group=matrix +package app + +import "context" +import "math/rand" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "5752bb84-0b65-4a7f-b2fa-bfb77a53a97f") + + routes := make([]dogma.IntegrationRoute, 0) + + routes[0] = dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]]() + routes[rand.Int()] = dogma.RecordsEvent[stubs.EventStub[stubs.TypeA]]() + + c.Routes(routes...) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "e4183527-c234-42d5-8709-3dc8b9d5caa4") + c.RegisterIntegration(Integration{}) +} +``` + +## Colliding indices + +```go au:input au:group=matrix +package app + +import "context" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "5752bb84-0b65-4a7f-b2fa-bfb77a53a97f") + + routes := make([]dogma.IntegrationRoute, 0) + + routes[0] = dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]]() + routes[0] = dogma.RecordsEvent[stubs.EventStub[stubs.TypeA]]() + + c.Routes(routes...) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "e4183527-c234-42d5-8709-3dc8b9d5caa4") + c.RegisterIntegration(Integration{}) +} +``` diff --git a/config/staticconfig/testdata/handler-with-variadic-routes.md b/config/staticconfig/testdata/handler-with-variadic-routes.md new file mode 100644 index 00000000..ac12c63a --- /dev/null +++ b/config/staticconfig/testdata/handler-with-variadic-routes.md @@ -0,0 +1,111 @@ +# Handler with variadic routes + +This test verifies that the static analyzer correctly identifies routes that are +configured via a slice which is used as the variadic parameter to the `Routes()` +method. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - valid identity app/e4183527-c234-42d5-8709-3dc8b9d5caa4 + - valid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (value unavailable) + - valid identity handler/5752bb84-0b65-4a7f-b2fa-bfb77a53a97f + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) +``` + +## Appended + +```go au:input au:group=matrix +package app + +import "context" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "5752bb84-0b65-4a7f-b2fa-bfb77a53a97f") + + var routes []dogma.IntegrationRoute + + routes = append( + routes, + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]](), + dogma.RecordsEvent[stubs.EventStub[stubs.TypeA]](), + ) + + c.Routes(routes...) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "e4183527-c234-42d5-8709-3dc8b9d5caa4") + c.RegisterIntegration(Integration{}) +} +``` + +## Assigned to index + +```go au:input au:group=matrix +package app + +import "context" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "5752bb84-0b65-4a7f-b2fa-bfb77a53a97f") + + routes := make([]dogma.IntegrationRoute, 1) + routes[0] = dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]]() + routes[1] = dogma.RecordsEvent[stubs.EventStub[stubs.TypeA]]() + + c.Routes(routes...) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "e4183527-c234-42d5-8709-3dc8b9d5caa4") + c.RegisterIntegration(Integration{}) +} +``` + +## Assigned to index of sub-slice + +```go au:input au:group=matrix +package app + +import "context" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "5752bb84-0b65-4a7f-b2fa-bfb77a53a97f") + + routes := make([]dogma.IntegrationRoute, 1) + routes[:1][0] = dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]]() + routes[1:][0] = dogma.RecordsEvent[stubs.EventStub[stubs.TypeA]]() + + c.Routes(routes...) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "e4183527-c234-42d5-8709-3dc8b9d5caa4") + c.RegisterIntegration(Integration{}) +} +``` diff --git a/config/staticconfig/varargs.go b/config/staticconfig/varargs.go index 9f8a3ebe..a2e6736c 100644 --- a/config/staticconfig/varargs.go +++ b/config/staticconfig/varargs.go @@ -1,27 +1,14 @@ package staticconfig import ( + "github.com/dogmatiq/enginekit/collections/sets" "github.com/dogmatiq/enginekit/config" "github.com/dogmatiq/enginekit/config/internal/configbuilder" "github.com/dogmatiq/enginekit/config/staticconfig/internal/ssax" + "github.com/dogmatiq/enginekit/optional" "golang.org/x/tools/go/ssa" ) -type variadicConfigurerCallContext[ - T config.Entity, - E any, - B configbuilder.EntityBuilder[T, E], - TC config.Component, - BC configbuilder.ComponentBuilder[TC], -] struct { - *configurerCallContext[T, E, B] - - BuildChild func(func(BC)) - AnalyzeChild func(*configurerCallContext[T, E, B], BC, ssa.Value) - - seen map[ssa.Value]struct{} -} - // analyzeVariadicArguments analyzes the variadic arguments of a method call. func analyzeVariadicArguments[ T config.Entity, @@ -34,98 +21,128 @@ func analyzeVariadicArguments[ buildChild func(func(BC)), analyzeChild func(*configurerCallContext[T, E, B], BC, ssa.Value), ) { - walkUpVariadic( - &variadicConfigurerCallContext[T, E, B, TC, BC]{ - configurerCallContext: ctx, - BuildChild: buildChild, - AnalyzeChild: analyzeChild, - seen: map[ssa.Value]struct{}{}, - }, + allocs := collectVariadicAllocations( + ctx.Builder, ctx.Args[len(ctx.Args)-1], // varadic slice is always the last argument ) -} -func walkUpVariadic[ - T config.Entity, - E any, - B configbuilder.EntityBuilder[T, E], - TC config.Component, - BC configbuilder.ComponentBuilder[TC], -]( - ctx *variadicConfigurerCallContext[T, E, B, TC, BC], - v ssa.Value, -) { - if _, ok := ctx.seen[v]; ok { - return - } - ctx.seen[v] = struct{}{} + var isVarArg func(v ssa.Value) (optional.Optional[int], bool) - switch v := v.(type) { - default: - unimplementedAnalysis(ctx.Builder, v) + isVarArg = func(v ssa.Value) (optional.Optional[int], bool) { + if allocs.Has(v) { + return optional.Some(0), true + } - case *ssa.Const: - // We've found a nil slice. + switch v := v.(type) { + case *ssa.Slice: + if index, ok := isVarArg(v.X); ok { + if v.Low == nil { + return index, true + } - case *ssa.Phi: - for _, edge := range v.Edges { - walkUpVariadic(ctx, edge) + return optional.Sum( + index, + ssax.AsInt(v.Low), + ), true + } + case *ssa.IndexAddr: + if index, ok := isVarArg(v.X); ok { + return optional.Sum( + index, + ssax.AsInt(v.Index), + ), true + } + default: + unimplementedAnalysis(ctx.Builder, v) } - case *ssa.Alloc: - walkDownVariadic(ctx, v) - - case *ssa.Slice: - walkUpVariadic(ctx, v.X) + return optional.None[int](), false + } - case *ssa.Call: - call := v.Common() + indexCounts := map[int]int{} + hasUnknownIndices := false + var children []func(BC) - if fn, ok := call.Value.(*ssa.Builtin); ok { - if fn.Name() == "append" { - for _, arg := range call.Args { - walkUpVariadic(ctx, arg) - } - } + for block := range ssax.WalkFunc(ctx.FunctionUnderAnalysis) { + if !ssax.PathExists(block, ctx.Instruction.Block()) { + continue } - } -} -func walkDownVariadic[ - T config.Entity, - E any, - B configbuilder.EntityBuilder[T, E], - TC config.Component, - BC configbuilder.ComponentBuilder[TC], -]( - ctx *variadicConfigurerCallContext[T, E, B, TC, BC], - alloc *ssa.Alloc, -) { - for block := range ssax.WalkBlock(alloc.Block()) { unconditional := ssax.IsUnconditional(block) for inst := range ssax.InstructionsBefore(block, ctx.Instruction) { - switch inst := inst.(type) { - case *ssa.Store: - addr, ok := inst.Addr.(*ssa.IndexAddr) + if inst, ok := inst.(*ssa.Store); ok { + index, ok := isVarArg(inst.Addr) if !ok { continue } - if addr.X != alloc { - continue + if i, ok := index.TryGet(); ok { + indexCounts[i]++ + } else { + hasUnknownIndices = true } - ctx.BuildChild(func(b BC) { + children = append(children, func(b BC) { ctx.Apply(b) - if !unconditional { + if hasUnknownIndices || !unconditional { + b.Speculative() + } else if i, ok := index.TryGet(); ok && indexCounts[i] > 1 { b.Speculative() } - ctx.AnalyzeChild(ctx.configurerCallContext, b, inst.Val) + analyzeChild(ctx, b, inst.Val) }) } } } + + for _, child := range children { + buildChild(child) + } +} + +func collectVariadicAllocations( + b configbuilder.UntypedComponentBuilder, + v ssa.Value, +) *sets.Set[ssa.Value] { + allocs := sets.New[ssa.Value]() + + var collect func(v ssa.Value) + collect = func(v ssa.Value) { + switch v := v.(type) { + case *ssa.Alloc: + allocs.Add(v) + + case *ssa.Slice: + collect(v.X) + + case *ssa.Const: + // We've found a nil slice. + + case *ssa.Phi: + for _, edge := range v.Edges { + collect(edge) + } + + case *ssa.Call: + call := v.Common() + + if fn, ok := call.Value.(*ssa.Builtin); ok { + if fn.Name() == "append" { + for _, arg := range call.Args { + collect(arg) + } + } + } + + default: + unimplementedAnalysis(b, v) + } + } + + collect(v) + + return allocs } diff --git a/optional/numeric.go b/optional/numeric.go new file mode 100644 index 00000000..05df2fb3 --- /dev/null +++ b/optional/numeric.go @@ -0,0 +1,25 @@ +package optional + +type numeric interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | + ~float32 | ~float64 | + ~complex64 | ~complex128 +} + +// Sum returns the sum of all of the given values. If are of the values are +// none, then the result is also none. +func Sum[T numeric](values ...Optional[T]) Optional[T] { + var sum T + + for _, value := range values { + v, ok := value.TryGet() + if !ok { + return None[T]() + } + + sum += v + } + + return Some(sum) +} From 566bd5fc1d54da9afd9616525afc47c0c489ef90 Mon Sep 17 00:00:00 2001 From: James Harris Date: Wed, 6 Nov 2024 10:12:54 +1000 Subject: [PATCH 34/38] Add tests for nil routes. --- config/staticconfig/route.go | 3 + .../_pending/dynamic-routes-in-handlers.md | 58 ------------------- .../_pending/nil-routes-in-handlers.md | 50 ---------------- .../testdata/handler-with-nil-route.md | 37 ++++++++++++ 4 files changed, 40 insertions(+), 108 deletions(-) delete mode 100644 config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md delete mode 100644 config/staticconfig/testdata/_pending/nil-routes-in-handlers.md create mode 100644 config/staticconfig/testdata/handler-with-nil-route.md diff --git a/config/staticconfig/route.go b/config/staticconfig/route.go index fa66d314..0ba8df7c 100644 --- a/config/staticconfig/route.go +++ b/config/staticconfig/route.go @@ -31,6 +31,9 @@ func analyzeRoute[ r ssa.Value, ) { switch r := r.(type) { + case *ssa.Const: + // We've found a nil route. + case *ssa.MakeInterface: analyzeRoute(ctx, b, r.X) diff --git a/config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md b/config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md deleted file mode 100644 index d91ab685..00000000 --- a/config/staticconfig/testdata/_pending/dynamic-routes-in-handlers.md +++ /dev/null @@ -1,58 +0,0 @@ -# Dynamic routes inside Dogma Application Handlers - -This test verifies that static analysis correctly parses routes in handles that -are dynamically populated. - -```go au:input au:group=matrix -package app - -import ( - "context" - . "github.com/dogmatiq/dogma" - "github.com/dogmatiq/enginekit/enginetest/stubs" -) - -// App implements Application interface. -type App struct{} - -// Configure configures the behavior of the engine as it relates to this -// application. -func (App) Configure(c ApplicationConfigurer) { - c.Identity("", "3bc3849b-abe0-4c4e-9db4-e48dc28c9a26") - c.RegisterIntegration(IntegrationHandler{}) -} - -// IntegrationHandler is a test implementation of -// IntegrationMessageHandler. -type IntegrationHandler struct{} - -// Configure configures the behavior of the engine as it relates to this -// handler. -func (IntegrationHandler) Configure(c IntegrationConfigurer) { - c.Identity("", "3a06b7da-1079-4e4b-a6a6-064c62241918") - - routes := []IntegrationRoute{ - HandlesCommand[stubs.CommandStub[stubs.TypeA]](), - RecordsEvent[stubs.EventStub[stubs.TypeA]](), - } - - c.Routes(routes...) -} - -// HandleCommand handles a command message that has been routed to this handler. -func (IntegrationHandler) HandleCommand( - context.Context, - IntegrationCommandScope, - Command, -) error { - return nil -} -``` - -```au:output au:group=matrix -application (3bc3849b-abe0-4c4e-9db4-e48dc28c9a26) App - - - integration (3a06b7da-1079-4e4b-a6a6-064c62241918) IntegrationHandler - handles CommandStub[TypeA]? - records EventStub[TypeA]! -``` diff --git a/config/staticconfig/testdata/_pending/nil-routes-in-handlers.md b/config/staticconfig/testdata/_pending/nil-routes-in-handlers.md deleted file mode 100644 index 7f02a313..00000000 --- a/config/staticconfig/testdata/_pending/nil-routes-in-handlers.md +++ /dev/null @@ -1,50 +0,0 @@ -# Nil Routes in Dogma Application Handlers - -This test verifies that static analysis correctly parses `nil` routes inside -Dogma Application handlers. - -```go au:input au:group=matrix -package app - -import ( - "context" - . "github.com/dogmatiq/dogma" -) - -// App implements Application interface. -type App struct{} - -// Configure configures the behavior of the engine as it relates to this -// application. -func (App) Configure(c ApplicationConfigurer) { - c.Identity("", "c100edcc-6dcc-42ed-ac75-69eecb3d0ec4") - c.RegisterIntegration(IntegrationHandler{}) -} - - -// IntegrationHandler is a test implementation of -// IntegrationMessageHandler. -type IntegrationHandler struct{} - -// Configure configures the behavior of the engine as it relates to this -// handler. -func (IntegrationHandler) Configure(c IntegrationConfigurer) { - c.Identity("", "363039e5-2938-4b2c-9bec-dcb29dee2da1") - c.Routes(nil) -} - -// HandleCommand handles a command message that has been routed to this handler. -func (IntegrationHandler) HandleCommand( - context.Context, - IntegrationCommandScope, - Command, -) error { - return nil -} -``` - -```au:output au:group=matrix -application (c100edcc-6dcc-42ed-ac75-69eecb3d0ec4) App - - - integration (363039e5-2938-4b2c-9bec-dcb29dee2da1) IntegrationHandler -``` diff --git a/config/staticconfig/testdata/handler-with-nil-route.md b/config/staticconfig/testdata/handler-with-nil-route.md new file mode 100644 index 00000000..fd2170f4 --- /dev/null +++ b/config/staticconfig/testdata/handler-with-nil-route.md @@ -0,0 +1,37 @@ +# Handler with nil route + +This test verifies that the static analyzer correctly detects a `nil` route. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - valid identity app/e4183527-c234-42d5-8709-3dc8b9d5caa4 + - invalid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration (value unavailable) + - no handles-command routes + - valid identity handler/5752bb84-0b65-4a7f-b2fa-bfb77a53a97f + - incomplete route + - route type is unavailable + - message type name is unavailable +``` + +```go au:input au:group=matrix +package app + +import "context" +import "github.com/dogmatiq/dogma" + +type Integration struct{} + +func (Integration) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "5752bb84-0b65-4a7f-b2fa-bfb77a53a97f") + c.Routes(nil) +} + +func (Integration) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "e4183527-c234-42d5-8709-3dc8b9d5caa4") + c.RegisterIntegration(Integration{}) +} +``` From d97328e95b9610ba967a9295edc576c4e3b56615 Mon Sep 17 00:00:00 2001 From: James Harris Date: Wed, 6 Nov 2024 10:27:45 +1000 Subject: [PATCH 35/38] Add more tests for generic types. --- config/staticconfig/entity.go | 5 -- .../testdata/_pending/aliased-handlers.md | 60 ------------------- .../testdata/alias-for-generic.md | 23 ------- config/staticconfig/testdata/generic-app.md | 43 +++++++++++++ .../testdata/generic-handler-via-alias.md | 38 ++++++++++++ .../staticconfig/testdata/generic-handler.md | 36 +++++++++++ 6 files changed, 117 insertions(+), 88 deletions(-) delete mode 100644 config/staticconfig/testdata/_pending/aliased-handlers.md delete mode 100644 config/staticconfig/testdata/alias-for-generic.md create mode 100644 config/staticconfig/testdata/generic-app.md create mode 100644 config/staticconfig/testdata/generic-handler-via-alias.md create mode 100644 config/staticconfig/testdata/generic-handler.md diff --git a/config/staticconfig/entity.go b/config/staticconfig/entity.go index fdd16d53..41f1cd9e 100644 --- a/config/staticconfig/entity.go +++ b/config/staticconfig/entity.go @@ -1,9 +1,7 @@ package staticconfig import ( - "fmt" "go/types" - "os" "runtime" "github.com/dogmatiq/enginekit/config" @@ -81,9 +79,6 @@ func analyzeEntity[ builder.TypeName(typename.OfStatic(t)) configure := ctx.LookupMethod(t, "Configure") - fmt.Println("===========================================") - configure.WriteTo(os.Stderr) - ectx := &entityContext[T, E, B]{ context: ctx, EntityType: t, diff --git a/config/staticconfig/testdata/_pending/aliased-handlers.md b/config/staticconfig/testdata/_pending/aliased-handlers.md deleted file mode 100644 index 8de00e09..00000000 --- a/config/staticconfig/testdata/_pending/aliased-handlers.md +++ /dev/null @@ -1,60 +0,0 @@ -# Type Aliased Handlers - -This test verifies that static analysis can correctly parse handlers that are -declared as type aliases. - -```go au:input au:group=matrix -package app - -import ( - "context" - . "github.com/dogmatiq/dogma" - "github.com/dogmatiq/enginekit/enginetest/stubs" -) - -type ( - // IntegrationHandlerAlias is a test type alias of IntegrationHandler. - IntegrationHandlerAlias = IntegrationHandler -) - -// App implements Application interface. -type App struct{} - -// Configure configures the behavior of the engine as it relates to this -// application. -func (App) Configure(c ApplicationConfigurer) { - c.Identity("", "1b828a1c-eba1-4e4c-88b8-e49f78ad15c7") - - c.RegisterIntegration(IntegrationHandlerAlias{}) -} - -// IntegrationHandler is a test implementation of -// IntegrationMessageHandler. -type IntegrationHandler struct{} - -// Configure configures the behavior of the engine as it relates to this -// handler. -func (IntegrationHandler) Configure(c IntegrationConfigurer) { - c.Identity("", "4d8cd3f5-21dc-475b-a8dc-80138adde3f2") - - c.Routes( - HandlesCommand[stubs.CommandStub[stubs.TypeB]](), - ) -} - -// HandleCommand handles a command message that has been routed to this handler. -func (IntegrationHandler) HandleCommand( - context.Context, - IntegrationCommandScope, - Command, -) error { - return nil -} -``` - -```au:output au:group=matrix -application (1b828a1c-eba1-4e4c-88b8-e49f78ad15c7) App - - - integration (4d8cd3f5-21dc-475b-a8dc-80138adde3f2) IntegrationHandlerAlias - handles CommandStub[TypeB]? -``` diff --git a/config/staticconfig/testdata/alias-for-generic.md b/config/staticconfig/testdata/alias-for-generic.md deleted file mode 100644 index 3bcba295..00000000 --- a/config/staticconfig/testdata/alias-for-generic.md +++ /dev/null @@ -1,23 +0,0 @@ -# Generic application instantiated via an alias - -This test ensures that the static analyzer finds an instantiated generic type -that implements the `dogma.Application` interface. - -```au:output au:group=matrix -valid application github.com/dogmatiq/enginekit/config/staticconfig.Alias (value unavailable) - - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 -``` - -```go au:input au:group=matrix -package app - -import "github.com/dogmatiq/dogma" - -type App[T any] struct{} - -func (App[T]) Configure(c dogma.ApplicationConfigurer) { - c.Identity("app", "8a6baab1-ee64-402e-a081-e43f4bebc243") -} - -type Alias = App[int] -``` diff --git a/config/staticconfig/testdata/generic-app.md b/config/staticconfig/testdata/generic-app.md new file mode 100644 index 00000000..65b93fea --- /dev/null +++ b/config/staticconfig/testdata/generic-app.md @@ -0,0 +1,43 @@ +# Generic application + +This test ensures that the static analyzer finds an instantiated generic type +that implements the `dogma.Application` interface. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - valid identity app/8a6baab1-ee64-402e-a081-e43f4bebc243 +``` + +## Instatiated using a type alias + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" + +type App = AppImpl[int] + +type AppImpl[T any] struct{} + +func (AppImpl[T]) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "8a6baab1-ee64-402e-a081-e43f4bebc243") +} +``` + +## Instatiated by embedding in a named struct + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" + +type App struct { + AppImpl[int] +} + +type AppImpl[T any] struct{} + +func (AppImpl[T]) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "8a6baab1-ee64-402e-a081-e43f4bebc243") +} +``` diff --git a/config/staticconfig/testdata/generic-handler-via-alias.md b/config/staticconfig/testdata/generic-handler-via-alias.md new file mode 100644 index 00000000..b9826cd7 --- /dev/null +++ b/config/staticconfig/testdata/generic-handler-via-alias.md @@ -0,0 +1,38 @@ +# Generic handler via alias + +This test ensures that the static analyzer can analyze handlers that are +implemented using generic types when registered using an alias. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - valid identity app/3109677f-5ed5-4a30-86a1-9975273c5a38 + - valid integration github.com/dogmatiq/enginekit/config/staticconfig.Alias (value unavailable) + - valid identity handler/40393d25-f95a-46ea-8702-068643c20ed6 + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) +``` + +```go au:input au:group=matrix +package app + +import "context" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Alias = Integration[stubs.CommandStub[stubs.TypeA]] + +type Integration[T dogma.Command] struct{} + +func (Integration[T]) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "40393d25-f95a-46ea-8702-068643c20ed6") + c.Routes(dogma.HandlesCommand[T]()) +} + +func (Integration[T]) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "3109677f-5ed5-4a30-86a1-9975273c5a38") + c.RegisterIntegration(Alias{}) +} +``` diff --git a/config/staticconfig/testdata/generic-handler.md b/config/staticconfig/testdata/generic-handler.md new file mode 100644 index 00000000..1243d4dc --- /dev/null +++ b/config/staticconfig/testdata/generic-handler.md @@ -0,0 +1,36 @@ +# Generic handler + +This test ensures that the static analyzer can analyze handlers that are +implemented using generic types. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - valid identity app/3109677f-5ed5-4a30-86a1-9975273c5a38 + - valid integration github.com/dogmatiq/enginekit/config/staticconfig.Integration[github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA]] (value unavailable) + - valid identity handler/40393d25-f95a-46ea-8702-068643c20ed6 + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) +``` + +```go au:input au:group=matrix +package app + +import "context" +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Integration[T dogma.Command] struct{} + +func (Integration[T]) Configure(c dogma.IntegrationConfigurer) { + c.Identity("handler", "40393d25-f95a-46ea-8702-068643c20ed6") + c.Routes(dogma.HandlesCommand[T]()) +} + +func (Integration[T]) HandleCommand(context.Context, dogma.IntegrationCommandScope, dogma.Command) error { return nil } + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "3109677f-5ed5-4a30-86a1-9975273c5a38") + c.RegisterIntegration(Integration[stubs.CommandStub[stubs.TypeA]]{}) +} +``` From 6e39eb55ba2ae87c3062d7ecbab7ec9ef63a009b Mon Sep 17 00:00:00 2001 From: James Harris Date: Wed, 6 Nov 2024 10:36:34 +1000 Subject: [PATCH 36/38] Add tests for multiple handlers of the same type. --- .../_pending/multiple-handlers-of-a-kind.md | 94 ------------------- .../testdata/multiple-handlers.md | 54 +++++++++++ 2 files changed, 54 insertions(+), 94 deletions(-) delete mode 100644 config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md create mode 100644 config/staticconfig/testdata/multiple-handlers.md diff --git a/config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md b/config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md deleted file mode 100644 index 61165024..00000000 --- a/config/staticconfig/testdata/_pending/multiple-handlers-of-a-kind.md +++ /dev/null @@ -1,94 +0,0 @@ -# Multiple handlers of a same kind - -This test verifies that static analysis can correctly parse multiple handlers of -a same kind. - -```go au:input au:group=matrix -package app - -import ( - "context" - . "github.com/dogmatiq/dogma" - "github.com/dogmatiq/enginekit/enginetest/stubs" -) - -// App implements Application interface. -type App struct{} - -// Configure configures the behavior of the engine as it relates to this -// application. -func (App) Configure(c ApplicationConfigurer) { - c.Identity("", "8961f548-1afc-4996-894c-956835c83199") - - c.RegisterIntegration(FirstIntegrationHandler{}) - c.RegisterIntegration(SecondIntegrationHandler{}) -} - -// FirstIntegrationHandler is a test implementation of -// IntegrationMessageHandler. -type FirstIntegrationHandler struct{} - -// Configure configures the behavior of the engine as it relates to this -// handler. -func (FirstIntegrationHandler) Configure(c IntegrationConfigurer) { - c.Identity("", "14cf2812-eead-43b3-9c9c-10db5b469e94") - - c.Routes( - HandlesCommand[stubs.CommandStub[stubs.TypeC]](), - ) -} - -// RouteCommandToInstance returns the ID of the integration instance that is -// targetted by m. -func (FirstIntegrationHandler) RouteCommandToInstance(Command) string { - return "" -} - -// HandleCommand handles a command message that has been routed to this handler. -func (FirstIntegrationHandler) HandleCommand( - context.Context, - IntegrationCommandScope, - Command, -) error { - return nil -} - -// SecondIntegrationHandler is a test implementation of -// IntegrationMessageHandler. -type SecondIntegrationHandler struct{} - -// Configure configures the behavior of the engine as it relates to this -// handler. -func (SecondIntegrationHandler) Configure(c IntegrationConfigurer) { - c.Identity("", "6bed3fbc-30e2-44c7-9a5b-e440ffe370d9") - - c.Routes( - HandlesCommand[stubs.CommandStub[stubs.TypeD]](), - ) -} - -// RouteCommandToInstance returns the ID of the integration instance that is -// targetted by m. -func (SecondIntegrationHandler) RouteCommandToInstance(Command) string { - return "" -} - -// HandleCommand handles a command message that has been routed to this handler. -func (SecondIntegrationHandler) HandleCommand( - context.Context, - IntegrationCommandScope, - Command, -) error { - return nil -} -``` - -```au:output au:group=matrix -application (8961f548-1afc-4996-894c-956835c83199) App - - - integration (14cf2812-eead-43b3-9c9c-10db5b469e94) FirstIntegrationHandler - handles CommandStub[TypeC]? - - - integration (6bed3fbc-30e2-44c7-9a5b-e440ffe370d9) SecondIntegrationHandler - handles CommandStub[TypeD]? -``` diff --git a/config/staticconfig/testdata/multiple-handlers.md b/config/staticconfig/testdata/multiple-handlers.md new file mode 100644 index 00000000..150f7255 --- /dev/null +++ b/config/staticconfig/testdata/multiple-handlers.md @@ -0,0 +1,54 @@ +# Multiple handlers of the same type + +This test ensures that the static analyzer supports multiple handlers of the +same handler type. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - valid identity app/c0d4a0fc-2075-4a41-a7bf-7d1870dc0de9 + - valid aggregate github.com/dogmatiq/enginekit/config/staticconfig.One (value unavailable) + - valid identity one/62e0efa9-c5a0-4b5c-a237-9b51533a6963 + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) + - valid aggregate github.com/dogmatiq/enginekit/config/staticconfig.Two (value unavailable) + - valid identity two/0c3e2f49-acd0-4d82-800d-5d6d839535de + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeB] (type unavailable) + - valid records-event route for github.com/dogmatiq/enginekit/enginetest/stubs.EventStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeB] (type unavailable) +``` + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type ( + One struct { dogma.AggregateMessageHandler } + Two struct { dogma.AggregateMessageHandler } +) + +func (One) Configure(c dogma.AggregateConfigurer) { + c.Identity("one", "62e0efa9-c5a0-4b5c-a237-9b51533a6963") + c.Routes( + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]](), + dogma.RecordsEvent[stubs.EventStub[stubs.TypeA]](), + ) +} + + +func (Two) Configure(c dogma.AggregateConfigurer) { + c.Identity("two", "0c3e2f49-acd0-4d82-800d-5d6d839535de") + c.Routes( + dogma.HandlesCommand[stubs.CommandStub[stubs.TypeB]](), + dogma.RecordsEvent[stubs.EventStub[stubs.TypeB]](), + ) +} + +type App struct{} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "c0d4a0fc-2075-4a41-a7bf-7d1870dc0de9") + c.RegisterAggregate(One{}) + c.RegisterAggregate(Two{}) +} +``` From 7bc843406000a3a7bf7e8c6102247b508708f0ee Mon Sep 17 00:00:00 2001 From: James Harris Date: Wed, 6 Nov 2024 11:17:19 +1000 Subject: [PATCH 37/38] Add tests for "generic adaptors". --- config/staticconfig/internal/ssax/type.go | 17 +++++ .../testdata/_pending/generic-handler.md | 69 ------------------- .../conditional-excluded-by-const-expr.md | 6 +- .../conditional-included-by-const-expr.md | 8 +-- .../testdata/conditional-present-in-method.md | 10 +-- config/staticconfig/testdata/conditional.md | 16 ++--- .../testdata/handler-adaptor-generic.md | 42 +++++++++++ .../identity-from-non-const-static.md | 6 +- .../testdata/identity-from-non-const.md | 2 +- config/staticconfig/testdata/indirect.md | 10 +-- config/staticconfig/testdata/no-apps.md | 2 +- 11 files changed, 89 insertions(+), 99 deletions(-) delete mode 100644 config/staticconfig/testdata/_pending/generic-handler.md create mode 100644 config/staticconfig/testdata/handler-adaptor-generic.md diff --git a/config/staticconfig/internal/ssax/type.go b/config/staticconfig/internal/ssax/type.go index 792f9141..bedf8a71 100644 --- a/config/staticconfig/internal/ssax/type.go +++ b/config/staticconfig/internal/ssax/type.go @@ -80,12 +80,28 @@ func ConcreteType(v ssa.Value) optional.Optional[types.Type] { case *ssa.BinOp: case *ssa.Builtin: case *ssa.Call: + call := v.Common() + r := call.Signature().Results() + + if r.Len() != 1 { + return optional.None[types.Type]() + } + + t := r.At(0).Type() + + if IsAbstract(t) { + return optional.None[types.Type]() + } + + return optional.Some(t) + case *ssa.ChangeInterface: case *ssa.ChangeType: case *ssa.Const: // We made it past the IsAbstract() check so we know this is a constant // nil value for an interface, and hence no type information is present. return optional.None[types.Type]() + case *ssa.Convert: case *ssa.Extract: case *ssa.Field: @@ -100,6 +116,7 @@ func ConcreteType(v ssa.Value) optional.Optional[types.Type] { case *ssa.MakeClosure: case *ssa.MakeInterface: return ConcreteType(v.X) + case *ssa.MakeMap: case *ssa.MakeSlice: case *ssa.MultiConvert: diff --git a/config/staticconfig/testdata/_pending/generic-handler.md b/config/staticconfig/testdata/_pending/generic-handler.md deleted file mode 100644 index 291dfa1f..00000000 --- a/config/staticconfig/testdata/_pending/generic-handler.md +++ /dev/null @@ -1,69 +0,0 @@ -# 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 au:group=matrix -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 au:group=matrix -application (e522c782-48d2-4c47-a4c9-81e0d7cdeba0) App - - - integration (abc7c329-c9da-4161-a8e2-6ab45be2dd83) *InstantiatedIntegration - handles CommandStub[TypeA]? -``` diff --git a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md index 45f9f944..149c8cb4 100644 --- a/config/staticconfig/testdata/conditional-excluded-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-excluded-by-const-expr.md @@ -17,7 +17,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if true { return } @@ -35,7 +35,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if false { c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") } @@ -51,7 +51,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { panic("prevent defer") defer c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") } diff --git a/config/staticconfig/testdata/conditional-included-by-const-expr.md b/config/staticconfig/testdata/conditional-included-by-const-expr.md index 06211e7e..209402dc 100644 --- a/config/staticconfig/testdata/conditional-included-by-const-expr.md +++ b/config/staticconfig/testdata/conditional-included-by-const-expr.md @@ -18,7 +18,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if false { return } @@ -36,7 +36,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if true { c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") } @@ -52,7 +52,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if true { defer c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") } @@ -68,7 +68,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if cond() { c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") } diff --git a/config/staticconfig/testdata/conditional-present-in-method.md b/config/staticconfig/testdata/conditional-present-in-method.md index f3ffeacc..cfc36d0b 100644 --- a/config/staticconfig/testdata/conditional-present-in-method.md +++ b/config/staticconfig/testdata/conditional-present-in-method.md @@ -19,7 +19,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if rand.Int() == 0 { } @@ -37,7 +37,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if rand.Int() == 0 { } else { } @@ -56,7 +56,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { switch rand.Int() { case 0: } @@ -75,7 +75,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { for range rand.Int() { } @@ -94,7 +94,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { select { case <-time.After(time.Duration(rand.Int())): default: diff --git a/config/staticconfig/testdata/conditional.md b/config/staticconfig/testdata/conditional.md index dd0ebac2..eb74f120 100644 --- a/config/staticconfig/testdata/conditional.md +++ b/config/staticconfig/testdata/conditional.md @@ -18,7 +18,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if rand.Int() == 0 { c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") } @@ -35,7 +35,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if rand.Int() == 0 { } else { c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") @@ -53,7 +53,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if rand.Int() == 0 { return } @@ -72,7 +72,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if rand.Int() == 0 { panic("oh no") } @@ -91,7 +91,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { switch rand.Int() { case 0: c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") @@ -109,7 +109,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { for range rand.Int() { c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") } @@ -127,7 +127,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { select { case <-time.After(time.Duration(rand.Int())): c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") @@ -146,7 +146,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { if rand.Int() == 0 { defer c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") } diff --git a/config/staticconfig/testdata/handler-adaptor-generic.md b/config/staticconfig/testdata/handler-adaptor-generic.md new file mode 100644 index 00000000..8b4f3f51 --- /dev/null +++ b/config/staticconfig/testdata/handler-adaptor-generic.md @@ -0,0 +1,42 @@ +# Generic handler adaptor + +This test ensures that the static analyzer can analyze configuration logic that +is implemented within a type that is passed to a handler as a type parameter. + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - valid identity app/24c4a011-3d9e-493a-95c6-ef9ab059f65f + - valid integration github.com/dogmatiq/enginekit/config/staticconfig.Adaptor[github.com/dogmatiq/enginekit/config/staticconfig.impl] (value unavailable) + - valid identity integration/a57834ad-251a-4672-9b82-f2a538a64655 + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) +``` + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +type Adaptor[T interface { Configure(dogma.IntegrationConfigurer) }] struct { + dogma.IntegrationMessageHandler + Impl T +} + +func (a Adaptor[T]) Configure(c dogma.IntegrationConfigurer) { + a.Impl.Configure(c) +} + +type impl struct {} + +func (impl) Configure(c dogma.IntegrationConfigurer) { + c.Identity("integration", "a57834ad-251a-4672-9b82-f2a538a64655") + c.Routes(dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]]()) +} + +type App struct {} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "24c4a011-3d9e-493a-95c6-ef9ab059f65f") + c.RegisterIntegration(Adaptor[impl]{}) +} +``` diff --git a/config/staticconfig/testdata/identity-from-non-const-static.md b/config/staticconfig/testdata/identity-from-non-const-static.md index 84c2648a..e248fa0d 100644 --- a/config/staticconfig/testdata/identity-from-non-const-static.md +++ b/config/staticconfig/testdata/identity-from-non-const-static.md @@ -18,7 +18,7 @@ import "github.com/dogmatiq/dogma" type App struct { } -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { c.Identity(name(), "a0a0edb7-ce45-4eb4-940c-0f77459ae2a0") } @@ -37,7 +37,7 @@ import "github.com/dogmatiq/dogma" type App struct { } -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { c.Identity(ident()) } @@ -56,7 +56,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { c.Identity(ident()) } diff --git a/config/staticconfig/testdata/identity-from-non-const.md b/config/staticconfig/testdata/identity-from-non-const.md index 3a2f80f5..47ebd144 100644 --- a/config/staticconfig/testdata/identity-from-non-const.md +++ b/config/staticconfig/testdata/identity-from-non-const.md @@ -54,7 +54,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { c.Identity(ident()) } diff --git a/config/staticconfig/testdata/indirect.md b/config/staticconfig/testdata/indirect.md index 2425f3b0..e37acce2 100644 --- a/config/staticconfig/testdata/indirect.md +++ b/config/staticconfig/testdata/indirect.md @@ -22,7 +22,7 @@ func (a App) Configure(c dogma.ApplicationConfigurer) { a.setup(c) } -func (a App) setup(c dogma.ApplicationConfigurer) { +func (App) setup(c dogma.ApplicationConfigurer) { c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") } ``` @@ -36,7 +36,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { setup(c) } @@ -54,7 +54,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { setup(c) } @@ -72,7 +72,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { defer c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") } ``` @@ -90,7 +90,7 @@ import "github.com/dogmatiq/dogma" type App struct {} -func (a App) Configure(c dogma.ApplicationConfigurer) { +func (App) Configure(c dogma.ApplicationConfigurer) { go c.Identity("app", "de142370-93ee-409c-9336-5084d9c5e285") } ``` diff --git a/config/staticconfig/testdata/no-apps.md b/config/staticconfig/testdata/no-apps.md index 2d7cd2d0..d5bd9014 100644 --- a/config/staticconfig/testdata/no-apps.md +++ b/config/staticconfig/testdata/no-apps.md @@ -26,7 +26,7 @@ import _ "github.com/dogmatiq/dogma" // it's compatible. type App struct{} -func (a App) Configure(c ApplicationConfigurer) { +func (App) Configure(c ApplicationConfigurer) { c.Identity("name", "ee6ca834-34a3-4e59-8c36-7aeb796401d7") } From db8e5a1f0430ea2e5f84239f13cf96636a76296a Mon Sep 17 00:00:00 2001 From: James Harris Date: Wed, 6 Nov 2024 11:17:22 +1000 Subject: [PATCH 38/38] WIP [ci skip] --- .../testdata/_handler-adaptor-constructor.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 config/staticconfig/testdata/_handler-adaptor-constructor.md diff --git a/config/staticconfig/testdata/_handler-adaptor-constructor.md b/config/staticconfig/testdata/_handler-adaptor-constructor.md new file mode 100644 index 00000000..53d230e1 --- /dev/null +++ b/config/staticconfig/testdata/_handler-adaptor-constructor.md @@ -0,0 +1,43 @@ +# Handler adaptor function + +This test ensures that the static analyzer can analyze configuration logic that +is implemented within a type that has a `Configure()` method with the same +signature as the handler interface. + +This is a naive implementation that was originalled added to allow analysis of +the various adaptors in the [projectionkit] module. + +[projectionkit]: https://github.com/dogmatiq/projectionkit + +```au:output au:group=matrix +valid application github.com/dogmatiq/enginekit/config/staticconfig.App (value unavailable) + - valid identity app/24c4a011-3d9e-493a-95c6-ef9ab059f65f + - valid integration github.com/dogmatiq/enginekit/config/staticconfig.Adaptor[github.com/dogmatiq/enginekit/config/staticconfig.impl] (value unavailable) + - valid identity integration/a57834ad-251a-4672-9b82-f2a538a64655 + - valid handles-command route for github.com/dogmatiq/enginekit/enginetest/stubs.CommandStub[github.com/dogmatiq/enginekit/enginetest/stubs.TypeA] (type unavailable) +``` + +```go au:input au:group=matrix +package app + +import "github.com/dogmatiq/dogma" +import "github.com/dogmatiq/enginekit/enginetest/stubs" + +func NewAdaptor(any) dogma.IntegrationMessageHandler { + panic("not implemented") +} + +type impl struct {} + +func (impl) Configure(c dogma.IntegrationConfigurer) { + c.Identity("integration", "a57834ad-251a-4672-9b82-f2a538a64655") + c.Routes(dogma.HandlesCommand[stubs.CommandStub[stubs.TypeA]]()) +} + +type App struct {} + +func (App) Configure(c dogma.ApplicationConfigurer) { + c.Identity("app", "24c4a011-3d9e-493a-95c6-ef9ab059f65f") + c.RegisterIntegration(NewAdaptor(impl{})) +} +```