Skip to content

Commit

Permalink
feat: migrate configs and secrets to new extractor
Browse files Browse the repository at this point in the history
  • Loading branch information
worstell committed Jul 12, 2024
1 parent 6508d57 commit 34a97db
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 159 deletions.
154 changes: 0 additions & 154 deletions go-runtime/compile/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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-- {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()})
Expand Down
8 changes: 4 additions & 4 deletions go-runtime/compile/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -555,9 +555,9 @@ func TestErrorReporting(t *testing.T) {
`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"`,
`13:13-34: expected string literal for argument at index 0`,
`16:5-5: duplicate config declaration for "failing.FTL_ENDPOINT"; already declared at "15:5"`,
`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`,
`19:5-5: duplicate secret declaration for "failing.FTL_ENDPOINT"; already declared at "18:5"`,
`21:6-6: multiple Go type mappings found for "ftl/failing/child.MultipleMappings"`,
`22:14-44: duplicate database declaration at 21:14-44`,
`25:2-10: unsupported type "error" for field "BadParam"`,
Expand Down Expand Up @@ -593,7 +593,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`,
Expand All @@ -603,7 +603,7 @@ 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" in "ftl/failing"; already declared in "ftl/failing/child"`,
}
assert.Equal(t, expected, actual)
}
Expand Down
88 changes: 88 additions & 0 deletions go-runtime/schema/call/analyzer.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
50 changes: 49 additions & 1 deletion go-runtime/schema/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"go/token"
"go/types"
"reflect"
"strconv"
"strings"

"github.com/alecthomas/types/optional"
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 34a97db

Please sign in to comment.