diff --git a/.gitignore b/.gitignore index fe884d1d6..5a02738e2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ examples/**/go.work examples/**/go.work.sum testdata/**/go.work testdata/**/go.work.sum +go-runtime/schema/testdata/test/test.go # Leaving old _ftl for now to avoid old stuff getting checked in **/testdata/**/_ftl diff --git a/backend/schema/validate.go b/backend/schema/validate.go index 6936eb3be..7fed67dfc 100644 --- a/backend/schema/validate.go +++ b/backend/schema/validate.go @@ -372,6 +372,12 @@ func ValidateModule(module *Module) error { }) merr = cleanErrors(merr) + SortModuleDecls(module) + return errors.Join(merr...) +} + +// SortModuleDecls sorts the declarations in a module. +func SortModuleDecls(module *Module) { sort.SliceStable(module.Decls, func(i, j int) bool { iDecl := module.Decls[i] jDecl := module.Decls[j] @@ -382,7 +388,6 @@ func ValidateModule(module *Module) error { } return iPriority < jPriority }) - return errors.Join(merr...) } // getDeclSortingPriority (used for schema sorting) is pulled out into it's own switch so the Go sumtype check will fail diff --git a/buildengine/build_go_test.go b/buildengine/build_go_test.go index 19903ca0a..8f324379c 100644 --- a/buildengine/build_go_test.go +++ b/buildengine/build_go_test.go @@ -40,8 +40,8 @@ func TestExternalType(t *testing.T) { } testBuild(t, bctx, "", "unsupported external type", []assertion{ assertBuildProtoErrors( - `unsupported external type "time.Month"; see FTL docs on using external types: tbd54566975.github.io/ftl/docs/reference/externaltypes/`, `unsupported type "time.Month" for field "Month"`, + `unsupported external type "time.Month"; see FTL docs on using external types: tbd54566975.github.io/ftl/docs/reference/externaltypes/`, `unsupported response type "ftl/external.ExternalResponse"`, ), }) diff --git a/go-runtime/compile/schema_test.go b/go-runtime/compile/schema_test.go index 93a260b8c..72a036274 100644 --- a/go-runtime/compile/schema_test.go +++ b/go-runtime/compile/schema_test.go @@ -193,57 +193,66 @@ func TestExtractModuleSchemaTwo(t *testing.T) { } actual := schema.Normalise(r.Module) expected := `module two { - typealias ExternalAlias Any - +typemap kotlin "com.foo.bar.NonFTLType" - +typemap go "github.com/TBD54566975/ftl/go-runtime/compile/testdata.lib.NonFTLType" - - typealias TransitiveAlias Any - +typemap go "github.com/TBD54566975/ftl/go-runtime/compile/testdata.lib.NonFTLType" - - export enum TwoEnum: String { - Blue = "Blue" - Green = "Green" - Red = "Red" - } + typealias ExplicitAliasAlias Any + +typemap kotlin "com.foo.bar.NonFTLType" + +typemap go "github.com/TBD54566975/ftl/go-runtime/compile/testdata.lib.NonFTLType" - export enum TypeEnum { - Exported two.Exported - List [String] - Scalar String - WithoutDirective two.WithoutDirective - } + typealias ExplicitAliasType Any + +typemap kotlin "com.foo.bar.NonFTLType" + +typemap go "github.com/TBD54566975/ftl/go-runtime/compile/testdata.lib.NonFTLType" - export data Exported { - } + typealias TransitiveAliasAlias Any + +typemap go "github.com/TBD54566975/ftl/go-runtime/compile/testdata.lib.NonFTLType" - data NonFtlField { - field two.ExternalAlias - transitive two.TransitiveAlias - } + typealias TransitiveAliasType Any + +typemap go "github.com/TBD54566975/ftl/go-runtime/compile/testdata.lib.NonFTLType" - export data Payload { - body T - } + export enum TwoEnum: String { + Blue = "Blue" + Green = "Green" + Red = "Red" + } - export data User { - name String - } + export enum TypeEnum { + Exported two.Exported + List [String] + Scalar String + WithoutDirective two.WithoutDirective + } - export data UserResponse { - user two.User - } + export data Exported { + } - export data WithoutDirective { - } + data NonFtlField { + explicitType two.ExplicitAliasType + explicitAlias two.ExplicitAliasAlias + transitiveType two.TransitiveAliasType + transitiveAlias two.TransitiveAliasAlias + } - export verb callsTwo(two.Payload) two.Payload - +calls two.two + export data Payload { + body T + } - export verb returnsUser(Unit) two.UserResponse + export data User { + name String + } - export verb two(two.Payload) two.Payload - } -` + export data UserResponse { + user two.User + } + + export data WithoutDirective { + } + + export verb callsTwo(two.Payload) two.Payload + +calls two.two + + export verb returnsUser(Unit) two.UserResponse + + export verb two(two.Payload) two.Payload + } + ` assert.Equal(t, normaliseString(expected), normaliseString(actual.String())) } @@ -536,7 +545,7 @@ func TestErrorReporting(t *testing.T) { `25:2-10: unsupported type "error" for field "BadParam"`, `28:2-17: unsupported type "uint64" for field "AnotherBadParam"`, `31:3-3: unexpected directive "ftl:export" attached for verb, did you mean to use '//ftl:verb export' instead?`, - `37:36-36: unsupported request type "ftl/failing.Request"`, + `37:40-40: unsupported request type "ftl/failing.Request"`, `37:50-50: unsupported response type "ftl/failing.Response"`, `38:16-29: call first argument must be a function but is an unresolved reference to lib.OtherFunc`, `38:16-29: call first argument must be a function in an ftl module`, @@ -551,12 +560,12 @@ func TestErrorReporting(t *testing.T) { `60:1-2: first parameter must be context.Context`, `60:18-18: unsupported response type "ftl/failing.Response"`, `65:1-2: must have at most two results (, error)`, - `65:41-41: unsupported request type "ftl/failing.Request"`, + `65:45-45: unsupported request type "ftl/failing.Request"`, `70:1-2: must at least return an error`, - `70:36-36: unsupported request type "ftl/failing.Request"`, - `74:35-35: unsupported request type "ftl/failing.Request"`, + `70:40-40: unsupported request type "ftl/failing.Request"`, + `74:39-39: unsupported request type "ftl/failing.Request"`, `74:48-48: must return an error but is ftl/failing.Response`, - `79:41-41: unsupported request type "ftl/failing.Request"`, + `79:45-45: unsupported request type "ftl/failing.Request"`, `79:63-63: must return an error but is string`, `79:63-63: second result must not be ftl.Unit`, // `86:1-2: duplicate declaration of "WrongResponse" at 79:6`, TODO: fix this @@ -580,8 +589,8 @@ func TestErrorReporting(t *testing.T) { // failing/child/child.go expectedChild := []string{ `9:2-6: unsupported type "uint64" for field "Body"`, - `14:2-2: unsupported external type "github.com/TBD54566975/ftl/go-runtime/compile/testdata.lib.NonFTLType"; see FTL docs on using external types: tbd54566975.github.io/ftl/docs/reference/externaltypes/`, `14:2-7: unsupported type "github.com/TBD54566975/ftl/go-runtime/compile/testdata.NonFTLType" for field "Field"`, + `14:8-8: unsupported external type "github.com/TBD54566975/ftl/go-runtime/compile/testdata.NonFTLType"; see FTL docs on using external types: tbd54566975.github.io/ftl/docs/reference/externaltypes/`, `19:6-41: declared type github.com/blah.lib.NonFTLType in typemap does not match native type github.com/TBD54566975/ftl/go-runtime/compile/testdata.lib.NonFTLType`, `24:6-6: multiple Go type mappings found for "ftl/failing/child.MultipleMappings"`, `34:2-13: enum variant "SameVariant" conflicts with existing enum variant of "EnumVariantConflictParent" at "196:2"`, diff --git a/go-runtime/compile/testdata/go/two/two.go b/go-runtime/compile/testdata/go/two/two.go index 715555697..3bd40ef3a 100644 --- a/go-runtime/compile/testdata/go/two/two.go +++ b/go-runtime/compile/testdata/go/two/two.go @@ -70,12 +70,20 @@ func ReturnsUser(ctx context.Context) (UserResponse, error) { //ftl:data type NonFTLField struct { - Field ExternalAlias - Transitive TransitiveAlias + ExplicitType ExplicitAliasType + ExplicitAlias ExplicitAliasAlias + TransitiveType TransitiveAliasType + TransitiveAlias TransitiveAliasAlias } //ftl:typealias //ftl:typemap kotlin "com.foo.bar.NonFTLType" -type ExternalAlias lib.NonFTLType +type ExplicitAliasType lib.NonFTLType -type TransitiveAlias lib.NonFTLType +//ftl:typealias +//ftl:typemap kotlin "com.foo.bar.NonFTLType" +type ExplicitAliasAlias = lib.NonFTLType + +type TransitiveAliasType lib.NonFTLType + +type TransitiveAliasAlias = lib.NonFTLType diff --git a/go-runtime/schema/common/common.go b/go-runtime/schema/common/common.go index 14f156993..993b8a219 100644 --- a/go-runtime/schema/common/common.go +++ b/go-runtime/schema/common/common.go @@ -128,85 +128,6 @@ func ExtractComments(doc *ast.CommentGroup) []string { return comments } -// ExtractType extracts the schema type for the given Go type. -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: - 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]() - } -} - // ExtractFuncForDecl returns the registered extraction function for the given declaration type. func ExtractFuncForDecl(t schema.Decl) (ExtractDeclFunc[schema.Decl, ast.Node], error) { if f, ok := extractorRegistery.Load(reflect.TypeOf(t)); ok { @@ -251,228 +172,419 @@ func IsPathInModule(pkg *types.Package, path string) bool { return strings.HasPrefix(path, "ftl/"+moduleName) } -// GetObjectForNode returns the types.Object for the given node. -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]) +// ExtractType extracts the schema type for the given node. +func ExtractType(pass *analysis.Pass, node ast.Node) optional.Option[schema.Type] { + tnode := GetTypeInfoForNode(node, pass.TypesInfo) + externalType := extractExternalType(pass, node) + if externalType.Ok() { + return externalType + } + + switch typ := node.(type) { + case *ast.ArrayType: + return extractSlice(pass, typ) + + case *ast.MapType: + return extractMap(pass, typ) + + case *ast.InterfaceType: + t, ok := tnode.Get() + if !ok { + return optional.None[schema.Type]() } - case *ast.Field: - if len(n.Names) > 0 { - obj = typesInfo.ObjectOf(n.Names[0]) + iType, ok := t.Underlying().(*types.Interface) + if !ok { + return optional.None[schema.Type]() } - case *ast.ImportSpec: - obj = typesInfo.ObjectOf(n.Name) - case *ast.ValueSpec: - if len(n.Names) > 0 { - obj = typesInfo.ObjectOf(n.Names[0]) + if iType.Underlying().String() == "any" { + return optional.Some[schema.Type](&schema.Any{Pos: GoPosToSchemaPos(pass.Fset, node.Pos())}) + } + if _, ok := t.(*types.Named); ok { + return extractRef(pass, node) } + return optional.None[schema.Type]() + + case *ast.Field: + return ExtractType(pass, typ.Type) + 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) -} + if _, ok := typ.Type.(*ast.StructType); ok { + return extractRef(pass, typ) + } + return ExtractType(pass, typ.Type) -func GetTypeInfoForNode(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() + if t, ok := tnode.Get(); ok { + if tparam, ok := t.(*types.TypeParam); ok { + return optional.Some[schema.Type](&schema.Ref{Pos: GoPosToSchemaPos(pass.Fset, node.Pos()), Name: tparam.Obj().Id()}) + } + switch underlying := t.Underlying().(type) { + case *types.Basic: + if underlying.Kind() == types.Invalid { + return optional.None[schema.Type]() + } + if _, ok := t.(*types.Named); ok { + return extractRef(pass, node) + } + return extractBasicType(pass, node.Pos(), underlying) + case *types.Interface: + if underlying.String() == "any" { + return optional.Some[schema.Type](&schema.Any{Pos: GoPosToSchemaPos(pass.Fset, node.Pos())}) + } + if _, ok := t.(*types.Named); ok { + return extractRef(pass, node) + } + return optional.None[schema.Type]() + } + } - case *ast.AssignStmt: - if len(n.Lhs) > 0 { - return info.TypeOf(n.Lhs[0]) + return extractRef(pass, typ) + + case *ast.SelectorExpr: // Selector expression e.g. ftl.Unit, ftl.Option, foo.Bar + var ident *ast.Ident + var ok bool + if ident, ok = typ.X.(*ast.Ident); !ok { + return optional.None[schema.Type]() } - case *ast.ValueSpec: - if len(n.Names) > 0 { - if obj := info.ObjectOf(n.Names[0]); obj != nil { - return obj.Type() + + for _, im := range pass.Pkg.Imports() { + if im.Name() != ident.Name { + continue } - } - 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() + + 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: + return optional.Some[schema.Type](&schema.Optional{ + Pos: GoPosToSchemaPos(pass.Fset, node.Pos()), + }) + + default: // Data ref + if strings.HasPrefix(im.Path(), pass.Pkg.Path()+"/") { + // subpackage, same module + return ExtractType(pass, typ.Sel) + } + + if !IsPathInModule(pass.Pkg, im.Path()) && IsExternalType(im.Path()) { + NoEndColumnErrorf(pass, node.Pos(), "unsupported external type %q; see FTL docs on using external types: %s", + im.Path()+"."+typ.Sel.Name, "tbd54566975.github.io/ftl/docs/reference/externaltypes/") + return optional.None[schema.Type]() + } + + // FTL, different module + 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.GenDecl: - for _, spec := range n.Specs { - if t := GetTypeInfoForNode(spec, info); t != nil { - return t + + case *ast.IndexListExpr: + t, ok := ExtractType(pass, typ.X).Get() + if !ok { + return optional.None[schema.Type]() + } + ref, ok := t.(*schema.Ref) + if !ok { + return optional.None[schema.Type]() + } + var params []schema.Type + for _, idx := range typ.Indices { + if param, ok := ExtractType(pass, idx).Get(); ok { + params = append(params, param) } } - case ast.Expr: - return info.TypeOf(n) + ref.TypeParameters = params + return optional.Some[schema.Type](ref) + + case *ast.IndexExpr: // Generic type, e.g. ftl.Option[string] + t, ok := ExtractType(pass, typ.X).Get() + if !ok { + return optional.None[schema.Type]() + } + idx, ok := ExtractType(pass, typ.Index).Get() + if !ok { + return optional.None[schema.Type]() + } + switch s := t.(type) { + case *schema.Ref: + s.TypeParameters = []schema.Type{idx} + case *schema.Optional: + s.Type = idx + default: + return optional.None[schema.Type]() + } + return optional.Some[schema.Type](t) } - return nil + + return optional.None[schema.Type]() } -func extractRef(pass *analysis.Pass, pos token.Pos, named *types.Named) optional.Option[schema.Type] { - if named.Obj().Pkg() == nil { +// extracts a ref to the type alias over an external type +func extractExternalType(pass *analysis.Pass, node ast.Node) optional.Option[schema.Type] { + obj, ok := GetObjectForNode(pass.TypesInfo, node).Get() + if !ok { + return optional.None[schema.Type]() + } + + tn, ok := obj.(*types.TypeName) + if !ok { + return optional.None[schema.Type]() + } + + if tn.Pkg() == nil { return optional.None[schema.Type]() } - nodePath := named.Obj().Pkg().Path() - if !IsPathInModule(pass.Pkg, nodePath) && IsExternalType(named.Obj()) { - NoEndColumnErrorf(pass, pos, "unsupported external type %q; see FTL docs on using external types: %s", - GetNativeName(named.Obj()), "tbd54566975.github.io/ftl/docs/reference/externaltypes/") + moduleName, err := FtlModuleFromGoPackage(tn.Pkg().Path()) + if err != nil { + return optional.None[schema.Type]() + } + currentModule, err := FtlModuleFromGoPackage(pass.Pkg.Path()) + if err != nil { + return optional.None[schema.Type]() + } + if underlying, ok := obj.Type().(*types.Named); ok && + moduleName == currentModule && // type is in this module + IsExternalType(underlying.Obj().Pkg().Path()) { // alias— e.g. type MyType = foo.OtherType + MarkNeedsExtraction(pass, obj) + return optional.Some[schema.Type](&schema.Ref{ + Pos: GoPosToSchemaPos(pass.Fset, node.Pos()), + Module: moduleName, + Name: strcase.ToUpperCamel(obj.Name()), + }) + } + return optional.None[schema.Type]() +} + +func extractBasicType(pass *analysis.Pass, pos token.Pos, basic *types.Basic) optional.Option[schema.Type] { + switch basic.Kind() { + case types.String: + return optional.Some[schema.Type](&schema.String{Pos: GoPosToSchemaPos(pass.Fset, pos)}) + + case types.Int: + return optional.Some[schema.Type](&schema.Int{Pos: GoPosToSchemaPos(pass.Fset, pos)}) + + case types.Bool: + return optional.Some[schema.Type](&schema.Bool{Pos: GoPosToSchemaPos(pass.Fset, pos)}) + + case types.Float64: + return optional.Some[schema.Type](&schema.Float{Pos: GoPosToSchemaPos(pass.Fset, pos)}) + + default: + return optional.None[schema.Type]() + } +} + +func extractRef(pass *analysis.Pass, node ast.Node) optional.Option[schema.Type] { + obj, ok := GetObjectForNode(pass.TypesInfo, node).Get() + if !ok { + return optional.None[schema.Type]() + } + if obj.Pkg() == nil { + return optional.None[schema.Type]() + } + + nodePath := obj.Pkg().Path() + if !IsPathInModule(pass.Pkg, nodePath) && IsExternalType(nodePath) { + NoEndColumnErrorf(pass, node.Pos(), "unsupported external type %q; see FTL docs on using external types: %s", + GetNativeName(obj), "tbd54566975.github.io/ftl/docs/reference/externaltypes/") return optional.None[schema.Type]() } moduleName, err := FtlModuleFromGoPackage(nodePath) if err != nil { - noEndColumnWrapf(pass, pos, err, "") + noEndColumnWrapf(pass, node.Pos(), err, "") return optional.None[schema.Type]() } ref := &schema.Ref{ - Pos: GoPosToSchemaPos(pass.Fset, pos), + Pos: GoPosToSchemaPos(pass.Fset, node.Pos()), Module: moduleName, - Name: strcase.ToUpperCamel(named.Obj().Name()), } + if t, ok := node.(*ast.TypeSpec); ok { + if t.TypeParams != nil { + for _, p := range t.TypeParams.List { + param, ok := ExtractType(pass, p).Get() + var typename string + if t, ok := GetTypeInfoForNode(p, pass.TypesInfo).Get(); ok { + typename = fmt.Sprintf("%q ", t.String()) + } + if !ok { + Errorf(pass, p, "unsupported type %sfor type argument", typename) + continue + } - 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 + // Fully qualify the Ref if needed + if r, okArg := param.(*schema.Ref); okArg { + if r.Module == "" { + r.Module = moduleName + } + param = r + } + ref.TypeParameters = append(ref.TypeParameters, param) } - typeArg = r } - ref.TypeParameters = append(ref.TypeParameters, typeArg) + } + ref.Name = strcase.ToUpperCamel(getNodeName(node)) + if ref.Name == "" { + return optional.None[schema.Type]() } 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()) + MarkNeedsExtraction(pass, 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() +func extractMap(pass *analysis.Pass, node *ast.MapType) optional.Option[schema.Type] { + key, ok := ExtractType(pass, node.Key).Get() if !ok { return optional.None[schema.Type]() } - value, ok := ExtractType(pass, pos, tnode.Elem()).Get() + value, ok := ExtractType(pass, node.Value).Get() if !ok { return optional.None[schema.Type]() } - return optional.Some[schema.Type](&schema.Map{Pos: GoPosToSchemaPos(pass.Fset, pos), Key: key, Value: value}) + return optional.Some[schema.Type](&schema.Map{Pos: GoPosToSchemaPos(pass.Fset, node.Pos()), Key: key, Value: value}) } -func extractSlice(pass *analysis.Pass, pos token.Pos, tnode *types.Slice) optional.Option[schema.Type] { +func extractSlice(pass *analysis.Pass, node *ast.ArrayType) optional.Option[schema.Type] { + typ, ok := GetTypeInfoForNode(node, pass.TypesInfo).Get() + if !ok { + return optional.None[schema.Type]() + } + tnode, ok := typ.(*types.Slice) + if !ok { + return optional.None[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)}) + return optional.Some[schema.Type](&schema.Bytes{Pos: GoPosToSchemaPos(pass.Fset, node.Pos())}) } - value, ok := ExtractType(pass, pos, tnode.Elem()).Get() + value, ok := ExtractType(pass, node.Elt).Get() if !ok { return optional.None[schema.Type]() } return optional.Some[schema.Type](&schema.Array{ - Pos: GoPosToSchemaPos(pass.Fset, pos), + Pos: GoPosToSchemaPos(pass.Fset, node.Pos()), Element: value, }) } -// ExtractTypeForNode extracts the schema type for the given node. -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]() +func getNodeName(node ast.Node) string { + switch t := node.(type) { + case *ast.Ident: + return t.Name + case *ast.TypeSpec: + return t.Name.Name + case *ast.Field: + if len(t.Names) > 0 { + return getNodeName(t.Names[0]) } + } + return "" +} - 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]() - } - if underlying, ok := ExtractType(pass, node.Pos(), index).Get(); ok { - return optional.Some[schema.Type](&schema.Optional{ - Pos: GoPosToSchemaPos(pass.Fset, node.Pos()), - Type: underlying, - }) - } - return optional.None[schema.Type]() - - default: // Data ref - if strings.HasPrefix(im.Path(), pass.Pkg.Path()) { - // subpackage, same module - return ExtractType(pass, node.Pos(), pass.TypesInfo.TypeOf(typ.Sel)) - } - - if !IsPathInModule(pass.Pkg, im.Path()) && !strings.HasPrefix(im.Path(), "ftl/") { - // Non-FTL - return optional.Some[schema.Type](&schema.Any{}) - } +// GetObjectForNode returns the types.Object for the given node. +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) + case *ast.Ident: + obj = typesInfo.ObjectOf(n) + case *ast.IndexExpr: + return GetObjectForNode(typesInfo, n.X) + case *ast.IndexListExpr: + return GetObjectForNode(typesInfo, n.X) + case *ast.SelectorExpr: + return GetObjectForNode(typesInfo, n.Sel) + case *ast.ArrayType: + return GetObjectForNode(typesInfo, n.Elt) + default: + return optional.None[types.Object]() + } + if obj == nil { + return optional.None[types.Object]() + } + return optional.Some(obj) +} - // FTL, different module - 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, - }) +func GetTypeInfoForNode(node ast.Node, info *types.Info) optional.Option[types.Type] { + switch n := node.(type) { + case *ast.Ident: + if obj := info.ObjectOf(n); obj != nil { + return optional.Some(obj.Type()) + } + case *ast.AssignStmt: + if len(n.Lhs) > 0 { + return optional.Some(info.TypeOf(n.Lhs[0])) + } + case *ast.ValueSpec: + if len(n.Names) > 0 { + if obj := info.ObjectOf(n.Names[0]); obj != nil { + return optional.Some(obj.Type()) } } - - 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)) + case *ast.TypeSpec: + return optional.Some(info.TypeOf(n.Type)) + case *ast.CompositeLit: + return optional.Some(info.TypeOf(n)) + case *ast.CallExpr: + return optional.Some(info.TypeOf(n)) + case *ast.FuncDecl: + if n.Name != nil { + if obj := info.ObjectOf(n.Name); obj != nil { + return optional.Some(obj.Type()) + } } - - default: - tnode := GetTypeInfoForNode(node, pass.TypesInfo) - if _, ok := tnode.(*types.Struct); ok { - tnode = obj.Type() + case *ast.GenDecl: + for _, spec := range n.Specs { + if t := GetTypeInfoForNode(spec, info); t.Ok() { + return t + } } - return ExtractType(pass, node.Pos(), tnode) - } + case *ast.Field: + return optional.Some(info.TypeOf(n.Type)) + case *ast.SliceExpr: + return optional.Some(info.TypeOf(n)) + case ast.Expr: + return optional.Some(info.TypeOf(n)) - return optional.None[schema.Type]() + } + return optional.None[types.Type]() } // IsSelfReference returns true if the schema reference refers to this object itself. @@ -498,8 +610,11 @@ func GetNativeName(obj types.Object) string { } // IsExternalType returns true if the object is from an external package. -func IsExternalType(obj types.Object) bool { - return !strings.HasPrefix(obj.Pkg().Path(), "ftl/") +func IsExternalType(path string) bool { + return !strings.HasPrefix(path, "ftl/") && + path != "time.Time" && + path != FtlUnitTypePath && + path != FtlOptionTypePath } // GetDeclTypeName returns the name of the declaration type, e.g. "verb" for *schema.Verb. diff --git a/go-runtime/schema/common/fact.go b/go-runtime/schema/common/fact.go index e7b94c746..53b05428d 100644 --- a/go-runtime/schema/common/fact.go +++ b/go-runtime/schema/common/fact.go @@ -94,6 +94,11 @@ type FailedExtraction struct{} func (*FailedExtraction) schemaFactValue() {} +// ExternalType is a fact for marking an external type. +type ExternalType struct{} + +func (*ExternalType) schemaFactValue() {} + // MarkSchemaDecl marks the given object as having been extracted to the given schema decl. func MarkSchemaDecl(pass *analysis.Pass, obj types.Object, decl schema.Decl) { fact := newFact(pass, obj) diff --git a/go-runtime/schema/configsecret/analyzer.go b/go-runtime/schema/configsecret/analyzer.go index 6545fee2d..cfa7d9de5 100644 --- a/go-runtime/schema/configsecret/analyzer.go +++ b/go-runtime/schema/configsecret/analyzer.go @@ -78,10 +78,9 @@ func extractConfigSecret[T schema.Decl]( index := node.Fun.(*ast.IndexExpr) //nolint:forcetypeassert // Type parameter - tp := pass.TypesInfo.Types[index.Index].Type - st, ok := common.ExtractType(pass, index.Index.Pos(), tp).Get() + st, ok := common.ExtractType(pass, index.Index).Get() if !ok { - common.Errorf(pass, index.Index, "config is unsupported type %q", tp) + common.Errorf(pass, index.Index, "config is unsupported type") return optional.None[schema.Decl]() } diff --git a/go-runtime/schema/data/analyzer.go b/go-runtime/schema/data/analyzer.go index d9197e876..6ddb93581 100644 --- a/go-runtime/schema/data/analyzer.go +++ b/go-runtime/schema/data/analyzer.go @@ -1,19 +1,19 @@ package data import ( + "fmt" "go/ast" - "go/token" "go/types" "reflect" + "strconv" "strings" "unicode" - "github.com/alecthomas/types/optional" - "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 ( @@ -31,23 +31,17 @@ func Extract(pass *analysis.Pass, node *ast.TypeSpec, obj types.Object) optional if _, ok := named.Underlying().(*types.Struct); !ok { return optional.None[*schema.Data]() } - decl, ok := extractData(pass, node.Pos(), named).Get() + decl, ok := extractData(pass, node, 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.IsPathInModule(pass.Pkg, nodePath) { - return optional.None[*schema.Data]() - } - +func extractData(pass *analysis.Pass, node *ast.TypeSpec, named *types.Named) optional.Option[*schema.Data] { out := &schema.Data{ - Pos: common.GoPosToSchemaPos(fset, pos), - Name: strcase.ToUpperCamel(named.Obj().Name()), + Pos: common.GoPosToSchemaPos(pass.Fset, node.Pos()), + Name: strcase.ToUpperCamel(node.Name.Name), } common.ApplyMetadata[*schema.Data](pass, named.Obj(), func(md *common.ExtractedMetadata) { out.Comments = md.Comments @@ -56,57 +50,62 @@ func extractData(pass *analysis.Pass, pos token.Pos, named *types.Named) optiona for i := range named.TypeParams().Len() { param := named.TypeParams().At(i) out.TypeParameters = append(out.TypeParameters, &schema.TypeParameter{ - Pos: common.GoPosToSchemaPos(fset, pos), + Pos: common.GoPosToSchemaPos(pass.Fset, node.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) + structType, ok := node.Type.(*ast.StructType) 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 { + for i := range structType.Fields.List { + f := structType.Fields.List[i] + var name string + if len(f.Names) > 0 && len(f.Names[0].Name) > 0 { + name = f.Names[0].Name + } + if name == "" { + common.Errorf(pass, f, "anonymous fields are not supported") + fieldErrors = true + continue + } + if ft, ok := common.ExtractType(pass, 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()) + if unicode.IsLower(rune(name[0])) { + name = f.Names[0].Name + common.TokenErrorf(pass, f.Pos(), name, "struct field %s must be exported by starting with an "+ + "uppercase letter", 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, - }) + if tag := f.Tag; tag != nil { + jsonFieldName, err := parseTag(pass, f, tag, aliasFieldTag) + if err != nil { + fieldErrors = true + continue + } + + if jsonFieldName != "" { + metadata = append(metadata, &schema.MetadataAlias{ + Pos: common.GoPosToSchemaPos(pass.Fset, node.Pos()), + Kind: schema.AliasKindJSON, + Alias: jsonFieldName, + }) + } } out.Fields = append(out.Fields, &schema.Field{ - Pos: common.GoPosToSchemaPos(pass.Fset, pos), - Name: strcase.ToLowerCamel(f.Name()), + Pos: common.GoPosToSchemaPos(pass.Fset, node.Pos()), + Name: strcase.ToLowerCamel(name), Type: ft, Metadata: metadata, }) } else { - common.TokenErrorf(pass, f.Pos(), f.Name(), "unsupported type %q for field %q", f.Type(), f.Name()) + common.TokenErrorf(pass, f.Pos(), name, "unsupported type %q for field %q", pass.TypesInfo.TypeOf(f.Type), name) fieldErrors = true } } @@ -115,3 +114,18 @@ func extractData(pass *analysis.Pass, pos token.Pos, named *types.Named) optiona } return optional.Some(out) } + +func parseTag(pass *analysis.Pass, f *ast.Field, tag *ast.BasicLit, fieldTag string) (string, error) { + unquoted, err := strconv.Unquote(tag.Value) + if err != nil { + common.Wrapf(pass, f, err, "failed to unquote tag value %q", tag.Value) + return "", fmt.Errorf("failed to unquote tag value: %w", err) + } + tagContent := reflect.StructTag(unquoted).Get(fieldTag) + tagParts := strings.Split(tagContent, ",") + jsonFieldName := "" + if len(tagParts) > 0 { + jsonFieldName = tagParts[0] + } + return jsonFieldName, nil +} diff --git a/go-runtime/schema/enum/analyzer.go b/go-runtime/schema/enum/analyzer.go index c60eb13b5..8b75408ee 100644 --- a/go-runtime/schema/enum/analyzer.go +++ b/go-runtime/schema/enum/analyzer.go @@ -46,7 +46,7 @@ func Extract(pass *analysis.Pass, node *ast.TypeSpec, obj types.Object) optional return optional.None[*schema.Enum]() } - typ, ok := common.ExtractType(pass, node.Pos(), pass.TypesInfo.TypeOf(node.Type)).Get() + typ, ok := common.ExtractType(pass, node).Get() if !ok { return optional.None[*schema.Enum]() } @@ -112,6 +112,7 @@ func findTypeValueVariants(pass *analysis.Pass, obj types.Object) []*schema.Enum value, ok := fact.GetValue(pass).Get() if !ok { common.NoEndColumnErrorf(pass, vObj.Pos(), "invalid type for enum variant %q", fact.Variant.Name) + continue } fact.Variant.Value = value variants = append(variants, fact.Variant) diff --git a/go-runtime/schema/extract.go b/go-runtime/schema/extract.go index 767f1d3a5..dcc43531a 100644 --- a/go-runtime/schema/extract.go +++ b/go-runtime/schema/extract.go @@ -16,6 +16,7 @@ import ( "github.com/TBD54566975/ftl/go-runtime/schema/valueenumvariant" "github.com/alecthomas/types/optional" "github.com/alecthomas/types/tuple" + sets "github.com/deckarep/golang-set/v2" "golang.org/x/exp/maps" "github.com/TBD54566975/ftl/backend/schema" @@ -189,6 +190,7 @@ func combineAllPackageResults(results map[*analysis.Analyzer][]any, diagnostics } combined.Module.AddDecls(maps.Keys(extractedDecls)) + externalTypeAliases := sets.NewSet[*schema.TypeAlias]() for decl, obj := range extractedDecls { if ta, ok := decl.(*schema.TypeAlias); ok && len(ta.Metadata) > 0 { fqName, err := goQualifiedNameForWidenedType(obj, ta.Metadata) @@ -199,6 +201,7 @@ func combineAllPackageResults(results map[*analysis.Analyzer][]any, diagnostics } refResults[schema.RefKey{Module: combined.Module.Name, Name: ta.Name}] = refResult{typ: widened, obj: obj, fqName: optional.Some(fqName)} + externalTypeAliases.Add(ta) } combined.NativeNames[decl] = common.GetNativeName(obj) } diff --git a/go-runtime/schema/extract_test.go b/go-runtime/schema/extract_test.go new file mode 100644 index 000000000..9f8d95f11 --- /dev/null +++ b/go-runtime/schema/extract_test.go @@ -0,0 +1,371 @@ +package schema + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "text/template" + + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/internal/slices" + "github.com/alecthomas/assert/v2" +) + +var symbols = []string{ + "any", "bool", "[]byte", "float64", "int", "string", "time.Time", "ftl.Unit", +} + +// generate all possible permutations for slices, maps and ftl.Option[...] +func generatePermutations(symbols []string) []string { + permutations := append([]string{}, symbols...) + for _, symbol := range symbols { + permutations = append(permutations, "[]"+symbol) + permutations = append(permutations, "ftl.Option["+symbol+"]") + // don't add slices as map keys + if strings.HasPrefix(symbol, "[]") { + continue + } + for _, nestedSymbol := range symbols { + permutations = append(permutations, fmt.Sprintf("map[%s]%s", symbol, nestedSymbol)) + } + } + return permutations +} + +func generateSymbolTypeStrings(symbols []string) map[string]string { + symbolTypeStringMap := make(map[string]string) + for _, symbol := range symbols { + symbolTypeStringMap[symbol] = getSchemaType(symbol).String() + } + return symbolTypeStringMap +} + +func getSchemaType(symbol string) schema.Type { + switch symbol { + case "any": + return &schema.Any{} + case "bool": + return &schema.Bool{} + case "[]byte": + return &schema.Bytes{} + case "float64": + return &schema.Float{} + case "int": + return &schema.Int{} + case "string": + return &schema.String{} + case "time.Time": + return &schema.Time{} + case "ftl.Unit": + return &schema.Unit{} + default: + if strings.HasPrefix(symbol, "[]") { + // `[]` is 2 characters long + return &schema.Array{Element: getSchemaType(symbol[2:])} + } + if strings.HasPrefix(symbol, "map[") { + key := symbol[4:findMatchingBracketIndex(symbol, 3)] + value := symbol[4+len(key)+1:] // remainder of the string after the key + return &schema.Map{Key: getSchemaType(key), Value: getSchemaType(value)} + } + if strings.HasPrefix(symbol, "ftl.Option[") { + // `ftl.Option[` is 11 characters long + return &schema.Optional{Type: getSchemaType(symbol[11 : len(symbol)-1])} + } + panic(fmt.Sprintf("unexpected symbol: %s", symbol)) + } +} + +func findMatchingBracketIndex(symbol string, startIdx int) int { + bracketCount := 1 + for i := startIdx + 1; i < len(symbol); i++ { + switch symbol[i] { + case '[': + bracketCount++ + case ']': + bracketCount-- + if bracketCount == 0 { + return i + } + } + } + return -1 +} + +func generateSourceCode(symbol string) string { + return `package test + +import ( + "context" + ` + (func() string { + if strings.Contains(symbol, "time.Time") { + return "\t\"time\"\n" + } + return "" + }()) + ` + "github.com/TBD54566975/ftl/go-runtime/ftl" +) + +var config = ftl.Config[` + symbol + `]("cfg") + +var secret = ftl.Secret[` + symbol + `]("secret") + +//ftl:data +type Data struct { + DataField ` + symbol + ` + ParamDataField ParameterizedData[` + symbol + `] +} + +type ParameterizedData[T any] struct { + Field T +} + +//ftl:data export +type ExportedData struct { + Field string +} + +//ftl:verb +func DataFunc(ctx context.Context, req Data) (Data, error) { + return Data{}, nil +} + + +var db = ftl.PostgresDatabase("testDb") + +` + (func() string { + if symbol == "int" || symbol == "string" { + return ` + +//ftl:enum +type Color ` + symbol + ` +const ( + ` + (func() string { + switch symbol { + case "int": + return `Red Color = iota + Blue + Green` + case "string": + return `Red Color = "Red" + Blue Color = "Blue" + Green Color = "Green"` + } + return "" + }()) + ` +) +` + } + return "" + }()) + ` + +` + (func() string { + if symbol != "any" { + return `//ftl:enum +type Discriminator interface { + tag() +} + +type Variant ` + symbol + ` +func (Variant) tag() {} + +//ftl:enum export +type ExportedDiscriminator interface { + exportedTag() +} + +type ExportedVariant ` + symbol + ` +func (ExportedVariant) exportedTag() {} +` + } + return "" + }()) + ` + +var Topic = ftl.Topic[` + symbol + `]("topic") + +//ftl:export +var ExportedTopic = ftl.Topic[` + symbol + `]("exported_topic") + +var _ = ftl.Subscription(Topic, "subscription") + +//ftl:typealias +type Alias ` + symbol + ` + +//ftl:typealias export +type ExportedAlias ` + symbol + ` + +//ftl:typealias +type EqualAlias = ` + symbol + ` + +//ftl:typealias export +type ExportedEqualAlias = ` + symbol + ` + +//ftl:verb +func Func(ctx context.Context, req ` + symbol + `) (` + symbol + `, error) { + panic("not implemented") +} + +//ftl:verb export +func ExportedFunc(ctx context.Context, req ` + symbol + `) (` + symbol + `, error) { + panic("not implemented") +} + +//ftl:verb +func SourceFunc(ctx context.Context) (` + symbol + `, error) { + panic("not implemented") +} + +//ftl:verb +func SinkFunc(ctx context.Context, req ` + symbol + `) error { + panic("not implemented") +} + +//ftl:verb +func EmptyVerbFunc(ctx context.Context) error { + return nil +} +` +} + +func FuzzExtract(f *testing.F) { + if testing.Short() { + f.Skip("skipping test in short mode") + } + + allSymbols := generatePermutations(symbols) + for _, symbol := range allSymbols { + f.Add(symbol) + } + typenames := generateSymbolTypeStrings(allSymbols) + + f.Fuzz(func(t *testing.T, symbolType string) { + code := generateSourceCode(symbolType) + + moduleDir := "testdata/test" + abs, err := filepath.Abs(moduleDir) + assert.NoError(t, err) + filePath := filepath.Join(abs, "test.go") + err = os.WriteFile(filePath, []byte(code), 0600) + assert.NoError(t, err) + defer os.Remove(abs) + + r, err := Extract(abs) + assert.NoError(t, err) + expected := tmpl(symbolType, typenames[symbolType]) + + schema.SortModuleDecls(r.Module) + assert.Equal(t, normaliseString(expected), normaliseString(r.Module.String())) + }) +} + +func tmpl(symbolType string, typename string) string { + var typeEnum string + if symbolType != "any" { + typeEnum = fmt.Sprintf( + ` + enum Discriminator { + Variant %s + } + + export enum ExportedDiscriminator { + ExportedVariant %s + } +`, typename, typename) + } + + var valueEnum string + switch symbolType { + case "int": + valueEnum = ` +enum Color: Int { + Blue = 1 + Green = 2 + Red = 0 + } +` + case "string": + valueEnum = ` +enum Color: String { + Blue = "Blue" + Green = "Green" + Red = "Red" + } +` + } + + data := struct { + TypeName string + TypeEnum string + ValueEnum string + }{ + TypeName: typename, + TypeEnum: typeEnum, + ValueEnum: valueEnum, + } + + const tmpl = ` +module test { + config cfg {{.TypeName}} + secret secret {{.TypeName}} + + database postgres testDb + + export topic exported_topic {{.TypeName}} + topic topic {{.TypeName}} + subscription subscription test.topic + + typealias Alias {{.TypeName}} + + typealias EqualAlias {{.TypeName}} + + export typealias ExportedAlias {{.TypeName}} + + export typealias ExportedEqualAlias {{.TypeName}} +{{.ValueEnum}}{{.TypeEnum}} + data Data { + dataField {{.TypeName}} + paramDataField test.ParameterizedData<{{.TypeName}}> + } + + export data ExportedData { + field String + } + + data ParameterizedData { + field T + } + + verb dataFunc(test.Data) test.Data + + verb emptyVerbFunc(Unit) Unit + + export verb exportedFunc({{.TypeName}}) {{.TypeName}} + + verb func({{.TypeName}}) {{.TypeName}} + + verb sinkFunc({{.TypeName}}) Unit + + verb sourceFunc(Unit) {{.TypeName}} +} +` + + t, err := template.New("test").Parse(tmpl) + if err != nil { + panic(err) + } + + var result bytes.Buffer + err = t.Execute(&result, data) + if err != nil { + panic(err) + } + + return result.String() +} + +func normaliseString(s string) string { + return strings.TrimSpace(strings.Join(slices.Map(strings.Split(s, "\n"), strings.TrimSpace), "\n")) +} diff --git a/go-runtime/schema/testdata/test/ftl.toml b/go-runtime/schema/testdata/test/ftl.toml new file mode 100644 index 000000000..c26718340 --- /dev/null +++ b/go-runtime/schema/testdata/test/ftl.toml @@ -0,0 +1,2 @@ +module = "test" +language = "go" diff --git a/go-runtime/schema/testdata/test/go.mod b/go-runtime/schema/testdata/test/go.mod new file mode 100644 index 000000000..60effaed6 --- /dev/null +++ b/go-runtime/schema/testdata/test/go.mod @@ -0,0 +1,45 @@ +module ftl/test + +go 1.22.2 + +replace github.com/TBD54566975/ftl => ../../../.. + +require github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000 + +require ( + connectrpc.com/connect v1.16.1 // indirect + connectrpc.com/grpcreflect v1.2.0 // indirect + connectrpc.com/otelconnect v0.7.0 // indirect + github.com/alecthomas/atomic v0.1.0-alpha2 // indirect + github.com/alecthomas/concurrency v0.0.2 // indirect + github.com/alecthomas/participle/v2 v2.1.1 // indirect + github.com/alecthomas/types v0.16.0 // indirect + github.com/alessio/shellescape v1.4.2 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/puzpuzpuz/xsync/v3 v3.3.1 // indirect + github.com/swaggest/jsonschema-go v0.3.72 // indirect + github.com/swaggest/refl v1.3.0 // indirect + github.com/zalando/go-keyring v0.2.5 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/go-runtime/schema/testdata/test/go.sum b/go-runtime/schema/testdata/test/go.sum new file mode 100644 index 000000000..ca2013af9 --- /dev/null +++ b/go-runtime/schema/testdata/test/go.sum @@ -0,0 +1,144 @@ +connectrpc.com/connect v1.16.1 h1:rOdrK/RTI/7TVnn3JsVxt3n028MlTRwmK5Q4heSpjis= +connectrpc.com/connect v1.16.1/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw= +connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= +connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= +connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY= +connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc= +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= +github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8= +github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI= +github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo= +github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/types v0.16.0 h1:o9+JSwCRB6DDaWDeR/Mg7v/zh3R+MlknM6DrnDyY7U0= +github.com/alecthomas/types v0.16.0/go.mod h1:Tswm0qQpjpVq8rn70OquRsUtFxbQKub/8TMyYYGI0+k= +github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= +github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/bool64/dev v0.2.35 h1:M17TLsO/pV2J7PYI/gpe3Ua26ETkzZGb+dC06eoMqlk= +github.com/bool64/dev v0.2.35/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/puzpuzpuz/xsync/v3 v3.3.1 h1:vZPJk3OOfoaSjy3cdTX3BZxhDCUVp9SqdHnd+ilGlbQ= +github.com/puzpuzpuz/xsync/v3 v3.3.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +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/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= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/swaggest/jsonschema-go v0.3.72 h1:IHaGlR1bdBUBPfhe4tfacN2TGAPKENEGiNyNzvnVHv4= +github.com/swaggest/jsonschema-go v0.3.72/go.mod h1:OrGyEoVqpfSFJ4Am4V/FQcQ3mlEC1vVeleA+5ggbVW4= +github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= +github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8= +github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/sdk/metric v1.28.0 h1:OkuaKgKrgAbYrrY0t92c+cC+2F6hsFNnCQArXCKlg08= +go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M= +modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk= +modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/go-runtime/schema/topic/analyzer.go b/go-runtime/schema/topic/analyzer.go index 70231de6b..1eb026c76 100644 --- a/go-runtime/schema/topic/analyzer.go +++ b/go-runtime/schema/topic/analyzer.go @@ -55,7 +55,7 @@ func extractTopic(pass *analysis.Pass, node *ast.GenDecl, callExpr *ast.CallExpr common.Errorf(pass, node, "must have an event type as a type parameter") return optional.None[*schema.Topic]() } - typeParamType, ok := common.ExtractType(pass, node.Pos(), pass.TypesInfo.TypeOf(indexExpr.Index)).Get() + typeParamType, ok := common.ExtractType(pass, indexExpr.Index).Get() if !ok { common.Errorf(pass, node, "unsupported event type") return optional.None[*schema.Topic]() diff --git a/go-runtime/schema/transitive/analyzer.go b/go-runtime/schema/transitive/analyzer.go index 35c99e1f0..ebedb0200 100644 --- a/go-runtime/schema/transitive/analyzer.go +++ b/go-runtime/schema/transitive/analyzer.go @@ -106,7 +106,11 @@ func inferDeclType(pass *analysis.Pass, node ast.Node, obj types.Object) optiona if _, ok := ts.Type.(*ast.InterfaceType); ok { return optional.Some[schema.Decl](&schema.Enum{}) } - t, ok := common.ExtractTypeForNode(pass, obj, ts.Type, nil).Get() + // underlying type is external, try to extract as a type alias + if o, ok := common.GetObjectForNode(pass.TypesInfo, ts.Type).Get(); ok && o.Pkg() != nil && common.IsExternalType(o.Pkg().Path()) { + return optional.Some[schema.Decl](&schema.TypeAlias{}) + } + t, ok := common.ExtractType(pass, ts.Type).Get() if !ok { return optional.None[schema.Decl]() } diff --git a/go-runtime/schema/typealias/analyzer.go b/go-runtime/schema/typealias/analyzer.go index 409fe3b68..afc3ff6c4 100644 --- a/go-runtime/schema/typealias/analyzer.go +++ b/go-runtime/schema/typealias/analyzer.go @@ -17,15 +17,9 @@ import ( var Extractor = common.NewDeclExtractor[*schema.TypeAlias, *ast.TypeSpec]("typealias", Extract) func Extract(pass *analysis.Pass, node *ast.TypeSpec, obj types.Object) optional.Option[*schema.TypeAlias] { - schType, ok := common.ExtractTypeForNode(pass, obj, node.Type, nil).Get() - if !ok { - return optional.None[*schema.TypeAlias]() - } - alias := &schema.TypeAlias{ Pos: common.GoPosToSchemaPos(pass.Fset, node.Pos()), Name: strcase.ToUpperCamel(obj.Name()), - Type: schType, } var hasGoTypeMapping bool common.ApplyMetadata[*schema.TypeAlias](pass, obj, func(md *common.ExtractedMetadata) { @@ -57,17 +51,24 @@ func Extract(pass *analysis.Pass, node *ast.TypeSpec, obj types.Object) optional }) // if widening an external type, implicitly add a Go type mapping if one does not exist - if _, ok := alias.Type.(*schema.Any); ok && - !strings.HasPrefix(qualifiedNameFromSelectorExpr(pass, node.Type), "ftl/") && - !hasGoTypeMapping { - alias.Metadata = append(alias.Metadata, &schema.MetadataTypeMap{ - Pos: common.GoPosToSchemaPos(pass.Fset, obj.Pos()), - Runtime: "go", - NativeName: qualifiedNameFromSelectorExpr(pass, node.Type), - }) + if nn := qualifiedNameFromSelectorExpr(pass, node.Type); nn != "" && common.IsExternalType(nn) { + alias.Type = &schema.Any{} + if !hasGoTypeMapping { + alias.Metadata = append(alias.Metadata, &schema.MetadataTypeMap{ + Pos: common.GoPosToSchemaPos(pass.Fset, obj.Pos()), + Runtime: "go", + NativeName: nn, + }) + } return optional.Some(alias) } + schType, ok := common.ExtractType(pass, node.Type).Get() + if !ok { + return optional.None[*schema.TypeAlias]() + } + alias.Type = schType + // type aliases must have an underlying type, and the type cannot be a reference to the alias itself. if common.IsSelfReference(pass, obj, schType) { return optional.None[*schema.TypeAlias]() diff --git a/go-runtime/schema/typeenumvariant/analyzer.go b/go-runtime/schema/typeenumvariant/analyzer.go index f855416bc..a199bc9c3 100644 --- a/go-runtime/schema/typeenumvariant/analyzer.go +++ b/go-runtime/schema/typeenumvariant/analyzer.go @@ -61,7 +61,7 @@ func extractEnumVariant(pass *analysis.Pass, node *ast.TypeSpec, obj types.Objec // valueFunc is only executed if this potential variant actually makes it to the schema. // Executing may result in transitive schema extraction, so we only execute if necessary. valueFunc := func(p *analysis.Pass) optional.Option[*schema.TypeValue] { - value, ok := common.ExtractTypeForNode(p, obj, node.Type, nil).Get() + value, ok := common.ExtractType(p, node).Get() if !ok { return optional.None[*schema.TypeValue]() } diff --git a/go-runtime/schema/verb/analyzer.go b/go-runtime/schema/verb/analyzer.go index fb93c5249..832f8eb06 100644 --- a/go-runtime/schema/verb/analyzer.go +++ b/go-runtime/schema/verb/analyzer.go @@ -17,10 +17,10 @@ import ( // Extractor extracts verbs to the module schema. var Extractor = common.NewDeclExtractor[*schema.Verb, *ast.FuncDecl]("verb", Extract) -func Extract(pass *analysis.Pass, root *ast.FuncDecl, obj types.Object) optional.Option[*schema.Verb] { +func Extract(pass *analysis.Pass, node *ast.FuncDecl, obj types.Object) optional.Option[*schema.Verb] { verb := &schema.Verb{ - Pos: common.GoPosToSchemaPos(pass.Fset, root.Pos()), - Name: strcase.ToLowerCamel(root.Name.Name), + Pos: common.GoPosToSchemaPos(pass.Fset, node.Pos()), + Name: strcase.ToLowerCamel(node.Name.Name), } if !common.ApplyMetadata[*schema.Verb](pass, obj, func(md *common.ExtractedMetadata) { verb.Comments = md.Comments @@ -33,29 +33,29 @@ func Extract(pass *analysis.Pass, root *ast.FuncDecl, obj types.Object) optional 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") + common.Errorf(pass, node, "ftl:verb cannot be a method") return optional.None[*schema.Verb]() } - params := sig.Params() - results := sig.Results() - reqt, respt := checkSignature(pass, root, sig) + + reqt, respt := checkSignature(pass, node, sig) req := optional.Some[schema.Type](&schema.Unit{}) if reqt.Ok() { - req = common.ExtractType(pass, params.At(1).Pos(), params.At(1).Type()) + req = common.ExtractType(pass, node.Type.Params.List[1]) } resp := optional.Some[schema.Type](&schema.Unit{}) if respt.Ok() { - resp = common.ExtractType(pass, results.At(0).Pos(), results.At(0).Type()) + resp = common.ExtractType(pass, node.Type.Results.List[0]) } + + params := sig.Params() + results := sig.Results() 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()) + common.Errorf(pass, node.Type.Params.List[1], "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()) + common.Errorf(pass, node.Type.Results.List[0], "unsupported response type %q", results.At(0).Type()) } verb.Request = reqV verb.Response = resV