mirror of
				https://github.com/go-delve/delve.git
				synced 2025-10-31 18:57:18 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			389 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			389 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package main
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"go/ast"
 | |
| 	"go/format"
 | |
| 	"go/token"
 | |
| 	"go/types"
 | |
| 	"log"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 	"unicode"
 | |
| 
 | |
| 	"golang.org/x/tools/go/packages"
 | |
| )
 | |
| 
 | |
| // getSuitableMethods returns the list of methods of service/rpc2.RPCServer that are exported as API calls
 | |
| func getSuitableMethods(pkg *types.Package, typename string) []*types.Func {
 | |
| 	r := []*types.Func{}
 | |
| 	mset := types.NewMethodSet(types.NewPointer(pkg.Scope().Lookup(typename).Type()))
 | |
| 	for i := 0; i < mset.Len(); i++ {
 | |
| 		fn := mset.At(i).Obj().(*types.Func)
 | |
| 
 | |
| 		if !fn.Exported() {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if fn.Name() == "Command" || fn.Name() == "Restart" || fn.Name() == "State" {
 | |
| 			r = append(r, fn)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		sig, ok := fn.Type().(*types.Signature)
 | |
| 		if !ok {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// arguments must be (args, *reply)
 | |
| 		if sig.Params().Len() != 2 {
 | |
| 			continue
 | |
| 		}
 | |
| 		if ntyp, isname := sig.Params().At(0).Type().(*types.Named); !isname {
 | |
| 			continue
 | |
| 		} else if _, isstr := ntyp.Underlying().(*types.Struct); !isstr {
 | |
| 			continue
 | |
| 		}
 | |
| 		if _, isptr := sig.Params().At(1).Type().(*types.Pointer); !isptr {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		// return values must be (error)
 | |
| 		if sig.Results().Len() != 1 {
 | |
| 			continue
 | |
| 		}
 | |
| 		if sig.Results().At(0).Type().String() != "error" {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		r = append(r, fn)
 | |
| 	}
 | |
| 	return r
 | |
| }
 | |
| 
 | |
| func fieldsOfStruct(typ types.Type) (fieldNames, fieldTypes []string) {
 | |
| 	styp := typ.(*types.Named).Underlying().(*types.Struct)
 | |
| 	for i := 0; i < styp.NumFields(); i++ {
 | |
| 		fieldNames = append(fieldNames, styp.Field(i).Name())
 | |
| 		fieldTypes = append(fieldTypes, styp.Field(i).Type().String())
 | |
| 	}
 | |
| 	return fieldNames, fieldTypes
 | |
| }
 | |
| 
 | |
| func camelToDash(in string) string {
 | |
| 	out := []rune{}
 | |
| 	for i, ch := range in {
 | |
| 		isupper := func(i int) bool {
 | |
| 			ch := in[i]
 | |
| 			return ch >= 'A' && ch <= 'Z'
 | |
| 		}
 | |
| 
 | |
| 		if i > 0 && isupper(i) {
 | |
| 			if !isupper(i - 1) {
 | |
| 				out = append(out, '_')
 | |
| 			} else if i+1 < len(in) && !isupper(i+1) {
 | |
| 				out = append(out, '_')
 | |
| 			}
 | |
| 		}
 | |
| 		out = append(out, unicode.ToLower(ch))
 | |
| 	}
 | |
| 	return string(out)
 | |
| }
 | |
| 
 | |
| type binding struct {
 | |
| 	name string
 | |
| 	fn   *types.Func
 | |
| 
 | |
| 	argType, retType string
 | |
| 
 | |
| 	argNames []string
 | |
| 	argTypes []string
 | |
| 
 | |
| 	docStr string
 | |
| }
 | |
| 
 | |
| func processServerMethods(serverMethods []*types.Func, funcDeclByPos map[token.Pos]*ast.FuncDecl) []binding {
 | |
| 	bindings := make([]binding, len(serverMethods))
 | |
| 	for i, fn := range serverMethods {
 | |
| 		sig, _ := fn.Type().(*types.Signature)
 | |
| 		argNames, argTypes := fieldsOfStruct(sig.Params().At(0).Type())
 | |
| 
 | |
| 		name := camelToDash(fn.Name())
 | |
| 
 | |
| 		switch name {
 | |
| 		case "set":
 | |
| 			// avoid collision with builtin that already exists in starlark
 | |
| 			name = "set_expr"
 | |
| 		case "command":
 | |
| 			name = "raw_command"
 | |
| 		default:
 | |
| 			// remove list_ prefix, it looks better
 | |
| 			const listPrefix = "list_"
 | |
| 			if strings.HasPrefix(name, listPrefix) {
 | |
| 				name = name[len(listPrefix):]
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		retType := sig.Params().At(1).Type().String()
 | |
| 		switch fn.Name() {
 | |
| 		case "Command":
 | |
| 			retType = "rpc2.CommandOut"
 | |
| 		case "Restart":
 | |
| 			retType = "rpc2.RestartOut"
 | |
| 		case "State":
 | |
| 			retType = "rpc2.StateOut"
 | |
| 		}
 | |
| 
 | |
| 		docStr := ""
 | |
| 
 | |
| 		if decl := funcDeclByPos[fn.Pos()]; decl != nil && decl.Doc != nil {
 | |
| 			docs := []string{}
 | |
| 			for _, cmnt := range decl.Doc.List {
 | |
| 				docs = append(docs, strings.TrimPrefix(strings.TrimPrefix(cmnt.Text, "//"), " "))
 | |
| 			}
 | |
| 
 | |
| 			// fix name of the function in the first line of the documentation
 | |
| 			if fields := strings.SplitN(docs[0], " ", 2); len(fields) == 2 && fields[0] == fn.Name() {
 | |
| 				docs[0] = name + " " + fields[1]
 | |
| 			}
 | |
| 
 | |
| 			docStr = strings.Join(docs, "\n")
 | |
| 		}
 | |
| 
 | |
| 		bindings[i] = binding{
 | |
| 			name:     name,
 | |
| 			fn:       fn,
 | |
| 			argType:  sig.Params().At(0).Type().String(),
 | |
| 			retType:  retType,
 | |
| 			argNames: argNames,
 | |
| 			argTypes: argTypes,
 | |
| 			docStr:   docStr,
 | |
| 		}
 | |
| 	}
 | |
| 	return bindings
 | |
| }
 | |
| 
 | |
| func removePackagePath(typePath string) string {
 | |
| 	lastSlash := strings.LastIndex(typePath, "/")
 | |
| 	if lastSlash < 0 {
 | |
| 		return typePath
 | |
| 	}
 | |
| 	return typePath[lastSlash+1:]
 | |
| }
 | |
| 
 | |
| func genMapping(bindings []binding) []byte {
 | |
| 	buf := bytes.NewBuffer([]byte{})
 | |
| 
 | |
| 	fmt.Fprintf(buf, "// DO NOT EDIT: auto-generated using _scripts/gen-starlark-bindings.go\n\n")
 | |
| 	fmt.Fprintf(buf, "package starbind\n\n")
 | |
| 	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")
 | |
| 	fmt.Fprintf(buf, "func (env *Env) starlarkPredeclare() (starlark.StringDict, map[string]string) {\n")
 | |
| 	fmt.Fprintf(buf, "r := starlark.StringDict{}\n")
 | |
| 	fmt.Fprintf(buf, "doc := make(map[string]string)\n\n")
 | |
| 
 | |
| 	for _, binding := range bindings {
 | |
| 		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)
 | |
| 		fmt.Fprintf(buf, "if err := isCancelled(thread); err != nil { return starlark.None, decorateError(thread, err) }\n")
 | |
| 		fmt.Fprintf(buf, "var rpcArgs %s\n", removePackagePath(binding.argType))
 | |
| 		fmt.Fprintf(buf, "var rpcRet %s\n", removePackagePath(binding.retType))
 | |
| 
 | |
| 		// unmarshal normal unnamed arguments
 | |
| 		for i := range binding.argNames {
 | |
| 			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])
 | |
| 
 | |
| 			switch binding.argTypes[i] {
 | |
| 			case "*github.com/go-delve/delve/service/api.LoadConfig":
 | |
| 				if binding.fn.Name() != "Stacktrace" {
 | |
| 					fmt.Fprintf(buf, "else { cfg := env.ctx.LoadConfig(); rpcArgs.%s = &cfg }", binding.argNames[i])
 | |
| 				}
 | |
| 			case "github.com/go-delve/delve/service/api.LoadConfig":
 | |
| 				fmt.Fprintf(buf, "else { rpcArgs.%s = env.ctx.LoadConfig() }", binding.argNames[i])
 | |
| 			case "*github.com/go-delve/delve/service/api.EvalScope":
 | |
| 				fmt.Fprintf(buf, "else { scope := env.ctx.Scope(); rpcArgs.%s = &scope }", binding.argNames[i])
 | |
| 			case "github.com/go-delve/delve/service/api.EvalScope":
 | |
| 				fmt.Fprintf(buf, "else { rpcArgs.%s = env.ctx.Scope() }", binding.argNames[i])
 | |
| 			}
 | |
| 
 | |
| 			fmt.Fprintf(buf, "\n")
 | |
| 
 | |
| 		}
 | |
| 
 | |
| 		// unmarshal keyword arguments
 | |
| 		if len(binding.argNames) > 0 {
 | |
| 			fmt.Fprintf(buf, "for _, kv := range kwargs {\n")
 | |
| 			fmt.Fprintf(buf, "var err error\n")
 | |
| 			fmt.Fprintf(buf, "switch kv[0].(starlark.String) {\n")
 | |
| 			for i := range binding.argNames {
 | |
| 				fmt.Fprintf(buf, "case %q: ", binding.argNames[i])
 | |
| 				fmt.Fprintf(buf, "err = unmarshalStarlarkValue(kv[1], &rpcArgs.%s, %q)\n", binding.argNames[i], binding.argNames[i])
 | |
| 			}
 | |
| 			fmt.Fprintf(buf, "default: err = fmt.Errorf(\"unknown argument %%q\", kv[0])")
 | |
| 			fmt.Fprintf(buf, "}\n")
 | |
| 			fmt.Fprintf(buf, "if err != nil { return starlark.None, decorateError(thread, err) }\n")
 | |
| 			fmt.Fprintf(buf, "}\n")
 | |
| 		}
 | |
| 
 | |
| 		fmt.Fprintf(buf, "err := env.ctx.Client().CallAPI(%q, &rpcArgs, &rpcRet)\n", binding.fn.Name())
 | |
| 		fmt.Fprintf(buf, "if err != nil { return starlark.None, err }\n")
 | |
| 		fmt.Fprintf(buf, "return env.interfaceToStarlarkValue(rpcRet), nil\n")
 | |
| 
 | |
| 		fmt.Fprintf(buf, "})\n")
 | |
| 
 | |
| 		// builtin documentation
 | |
| 		docstr := new(strings.Builder)
 | |
| 		fmt.Fprintf(docstr, "builtin %s(", binding.name)
 | |
| 		for i, argname := range binding.argNames {
 | |
| 			if i != 0 {
 | |
| 				fmt.Fprintf(docstr, ", ")
 | |
| 			}
 | |
| 			fmt.Fprintf(docstr, argname)
 | |
| 		}
 | |
| 		fmt.Fprintf(docstr, ")")
 | |
| 		if binding.docStr != "" {
 | |
| 			fmt.Fprintf(docstr, "\n\n%s", binding.docStr)
 | |
| 		}
 | |
| 		fmt.Fprintf(buf, "doc[%q] = %q\n", binding.name, docstr.String())
 | |
| 	}
 | |
| 
 | |
| 	fmt.Fprintf(buf, "return r, doc\n")
 | |
| 	fmt.Fprintf(buf, "}\n")
 | |
| 
 | |
| 	return buf.Bytes()
 | |
| }
 | |
| 
 | |
| func genDocs(bindings []binding) []byte {
 | |
| 	var buf bytes.Buffer
 | |
| 
 | |
| 	fmt.Fprintf(&buf, "Function | API Call\n")
 | |
| 	fmt.Fprintf(&buf, "---------|---------\n")
 | |
| 
 | |
| 	for _, binding := range bindings {
 | |
| 		argNames := strings.Join(binding.argNames, ", ")
 | |
| 		fmt.Fprintf(&buf, "%s(%s) | Equivalent to API call [%s](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.%s)\n", binding.name, argNames, binding.fn.Name(), binding.fn.Name())
 | |
| 	}
 | |
| 
 | |
| 	fmt.Fprintf(&buf, "dlv_command(command) | Executes the specified command as if typed at the dlv_prompt\n")
 | |
| 	fmt.Fprintf(&buf, "read_file(path) | Reads the file as a string\n")
 | |
| 	fmt.Fprintf(&buf, "write_file(path, contents) | Writes string to a file\n")
 | |
| 	fmt.Fprintf(&buf, "cur_scope() | Returns the current evaluation scope\n")
 | |
| 	fmt.Fprintf(&buf, "default_load_config() | Returns the current default load configuration\n")
 | |
| 
 | |
| 	return buf.Bytes()
 | |
| }
 | |
| 
 | |
| const (
 | |
| 	startOfMappingTable = "<!-- BEGIN MAPPING TABLE -->"
 | |
| 	endOfMappingTable   = "<!-- END MAPPING TABLE -->"
 | |
| )
 | |
| 
 | |
| func spliceDocs(docpath string, docs []byte, outpath string) {
 | |
| 	docbuf, err := os.ReadFile(docpath)
 | |
| 	if err != nil {
 | |
| 		log.Fatalf("could not read doc file: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	v := strings.Split(string(docbuf), startOfMappingTable)
 | |
| 	if len(v) != 2 {
 | |
| 		log.Fatal("could not find start of mapping table")
 | |
| 	}
 | |
| 	header := v[0]
 | |
| 	v = strings.Split(v[1], endOfMappingTable)
 | |
| 	if len(v) != 2 {
 | |
| 		log.Fatal("could not find end of mapping table")
 | |
| 	}
 | |
| 	footer := v[1]
 | |
| 
 | |
| 	outbuf := make([]byte, 0, len(header)+len(docs)+len(footer)+len(startOfMappingTable)+len(endOfMappingTable)+1)
 | |
| 	outbuf = append(outbuf, []byte(header)...)
 | |
| 	outbuf = append(outbuf, []byte(startOfMappingTable)...)
 | |
| 	outbuf = append(outbuf, '\n')
 | |
| 	outbuf = append(outbuf, docs...)
 | |
| 	outbuf = append(outbuf, []byte(endOfMappingTable)...)
 | |
| 	outbuf = append(outbuf, []byte(footer)...)
 | |
| 
 | |
| 	if outpath != "-" {
 | |
| 		err = os.WriteFile(outpath, outbuf, 0o664)
 | |
| 		if err != nil {
 | |
| 			log.Fatalf("could not write documentation file: %v", err)
 | |
| 		}
 | |
| 	} else {
 | |
| 		os.Stdout.Write(outbuf)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func usage() {
 | |
| 	fmt.Fprintf(os.Stderr, "gen-starlark-bindings [doc|doc/dummy|go] <destination file>\n\n")
 | |
| 	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")
 | |
| 	os.Exit(1)
 | |
| }
 | |
| 
 | |
| func main() {
 | |
| 	if len(os.Args) != 3 {
 | |
| 		usage()
 | |
| 	}
 | |
| 	kind := os.Args[1]
 | |
| 	path := os.Args[2]
 | |
| 
 | |
| 	fset := &token.FileSet{}
 | |
| 	cfg := &packages.Config{
 | |
| 		Mode: packages.LoadSyntax,
 | |
| 		Fset: fset,
 | |
| 	}
 | |
| 	pkgs, err := packages.Load(cfg, "github.com/go-delve/delve/service/rpc2")
 | |
| 	if err != nil {
 | |
| 		log.Fatalf("could not load packages: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	var serverMethods []*types.Func
 | |
| 	funcDeclByPos := make(map[token.Pos]*ast.FuncDecl)
 | |
| 	packages.Visit(pkgs, func(pkg *packages.Package) bool {
 | |
| 		if pkg.PkgPath == "github.com/go-delve/delve/service/rpc2" {
 | |
| 			serverMethods = getSuitableMethods(pkg.Types, "RPCServer")
 | |
| 		}
 | |
| 		for _, file := range pkg.Syntax {
 | |
| 			ast.Inspect(file, func(n ast.Node) bool {
 | |
| 				if n, ok := n.(*ast.FuncDecl); ok {
 | |
| 					funcDeclByPos[n.Name.Pos()] = n
 | |
| 				}
 | |
| 				return true
 | |
| 			})
 | |
| 		}
 | |
| 		return true
 | |
| 	}, nil)
 | |
| 
 | |
| 	bindings := processServerMethods(serverMethods, funcDeclByPos)
 | |
| 
 | |
| 	switch kind {
 | |
| 	case "go":
 | |
| 		mapping := genMapping(bindings)
 | |
| 
 | |
| 		outfh := os.Stdout
 | |
| 		if path != "-" {
 | |
| 			outfh, err = os.Create(path)
 | |
| 			if err != nil {
 | |
| 				log.Fatalf("could not create output file: %v", err)
 | |
| 			}
 | |
| 			defer outfh.Close()
 | |
| 		}
 | |
| 
 | |
| 		src, err := format.Source(mapping)
 | |
| 		if err != nil {
 | |
| 			fmt.Fprintf(os.Stderr, "%s", string(mapping))
 | |
| 			log.Fatal(err)
 | |
| 		}
 | |
| 		outfh.Write(src)
 | |
| 
 | |
| 	case "doc":
 | |
| 		docs := genDocs(bindings)
 | |
| 		spliceDocs(path, docs, path)
 | |
| 
 | |
| 	case "doc/dummy":
 | |
| 		docs := genDocs(bindings)
 | |
| 		spliceDocs(path, docs, "-")
 | |
| 
 | |
| 	default:
 | |
| 		usage()
 | |
| 	}
 | |
| }
 | 
