Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: extract verb call metadata #2166

Merged
merged 1 commit into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 1 addition & 67 deletions go-runtime/compile/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
var (
fset = token.NewFileSet()

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"
Expand Down Expand Up @@ -117,76 +116,11 @@ func visitCallExpr(pctx *parseContext, node *ast.CallExpr, stack []ast.Node) {
if fn == nil {
return
}
switch fn.FullName() {
case ftlCallFuncPath:
parseCall(pctx, node, stack)

case ftlFSMFuncPath:
if fn.FullName() == ftlFSMFuncPath {
parseFSMDecl(pctx, node, stack)
}
}

func parseCall(pctx *parseContext, node *ast.CallExpr, stack []ast.Node) {
var activeFuncDecl *ast.FuncDecl
for i := len(stack) - 1; i >= 0; i-- {
if found, ok := stack[i].(*ast.FuncDecl); ok {
activeFuncDecl = found
break
}
// use element
}
if activeFuncDecl == nil {
return
}
expectedVerbName := strcase.ToLowerCamel(activeFuncDecl.Name.Name)
var activeVerb *schema.Verb
for _, decl := range pctx.module.Decls {
if aVerb, ok := decl.(*schema.Verb); ok && aVerb.Name == expectedVerbName {
activeVerb = aVerb
break
}
}
if activeVerb == nil {
return
}
if len(node.Args) != 3 {
pctx.errors.add(errorf(node, "call must have exactly three arguments"))
return
}
ref := parseVerbRef(pctx, node.Args[1])
if ref == nil {
var suffix string
var ok bool
ref, ok = parseSelectorRef(node.Args[1])
if ok && pctx.schema.Resolve(ref).Ok() {
suffix = ", does it need to be exported?"
}
if sel, ok := node.Args[1].(*ast.SelectorExpr); ok {
pctx.errors.add(errorf(node.Args[1], "call first argument must be a function but is an unresolved reference to %s.%s%s", sel.X, sel.Sel, suffix))
}
pctx.errors.add(errorf(node.Args[1], "call first argument must be a function in an ftl module%s", suffix))
return
}
activeVerb.AddCall(ref)
}

func parseSelectorRef(node ast.Expr) (*schema.Ref, bool) {
sel, ok := node.(*ast.SelectorExpr)
if !ok {
return nil, false
}
ident, ok := sel.X.(*ast.Ident)
if !ok {
return nil, false
}
return &schema.Ref{
Pos: goPosToSchemaPos(node.Pos()),
Module: ident.Name,
Name: strcase.ToLowerCamel(sel.Sel.Name),
}, true

}

func parseVerbRef(pctx *parseContext, node ast.Expr) *schema.Ref {
_, verbFn := deref[*types.Func](pctx.pkg, node)
if verbFn == nil {
Expand Down
11 changes: 8 additions & 3 deletions go-runtime/compile/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,13 @@ func TestExtractModuleSchemaTwo(t *testing.T) {
export verb callsTwo(two.Payload<String>) two.Payload<String>
+calls two.two

export verb callsTwoAndThree(two.Payload<String>) two.Payload<String>
+calls two.three, two.two

export verb returnsUser(Unit) two.UserResponse

export verb three(two.Payload<String>) two.Payload<String>

export verb two(two.Payload<String>) two.Payload<String>
}
`
Expand Down Expand Up @@ -547,10 +552,10 @@ func TestErrorReporting(t *testing.T) {
`31:3-3: unexpected directive "ftl:export" attached for verb, did you mean to use '//ftl:verb export' instead?`,
`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`,
`38:16-29: call first argument must be a function but is an unresolved reference to lib.OtherFunc, does it need to be exported?`,
`38:16-29: call first argument must be a function in an ftl module, does it need to be exported?`,
`39:2-46: call must have exactly three arguments`,
`40:16-25: call first argument must be a function in an ftl module`,
`40:16-25: call first argument must be a function in an ftl module, does it need to be exported?`,
`45:1-2: must have at most two parameters (context.Context, struct)`,
`45:69-69: unsupported response type "ftl/failing.Response"`,
`50:22-27: first parameter must be of type context.Context but is ftl/failing.Request`,
Expand Down
27 changes: 27 additions & 0 deletions go-runtime/compile/testdata/go/two/two.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,22 @@ func Two(ctx context.Context, req Payload[string]) (Payload[string], error) {
return Payload[string]{}, nil
}

//ftl:verb export
func Three(ctx context.Context, req Payload[string]) (Payload[string], error) {
return Payload[string]{}, nil
}

//ftl:verb export
func CallsTwo(ctx context.Context, req Payload[string]) (Payload[string], error) {
return ftl.Call(ctx, Two, req)
}

//ftl:verb export
func CallsTwoAndThree(ctx context.Context, req Payload[string]) (Payload[string], error) {
err := transitiveVerbCall(ctx, req)
return Payload[string]{}, err
}

//ftl:verb export
func ReturnsUser(ctx context.Context) (UserResponse, error) {
return UserResponse{
Expand Down Expand Up @@ -87,3 +98,19 @@ type ExplicitAliasAlias = lib.NonFTLType
type TransitiveAliasType lib.NonFTLType

type TransitiveAliasAlias = lib.NonFTLType

type TransitiveAlias lib.NonFTLType

func transitiveVerbCall(ctx context.Context, req Payload[string]) error {
_, err := ftl.Call(ctx, Two, req)
if err != nil {
return err
}
err = superTransitiveVerbCall(ctx, req)
return err
}

func superTransitiveVerbCall(ctx context.Context, req Payload[string]) error {
_, err := ftl.Call(ctx, Three, req)
return err
}
65 changes: 61 additions & 4 deletions go-runtime/schema/call/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import (
"go/types"
"strings"

"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/TBD54566975/golang-tools/go/analysis/passes/inspect"
"github.com/TBD54566975/golang-tools/go/ast/inspector"
)

const (
ftlCallFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Call"
ftlPkgPath = "github.com/TBD54566975/ftl/go-runtime/ftl"
ftlTopicHandleTypeName = "TopicHandle"
)
Expand All @@ -23,19 +26,73 @@ 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.FuncDecl)(nil),
(*ast.CallExpr)(nil),
}
var currentFunc *ast.FuncDecl
in.Preorder(nodeFilter, func(n ast.Node) {
node := n.(*ast.CallExpr) //nolint:forcetypeassert
validateCallExpr(pass, node)
switch node := n.(type) {
case *ast.FuncDecl:
currentFunc = node
case *ast.CallExpr:
validateCallExpr(pass, node)
if currentFunc == nil {
return
}
parentFuncObj, ok := common.GetObjectForNode(pass.TypesInfo, currentFunc).Get()
if !ok {
return
}
_, fn := common.Deref[*types.Func](pass, node.Fun)
if fn == nil {
return
}
if fn.FullName() == ftlCallFuncPath {
extractVerbCall(pass, parentFuncObj, node)
return
}
common.MarkFunctionCall(pass, parentFuncObj, fn)
}
})
return common.NewExtractorResult(pass), nil
}

func extractVerbCall(pass *analysis.Pass, parentFuncObj types.Object, node *ast.CallExpr) {
if len(node.Args) != 3 {
common.Errorf(pass, node, "call must have exactly three arguments")
return
}
ref := parseVerbRef(pass, node.Args[1])
if ref == nil {
if sel, ok := node.Args[1].(*ast.SelectorExpr); ok {
common.Errorf(pass, node.Args[1], "call first argument must be a function but is an unresolved "+
"reference to %s.%s, does it need to be exported?", sel.X, sel.Sel)
}
common.Errorf(pass, node.Args[1], "call first argument must be a function in an ftl module, does "+
"it need to be exported?")
return
}
common.MarkVerbCall(pass, parentFuncObj, ref)
}

func parseVerbRef(pass *analysis.Pass, node ast.Expr) *schema.Ref {
_, verbFn := common.Deref[*types.Func](pass, node)
if verbFn == nil {
return nil
}
moduleName, err := common.FtlModuleFromGoPackage(verbFn.Pkg().Path())
if err != nil {
return nil
}
return &schema.Ref{
Pos: common.GoPosToSchemaPos(pass.Fset, node.Pos()),
Module: moduleName,
Name: strcase.ToLowerCamel(verbFn.Name()),
}
}

// validateCallExpr validates all function calls
// checks if the function call is:
// - a direct verb call to an external module
Expand Down
46 changes: 44 additions & 2 deletions go-runtime/schema/common/fact.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ type ExternalType struct{}

func (*ExternalType) schemaFactValue() {}

// FunctionCall is a fact for marking an outbound function call on a function.
type FunctionCall struct {
// The function being called.
Callee types.Object
}

func (*FunctionCall) schemaFactValue() {}

// VerbCall is a fact for marking a call to an FTL verb on a function.
type VerbCall struct {
// The verb being called.
VerbRef *schema.Ref
}

func (*VerbCall) 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)
Expand Down Expand Up @@ -148,6 +164,20 @@ func MarkMaybeTypeEnum(pass *analysis.Pass, obj types.Object, enum *schema.Enum)
pass.ExportObjectFact(obj, fact)
}

// MarkFunctionCall marks the given object as having an outbound function call.
func MarkFunctionCall(pass *analysis.Pass, obj types.Object, callee types.Object) {
fact := newFact(pass, obj)
fact.Add(&FunctionCall{Callee: callee})
pass.ExportObjectFact(obj, fact)
}

// MarkVerbCall marks the given object as having a call to an FTL verb.
func MarkVerbCall(pass *analysis.Pass, obj types.Object, verbRef *schema.Ref) {
fact := newFact(pass, obj)
fact.Add(&VerbCall{VerbRef: verbRef})
pass.ExportObjectFact(obj, fact)
}

// GetAllFactsExtractionStatus merges schema facts inclusive of all available results and the present pass facts.
// For a given object, it provides the current extraction status.
//
Expand Down Expand Up @@ -197,9 +227,9 @@ func GetAllFactsExtractionStatus(pass *analysis.Pass) map[types.Object]SchemaFac
return facts
}

// GetAllFacts returns all facts of the provided type marked on objects, across the current pass and results from
// GetAllFactsOfType returns all facts of the provided type marked on objects, across the current pass and results from
// prior passes. If multiple of the same fact type are marked on a single object, the first fact is returned.
func GetAllFacts[T SchemaFactValue](pass *analysis.Pass) map[types.Object]T {
func GetAllFactsOfType[T SchemaFactValue](pass *analysis.Pass) map[types.Object]T {
return getFactsScoped[T](allFacts(pass))
}

Expand Down Expand Up @@ -265,6 +295,18 @@ func GetFactsForObject[T SchemaFactValue](pass *analysis.Pass, obj types.Object)
return facts
}

func GetAllFacts(pass *analysis.Pass) map[types.Object][]SchemaFactValue {
facts := make(map[types.Object][]SchemaFactValue)
for _, fact := range allFacts(pass) {
sf, ok := fact.Fact.(SchemaFact)
if !ok {
continue
}
facts[fact.Object] = sf.Get()
}
return facts
}

func allFacts(pass *analysis.Pass) []analysis.ObjectFact {
var all []analysis.ObjectFact
all = append(all, pass.AllObjectFacts()...)
Expand Down
6 changes: 3 additions & 3 deletions go-runtime/schema/enum/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func Extract(pass *analysis.Pass, node *ast.TypeSpec, obj types.Object) optional

func findValueEnumVariants(pass *analysis.Pass, obj types.Object) []*schema.EnumVariant {
var variants []*schema.EnumVariant
for o, fact := range common.GetAllFacts[*common.MaybeValueEnumVariant](pass) {
for o, fact := range common.GetAllFactsOfType[*common.MaybeValueEnumVariant](pass) {
if o.Type() == obj.Type() && validateVariant(pass, o, fact.Variant) {
variants = append(variants, fact.Variant)
}
Expand All @@ -79,7 +79,7 @@ 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) {
for _, fact := range common.GetAllFactsOfType[*common.ExtractedDecl](pass) {
if fact.Decl == nil {
continue
}
Expand All @@ -100,7 +100,7 @@ func validateVariant(pass *analysis.Pass, obj types.Object, variant *schema.Enum

func findTypeValueVariants(pass *analysis.Pass, obj types.Object) []*schema.EnumVariant {
var variants []*schema.EnumVariant
for vObj, fact := range common.GetAllFacts[*common.MaybeTypeEnumVariant](pass) {
for vObj, fact := range common.GetAllFactsOfType[*common.MaybeTypeEnumVariant](pass) {
if fact.Parent != obj {
continue
}
Expand Down
Loading
Loading