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  }