Commit 7b68975a authored by Guillaume Ballet's avatar Guillaume Ballet Committed by Felix Lange

console, internal/jsre: use github.com/dop251/goja (#20470)

This replaces the JavaScript interpreter used by the console with goja,
which is actively maintained and a lot faster than otto. Clef still uses otto
and eth/tracers still uses duktape, so we are currently dependent on three
different JS interpreters. We're looking to replace the remaining uses of otto
soon though.
parent 60deeb10
......@@ -51,7 +51,9 @@ func TestConsoleWelcome(t *testing.T) {
geth.SetTemplateFunc("goarch", func() string { return runtime.GOARCH })
geth.SetTemplateFunc("gover", runtime.Version)
geth.SetTemplateFunc("gethver", func() string { return params.VersionWithCommit("", "") })
geth.SetTemplateFunc("niltime", func() string { return time.Unix(0, 0).Format(time.RFC1123) })
geth.SetTemplateFunc("niltime", func() string {
return time.Unix(0, 0).Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)")
})
geth.SetTemplateFunc("apis", func() string { return ipcAPIs })
// Verify the actual welcome message to the required template
......@@ -142,7 +144,9 @@ func testAttachWelcome(t *testing.T, geth *testgeth, endpoint, apis string) {
attach.SetTemplateFunc("gover", runtime.Version)
attach.SetTemplateFunc("gethver", func() string { return params.VersionWithCommit("", "") })
attach.SetTemplateFunc("etherbase", func() string { return geth.Etherbase })
attach.SetTemplateFunc("niltime", func() string { return time.Unix(0, 0).Format(time.RFC1123) })
attach.SetTemplateFunc("niltime", func() string {
return time.Unix(0, 0).Format("Mon Jan 02 2006 15:04:05 GMT-0700 (MST)")
})
attach.SetTemplateFunc("ipc", func() bool { return strings.HasPrefix(endpoint, "ipc") })
attach.SetTemplateFunc("datadir", func() string { return geth.Datadir })
attach.SetTemplateFunc("apis", func() string { return apis })
......
This diff is collapsed.
This diff is collapsed.
......@@ -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 <eval>:1:7(1)\n\n"
if output := tester.output.String(); output != want {
t.Fatalf("pretty error mismatch: have %s, want %s", output, want)
}
......
......@@ -16,13 +16,16 @@ require (
github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9
github.com/davecgh/go-spew v1.1.1
github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea
github.com/dlclark/regexp2 v1.2.0 // indirect
github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf
github.com/dop251/goja v0.0.0-20200106141417-aaec0e7bde29
github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c
github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa
github.com/fatih/color v1.3.0
github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-sourcemap/sourcemap v2.1.2+incompatible // indirect
github.com/go-stack/stack v1.8.0
github.com/golang/protobuf v1.3.2-0.20190517061210-b285ee9cfc6c
github.com/golang/snappy v0.0.1
......
......@@ -57,8 +57,14 @@ github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vs
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf h1:sh8rkQZavChcmakYiSlqu2425CHyFXLZZnvm7PDpU8M=
github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/dop251/goja v0.0.0-20191203121440-007eef3bc40f h1:vtCDQseO/Sbu5IZSoc2uzZ7CkSoai7OtpcwGFK5FlyE=
github.com/dop251/goja v0.0.0-20191203121440-007eef3bc40f/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
github.com/dop251/goja v0.0.0-20200106141417-aaec0e7bde29 h1:Ewd9K+mC725sITA12QQHRqWj78NU4t7EhlFVVgdlzJg=
github.com/dop251/goja v0.0.0-20200106141417-aaec0e7bde29/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c h1:JHHhtb9XWJrGNMcrVP6vyzO4dusgi/HnceHTgxSejUM=
github.com/edsrzf/mmap-go v0.0.0-20160512033002-935e0e8a636c/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/elastic/gosigar v0.8.1-0.20180330100440-37f05ff46ffa h1:XKAhUk/dtp+CV0VO6mhG2V7jA9vbcGcnYF/Ay9NjZrY=
......@@ -77,6 +83,8 @@ github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2i
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-sourcemap/sourcemap v2.1.2+incompatible h1:0b/xya7BKGhXuqFESKM4oIiRo9WOt2ebz7KxfreD6ug=
github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
......
......@@ -20,35 +20,43 @@ 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
if len(parts) > 1 {
objRef = strings.Join(parts[0:len(parts)-1], ".")
prefix = parts[len(parts)-1]
if len(parts) == 0 {
return nil
}
obj, _ := vm.Object(objRef)
if obj == nil {
return nil
// Find the right-most fully named object in the line. e.g. if line = "x.y.z"
// and "x.y" is an object, obj will reference "x.y".
obj := vm.GlobalObject()
for i := 0; i < len(parts)-1; i++ {
v := obj.Get(parts[i])
if v == nil {
return nil // No object was found
}
obj = v.ToObject(vm)
}
// Go over the keys of the object and retain the keys matching prefix.
// Example: if line = "x.y.z" and "x.y" exists and has keys "zebu", "zebra"
// and "platypus", then "x.y.zebu" and "x.y.zebra" will be added to results.
prefix := parts[len(parts)-1]
iterOwnAndConstructorKeys(vm, obj, func(k string) {
if strings.HasPrefix(k, prefix) {
if objRef == "this" {
if len(parts) == 1 {
results = append(results, k)
} else {
results = append(results, strings.Join(parts[:len(parts)-1], ".")+"."+k)
......@@ -59,9 +67,9 @@ 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)
obj := obj.Get(parts[len(parts)-1])
if obj != nil {
if obj.Class() == "Function" {
if _, isfunc := goja.AssertFunction(obj); isfunc {
results[0] += "("
} else {
results[0] += "."
......
......@@ -39,6 +39,10 @@ func TestCompleteKeywords(t *testing.T) {
input string
want []string
}{
{
input: "St",
want: []string{"String"},
},
{
input: "x",
want: []string{"x."},
......
This diff is collapsed.
......@@ -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)
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)
}
......@@ -105,7 +105,7 @@ func TestBind(t *testing.T) {
jsre := New("", os.Stdout)
defer jsre.Stop(false)
jsre.Bind("no", &testNativeObjectBinding{})
jsre.Set("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)
}
......
......@@ -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,47 @@ 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) {
if goja.IsNull(v) || goja.IsUndefined(v) {
fmt.Fprint(ctx.w, SpecialColor(v.String()))
return
}
kind := v.ExportType().Kind()
switch {
case v.IsObject():
ctx.printObject(v.Object(), level, inArray)
case v.IsNull():
fmt.Fprint(ctx.w, SpecialColor("null"))
case v.IsUndefined():
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():
fmt.Fprint(ctx.w, NumberColor("NaN"))
case v.IsNumber():
s, _ := v.ToString()
fmt.Fprint(ctx.w, NumberColor("%s", s))
case kind == reflect.Bool:
fmt.Fprint(ctx.w, SpecialColor("%t", v.ToBoolean()))
case kind == reflect.String:
fmt.Fprint(ctx.w, StringColor("%q", v.String()))
case kind >= reflect.Int && kind <= reflect.Complex128:
fmt.Fprint(ctx.w, NumberColor("%s", v.String()))
default:
fmt.Fprint(ctx.w, "<unprintable>")
if obj, ok := v.(*goja.Object); ok {
ctx.printObject(obj, level, inArray)
} else {
fmt.Fprintf(ctx.w, "<unprintable %T>", v)
}
}
}
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 +134,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 +162,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 +176,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 +204,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 +227,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 +242,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 +267,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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment