diff --git a/internal/lsp/command/command_gen.go b/internal/lsp/command/command_gen.go new file mode 100644 index 00000000000..d740afa36f3 --- /dev/null +++ b/internal/lsp/command/command_gen.go @@ -0,0 +1,266 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package command + +// Code generated by generate.go. DO NOT EDIT. + +import ( + "fmt" + + "golang.org/x/tools/internal/lsp/protocol" +) + +func Dispatch(params *protocol.ExecuteCommandParams, s Interface) (interface{}, error) { + switch params.Command { + case "gopls.add_dependency": + var a0 DependencyArgs + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.AddDependency(a0) + return nil, err + case "gopls.check_upgrades": + var a0 CheckUpgradesArgs + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.CheckUpgrades(a0) + return nil, err + case "gopls.generate": + var a0 GenerateArgs + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.Generate(a0) + return nil, err + case "gopls.generate_gopls_mod": + var a0 URIArg + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.GenerateGoplsMod(a0) + return nil, err + case "gopls.go_get_package": + var a0 GoGetPackageArgs + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.GoGetPackage(a0) + return nil, err + case "gopls.regenerate_cgo": + var a0 URIArg + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.RegenerateCgo(a0) + return nil, err + case "gopls.remove_dependency": + var a0 RemoveDependencyArgs + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.RemoveDependency(a0) + return nil, err + case "gopls.run_tests": + var a0 RunTestsArgs + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.RunTests(a0) + return nil, err + case "gopls.tidy": + var a0 URIArg + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.Tidy(a0) + return nil, err + case "gopls.toggle_details": + var a0 URIArg + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.ToggleDetails(a0) + return nil, err + case "gopls.update_go_sum": + var a0 URIArg + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.UpdateGoSum(a0) + return nil, err + case "gopls.upgrade_dependency": + var a0 DependencyArgs + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.UpgradeDependency(a0) + return nil, err + case "gopls.vendor": + var a0 URIArg + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + err := s.Vendor(a0) + return nil, err + } + return nil, fmt.Errorf("unsupported command %q", params.Command) +} + +func NewAddDependencyCommand(title string, a0 DependencyArgs) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.add_dependency", + Arguments: args, + }, nil +} + +func NewCheckUpgradesCommand(title string, a0 CheckUpgradesArgs) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.check_upgrades", + Arguments: args, + }, nil +} + +func NewGenerateCommand(title string, a0 GenerateArgs) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.generate", + Arguments: args, + }, nil +} + +func NewGenerateGoplsModCommand(title string, a0 URIArg) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.generate_gopls_mod", + Arguments: args, + }, nil +} + +func NewGoGetPackageCommand(title string, a0 GoGetPackageArgs) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.go_get_package", + Arguments: args, + }, nil +} + +func NewRegenerateCgoCommand(title string, a0 URIArg) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.regenerate_cgo", + Arguments: args, + }, nil +} + +func NewRemoveDependencyCommand(title string, a0 RemoveDependencyArgs) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.remove_dependency", + Arguments: args, + }, nil +} + +func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.run_tests", + Arguments: args, + }, nil +} + +func NewTidyCommand(title string, a0 URIArg) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.tidy", + Arguments: args, + }, nil +} + +func NewToggleDetailsCommand(title string, a0 URIArg) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.toggle_details", + Arguments: args, + }, nil +} + +func NewUpdateGoSumCommand(title string, a0 URIArg) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.update_go_sum", + Arguments: args, + }, nil +} + +func NewUpgradeDependencyCommand(title string, a0 DependencyArgs) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.upgrade_dependency", + Arguments: args, + }, nil +} + +func NewVendorCommand(title string, a0 URIArg) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.vendor", + Arguments: args, + }, nil +} diff --git a/internal/lsp/command/commandmeta/meta.go b/internal/lsp/command/commandmeta/meta.go new file mode 100644 index 00000000000..0859eb546a8 --- /dev/null +++ b/internal/lsp/command/commandmeta/meta.go @@ -0,0 +1,219 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package commandmeta provides metadata about LSP commands, by analyzing the +// command.Interface type. +package commandmeta + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + "reflect" + "strings" + "unicode" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/lsp/command" +) + +type Command struct { + MethodName string + Name string + // TODO(rFindley): I think Title can actually be eliminated. In all cases + // where we use it, there is probably a more appropriate contextual title. + Title string + Doc string + Args []*Field + Result types.Type +} + +type Field struct { + Name string + Doc string + JSONTag string + Type types.Type + // In some circumstances, we may want to recursively load additional field + // descriptors for fields of struct types, documenting their internals. + Fields []*Field +} + +func Load() (*packages.Package, []*Command, error) { + pkgs, err := packages.Load( + &packages.Config{ + Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedDeps, + }, + "golang.org/x/tools/internal/lsp/command", + ) + if err != nil { + return nil, nil, err + } + pkg := pkgs[0] + + // For a bit of type safety, use reflection to get the interface name within + // the package scope. + it := reflect.TypeOf((*command.Interface)(nil)).Elem() + obj := pkg.Types.Scope().Lookup(it.Name()).Type().Underlying().(*types.Interface) + + // Load command metadata corresponding to each interface method. + var commands []*Command + loader := fieldLoader{make(map[types.Object]*Field)} + for i := 0; i < obj.NumMethods(); i++ { + m := obj.Method(i) + c, err := loader.loadMethod(pkg, m) + if err != nil { + return nil, nil, fmt.Errorf("loading %s: %v", m.Name(), err) + } + commands = append(commands, c) + } + return pkg, commands, nil +} + +// fieldLoader loads field information, memoizing results to prevent infinite +// recursion. +type fieldLoader struct { + loaded map[types.Object]*Field +} + +var universeError = types.Universe.Lookup("error").Type() + +func (l *fieldLoader) loadMethod(pkg *packages.Package, m *types.Func) (*Command, error) { + node, err := findField(pkg, m.Pos()) + if err != nil { + return nil, err + } + title, doc := splitDoc(node.Doc.Text()) + c := &Command{ + MethodName: m.Name(), + Name: lspName(m.Name()), + Doc: doc, + Title: title, + } + sig := m.Type().Underlying().(*types.Signature) + rlen := sig.Results().Len() + if rlen > 2 || rlen == 0 { + return nil, fmt.Errorf("must have 1 or 2 returns, got %d", rlen) + } + finalResult := sig.Results().At(rlen - 1) + if !types.Identical(finalResult.Type(), universeError) { + return nil, fmt.Errorf("final return must be error") + } + if rlen == 2 { + c.Result = sig.Results().At(0).Type() + } + ftype := node.Type.(*ast.FuncType) + if sig.Params().Len() != ftype.Params.NumFields() { + panic("bug: mismatching method params") + } + for i, p := range ftype.Params.List { + pt := sig.Params().At(i) + fld, err := l.loadField(pkg, p, pt, "") + if err != nil { + return nil, err + } + c.Args = append(c.Args, fld) + } + return c, nil +} + +func (l *fieldLoader) loadField(pkg *packages.Package, node *ast.Field, obj *types.Var, tag string) (*Field, error) { + if existing, ok := l.loaded[obj]; ok { + return existing, nil + } + fld := &Field{ + Name: obj.Name(), + Doc: strings.TrimSpace(node.Doc.Text()), + Type: obj.Type(), + JSONTag: reflect.StructTag(tag).Get("json"), + } + under := fld.Type.Underlying() + if p, ok := under.(*types.Pointer); ok { + under = p.Elem() + } + if s, ok := under.(*types.Struct); ok { + for i := 0; i < s.NumFields(); i++ { + obj2 := s.Field(i) + pkg2 := pkg + if obj2.Pkg() != pkg2.Types { + pkg2 = pkg.Imports[obj2.Pkg().Path()] + } + node2, err := findField(pkg2, obj2.Pos()) + if err != nil { + return nil, err + } + tag := s.Tag(i) + structField, err := l.loadField(pkg2, node2, obj2, tag) + if err != nil { + return nil, err + } + fld.Fields = append(fld.Fields, structField) + } + } + return fld, nil +} + +// splitDoc parses a command doc string to separate the title from normal +// documentation. +// +// The doc comment should be of the form: "MethodName: Title\nDocumentation" +func splitDoc(text string) (title, doc string) { + docParts := strings.SplitN(doc, "\n", 2) + titleParts := strings.SplitN(docParts[0], ":", 2) + if len(titleParts) > 1 { + title = strings.TrimSpace(titleParts[1]) + } + if len(docParts) > 1 { + doc = strings.TrimSpace(docParts[1]) + } + return title, doc +} + +// lspName returns the normalized command name to use in the LSP. +func lspName(methodName string) string { + words := splitCamel(methodName) + for i := range words { + words[i] = strings.ToLower(words[i]) + } + return "gopls." + strings.Join(words, "_") +} + +// splitCamel splits s into words, according to camel-case word boundaries. +func splitCamel(s string) []string { + var words []string + for len(s) > 0 { + last := strings.LastIndexFunc(s, unicode.IsUpper) + if last < 0 { + last = 0 + } + words = append(words, s[last:]) + s = s[:last] + } + for i := 0; i < len(words)/2; i++ { + j := len(words) - i - 1 + words[i], words[j] = words[j], words[i] + } + return words +} + +// findField finds the struct field or interface method positioned at pos, +// within the AST. +func findField(pkg *packages.Package, pos token.Pos) (*ast.Field, error) { + fset := pkg.Fset + var file *ast.File + for _, f := range pkg.Syntax { + if fset.Position(f.Pos()).Filename == fset.Position(pos).Filename { + file = f + break + } + } + if file == nil { + return nil, fmt.Errorf("no file for pos %v", pos) + } + path, _ := astutil.PathEnclosingInterval(file, pos, pos) + // This is fragile, but in the cases we care about, the field will be in + // path[1]. + return path[1].(*ast.Field), nil +} diff --git a/internal/lsp/command/generate.go b/internal/lsp/command/generate.go new file mode 100644 index 00000000000..49d72b54200 --- /dev/null +++ b/internal/lsp/command/generate.go @@ -0,0 +1,24 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build ignore + +package main + +import ( + "fmt" + "io/ioutil" + "os" + + "golang.org/x/tools/internal/lsp/command/generate" +) + +func main() { + content, err := generate.Generate() + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + ioutil.WriteFile("command_gen.go", content, 0644) +} diff --git a/internal/lsp/command/generate/generate.go b/internal/lsp/command/generate/generate.go new file mode 100644 index 00000000000..d206e22cecc --- /dev/null +++ b/internal/lsp/command/generate/generate.go @@ -0,0 +1,136 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package generate is used to generate command bindings from the gopls command +// interface. +package generate + +import ( + "bytes" + "fmt" + "go/types" + "text/template" + + "golang.org/x/tools/internal/imports" + "golang.org/x/tools/internal/lsp/command/commandmeta" +) + +const src = `// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package command + +// Code generated by generate.go. DO NOT EDIT. + +import ( + {{range $k, $v := .Imports -}} + "{{$k}}" + {{end}} +) + +func Dispatch(params *protocol.ExecuteCommandParams, s Interface) (interface{}, error) { + switch params.Command { + {{- range .Commands}} + case "{{.Name}}": + {{- if .Args -}} + {{- range $i, $v := .Args}} + var a{{$i}} {{typeString $v.Type}} + {{- end}} + if err := UnmarshalArgs(params.Arguments{{range $i, $v := .Args}}, &a{{$i}}{{end}}); err != nil { + return nil, err + } + {{end -}} + {{- if .Result -}}res, {{end}}err := s.{{.MethodName}}({{block "callargs" .}}{{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}}{{end}}{{end}}) + return {{if .Result}}res{{else}}nil{{end}}, err + {{- end}} + } + return nil, fmt.Errorf("unsupported command %q", params.Command) +} +{{- range .Commands}} + +func New{{.MethodName}}Command(title string, {{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}} {{typeString $v.Type}}{{end}}) (protocol.Command, error) { + args, err := MarshalArgs({{template "callargs" .}}) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "{{.Name}}", + Arguments: args, + }, nil +} +{{end}} +` + +type data struct { + Imports map[string]bool + Commands []*commandmeta.Command +} + +func Generate() ([]byte, error) { + pkg, cmds, err := commandmeta.Load() + if err != nil { + return nil, err + } + qf := func(p *types.Package) string { + if p == pkg.Types { + return "" + } + return p.Name() + } + tmpl, err := template.New("").Funcs(template.FuncMap{ + "typeString": func(t types.Type) string { + return types.TypeString(t, qf) + }, + }).Parse(src) + if err != nil { + return nil, err + } + d := data{ + Commands: cmds, + Imports: map[string]bool{ + "fmt": true, + "golang.org/x/tools/internal/lsp/protocol": true, + }, + } + const thispkg = "golang.org/x/tools/internal/lsp/command" + for _, c := range d.Commands { + for _, arg := range c.Args { + pth := pkgPath(arg.Type) + if pth != "" && pth != thispkg { + d.Imports[pth] = true + } + } + pth := pkgPath(c.Result) + if pth != "" && pth != thispkg { + d.Imports[pth] = true + } + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, d); err != nil { + return nil, fmt.Errorf("executing: %v", err) + } + + opts := &imports.Options{ + AllErrors: true, + FormatOnly: true, + Comments: true, + } + content, err := imports.Process("", buf.Bytes(), opts) + if err != nil { + return nil, fmt.Errorf("goimports: %v", err) + } + return content, nil +} + +func pkgPath(t types.Type) string { + if n, ok := t.(*types.Named); ok { + if pkg := n.Obj().Pkg(); pkg != nil { + return pkg.Path() + } + } + return "" +} diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go new file mode 100644 index 00000000000..7ffcd85f618 --- /dev/null +++ b/internal/lsp/command/interface.go @@ -0,0 +1,143 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package command defines the interface provided by gopls for the +// workspace/executeCommand LSP request. +// +// This interface is fully specified by the Interface type, provided it +// conforms to the restrictions outlined in its doc string. +// +// Bindings for server-side command dispatch and client-side serialization are +// also provided by this package, via code generation. +package command + +//go:generate go run generate.go + +import "golang.org/x/tools/internal/lsp/protocol" + +// Interface defines the interface gopls exposes for the +// workspace/executeCommand request. +// +// This interface is used to generate marshaling/unmarshaling code, dispatch, +// and documentation, and so has some additional restrictions: +// 1. All method arguments must be JSON serializable. +// 2. Methods must return either error or (T, error), where T is a +// JSON serializable type. +// 3. The first line of the doc string is special. Everything after the colon +// is considered the command 'Title'. +// TODO(rFindley): reconsider this -- Title may be unnecessary. +type Interface interface { + // RunTests: Run test(s) + // + // Runs `go test` for a specific set of test or benchmark functions. + RunTests(RunTestsArgs) error + + // Generate: Run go generate + // + // Runs `go generate` for a given directory. + Generate(GenerateArgs) error + + // RegenerateCgo: Regenerate cgo + // + // Regenerates cgo definitions. + RegenerateCgo(URIArg) error + + // Tidy: Run go mod tidy + // + // Runs `go mod tidy` for a module. + Tidy(URIArg) error + + // Vendor: Run go mod vendor + // + // Runs `go mod vendor` for a module. + Vendor(URIArg) error + + // UpdateGoSum: Update go.sum + // + // Updates the go.sum file for a module. + UpdateGoSum(URIArg) error + + // CheckUpgrades: Check for upgrades + // + // Checks for module upgrades. + CheckUpgrades(CheckUpgradesArgs) error + + // AddDependency: Add dependency + // + // Adds a dependency to the go.mod file for a module. + AddDependency(DependencyArgs) error + + // UpgradeDependency: Upgrade dependency + // + // Upgrades a dependency in the go.mod file for a module. + UpgradeDependency(DependencyArgs) error + + // RemoveDependency: Remove dependency + // + // Removes a dependency from the go.mod file of a module. + RemoveDependency(RemoveDependencyArgs) error + + // GoGetPackage: go get package + // + // Runs `go get` to fetch a package. + GoGetPackage(GoGetPackageArgs) error + + // ToggleDetails: Toggle gc_details + // + // Toggle the calculation of gc annotations. + ToggleDetails(URIArg) error + + // GenerateGoplsMod: Generate gopls.mod + // + // (Re)generate the gopls.mod file for a workspace. + GenerateGoplsMod(URIArg) error +} + +type RunTestsArgs struct { + // URI is the test file containing the tests to run. + URI protocol.DocumentURI + + // Tests holds specific test names to run, e.g. TestFoo. + Tests []string + + // Benchmarks holds specific benchmarks to run, e.g. BenchmarkFoo. + Benchmarks []string +} + +type GenerateArgs struct { + // URI is any file within the directory to generate. Usually this is the file + // containing the '//go:generate' directive. + URI protocol.DocumentURI + + // Recursive controls whether to generate recursively (go generate ./...) + Recursive bool +} + +// TODO(rFindley): document the rest of these once the docgen is fleshed out. + +type URIArg struct { + URI protocol.DocumentURI +} + +type CheckUpgradesArgs struct { + URI protocol.DocumentURI + Modules []string +} + +type DependencyArgs struct { + URI protocol.DocumentURI + GoCmdArgs []string + AddRequire bool +} + +type RemoveDependencyArgs struct { + URI protocol.DocumentURI + ModulePath string + OnlyDiagnostic bool +} + +type GoGetPackageArgs struct { + URI protocol.DocumentURI + Pkg string +} diff --git a/internal/lsp/command/interface_test.go b/internal/lsp/command/interface_test.go new file mode 100644 index 00000000000..d58545e0114 --- /dev/null +++ b/internal/lsp/command/interface_test.go @@ -0,0 +1,31 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package command_test + +import ( + "bytes" + "io/ioutil" + "testing" + + "golang.org/x/tools/internal/lsp/command/generate" + "golang.org/x/tools/internal/testenv" +) + +func TestGenerated(t *testing.T) { + testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. + + onDisk, err := ioutil.ReadFile("command_gen.go") + if err != nil { + t.Fatal(err) + } + + generated, err := generate.Generate() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(onDisk, generated) { + t.Error("command_gen.go is stale -- regenerate") + } +} diff --git a/internal/lsp/command/util.go b/internal/lsp/command/util.go new file mode 100644 index 00000000000..c81aaf6dbf3 --- /dev/null +++ b/internal/lsp/command/util.go @@ -0,0 +1,54 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package command + +import ( + "encoding/json" + "fmt" +) + +// MarshalArgs encodes the given arguments to json.RawMessages. This function +// is used to construct arguments to a protocol.Command. +// +// Example usage: +// +// jsonArgs, err := EncodeArgs(1, "hello", true, StructuredArg{42, 12.6}) +// +func MarshalArgs(args ...interface{}) ([]json.RawMessage, error) { + var out []json.RawMessage + for _, arg := range args { + argJSON, err := json.Marshal(arg) + if err != nil { + return nil, err + } + out = append(out, argJSON) + } + return out, nil +} + +// UnmarshalArgs decodes the given json.RawMessages to the variables provided +// by args. Each element of args should be a pointer. +// +// Example usage: +// +// var ( +// num int +// str string +// bul bool +// structured StructuredArg +// ) +// err := UnmarshalArgs(args, &num, &str, &bul, &structured) +// +func UnmarshalArgs(jsonArgs []json.RawMessage, args ...interface{}) error { + if len(args) != len(jsonArgs) { + return fmt.Errorf("DecodeArgs: expected %d input arguments, got %d JSON arguments", len(args), len(jsonArgs)) + } + for i, arg := range args { + if err := json.Unmarshal(jsonArgs[i], arg); err != nil { + return err + } + } + return nil +}