diff --git a/buildengine/build_go_test.go b/buildengine/build_go_test.go index e97aefcc71..7d2d30d11e 100644 --- a/buildengine/build_go_test.go +++ b/buildengine/build_go_test.go @@ -184,6 +184,10 @@ func TestExternalType(t *testing.T) { sch: &schema.Schema{}, } testBuild(t, bctx, true, []assertion{ - assertBuildProtoErrors("unsupported external type \"time.Month\""), + assertBuildProtoErrors( + "unsupported external type \"time.Month\"", + "invalid type \"ftl/external.ExternalResponse\"", + "invalid response type \"ftl/external.ExternalResponse\"", + ), }) } diff --git a/go-runtime/compile/schema.go b/go-runtime/compile/schema.go index 9b224f648a..d2f16f85ca 100644 --- a/go-runtime/compile/schema.go +++ b/go-runtime/compile/schema.go @@ -276,15 +276,15 @@ func checkSignature(pctx *parseContext, node *ast.FuncDecl, sig *types.Signature if params.Len() == 0 { pctx.errors = append(pctx.errors, errorf(node, "first parameter must be context.Context")) } else if !types.AssertableTo(contextIfaceType(), params.At(0).Type()) { - pctx.errors = append(pctx.errors, errorf(node, "first parameter must be of type context.Context but is %s", params.At(0).Type())) + pctx.errors = append(pctx.errors, tokenErrorf(params.At(0).Pos(), params.At(0).Name(), "first parameter must be of type context.Context but is %s", params.At(0).Type())) } if params.Len() == 2 { if !isType[*types.Struct](params.At(1).Type()) { - pctx.errors = append(pctx.errors, errorf(node, "second parameter must be a struct but is %s", params.At(1).Type())) + pctx.errors = append(pctx.errors, tokenErrorf(params.At(1).Pos(), params.At(1).Name(), "second parameter must be a struct but is %s", params.At(1).Type())) } if params.At(1).Type().String() == ftlUnitTypePath { - pctx.errors = append(pctx.errors, errorf(node, "second parameter must not be ftl.Unit")) + pctx.errors = append(pctx.errors, tokenErrorf(params.At(1).Pos(), params.At(1).Name(), "second parameter must not be ftl.Unit")) } req = optional.Some(params.At(1)) @@ -296,14 +296,14 @@ func checkSignature(pctx *parseContext, node *ast.FuncDecl, sig *types.Signature if results.Len() == 0 { pctx.errors = append(pctx.errors, errorf(node, "must at least return an error")) } else if !types.AssertableTo(errorIFaceType(), results.At(results.Len()-1).Type()) { - pctx.errors = append(pctx.errors, errorf(node, "must return an error but is %s", results.At(0).Type())) + pctx.errors = append(pctx.errors, tokenErrorf(results.At(results.Len()-1).Pos(), results.At(results.Len()-1).Name(), "must return an error but is %s", results.At(0).Type())) } if results.Len() == 2 { if !isType[*types.Struct](results.At(0).Type()) { - pctx.errors = append(pctx.errors, errorf(node, "first result must be a struct but is %s", results.At(0).Type())) + pctx.errors = append(pctx.errors, tokenErrorf(results.At(0).Pos(), results.At(0).Name(), "first result must be a struct but is %s", results.At(0).Type())) } if results.At(1).Type().String() == ftlUnitTypePath { - pctx.errors = append(pctx.errors, errorf(node, "second result must not be ftl.Unit")) + pctx.errors = append(pctx.errors, tokenErrorf(results.At(1).Pos(), results.At(1).Name(), "second result must not be ftl.Unit")) } resp = optional.Some(results.At(0)) } @@ -465,7 +465,7 @@ func visitFuncDecl(pctx *parseContext, node *ast.FuncDecl) (verb *schema.Verb) { for _, name := range pctx.nativeNames { if name == node.Name.Name { - pctx.errors = append(pctx.errors, errorf(node, "verb %q already exported", node.Name.Name)) + pctx.errors = append(pctx.errors, noEndColumnErrorf(node.Pos(), "verb %q already exported", node.Name.Name)) return nil } } @@ -494,10 +494,14 @@ func visitFuncDecl(pctx *parseContext, node *ast.FuncDecl) (verb *schema.Verb) { } reqV, reqOk := req.Get() resV, respOk := resp.Get() - if !reqOk || !respOk { - return nil + if !reqOk { + pctx.errors = append(pctx.errors, tokenErrorf(params.At(1).Pos(), params.At(1).Name(), + "invalid request type %q", params.At(1).Type())) + } + if !respOk { + pctx.errors = append(pctx.errors, tokenErrorf(results.At(0).Pos(), results.At(0).Name(), + "invalid response type %q", results.At(0).Type())) } - verb = &schema.Verb{ Pos: goPosToSchemaPos(node.Pos()), Comments: visitComments(node.Doc), @@ -613,6 +617,8 @@ func visitStruct(pctx *parseContext, pos token.Pos, tnode types.Type) optional.O pctx.errors = append(pctx.errors, tokenErrorf(pos, named.String(), "expected struct but got %s", named)) return optional.None[*schema.Ref]() } + + fieldErrors := false for i := range s.NumFields() { f := s.Field(i) if ft, ok := visitType(pctx, f.Pos(), f.Type()).Get(); ok { @@ -620,6 +626,7 @@ func visitStruct(pctx *parseContext, pos token.Pos, tnode types.Type) optional.O if len(f.Name()) > 0 && unicode.IsLower(rune(f.Name()[0])) { pctx.errors = append(pctx.errors, tokenErrorf(f.Pos(), f.Name(), "struct field %s must be exported by starting with an uppercase letter", f.Name())) + fieldErrors = true } // Extract the JSON tag and split it to get just the field name @@ -644,8 +651,14 @@ func visitStruct(pctx *parseContext, pos token.Pos, tnode types.Type) optional.O Type: ft, Metadata: metadata, }) + } else { + fieldErrors = true } } + if fieldErrors { + return optional.None[*schema.Ref]() + } + pctx.module.AddData(out) return optional.Some[*schema.Ref](dataRef) } @@ -733,6 +746,7 @@ func visitType(pctx *parseContext, pos token.Pos, tnode types.Type) optional.Opt if ref, ok := visitStruct(pctx, pos, tnode).Get(); ok { return optional.Some[schema.Type](ref) } + pctx.errors = append(pctx.errors, noEndColumnErrorf(pos, "invalid type %q", tnode)) return optional.None[schema.Type]() } @@ -759,6 +773,7 @@ func visitType(pctx *parseContext, pos token.Pos, tnode types.Type) optional.Opt if ref, ok := visitStruct(pctx, pos, tnode).Get(); ok { return optional.Some[schema.Type](ref) } + pctx.errors = append(pctx.errors, noEndColumnErrorf(pos, "invalid type %q", tnode)) return optional.None[schema.Type]() } diff --git a/go-runtime/compile/schema_test.go b/go-runtime/compile/schema_test.go index e17ef0f4b9..b0becc49fe 100644 --- a/go-runtime/compile/schema_test.go +++ b/go-runtime/compile/schema_test.go @@ -249,21 +249,42 @@ func TestErrorReporting(t *testing.T) { filename+":13:2-2: unsupported type \"error\"\n"+ filename+":16:2-2: unsupported basic type \"uint64\"\n"+ filename+":19:3-3: unexpected token \"verb\" (expected Directive)\n"+ + filename+":25:1-1: invalid type \"ftl/failing.Request\"\n"+ + filename+":25:1-1: invalid type \"ftl/failing.Response\"\n"+ + filename+":25:36-39: invalid request type \"ftl/failing.Request\"\n"+ + filename+":25:50-50: invalid response type \"ftl/failing.Response\"\n"+ filename+":26:16-29: call first argument must be a function in an ftl module\n"+ filename+":27:2-46: call must have exactly three arguments\n"+ filename+":28:16-25: call first argument must be a function\n"+ + filename+":33:1-1: invalid type \"ftl/failing.Response\"\n"+ filename+":33:1-2: must have at most two parameters (context.Context, struct)\n"+ - filename+":38:1-2: first parameter must be of type context.Context but is ftl/failing.Request\n"+ - filename+":38:1-2: second parameter must be a struct but is string\n"+ - filename+":43:1-2: second parameter must not be ftl.Unit\n"+ + filename+":33:69-69: invalid response type \"ftl/failing.Response\"\n"+ + filename+":38:1-1: invalid type \"ftl/failing.Response\"\n"+ + filename+":38:22-27: first parameter must be of type context.Context but is ftl/failing.Request\n"+ + filename+":38:37-43: second parameter must be a struct but is string\n"+ + filename+":38:53-53: invalid response type \"ftl/failing.Response\"\n"+ + filename+":43:1-1: invalid type \"ftl/failing.Response\"\n"+ + filename+":43:43-47: second parameter must not be ftl.Unit\n"+ + filename+":43:59-59: invalid response type \"ftl/failing.Response\"\n"+ + filename+":48:1-1: invalid type \"ftl/failing.Response\"\n"+ filename+":48:1-2: first parameter must be context.Context\n"+ + filename+":48:18-18: invalid response type \"ftl/failing.Response\"\n"+ + filename+":53:1-1: invalid type \"ftl/failing.Request\"\n"+ filename+":53:1-2: must have at most two results (struct, error)\n"+ + filename+":53:41-44: invalid request type \"ftl/failing.Request\"\n"+ + filename+":58:1-1: invalid type \"ftl/failing.Request\"\n"+ filename+":58:1-2: must at least return an error\n"+ - filename+":62:1-2: must return an error but is ftl/failing.Response\n"+ - filename+":67:1-2: first result must be a struct but is string\n"+ - filename+":67:1-2: must return an error but is string\n"+ - filename+":67:1-2: second result must not be ftl.Unit\n"+ - filename+":74:1-2: verb \"WrongResponse\" already exported\n"+ + filename+":58:36-39: invalid request type \"ftl/failing.Request\"\n"+ + filename+":62:1-1: invalid type \"ftl/failing.Request\"\n"+ + filename+":62:35-38: invalid request type \"ftl/failing.Request\"\n"+ + filename+":62:48-48: must return an error but is ftl/failing.Response\n"+ + filename+":67:1-1: invalid type \"ftl/failing.Request\"\n"+ + filename+":67:41-44: invalid request type \"ftl/failing.Request\"\n"+ + filename+":67:55-55: first result must be a struct but is string\n"+ + filename+":67:63-63: must return an error but is string\n"+ + filename+":67:63-63: second result must not be ftl.Unit\n"+ + filename+":74:1-1: verb \"WrongResponse\" already exported\n"+ + filename+":79:6-6: invalid type \"ftl/failing.BadStruct\"\n"+ filename+":80:2-12: struct field unexported must be exported by starting with an uppercase letter", ) } @@ -274,5 +295,5 @@ func TestDuplicateVerbNames(t *testing.T) { } pwd, _ := os.Getwd() _, _, err := ExtractModuleSchema("testdata/duplicateverbs") - assert.EqualError(t, err, filepath.Join(pwd, `testdata/duplicateverbs/duplicateverbs.go`)+`:23:1-2: verb "Time" already exported`) + assert.EqualError(t, err, filepath.Join(pwd, `testdata/duplicateverbs/duplicateverbs.go`)+`:23:1-1: verb "Time" already exported`) }