github.com/grailbio/bigslice@v0.0.0-20230519005545-30c4c12152ad/analysis/typecheck/typecheck.go (about) 1 package typecheck 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/types" 7 8 "golang.org/x/tools/go/analysis" 9 "golang.org/x/tools/go/analysis/passes/inspect" 10 "golang.org/x/tools/go/ast/inspector" 11 "golang.org/x/tools/go/types/typeutil" 12 ) 13 14 var Analyzer = &analysis.Analyzer{ 15 Name: "bigslice_typecheck", 16 Doc: `check bigslice func call arguments 17 18 Basic typechecker for bigslice programs that inspects session.Run and 19 session.Must calls to ensure the arguments are compatible with the Func. 20 Checks are limited by static analysis and are best-effort. For example, the call 21 session.Must(ctx, chooseFunc(), args...) 22 cannot be checked, because it uses chooseFunc() instead of a simple identifier. 23 24 Typechecking does not include any slice operations yet.`, 25 Requires: []*analysis.Analyzer{inspect.Analyzer}, 26 Run: run, 27 } 28 29 const ( 30 funcFullName = "github.com/grailbio/bigslice.Func" 31 execMustFullName = "(*github.com/grailbio/bigslice/exec.Session).Must" 32 execRunFullName = "(*github.com/grailbio/bigslice/exec.Session).Run" 33 ) 34 35 func run(pass *analysis.Pass) (interface{}, error) { 36 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 37 38 // funcTypes describes the types of declared bigslice.FuncValues. 39 // TODO: As stated below, we're only recording top-level func for now. 40 // If that changes, this map should be keyed by a global identifier, not *ast.Ident. 41 // TODO: We may also want to return these types as Facts to allow checking across packages. 42 funcTypes := map[string]*types.Signature{} 43 44 // Collect the types of bigslice.Funcs. 45 // TODO: ValueSpec captures top-level vars, but maybe we should include non-top-level ones, too. 46 inspect.Preorder([]ast.Node{&ast.ValueSpec{}}, func(node ast.Node) { 47 valueSpec := node.(*ast.ValueSpec) 48 for valueIdx, value := range valueSpec.Values { 49 call, ok := value.(*ast.CallExpr) 50 if !ok { 51 continue 52 } 53 fn := typeutil.StaticCallee(pass.TypesInfo, call) 54 if fn == nil { 55 continue 56 } 57 if fn.FullName() != funcFullName { 58 continue 59 } 60 if len(call.Args) != 1 { 61 panic(fmt.Errorf("unexpected arguments to bigslice.Func: %v", call.Args)) 62 } 63 implAst := call.Args[0] 64 implType := pass.TypesInfo.TypeOf(implAst) 65 implSig, ok := implType.(*types.Signature) 66 if !ok { 67 pass.ReportRangef(implAst, "argument to bigslice.Func must be a function, not %v", implType) 68 continue 69 } 70 71 var invalidParams bool 72 for i := 0; i < implSig.Params().Len(); i++ { 73 param := implSig.Params().At(i) 74 if err := checkValidFuncArg(param.Type()); err != nil { 75 pass.Reportf(param.Pos(), 76 "bigslice type error: Func argument %q [%d]: %v", param.Name(), i, err) 77 invalidParams = true 78 } 79 } 80 if invalidParams { 81 continue 82 } 83 84 funcType := pass.TypesInfo.TypeOf(call.Args[0]).Underlying().(*types.Signature) 85 funcTypes[valueSpec.Names[valueIdx].Name] = funcType 86 } 87 }) 88 89 inspect.Preorder([]ast.Node{&ast.CallExpr{}}, func(node ast.Node) { 90 call := node.(*ast.CallExpr) 91 fn := typeutil.StaticCallee(pass.TypesInfo, call) 92 if fn == nil { 93 return 94 } 95 if name := fn.FullName(); name != execRunFullName && name != execMustFullName { 96 return 97 } 98 99 funcValueIdent, ok := call.Args[1].(*ast.Ident) 100 if !ok { 101 // This function invocation is more complicated than a simple identifier. 102 // Give up on typechecking this call. 103 return 104 } 105 funcType, ok := funcTypes[funcValueIdent.Name] 106 if !ok { 107 // TODO: Propagate bigslice.Func types as Facts so we can do a better job here. 108 return 109 } 110 111 wantArgTypes := funcType.Params() 112 gotArgs := call.Args[2:] 113 if want, got := wantArgTypes.Len(), len(gotArgs); want != got { 114 pass.ReportRangef(funcValueIdent, 115 "bigslice type error: %s requires %d arguments, but got %d", 116 funcValueIdent.Name, want, got) 117 return 118 } 119 120 for i, gotArg := range gotArgs { 121 wantType := wantArgTypes.At(i).Type() 122 gotType := pass.TypesInfo.TypeOf(gotArg) 123 if !types.AssignableTo(gotType, wantType) { 124 pass.ReportRangef(gotArg, 125 "bigslice type error: func %q argument %q [%d] requires %v, but got %v", 126 funcValueIdent.Name, wantArgTypes.At(i).Name(), i, wantType, gotType) 127 } 128 } 129 }) 130 131 return nil, nil 132 } 133 134 func checkValidFuncArg(typ types.Type) error { 135 switch typ.(type) { 136 case *types.Tuple: 137 panic("Tuple not expected") 138 default: 139 // TODO: Consider investigating other types more thoroughly. 140 return nil 141 142 case *types.Chan, *types.Signature: 143 return fmt.Errorf("unsupported argument type: %s (can't be serialized)", typ.String()) 144 } 145 }