github.com/neilgarb/delve@v1.9.2-nobreaks/_scripts/gen-starlark-bindings.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"go/format"
     7  	"go/token"
     8  	"go/types"
     9  	"io/ioutil"
    10  	"log"
    11  	"os"
    12  	"strings"
    13  	"unicode"
    14  
    15  	"golang.org/x/tools/go/packages"
    16  )
    17  
    18  // getSuitableMethods returns the list of methods of service/rpc2.RPCServer that are exported as API calls
    19  func getSuitableMethods(pkg *types.Package, typename string) []*types.Func {
    20  	r := []*types.Func{}
    21  	mset := types.NewMethodSet(types.NewPointer(pkg.Scope().Lookup(typename).Type()))
    22  	for i := 0; i < mset.Len(); i++ {
    23  		fn := mset.At(i).Obj().(*types.Func)
    24  
    25  		if !fn.Exported() {
    26  			continue
    27  		}
    28  
    29  		if fn.Name() == "Command" || fn.Name() == "Restart" || fn.Name() == "State" {
    30  			r = append(r, fn)
    31  			continue
    32  		}
    33  
    34  		sig, ok := fn.Type().(*types.Signature)
    35  		if !ok {
    36  			continue
    37  		}
    38  
    39  		// arguments must be (args, *reply)
    40  		if sig.Params().Len() != 2 {
    41  			continue
    42  		}
    43  		if ntyp, isname := sig.Params().At(0).Type().(*types.Named); !isname {
    44  			continue
    45  		} else if _, isstr := ntyp.Underlying().(*types.Struct); !isstr {
    46  			continue
    47  		}
    48  		if _, isptr := sig.Params().At(1).Type().(*types.Pointer); !isptr {
    49  			continue
    50  		}
    51  
    52  		// return values must be (error)
    53  		if sig.Results().Len() != 1 {
    54  			continue
    55  		}
    56  		if sig.Results().At(0).Type().String() != "error" {
    57  			continue
    58  		}
    59  
    60  		r = append(r, fn)
    61  	}
    62  	return r
    63  }
    64  
    65  func fieldsOfStruct(typ types.Type) (fieldNames, fieldTypes []string) {
    66  	styp := typ.(*types.Named).Underlying().(*types.Struct)
    67  	for i := 0; i < styp.NumFields(); i++ {
    68  		fieldNames = append(fieldNames, styp.Field(i).Name())
    69  		fieldTypes = append(fieldTypes, styp.Field(i).Type().String())
    70  	}
    71  	return fieldNames, fieldTypes
    72  }
    73  
    74  func camelToDash(in string) string {
    75  	out := []rune{}
    76  	for i, ch := range in {
    77  		isupper := func(i int) bool {
    78  			ch := in[i]
    79  			return ch >= 'A' && ch <= 'Z'
    80  		}
    81  
    82  		if i > 0 && isupper(i) {
    83  			if !isupper(i - 1) {
    84  				out = append(out, '_')
    85  			} else if i+1 < len(in) && !isupper(i+1) {
    86  				out = append(out, '_')
    87  			}
    88  		}
    89  		out = append(out, unicode.ToLower(ch))
    90  	}
    91  	return string(out)
    92  }
    93  
    94  type binding struct {
    95  	name string
    96  	fn   *types.Func
    97  
    98  	argType, retType string
    99  
   100  	argNames []string
   101  	argTypes []string
   102  }
   103  
   104  func processServerMethods(serverMethods []*types.Func) []binding {
   105  	bindings := make([]binding, len(serverMethods))
   106  	for i, fn := range serverMethods {
   107  		sig, _ := fn.Type().(*types.Signature)
   108  		argNames, argTypes := fieldsOfStruct(sig.Params().At(0).Type())
   109  
   110  		name := camelToDash(fn.Name())
   111  
   112  		switch name {
   113  		case "set":
   114  			// avoid collision with builtin that already exists in starlark
   115  			name = "set_expr"
   116  		case "command":
   117  			name = "raw_command"
   118  		default:
   119  			// remove list_ prefix, it looks better
   120  			const listPrefix = "list_"
   121  			if strings.HasPrefix(name, listPrefix) {
   122  				name = name[len(listPrefix):]
   123  			}
   124  		}
   125  
   126  		retType := sig.Params().At(1).Type().String()
   127  		switch fn.Name() {
   128  		case "Command":
   129  			retType = "rpc2.CommandOut"
   130  		case "Restart":
   131  			retType = "rpc2.RestartOut"
   132  		case "State":
   133  			retType = "rpc2.StateOut"
   134  		}
   135  
   136  		bindings[i] = binding{
   137  			name:     name,
   138  			fn:       fn,
   139  			argType:  sig.Params().At(0).Type().String(),
   140  			retType:  retType,
   141  			argNames: argNames,
   142  			argTypes: argTypes,
   143  		}
   144  	}
   145  	return bindings
   146  }
   147  
   148  func removePackagePath(typePath string) string {
   149  	lastSlash := strings.LastIndex(typePath, "/")
   150  	if lastSlash < 0 {
   151  		return typePath
   152  	}
   153  	return typePath[lastSlash+1:]
   154  }
   155  
   156  func genMapping(bindings []binding) []byte {
   157  	buf := bytes.NewBuffer([]byte{})
   158  
   159  	fmt.Fprintf(buf, "// DO NOT EDIT: auto-generated using _scripts/gen-starlark-bindings.go\n\n")
   160  	fmt.Fprintf(buf, "package starbind\n\n")
   161  	fmt.Fprintf(buf, "import ( \"go.starlark.net/starlark\" \n \"github.com/go-delve/delve/service/api\" \n \"github.com/go-delve/delve/service/rpc2\" \n \"fmt\" )\n\n")
   162  	fmt.Fprintf(buf, "func (env *Env) starlarkPredeclare() starlark.StringDict {\n")
   163  	fmt.Fprintf(buf, "r := starlark.StringDict{}\n\n")
   164  
   165  	for _, binding := range bindings {
   166  		fmt.Fprintf(buf, "r[%q] = starlark.NewBuiltin(%q, func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {", binding.name, binding.name)
   167  		fmt.Fprintf(buf, "if err := isCancelled(thread); err != nil { return starlark.None, decorateError(thread, err) }\n")
   168  		fmt.Fprintf(buf, "var rpcArgs %s\n", removePackagePath(binding.argType))
   169  		fmt.Fprintf(buf, "var rpcRet %s\n", removePackagePath(binding.retType))
   170  
   171  		// unmarshal normal unnamed arguments
   172  		for i := range binding.argNames {
   173  			fmt.Fprintf(buf, "if len(args) > %d && args[%d] != starlark.None { err := unmarshalStarlarkValue(args[%d], &rpcArgs.%s, %q); if err != nil { return starlark.None, decorateError(thread, err) } }", i, i, i, binding.argNames[i], binding.argNames[i])
   174  
   175  			switch binding.argTypes[i] {
   176  			case "*github.com/go-delve/delve/service/api.LoadConfig":
   177  				if binding.fn.Name() != "Stacktrace" {
   178  					fmt.Fprintf(buf, "else { cfg := env.ctx.LoadConfig(); rpcArgs.%s = &cfg }", binding.argNames[i])
   179  				}
   180  			case "github.com/go-delve/delve/service/api.LoadConfig":
   181  				fmt.Fprintf(buf, "else { rpcArgs.%s = env.ctx.LoadConfig() }", binding.argNames[i])
   182  			case "*github.com/go-delve/delve/service/api.EvalScope":
   183  				fmt.Fprintf(buf, "else { scope := env.ctx.Scope(); rpcArgs.%s = &scope }", binding.argNames[i])
   184  			case "github.com/go-delve/delve/service/api.EvalScope":
   185  				fmt.Fprintf(buf, "else { rpcArgs.%s = env.ctx.Scope() }", binding.argNames[i])
   186  			}
   187  
   188  			fmt.Fprintf(buf, "\n")
   189  
   190  		}
   191  
   192  		// unmarshal keyword arguments
   193  		if len(binding.argNames) > 0 {
   194  			fmt.Fprintf(buf, "for _, kv := range kwargs {\n")
   195  			fmt.Fprintf(buf, "var err error\n")
   196  			fmt.Fprintf(buf, "switch kv[0].(starlark.String) {\n")
   197  			for i := range binding.argNames {
   198  				fmt.Fprintf(buf, "case %q: ", binding.argNames[i])
   199  				fmt.Fprintf(buf, "err = unmarshalStarlarkValue(kv[1], &rpcArgs.%s, %q)\n", binding.argNames[i], binding.argNames[i])
   200  			}
   201  			fmt.Fprintf(buf, "default: err = fmt.Errorf(\"unknown argument %%q\", kv[0])")
   202  			fmt.Fprintf(buf, "}\n")
   203  			fmt.Fprintf(buf, "if err != nil { return starlark.None, decorateError(thread, err) }\n")
   204  			fmt.Fprintf(buf, "}\n")
   205  		}
   206  
   207  		fmt.Fprintf(buf, "err := env.ctx.Client().CallAPI(%q, &rpcArgs, &rpcRet)\n", binding.fn.Name())
   208  		fmt.Fprintf(buf, "if err != nil { return starlark.None, err }\n")
   209  		fmt.Fprintf(buf, "return env.interfaceToStarlarkValue(rpcRet), nil\n")
   210  
   211  		fmt.Fprintf(buf, "})\n")
   212  	}
   213  
   214  	fmt.Fprintf(buf, "return r\n")
   215  	fmt.Fprintf(buf, "}\n")
   216  
   217  	return buf.Bytes()
   218  }
   219  
   220  func genDocs(bindings []binding) []byte {
   221  	var buf bytes.Buffer
   222  
   223  	fmt.Fprintf(&buf, "Function | API Call\n")
   224  	fmt.Fprintf(&buf, "---------|---------\n")
   225  
   226  	for _, binding := range bindings {
   227  		argNames := strings.Join(binding.argNames, ", ")
   228  		fmt.Fprintf(&buf, "%s(%s) | Equivalent to API call [%s](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.%s)\n", binding.name, argNames, binding.fn.Name(), binding.fn.Name())
   229  	}
   230  
   231  	fmt.Fprintf(&buf, "dlv_command(command) | Executes the specified command as if typed at the dlv_prompt\n")
   232  	fmt.Fprintf(&buf, "read_file(path) | Reads the file as a string\n")
   233  	fmt.Fprintf(&buf, "write_file(path, contents) | Writes string to a file\n")
   234  	fmt.Fprintf(&buf, "cur_scope() | Returns the current evaluation scope\n")
   235  	fmt.Fprintf(&buf, "default_load_config() | Returns the current default load configuration\n")
   236  
   237  	return buf.Bytes()
   238  }
   239  
   240  const (
   241  	startOfMappingTable = "<!-- BEGIN MAPPING TABLE -->"
   242  	endOfMappingTable   = "<!-- END MAPPING TABLE -->"
   243  )
   244  
   245  func spliceDocs(docpath string, docs []byte, outpath string) {
   246  	docbuf, err := ioutil.ReadFile(docpath)
   247  	if err != nil {
   248  		log.Fatalf("could not read doc file: %v", err)
   249  	}
   250  
   251  	v := strings.Split(string(docbuf), startOfMappingTable)
   252  	if len(v) != 2 {
   253  		log.Fatal("could not find start of mapping table")
   254  	}
   255  	header := v[0]
   256  	v = strings.Split(v[1], endOfMappingTable)
   257  	if len(v) != 2 {
   258  		log.Fatal("could not find end of mapping table")
   259  	}
   260  	footer := v[1]
   261  
   262  	outbuf := make([]byte, 0, len(header)+len(docs)+len(footer)+len(startOfMappingTable)+len(endOfMappingTable)+1)
   263  	outbuf = append(outbuf, []byte(header)...)
   264  	outbuf = append(outbuf, []byte(startOfMappingTable)...)
   265  	outbuf = append(outbuf, '\n')
   266  	outbuf = append(outbuf, docs...)
   267  	outbuf = append(outbuf, []byte(endOfMappingTable)...)
   268  	outbuf = append(outbuf, []byte(footer)...)
   269  
   270  	if outpath != "-" {
   271  		err = ioutil.WriteFile(outpath, outbuf, 0664)
   272  		if err != nil {
   273  			log.Fatalf("could not write documentation file: %v", err)
   274  		}
   275  	} else {
   276  		os.Stdout.Write(outbuf)
   277  	}
   278  }
   279  
   280  func usage() {
   281  	fmt.Fprintf(os.Stderr, "gen-starlark-bindings [doc|doc/dummy|go] <destination file>\n\n")
   282  	fmt.Fprintf(os.Stderr, "Writes starlark documentation (doc) or mapping file (go) to <destination file>. Specify doc/dummy to generated documentation without overwriting the destination file.\n")
   283  	os.Exit(1)
   284  }
   285  
   286  func main() {
   287  	if len(os.Args) != 3 {
   288  		usage()
   289  	}
   290  	kind := os.Args[1]
   291  	path := os.Args[2]
   292  
   293  	fset := &token.FileSet{}
   294  	cfg := &packages.Config{
   295  		Mode: packages.LoadSyntax,
   296  		Fset: fset,
   297  	}
   298  	pkgs, err := packages.Load(cfg, "github.com/go-delve/delve/service/rpc2")
   299  	if err != nil {
   300  		log.Fatalf("could not load packages: %v", err)
   301  	}
   302  
   303  	var serverMethods []*types.Func
   304  	packages.Visit(pkgs, func(pkg *packages.Package) bool {
   305  		if pkg.PkgPath == "github.com/go-delve/delve/service/rpc2" {
   306  			serverMethods = getSuitableMethods(pkg.Types, "RPCServer")
   307  		}
   308  		return true
   309  	}, nil)
   310  
   311  	bindings := processServerMethods(serverMethods)
   312  
   313  	switch kind {
   314  	case "go":
   315  		mapping := genMapping(bindings)
   316  
   317  		outfh := os.Stdout
   318  		if path != "-" {
   319  			outfh, err = os.Create(path)
   320  			if err != nil {
   321  				log.Fatalf("could not create output file: %v", err)
   322  			}
   323  			defer outfh.Close()
   324  		}
   325  
   326  		src, err := format.Source(mapping)
   327  		if err != nil {
   328  			fmt.Fprintf(os.Stderr, "%s", string(mapping))
   329  			log.Fatal(err)
   330  		}
   331  		outfh.Write(src)
   332  
   333  	case "doc":
   334  		docs := genDocs(bindings)
   335  		spliceDocs(path, docs, path)
   336  
   337  	case "doc/dummy":
   338  		docs := genDocs(bindings)
   339  		spliceDocs(path, docs, "-")
   340  
   341  	default:
   342  		usage()
   343  	}
   344  }