diff --git a/go-runtime/compile/schema.go b/go-runtime/compile/schema.go index bc13d1ea7e..09cc41815e 100644 --- a/go-runtime/compile/schema.go +++ b/go-runtime/compile/schema.go @@ -26,17 +26,13 @@ import ( var ( fset = token.NewFileSet() - ftlPkgPath = "github.com/TBD54566975/ftl/go-runtime/ftl" ftlCallFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Call" ftlFSMFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.FSM" ftlTransitionFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Transition" ftlStartFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Start" - ftlConfigFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Config" - ftlSecretFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Secret" //nolint:gosec ftlPostgresDBFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.PostgresDatabase" ftlTopicFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Topic" ftlSubscriptionFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Subscription" - ftlTopicHandleTypeName = "TopicHandle" aliasFieldTag = "json" ) @@ -65,15 +61,6 @@ func errorf(node ast.Node, format string, args ...interface{}) *schema.Error { return schema.Errorf(pos, endCol, format, args...) } -func tokenWrapf(pos token.Pos, tokenText string, err error, format string, args ...interface{}) *schema.Error { - goPos := goPosToSchemaPos(pos) - endColumn := goPos.Column - if len(tokenText) > 0 { - endColumn += utf8.RuneCountInString(tokenText) - } - return schema.Wrapf(goPos, endColumn, err, format, args...) -} - //nolint:unparam func wrapf(node ast.Node, err error, format string, args ...interface{}) *schema.Error { pos, endCol := goNodePosToSchemaPos(node) @@ -225,8 +212,6 @@ func extractTopicDecl(pctx *parseContext, node *ast.CallExpr, stack []ast.Node) } func visitCallExpr(pctx *parseContext, node *ast.CallExpr, stack []ast.Node) { - validateCallExpr(pctx, node) - _, fn := deref[*types.Func](pctx.pkg, node.Fun) if fn == nil { return @@ -235,10 +220,6 @@ func visitCallExpr(pctx *parseContext, node *ast.CallExpr, stack []ast.Node) { case ftlCallFuncPath: parseCall(pctx, node, stack) - case ftlConfigFuncPath, ftlSecretFuncPath: - // Secret/config declaration: ftl.Config[<type>](<name>) - parseConfigDecl(pctx, node, fn) - case ftlFSMFuncPath: parseFSMDecl(pctx, node, stack) @@ -253,56 +234,6 @@ func visitCallExpr(pctx *parseContext, node *ast.CallExpr, stack []ast.Node) { } } -// validateCallExpr validates all function calls -// checks if the function call is: -// - a direct verb call to an external module -// - a direct publish call on an external module's topic -func validateCallExpr(pctx *parseContext, node *ast.CallExpr) { - selExpr, ok := node.Fun.(*ast.SelectorExpr) - if !ok { - return - } - var lhsIdent *ast.Ident - if expr, ok := selExpr.X.(*ast.SelectorExpr); ok { - lhsIdent = expr.Sel - } else if ident, ok := selExpr.X.(*ast.Ident); ok { - lhsIdent = ident - } else { - return - } - lhsObject := pctx.pkg.TypesInfo.ObjectOf(lhsIdent) - if lhsObject == nil { - return - } - var lhsPkgPath string - if pkgName, ok := lhsObject.(*types.PkgName); ok { - lhsPkgPath = pkgName.Imported().Path() - } else { - lhsPkgPath = lhsObject.Pkg().Path() - } - var lhsIsExternal bool - if !pctx.isPathInPkg(lhsPkgPath) { - lhsIsExternal = true - } - - if lhsType, ok := pctx.pkg.TypesInfo.TypeOf(selExpr.X).(*types.Named); ok && lhsType.Obj().Pkg() != nil && lhsType.Obj().Pkg().Path() == ftlPkgPath { - // Calling a function on an FTL type - if lhsType.Obj().Name() == ftlTopicHandleTypeName && selExpr.Sel.Name == "Publish" { - if lhsIsExternal { - pctx.errors.add(errorf(node, "can not publish directly to topics in other modules")) - } - } - return - } - - if lhsIsExternal && strings.HasPrefix(lhsPkgPath, "ftl/") { - if sig, ok := pctx.pkg.TypesInfo.TypeOf(selExpr.Sel).(*types.Signature); ok && sig.Recv() == nil { - // can not call functions in external modules directly - pctx.errors.add(errorf(node, "can not call verbs in other modules directly: use ftl.Call(…) instead")) - } - } -} - func parseCall(pctx *parseContext, node *ast.CallExpr, stack []ast.Node) { var activeFuncDecl *ast.FuncDecl for i := len(stack) - 1; i >= 0; i-- { @@ -465,65 +396,6 @@ func parseFSMTransition(pctx *parseContext, node *ast.CallExpr, fn *types.Func, } } -func parseConfigDecl(pctx *parseContext, node *ast.CallExpr, fn *types.Func) { - name, schemaErr := extractStringLiteralArg(node, 0) - if schemaErr != nil { - pctx.errors.add(schemaErr) - return - } - if !schema.ValidateName(name) { - pctx.errors.add(errorf(node, "config and secret names must be valid identifiers")) - } - - index := node.Fun.(*ast.IndexExpr) //nolint:forcetypeassert - - // Type parameter - tp := pctx.pkg.TypesInfo.Types[index.Index].Type - st, ok := visitType(pctx, index.Index.Pos(), tp, false).Get() - if !ok { - pctx.errors.add(errorf(index.Index, "unsupported type %q", tp)) - return - } - - // Add the declaration to the module. - var decl schema.Decl - if fn.FullName() == ftlConfigFuncPath { - decl = &schema.Config{ - Pos: goPosToSchemaPos(node.Pos()), - Name: name, - Type: st, - } - } else { - decl = &schema.Secret{ - Pos: goPosToSchemaPos(node.Pos()), - Name: name, - Type: st, - } - } - - // Check for duplicates - _, endCol := goNodePosToSchemaPos(node) - for _, d := range pctx.module.Decls { - switch fn.FullName() { - case ftlConfigFuncPath: - c, ok := d.(*schema.Config) - if ok && c.Name == name && c.Type.String() == st.String() { - pctx.errors.add(errorf(node, "duplicate config declaration at %d:%d-%d", c.Pos.Line, c.Pos.Column, endCol)) - return - } - case ftlSecretFuncPath: - s, ok := d.(*schema.Secret) - if ok && s.Name == name && s.Type.String() == st.String() { - pctx.errors.add(errorf(node, "duplicate secret declaration at %d:%d-%d", s.Pos.Line, s.Pos.Column, endCol)) - return - } - default: - } - } - - pctx.module.Decls = append(pctx.module.Decls, decl) -} - func parseDatabaseDecl(pctx *parseContext, node *ast.CallExpr, dbType string) { name, schemaErr := extractStringLiteralArg(node, 0) if schemaErr != nil { @@ -832,32 +704,6 @@ func visitStruct(pctx *parseContext, pos token.Pos, tnode types.Type, isExported return optional.Some[*schema.Ref](dataRef) } -func visitConst(pctx *parseContext, cnode *types.Const) optional.Option[schema.Value] { - if b, ok := cnode.Type().Underlying().(*types.Basic); ok { - switch b.Kind() { - case types.String: - value, err := strconv.Unquote(cnode.Val().String()) - if err != nil { - pctx.errors.add(tokenWrapf(cnode.Pos(), cnode.Val().String(), err, "unsupported string constant")) - return optional.None[schema.Value]() - } - return optional.Some[schema.Value](&schema.StringValue{Pos: goPosToSchemaPos(cnode.Pos()), Value: value}) - - case types.Int: - value, err := strconv.ParseInt(cnode.Val().String(), 10, 64) - if err != nil { - pctx.errors.add(tokenWrapf(cnode.Pos(), cnode.Val().String(), err, "unsupported int constant")) - return optional.None[schema.Value]() - } - return optional.Some[schema.Value](&schema.IntValue{Pos: goPosToSchemaPos(cnode.Pos()), Value: int(value)}) - - default: - return optional.None[schema.Value]() - } - } - return optional.None[schema.Value]() -} - func visitType(pctx *parseContext, pos token.Pos, tnode types.Type, isExported bool) optional.Option[schema.Type] { if tparam, ok := tnode.(*types.TypeParam); ok { return optional.Some[schema.Type](&schema.Ref{Pos: goPosToSchemaPos(pos), Name: tparam.Obj().Id()}) diff --git a/go-runtime/compile/schema_test.go b/go-runtime/compile/schema_test.go index 597f6eae0e..153211cff1 100644 --- a/go-runtime/compile/schema_test.go +++ b/go-runtime/compile/schema_test.go @@ -543,26 +543,28 @@ func TestErrorReporting(t *testing.T) { r, err := ExtractModuleSchema("testdata/failing", &schema.Schema{}) assert.NoError(t, err) + var actualParent []string + var actualChild []string filename := filepath.Join(pwd, `testdata/failing/failing.go`) subFilename := filepath.Join(pwd, `testdata/failing/child/child.go`) - actual := slices.Map(r.Errors, func(e *schema.Error) string { - str := strings.ReplaceAll(e.Error(), filename+":", "") - str = strings.ReplaceAll(str, subFilename+":", "") - return str - }) - expected := []string{ - `6:2-6: unsupported type "uint64" for field "Body"`, - `11:2-2: unsupported external type "github.com/TBD54566975/ftl/go-runtime/compile/testdata.lib.NonFTLType"`, - `11:2-7: unsupported type "github.com/TBD54566975/ftl/go-runtime/compile/testdata.NonFTLType" for field "Field"`, + for _, e := range r.Errors { + str := strings.ReplaceAll(e.Error(), subFilename+":", "") + str = strings.ReplaceAll(str, filename+":", "") + if e.Pos.Filename == filename { + actualParent = append(actualParent, str) + } else { + actualChild = append(actualChild, str) + } + } + + // failing/failing.go + expectedParent := []string{ `13:13-34: expected string literal for argument at index 0`, - `16: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`, - `16:18-52: duplicate config declaration at 15:18-52`, - `19:18-52: duplicate secret declaration at 18:18-52`, - `21:6-6: multiple Go type mappings found for "ftl/failing/child.MultipleMappings"`, + `16:18-18: duplicate config declaration for "failing.FTL_ENDPOINT"; already declared at "37:18"`, + `19:18-18: duplicate secret declaration for "failing.FTL_ENDPOINT"; already declared at "38:18"`, `22:14-44: duplicate database declaration at 21:14-44`, `25:2-10: unsupported type "error" for field "BadParam"`, `28:2-17: unsupported type "uint64" for field "AnotherBadParam"`, - `31:2-13: enum variant "SameVariant" conflicts with existing enum variant of "EnumVariantConflictParent" at "196:2"`, `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:50-50: unsupported response type "ftl/failing.Response"`, @@ -593,7 +595,7 @@ func TestErrorReporting(t *testing.T) { `90:3-3: unexpected directive "ftl:verb"`, `99:6-18: "BadValueEnum" is a value enum and cannot be tagged as a variant of type enum "TypeEnum" directly`, `108:6-35: "BadValueEnumOrderDoesntMatter" is a value enum and cannot be tagged as a variant of type enum "TypeEnum" directly`, - `124:21-60: config and secret names must be valid identifiers`, + `124:21-60: config names must be valid identifiers`, `130:1-1: schema declaration contains conflicting directives`, `130:1-26: only one directive expected when directive "ftl:enum" is present, found multiple`, `152:6-45: enum discriminator "TypeEnum3" cannot contain exported methods`, @@ -603,9 +605,20 @@ func TestErrorReporting(t *testing.T) { `175:9-26: can not call verbs in other modules directly: use ftl.Call(…) instead`, `180:2-12: struct field unexported must be exported by starting with an uppercase letter`, `184:6-6: unsupported type "ftl/failing/child.BadChildStruct" for field "child"`, - `189:6-6: duplicate Data declaration for "failing.Redeclared" in "ftl/failing"; already declared in "ftl/failing/child"`, + `189:6-6: duplicate data declaration for "failing.Redeclared"; already declared at "27:6"`, } - assert.Equal(t, expected, actual) + + // 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"`, + `14:2-7: unsupported type "github.com/TBD54566975/ftl/go-runtime/compile/testdata.NonFTLType" for field "Field"`, + `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"`, + } + assert.Equal(t, expectedParent, actualParent) + assert.Equal(t, expectedChild, actualChild) } // Where parsing is correct but validation of the schema fails diff --git a/go-runtime/compile/testdata/failing/child/child.go b/go-runtime/compile/testdata/failing/child/child.go index 4e35f68aa0..f435311375 100644 --- a/go-runtime/compile/testdata/failing/child/child.go +++ b/go-runtime/compile/testdata/failing/child/child.go @@ -1,6 +1,9 @@ package child -import lib "github.com/TBD54566975/ftl/go-runtime/compile/testdata" +import ( + lib "github.com/TBD54566975/ftl/go-runtime/compile/testdata" + "github.com/TBD54566975/ftl/go-runtime/ftl" +) type BadChildStruct struct { Body uint64 @@ -30,3 +33,6 @@ type EnumVariantConflictChild int const ( SameVariant EnumVariantConflictChild = iota ) + +var duplConfig = ftl.Config[string]("FTL_ENDPOINT") +var duplSecret = ftl.Secret[string]("FTL_ENDPOINT") diff --git a/go-runtime/compile/testdata/failing/failing.go b/go-runtime/compile/testdata/failing/failing.go index b802a369e6..e33ed4fd90 100644 --- a/go-runtime/compile/testdata/failing/failing.go +++ b/go-runtime/compile/testdata/failing/failing.go @@ -12,11 +12,11 @@ import ( var empty = ftl.Config[string](1) +// var duplConfig = ftl.Config[string]("FTL_ENDPOINT") var goodConfig = ftl.Config[string]("FTL_ENDPOINT") -var duplConfig = ftl.Config[string]("FTL_ENDPOINT") +// var duplSecret = ftl.Secret[string]("FTL_ENDPOINT") var goodSecret = ftl.Secret[string]("FTL_ENDPOINT") -var duplSecret = ftl.Secret[string]("FTL_ENDPOINT") var goodDB = ftl.PostgresDatabase("testDb") var duplDB = ftl.PostgresDatabase("testDb") diff --git a/go-runtime/schema/call/analyzer.go b/go-runtime/schema/call/analyzer.go new file mode 100644 index 0000000000..153b7b3127 --- /dev/null +++ b/go-runtime/schema/call/analyzer.go @@ -0,0 +1,88 @@ +package call + +import ( + "go/ast" + "go/types" + "strings" + + "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" +) + +const ( + ftlPkgPath = "github.com/TBD54566975/ftl/go-runtime/ftl" + ftlTopicHandleTypeName = "TopicHandle" +) + +// Extractor extracts all function calls. +var Extractor = common.NewExtractor("validate", (*Fact)(nil), Extract) + +type Tag struct{} // Tag uniquely identifies the fact type for this extractor. +type Fact = common.DefaultFact[Tag] + +func Extract(pass *analysis.Pass) (interface{}, error) { + //TODO: implement call metadata extraction (for now this just validates all calls) + + in := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) //nolint:forcetypeassert + nodeFilter := []ast.Node{ + (*ast.CallExpr)(nil), + } + in.Preorder(nodeFilter, func(n ast.Node) { + node := n.(*ast.CallExpr) //nolint:forcetypeassert + validateCallExpr(pass, node) + }) + return common.NewExtractorResult(pass), nil +} + +// validateCallExpr validates all function calls +// checks if the function call is: +// - a direct verb call to an external module +// - a direct publish call on an external module's topic +func validateCallExpr(pass *analysis.Pass, node *ast.CallExpr) { + selExpr, ok := node.Fun.(*ast.SelectorExpr) + if !ok { + return + } + var lhsIdent *ast.Ident + if expr, ok := selExpr.X.(*ast.SelectorExpr); ok { + lhsIdent = expr.Sel + } else if ident, ok := selExpr.X.(*ast.Ident); ok { + lhsIdent = ident + } else { + return + } + lhsObject := pass.TypesInfo.ObjectOf(lhsIdent) + if lhsObject == nil { + return + } + var lhsPkgPath string + if pkgName, ok := lhsObject.(*types.PkgName); ok { + lhsPkgPath = pkgName.Imported().Path() + } else { + lhsPkgPath = lhsObject.Pkg().Path() + } + var lhsIsExternal bool + if !common.IsPathInPkg(pass.Pkg, lhsPkgPath) { + lhsIsExternal = true + } + + if lhsType, ok := pass.TypesInfo.TypeOf(selExpr.X).(*types.Named); ok && lhsType.Obj().Pkg() != nil && + lhsType.Obj().Pkg().Path() == ftlPkgPath { + // Calling a function on an FTL type + if lhsType.Obj().Name() == ftlTopicHandleTypeName && selExpr.Sel.Name == "Publish" { + if lhsIsExternal { + common.Errorf(pass, node, "can not publish directly to topics in other modules") + } + } + return + } + + if lhsIsExternal && strings.HasPrefix(lhsPkgPath, "ftl/") { + if sig, ok := pass.TypesInfo.TypeOf(selExpr.Sel).(*types.Signature); ok && sig.Recv() == nil { + // can not call functions in external modules directly + common.Errorf(pass, node, "can not call verbs in other modules directly: use ftl.Call(…) instead") + } + } +} diff --git a/go-runtime/schema/common/common.go b/go-runtime/schema/common/common.go index 377e839035..24fec17a86 100644 --- a/go-runtime/schema/common/common.go +++ b/go-runtime/schema/common/common.go @@ -6,6 +6,7 @@ import ( "go/token" "go/types" "reflect" + "strconv" "strings" "github.com/alecthomas/types/optional" @@ -482,7 +483,54 @@ func GetDeclTypeName(d schema.Decl) string { if lastDotIndex == -1 { return typeStr } - return typeStr[lastDotIndex+1:] + return strcase.ToLowerCamel(typeStr[lastDotIndex+1:]) +} + +func Deref[T types.Object](pass *analysis.Pass, node ast.Expr) (string, T) { + var obj T + switch node := node.(type) { + case *ast.Ident: + obj, _ = pass.TypesInfo.Uses[node].(T) + return "", obj + + case *ast.SelectorExpr: + x, ok := node.X.(*ast.Ident) + if !ok { + return "", obj + } + obj, _ = pass.TypesInfo.Uses[node.Sel].(T) + return x.Name, obj + + case *ast.IndexExpr: + return Deref[T](pass, node.X) + + default: + return "", obj + } +} + +func ExtractStringLiteralArg(pass *analysis.Pass, node *ast.CallExpr, argIndex int) string { + if argIndex >= len(node.Args) { + Errorf(pass, node, "expected string argument at index %d", argIndex) + return "" + } + + literal, ok := node.Args[argIndex].(*ast.BasicLit) + if !ok || literal.Kind != token.STRING { + Errorf(pass, node, "expected string literal for argument at index %d", argIndex) + return "" + } + + s, err := strconv.Unquote(literal.Value) + if err != nil { + Wrapf(pass, node, err, "") + return "" + } + if s == "" { + Errorf(pass, node, "expected non-empty string literal for argument at index %d", argIndex) + return "" + } + return s } func isLocalRef(pass *analysis.Pass, ref *schema.Ref) bool { diff --git a/go-runtime/schema/configsecret/analyzer.go b/go-runtime/schema/configsecret/analyzer.go new file mode 100644 index 0000000000..2f60064bcf --- /dev/null +++ b/go-runtime/schema/configsecret/analyzer.go @@ -0,0 +1,111 @@ +package configsecret + +import ( + "go/ast" + "go/types" + + "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/go-runtime/schema/common" + "github.com/TBD54566975/golang-tools/go/analysis" +) + +const ( + ftlConfigFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Config" + ftlSecretFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Secret" //nolint:gosec +) + +// Extractor extracts configs and secrets. +var Extractor = common.NewExtractor("configsecret", (*Fact)(nil), Extract) + +type Tag struct{} // Tag uniquely identifies the fact type for this extractor. +type Fact = common.DefaultFact[Tag] + +func Extract(pass *analysis.Pass) (interface{}, error) { + in := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) //nolint:forcetypeassert + nodeFilter := []ast.Node{ + (*ast.ValueSpec)(nil), + } + in.Preorder(nodeFilter, func(n ast.Node) { + node := n.(*ast.ValueSpec) //nolint:forcetypeassert + obj, ok := common.GetObjectForNode(pass.TypesInfo, node).Get() + if !ok { + return + } + if len(node.Values) != 1 { + return + } + callExpr, ok := node.Values[0].(*ast.CallExpr) + if !ok { + return + } + _, fn := common.Deref[*types.Func](pass, callExpr.Fun) + if fn == nil { + return + } + var comments []string + if md, ok := common.GetFactForObject[*common.ExtractedMetadata](pass, obj).Get(); ok { + comments = md.Comments + } + var decl optional.Option[schema.Decl] + switch fn.FullName() { + case ftlSecretFuncPath: + decl = extractConfigSecret[*schema.Secret](pass, callExpr, comments) + case ftlConfigFuncPath: + decl = extractConfigSecret[*schema.Config](pass, callExpr, comments) + } + if d, ok := decl.Get(); ok { + common.MarkSchemaDecl(pass, obj, d) + } + }) + return common.NewExtractorResult(pass), nil +} + +func extractConfigSecret[T schema.Decl]( + pass *analysis.Pass, + node *ast.CallExpr, + comments []string, +) optional.Option[schema.Decl] { + name := common.ExtractStringLiteralArg(pass, node, 0) + if name == "" { + return optional.None[schema.Decl]() + } + var t T + if !schema.ValidateName(name) { + common.Errorf(pass, node, "%s names must be valid identifiers", common.GetDeclTypeName(t)) + } + + 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() + if !ok { + common.Errorf(pass, index.Index, "config is unsupported type %q", tp) + return optional.None[schema.Decl]() + } + + var decl schema.Decl + switch any(t).(type) { + case *schema.Config: + decl = &schema.Config{ + Pos: common.GoPosToSchemaPos(pass.Fset, node.Pos()), + Comments: comments, + Name: name, + Type: st, + } + case *schema.Secret: + decl = &schema.Secret{ + Pos: common.GoPosToSchemaPos(pass.Fset, node.Pos()), + Comments: comments, + Name: name, + Type: st, + } + default: + return optional.None[schema.Decl]() + } + + return optional.Some(decl) +} diff --git a/go-runtime/schema/enum/analyzer.go b/go-runtime/schema/enum/analyzer.go index 2a5edb5cf7..cbee770530 100644 --- a/go-runtime/schema/enum/analyzer.go +++ b/go-runtime/schema/enum/analyzer.go @@ -81,6 +81,9 @@ func findValueEnumVariants(pass *analysis.Pass, obj types.Object) []*schema.Enum func validateVariant(pass *analysis.Pass, obj types.Object, variant *schema.EnumVariant) bool { for _, fact := range common.GetAllFacts[*common.ExtractedDecl](pass) { + if fact.Decl == nil { + continue + } existingEnum, ok := fact.Decl.(*schema.Enum) if !ok { continue diff --git a/go-runtime/schema/extract.go b/go-runtime/schema/extract.go index 5b4d493185..b155defecb 100644 --- a/go-runtime/schema/extract.go +++ b/go-runtime/schema/extract.go @@ -4,11 +4,14 @@ import ( "fmt" "go/types" + "github.com/TBD54566975/ftl/go-runtime/schema/call" + "github.com/TBD54566975/ftl/go-runtime/schema/configsecret" "github.com/TBD54566975/ftl/go-runtime/schema/enum" "github.com/TBD54566975/ftl/go-runtime/schema/typeenum" "github.com/TBD54566975/ftl/go-runtime/schema/typeenumvariant" "github.com/TBD54566975/ftl/go-runtime/schema/valueenumvariant" "github.com/alecthomas/types/optional" + "github.com/alecthomas/types/tuple" "golang.org/x/exp/maps" "github.com/TBD54566975/ftl/backend/schema" @@ -38,6 +41,7 @@ var Extractors = [][]*analysis.Analyzer{ }, { metadata.Extractor, + call.Extractor, }, { // must run before typeenumvariant.Extractor; typeenum.Extractor determines all possible discriminator @@ -48,6 +52,7 @@ var Extractors = [][]*analysis.Analyzer{ typealias.Extractor, verb.Extractor, data.Extractor, + configsecret.Extractor, valueenumvariant.Extractor, typeenumvariant.Extractor, }, @@ -135,7 +140,7 @@ func combineAllPackageResults(results map[*analysis.Analyzer][]any, diagnostics refResults := make(map[schema.RefKey]refResult) extractedDecls := make(map[schema.Decl]types.Object) // for identifying duplicates - declKeys := make(map[string]types.Object) + declKeys := make(map[string]tuple.Pair[types.Object, schema.Position]) for _, r := range fResults { fr, ok := r.(finalize.Result) if !ok { @@ -153,14 +158,24 @@ func combineAllPackageResults(results map[*analysis.Analyzer][]any, diagnostics } copyFailedRefs(refResults, fr.Failed) for decl, obj := range fr.Extracted { - if existing, ok := declKeys[decl.String()]; ok && existing != obj { + typename := common.GetDeclTypeName(decl) + var key string + switch d := decl.(type) { + case *schema.Config: + key = typename + d.Name + ":" + d.Type.String() + case *schema.Secret: + key = typename + d.Name + ":" + d.Type.String() + default: + key = typename + d.GetName() + } + if value, ok := declKeys[key]; ok && value.A != obj { // decls redeclared in subpackage combined.Errors = append(combined.Errors, schema.Errorf(decl.Position(), decl.Position().Column, - "duplicate %s declaration for %q in %q; already declared in %q", common.GetDeclTypeName(decl), - combined.Module.Name+"."+decl.GetName(), obj.Pkg().Path(), existing.Pkg().Path())) + "duplicate %s declaration for %q; already declared at %q", typename, + combined.Module.Name+"."+decl.GetName(), value.B)) continue } - declKeys[decl.String()] = obj + declKeys[key] = tuple.Pair[types.Object, schema.Position]{A: obj, B: decl.Position()} extractedDecls[decl] = obj } maps.Copy(combined.NativeNames, fr.NativeNames)