From f52cae1694f31a7d7f9464eab40e4ce7fac8dbc5 Mon Sep 17 00:00:00 2001 From: Guillaume Ballet Date: Wed, 18 Dec 2019 19:46:29 +0100 Subject: [PATCH] console: apply changes from #19761 Completion tests still seem to fail --- console/bridge.go | 1 - console/console.go | 81 ++++++++------ console/console_test.go | 2 +- internal/jsre/completion.go | 17 +-- internal/jsre/completion_test.go | 4 +- internal/jsre/jsre.go | 125 +++++++++++----------- internal/jsre/jsre_test.go | 36 +++---- internal/jsre/pretty.go | 175 +++++++++++++++++++------------ 8 files changed, 246 insertions(+), 195 deletions(-) diff --git a/console/bridge.go b/console/bridge.go index 0d4670e756..0382a7311f 100644 --- a/console/bridge.go +++ b/console/bridge.go @@ -27,7 +27,6 @@ import ( "github.com/dop251/goja" "github.com/ethereum/go-ethereum/accounts/scwallet" "github.com/ethereum/go-ethereum/accounts/usbwallet" - "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rpc" ) diff --git a/console/console.go b/console/console.go index 5326ed2c87..bf8d2667d5 100644 --- a/console/console.go +++ b/console/console.go @@ -28,12 +28,12 @@ import ( "strings" "syscall" + "github.com/dop251/goja" "github.com/ethereum/go-ethereum/internal/jsre" "github.com/ethereum/go-ethereum/internal/web3ext" "github.com/ethereum/go-ethereum/rpc" "github.com/mattn/go-colorable" "github.com/peterh/liner" - "github.com/robertkrimen/otto" ) var ( @@ -64,13 +64,14 @@ type Config struct { // JavaScript console attached to a running node via an external or in-process RPC // client. type Console struct { - client *rpc.Client // RPC client to execute Ethereum requests through - jsre *jsre.JSRE // JavaScript runtime environment running the interpreter - prompt string // Input prompt prefix string - prompter UserPrompter // Input prompter to allow interactive user feedback - histPath string // Absolute path to the console scrollback history - history []string // Scroll history maintained by the console - printer io.Writer // Output writer to serialize any display strings to + client *rpc.Client // RPC client to execute Ethereum requests through + jsre *jsre.JSRE // JavaScript runtime environment running the interpreter + prompt string // Input prompt prefix string + prompter UserPrompter // Input prompter to allow interactive user feedback + histPath string // Absolute path to the console scrollback history + history []string // Scroll history maintained by the console + printer io.Writer // Output writer to serialize any display strings to + runtime *goja.Runtime // The javascript runtime } // New initializes a JavaScript interpreted runtime environment and sets defaults @@ -86,10 +87,15 @@ func New(config Config) (*Console, error) { if config.Printer == nil { config.Printer = colorable.NewColorableStdout() } + + // Create the JS runtime + runtime := goja.New() + // Initialize the console and return console := &Console{ + runtime: runtime, client: config.Client, - jsre: jsre.New(config.DocRoot, config.Printer), + jsre: jsre.New(config.DocRoot, config.Printer, runtime), prompt: config.Prompt, prompter: config.Prompter, printer: config.Printer, @@ -108,22 +114,31 @@ func New(config Config) (*Console, error) { // the console's JavaScript namespaces based on the exposed modules. func (c *Console) init(preload []string) error { // Initialize the JavaScript <-> Go RPC bridge - bridge := newBridge(c.client, c.prompter, c.printer) - c.jsre.Set("jeth", struct{}{}) + bridge := newBridge(c.client, c.prompter, c.printer, c.runtime) + c.jsre.Run("jeth = {};") + c.jsre.Run("console = {};") - jethObj, _ := c.jsre.Get("jeth") - jethObj.Object().Set("send", bridge.Send) - jethObj.Object().Set("sendAsync", bridge.Send) + jethObj := c.jsre.Get("jeth").ToObject(c.runtime) + if err := jethObj.Set("send", bridge.Send); err != nil { + panic(err) + } + if err := jethObj.Set("sendAsync", bridge.Send); err != nil { + panic(err) + } - consoleObj, _ := c.jsre.Get("console") - consoleObj.Object().Set("log", c.consoleOutput) - consoleObj.Object().Set("error", c.consoleOutput) + consoleObj := c.runtime.Get("console").ToObject(c.runtime) + if err := consoleObj.Set("log", c.consoleOutput); err != nil { + panic(err) + } + if err := consoleObj.Set("error", c.consoleOutput); err != nil { + panic(err) + } // Load all the internal utility JavaScript libraries - if err := c.jsre.Compile("bignumber.js", jsre.BignumberJs); err != nil { + if err := c.jsre.Compile("bignumber.js", string(jsre.BignumberJs)); err != nil { return fmt.Errorf("bignumber.js: %v", err) } - if err := c.jsre.Compile("web3.js", jsre.Web3Js); err != nil { + if err := c.jsre.Compile("web3.js", string(jsre.Web3Js)); err != nil { return fmt.Errorf("web3.js: %v", err) } if _, err := c.jsre.Run("var Web3 = require('web3');"); err != nil { @@ -148,7 +163,7 @@ func (c *Console) init(preload []string) error { return fmt.Errorf("%s.js: %v", api, err) } flatten += fmt.Sprintf("var %s = web3.%s; ", api, api) - } else if obj, err := c.jsre.Run("web3." + api); err == nil && obj.IsObject() { + } else if obj, err := c.jsre.Run("web3." + api); err == nil && obj.ToObject(c.runtime) != nil { // Enable web3.js built-in extension if available. flatten += fmt.Sprintf("var %s = web3.%s; ", api, api) } @@ -162,16 +177,16 @@ func (c *Console) init(preload []string) error { // If the console is in interactive mode, instrument password related methods to query the user if c.prompter != nil { // Retrieve the account management object to instrument - personal, err := c.jsre.Get("personal") - if err != nil { - return err + personal := c.jsre.Get("personal") + if personal == nil { + return fmt.Errorf("Could not find personal") } // Override the openWallet, unlockAccount, newAccount and sign methods since // these require user interaction. Assign these method in the Console the // original web3 callbacks. These will be called by the jeth.* methods after // they got the password from the user and send the original web3 request to // the backend. - if obj := personal.Object(); obj != nil { // make sure the personal api is enabled over the interface + if obj := personal.ToObject(c.runtime); obj != nil { // make sure the personal api is enabled over the interface if _, err = c.jsre.Run(`jeth.openWallet = personal.openWallet;`); err != nil { return fmt.Errorf("personal.openWallet: %v", err) } @@ -191,11 +206,11 @@ func (c *Console) init(preload []string) error { } } // The admin.sleep and admin.sleepBlocks are offered by the console and not by the RPC layer. - admin, err := c.jsre.Get("admin") - if err != nil { - return err + admin := c.jsre.Get("admin") + if admin == nil { + return fmt.Errorf("Could not find admin") } - if obj := admin.Object(); obj != nil { // make sure the admin api is enabled over the interface + if obj := admin.ToObject(c.runtime); obj != nil { // make sure the admin api is enabled over the interface obj.Set("sleepBlocks", bridge.SleepBlocks) obj.Set("sleep", bridge.Sleep) obj.Set("clearHistory", c.clearHistory) @@ -204,8 +219,8 @@ func (c *Console) init(preload []string) error { for _, path := range preload { if err := c.jsre.Exec(path); err != nil { failure := err.Error() - if ottoErr, ok := err.(*otto.Error); ok { - failure = ottoErr.String() + if gojaErr, ok := err.(*goja.Exception); ok { + failure = gojaErr.String() } return fmt.Errorf("%s: %v", path, failure) } @@ -235,13 +250,13 @@ func (c *Console) clearHistory() { // consoleOutput is an override for the console.log and console.error methods to // stream the output into the configured output stream instead of stdout. -func (c *Console) consoleOutput(call otto.FunctionCall) otto.Value { +func (c *Console) consoleOutput(call goja.FunctionCall) goja.Value { var output []string - for _, argument := range call.ArgumentList { + for _, argument := range call.Arguments { output = append(output, fmt.Sprintf("%v", argument)) } fmt.Fprintln(c.printer, strings.Join(output, " ")) - return otto.Value{} + return goja.Null() } // AutoCompleteInput is a pre-assembled word completer to be used by the user diff --git a/console/console_test.go b/console/console_test.go index 89dd7cd838..9a2b474442 100644 --- a/console/console_test.go +++ b/console/console_test.go @@ -289,7 +289,7 @@ func TestPrettyError(t *testing.T) { defer tester.Close(t) tester.console.Evaluate("throw 'hello'") - want := jsre.ErrorColor("hello") + "\n" + want := jsre.ErrorColor("hello") + "\n\tat :1:7(1)\n\n" if output := tester.output.String(); output != want { t.Fatalf("pretty error mismatch: have %s, want %s", output, want) } diff --git a/internal/jsre/completion.go b/internal/jsre/completion.go index 7f484bbbb8..5514a8ce68 100644 --- a/internal/jsre/completion.go +++ b/internal/jsre/completion.go @@ -20,29 +20,32 @@ import ( "sort" "strings" - "github.com/robertkrimen/otto" + "github.com/dop251/goja" ) // CompleteKeywords returns potential continuations for the given line. Since line is // evaluated, callers need to make sure that evaluating line does not have side effects. func (jsre *JSRE) CompleteKeywords(line string) []string { var results []string - jsre.Do(func(vm *otto.Otto) { + jsre.Do(func(vm *goja.Runtime) { results = getCompletions(vm, line) }) return results } -func getCompletions(vm *otto.Otto, line string) (results []string) { +func getCompletions(vm *goja.Runtime, line string) (results []string) { parts := strings.Split(line, ".") objRef := "this" prefix := line + var obj *goja.Object if len(parts) > 1 { objRef = strings.Join(parts[0:len(parts)-1], ".") prefix = parts[len(parts)-1] + obj, _ = vm.Get(objRef).(*goja.Object) + } else { + obj = vm.GlobalObject() } - obj, _ := vm.Object(objRef) if obj == nil { return nil } @@ -59,9 +62,11 @@ func getCompletions(vm *otto.Otto, line string) (results []string) { // Append opening parenthesis (for functions) or dot (for objects) // if the line itself is the only completion. if len(results) == 1 && results[0] == line { - obj, _ := vm.Object(line) + /* XXX Get will return `nil` et j'avais suppose que ca lancerait + une exception donc je dois tout revoir */ + obj := vm.Get(line) if obj != nil { - if obj.Class() == "Function" { + if _, isfunc := goja.AssertFunction(obj); isfunc { results[0] += "(" } else { results[0] += "." diff --git a/internal/jsre/completion_test.go b/internal/jsre/completion_test.go index ccbd73dccc..80adea5303 100644 --- a/internal/jsre/completion_test.go +++ b/internal/jsre/completion_test.go @@ -20,10 +20,12 @@ import ( "os" "reflect" "testing" + + "github.com/dop251/goja" ) func TestCompleteKeywords(t *testing.T) { - re := New("", os.Stdout) + re := New("", os.Stdout, goja.New()) re.Run(` function theClass() { this.foo = 3; diff --git a/internal/jsre/jsre.go b/internal/jsre/jsre.go index 1b3528a036..f6d3bc77f6 100644 --- a/internal/jsre/jsre.go +++ b/internal/jsre/jsre.go @@ -26,9 +26,9 @@ import ( "math/rand" "time" + "github.com/dop251/goja" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/internal/jsre/deps" - "github.com/robertkrimen/otto" ) var ( @@ -37,7 +37,7 @@ var ( ) /* -JSRE is a generic JS runtime environment embedding the otto JS interpreter. +JSRE is a generic JS runtime environment embedding the goja JS interpreter. It provides some helper functions to - load code from files - run code snippets @@ -50,6 +50,7 @@ type JSRE struct { evalQueue chan *evalReq stopEventLoop chan bool closed chan struct{} + vm *goja.Runtime } // jsTimer is a single timer instance with a callback function @@ -57,23 +58,24 @@ type jsTimer struct { timer *time.Timer duration time.Duration interval bool - call otto.FunctionCall + call goja.FunctionCall } // evalReq is a serialized vm execution request processed by runEventLoop. type evalReq struct { - fn func(vm *otto.Otto) + fn func(vm *goja.Runtime) done chan bool } // runtime must be stopped with Stop() after use and cannot be used after stopping -func New(assetPath string, output io.Writer) *JSRE { +func New(assetPath string, output io.Writer, vm *goja.Runtime) *JSRE { re := &JSRE{ assetPath: assetPath, output: output, closed: make(chan struct{}), evalQueue: make(chan *evalReq), stopEventLoop: make(chan bool), + vm: vm, } go re.runEventLoop() re.Set("loadScript", re.loadScript) @@ -99,21 +101,20 @@ func randomSource() *rand.Rand { // serialized way and calls timer callback functions at the appropriate time. // Exported functions always access the vm through the event queue. You can -// call the functions of the otto vm directly to circumvent the queue. These +// call the functions of the goja vm directly to circumvent the queue. These // functions should be used if and only if running a routine that was already // called from JS through an RPC call. func (re *JSRE) runEventLoop() { defer close(re.closed) - vm := otto.New() r := randomSource() - vm.SetRandomSource(r.Float64) + re.vm.SetRandSource(r.Float64) registry := map[*jsTimer]*jsTimer{} ready := make(chan *jsTimer) - newTimer := func(call otto.FunctionCall, interval bool) (*jsTimer, otto.Value) { - delay, _ := call.Argument(1).ToInteger() + newTimer := func(call goja.FunctionCall, interval bool) (*jsTimer, goja.Value) { + delay := call.Argument(1).ToInteger() if 0 >= delay { delay = 1 } @@ -128,47 +129,43 @@ func (re *JSRE) runEventLoop() { ready <- timer }) - value, err := call.Otto.ToValue(timer) - if err != nil { - panic(err) - } - return timer, value + return timer, re.vm.ToValue(timer) } - setTimeout := func(call otto.FunctionCall) otto.Value { + setTimeout := func(call goja.FunctionCall) goja.Value { _, value := newTimer(call, false) return value } - setInterval := func(call otto.FunctionCall) otto.Value { + setInterval := func(call goja.FunctionCall) goja.Value { _, value := newTimer(call, true) return value } - clearTimeout := func(call otto.FunctionCall) otto.Value { - timer, _ := call.Argument(0).Export() + clearTimeout := func(call goja.FunctionCall) goja.Value { + timer := call.Argument(0).Export() if timer, ok := timer.(*jsTimer); ok { timer.timer.Stop() delete(registry, timer) } - return otto.UndefinedValue() + return goja.Undefined() } - vm.Set("_setTimeout", setTimeout) - vm.Set("_setInterval", setInterval) - vm.Run(`var setTimeout = function(args) { + re.vm.Set("_setTimeout", setTimeout) + re.vm.Set("_setInterval", setInterval) + re.vm.RunString(`var setTimeout = function(args) { if (arguments.length < 1) { throw TypeError("Failed to execute 'setTimeout': 1 argument required, but only 0 present."); } return _setTimeout.apply(this, arguments); }`) - vm.Run(`var setInterval = function(args) { + re.vm.RunString(`var setInterval = function(args) { if (arguments.length < 1) { throw TypeError("Failed to execute 'setInterval': 1 argument required, but only 0 present."); } return _setInterval.apply(this, arguments); }`) - vm.Set("clearTimeout", clearTimeout) - vm.Set("clearInterval", clearTimeout) + re.vm.Set("clearTimeout", clearTimeout) + re.vm.Set("clearInterval", clearTimeout) var waitForCallbacks bool @@ -178,8 +175,8 @@ loop: case timer := <-ready: // execute callback, remove/reschedule the timer var arguments []interface{} - if len(timer.call.ArgumentList) > 2 { - tmp := timer.call.ArgumentList[2:] + if len(timer.call.Arguments) > 2 { + tmp := timer.call.Arguments[2:] arguments = make([]interface{}, 2+len(tmp)) for i, value := range tmp { arguments[i+2] = value @@ -187,11 +184,12 @@ loop: } else { arguments = make([]interface{}, 1) } - arguments[0] = timer.call.ArgumentList[0] - _, err := vm.Call(`Function.call.call`, nil, arguments...) - if err != nil { - fmt.Println("js error:", err, arguments) + arguments[0] = timer.call.Arguments[0] + call, isFunc := goja.AssertFunction(timer.call.Arguments[0]) + if !isFunc { + panic(re.vm.ToValue("js error: timer/timeout callback is not a function")) } + call(goja.Null(), timer.call.Arguments...) _, inreg := registry[timer] // when clearInterval is called from within the callback don't reset it if timer.interval && inreg { @@ -204,7 +202,7 @@ loop: } case req := <-re.evalQueue: // run the code, send the result back - req.fn(vm) + req.fn(re.vm) close(req.done) if waitForCallbacks && (len(registry) == 0) { break loop @@ -223,7 +221,7 @@ loop: } // Do executes the given function on the JS event loop. -func (re *JSRE) Do(fn func(*otto.Otto)) { +func (re *JSRE) Do(fn func(*goja.Runtime)) { done := make(chan bool) req := &evalReq{fn, done} re.evalQueue <- req @@ -246,13 +244,13 @@ func (re *JSRE) Exec(file string) error { if err != nil { return err } - var script *otto.Script - re.Do(func(vm *otto.Otto) { - script, err = vm.Compile(file, code) + var script *goja.Program + re.Do(func(vm *goja.Runtime) { + script, err = goja.Compile(file, string(code), false) if err != nil { return } - _, err = vm.Run(script) + _, err = vm.RunProgram(script) }) return err } @@ -264,43 +262,38 @@ func (re *JSRE) Bind(name string, v interface{}) error { } // Run runs a piece of JS code. -func (re *JSRE) Run(code string) (v otto.Value, err error) { - re.Do(func(vm *otto.Otto) { v, err = vm.Run(code) }) +func (re *JSRE) Run(code string) (v goja.Value, err error) { + re.Do(func(vm *goja.Runtime) { v, err = vm.RunString(code) }) return v, err } // Get returns the value of a variable in the JS environment. -func (re *JSRE) Get(ns string) (v otto.Value, err error) { - re.Do(func(vm *otto.Otto) { v, err = vm.Get(ns) }) - return v, err +func (re *JSRE) Get(ns string) (v goja.Value) { + re.Do(func(vm *goja.Runtime) { v = vm.Get(ns) }) + return v } // Set assigns value v to a variable in the JS environment. func (re *JSRE) Set(ns string, v interface{}) (err error) { - re.Do(func(vm *otto.Otto) { err = vm.Set(ns, v) }) + re.Do(func(vm *goja.Runtime) { vm.Set(ns, v) }) return err } // loadScript executes a JS script from inside the currently executing JS code. -func (re *JSRE) loadScript(call otto.FunctionCall) otto.Value { - file, err := call.Argument(0).ToString() - if err != nil { - // TODO: throw exception - return otto.FalseValue() - } +func (re *JSRE) loadScript(call goja.FunctionCall) goja.Value { + file := call.Argument(0).ToString().String() file = common.AbsolutePath(re.assetPath, file) source, err := ioutil.ReadFile(file) if err != nil { - // TODO: throw exception - return otto.FalseValue() + // Panicking with a goja.Value arg will cause a JS exception + // in the caller. + panic(re.vm.ToValue(fmt.Sprintf("Could not read file %s: %v", file, err))) } - if _, err := compileAndRun(call.Otto, file, source); err != nil { - // TODO: throw exception - fmt.Println("err:", err) - return otto.FalseValue() + value, err := compileAndRun(re.vm, file, string(source)) + if err != nil { + panic(re.vm.ToValue(fmt.Sprintf("Error while compiling or running script: %v", err))) } - // TODO: return evaluation result - return otto.TrueValue() + return value } // Evaluate executes code and pretty prints the result to the specified output @@ -308,8 +301,8 @@ func (re *JSRE) loadScript(call otto.FunctionCall) otto.Value { func (re *JSRE) Evaluate(code string, w io.Writer) error { var fail error - re.Do(func(vm *otto.Otto) { - val, err := vm.Run(code) + re.Do(func(vm *goja.Runtime) { + val, err := vm.RunString(code) if err != nil { prettyError(vm, err, w) } else { @@ -321,15 +314,15 @@ func (re *JSRE) Evaluate(code string, w io.Writer) error { } // Compile compiles and then runs a piece of JS code. -func (re *JSRE) Compile(filename string, src interface{}) (err error) { - re.Do(func(vm *otto.Otto) { _, err = compileAndRun(vm, filename, src) }) +func (re *JSRE) Compile(filename string, src string) (err error) { + re.Do(func(vm *goja.Runtime) { _, err = compileAndRun(vm, filename, src) }) return err } -func compileAndRun(vm *otto.Otto, filename string, src interface{}) (otto.Value, error) { - script, err := vm.Compile(filename, src) +func compileAndRun(vm *goja.Runtime, filename string, src string) (goja.Value, error) { + script, err := goja.Compile(filename, src, false) if err != nil { - return otto.Value{}, err + return goja.Null(), err } - return vm.Run(script) + return vm.RunProgram(script) } diff --git a/internal/jsre/jsre_test.go b/internal/jsre/jsre_test.go index bcb6e0dd23..0722d3640b 100644 --- a/internal/jsre/jsre_test.go +++ b/internal/jsre/jsre_test.go @@ -20,25 +20,24 @@ import ( "io/ioutil" "os" "path" + "reflect" "testing" "time" - "github.com/robertkrimen/otto" + "github.com/dop251/goja" ) -type testNativeObjectBinding struct{} +type testNativeObjectBinding struct { + vm *goja.Runtime +} type msg struct { Msg string } -func (no *testNativeObjectBinding) TestMethod(call otto.FunctionCall) otto.Value { - m, err := call.Argument(0).ToString() - if err != nil { - return otto.UndefinedValue() - } - v, _ := call.Otto.ToValue(&msg{m}) - return v +func (no *testNativeObjectBinding) TestMethod(call goja.FunctionCall) goja.Value { + m := call.Argument(0).ToString().String() + return no.vm.ToValue(&msg{m}) } func newWithTestJS(t *testing.T, testjs string) (*JSRE, string) { @@ -51,7 +50,8 @@ func newWithTestJS(t *testing.T, testjs string) (*JSRE, string) { t.Fatal("cannot create test.js:", err) } } - return New(dir, os.Stdout), dir + jsre := New(dir, os.Stdout, goja.New()) + return jsre, dir } func TestExec(t *testing.T) { @@ -66,11 +66,11 @@ func TestExec(t *testing.T) { if err != nil { t.Errorf("expected no error, got %v", err) } - if !val.IsString() { + if val.ExportType().Kind() != reflect.String { t.Errorf("expected string value, got %v", val) } exp := "testMsg" - got, _ := val.ToString() + got := val.ToString().String() if exp != got { t.Errorf("expected '%v', got '%v'", exp, got) } @@ -90,11 +90,11 @@ func TestNatto(t *testing.T) { if err != nil { t.Errorf("expected no error, got %v", err) } - if !val.IsString() { + if val.ExportType().Kind() != reflect.String { t.Errorf("expected string value, got %v", val) } exp := "testMsg" - got, _ := val.ToString() + got := val.ToString().String() if exp != got { t.Errorf("expected '%v', got '%v'", exp, got) } @@ -102,10 +102,10 @@ func TestNatto(t *testing.T) { } func TestBind(t *testing.T) { - jsre := New("", os.Stdout) + jsre := New("", os.Stdout, goja.New()) defer jsre.Stop(false) - jsre.Bind("no", &testNativeObjectBinding{}) + jsre.Bind("no", &testNativeObjectBinding{vm: jsre.vm}) _, err := jsre.Run(`no.TestMethod("testMsg")`) if err != nil { @@ -125,11 +125,11 @@ func TestLoadScript(t *testing.T) { if err != nil { t.Errorf("expected no error, got %v", err) } - if !val.IsString() { + if val.ExportType().Kind() != reflect.String { t.Errorf("expected string value, got %v", val) } exp := "testMsg" - got, _ := val.ToString() + got := val.ToString().String() if exp != got { t.Errorf("expected '%v', got '%v'", exp, got) } diff --git a/internal/jsre/pretty.go b/internal/jsre/pretty.go index 16fa91b67d..57bfe03bc1 100644 --- a/internal/jsre/pretty.go +++ b/internal/jsre/pretty.go @@ -19,12 +19,13 @@ package jsre import ( "fmt" "io" + "reflect" "sort" "strconv" "strings" + "github.com/dop251/goja" "github.com/fatih/color" - "github.com/robertkrimen/otto" ) const ( @@ -52,29 +53,29 @@ var boringKeys = map[string]bool{ } // prettyPrint writes value to standard output. -func prettyPrint(vm *otto.Otto, value otto.Value, w io.Writer) { +func prettyPrint(vm *goja.Runtime, value goja.Value, w io.Writer) { ppctx{vm: vm, w: w}.printValue(value, 0, false) } // prettyError writes err to standard output. -func prettyError(vm *otto.Otto, err error, w io.Writer) { +func prettyError(vm *goja.Runtime, err error, w io.Writer) { failure := err.Error() - if ottoErr, ok := err.(*otto.Error); ok { - failure = ottoErr.String() + if gojaErr, ok := err.(*goja.Exception); ok { + failure = gojaErr.String() } fmt.Fprint(w, ErrorColor("%s", failure)) } -func (re *JSRE) prettyPrintJS(call otto.FunctionCall) otto.Value { - for _, v := range call.ArgumentList { - prettyPrint(call.Otto, v, re.output) +func (re *JSRE) prettyPrintJS(call goja.FunctionCall) goja.Value { + for _, v := range call.Arguments { + prettyPrint(re.vm, v, re.output) fmt.Fprintln(re.output) } - return otto.UndefinedValue() + return goja.Undefined() } type ppctx struct { - vm *otto.Otto + vm *goja.Runtime w io.Writer } @@ -82,35 +83,54 @@ func (ctx ppctx) indent(level int) string { return strings.Repeat(indentString, level) } -func (ctx ppctx) printValue(v otto.Value, level int, inArray bool) { +func (ctx ppctx) printValue(v goja.Value, level int, inArray bool) { switch { - case v.IsObject(): - ctx.printObject(v.Object(), level, inArray) - case v.IsNull(): + case goja.IsNull(v): fmt.Fprint(ctx.w, SpecialColor("null")) - case v.IsUndefined(): + case goja.IsUndefined(v): fmt.Fprint(ctx.w, SpecialColor("undefined")) - case v.IsString(): - s, _ := v.ToString() - fmt.Fprint(ctx.w, StringColor("%q", s)) - case v.IsBoolean(): - b, _ := v.ToBoolean() - fmt.Fprint(ctx.w, SpecialColor("%t", b)) - case v.IsNaN(): + case goja.IsNaN(v): fmt.Fprint(ctx.w, NumberColor("NaN")) - case v.IsNumber(): - s, _ := v.ToString() - fmt.Fprint(ctx.w, NumberColor("%s", s)) default: - fmt.Fprint(ctx.w, "") + switch v.ExportType().Kind() { + case reflect.String: + s := v.ToString().String() + fmt.Fprint(ctx.w, StringColor("%q", s)) + case reflect.Bool: + b := v.ToBoolean() + fmt.Fprint(ctx.w, SpecialColor("%t", b)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s := v.ToString().String() + fmt.Fprint(ctx.w, NumberColor("%s", s)) + default: + if obj, ok := v.(*goja.Object); ok { + ctx.printObject(obj, level, inArray) + } else { + fmt.Fprint(ctx.w, "") + } + } } } -func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) { - switch obj.Class() { +// SafeGet attempt to get the value associated to `key`, and +// catches the panic that goja creates if an error occurs in +// key. +func SafeGet(obj *goja.Object, key string) (ret goja.Value) { + defer func() { + if r := recover(); r != nil { + ret = goja.Undefined() + } + }() + ret = obj.Get(key) + + return ret +} + +func (ctx ppctx) printObject(obj *goja.Object, level int, inArray bool) { + switch obj.ClassName() { case "Array", "GoArray": - lv, _ := obj.Get("length") - len, _ := lv.ToInteger() + lv := obj.Get("length") + len := lv.ToInteger() if len == 0 { fmt.Fprintf(ctx.w, "[]") return @@ -121,8 +141,8 @@ func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) { } fmt.Fprint(ctx.w, "[") for i := int64(0); i < len; i++ { - el, err := obj.Get(strconv.FormatInt(i, 10)) - if err == nil { + el := obj.Get(strconv.FormatInt(i, 10)) + if el != nil { ctx.printValue(el, level+1, true) } if i < len-1 { @@ -149,7 +169,7 @@ func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) { } fmt.Fprintln(ctx.w, "{") for i, k := range keys { - v, _ := obj.Get(k) + v := SafeGet(obj, k) fmt.Fprintf(ctx.w, "%s%s: ", ctx.indent(level+1), k) ctx.printValue(v, level+1, false) if i < len(keys)-1 { @@ -163,29 +183,25 @@ func (ctx ppctx) printObject(obj *otto.Object, level int, inArray bool) { fmt.Fprintf(ctx.w, "%s}", ctx.indent(level)) case "Function": - // Use toString() to display the argument list if possible. - if robj, err := obj.Call("toString"); err != nil { - fmt.Fprint(ctx.w, FunctionColor("function()")) - } else { - desc := strings.Trim(strings.Split(robj.String(), "{")[0], " \t\n") - desc = strings.Replace(desc, " (", "(", 1) - fmt.Fprint(ctx.w, FunctionColor("%s", desc)) - } + robj := obj.ToString() + desc := strings.Trim(strings.Split(robj.String(), "{")[0], " \t\n") + desc = strings.Replace(desc, " (", "(", 1) + fmt.Fprint(ctx.w, FunctionColor("%s", desc)) case "RegExp": fmt.Fprint(ctx.w, StringColor("%s", toString(obj))) default: - if v, _ := obj.Get("toString"); v.IsFunction() && level <= maxPrettyPrintLevel { - s, _ := obj.Call("toString") - fmt.Fprintf(ctx.w, "<%s %s>", obj.Class(), s.String()) + if level <= maxPrettyPrintLevel { + s := obj.ToString().String() + fmt.Fprintf(ctx.w, "<%s %s>", obj.ClassName(), s) } else { - fmt.Fprintf(ctx.w, "<%s>", obj.Class()) + fmt.Fprintf(ctx.w, "<%s>", obj.ClassName()) } } } -func (ctx ppctx) fields(obj *otto.Object) []string { +func (ctx ppctx) fields(obj *goja.Object) []string { var ( vals, methods []string seen = make(map[string]bool) @@ -195,11 +211,22 @@ func (ctx ppctx) fields(obj *otto.Object) []string { return } seen[k] = true - if v, _ := obj.Get(k); v.IsFunction() { - methods = append(methods, k) - } else { + + key := SafeGet(obj, k) + if key == nil { + // The value corresponding to that key could not be found + // (typically because it is backed by an RPC call that is + // not supported by this instance. Add it to the list of + // values so that it appears as `undefined` to the user. vals = append(vals, k) + } else { + if _, callable := goja.AssertFunction(key); callable { + methods = append(methods, k) + } else { + vals = append(vals, k) + } } + } iterOwnAndConstructorKeys(ctx.vm, obj, add) sort.Strings(vals) @@ -207,13 +234,13 @@ func (ctx ppctx) fields(obj *otto.Object) []string { return append(vals, methods...) } -func iterOwnAndConstructorKeys(vm *otto.Otto, obj *otto.Object, f func(string)) { +func iterOwnAndConstructorKeys(vm *goja.Runtime, obj *goja.Object, f func(string)) { seen := make(map[string]bool) iterOwnKeys(vm, obj, func(prop string) { seen[prop] = true f(prop) }) - if cp := constructorPrototype(obj); cp != nil { + if cp := constructorPrototype(vm, obj); cp != nil { iterOwnKeys(vm, cp, func(prop string) { if !seen[prop] { f(prop) @@ -222,10 +249,17 @@ func iterOwnAndConstructorKeys(vm *otto.Otto, obj *otto.Object, f func(string)) } } -func iterOwnKeys(vm *otto.Otto, obj *otto.Object, f func(string)) { - Object, _ := vm.Object("Object") - rv, _ := Object.Call("getOwnPropertyNames", obj.Value()) - gv, _ := rv.Export() +func iterOwnKeys(vm *goja.Runtime, obj *goja.Object, f func(string)) { + Object := vm.Get("Object").ToObject(vm) + getOwnPropertyNames, isFunc := goja.AssertFunction(Object.Get("getOwnPropertyNames")) + if !isFunc { + panic(vm.ToValue("Object.getOwnPropertyNames isn't a function")) + } + rv, err := getOwnPropertyNames(goja.Null(), obj) + if err != nil { + panic(vm.ToValue(fmt.Sprintf("Error getting object properties: %v", err))) + } + gv := rv.Export() switch gv := gv.(type) { case []interface{}: for _, v := range gv { @@ -240,32 +274,35 @@ func iterOwnKeys(vm *otto.Otto, obj *otto.Object, f func(string)) { } } -func (ctx ppctx) isBigNumber(v *otto.Object) bool { +func (ctx ppctx) isBigNumber(v *goja.Object) bool { // Handle numbers with custom constructor. - if v, _ := v.Get("constructor"); v.Object() != nil { - if strings.HasPrefix(toString(v.Object()), "function BigNumber") { + if obj := v.Get("constructor").ToObject(ctx.vm); obj != nil { + if strings.HasPrefix(toString(obj), "function BigNumber") { return true } } // Handle default constructor. - BigNumber, _ := ctx.vm.Object("BigNumber.prototype") + BigNumber := ctx.vm.Get("BigNumber").ToObject(ctx.vm) if BigNumber == nil { return false } - bv, _ := BigNumber.Call("isPrototypeOf", v) - b, _ := bv.ToBoolean() - return b + prototype := BigNumber.Get("prototype").ToObject(ctx.vm) + isPrototypeOf, callable := goja.AssertFunction(prototype.Get("isPrototypeOf")) + if !callable { + return false + } + bv, _ := isPrototypeOf(prototype, v) + return bv.ToBoolean() } -func toString(obj *otto.Object) string { - s, _ := obj.Call("toString") - return s.String() +func toString(obj *goja.Object) string { + return obj.ToString().String() } -func constructorPrototype(obj *otto.Object) *otto.Object { - if v, _ := obj.Get("constructor"); v.Object() != nil { - if v, _ = v.Object().Get("prototype"); v.Object() != nil { - return v.Object() +func constructorPrototype(vm *goja.Runtime, obj *goja.Object) *goja.Object { + if v := obj.Get("constructor"); v != nil { + if v := v.ToObject(vm).Get("prototype"); v != nil { + return v.ToObject(vm) } } return nil