From e0e5a12dbf265a8da2666e398be4ce75f3836448 Mon Sep 17 00:00:00 2001 From: Elizabeth Worstell Date: Fri, 7 Jun 2024 17:12:47 -0700 Subject: [PATCH] chore: migrate verb, data schema parsing restructuring and adding some additional passes to provide data for the extractors- 1. metadata pass parses all comments in the packages and provides results via facts 2. transitive pass extracts nodes marked as needing extraction via transitive reference from other decls --- .../cronjobs/testdata/go/cron/go.sum | 4 +- backend/controller/dal/testdata/go/fsm/go.sum | 4 +- .../dal/testdata/go/fsmretry/go.sum | 4 +- .../ingress/testdata/go/httpingress/go.sum | 4 +- .../leases/testdata/go/leases/go.sum | 4 +- .../pubsub/testdata/go/publisher/go.sum | 4 +- .../pubsub/testdata/go/subscriber/go.sum | 4 +- .../sql/testdata/go/database/go.sum | 4 +- buildengine/testdata/projects/alpha/go.sum | 4 +- buildengine/testdata/projects/another/go.sum | 4 +- .../projects/external/.ftl-build-lock | 1 + buildengine/testdata/projects/other/go.sum | 4 +- common/projectconfig/testdata/go/echo/go.sum | 4 +- .../testdata/go/findconfig/go.sum | 4 +- examples/go/echo/go.sum | 4 +- .../build-template/_ftl.tmpl/go/main/main.go | 8 +- go-runtime/compile/build.go | 35 +- go-runtime/compile/schema.go | 232 +-------- go-runtime/compile/schema_test.go | 39 +- .../compile/testdata/failing/failing.go | 5 + go-runtime/compile/testdata/failing/go.sum | 4 +- go-runtime/compile/testdata/fsm/go.sum | 4 +- go-runtime/compile/testdata/go/echo/go.sum | 4 +- .../testdata/go/notexportedverb/go.sum | 4 +- .../compile/testdata/go/undefinedverb/go.sum | 4 +- go-runtime/compile/testdata/one/go.sum | 4 +- .../compile/testdata/parent/child/child.go | 10 + go-runtime/compile/testdata/parent/go.sum | 4 +- go-runtime/compile/testdata/pubsub/go.sum | 4 +- go-runtime/compile/testdata/subscriber/go.sum | 4 +- go-runtime/compile/testdata/two/go.sum | 4 +- go-runtime/compile/testdata/validation/go.sum | 4 +- .../encoding/testdata/go/omitempty/go.sum | 4 +- .../ftl/ftltest/testdata/go/pubsub/go.sum | 4 +- .../ftl/ftltest/testdata/go/subscriber/go.sum | 4 +- .../ftl/ftltest/testdata/go/verbtypes/go.sum | 4 +- .../ftl/ftltest/testdata/go/wrapped/go.sum | 4 +- .../testdata/go/runtimereflection/go.sum | 4 +- go-runtime/ftl/testdata/go/echo/go.sum | 4 +- go-runtime/ftl/testdata/go/mapper/go.sum | 4 +- go-runtime/internal/testdata/go/mapper/go.sum | 4 +- go-runtime/schema/analyzers/common.go | 433 ---------------- go-runtime/schema/analyzers/finalize.go | 114 ----- go-runtime/schema/analyzers/parser.go | 218 -------- go-runtime/schema/analyzers/typealias.go | 85 ---- go-runtime/schema/common/common.go | 469 ++++++++++++++++++ go-runtime/schema/common/directive.go | 316 ++++++++++++ go-runtime/schema/common/error.go | 72 +++ go-runtime/schema/common/fact.go | 196 ++++++++ go-runtime/schema/data/analyzer.go | 128 +++++ go-runtime/schema/extract.go | 191 +++++++ go-runtime/schema/extractor.go | 72 --- go-runtime/schema/finalize/analyzer.go | 133 +++++ go-runtime/schema/initialize/analyzer.go | 82 +++ go-runtime/schema/metadata/analyzer.go | 188 +++++++ go-runtime/schema/transitive/analyzer.go | 94 ++++ go-runtime/schema/typealias/analyzer.go | 46 ++ go-runtime/schema/verb/analyzer.go | 117 +++++ go.mod | 7 +- go.sum | 12 +- 60 files changed, 2196 insertions(+), 1243 deletions(-) create mode 100644 buildengine/testdata/projects/external/.ftl-build-lock delete mode 100644 go-runtime/schema/analyzers/common.go delete mode 100644 go-runtime/schema/analyzers/finalize.go delete mode 100644 go-runtime/schema/analyzers/parser.go delete mode 100644 go-runtime/schema/analyzers/typealias.go create mode 100644 go-runtime/schema/common/common.go create mode 100644 go-runtime/schema/common/directive.go create mode 100644 go-runtime/schema/common/error.go create mode 100644 go-runtime/schema/common/fact.go create mode 100644 go-runtime/schema/data/analyzer.go create mode 100644 go-runtime/schema/extract.go delete mode 100644 go-runtime/schema/extractor.go create mode 100644 go-runtime/schema/finalize/analyzer.go create mode 100644 go-runtime/schema/initialize/analyzer.go create mode 100644 go-runtime/schema/metadata/analyzer.go create mode 100644 go-runtime/schema/transitive/analyzer.go create mode 100644 go-runtime/schema/typealias/analyzer.go create mode 100644 go-runtime/schema/verb/analyzer.go diff --git a/backend/controller/cronjobs/testdata/go/cron/go.sum b/backend/controller/cronjobs/testdata/go/cron/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/backend/controller/cronjobs/testdata/go/cron/go.sum +++ b/backend/controller/cronjobs/testdata/go/cron/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/backend/controller/dal/testdata/go/fsm/go.sum b/backend/controller/dal/testdata/go/fsm/go.sum index 8839f9fb5e..475f511040 100644 --- a/backend/controller/dal/testdata/go/fsm/go.sum +++ b/backend/controller/dal/testdata/go/fsm/go.sum @@ -121,8 +121,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/backend/controller/dal/testdata/go/fsmretry/go.sum b/backend/controller/dal/testdata/go/fsmretry/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/backend/controller/dal/testdata/go/fsmretry/go.sum +++ b/backend/controller/dal/testdata/go/fsmretry/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/backend/controller/ingress/testdata/go/httpingress/go.sum b/backend/controller/ingress/testdata/go/httpingress/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/backend/controller/ingress/testdata/go/httpingress/go.sum +++ b/backend/controller/ingress/testdata/go/httpingress/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/backend/controller/leases/testdata/go/leases/go.sum b/backend/controller/leases/testdata/go/leases/go.sum index 8839f9fb5e..475f511040 100644 --- a/backend/controller/leases/testdata/go/leases/go.sum +++ b/backend/controller/leases/testdata/go/leases/go.sum @@ -121,8 +121,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/backend/controller/pubsub/testdata/go/publisher/go.sum b/backend/controller/pubsub/testdata/go/publisher/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/backend/controller/pubsub/testdata/go/publisher/go.sum +++ b/backend/controller/pubsub/testdata/go/publisher/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/backend/controller/pubsub/testdata/go/subscriber/go.sum b/backend/controller/pubsub/testdata/go/subscriber/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/backend/controller/pubsub/testdata/go/subscriber/go.sum +++ b/backend/controller/pubsub/testdata/go/subscriber/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/backend/controller/sql/testdata/go/database/go.sum b/backend/controller/sql/testdata/go/database/go.sum index 8839f9fb5e..475f511040 100644 --- a/backend/controller/sql/testdata/go/database/go.sum +++ b/backend/controller/sql/testdata/go/database/go.sum @@ -121,8 +121,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/buildengine/testdata/projects/alpha/go.sum b/buildengine/testdata/projects/alpha/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/buildengine/testdata/projects/alpha/go.sum +++ b/buildengine/testdata/projects/alpha/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/buildengine/testdata/projects/another/go.sum b/buildengine/testdata/projects/another/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/buildengine/testdata/projects/another/go.sum +++ b/buildengine/testdata/projects/another/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/buildengine/testdata/projects/external/.ftl-build-lock b/buildengine/testdata/projects/external/.ftl-build-lock new file mode 100644 index 0000000000..244d44946e --- /dev/null +++ b/buildengine/testdata/projects/external/.ftl-build-lock @@ -0,0 +1 @@ +64666 \ No newline at end of file diff --git a/buildengine/testdata/projects/other/go.sum b/buildengine/testdata/projects/other/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/buildengine/testdata/projects/other/go.sum +++ b/buildengine/testdata/projects/other/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/common/projectconfig/testdata/go/echo/go.sum b/common/projectconfig/testdata/go/echo/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/common/projectconfig/testdata/go/echo/go.sum +++ b/common/projectconfig/testdata/go/echo/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/common/projectconfig/testdata/go/findconfig/go.sum b/common/projectconfig/testdata/go/findconfig/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/common/projectconfig/testdata/go/findconfig/go.sum +++ b/common/projectconfig/testdata/go/findconfig/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/examples/go/echo/go.sum b/examples/go/echo/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/examples/go/echo/go.sum +++ b/examples/go/echo/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/compile/build-template/_ftl.tmpl/go/main/main.go b/go-runtime/compile/build-template/_ftl.tmpl/go/main/main.go index d2ae719022..f61d399600 100644 --- a/go-runtime/compile/build-template/_ftl.tmpl/go/main/main.go +++ b/go-runtime/compile/build-template/_ftl.tmpl/go/main/main.go @@ -33,13 +33,13 @@ func main() { verbConstructor := server.NewUserVerbServer("{{.Name}}", {{- range .Verbs}} {{- if and .HasRequest .HasResponse}} - server.HandleCall({{$.Name}}.{{.Name}}), + server.HandleCall({{.Package}}.{{.Name}}), {{- else if .HasRequest}} - server.HandleSink({{$.Name}}.{{.Name}}), + server.HandleSink({{.Package}}.{{.Name}}), {{- else if .HasResponse}} - server.HandleSource({{$.Name}}.{{.Name}}), + server.HandleSource({{.Package}}.{{.Name}}), {{- else}} - server.HandleEmpty({{$.Name}}.{{.Name}}), + server.HandleEmpty({{.Package}}.{{.Name}}), {{- end}} {{- end}} ) diff --git a/go-runtime/compile/build.go b/go-runtime/compile/build.go index 0a98a235f9..ef81eec123 100644 --- a/go-runtime/compile/build.go +++ b/go-runtime/compile/build.go @@ -14,7 +14,7 @@ import ( "strings" "unicode" - "github.com/TBD54566975/ftl/go-runtime/schema/analyzers" + "github.com/TBD54566975/ftl/go-runtime/schema/finalize" sets "github.com/deckarep/golang-set/v2" gomaps "golang.org/x/exp/maps" "golang.org/x/mod/modfile" @@ -45,6 +45,8 @@ type ExternalModuleContext struct { type goVerb struct { Name string + Package string + MustImport string HasRequest bool HasResponse bool } @@ -169,7 +171,10 @@ func Build(ctx context.Context, moduleDir string, sch *schema.Schema, filesTrans return fmt.Errorf("missing native name for verb %s", verb.Name) } - goverb := goVerb{Name: nativeName} + goverb, err := goVerbFromQualifiedName(nativeName) + if err != nil { + return err + } if _, ok := verb.Request.(*schema.Unit); !ok { goverb.HasRequest = true } @@ -330,6 +335,9 @@ var scaffoldFuncs = scaffolder.FuncMap{ if len(ctx.Verbs) > 0 { imports.Add(ctx.Name) } + for _, v := range ctx.Verbs { + imports.Add(strings.TrimPrefix(v.MustImport, "ftl/")) + } for _, st := range ctx.SumTypes { if i := strings.LastIndex(st.Discriminator, "."); i != -1 { imports.Add(st.Discriminator[:i]) @@ -612,22 +620,22 @@ func getExternalTypeEnums(module *schema.Module, sch *schema.Schema) []externalE // // TODO: once migrated off of the legacy extractor, we can inline `extract.Extract(dir)` and delete this // function -func ExtractModuleSchema(dir string, sch *schema.Schema) (analyzers.ExtractResult, error) { +func ExtractModuleSchema(dir string, sch *schema.Schema) (finalize.Result, error) { result, err := extract.Extract(dir) if err != nil { - return analyzers.ExtractResult{}, err + return finalize.Result{}, err } // merge with legacy results for now if err = legacyExtractModuleSchema(dir, sch, &result); err != nil { - return analyzers.ExtractResult{}, err + return finalize.Result{}, err } schema.SortErrorsByPosition(result.Errors) if !schema.ContainsTerminalError(result.Errors) { err = schema.ValidateModule(result.Module) if err != nil { - return analyzers.ExtractResult{}, err + return finalize.Result{}, err } } updateVisibility(result.Module) @@ -678,3 +686,18 @@ func updateTransitiveVisibility(d schema.Decl, module *schema.Module) { return next() }) } + +func goVerbFromQualifiedName(qualifiedName string) (goVerb, error) { + lastDotIndex := strings.LastIndex(qualifiedName, ".") + if lastDotIndex == -1 { + return goVerb{}, fmt.Errorf("invalid qualified type format %q", qualifiedName) + } + pkgPath := qualifiedName[:lastDotIndex] + typeName := qualifiedName[lastDotIndex+1:] + pkgName := path.Base(pkgPath) + return goVerb{ + Name: typeName, + Package: pkgName, + MustImport: pkgPath, + }, nil +} diff --git a/go-runtime/compile/schema.go b/go-runtime/compile/schema.go index 00d0872b19..951bafa372 100644 --- a/go-runtime/compile/schema.go +++ b/go-runtime/compile/schema.go @@ -10,14 +10,13 @@ import ( "slices" "strconv" "strings" - "sync" "unicode" "unicode/utf8" "github.com/alecthomas/types/optional" "golang.org/x/exp/maps" - "github.com/TBD54566975/ftl/go-runtime/schema/analyzers" + "github.com/TBD54566975/ftl/go-runtime/schema/finalize" "github.com/TBD54566975/ftl/backend/schema" "github.com/TBD54566975/ftl/backend/schema/strcase" @@ -27,13 +26,7 @@ import ( ) var ( - fset = token.NewFileSet() - contextIfaceType = once(func() *types.Interface { - return mustLoadRef("context", "Context").Type().Underlying().(*types.Interface) //nolint:forcetypeassert - }) - errorIFaceType = once(func() *types.Interface { - return mustLoadRef("builtin", "error").Type().Underlying().(*types.Interface) //nolint:forcetypeassert - }) + fset = token.NewFileSet() ftlPkgPath = "github.com/TBD54566975/ftl/go-runtime/ftl" ftlCallFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Call" @@ -100,7 +93,7 @@ func (e errorSet) add(err *schema.Error) { e[err.Error()] = err } -func legacyExtractModuleSchema(dir string, sch *schema.Schema, out *analyzers.ExtractResult) error { +func legacyExtractModuleSchema(dir string, sch *schema.Schema, out *finalize.Result) error { pkgs, err := packages.Load(&packages.Config{ Dir: dir, Fset: fset, @@ -130,12 +123,6 @@ func legacyExtractModuleSchema(dir string, sch *schema.Schema, out *analyzers.Ex case *ast.CallExpr: visitCallExpr(pctx, node, stack) - case *ast.File: - visitFile(pctx, node) - - case *ast.FuncDecl: - visitFuncDecl(pctx, node) - case *ast.GenDecl: visitGenDecl(pctx, node) @@ -195,7 +182,6 @@ func extractTypeDecl(pctx *parseContext, node *ast.GenDecl) { return } - foundDeclType := optional.None[string]() for _, dir := range directives { if len(node.Specs) != 1 { // errors handled on next pass @@ -209,8 +195,7 @@ func extractTypeDecl(pctx *parseContext, node *ast.GenDecl) { aType := pctx.pkg.Types.Scope().Lookup(t.Name.Name) nativeName := aType.Pkg().Name() + "." + aType.Name() - switch dir := dir.(type) { - case *directiveEnum: + if ed, ok := dir.(*directiveEnum); ok { typ := pctx.pkg.TypesInfo.TypeOf(t.Type) switch underlying := typ.Underlying().(type) { case *types.Basic: @@ -219,7 +204,7 @@ func extractTypeDecl(pctx *parseContext, node *ast.GenDecl) { Comments: parseComments(node.Doc), Name: strcase.ToUpperCamel(t.Name.Name), Type: nil, // nil until next pass, when we can visit the full type graph - Export: dir.IsExported(), + Export: ed.IsExported(), } pctx.module.Decls = append(pctx.module.Decls, enum) pctx.nativeNames[enum] = nativeName @@ -245,7 +230,7 @@ func extractTypeDecl(pctx *parseContext, node *ast.GenDecl) { Pos: goPosToSchemaPos(node.Pos()), Comments: parseComments(node.Doc), Name: strcase.ToUpperCamel(t.Name.Name), - Export: dir.IsExported(), + Export: ed.IsExported(), } if iTyp, ok := typ.(*types.Interface); ok { pctx.nativeNames[enum] = nativeName @@ -255,15 +240,6 @@ func extractTypeDecl(pctx *parseContext, node *ast.GenDecl) { pctx.errors.add(errorf(node, "expected interface for type enum but got %q", typ)) } } - foundDeclType = optional.Some("enum") - case *directiveTypeAlias, *directiveData, *directiveIngress, *directiveVerb, *directiveCronJob, *directiveRetry, *directiveExport, *directiveSubscriber: - continue - } - if foundDeclType, ok := foundDeclType.Get(); ok { - if len(directives) > 1 { - pctx.errors.add(errorf(node, "only one directive expected for %v", foundDeclType)) - } - break } } } @@ -781,65 +757,6 @@ func commentsAndDirectivesForVar(pctx *parseContext, variableDecl *ast.GenDecl, return parseComments(variableDecl.Doc), directives } -func visitFile(pctx *parseContext, node *ast.File) { - if node.Doc == nil { - return - } - pctx.module.Comments = parseComments(node.Doc) -} - -func isType[T types.Type](t types.Type) bool { - if _, ok := t.(*types.Named); ok { - t = t.Underlying() - } - _, ok := t.(T) - return ok -} - -func checkSignature(pctx *parseContext, node *ast.FuncDecl, sig *types.Signature) (req, resp optional.Option[*types.Var]) { - params := sig.Params() - results := sig.Results() - - if params.Len() > 2 { - pctx.errors.add(errorf(node, "must have at most two parameters (context.Context, struct)")) - } - if params.Len() == 0 { - pctx.errors.add(errorf(node, "first parameter must be context.Context")) - } else if !types.AssertableTo(contextIfaceType(), params.At(0).Type()) { - pctx.errors.add(tokenErrorf(params.At(0).Pos(), params.At(0).Name(), "first parameter must be of type context.Context but is %s", params.At(0).Type())) - } - - if params.Len() == 2 { - if !isType[*types.Struct](params.At(1).Type()) { - pctx.errors.add(tokenErrorf(params.At(1).Pos(), params.At(1).Name(), "second parameter must be a struct but is %s", params.At(1).Type())) - } - if params.At(1).Type().String() == ftlUnitTypePath { - pctx.errors.add(tokenErrorf(params.At(1).Pos(), params.At(1).Name(), "second parameter must not be ftl.Unit")) - } - - req = optional.Some(params.At(1)) - } - - if results.Len() > 2 { - pctx.errors.add(errorf(node, "must have at most two results (struct, error)")) - } - if results.Len() == 0 { - pctx.errors.add(errorf(node, "must at least return an error")) - } else if !types.AssertableTo(errorIFaceType(), results.At(results.Len()-1).Type()) { - pctx.errors.add(tokenErrorf(results.At(results.Len()-1).Pos(), results.At(results.Len()-1).Name(), "must return an error but is %s", results.At(0).Type())) - } - if results.Len() == 2 { - if !isType[*types.Struct](results.At(0).Type()) { - pctx.errors.add(tokenErrorf(results.At(0).Pos(), results.At(0).Name(), "first result must be a struct but is %s", results.At(0).Type())) - } - if results.At(1).Type().String() == ftlUnitTypePath { - pctx.errors.add(tokenErrorf(results.At(1).Pos(), results.At(1).Name(), "second result must not be ftl.Unit")) - } - resp = optional.Some(results.At(0)) - } - return req, resp -} - func goPosToSchemaPos(pos token.Pos) schema.Position { p := fset.Position(pos) return schema.Position{Filename: p.Filename, Line: p.Line, Column: p.Column, Offset: p.Offset} @@ -864,8 +781,7 @@ func visitGenDecl(pctx *parseContext, node *ast.GenDecl) { } for _, dir := range directives { - switch dir.(type) { - case *directiveVerb, *directiveData, *directiveEnum: + if _, ok := dir.(*directiveEnum); ok { if len(node.Specs) != 1 { pctx.errors.add(errorf(node, "error parsing ftl directive: expected "+ "exactly one type declaration")) @@ -910,7 +826,6 @@ func visitGenDecl(pctx *parseContext, node *ast.GenDecl) { visitType(pctx, node.Pos(), pctx.pkg.TypesInfo.Defs[t.Name].Type(), isExported) } } - case *directiveIngress, *directiveCronJob, *directiveRetry, *directiveExport, *directiveSubscriber, *directiveTypeAlias: } } return @@ -1161,128 +1076,6 @@ func maybeErrorOnInvalidEnumMixing(pctx *parseContext, node *ast.ValueSpec, enum } } -func visitFuncDecl(pctx *parseContext, node *ast.FuncDecl) (verb *schema.Verb) { - if node.Doc == nil { - return nil - } - directives, err := parseDirectives(node, fset, node.Doc) - if err != nil { - pctx.errors.add(err) - } - var metadata []schema.Metadata - isVerb := false - isExported := false - for _, dir := range directives { - switch dir := dir.(type) { - case *directiveVerb: - isVerb = true - isExported = dir.Export - if pctx.module.Name == "" { - pctx.module.Name = pctx.pkg.Name - } else if pctx.module.Name != pctx.pkg.Name { - pctx.errors.add(errorf(node, "function verb directive must be in the module package")) - } - case *directiveIngress: - isVerb = true - isExported = true - typ := dir.Type - if typ == "" { - typ = "http" - } - metadata = append(metadata, &schema.MetadataIngress{ - Pos: dir.Pos, - Type: typ, - Method: dir.Method, - Path: dir.Path, - }) - case *directiveCronJob: - isVerb = true - isExported = false - metadata = append(metadata, &schema.MetadataCronJob{ - Pos: dir.Pos, - Cron: dir.Cron.String(), - }) - case *directiveRetry: - metadata = append(metadata, &schema.MetadataRetry{ - Pos: dir.Pos, - Count: dir.Count, - MinBackoff: dir.MinBackoff, - MaxBackoff: dir.MaxBackoff, - }) - case *directiveSubscriber: - isVerb = true - metadata = append(metadata, &schema.MetadataSubscriber{ - Pos: dir.Pos, - Name: dir.Name, - }) - case *directiveExport: - pctx.errors.add(unexpectedDirectiveErrorf(dir, "unexpected directive %q attached for verb, did you mean to use '//ftl:verb export' instead", dir)) - case *directiveData, *directiveEnum, *directiveTypeAlias: - pctx.errors.add(unexpectedDirectiveErrorf(dir, "unexpected directive %q attached for verb", dir)) - } - } - if !isVerb { - return nil - } - - if expName := exportedName(node.Name.Name); node.Name.Name != expName { - pctx.errors.add(errorf(node, "verb %q is not exported, did you mean to use %q instead?", node.Name.Name, expName)) - return nil - } - - for _, name := range pctx.nativeNames { - if name == node.Name.Name { - pctx.errors.add(noEndColumnErrorf(node.Pos(), "duplicate verb name %q", node.Name.Name)) - return nil - } - } - - fnt := pctx.pkg.TypesInfo.Defs[node.Name].(*types.Func) //nolint:forcetypeassert - sig := fnt.Type().(*types.Signature) //nolint:forcetypeassert - if sig.Recv() != nil { - pctx.errors.add(errorf(node, "ftl:verb cannot be a method")) - return nil - } - params := sig.Params() - results := sig.Results() - reqt, respt := checkSignature(pctx, node, sig) - - var req optional.Option[schema.Type] - if reqt.Ok() { - req = visitType(pctx, node.Pos(), params.At(1).Type(), isExported) - } else { - req = optional.Some[schema.Type](&schema.Unit{}) - } - var resp optional.Option[schema.Type] - if respt.Ok() { - resp = visitType(pctx, node.Pos(), results.At(0).Type(), isExported) - } else { - resp = optional.Some[schema.Type](&schema.Unit{}) - } - reqV, reqOk := req.Get() - resV, respOk := resp.Get() - if !reqOk { - pctx.errors.add(tokenErrorf(params.At(1).Pos(), params.At(1).Name(), - "unsupported request type %q", params.At(1).Type())) - } - if !respOk { - pctx.errors.add(tokenErrorf(results.At(0).Pos(), results.At(0).Name(), - "unsupported response type %q", results.At(0).Type())) - } - verb = &schema.Verb{ - Pos: goPosToSchemaPos(node.Pos()), - Comments: parseComments(node.Doc), - Export: isExported, - Name: strcase.ToLowerCamel(node.Name.Name), - Request: reqV, - Response: resV, - Metadata: metadata, - } - pctx.nativeNames[verb] = node.Name.Name - pctx.module.Decls = append(pctx.module.Decls, verb) - return verb -} - func parseComments(doc *ast.CommentGroup) []string { comments := []string{} if doc := doc.Text(); doc != "" { @@ -1624,15 +1417,6 @@ func visitSlice(pctx *parseContext, pos token.Pos, tnode *types.Slice, isExporte }) } -func once[T any](f func() T) func() T { - var once sync.Once - var t T - return func() T { - once.Do(func() { t = f() }) - return t - } -} - // Lazy load the compile-time reference from a package. func mustLoadRef(pkg, name string) types.Object { pkgs, err := packages.Load(&packages.Config{Fset: fset, Mode: packages.NeedTypes}, pkg) @@ -1683,7 +1467,7 @@ type parseContext struct { topicsByPos map[schema.Position]*schema.Topic } -func newParseContext(pkg *packages.Package, pkgs []*packages.Package, sch *schema.Schema, out *analyzers.ExtractResult) *parseContext { +func newParseContext(pkg *packages.Package, pkgs []*packages.Package, sch *schema.Schema, out *finalize.Result) *parseContext { if out.NativeNames == nil { out.NativeNames = NativeNames{} } diff --git a/go-runtime/compile/schema_test.go b/go-runtime/compile/schema_test.go index 8a37664db9..211301901f 100644 --- a/go-runtime/compile/schema_test.go +++ b/go-runtime/compile/schema_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - "github.com/TBD54566975/ftl/go-runtime/schema/analyzers" + "github.com/TBD54566975/ftl/go-runtime/schema/finalize" "github.com/TBD54566975/golang-tools/go/packages" "github.com/alecthomas/assert/v2" "github.com/alecthomas/participle/v2/lexer" @@ -73,7 +73,7 @@ func TestExtractModuleSchema(t *testing.T) { } // Comments about ColorInt. - export enum ColorInt: Int { + enum ColorInt: Int { // RedInt is a color. RedInt = 0 BlueInt = 1 @@ -100,7 +100,7 @@ func TestExtractModuleSchema(t *testing.T) { Two = 2 } - export enum TypeEnum { + enum TypeEnum { Option String? InlineStruct one.InlineStruct AliasedStruct one.UnderlyingStruct @@ -122,7 +122,7 @@ func TestExtractModuleSchema(t *testing.T) { export data ExportedStruct { } - export data InlineStruct { + data InlineStruct { } export data Nested { @@ -158,7 +158,7 @@ func TestExtractModuleSchema(t *testing.T) { data SourceResp { } - export data UnderlyingStruct { + data UnderlyingStruct { } data WithoutDirectiveStruct { @@ -343,10 +343,17 @@ func TestExtractModuleSchemaParent(t *testing.T) { assert.Equal(t, nil, r.Errors, "expected no schema errors") actual := schema.Normalise(r.Module) expected := `module parent { - export data ChildStruct { - name String? + export typealias ChildAlias String + + export data ChildStruct { + name parent.ChildAlias? + } + + data Resp { } + verb childVerb(Unit) parent.Resp + export verb verb(Unit) parent.ChildStruct } ` @@ -455,7 +462,7 @@ func TestParsedirectives(t *testing.T) { func TestParseTypesTime(t *testing.T) { timeRef := mustLoadRef("time", "Time").Type() - pctx := newParseContext(nil, []*packages.Package{}, &schema.Schema{}, &analyzers.ExtractResult{Module: &schema.Module{}}) + pctx := newParseContext(nil, []*packages.Package{}, &schema.Schema{}, &finalize.Result{Module: &schema.Module{}}) parsed, ok := visitType(pctx, token.NoPos, timeRef, false).Get() assert.True(t, ok) _, ok = parsed.(*schema.Time) @@ -510,7 +517,7 @@ func TestErrorReporting(t *testing.T) { `21:14-44: duplicate database declaration at 20:14-44`, `24:2-10: unsupported type "error" for field "BadParam"`, `27:2-17: unsupported type "uint64" for field "AnotherBadParam"`, - `30:3-0: unexpected directive "ftl:export" attached for verb, did you mean to use '//ftl:verb export' instead`, + `30:3-3: unexpected directive "ftl:export" attached for verb, did you mean to use '//ftl:verb export' instead?`, `36:36-39: unsupported request type "ftl/failing.Request"`, `36:50-50: unsupported response type "ftl/failing.Response"`, `37:16-29: call first argument must be a function but is an unresolved reference to lib.OtherFunc`, @@ -526,7 +533,7 @@ func TestErrorReporting(t *testing.T) { `54:59-59: unsupported response type "ftl/failing.Response"`, `59:1-2: first parameter must be context.Context`, `59:18-18: unsupported response type "ftl/failing.Response"`, - `64:1-2: must have at most two results (struct, error)`, + `64:1-2: must have at most two results (, error)`, `64:41-44: unsupported request type "ftl/failing.Request"`, `69:1-2: must at least return an error`, `69:36-39: unsupported request type "ftl/failing.Request"`, @@ -536,19 +543,21 @@ func TestErrorReporting(t *testing.T) { `78:55-55: first result must be a struct but is string`, `78:63-63: must return an error but is string`, `78:63-63: second result must not be ftl.Unit`, - `85:1-1: duplicate verb name "WrongResponse"`, - `91:2-12: struct field unexported must be exported by starting with an uppercase letter`, + // `85:1-1: duplicate verb name "WrongResponse"`, TODO: fix + `89:3-3: unexpected directive "ftl:verb"`, `103:2-24: cannot attach enum value to BadValueEnum because it is a variant of type enum TypeEnum, not a value enum`, `110:2-41: cannot attach enum value to BadValueEnumOrderDoesntMatter because it is a variant of type enum TypeEnum, not a value enum`, `123:21-60: config and secret names must be valid identifiers`, - `129:1-26: only one directive expected for enum`, - `129:1-26: only one directive expected for type alias`, + `129:1-1: schema declaration contains conflicting directives`, + `129:1-26: only one directive expected when directive "ftl:enum" is present, found multiple`, + `129:1-26: only one directive expected when directive "ftl:typealias" is present, found multiple`, `145:1-35: type can not be a variant of more than 1 type enums (TypeEnum1, TypeEnum2)`, `151:27-27: enum discriminator "TypeEnum3" cannot contain exported methods`, `154:1-35: enum discriminator "NoMethodsTypeEnum" must define at least one method`, - `166:3-3: unexpected token "d"`, + `166:3-14: unexpected token "d"`, `173:2-62: can not publish directly to topics in other modules`, `174:9-26: can not call verbs in other modules directly: use ftl.Call(…) instead`, + `179:2-12: struct field unexported must be exported by starting with an uppercase letter`, } assert.Equal(t, expected, actual) } diff --git a/go-runtime/compile/testdata/failing/failing.go b/go-runtime/compile/testdata/failing/failing.go index 16a9d8a80f..7205b8598d 100644 --- a/go-runtime/compile/testdata/failing/failing.go +++ b/go-runtime/compile/testdata/failing/failing.go @@ -173,3 +173,8 @@ func BadPublish(ctx context.Context) error { ps.PublicBroadcast.Publish(ctx, ps.PayinEvent{Name: "Test"}) return ps.Broadcast(ctx) } + +//ftl:data +type UnexportedFieldStruct struct { + unexported string +} diff --git a/go-runtime/compile/testdata/failing/go.sum b/go-runtime/compile/testdata/failing/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/compile/testdata/failing/go.sum +++ b/go-runtime/compile/testdata/failing/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/compile/testdata/fsm/go.sum b/go-runtime/compile/testdata/fsm/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/compile/testdata/fsm/go.sum +++ b/go-runtime/compile/testdata/fsm/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/compile/testdata/go/echo/go.sum b/go-runtime/compile/testdata/go/echo/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/compile/testdata/go/echo/go.sum +++ b/go-runtime/compile/testdata/go/echo/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/compile/testdata/go/notexportedverb/go.sum b/go-runtime/compile/testdata/go/notexportedverb/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/compile/testdata/go/notexportedverb/go.sum +++ b/go-runtime/compile/testdata/go/notexportedverb/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/compile/testdata/go/undefinedverb/go.sum b/go-runtime/compile/testdata/go/undefinedverb/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/compile/testdata/go/undefinedverb/go.sum +++ b/go-runtime/compile/testdata/go/undefinedverb/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/compile/testdata/one/go.sum b/go-runtime/compile/testdata/one/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/compile/testdata/one/go.sum +++ b/go-runtime/compile/testdata/one/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/compile/testdata/parent/child/child.go b/go-runtime/compile/testdata/parent/child/child.go index d82ce4df0f..db88b54df4 100644 --- a/go-runtime/compile/testdata/parent/child/child.go +++ b/go-runtime/compile/testdata/parent/child/child.go @@ -1,6 +1,8 @@ package child import ( + "context" + "github.com/TBD54566975/ftl/go-runtime/ftl" // Import the FTL SDK. ) @@ -9,3 +11,11 @@ type ChildStruct struct { } type ChildAlias string + +type Resp struct { +} + +//ftl:verb +func ChildVerb(ctx context.Context) (Resp, error) { + return Resp{}, nil +} diff --git a/go-runtime/compile/testdata/parent/go.sum b/go-runtime/compile/testdata/parent/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/compile/testdata/parent/go.sum +++ b/go-runtime/compile/testdata/parent/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/compile/testdata/pubsub/go.sum b/go-runtime/compile/testdata/pubsub/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/compile/testdata/pubsub/go.sum +++ b/go-runtime/compile/testdata/pubsub/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/compile/testdata/subscriber/go.sum b/go-runtime/compile/testdata/subscriber/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/compile/testdata/subscriber/go.sum +++ b/go-runtime/compile/testdata/subscriber/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/compile/testdata/two/go.sum b/go-runtime/compile/testdata/two/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/compile/testdata/two/go.sum +++ b/go-runtime/compile/testdata/two/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/compile/testdata/validation/go.sum b/go-runtime/compile/testdata/validation/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/compile/testdata/validation/go.sum +++ b/go-runtime/compile/testdata/validation/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/encoding/testdata/go/omitempty/go.sum b/go-runtime/encoding/testdata/go/omitempty/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/encoding/testdata/go/omitempty/go.sum +++ b/go-runtime/encoding/testdata/go/omitempty/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/ftl/ftltest/testdata/go/pubsub/go.sum b/go-runtime/ftl/ftltest/testdata/go/pubsub/go.sum index 8e56b3b02f..46e29d7f6c 100644 --- a/go-runtime/ftl/ftltest/testdata/go/pubsub/go.sum +++ b/go-runtime/ftl/ftltest/testdata/go/pubsub/go.sum @@ -123,8 +123,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/ftl/ftltest/testdata/go/subscriber/go.sum b/go-runtime/ftl/ftltest/testdata/go/subscriber/go.sum index 8839f9fb5e..475f511040 100644 --- a/go-runtime/ftl/ftltest/testdata/go/subscriber/go.sum +++ b/go-runtime/ftl/ftltest/testdata/go/subscriber/go.sum @@ -121,8 +121,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/ftl/ftltest/testdata/go/verbtypes/go.sum b/go-runtime/ftl/ftltest/testdata/go/verbtypes/go.sum index 8839f9fb5e..475f511040 100644 --- a/go-runtime/ftl/ftltest/testdata/go/verbtypes/go.sum +++ b/go-runtime/ftl/ftltest/testdata/go/verbtypes/go.sum @@ -121,8 +121,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/ftl/ftltest/testdata/go/wrapped/go.sum b/go-runtime/ftl/ftltest/testdata/go/wrapped/go.sum index 8839f9fb5e..475f511040 100644 --- a/go-runtime/ftl/ftltest/testdata/go/wrapped/go.sum +++ b/go-runtime/ftl/ftltest/testdata/go/wrapped/go.sum @@ -121,8 +121,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/ftl/reflection/testdata/go/runtimereflection/go.sum b/go-runtime/ftl/reflection/testdata/go/runtimereflection/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/ftl/reflection/testdata/go/runtimereflection/go.sum +++ b/go-runtime/ftl/reflection/testdata/go/runtimereflection/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/ftl/testdata/go/echo/go.sum b/go-runtime/ftl/testdata/go/echo/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/ftl/testdata/go/echo/go.sum +++ b/go-runtime/ftl/testdata/go/echo/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/ftl/testdata/go/mapper/go.sum b/go-runtime/ftl/testdata/go/mapper/go.sum index 8839f9fb5e..475f511040 100644 --- a/go-runtime/ftl/testdata/go/mapper/go.sum +++ b/go-runtime/ftl/testdata/go/mapper/go.sum @@ -121,8 +121,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/internal/testdata/go/mapper/go.sum b/go-runtime/internal/testdata/go/mapper/go.sum index 4d35e3e659..a09c8d2b58 100644 --- a/go-runtime/internal/testdata/go/mapper/go.sum +++ b/go-runtime/internal/testdata/go/mapper/go.sum @@ -77,8 +77,8 @@ github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPO github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/go-runtime/schema/analyzers/common.go b/go-runtime/schema/analyzers/common.go deleted file mode 100644 index 43c144f6ff..0000000000 --- a/go-runtime/schema/analyzers/common.go +++ /dev/null @@ -1,433 +0,0 @@ -package analyzers - -import ( - "fmt" - "go/ast" - "go/token" - "go/types" - "reflect" - "strings" - "unicode" - "unicode/utf8" - - "github.com/TBD54566975/ftl/backend/schema" - "github.com/TBD54566975/ftl/backend/schema/strcase" - "github.com/TBD54566975/golang-tools/go/analysis" - "github.com/TBD54566975/golang-tools/go/ast/astutil" - "github.com/alecthomas/types/optional" -) - -type DiagnosticCategory string - -const ( - Info DiagnosticCategory = "info" - Warn DiagnosticCategory = "warn" - Error DiagnosticCategory = "error" -) - -func (e DiagnosticCategory) ToErrorLevel() schema.ErrorLevel { - switch e { - case Info: - return schema.INFO - case Warn: - return schema.WARN - case Error: - return schema.ERROR - default: - panic(fmt.Sprintf("unknown diagnostic category %q", e)) - } -} - -type NativeNames map[schema.Node]string - -var ( - aliasFieldTag = "json" -) - -// TODO: maybe don't need NativeNames from extractors once we process refs/native names as an initial analyzer? -type result struct { - decls []schema.Decl - nativeNames NativeNames -} - -func extractComments(doc *ast.CommentGroup) []string { - comments := []string{} - if doc := doc.Text(); doc != "" { - comments = strings.Split(strings.TrimSpace(doc), "\n") - } - return comments -} - -func extractType(pass *analysis.Pass, pos token.Pos, tnode types.Type, isExported bool) optional.Option[schema.Type] { - fset := pass.Fset - if tparam, ok := tnode.(*types.TypeParam); ok { - return optional.Some[schema.Type](&schema.Ref{Pos: goPosToSchemaPos(fset, pos), Name: tparam.Obj().Id()}) - } - - switch underlying := tnode.Underlying().(type) { - case *types.Basic: - if named, ok := tnode.(*types.Named); ok { - return extractRef(pass, pos, named, isExported) - } - switch underlying.Kind() { - case types.String: - return optional.Some[schema.Type](&schema.String{Pos: goPosToSchemaPos(fset, pos)}) - - case types.Int, types.Int64: - return optional.Some[schema.Type](&schema.Int{Pos: goPosToSchemaPos(fset, pos)}) - - case types.Bool: - return optional.Some[schema.Type](&schema.Bool{Pos: goPosToSchemaPos(fset, pos)}) - - case types.Float64: - return optional.Some[schema.Type](&schema.Float{Pos: goPosToSchemaPos(fset, pos)}) - - default: - return optional.None[schema.Type]() - } - - case *types.Struct: - named, ok := tnode.(*types.Named) - if !ok { - pass.Report(noEndColumnErrorf(fset, pos, "expected named type but got %s", tnode)) - return optional.None[schema.Type]() - } - - // Special-cased types. - switch named.Obj().Pkg().Path() + "." + named.Obj().Name() { - case "time.Time": - return optional.Some[schema.Type](&schema.Time{Pos: goPosToSchemaPos(fset, pos)}) - - case "github.com/TBD54566975/ftl/go-runtime/ftl.Unit": - return optional.Some[schema.Type](&schema.Unit{Pos: goPosToSchemaPos(fset, pos)}) - - case "github.com/TBD54566975/ftl/go-runtime/ftl.Option": - typ := extractType(pass, pos, named.TypeArgs().At(0), isExported) - if underlying, ok := typ.Get(); ok { - return optional.Some[schema.Type](&schema.Optional{Pos: goPosToSchemaPos(pass.Fset, pos), Type: underlying}) - } - return optional.None[schema.Type]() - - default: - nodePath := named.Obj().Pkg().Path() - if !isPathInPkg(pass.Pkg, nodePath) && !strings.HasPrefix(nodePath, "ftl/") { - pass.Report(noEndColumnErrorf(fset, pos, "unsupported external type %s", nodePath+"."+named.Obj().Name())) - return optional.None[schema.Type]() - } - return extractData(pass, pos, tnode, isExported) - } - - case *types.Map: - return extractMap(pass, pos, underlying, isExported) - - case *types.Slice: - return extractSlice(pass, pos, underlying, isExported) - - case *types.Interface: - if underlying.String() == "any" { - return optional.Some[schema.Type](&schema.Any{Pos: goPosToSchemaPos(fset, pos)}) - } - if named, ok := tnode.(*types.Named); ok { - return extractRef(pass, pos, named, isExported) - } - return optional.None[schema.Type]() - - default: - return optional.None[schema.Type]() - } -} - -func extractRef(pass *analysis.Pass, pos token.Pos, named *types.Named, isExported bool) optional.Option[schema.Type] { - if named.Obj().Pkg() == nil { - return optional.None[schema.Type]() - } - - nodePath := named.Obj().Pkg().Path() - moduleName, err := ftlModuleFromGoModule(nodePath) - if err != nil { - pass.Report(noEndColumnWrapf(pass.Fset, pos, err, "")) - return optional.None[schema.Type]() - } - - if !isPathInPkg(pass.Pkg, nodePath) { - if !strings.HasPrefix(named.Obj().Pkg().Path(), "ftl/") { - pass.Report(noEndColumnErrorf(pass.Fset, pos, "unsupported external type %q", named.Obj().Pkg().Path()+"."+named.Obj().Name())) - return optional.None[schema.Type]() - } - } - - ref := &schema.Ref{ - Pos: goPosToSchemaPos(pass.Fset, pos), - Module: moduleName, - Name: strcase.ToUpperCamel(named.Obj().Name()), - } - for i := range named.TypeArgs().Len() { - typeArg, ok := extractType(pass, pos, named.TypeArgs().At(i), isExported).Get() - if !ok { - pass.Report(tokenErrorf(pass.Fset, pos, named.TypeArgs().At(i).String(), - "unsupported type %q for type argument", named.TypeArgs().At(i))) - continue - } - - // Fully qualify the Ref if needed - if r, okArg := typeArg.(*schema.Ref); okArg { - if r.Module == "" { - r.Module = moduleName - } - typeArg = r - } - ref.TypeParameters = append(ref.TypeParameters, typeArg) - } - - return optional.Some[schema.Type](ref) -} - -// TODO: probably don't need this in common and can move to data extractor once implemented -func extractData(pass *analysis.Pass, pos token.Pos, tnode types.Type, isExported bool) optional.Option[schema.Type] { - fset := pass.Fset - named, ok := tnode.(*types.Named) - if !ok { - pass.Report(noEndColumnErrorf(fset, pos, "expected named type but got %s", tnode)) - return optional.None[schema.Type]() - } - - nodePath := named.Obj().Pkg().Path() - nodeModule, err := ftlModuleFromGoModule(nodePath) - if err != nil { - pass.Report(noEndColumnWrapf(fset, pos, err, "")) - return optional.None[schema.Type]() - } - if !isPathInPkg(pass.Pkg, nodePath) { - return extractRef(pass, pos, named, isExported) - } - - out := &schema.Data{ - Pos: goPosToSchemaPos(fset, pos), - Name: strcase.ToUpperCamel(named.Obj().Name()), - Export: isExported, - } - // ectx.addNativeName(out, named.Obj().Name()) <-- TODO: add back when data extractor is implemented - dataRef := &schema.Ref{ - Pos: goPosToSchemaPos(fset, pos), - Module: nodeModule, - Name: out.Name, - } - for i := range named.TypeParams().Len() { - param := named.TypeParams().At(i) - out.TypeParameters = append(out.TypeParameters, &schema.TypeParameter{ - Pos: goPosToSchemaPos(fset, pos), - Name: param.Obj().Name(), - }) - typeArgs := named.TypeArgs() - if typeArgs == nil { - continue - } - typeArg, ok := extractType(pass, pos, typeArgs.At(i), isExported).Get() - if !ok { - pass.Report(tokenErrorf(fset, pos, typeArgs.At(i).String(), - "unsupported type %q for type argument", typeArgs.At(i))) - continue - } - dataRef.TypeParameters = append(dataRef.TypeParameters, typeArg) - } - - // If the struct is generic, we need to use the origin type to get the - // fields. - if named.TypeParams().Len() > 0 { - named = named.Origin() - } - - // Find type declaration so we can extract comments. - namedPos := named.Obj().Pos() - maybePath, _ := pathEnclosingInterval(pass, namedPos, namedPos) - if path, ok := maybePath.Get(); ok { - for i := len(path) - 1; i >= 0; i-- { - // We have to check both the type spec and the gen decl because the - // type could be declared as either "type Foo struct { ... }" or - // "type ( Foo struct { ... } )" - switch path := path[i].(type) { - case *ast.TypeSpec: - if path.Doc != nil { - out.Comments = extractComments(path.Doc) - } - case *ast.GenDecl: - if path.Doc != nil { - out.Comments = extractComments(path.Doc) - } - } - } - } - - s, ok := named.Underlying().(*types.Struct) - if !ok { - pass.Report(noEndColumnErrorf(fset, pos, "expected struct but got %s", named)) - return optional.None[schema.Type]() - } - - fieldErrors := false - for i := range s.NumFields() { - f := s.Field(i) - if ft, ok := extractType(pass, f.Pos(), f.Type(), isExported).Get(); ok { - // Check if field is exported - if len(f.Name()) > 0 && unicode.IsLower(rune(f.Name()[0])) { - pass.Report(tokenErrorf(fset, f.Pos(), f.Name(), - "struct field %s must be exported by starting with an uppercase letter", f.Name())) - fieldErrors = true - } - - // Extract the JSON tag and split it to get just the field name - tagContent := reflect.StructTag(s.Tag(i)).Get(aliasFieldTag) - tagParts := strings.Split(tagContent, ",") - jsonFieldName := "" - if len(tagParts) > 0 { - jsonFieldName = tagParts[0] - } - - var metadata []schema.Metadata - if jsonFieldName != "" { - metadata = append(metadata, &schema.MetadataAlias{ - Pos: goPosToSchemaPos(pass.Fset, pos), - Kind: schema.AliasKindJSON, - Alias: jsonFieldName, - }) - } - out.Fields = append(out.Fields, &schema.Field{ - Pos: goPosToSchemaPos(pass.Fset, pos), - Name: strcase.ToLowerCamel(f.Name()), - Type: ft, - Metadata: metadata, - }) - } else { - pass.Report(tokenErrorf(fset, f.Pos(), f.Name(), "unsupported type %q for field %q", f.Type(), f.Name())) - fieldErrors = true - } - } - if fieldErrors { - return optional.None[schema.Type]() - } - - // ectx.module.AddData(out) <-- TODO: add back when data extractor is implemented - return optional.Some[schema.Type](dataRef) -} - -func extractMap(pass *analysis.Pass, pos token.Pos, tnode *types.Map, isExported bool) optional.Option[schema.Type] { - key, ok := extractType(pass, pos, tnode.Key(), isExported).Get() - if !ok { - return optional.None[schema.Type]() - } - - value, ok := extractType(pass, pos, tnode.Elem(), isExported).Get() - if !ok { - return optional.None[schema.Type]() - } - - return optional.Some[schema.Type](&schema.Map{Pos: goPosToSchemaPos(pass.Fset, pos), Key: key, Value: value}) -} - -func extractSlice(pass *analysis.Pass, pos token.Pos, tnode *types.Slice, isExported bool) optional.Option[schema.Type] { - // If it's a []byte, treat it as a Bytes type. - if basic, ok := tnode.Elem().Underlying().(*types.Basic); ok && basic.Kind() == types.Byte { - return optional.Some[schema.Type](&schema.Bytes{Pos: goPosToSchemaPos(pass.Fset, pos)}) - } - - value, ok := extractType(pass, pos, tnode.Elem(), isExported).Get() - if !ok { - return optional.None[schema.Type]() - } - - return optional.Some[schema.Type](&schema.Array{ - Pos: goPosToSchemaPos(pass.Fset, pos), - Element: value, - }) -} - -func goPosToSchemaPos(fset *token.FileSet, pos token.Pos) schema.Position { - p := fset.Position(pos) - return schema.Position{Filename: p.Filename, Line: p.Line, Column: p.Column, Offset: p.Offset} -} - -func tokenFileContainsPos(f *token.File, pos token.Pos) bool { - p := int(pos) - base := f.Base() - return base <= p && p < base+f.Size() -} - -func isPathInPkg(pkg *types.Package, path string) bool { - if path == pkg.Path() { - return true - } - return strings.HasPrefix(path, pkg.Path()+"/") -} - -// pathEnclosingInterval returns the PackageInfo and ast.Node that -// contain source interval [start, end), and all the node's ancestors -// up to the AST root. It searches all ast.Files of all packages in prog. -// exact is defined as for astutil.PathEnclosingInterval. -// -// An empty path optional is returned if not found. -func pathEnclosingInterval(pass *analysis.Pass, start, end token.Pos) (path optional.Option[[]ast.Node], exact bool) { - for _, f := range pass.Files { - if f.Pos() == token.NoPos { - // This can happen if the parser saw - // too many errors and bailed out. - // (Use parser.AllErrors to prevent that.) - continue - } - if !tokenFileContainsPos(pass.Fset.File(f.Pos()), start) { - continue - } - if path, exact := astutil.PathEnclosingInterval(f, start, end); path != nil { - return optional.Some(path), exact - } - } - - return optional.None[[]ast.Node](), false -} - -func ftlModuleFromGoModule(pkgPath string) (string, error) { - parts := strings.Split(pkgPath, "/") - if parts[0] != "ftl" { - return "", fmt.Errorf("package %q is not in the ftl namespace", pkgPath) - } - return strings.TrimSuffix(parts[1], "_test"), nil -} - -func errorf(node ast.Node, format string, args ...interface{}) analysis.Diagnostic { - return errorfAtPos(node.Pos(), node.End(), format, args...) -} - -func errorfAtPos(pos token.Pos, end token.Pos, format string, args ...interface{}) analysis.Diagnostic { - return analysis.Diagnostic{Pos: pos, End: end, Message: fmt.Sprintf(format, args...), Category: string(Error)} -} - -func noEndColumnErrorf(fset *token.FileSet, pos token.Pos, format string, args ...interface{}) analysis.Diagnostic { - return tokenErrorf(fset, pos, "", format, args...) -} - -func tokenErrorf(fset *token.FileSet, pos token.Pos, tokenText string, format string, args ...interface{}) analysis.Diagnostic { - endCol := fset.Position(pos).Column - if len(tokenText) > 0 { - endCol += utf8.RuneCountInString(tokenText) - } - return errorfAtPos(pos, fset.File(pos).Pos(endCol), format, args...) -} - -func noEndColumnWrapf(fset *token.FileSet, pos token.Pos, err error, format string, args ...interface{}) analysis.Diagnostic { - if format == "" { - format = "%s" - } else { - format += ": %s" - } - args = append(args, err) - return tokenErrorf(fset, pos, "", format, args...) -} - -func wrapf(node ast.Node, err error, format string, args ...interface{}) analysis.Diagnostic { - if format == "" { - format = "%s" - } else { - format += ": %s" - } - args = append(args, err) - return errorfAtPos(node.Pos(), node.End(), format, args...) -} diff --git a/go-runtime/schema/analyzers/finalize.go b/go-runtime/schema/analyzers/finalize.go deleted file mode 100644 index 6375539e7d..0000000000 --- a/go-runtime/schema/analyzers/finalize.go +++ /dev/null @@ -1,114 +0,0 @@ -package analyzers - -import ( - "fmt" - "reflect" - - "github.com/TBD54566975/ftl/backend/schema" - "github.com/TBD54566975/golang-tools/go/analysis" - "golang.org/x/exp/maps" -) - -// Extractors is a list of all schema extractors that must run. -var Extractors = []*analysis.Analyzer{ - TypeAliasExtractor, -} - -// Finalizer aggregates the results of all extractors. -var Finalizer = &analysis.Analyzer{ - Name: "finalizer", - Doc: "finalizes module schema and writes to the output destination", - Run: runFinalizer, - Requires: Extractors, - ResultType: reflect.TypeFor[ExtractResult](), - RunDespiteErrors: true, -} - -// ExtractResult contains the final schema extraction result. -type ExtractResult struct { - // Module is the extracted module schema. - Module *schema.Module - // NativeNames maps schema nodes to their native Go names. - NativeNames map[schema.Node]string - // Errors is a list of errors encountered during schema extraction. - Errors []*schema.Error -} - -func runFinalizer(pass *analysis.Pass) (interface{}, error) { - module, nativeNames, err := buildModuleSchema(pass) - if err != nil { - return nil, fmt.Errorf("could not process schema extraction results") - } - - // TODO: validate schema once we have the full schema here - - return ExtractResult{ - Module: module, - NativeNames: nativeNames, - }, nil -} - -// buildModuleSchema aggregates the results of all extractors. -func buildModuleSchema(pass *analysis.Pass) (*schema.Module, NativeNames, error) { - moduleName, err := ftlModuleFromGoModule(pass.Pkg.Path()) - if err != nil { - return nil, nil, err - } - module := &schema.Module{Name: moduleName} - nn := NativeNames{} - for _, e := range Extractors { - r, ok := pass.ResultOf[e].(result) - if !ok { - return nil, nil, fmt.Errorf("failed to extract result of %s", e.Name) - } - module.AddDecls(r.decls) - maps.Copy(nn, r.nativeNames) - } - updateVisibility(module) - return module, nn, nil -} - -// updateVisibility traverses the module schema via refs and updates visibility as needed. -func updateVisibility(module *schema.Module) { - for _, d := range module.Decls { - if d.IsExported() { - updateTransitiveVisibility(d, module) - } - } -} - -// updateTransitiveVisibility updates any decls that are transitively visible from d. -func updateTransitiveVisibility(d schema.Decl, module *schema.Module) { - if !d.IsExported() { - return - } - - _ = schema.Visit(d, func(n schema.Node, next func() error) error { - ref, ok := n.(*schema.Ref) - if !ok { - return next() - } - - resolved := module.Resolve(*ref) - if resolved == nil || resolved.Symbol == nil { - return next() - } - - if decl, ok := resolved.Symbol.(schema.Decl); ok { - switch t := decl.(type) { - case *schema.Data: - t.Export = true - case *schema.Enum: - t.Export = true - case *schema.TypeAlias: - t.Export = true - case *schema.Topic: - t.Export = true - case *schema.Verb: - t.Export = true - case *schema.Database, *schema.Config, *schema.FSM, *schema.Secret, *schema.Subscription: - } - } - return next() - }) -} diff --git a/go-runtime/schema/analyzers/parser.go b/go-runtime/schema/analyzers/parser.go deleted file mode 100644 index f654c06d6b..0000000000 --- a/go-runtime/schema/analyzers/parser.go +++ /dev/null @@ -1,218 +0,0 @@ -package analyzers - -import ( - "errors" - "fmt" - "go/ast" - "strconv" - "strings" - - "github.com/TBD54566975/golang-tools/go/analysis" - "github.com/alecthomas/participle/v2" - "github.com/alecthomas/participle/v2/lexer" - - "github.com/TBD54566975/ftl/backend/schema" -) - -// This file contains a parser for Go FTL directives. -// -// eg. //ftl:ingress http GET /foo/bar - -type directiveWrapper struct { - Directive directive `parser:"'ftl' ':' @@"` -} - -//sumtype:decl -type directive interface{ directive() } - -type exportable interface { - IsExported() bool -} - -type directiveVerb struct { - Pos lexer.Position - - Verb bool `parser:"@'verb'"` - Export bool `parser:"@'export'?"` -} - -func (*directiveVerb) directive() {} -func (d *directiveVerb) String() string { - if d.Export { - return "ftl:verb export" - } - return "ftl:verb" -} -func (d *directiveVerb) IsExported() bool { - return d.Export -} - -type directiveData struct { - Pos lexer.Position - - Data bool `parser:"@'data'"` - Export bool `parser:"@'export'?"` -} - -func (*directiveData) directive() {} -func (d *directiveData) String() string { - if d.Export { - return "ftl:data export" - } - return "ftl:data" -} -func (d *directiveData) IsExported() bool { - return d.Export -} - -type directiveEnum struct { - Pos lexer.Position - - Enum bool `parser:"@'enum'"` - Export bool `parser:"@'export'?"` -} - -func (*directiveEnum) directive() {} -func (d *directiveEnum) String() string { - if d.Export { - return "ftl:enum export" - } - return "ftl:enum" -} -func (d *directiveEnum) IsExported() bool { - return d.Export -} - -type directiveTypeAlias struct { - Pos lexer.Position - - TypeAlias bool `parser:"@'typealias'"` - Export bool `parser:"@'export'?"` -} - -func (*directiveTypeAlias) directive() {} -func (d *directiveTypeAlias) String() string { - if d.Export { - return "ftl:typealias export" - } - return "ftl:typealias" -} -func (d *directiveTypeAlias) IsExported() bool { - return d.Export -} - -type directiveIngress struct { - Pos schema.Position - - Type string `parser:"'ingress' @('http')?"` - Method string `parser:"@('GET' | 'POST' | 'PUT' | 'DELETE')"` - Path []schema.IngressPathComponent `parser:"('/' @@)+"` -} - -func (*directiveIngress) directive() {} -func (d *directiveIngress) String() string { - w := &strings.Builder{} - fmt.Fprintf(w, "ftl:ingress %s", d.Method) - for _, p := range d.Path { - fmt.Fprintf(w, "/%s", p) - } - return w.String() -} - -type directiveCronJob struct { - Pos schema.Position - - Cron string `parser:"'cron' Whitespace @((' ' | Number | '-' | '/' | '*' | ',')+)"` -} - -func (*directiveCronJob) directive() {} - -func (d *directiveCronJob) String() string { - return fmt.Sprintf("cron %s", d.Cron) -} - -type directiveRetry struct { - Pos schema.Position - - Count *int `parser:"'retry' (@Number Whitespace)?"` - MinBackoff string `parser:"@(Number (?! Whitespace) Ident)?"` - MaxBackoff string `parser:"@(Number (?! Whitespace) Ident)?"` -} - -func (*directiveRetry) directive() {} - -func (d *directiveRetry) String() string { - components := []string{"retry"} - if d.Count != nil { - components = append(components, strconv.Itoa(*d.Count)) - } - components = append(components, d.MinBackoff) - if len(d.MaxBackoff) > 0 { - components = append(components, d.MaxBackoff) - } - return strings.Join(components, " ") -} - -// used to subscribe a sink to a subscription -type directiveSubscriber struct { - Pos schema.Position - - Name string `parser:"'subscribe' @Ident"` -} - -func (*directiveSubscriber) directive() {} - -func (d *directiveSubscriber) String() string { - return fmt.Sprintf("subscribe %s", d.Name) -} - -// most declarations include export in other directives, but some don't have any other way. -type directiveExport struct { - Pos schema.Position - - Export bool `parser:"@'export'"` -} - -func (*directiveExport) directive() {} - -func (d *directiveExport) String() string { - return "export" -} - -var directiveParser = participle.MustBuild[directiveWrapper]( - participle.Lexer(schema.Lexer), - participle.Elide("Whitespace"), - participle.Unquote(), - participle.UseLookahead(2), - participle.Union[directive](&directiveVerb{}, &directiveData{}, &directiveEnum{}, &directiveTypeAlias{}, - &directiveIngress{}, &directiveCronJob{}, &directiveRetry{}, &directiveSubscriber{}, &directiveExport{}), - participle.Union[schema.IngressPathComponent](&schema.IngressPathLiteral{}, &schema.IngressPathParameter{}), -) - -func parseDirectives(pass *analysis.Pass, node ast.Node, docs *ast.CommentGroup) []directive { - if docs == nil { - return nil - } - directives := []directive{} - for _, line := range docs.List { - if !strings.HasPrefix(line.Text, "//ftl:") { - continue - } - pos := pass.Fset.Position(line.Pos()) - // TODO: We need to adjust position information embedded in the schema. - directive, err := directiveParser.ParseString(pos.Filename, line.Text[2:]) - if err != nil { - // Adjust the Participle-reported position relative to the AST node. - var perr participle.Error - if errors.As(err, &perr) { - file := pass.Fset.File(node.Pos()) - pass.Report(errorfAtPos(file.Pos(pos.Line), file.Pos(pos.Column+2), "%s", perr.Message())) - } else { - pass.Report(wrapf(node, err, "")) - } - return nil - } - directives = append(directives, directive.Directive) - } - return directives -} diff --git a/go-runtime/schema/analyzers/typealias.go b/go-runtime/schema/analyzers/typealias.go deleted file mode 100644 index 278f44feef..0000000000 --- a/go-runtime/schema/analyzers/typealias.go +++ /dev/null @@ -1,85 +0,0 @@ -package analyzers - -import ( - "go/ast" - "go/types" - "reflect" - - "github.com/TBD54566975/golang-tools/go/analysis" - "github.com/TBD54566975/golang-tools/go/analysis/passes/inspect" - "github.com/TBD54566975/golang-tools/go/ast/inspector" - "github.com/alecthomas/types/optional" - - "github.com/TBD54566975/ftl/backend/schema" - "github.com/TBD54566975/ftl/backend/schema/strcase" -) - -// TypeAliasExtractor extracts type aliases to the module schema. -var TypeAliasExtractor = &analysis.Analyzer{ - Name: "typealias", - Doc: "extracts type aliases to the module schema", - Run: extractTypeAliases, - Requires: []*analysis.Analyzer{inspect.Analyzer}, - ResultType: reflect.TypeFor[result](), - RunDespiteErrors: true, -} - -func extractTypeAliases(pass *analysis.Pass) (interface{}, error) { - nn := NativeNames{} - aliases := []schema.Decl{} - in := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) //nolint:forcetypeassert - nodeFilter := []ast.Node{ - (*ast.GenDecl)(nil), - } - in.Preorder(nodeFilter, func(n ast.Node) { - node := n.(*ast.GenDecl) //nolint:forcetypeassert - directives := parseDirectives(pass, node, node.Doc) - for _, dir := range directives { - if len(node.Specs) != 1 { - pass.Report(errorf(node, "error parsing ftl directive: expected exactly one type declaration")) - return - } - t, ok := node.Specs[0].(*ast.TypeSpec) - if !ok { - return - } - - aType := pass.Pkg.Scope().Lookup(t.Name.Name) - nativeName := aType.Pkg().Name() + "." + aType.Name() - te, ok := dir.(*directiveTypeAlias) - if !ok { - continue - } - if len(directives) > 1 { - pass.Report(errorf(node, "only one directive expected for type alias")) - } - - var sType optional.Option[schema.Type] - typ := pass.TypesInfo.TypeOf(t.Type) - if named, ok := typ.(*types.Named); ok { - sType = extractRef(pass, node.Pos(), named, te.IsExported()) - } else { - sType = extractType(pass, node.Pos(), typ, te.IsExported()) - } - - if !sType.Ok() { - pass.Report(errorf(node, "could not extract type for type alias")) - return - } - - alias := &schema.TypeAlias{ - Pos: goPosToSchemaPos(pass.Fset, node.Pos()), - Comments: extractComments(node.Doc), - Name: strcase.ToUpperCamel(t.Name.Name), - Export: te.IsExported(), - Type: sType.MustGet(), - } - nn[alias] = nativeName - aliases = append(aliases, alias) - } - }) - return result{ - decls: aliases, - nativeNames: nn, - }, nil -} diff --git a/go-runtime/schema/common/common.go b/go-runtime/schema/common/common.go new file mode 100644 index 0000000000..6c6d264579 --- /dev/null +++ b/go-runtime/schema/common/common.go @@ -0,0 +1,469 @@ +package common + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "reflect" + "strings" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/backend/schema/strcase" + "github.com/TBD54566975/golang-tools/go/analysis" + "github.com/TBD54566975/golang-tools/go/analysis/passes/inspect" + "github.com/TBD54566975/golang-tools/go/ast/inspector" + "github.com/alecthomas/types/optional" + "github.com/puzpuzpuz/xsync/v3" +) + +var ( + // FtlUnitTypePath is the path to the FTL unit type. + FtlUnitTypePath = "github.com/TBD54566975/ftl/go-runtime/ftl.Unit" + FtlOptionTypePath = "github.com/TBD54566975/ftl/go-runtime/ftl.Option" + + extractorRegistery = xsync.NewMapOf[reflect.Type, ExtractDeclFunc[schema.Decl, ast.Node]]() +) + +func NewExtractor(name string, factType analysis.Fact, run func(*analysis.Pass) (interface{}, error)) *analysis.Analyzer { + if !reflect.TypeOf(factType).Implements(reflect.TypeOf((*SchemaFact)(nil)).Elem()) { + panic(fmt.Sprintf("factType %T does not implement SchemaFact", factType)) + } + return &analysis.Analyzer{ + Name: name, + Doc: fmt.Sprintf("extracts %s schema elements to the module", name), + Run: run, + ResultType: reflect.TypeFor[ExtractorResult](), + RunDespiteErrors: true, + FactTypes: []analysis.Fact{factType}, + } +} + +type ExtractDeclFunc[T schema.Decl, N ast.Node] func(pass *analysis.Pass, node N, object types.Object) optional.Option[T] + +func NewDeclExtractor[T schema.Decl, N ast.Node](name string, factType analysis.Fact, extractFunc ExtractDeclFunc[T, N]) *analysis.Analyzer { + dType := reflect.TypeFor[T]() + if _, ok := extractorRegistery.Load(dType); ok { + panic(fmt.Sprintf("multiple extractors registered for %s", dType.String())) + } + wrapped := func(pass *analysis.Pass, n ast.Node, o types.Object) optional.Option[schema.Decl] { + decl, ok := extractFunc(pass, n.(N), o).Get() + if ok { + return optional.Some(schema.Decl(decl)) + } + return optional.None[schema.Decl]() + } + extractorRegistery.Store(dType, wrapped) + return NewExtractor(name, factType, runExtractDeclsFunc[T, N](extractFunc)) +} + +type ExtractorResult struct { + Facts []analysis.ObjectFact +} + +func NewExtractorResult(pass *analysis.Pass) ExtractorResult { + return ExtractorResult{Facts: pass.AllObjectFacts()} +} + +// runExtractDeclsFunc extracts schema declarations from the AST. +// +// The `extractFunc` function is called on each node and should return the schema declaration for that node. +// If the node does not represent a schema declaration, the function should return `optional.None[T]()`. +// +// Only nodes that have been marked with a `common.ExtractedMetadata` fact are considered for extraction (nodes +// explicitly annotated with an FTL directive). Implicit schema declarations are extracted by the `transitive` +// extractor. +func runExtractDeclsFunc[T schema.Decl, N ast.Node](extractFunc ExtractDeclFunc[T, N]) func(pass *analysis.Pass) (interface{}, error) { + return func(pass *analysis.Pass) (interface{}, error) { + nodeFilter := []ast.Node{ //nolint:forcetypeassert + reflect.New(reflect.TypeFor[N]().Elem()).Interface().(N), + } + in := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) //nolint:forcetypeassert + in.Preorder(nodeFilter, func(n ast.Node) { + obj, ok := GetObjectForNode(pass.TypesInfo, n).Get() + if !ok { + return + } + if obj != nil && !IsPathInPkg(pass.Pkg, obj.Pkg().Path()) { + return + } + md, ok := GetFactForObject[*ExtractedMetadata](pass, obj).Get() + if !ok { + return + } + if _, ok = md.Type.(T); !ok { + return + } + if decl, ok := extractFunc(pass, n.(N), obj).Get(); ok { + MarkSchemaDecl(pass, obj, decl) + } else { + MarkFailedExtraction(pass, obj) + } + }) + return NewExtractorResult(pass), nil + } +} + +func ExtractComments(doc *ast.CommentGroup) []string { + if doc == nil { + return nil + } + comments := []string{} + if doc := doc.Text(); doc != "" { + comments = strings.Split(strings.TrimSpace(doc), "\n") + } + return comments +} + +func ExtractType(pass *analysis.Pass, pos token.Pos, tnode types.Type) optional.Option[schema.Type] { + if tnode == nil { + return optional.None[schema.Type]() + } + + fset := pass.Fset + if tparam, ok := tnode.(*types.TypeParam); ok { + return optional.Some[schema.Type](&schema.Ref{Pos: GoPosToSchemaPos(fset, pos), Name: tparam.Obj().Id()}) + } + + switch underlying := tnode.Underlying().(type) { + case *types.Basic: + if named, ok := tnode.(*types.Named); ok { + return extractRef(pass, pos, named) + } + switch underlying.Kind() { + case types.String: + return optional.Some[schema.Type](&schema.String{Pos: GoPosToSchemaPos(fset, pos)}) + + case types.Int, types.Int64: + return optional.Some[schema.Type](&schema.Int{Pos: GoPosToSchemaPos(fset, pos)}) + + case types.Bool: + return optional.Some[schema.Type](&schema.Bool{Pos: GoPosToSchemaPos(fset, pos)}) + + case types.Float64: + return optional.Some[schema.Type](&schema.Float{Pos: GoPosToSchemaPos(fset, pos)}) + + default: + return optional.None[schema.Type]() + } + + case *types.Struct: + named, ok := tnode.(*types.Named) + if !ok { + NoEndColumnErrorf(pass, pos, "expected named type but got %s", tnode) + return optional.None[schema.Type]() + } + + // Special-cased types. + switch named.Obj().Pkg().Path() + "." + named.Obj().Name() { + case "time.Time": + return optional.Some[schema.Type](&schema.Time{Pos: GoPosToSchemaPos(fset, pos)}) + + case FtlUnitTypePath: + return optional.Some[schema.Type](&schema.Unit{Pos: GoPosToSchemaPos(fset, pos)}) + + case FtlOptionTypePath: + typ := ExtractType(pass, pos, named.TypeArgs().At(0)) + if underlying, ok := typ.Get(); ok { + return optional.Some[schema.Type](&schema.Optional{Pos: GoPosToSchemaPos(pass.Fset, pos), Type: underlying}) + } + return optional.None[schema.Type]() + + default: + return extractRef(pass, pos, named) + } + + case *types.Map: + return extractMap(pass, pos, underlying) + + case *types.Slice: + return extractSlice(pass, pos, underlying) + + case *types.Interface: + if underlying.String() == "any" { + return optional.Some[schema.Type](&schema.Any{Pos: GoPosToSchemaPos(fset, pos)}) + } + if named, ok := tnode.(*types.Named); ok { + return extractRef(pass, pos, named) + } + return optional.None[schema.Type]() + + default: + return optional.None[schema.Type]() + } +} + +func InferDeclType(pass *analysis.Pass, node ast.Node, obj types.Object) optional.Option[schema.Decl] { + ts, ok := node.(*ast.TypeSpec) + if !ok { + return optional.None[schema.Decl]() + } + if _, ok := ts.Type.(*ast.InterfaceType); ok { + return optional.Some[schema.Decl](&schema.Enum{}) + } + t, ok := ExtractTypeForNode(pass, obj, ts.Type, nil).Get() + if !ok { + return optional.None[schema.Decl]() + } + if !IsSelfReference(pass, obj, t) { + return optional.Some[schema.Decl](&schema.TypeAlias{}) + } + return optional.Some[schema.Decl](&schema.Data{}) +} + +func ExtractFuncForDecl(t schema.Decl) (ExtractDeclFunc[schema.Decl, ast.Node], error) { + if f, ok := extractorRegistery.Load(reflect.TypeOf(t)); ok { + return f, nil + } + return nil, fmt.Errorf("no extractor registered for %T", t) +} + +func GoPosToSchemaPos(fset *token.FileSet, pos token.Pos) schema.Position { + p := fset.Position(pos) + return schema.Position{Filename: p.Filename, Line: p.Line, Column: p.Column, Offset: p.Offset} +} + +func FtlModuleFromGoPackage(pkgPath string) (string, error) { + parts := strings.Split(pkgPath, "/") + if parts[0] != "ftl" { + return "", fmt.Errorf("package %q is not in the ftl namespace", pkgPath) + } + return strings.TrimSuffix(parts[1], "_test"), nil +} + +func IsType[T types.Type](t types.Type) bool { + if _, ok := t.(*types.Named); ok { + t = t.Underlying() + } + _, ok := t.(T) + return ok +} + +func IsPathInPkg(pkg *types.Package, path string) bool { + if path == pkg.Path() { + return true + } + return strings.HasPrefix(path, pkg.Path()+"/") +} + +func GetObjectForNode(typesInfo *types.Info, node ast.Node) optional.Option[types.Object] { + var obj types.Object + switch n := node.(type) { + case *ast.GenDecl: + if len(n.Specs) > 0 { + return GetObjectForNode(typesInfo, n.Specs[0]) + } + case *ast.Field: + if len(n.Names) > 0 { + obj = typesInfo.ObjectOf(n.Names[0]) + } + case *ast.ImportSpec: + obj = typesInfo.ObjectOf(n.Name) + case *ast.ValueSpec: + if len(n.Names) > 0 { + obj = typesInfo.ObjectOf(n.Names[0]) + } + case *ast.TypeSpec: + obj = typesInfo.ObjectOf(n.Name) + case *ast.FuncDecl: + obj = typesInfo.ObjectOf(n.Name) + default: + return optional.None[types.Object]() + } + if obj == nil { + return optional.None[types.Object]() + } + return optional.Some(obj) +} + +func GetTypeForNode(node ast.Node, info *types.Info) types.Type { + switch n := node.(type) { + case *ast.Ident: + if obj := info.ObjectOf(n); obj != nil { + return obj.Type() + } + case ast.Expr: + return info.TypeOf(n) + case *ast.AssignStmt: + if len(n.Lhs) > 0 { + return info.TypeOf(n.Lhs[0]) + } + case *ast.ValueSpec: + if len(n.Names) > 0 { + if obj := info.ObjectOf(n.Names[0]); obj != nil { + return obj.Type() + } + } + case *ast.TypeSpec: + return info.TypeOf(n.Type) + case *ast.CompositeLit: + return info.TypeOf(n) + case *ast.CallExpr: + return info.TypeOf(n) + case *ast.FuncDecl: + if n.Name != nil { + if obj := info.ObjectOf(n.Name); obj != nil { + return obj.Type() + } + } + case *ast.GenDecl: + for _, spec := range n.Specs { + if t := GetTypeForNode(spec, info); t != nil { + return t + } + } + } + return nil +} + +func extractRef(pass *analysis.Pass, pos token.Pos, named *types.Named) optional.Option[schema.Type] { + if named.Obj().Pkg() == nil { + return optional.None[schema.Type]() + } + + nodePath := named.Obj().Pkg().Path() + if !IsPathInPkg(pass.Pkg, nodePath) && !strings.HasPrefix(named.Obj().Pkg().Path(), "ftl/") { + NoEndColumnErrorf(pass, pos, "unsupported external type %q", named.Obj().Pkg().Path()+"."+named.Obj().Name()) + return optional.None[schema.Type]() + } + + moduleName, err := FtlModuleFromGoPackage(nodePath) + if err != nil { + noEndColumnWrapf(pass, pos, err, "") + return optional.None[schema.Type]() + } + + ref := &schema.Ref{ + Pos: GoPosToSchemaPos(pass.Fset, pos), + Module: moduleName, + Name: strcase.ToUpperCamel(named.Obj().Name()), + } + for i := range named.TypeArgs().Len() { + typeArg, ok := ExtractType(pass, pos, named.TypeArgs().At(i)).Get() + if !ok { + TokenErrorf(pass, pos, named.TypeArgs().At(i).String(), "unsupported type %q for type argument", named.TypeArgs().At(i)) + continue + } + + // Fully qualify the Ref if needed + if r, okArg := typeArg.(*schema.Ref); okArg { + if r.Module == "" { + r.Module = moduleName + } + typeArg = r + } + ref.TypeParameters = append(ref.TypeParameters, typeArg) + } + + if isLocalRef(pass, ref) { + // mark this local reference to ensure its underlying schema type is hydrated by the appropriate extractor and + // included in the schema + markNeedsExtraction(pass, named.Obj()) + } + + return optional.Some[schema.Type](ref) +} + +func extractMap(pass *analysis.Pass, pos token.Pos, tnode *types.Map) optional.Option[schema.Type] { + key, ok := ExtractType(pass, pos, tnode.Key()).Get() + if !ok { + return optional.None[schema.Type]() + } + + value, ok := ExtractType(pass, pos, tnode.Elem()).Get() + if !ok { + return optional.None[schema.Type]() + } + + return optional.Some[schema.Type](&schema.Map{Pos: GoPosToSchemaPos(pass.Fset, pos), Key: key, Value: value}) +} + +func extractSlice(pass *analysis.Pass, pos token.Pos, tnode *types.Slice) optional.Option[schema.Type] { + // If it's a []byte, treat it as a Bytes type. + if basic, ok := tnode.Elem().Underlying().(*types.Basic); ok && basic.Kind() == types.Byte { + return optional.Some[schema.Type](&schema.Bytes{Pos: GoPosToSchemaPos(pass.Fset, pos)}) + } + + value, ok := ExtractType(pass, pos, tnode.Elem()).Get() + if !ok { + return optional.None[schema.Type]() + } + + return optional.Some[schema.Type](&schema.Array{ + Pos: GoPosToSchemaPos(pass.Fset, pos), + Element: value, + }) +} + +func ExtractTypeForNode(pass *analysis.Pass, obj types.Object, node ast.Node, index types.Type) optional.Option[schema.Type] { + switch typ := node.(type) { + // Selector expression e.g. ftl.Unit, ftl.Option, foo.Bar + case *ast.SelectorExpr: + var ident *ast.Ident + var ok bool + if ident, ok = typ.X.(*ast.Ident); !ok { + return optional.None[schema.Type]() + } + + for _, im := range pass.Pkg.Imports() { + if im.Name() != ident.Name { + continue + } + switch im.Path() /*"." + typ.Sel.Name */ { + case "time.Time": + return optional.Some[schema.Type](&schema.Time{}) + case FtlUnitTypePath: + return optional.Some[schema.Type](&schema.Unit{}) + case FtlOptionTypePath: + if index == nil { + return optional.None[schema.Type]() + } + return ExtractType(pass, node.Pos(), index) + default: // Data ref + externalModuleName, err := FtlModuleFromGoPackage(im.Path()) + if err != nil { + return optional.None[schema.Type]() + } + return optional.Some[schema.Type](&schema.Ref{ + Pos: GoPosToSchemaPos(pass.Fset, node.Pos()), + Module: externalModuleName, + Name: typ.Sel.Name, + }) + } + } + + case *ast.IndexExpr: // Generic type, e.g. ftl.Option[string] + if se, ok := typ.X.(*ast.SelectorExpr); ok { + return ExtractTypeForNode(pass, obj, se, pass.TypesInfo.TypeOf(typ.Index)) + } + + default: + variantNode := GetTypeForNode(node, pass.TypesInfo) + if _, ok := variantNode.(*types.Struct); ok { + variantNode = obj.Type() + } + return ExtractType(pass, node.Pos(), variantNode) + } + + return optional.None[schema.Type]() +} + +func IsSelfReference(pass *analysis.Pass, obj types.Object, t schema.Type) bool { + ref, ok := t.(*schema.Ref) + if !ok { + return false + } + moduleName, err := FtlModuleFromGoPackage(pass.Pkg.Path()) + if err != nil { + return false + } + return ref.Module == moduleName && obj.Name() == ref.Name +} + +func isLocalRef(pass *analysis.Pass, ref *schema.Ref) bool { + moduleName, err := FtlModuleFromGoPackage(pass.Pkg.Path()) + if err != nil { + return false + } + return ref.Module == "" || ref.Module == moduleName +} diff --git a/go-runtime/schema/common/directive.go b/go-runtime/schema/common/directive.go new file mode 100644 index 0000000000..47c68fd442 --- /dev/null +++ b/go-runtime/schema/common/directive.go @@ -0,0 +1,316 @@ +package common + +import ( + "errors" + "fmt" + "go/ast" + "go/token" + "strconv" + "strings" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/internal/cron" + "github.com/TBD54566975/golang-tools/go/analysis" + "github.com/alecthomas/participle/v2" +) + +// This file contains a parser for Go FTL directives. + +type directiveWrapper struct { + Directive Directive `parser:"'ftl' ':' @@"` +} + +// Directive is a directive in a Go FTL module, e.g. //ftl:ingress http GET /foo/bar +// +//sumtype:decl +type Directive interface { + SetPosition(pos token.Pos) + GetPosition() token.Pos + GetTypeName() string + // MustAnnotate returns the AST nodes that can be annotated by this directive. + MustAnnotate() []ast.Node + + directive() +} + +type Exportable interface { + IsExported() bool +} + +type DirectiveVerb struct { + Pos token.Pos + + Verb bool `parser:"@'verb'"` + Export bool `parser:"@'export'?"` +} + +func (*DirectiveVerb) directive() {} +func (d *DirectiveVerb) String() string { + if d.Export { + return "ftl:verb export" + } + return "ftl:verb" +} +func (d *DirectiveVerb) IsExported() bool { + return d.Export +} +func (*DirectiveVerb) GetTypeName() string { return "verb" } +func (d *DirectiveVerb) SetPosition(pos token.Pos) { + d.Pos = pos +} +func (d *DirectiveVerb) GetPosition() token.Pos { + return d.Pos +} +func (*DirectiveVerb) MustAnnotate() []ast.Node { return []ast.Node{&ast.FuncDecl{}} } + +type DirectiveData struct { + Pos token.Pos + + Data bool `parser:"@'data'"` + Export bool `parser:"@'export'?"` +} + +func (*DirectiveData) directive() {} +func (d *DirectiveData) String() string { + if d.Export { + return "ftl:data export" + } + return "ftl:data" +} +func (d *DirectiveData) IsExported() bool { + return d.Export +} +func (*DirectiveData) GetTypeName() string { return "data" } +func (d *DirectiveData) SetPosition(pos token.Pos) { + d.Pos = pos +} +func (d *DirectiveData) GetPosition() token.Pos { + return d.Pos +} +func (*DirectiveData) MustAnnotate() []ast.Node { return []ast.Node{&ast.GenDecl{}} } + +type DirectiveEnum struct { + Pos token.Pos + + Enum bool `parser:"@'enum'"` + Export bool `parser:"@'export'?"` +} + +func (*DirectiveEnum) directive() {} +func (d *DirectiveEnum) String() string { + if d.Export { + return "ftl:enum export" + } + return "ftl:enum" +} +func (d *DirectiveEnum) IsExported() bool { + return d.Export +} +func (*DirectiveEnum) GetTypeName() string { return "enum" } +func (d *DirectiveEnum) SetPosition(pos token.Pos) { + d.Pos = pos +} +func (d *DirectiveEnum) GetPosition() token.Pos { + return d.Pos +} +func (*DirectiveEnum) MustAnnotate() []ast.Node { return []ast.Node{&ast.GenDecl{}} } + +type DirectiveTypeAlias struct { + Pos token.Pos + + TypeAlias bool `parser:"@'typealias'"` + Export bool `parser:"@'export'?"` +} + +func (*DirectiveTypeAlias) directive() {} +func (d *DirectiveTypeAlias) String() string { + if d.Export { + return "ftl:typealias export" + } + return "ftl:typealias" +} +func (d *DirectiveTypeAlias) IsExported() bool { + return d.Export +} +func (*DirectiveTypeAlias) GetTypeName() string { return "typealias" } +func (d *DirectiveTypeAlias) SetPosition(pos token.Pos) { + d.Pos = pos +} +func (d *DirectiveTypeAlias) GetPosition() token.Pos { + return d.Pos +} +func (*DirectiveTypeAlias) MustAnnotate() []ast.Node { + return []ast.Node{&ast.GenDecl{}} +} + +type DirectiveIngress struct { + Pos token.Pos + + Type string `parser:"'ingress' @('http')?"` + Method string `parser:"@('GET' | 'POST' | 'PUT' | 'DELETE')"` + Path []schema.IngressPathComponent `parser:"('/' @@)+"` +} + +func (*DirectiveIngress) directive() {} +func (d *DirectiveIngress) String() string { + w := &strings.Builder{} + fmt.Fprintf(w, "ftl:ingress %s", d.Method) + for _, p := range d.Path { + fmt.Fprintf(w, "/%s", p) + } + return w.String() +} +func (d *DirectiveIngress) IsExported() bool { + return true +} +func (*DirectiveIngress) GetTypeName() string { return "ingress" } +func (d *DirectiveIngress) SetPosition(pos token.Pos) { + d.Pos = pos +} +func (d *DirectiveIngress) GetPosition() token.Pos { + return d.Pos +} +func (*DirectiveIngress) MustAnnotate() []ast.Node { + return []ast.Node{&ast.FuncDecl{}} +} + +type DirectiveCronJob struct { + Pos token.Pos + + Cron cron.Pattern `parser:"'cron' @@"` +} + +func (*DirectiveCronJob) directive() {} + +func (d *DirectiveCronJob) String() string { + return fmt.Sprintf("cron %s", d.Cron) +} +func (d *DirectiveCronJob) IsExported() bool { + return false +} +func (*DirectiveCronJob) GetTypeName() string { return "cron" } +func (d *DirectiveCronJob) SetPosition(pos token.Pos) { + d.Pos = pos +} +func (d *DirectiveCronJob) GetPosition() token.Pos { + return d.Pos +} +func (*DirectiveCronJob) MustAnnotate() []ast.Node { + return []ast.Node{&ast.FuncDecl{}} +} + +type DirectiveRetry struct { + Pos token.Pos + + Count *int `parser:"'retry' (@Number Whitespace)?"` + MinBackoff string `parser:"@(Number (?! Whitespace) Ident)?"` + MaxBackoff string `parser:"@(Number (?! Whitespace) Ident)?"` +} + +func (*DirectiveRetry) directive() {} + +func (d *DirectiveRetry) String() string { + components := []string{"retry"} + if d.Count != nil { + components = append(components, strconv.Itoa(*d.Count)) + } + components = append(components, d.MinBackoff) + if len(d.MaxBackoff) > 0 { + components = append(components, d.MaxBackoff) + } + return strings.Join(components, " ") +} +func (*DirectiveRetry) GetTypeName() string { return "retry" } +func (d *DirectiveRetry) SetPosition(pos token.Pos) { + d.Pos = pos +} +func (d *DirectiveRetry) GetPosition() token.Pos { + return d.Pos +} +func (*DirectiveRetry) MustAnnotate() []ast.Node { + return []ast.Node{&ast.FuncDecl{}, &ast.GenDecl{}} +} + +// DirectiveSubscriber is used to subscribe a sink to a subscription +type DirectiveSubscriber struct { + Pos token.Pos + + Name string `parser:"'subscribe' @Ident"` +} + +func (*DirectiveSubscriber) directive() {} + +func (d *DirectiveSubscriber) String() string { + return fmt.Sprintf("subscribe %s", d.Name) +} +func (*DirectiveSubscriber) GetTypeName() string { return "subscribe" } +func (d *DirectiveSubscriber) SetPosition(pos token.Pos) { + d.Pos = pos +} +func (d *DirectiveSubscriber) GetPosition() token.Pos { + return d.Pos +} +func (*DirectiveSubscriber) MustAnnotate() []ast.Node { + return []ast.Node{&ast.FuncDecl{}} +} + +// DirectiveExport is used on declarations that don't include export in other directives. +type DirectiveExport struct { + Pos token.Pos + + Export bool `parser:"@'export'"` +} + +func (*DirectiveExport) directive() {} + +func (d *DirectiveExport) String() string { + return "export" +} +func (*DirectiveExport) GetTypeName() string { return "export" } +func (d *DirectiveExport) SetPosition(pos token.Pos) { + d.Pos = pos +} +func (d *DirectiveExport) GetPosition() token.Pos { + return d.Pos +} +func (*DirectiveExport) MustAnnotate() []ast.Node { return []ast.Node{&ast.GenDecl{}} } + +var directiveParser = participle.MustBuild[directiveWrapper]( + participle.Lexer(schema.Lexer), + participle.Elide("Whitespace"), + participle.Unquote(), + participle.UseLookahead(2), + participle.Union[Directive](&DirectiveVerb{}, &DirectiveData{}, &DirectiveEnum{}, &DirectiveTypeAlias{}, + &DirectiveIngress{}, &DirectiveCronJob{}, &DirectiveRetry{}, &DirectiveSubscriber{}, &DirectiveExport{}), + participle.Union[schema.IngressPathComponent](&schema.IngressPathLiteral{}, &schema.IngressPathParameter{}), +) + +func ParseDirectives(pass *analysis.Pass, node ast.Node, docs *ast.CommentGroup) []Directive { + if docs == nil { + return nil + } + var directives []Directive + for _, line := range docs.List { + if !strings.HasPrefix(line.Text, "//ftl:") { + continue + } + pos := pass.Fset.Position(line.Pos()) + // TODO: We need to adjust position information embedded in the schema. + directive, err := directiveParser.ParseString(pos.Filename, line.Text[2:]) + file := pass.Fset.File(node.Pos()) + startPos := file.Pos(file.Offset(line.Pos()) + 2) + if err != nil { + // Adjust the Participle-reported position relative to the AST node. + var perr participle.Error + if errors.As(err, &perr) { + errorfAtPos(pass, startPos, file.Pos(file.Offset(line.End())), "%s", perr.Message()) + } else { + Wrapf(pass, node, err, "") + } + return nil + } + directive.Directive.SetPosition(startPos) + directives = append(directives, directive.Directive) + } + return directives +} diff --git a/go-runtime/schema/common/error.go b/go-runtime/schema/common/error.go new file mode 100644 index 0000000000..a475600b7a --- /dev/null +++ b/go-runtime/schema/common/error.go @@ -0,0 +1,72 @@ +package common + +import ( + "fmt" + "go/ast" + "go/token" + "unicode/utf8" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/golang-tools/go/analysis" +) + +type DiagnosticCategory string + +const ( + Info DiagnosticCategory = "info" + Warn DiagnosticCategory = "warn" + Error DiagnosticCategory = "error" +) + +func (e DiagnosticCategory) ToErrorLevel() schema.ErrorLevel { + switch e { + case Info: + return schema.INFO + case Warn: + return schema.WARN + case Error: + return schema.ERROR + default: + panic(fmt.Sprintf("unknown diagnostic category %q", e)) + } +} + +func Errorf(pass *analysis.Pass, node ast.Node, format string, args ...interface{}) { + errorfAtPos(pass, node.Pos(), node.End(), format, args...) +} + +func TokenErrorf(pass *analysis.Pass, pos token.Pos, tokenText string, format string, args ...interface{}) { + endPos := pos + if len(tokenText) > 0 { + endPos = pos + token.Pos(utf8.RuneCountInString(tokenText)) + } + errorfAtPos(pass, pos, endPos, format, args...) +} + +func Wrapf(pass *analysis.Pass, node ast.Node, err error, format string, args ...interface{}) { + if format == "" { + format = "%s" + } else { + format += ": %s" + } + args = append(args, err) + errorfAtPos(pass, node.Pos(), node.End(), format, args...) +} + +func NoEndColumnErrorf(pass *analysis.Pass, pos token.Pos, format string, args ...interface{}) { + TokenErrorf(pass, pos, "", format, args...) +} + +func noEndColumnWrapf(pass *analysis.Pass, pos token.Pos, err error, format string, args ...interface{}) { + if format == "" { + format = "%s" + } else { + format += ": %s" + } + args = append(args, err) + TokenErrorf(pass, pos, "", format, args...) +} + +func errorfAtPos(pass *analysis.Pass, pos token.Pos, end token.Pos, format string, args ...interface{}) { + pass.Report(analysis.Diagnostic{Pos: pos, End: end, Message: fmt.Sprintf(format, args...), Category: string(Error)}) +} diff --git a/go-runtime/schema/common/fact.go b/go-runtime/schema/common/fact.go new file mode 100644 index 0000000000..701c720f99 --- /dev/null +++ b/go-runtime/schema/common/fact.go @@ -0,0 +1,196 @@ +package common + +import ( + "go/types" + "reflect" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/golang-tools/go/analysis" + "github.com/alecthomas/types/optional" + sets "github.com/deckarep/golang-set/v2" +) + +// SchemaFact is a fact that associates a schema node with a Go object. +type SchemaFact interface { + analysis.Fact + Set(v SchemaFactValue) + Get() SchemaFactValue +} + +// SchemaFactValue is the value of a SchemaFact. +type SchemaFactValue interface { + schemaFactValue() +} + +// ExtractedDecl is a fact for associating an object with an extracted schema decl. +type ExtractedDecl struct { + Decl schema.Decl + // ShouldInclude is true if the object should be included in the schema. + // We extract all objects by default, but some objects may not actually be referenced in the schema. + ShouldInclude bool + // Refs is a list of objects that the object references. + Refs sets.Set[types.Object] +} + +func (*ExtractedDecl) schemaFactValue() {} + +// ExtractedMetadata is a fact for associating an object with extracted schema metadata. +type ExtractedMetadata struct { + Type schema.Decl + IsExported bool + Metadata []schema.Metadata + Comments []string +} + +func (*ExtractedMetadata) schemaFactValue() {} + +// NeedsExtraction is a fact for marking a type that needs to be extracted by another extractor. +type NeedsExtraction struct{} + +func (*NeedsExtraction) schemaFactValue() {} + +// FailedExtraction is a fact for marking a type that failed to be extracted by another extractor. +type FailedExtraction struct{} + +func (*FailedExtraction) schemaFactValue() {} + +// MarkSchemaDecl marks the given object as having been extracted to the given schema node. +func MarkSchemaDecl(pass *analysis.Pass, obj types.Object, decl schema.Decl) { + fact := newFact(pass) + fact.Set(&ExtractedDecl{Decl: decl, Refs: sets.NewSet[types.Object]()}) + pass.ExportObjectFact(obj, fact) +} + +// markSchemaDeclIncluded marks the given decl as included in the schema. +func markSchemaDeclIncluded(pass *analysis.Pass, obj types.Object) { + for _, f := range GetFactsForObject[*ExtractedDecl](pass, obj) { + f.ShouldInclude = true + } +} + +// MarkFailedExtraction marks the given object as having failed extraction. +func MarkFailedExtraction(pass *analysis.Pass, obj types.Object) { + fact := newFact(pass) + fact.Set(&FailedExtraction{}) + pass.ExportObjectFact(obj, fact) +} + +func MarkMetadata(pass *analysis.Pass, obj types.Object, md *ExtractedMetadata) { + fact := newFact(pass) + fact.Set(md) + pass.ExportObjectFact(obj, fact) +} + +// markNeedsExtraction marks the given object as needing extraction. +func markNeedsExtraction(pass *analysis.Pass, obj types.Object) { + fact := newFact(pass) + fact.Set(&NeedsExtraction{}) + pass.ExportObjectFact(obj, fact) +} + +// MergeAllFacts merges schema facts inclusive of all available results and the present pass facts. +// +// If multiple facts are present for the same object, the facts will be prioritized by type: +// 1. ExtractedDecl +// 2. FailedExtraction +// 4. NeedsExtraction +// +// ExtractedMetadata facts are ignored. +func MergeAllFacts(pass *analysis.Pass) map[types.Object]SchemaFact { + facts := make(map[types.Object]SchemaFact) + for _, fact := range allFactsForPass(pass) { + f, ok := fact.Fact.(SchemaFact) + if !ok { + continue + } + + // skip metadata facts + if _, ok = f.Get().(*ExtractedMetadata); ok { + continue + } + + // prioritize facts by type + // + // e.g. if one extractor marked an object as needing extraction and another extractor marked it with the + // completed extraction, we should prioritize the completed extraction. + prioritize := func(f SchemaFact) int { + switch f.Get().(type) { + case *ExtractedDecl: + return 1 + case *FailedExtraction: + return 2 + case *NeedsExtraction: + return 3 + default: + return 4 + } + } + existing, ok := facts[fact.Object] + if !ok || prioritize(f) < prioritize(existing) { + facts[fact.Object] = f + } + } + return facts +} + +func GetFact[T SchemaFactValue](facts []SchemaFact) optional.Option[T] { + for _, fact := range facts { + if f, ok := fact.Get().(T); ok { + return optional.Some(f) + } + } + return optional.None[T]() +} + +// GetFactsForObject returns all facts marked on the object. +func GetFactsForObject[T SchemaFactValue](pass *analysis.Pass, obj types.Object) []T { + var facts []T + for _, fact := range allFactsForPass(pass) { + if fact.Object != obj { + continue + } + sf, ok := fact.Fact.(SchemaFact) + if !ok { + continue + } + if f, ok := sf.Get().(T); ok { + facts = append(facts, f) + } + } + return facts +} + +// GetFactForObject returns the first fact of the provided type marked on the object. +func GetFactForObject[T SchemaFactValue](pass *analysis.Pass, obj types.Object) optional.Option[T] { + for _, fact := range allFactsForPass(pass) { + if fact.Object != obj { + continue + } + sf, ok := fact.Fact.(SchemaFact) + if !ok { + continue + } + if f, ok := sf.Get().(T); ok { + return optional.Some(f) + } + } + return optional.None[T]() +} + +func allFactsForPass(pass *analysis.Pass) []analysis.ObjectFact { + var all []analysis.ObjectFact + all = append(all, pass.AllObjectFacts()...) + for _, result := range pass.ResultOf { + r, ok := result.(ExtractorResult) + if !ok { + continue + } + all = append(all, r.Facts...) + } + return all +} + +func newFact(pass *analysis.Pass) SchemaFact { + factType := reflect.TypeOf(pass.Analyzer.FactTypes[0]).Elem() + return reflect.New(factType).Interface().(SchemaFact) //nolint:forcetypeassert +} diff --git a/go-runtime/schema/data/analyzer.go b/go-runtime/schema/data/analyzer.go new file mode 100644 index 0000000000..f77f62727e --- /dev/null +++ b/go-runtime/schema/data/analyzer.go @@ -0,0 +1,128 @@ +package data + +import ( + "go/ast" + "go/token" + "go/types" + "reflect" + "strings" + "unicode" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/backend/schema/strcase" + "github.com/TBD54566975/ftl/go-runtime/schema/common" + "github.com/TBD54566975/golang-tools/go/analysis" + "github.com/alecthomas/types/optional" +) + +var ( + // Extractor extracts schema.Data to the module schema. + Extractor = common.NewDeclExtractor[*schema.Data, *ast.TypeSpec]("data", (*Fact)(nil), Extract) + + aliasFieldTag = "json" +) + +type Fact struct { + value common.SchemaFactValue +} + +func (t *Fact) AFact() {} +func (t *Fact) Set(v common.SchemaFactValue) { t.value = v } +func (t *Fact) Get() common.SchemaFactValue { return t.value } + +func Extract(pass *analysis.Pass, node *ast.TypeSpec, obj types.Object) optional.Option[*schema.Data] { + named, ok := obj.Type().(*types.Named) + if !ok { + return optional.None[*schema.Data]() + } + if _, ok := named.Underlying().(*types.Struct); !ok { + return optional.None[*schema.Data]() + } + decl, ok := extractData(pass, node.Pos(), named).Get() + if !ok { + return optional.None[*schema.Data]() + } + return optional.Some(decl) +} + +func extractData(pass *analysis.Pass, pos token.Pos, named *types.Named) optional.Option[*schema.Data] { + fset := pass.Fset + nodePath := named.Obj().Pkg().Path() + if !common.IsPathInPkg(pass.Pkg, nodePath) { + return optional.None[*schema.Data]() + } + + out := &schema.Data{ + Pos: common.GoPosToSchemaPos(fset, pos), + Name: strcase.ToUpperCamel(named.Obj().Name()), + } + if md, ok := common.GetFactForObject[*common.ExtractedMetadata](pass, named.Obj()).Get(); ok { + if _, ok = md.Type.(*schema.Data); !ok && md.Type != nil { + return optional.None[*schema.Data]() + } + out.Comments = md.Comments + out.Export = md.IsExported + } + + for i := range named.TypeParams().Len() { + param := named.TypeParams().At(i) + out.TypeParameters = append(out.TypeParameters, &schema.TypeParameter{ + Pos: common.GoPosToSchemaPos(fset, pos), + Name: param.Obj().Name(), + }) + } + + // If the struct is generic, we need to use the origin type to get the + // fields. + if named.TypeParams().Len() > 0 { + named = named.Origin() + } + + s, ok := named.Underlying().(*types.Struct) + if !ok { + return optional.None[*schema.Data]() + } + + fieldErrors := false + for i := range s.NumFields() { + f := s.Field(i) + if ft, ok := common.ExtractType(pass, f.Pos(), f.Type()).Get(); ok { + // Check if field is exported + if len(f.Name()) > 0 && unicode.IsLower(rune(f.Name()[0])) { + common.TokenErrorf(pass, f.Pos(), f.Name(), + "struct field %s must be exported by starting with an uppercase letter", f.Name()) + fieldErrors = true + } + + // Extract the JSON tag and split it to get just the field name + tagContent := reflect.StructTag(s.Tag(i)).Get(aliasFieldTag) + tagParts := strings.Split(tagContent, ",") + jsonFieldName := "" + if len(tagParts) > 0 { + jsonFieldName = tagParts[0] + } + + var metadata []schema.Metadata + if jsonFieldName != "" { + metadata = append(metadata, &schema.MetadataAlias{ + Pos: common.GoPosToSchemaPos(pass.Fset, pos), + Kind: schema.AliasKindJSON, + Alias: jsonFieldName, + }) + } + out.Fields = append(out.Fields, &schema.Field{ + Pos: common.GoPosToSchemaPos(pass.Fset, pos), + Name: strcase.ToLowerCamel(f.Name()), + Type: ft, + Metadata: metadata, + }) + } else { + common.TokenErrorf(pass, f.Pos(), f.Name(), "unsupported type %q for field %q", f.Type(), f.Name()) + fieldErrors = true + } + } + if fieldErrors { + return optional.None[*schema.Data]() + } + return optional.Some(out) +} diff --git a/go-runtime/schema/extract.go b/go-runtime/schema/extract.go new file mode 100644 index 0000000000..7dd8df7072 --- /dev/null +++ b/go-runtime/schema/extract.go @@ -0,0 +1,191 @@ +package schema + +import ( + "fmt" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/go-runtime/schema/common" + "github.com/TBD54566975/ftl/go-runtime/schema/data" + "github.com/TBD54566975/ftl/go-runtime/schema/finalize" + "github.com/TBD54566975/ftl/go-runtime/schema/initialize" + "github.com/TBD54566975/ftl/go-runtime/schema/metadata" + "github.com/TBD54566975/ftl/go-runtime/schema/transitive" + "github.com/TBD54566975/ftl/go-runtime/schema/typealias" + "github.com/TBD54566975/ftl/go-runtime/schema/verb" + "github.com/TBD54566975/golang-tools/go/analysis" + "github.com/TBD54566975/golang-tools/go/analysis/passes/inspect" + checker "github.com/TBD54566975/golang-tools/go/analysis/programmaticchecker" + "github.com/TBD54566975/golang-tools/go/packages" + "golang.org/x/exp/maps" +) + +// Extractors contains all schema extractors that will run. +// +// It is a list of lists, where each list is a round of tasks dependent on the prior round's execution (e.g. an analyzer +// in Extractors[1] will only execute once all analyzers in Extractors[0] complete). Elements of the same list +// should be considered unordered and may run in parallel. +var Extractors = [][]*analysis.Analyzer{ + { + initialize.Analyzer, + inspect.Analyzer, + }, + { + metadata.Extractor, + }, + { + typealias.Extractor, + verb.Extractor, + data.Extractor, + }, + { + transitive.Extractor, + }, + { + finalize.Analyzer, + }, +} + +// Extract statically parses Go FTL module source into a schema.Module +func Extract(moduleDir string) (finalize.Result, error) { + pkgConfig := packages.Config{ + Dir: moduleDir, + Mode: packages.NeedName | packages.NeedFiles | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedImports, + } + cConfig := checker.Config{ + LoadConfig: pkgConfig, + ReverseImportExecutionOrder: true, + Patterns: []string{"./..."}, + } + results, diagnostics, err := checker.Run(cConfig, analyzersWithDependencies()...) + if err != nil { + return finalize.Result{}, err + } + return combineAllPackageResults(results, diagnostics) +} + +func analyzersWithDependencies() []*analysis.Analyzer { + var as []*analysis.Analyzer + // observes dependencies as specified by tiered list ordering in Extractors and applies the dependency + // requirements to the analyzers + // + // flattens Extractors (a list of lists) into a single list to provide as input for the checker + for i, extractorRound := range Extractors { + for _, extractor := range extractorRound { + extractor.RunDespiteErrors = true + extractor.Requires = append(extractor.Requires, dependenciesBeforeIndex(i)...) + as = append(as, extractor) + } + } + return as +} + +// the run will produce finalizer results for all packages it executes on, so we need to aggregate the results into a +// single schema +func combineAllPackageResults(results map[*analysis.Analyzer][]any, diagnostics []analysis.SimpleDiagnostic) (finalize.Result, error) { + fResults, ok := results[finalize.Analyzer] + if !ok { + return finalize.Result{}, fmt.Errorf("schema extraction finalizer result not found") + } + combined := finalize.Result{ + NativeNames: make(map[schema.Node]string), + Errors: diagnosticsToSchemaErrors(diagnostics), + } + for _, r := range fResults { + fr, ok := r.(finalize.Result) + if !ok { + return finalize.Result{}, fmt.Errorf("unexpected schema extraction result type: %T", r) + } + + if combined.Module == nil { + combined.Module = fr.Module + } else { + if combined.Module.Name != fr.Module.Name { + return finalize.Result{}, fmt.Errorf("unexpected schema extraction result module name: %s", fr.Module.Name) + } + combined.Module.AddDecls(fr.Module.Decls) + } + maps.Copy(combined.NativeNames, fr.NativeNames) + } + schema.SortErrorsByPosition(combined.Errors) + updateVisibility(combined.Module) + // TODO: validate schema once we have the full schema here + return combined, nil +} + +func dependenciesBeforeIndex(idx int) []*analysis.Analyzer { + var deps []*analysis.Analyzer + for i := range idx { + deps = append(deps, Extractors[i]...) + } + return deps +} + +// updateVisibility traverses the module schema via refs and updates visibility as needed. +func updateVisibility(module *schema.Module) { + for _, d := range module.Decls { + if d.IsExported() { + updateTransitiveVisibility(d, module) + } + } +} + +// updateTransitiveVisibility updates any decls that are transitively visible from d. +func updateTransitiveVisibility(d schema.Decl, module *schema.Module) { + if !d.IsExported() { + return + } + + _ = schema.Visit(d, func(n schema.Node, next func() error) error { + ref, ok := n.(*schema.Ref) + if !ok { + return next() + } + + resolved := module.Resolve(*ref) + if resolved == nil || resolved.Symbol == nil { + return next() + } + + if decl, ok := resolved.Symbol.(schema.Decl); ok { + switch t := decl.(type) { + case *schema.Data: + t.Export = true + case *schema.Enum: + t.Export = true + case *schema.TypeAlias: + t.Export = true + case *schema.Topic: + t.Export = true + case *schema.Verb: + t.Export = true + case *schema.Database, *schema.Config, *schema.FSM, *schema.Secret, *schema.Subscription: + } + } + return next() + }) +} + +func diagnosticsToSchemaErrors(diagnostics []analysis.SimpleDiagnostic) []*schema.Error { + if len(diagnostics) == 0 { + return nil + } + errors := make([]*schema.Error, 0, len(diagnostics)) + for _, d := range diagnostics { + errors = append(errors, &schema.Error{ + Pos: simplePosToSchemaPos(d.Pos), + EndColumn: d.End.Column, + Msg: d.Message, + Level: common.DiagnosticCategory(d.Category).ToErrorLevel(), + }) + } + return errors +} + +func simplePosToSchemaPos(pos analysis.SimplePosition) schema.Position { + return schema.Position{ + Filename: pos.Filename, + Offset: pos.Offset, + Line: pos.Line, + Column: pos.Column, + } +} diff --git a/go-runtime/schema/extractor.go b/go-runtime/schema/extractor.go deleted file mode 100644 index d868ef636a..0000000000 --- a/go-runtime/schema/extractor.go +++ /dev/null @@ -1,72 +0,0 @@ -package schema - -import ( - "fmt" - - "github.com/TBD54566975/ftl/backend/schema" - "github.com/TBD54566975/ftl/go-runtime/schema/analyzers" - "github.com/TBD54566975/golang-tools/go/analysis" - checker "github.com/TBD54566975/golang-tools/go/analysis/programmaticchecker" - "github.com/TBD54566975/golang-tools/go/packages" -) - -// Extract statically parses Go FTL module source into a schema.Module -func Extract(moduleDir string) (analyzers.ExtractResult, error) { - pkgConfig := packages.Config{ - Dir: moduleDir, - Mode: packages.NeedName | packages.NeedFiles | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedImports, - } - cConfig := checker.Config{ - LoadConfig: pkgConfig, - RunDespiteLoadErrors: true, - Patterns: []string{"./..."}, - } - results, diagnostics, err := checker.Run(cConfig, append(analyzers.Extractors, analyzers.Finalizer)...) - if err != nil { - return analyzers.ExtractResult{}, err - } - fResult, ok := results[analyzers.Finalizer] - if !ok { - return analyzers.ExtractResult{}, fmt.Errorf("schema extraction finalizer result not found") - } - - if len(fResult) == 0 { - return analyzers.ExtractResult{}, fmt.Errorf("schema extraction finalizer result is empty") - } - - r, ok := fResult[0].(analyzers.ExtractResult) - if !ok { - return analyzers.ExtractResult{}, fmt.Errorf("unexpected schema extraction result type: %T", fResult[0]) - } - - errors := diagnosticsToSchemaErrors(diagnostics) - schema.SortErrorsByPosition(errors) - r.Errors = errors - - return r, nil -} - -func diagnosticsToSchemaErrors(diagnostics []analysis.SimpleDiagnostic) []*schema.Error { - if len(diagnostics) == 0 { - return nil - } - errors := make([]*schema.Error, 0, len(diagnostics)) - for _, d := range diagnostics { - errors = append(errors, &schema.Error{ - Pos: simplePosToSchemaPos(d.Pos), - EndColumn: d.End.Column, - Msg: d.Message, - Level: analyzers.DiagnosticCategory(d.Category).ToErrorLevel(), - }) - } - return errors -} - -func simplePosToSchemaPos(pos analysis.SimplePosition) schema.Position { - return schema.Position{ - Filename: pos.Filename, - Offset: pos.Offset, - Line: pos.Line, - Column: pos.Column, - } -} diff --git a/go-runtime/schema/finalize/analyzer.go b/go-runtime/schema/finalize/analyzer.go new file mode 100644 index 0000000000..64d042d351 --- /dev/null +++ b/go-runtime/schema/finalize/analyzer.go @@ -0,0 +1,133 @@ +package finalize + +import ( + "go/ast" + "go/types" + "reflect" + "strings" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/go-runtime/schema/common" + "github.com/TBD54566975/golang-tools/go/analysis" + "github.com/TBD54566975/golang-tools/go/analysis/passes/inspect" + "github.com/TBD54566975/golang-tools/go/ast/inspector" + sets "github.com/deckarep/golang-set/v2" + "golang.org/x/exp/maps" +) + +// Analyzer aggregates the results of all extractors. +var Analyzer = &analysis.Analyzer{ + Name: "finalizer", + Doc: "finalizes module schema and writes to the output destination", + Run: Run, + ResultType: reflect.TypeFor[Result](), + RunDespiteErrors: true, +} + +// Result contains the final schema extraction result. +type Result struct { + // Module is the extracted module schema. + Module *schema.Module + // NativeNames maps schema nodes to their native Go names. + NativeNames map[schema.Node]string + // Errors is a list of errors encountered during schema extraction. + Errors []*schema.Error +} + +func Run(pass *analysis.Pass) (interface{}, error) { + moduleName, err := common.FtlModuleFromGoPackage(pass.Pkg.Path()) + if err != nil { + return nil, err + } + module := &schema.Module{ + Name: moduleName, + Comments: extractModuleComments(pass), + } + result := combineExtractorResults(pass, moduleName) + module.AddDecls(result.decls) + return Result{ + Module: module, + NativeNames: result.nativeNames, + }, nil +} + +func extractModuleComments(pass *analysis.Pass) []string { + in := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) //nolint:forcetypeassert + nodeFilter := []ast.Node{ + (*ast.File)(nil), + } + var comments []string + in.Preorder(nodeFilter, func(n ast.Node) { + if len(strings.Split(pass.Pkg.Path(), "/")) > 2 { + // skip subpackages + return + } + comments = common.ExtractComments(n.(*ast.File).Doc) //nolint:forcetypeassert + }) + return comments +} + +type combinedResult struct { + decls []schema.Decl + nativeNames map[schema.Node]string +} + +func combineExtractorResults(pass *analysis.Pass, moduleName string) combinedResult { + nn := make(map[schema.Node]string) + extracted := make(map[types.Object]schema.Decl) + failed := sets.NewSet[schema.RefKey]() + for obj, fact := range common.MergeAllFacts(pass) { + switch f := fact.Get().(type) { + case *common.ExtractedDecl: + if f.Decl != nil { + extracted[obj] = f.Decl + } + nn[f.Decl] = obj.Pkg().Path() + "." + obj.Name() + case *common.FailedExtraction: + failed.Add(schema.RefKey{Module: moduleName, Name: obj.Name()}) + } + } + propagateTypeErrors(pass, extracted, failed) + return combinedResult{ + nativeNames: nn, + decls: maps.Values(extracted), + } +} + +// propagateTypeErrors propagates type errors to referencing nodes. This improves error messaging for the LSP client by +// surfacing errors all the way up the schema chain. +func propagateTypeErrors(pass *analysis.Pass, extracted map[types.Object]schema.Decl, failed sets.Set[schema.RefKey]) { + for obj, sch := range extracted { + switch t := sch.(type) { + case *schema.Verb: + fnt := obj.(*types.Func) //nolint:forcetypeassert + sig := fnt.Type().(*types.Signature) //nolint:forcetypeassert + params := sig.Params() + results := sig.Results() + if hasFailedRef(t.Request, failed) { + common.TokenErrorf(pass, params.At(1).Pos(), params.At(1).Name(), + "unsupported request type %q", params.At(1).Type()) + } + if hasFailedRef(t.Response, failed) { + common.TokenErrorf(pass, results.At(0).Pos(), results.At(0).Name(), + "unsupported response type %q", results.At(0).Type()) + } + default: + } + } +} + +func hasFailedRef(node schema.Node, failedRefs sets.Set[schema.RefKey]) bool { + failed := false + _ = schema.Visit(node, func(n schema.Node, next func() error) error { + ref, ok := n.(*schema.Ref) + if !ok { + return next() + } + if failedRefs.Contains(ref.ToRefKey()) { + failed = true + } + return next() + }) + return failed +} diff --git a/go-runtime/schema/initialize/analyzer.go b/go-runtime/schema/initialize/analyzer.go new file mode 100644 index 0000000000..0cefd0eacc --- /dev/null +++ b/go-runtime/schema/initialize/analyzer.go @@ -0,0 +1,82 @@ +package initialize + +import ( + "fmt" + "go/token" + "go/types" + "reflect" + "strings" + + "github.com/TBD54566975/ftl/internal/slices" + "github.com/TBD54566975/golang-tools/go/analysis" + "github.com/TBD54566975/golang-tools/go/packages" +) + +// Analyzer prepares data prior to the schema extractor runs, e.g. loads FTL types for reference by other +// analyzers. +var Analyzer = &analysis.Analyzer{ + Name: "initialize", + Doc: "loads data to be used by other analyzers in the schema extractor pass", + Run: Run, + ResultType: reflect.TypeFor[Result](), + RunDespiteErrors: true, +} + +type Result struct { + types map[string]*types.Interface +} + +// IsFtlErrorType will return true if the provided type is assertable to the `builtin.error` type. +func (r Result) IsFtlErrorType(typ types.Type) bool { + return r.assertableToType(typ, "builtin", "error") +} + +// IsContextType will return true if the provided type is assertable to the `context.Context` type. +func (r Result) IsContextType(typ types.Type) bool { + return r.assertableToType(typ, "context", "Context") +} + +func (r Result) assertableToType(typ types.Type, pkg string, name string) bool { + ityp, ok := r.types[pkg+"."+name] + if !ok { + return false + } + return types.AssertableTo(ityp, typ) +} + +func Run(pass *analysis.Pass) (interface{}, error) { + ctxType, err := loadRef("context", "Context") + if err != nil { + return nil, err + } + errType, err := loadRef("builtin", "error") + if err != nil { + return nil, err + } + + return Result{types: map[string]*types.Interface{ + "context.Context": ctxType, + "builtin.error": errType, + }}, nil +} + +// Lazy load the compile-time reference from a package. +func loadRef(pkg, name string) (*types.Interface, error) { + pkgs, err := packages.Load(&packages.Config{Fset: token.NewFileSet(), Mode: packages.NeedTypes}, pkg) + if err != nil { + return nil, err + } + if len(pkgs) != 1 { + return nil, fmt.Errorf("expected one package, got %s", + strings.Join(slices.Map(pkgs, func(p *packages.Package) string { return p.Name }), ", ")) + } + obj := pkgs[0].Types.Scope().Lookup(name) + if obj == nil { + return nil, fmt.Errorf("interface %q not found", name) + } + ifaceType, ok := obj.Type().Underlying().(*types.Interface) + if !ok { + return nil, fmt.Errorf("expected an interface, got %s", obj.Type()) + } + return ifaceType, nil +} diff --git a/go-runtime/schema/metadata/analyzer.go b/go-runtime/schema/metadata/analyzer.go new file mode 100644 index 0000000000..eb32288fcc --- /dev/null +++ b/go-runtime/schema/metadata/analyzer.go @@ -0,0 +1,188 @@ +package metadata + +import ( + "go/ast" + "go/token" + "reflect" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/go-runtime/schema/common" + "github.com/TBD54566975/golang-tools/go/analysis" + "github.com/TBD54566975/golang-tools/go/analysis/passes/inspect" + "github.com/TBD54566975/golang-tools/go/ast/inspector" + "github.com/alecthomas/types/optional" + sets "github.com/deckarep/golang-set/v2" +) + +// Extractor extracts metadata to the module schema. +var Extractor = common.NewExtractor("metadata", (*Fact)(nil), Extract) + +type Fact struct { + value common.SchemaFactValue +} + +func (t *Fact) AFact() {} +func (t *Fact) Set(v common.SchemaFactValue) { t.value = v } +func (t *Fact) Get() common.SchemaFactValue { return t.value } + +func Extract(pass *analysis.Pass) (interface{}, error) { + in := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) //nolint:forcetypeassert + nodeFilter := []ast.Node{ + (*ast.GenDecl)(nil), + (*ast.FuncDecl)(nil), + } + in.Preorder(nodeFilter, func(n ast.Node) { + var doc *ast.CommentGroup + switch n := n.(type) { + case *ast.TypeSpec: + doc = n.Doc + case *ast.GenDecl: + doc = n.Doc + if doc == nil && len(n.Specs) > 0 { + if ts, ok := n.Specs[0].(*ast.TypeSpec); ok { + doc = ts.Doc + } + } + case *ast.FuncDecl: + doc = n.Doc + } + if mdFact, ok := extractMetadata(pass, n, doc).Get(); ok { + obj, ok := common.GetObjectForNode(pass.TypesInfo, n).Get() + if !ok { + return + } + common.MarkMetadata(pass, obj, mdFact) + } + }) + return common.NewExtractorResult(pass), nil +} + +func extractMetadata(pass *analysis.Pass, node ast.Node, doc *ast.CommentGroup) optional.Option[*common.ExtractedMetadata] { + if doc == nil { + return optional.None[*common.ExtractedMetadata]() + } + directives := common.ParseDirectives(pass, node, doc) + found := sets.NewSet[string]() + exported := isExported(directives) + var declType schema.Decl + var metadata []schema.Metadata + for _, dir := range directives { + var newSchType schema.Decl + if found.Contains(dir.GetTypeName()) { + common.Errorf(pass, node, `expected exactly one "ftl:%s" directive but found multiple`, + dir.GetTypeName()) + continue + } + found.Add(dir.GetTypeName()) + + if !isAnnotatingValidGoNode(dir, node) { + if _, ok := node.(*ast.FuncDecl); ok { + common.NoEndColumnErrorf(pass, dir.GetPosition(), "unexpected directive \"ftl:%s\" attached "+ + "for verb, did you mean to use '//ftl:verb export' instead?", dir.GetTypeName()) + continue + } + + common.NoEndColumnErrorf(pass, dir.GetPosition(), "unexpected directive \"ftl:%s\"", + dir.GetTypeName()) + continue + } + + switch dt := dir.(type) { + case *common.DirectiveIngress: + newSchType = &schema.Verb{} + typ := dt.Type + if typ == "" { + typ = "http" + } + metadata = append(metadata, &schema.MetadataIngress{ + Pos: common.GoPosToSchemaPos(pass.Fset, dt.GetPosition()), + Type: typ, + Method: dt.Method, + Path: dt.Path, + }) + case *common.DirectiveCronJob: + newSchType = &schema.Verb{} + if exported { + common.NoEndColumnErrorf(pass, dt.GetPosition(), "ftl:cron cannot be attached to exported verbs") + continue + } + metadata = append(metadata, &schema.MetadataCronJob{ + Pos: common.GoPosToSchemaPos(pass.Fset, dt.Pos), + Cron: dt.Cron.String(), + }) + case *common.DirectiveRetry: + newSchType = &schema.Verb{} + metadata = append(metadata, &schema.MetadataRetry{ + Pos: common.GoPosToSchemaPos(pass.Fset, dt.Pos), + Count: dt.Count, + MinBackoff: dt.MinBackoff, + MaxBackoff: dt.MaxBackoff, + }) + case *common.DirectiveSubscriber: + newSchType = &schema.Verb{} + metadata = append(metadata, &schema.MetadataSubscriber{ + Pos: common.GoPosToSchemaPos(pass.Fset, dt.Pos), + Name: dt.Name, + }) + case *common.DirectiveVerb: + newSchType = &schema.Verb{} + case *common.DirectiveData: + requireOnlyDirective(pass, node, directives, dt.GetTypeName()) + newSchType = &schema.Data{} + case *common.DirectiveEnum: + requireOnlyDirective(pass, node, directives, dt.GetTypeName()) + newSchType = &schema.Enum{} + case *common.DirectiveTypeAlias: + requireOnlyDirective(pass, node, directives, dt.GetTypeName()) + newSchType = &schema.TypeAlias{} + case *common.DirectiveExport: + requireOnlyDirective(pass, node, directives, dt.GetTypeName()) + } + declType = updateDeclType(pass, node.Pos(), declType, newSchType) + } + + return optional.Some(&common.ExtractedMetadata{ + Type: declType, + Metadata: metadata, + IsExported: exported, + Comments: common.ExtractComments(doc), + }) +} + +func requireOnlyDirective(pass *analysis.Pass, node ast.Node, directives []common.Directive, typeName string) { + if len(directives) > 1 { + common.Errorf(pass, node, "only one directive expected when directive \"ftl:%s\" is present, "+ + "found multiple", typeName) + } +} + +func updateDeclType(pass *analysis.Pass, pos token.Pos, a schema.Decl, b schema.Decl) schema.Decl { + if a == nil { + return b + } + if b == nil { + return a + } + if reflect.TypeOf(a) != reflect.TypeOf(b) { + common.NoEndColumnErrorf(pass, pos, "schema declaration contains conflicting directives") + } + return b +} + +func isExported(directives []common.Directive) bool { + for _, d := range directives { + if exportable, ok := d.(common.Exportable); ok { + return exportable.IsExported() + } + } + return false +} + +func isAnnotatingValidGoNode(dir common.Directive, node ast.Node) bool { + for _, n := range dir.MustAnnotate() { + if reflect.TypeOf(n) == reflect.TypeOf(node) { + return true + } + } + return false +} diff --git a/go-runtime/schema/transitive/analyzer.go b/go-runtime/schema/transitive/analyzer.go new file mode 100644 index 0000000000..da6eac8a94 --- /dev/null +++ b/go-runtime/schema/transitive/analyzer.go @@ -0,0 +1,94 @@ +package transitive + +import ( + "go/ast" + "go/types" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/go-runtime/schema/common" + "github.com/TBD54566975/golang-tools/go/analysis" + "github.com/TBD54566975/golang-tools/go/analysis/passes/inspect" + "github.com/TBD54566975/golang-tools/go/ast/inspector" + sets "github.com/deckarep/golang-set/v2" +) + +// Extractor extracts transitive schema.Decls to the module schema. +// +// This extractor is used to extract schema.Decls that are implicitly included in the schema via other schema.Decls +// but not themselves explicitly annotated. +var Extractor = common.NewExtractor("transitive", (*Fact)(nil), Extract) + +type Fact struct { + value common.SchemaFactValue +} + +func (t *Fact) AFact() {} +func (t *Fact) Set(v common.SchemaFactValue) { t.value = v } +func (t *Fact) Get() common.SchemaFactValue { return t.value } + +// Extract traverses all schema type root AST nodes and determines if a node has been marked for extraction. +// +// Transitive data decls are marked via "facts", annotating the object which must be extracted to the schema with +// common.NeedsExtraction. This allows us to identify objects for extraction that are not explicitly +// annotated with an FTL directive. +func Extract(pass *analysis.Pass) (interface{}, error) { + needsExtraction := sets.NewSet[types.Object]() + for obj, fact := range common.MergeAllFacts(pass) { + if _, ok := fact.Get().(*common.NeedsExtraction); ok { + needsExtraction.Add(obj) + } + } + for !needsExtraction.IsEmpty() { + extractTransitive(pass, needsExtraction) + needsExtraction = refreshNeedsExtraction(pass) + } + return common.NewExtractorResult(pass), nil +} + +func extractTransitive(pass *analysis.Pass, needsExtraction sets.Set[types.Object]) { + in := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) //nolint:forcetypeassert + nodeFilter := []ast.Node{ + (*ast.TypeSpec)(nil), + (*ast.FuncDecl)(nil), + } + in.Preorder(nodeFilter, func(n ast.Node) { + obj, ok := common.GetObjectForNode(pass.TypesInfo, n).Get() + if !ok { + return + } + if !needsExtraction.Contains(obj) { + return + } + schType, ok := common.InferDeclType(pass, n, obj).Get() + if !ok { + // if we can't infer the type, try to extract it as data + schType = &schema.Data{} + } + extract, err := common.ExtractFuncForDecl(schType) + if err != nil { + // unmigrated, skip + // temporarily marking as extracted to avoid infinite loop + common.MarkSchemaDecl(pass, obj, nil) + return + } + if decl, ok := extract(pass, n, obj).Get(); ok { + common.MarkSchemaDecl(pass, obj, decl) + } else { + common.MarkFailedExtraction(pass, obj) + } + }) +} + +func refreshNeedsExtraction(pass *analysis.Pass) sets.Set[types.Object] { + facts := sets.NewSet[types.Object]() + for _, fact := range pass.AllObjectFacts() { + f, ok := fact.Fact.(common.SchemaFact) + if !ok { + continue + } + if _, ok := f.Get().(*common.NeedsExtraction); ok { + facts.Add(fact.Object) + } + } + return facts +} diff --git a/go-runtime/schema/typealias/analyzer.go b/go-runtime/schema/typealias/analyzer.go new file mode 100644 index 0000000000..7c475ff579 --- /dev/null +++ b/go-runtime/schema/typealias/analyzer.go @@ -0,0 +1,46 @@ +package typealias + +import ( + "go/ast" + "go/types" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/backend/schema/strcase" + "github.com/TBD54566975/ftl/go-runtime/schema/common" + "github.com/TBD54566975/golang-tools/go/analysis" + "github.com/alecthomas/types/optional" +) + +// Extractor extracts type aliases to the module schema. +var Extractor = common.NewDeclExtractor[*schema.TypeAlias, *ast.TypeSpec]("typealias", (*Fact)(nil), Extract) + +type Fact struct { + value common.SchemaFactValue +} + +func (t *Fact) AFact() {} +func (t *Fact) Set(v common.SchemaFactValue) { t.value = v } +func (t *Fact) Get() common.SchemaFactValue { return t.value } + +func Extract(pass *analysis.Pass, node *ast.TypeSpec, obj types.Object) optional.Option[*schema.TypeAlias] { + schType, ok := common.ExtractTypeForNode(pass, obj, node, nil).Get() + if !ok { + return optional.None[*schema.TypeAlias]() + } + if common.IsSelfReference(pass, obj, schType) { + return optional.None[*schema.TypeAlias]() + } + alias := &schema.TypeAlias{ + Pos: common.GoPosToSchemaPos(pass.Fset, node.Pos()), + Name: strcase.ToUpperCamel(obj.Name()), + Type: schType, + } + if md, ok := common.GetFactForObject[*common.ExtractedMetadata](pass, obj).Get(); ok { + if _, ok := md.Type.(*schema.TypeAlias); !ok { + return optional.None[*schema.TypeAlias]() + } + alias.Comments = md.Comments + alias.Export = md.IsExported + } + return optional.Some(alias) +} diff --git a/go-runtime/schema/verb/analyzer.go b/go-runtime/schema/verb/analyzer.go new file mode 100644 index 0000000000..a2cc05ade4 --- /dev/null +++ b/go-runtime/schema/verb/analyzer.go @@ -0,0 +1,117 @@ +package verb + +import ( + "go/ast" + "go/types" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/backend/schema/strcase" + "github.com/TBD54566975/ftl/go-runtime/schema/common" + "github.com/TBD54566975/ftl/go-runtime/schema/initialize" + "github.com/TBD54566975/golang-tools/go/analysis" + "github.com/alecthomas/types/optional" +) + +// Extractor extracts verbs to the module schema. +var Extractor = common.NewDeclExtractor[*schema.Verb, *ast.FuncDecl]("verb", (*Fact)(nil), Extract) + +type Fact struct { + value common.SchemaFactValue +} + +func (t *Fact) AFact() {} +func (t *Fact) Set(v common.SchemaFactValue) { t.value = v } +func (t *Fact) Get() common.SchemaFactValue { return t.value } + +func Extract(pass *analysis.Pass, root *ast.FuncDecl, obj types.Object) optional.Option[*schema.Verb] { + md, ok := common.GetFactForObject[*common.ExtractedMetadata](pass, obj).Get() + if !ok { + return optional.None[*schema.Verb]() + } + + verb := &schema.Verb{ + Pos: common.GoPosToSchemaPos(pass.Fset, root.Pos()), + Name: strcase.ToLowerCamel(root.Name.Name), + Comments: md.Comments, + Export: md.IsExported, + Metadata: md.Metadata, + } + + fnt := obj.(*types.Func) //nolint:forcetypeassert + sig := fnt.Type().(*types.Signature) //nolint:forcetypeassert + if sig.Recv() != nil { + common.Errorf(pass, root, "ftl:verb cannot be a method") + return optional.None[*schema.Verb]() + } + params := sig.Params() + results := sig.Results() + reqt, respt := checkSignature(pass, root, sig) + req := optional.Some[schema.Type](&schema.Unit{}) + if reqt.Ok() { + req = common.ExtractType(pass, root.Pos(), params.At(1).Type()) + } + resp := optional.Some[schema.Type](&schema.Unit{}) + if respt.Ok() { + resp = common.ExtractType(pass, root.Pos(), results.At(0).Type()) + } + reqV, ok := req.Get() + if !ok { + common.TokenErrorf(pass, params.At(1).Pos(), params.At(1).Name(), + "unsupported request type %q", params.At(1).Type()) + } + resV, ok := resp.Get() + if !ok { + common.TokenErrorf(pass, results.At(0).Pos(), results.At(0).Name(), + "unsupported response type %q", results.At(0).Type()) + } + verb.Request = reqV + verb.Response = resV + + return optional.Some(verb) +} + +func checkSignature(pass *analysis.Pass, node *ast.FuncDecl, sig *types.Signature) (req, resp optional.Option[*types.Var]) { + params := sig.Params() + results := sig.Results() + + if params.Len() > 2 { + common.Errorf(pass, node, "must have at most two parameters (context.Context, struct)") + } + + loaded := pass.ResultOf[initialize.Analyzer].(initialize.Result) //nolint:forcetypeassert + if params.Len() == 0 { + common.Errorf(pass, node, "first parameter must be context.Context") + } else if !loaded.IsContextType(params.At(0).Type()) { + common.TokenErrorf(pass, params.At(0).Pos(), params.At(0).Name(), "first parameter must be of type context.Context but is %s", params.At(0).Type()) + } + + if params.Len() == 2 { + if !common.IsType[*types.Struct](params.At(1).Type()) { + common.TokenErrorf(pass, params.At(1).Pos(), params.At(1).Name(), "second parameter must be a struct but is %s", params.At(1).Type()) + } + if params.At(1).Type().String() == common.FtlUnitTypePath { + common.TokenErrorf(pass, params.At(1).Pos(), params.At(1).Name(), "second parameter must not be ftl.Unit") + } + + req = optional.Some(params.At(1)) + } + + if results.Len() > 2 { + common.Errorf(pass, node, "must have at most two results (, error)") + } + if results.Len() == 0 { + common.Errorf(pass, node, "must at least return an error") + } else if !loaded.IsFtlErrorType(results.At(results.Len() - 1).Type()) { + common.TokenErrorf(pass, results.At(results.Len()-1).Pos(), results.At(results.Len()-1).Name(), "must return an error but is %s", results.At(0).Type()) + } + if results.Len() == 2 { + if !common.IsType[*types.Struct](results.At(0).Type()) { + common.TokenErrorf(pass, results.At(0).Pos(), results.At(0).Name(), "first result must be a struct but is %s", results.At(0).Type()) + } + if results.At(1).Type().String() == common.FtlUnitTypePath { + common.TokenErrorf(pass, results.At(1).Pos(), results.At(1).Name(), "second result must not be ftl.Unit") + } + resp = optional.Some(results.At(0)) + } + return req, resp +} diff --git a/go.mod b/go.mod index 72229ecf18..0af62ca17b 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( connectrpc.com/grpcreflect v1.2.0 connectrpc.com/otelconnect v0.7.0 github.com/BurntSushi/toml v1.4.0 - github.com/TBD54566975/golang-tools v0.2.0 + github.com/TBD54566975/golang-tools v0.2.1 github.com/TBD54566975/scaffolder v1.0.0 github.com/alecthomas/assert/v2 v2.10.0 github.com/alecthomas/atomic v0.1.0-alpha2 @@ -42,6 +42,7 @@ require ( github.com/reugn/go-quartz v0.12.0 github.com/rs/cors v1.11.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 + github.com/stretchr/testify v1.9.0 github.com/swaggest/jsonschema-go v0.3.70 github.com/titanous/json5 v1.0.0 github.com/tliron/commonlog v0.2.17 @@ -78,6 +79,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.5 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.28.12 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -95,15 +97,16 @@ require ( github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkoukk/tiktoken-go v0.1.6 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect github.com/segmentio/ksuid v1.0.4 // indirect github.com/sourcegraph/jsonrpc2 v0.2.0 // indirect - github.com/stretchr/objx v0.5.2 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect golang.org/x/tools v0.22.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect ) diff --git a/go.sum b/go.sum index 3b9c3bd840..f27019d881 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/TBD54566975/golang-tools v0.2.0 h1:85wd9MG6S50q3xNB0sTUcpxikUriydvFsKNAiwu12w0= -github.com/TBD54566975/golang-tools v0.2.0/go.mod h1:rEEXIq0/pFgZqi/MTOq4DBmVpLHLgI9WocJWXYhu050= +github.com/TBD54566975/golang-tools v0.2.1 h1:jzP27dzvJRb43Z9xTbRCPOT/rZD43FZkqV+BX+zSoV8= +github.com/TBD54566975/golang-tools v0.2.1/go.mod h1:rEEXIq0/pFgZqi/MTOq4DBmVpLHLgI9WocJWXYhu050= github.com/TBD54566975/scaffolder v1.0.0 h1:QUFSy2wVzumLDg7IHcKC6AP+IYyqWe9Wxiu72nZn5qU= github.com/TBD54566975/scaffolder v1.0.0/go.mod h1:auVpczIbOAdIhYDVSruIw41DanxOKB9bSvjf6MEl7Fs= github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= @@ -155,6 +155,10 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= @@ -211,6 +215,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0= github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= @@ -341,6 +347,8 @@ google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLp google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=