delve/_scripts/gen-starlark-bindings.go
Alessandro Arzilli 698f838616
terminal/starbind: allow modification of structs returned by API (#3872)
Structs returned to starlark scripts by API calls were immutable, this
made amend_breakpoint nearly impossible to use since its argument must
be a api.Breakpoint struct which the caller has received from
get_breakpoint and modified.
2024-12-04 19:09:59 -08:00

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()
}
}