Repository: robertkrimen/otto (go)

Issue:
Object.MarshalJSON
Hello!
I think it will be nice if otto.Object satisfies the json.Marshaler interface (like github.com/dop251/goja, file value.go, line 736)

Thanks

Pull request description:
Implemented MarshalJSON for Value and Object
Value and Object now conform to the json.Marshaler interface and produce the
correct JSON when passed (directly or indirectly) to json.Marshal().

Before, both types would marshal as "{}" because they're structs with no public
fields.

This fixes some nasty problems marshaling object trees that mix Go and JS
collections. This can happen when you pass a Go collection into a JS function,
which modifies it adding JS values; and then back in Go you marshal the
collection to JSON. Before this commit the JS values would marshal to "{}".
(The new unit test TestNestedJSONMarshaling demonstrates this.)

Fixes #262

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index e9d7804..08c17b1 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -71,6 +71,15 @@ linters:
     - gocyclo
     - misspell
     - cyclop
+    - varnamelen
+    - nonamedreturns
+    - maintidx
+    - ireturn
+    - exhaustruct
+    - nosnakecase
+    - deadcode
+    - dupword
+    - gci
 
 issues:
   exclude:
diff --git a/ast/node.go b/ast/node.go
index 7e45abe..80372c3 100644
--- a/ast/node.go
+++ b/ast/node.go
@@ -1,11 +1,10 @@
 /*
 Package ast declares types representing a JavaScript AST.
 
-Warning
+# Warning
 
 The parser and AST interfaces are still works-in-progress (particularly where
 node types are concerned) and may change in the future.
-
 */
 package ast
 
diff --git a/cmpl_evaluate_statement.go b/cmpl_evaluate_statement.go
index e16c6ac..056d795 100644
--- a/cmpl_evaluate_statement.go
+++ b/cmpl_evaluate_statement.go
@@ -259,6 +259,17 @@ resultBreak:
 				break
 			}
 		}
+
+		// this is to prevent for cycles with no body from running forever
+		if len(body) == 0 && self.otto.Interrupt != nil {
+			runtime.Gosched()
+			select {
+			case value := <-self.otto.Interrupt:
+				value()
+			default:
+			}
+		}
+
 		for _, node := range body {
 			value := self.cmpl_evaluate_nodeStatement(node)
 			switch value.kind {
diff --git a/error.go b/error.go
index c8b5af4..b36ad14 100644
--- a/error.go
+++ b/error.go
@@ -100,8 +100,7 @@ type Error struct {
 
 // Error returns a description of the error
 //
-//    TypeError: 'def' is not a function
-//
+//	TypeError: 'def' is not a function
 func (err Error) Error() string {
 	return err.format()
 }
@@ -109,10 +108,9 @@ func (err Error) Error() string {
 // String returns a description of the error and a trace of where the
 // error occurred.
 //
-//    TypeError: 'def' is not a function
-//        at xyz (<anonymous>:3:9)
-//        at <anonymous>:7:1/
-//
+//	TypeError: 'def' is not a function
+//	    at xyz (<anonymous>:3:9)
+//	    at <anonymous>:7:1/
 func (err Error) String() string {
 	return err.formatWithStack()
 }
diff --git a/file/file.go b/file/file.go
index 9093206..2884e99 100644
--- a/file/file.go
+++ b/file/file.go
@@ -1,5 +1,4 @@
 // Package file encapsulates the file abstractions used by the ast & parser.
-//
 package file
 
 import (
@@ -36,7 +35,6 @@ func (self *Position) isValid() bool {
 //	line:column         A valid position without filename
 //	file                An invalid position with filename
 //	-                   An invalid position without filename
-//
 func (self *Position) String() string {
 	str := self.Filename
 	if self.isValid() {
diff --git a/go.mod b/go.mod
index 05fdf00..16df2d4 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.14
 require (
 	github.com/chzyer/logex v1.1.10 // indirect
 	github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
-	github.com/stretchr/testify v1.7.0 // indirect
+	github.com/stretchr/testify v1.7.0
 	gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375
 	gopkg.in/sourcemap.v1 v1.0.5
 )
diff --git a/go.sum b/go.sum
index 79bb53a..5bcb549 100644
--- a/go.sum
+++ b/go.sum
@@ -9,6 +9,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375 h1:hPki/oSSWOLiI9Gc9jyIoj33O3j29fUc9PlLha2yDj0=
 gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375/go.mod h1:lNEQeAhU009zbRxng+XOj5ITVgY24WcbNnQopyfKoYQ=
diff --git a/otto.go b/otto.go
index b5b528d..0fa67c3 100644
--- a/otto.go
+++ b/otto.go
@@ -3,81 +3,81 @@ Package otto is a JavaScript parser and interpreter written natively in Go.
 
 http://godoc.org/github.com/robertkrimen/otto
 
-    import (
-        "github.com/robertkrimen/otto"
-    )
+	import (
+	    "github.com/robertkrimen/otto"
+	)
 
 Run something in the VM
 
-    vm := otto.New()
-    vm.Run(`
-        abc = 2 + 2;
-    	console.log("The value of abc is " + abc); // 4
-    `)
+	vm := otto.New()
+	vm.Run(`
+	    abc = 2 + 2;
+		console.log("The value of abc is " + abc); // 4
+	`)
 
 Get a value out of the VM
 
-    value, err := vm.Get("abc")
-    	value, _ := value.ToInteger()
-    }
+	value, err := vm.Get("abc")
+		value, _ := value.ToInteger()
+	}
 
 Set a number
 
-    vm.Set("def", 11)
-    vm.Run(`
-    	console.log("The value of def is " + def);
-    	// The value of def is 11
-    `)
+	vm.Set("def", 11)
+	vm.Run(`
+		console.log("The value of def is " + def);
+		// The value of def is 11
+	`)
 
 Set a string
 
-    vm.Set("xyzzy", "Nothing happens.")
-    vm.Run(`
-    	console.log(xyzzy.length); // 16
-    `)
+	vm.Set("xyzzy", "Nothing happens.")
+	vm.Run(`
+		console.log(xyzzy.length); // 16
+	`)
 
 Get the value of an expression
 
-    value, _ = vm.Run("xyzzy.length")
-    {
-    	// value is an int64 with a value of 16
-    	value, _ := value.ToInteger()
-    }
+	value, _ = vm.Run("xyzzy.length")
+	{
+		// value is an int64 with a value of 16
+		value, _ := value.ToInteger()
+	}
 
 An error happens
 
-    value, err = vm.Run("abcdefghijlmnopqrstuvwxyz.length")
-    if err != nil {
-    	// err = ReferenceError: abcdefghijlmnopqrstuvwxyz is not defined
-    	// If there is an error, then value.IsUndefined() is true
-    	...
-    }
+	value, err = vm.Run("abcdefghijlmnopqrstuvwxyz.length")
+	if err != nil {
+		// err = ReferenceError: abcdefghijlmnopqrstuvwxyz is not defined
+		// If there is an error, then value.IsUndefined() is true
+		...
+	}
 
 Set a Go function
 
-    vm.Set("sayHello", func(call otto.FunctionCall) otto.Value {
-        fmt.Printf("Hello, %s.\n", call.Argument(0).String())
-        return otto.Value{}
-    })
+	vm.Set("sayHello", func(call otto.FunctionCall) otto.Value {
+	    fmt.Printf("Hello, %s.\n", call.Argument(0).String())
+	    return otto.Value{}
+	})
 
 Set a Go function that returns something useful
 
-    vm.Set("twoPlus", func(call otto.FunctionCall) otto.Value {
-        right, _ := call.Argument(0).ToInteger()
-        result, _ := vm.ToValue(2 + right)
-        return result
-    })
+	vm.Set("twoPlus", func(call otto.FunctionCall) otto.Value {
+	    right, _ := call.Argument(0).ToInteger()
+	    result, _ := vm.ToValue(2 + right)
+	    return result
+	})
 
 Use the functions in JavaScript
 
-    result, _ = vm.Run(`
-        sayHello("Xyzzy");      // Hello, Xyzzy.
-        sayHello();             // Hello, undefined
+	result, _ = vm.Run(`
+	    sayHello("Xyzzy");      // Hello, Xyzzy.
+	    sayHello();             // Hello, undefined
 
-        result = twoPlus(2.0); // 4
-    `)
+	    result = twoPlus(2.0); // 4
+	`)
 
-Parser
+# Parser
 
 A separate parser is available in the parser package if you're just interested in building an AST.
 
@@ -85,23 +85,23 @@ http://godoc.org/github.com/robertkrimen/otto/parser
 
 Parse and return an AST
 
-    filename := "" // A filename is optional
-    src := `
-        // Sample xyzzy example
-        (function(){
-            if (3.14159 > 0) {
-                console.log("Hello, World.");
-                return;
-            }
+	filename := "" // A filename is optional
+	src := `
+	    // Sample xyzzy example
+	    (function(){
+	        if (3.14159 > 0) {
+	            console.log("Hello, World.");
+	            return;
+	        }
 
-            var xyzzy = NaN;
-            console.log("Nothing happens.");
-            return xyzzy;
-        })();
-    `
+	        var xyzzy = NaN;
+	        console.log("Nothing happens.");
+	        return xyzzy;
+	    })();
+	`
 
-    // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList
-    program, err := parser.ParseFile(nil, filename, src, 0)
+	// Parse some JavaScript, yielding a *ast.Program and/or an ErrorList
+	program, err := parser.ParseFile(nil, filename, src, 0)
 
 otto
 
@@ -126,24 +126,24 @@ Optionally include the JavaScript utility-belt library, underscore, with this im
 
 For more information: http://github.com/robertkrimen/otto/tree/master/underscore
 
-Caveat Emptor
+# Caveat Emptor
 
 The following are some limitations with otto:
 
-    * "use strict" will parse, but does nothing.
-    * The regular expression engine (re2/regexp) is not fully compatible with the ECMA5 specification.
-    * Otto targets ES5. ES6 features (eg: Typed Arrays) are not supported.
+  - "use strict" will parse, but does nothing.
+  - The regular expression engine (re2/regexp) is not fully compatible with the ECMA5 specification.
+  - Otto targets ES5. ES6 features (eg: Typed Arrays) are not supported.
 
-Regular Expression Incompatibility
+# Regular Expression Incompatibility
 
 Go translates JavaScript-style regular expressions into something that is "regexp" compatible via `parser.TransformRegExp`.
 Unfortunately, RegExp requires backtracking for some patterns, and backtracking is not supported by the standard Go engine: https://code.google.com/p/re2/wiki/Syntax
 
 Therefore, the following syntax is incompatible:
 
-    (?=)  // Lookahead (positive), currently a parsing error
-    (?!)  // Lookahead (backhead), currently a parsing error
-    \1    // Backreference (\1, \2, \3, ...), currently a parsing error
+	(?=)  // Lookahead (positive), currently a parsing error
+	(?!)  // Lookahead (backhead), currently a parsing error
+	\1    // Backreference (\1, \2, \3, ...), currently a parsing error
 
 A brief discussion of these limitations: "Regexp (?!re)" https://groups.google.com/forum/?fromgroups=#%21topic/golang-nuts/7qgSDWPIh_E
 
@@ -152,57 +152,57 @@ More information about re2: https://code.google.com/p/re2/
 In addition to the above, re2 (Go) has a different definition for \s: [\t\n\f\r ].
 The JavaScript definition, on the other hand, also includes \v, Unicode "Separator, Space", etc.
 
-Halting Problem
+# Halting Problem
 
 If you want to stop long running executions (like third-party code), you can use the interrupt channel to do this:
 
-    package main
-
-    import (
-        "errors"
-        "fmt"
-        "os"
-        "time"
-
-        "github.com/robertkrimen/otto"
-    )
-
-    var halt = errors.New("Stahp")
-
-    func main() {
-        runUnsafe(`var abc = [];`)
-        runUnsafe(`
-        while (true) {
-            // Loop forever
-        }`)
-    }
-
-    func runUnsafe(unsafe string) {
-        start := time.Now()
-        defer func() {
-            duration := time.Since(start)
-            if caught := recover(); caught != nil {
-                if caught == halt {
-                    fmt.Fprintf(os.Stderr, "Some code took to long! Stopping after: %v\n", duration)
-                    return
-                }
-                panic(caught) // Something else happened, repanic!
-            }
-            fmt.Fprintf(os.Stderr, "Ran code successfully: %v\n", duration)
-        }()
-
-        vm := otto.New()
-        vm.Interrupt = make(chan func(), 1) // The buffer prevents blocking
-
-        go func() {
-            time.Sleep(2 * time.Second) // Stop after two seconds
-            vm.Interrupt <- func() {
-                panic(halt)
-            }
-        }()
-
-        vm.Run(unsafe) // Here be dragons (risky code)
-    }
+	package main
+
+	import (
+	    "errors"
+	    "fmt"
+	    "os"
+	    "time"
+
+	    "github.com/robertkrimen/otto"
+	)
+
+	var halt = errors.New("Stahp")
+
+	func main() {
+	    runUnsafe(`var abc = [];`)
+	    runUnsafe(`
+	    while (true) {
+	        // Loop forever
+	    }`)
+	}
+
+	func runUnsafe(unsafe string) {
+	    start := time.Now()
+	    defer func() {
+	        duration := time.Since(start)
+	        if caught := recover(); caught != nil {
+	            if caught == halt {
+	                fmt.Fprintf(os.Stderr, "Some code took to long! Stopping after: %v\n", duration)
+	                return
+	            }
+	            panic(caught) // Something else happened, repanic!
+	        }
+	        fmt.Fprintf(os.Stderr, "Ran code successfully: %v\n", duration)
+	    }()
+
+	    vm := otto.New()
+	    vm.Interrupt = make(chan func(), 1) // The buffer prevents blocking
+
+	    go func() {
+	        time.Sleep(2 * time.Second) // Stop after two seconds
+	        vm.Interrupt <- func() {
+	            panic(halt)
+	        }
+	    }()
+
+	    vm.Run(unsafe) // Here be dragons (risky code)
+	}
 
 Where is setTimeout/setInterval?
 
@@ -220,11 +220,11 @@ Here is some more discussion of the issue:
 * http://en.wikipedia.org/wiki/Reentrancy_%28computing%29
 
 * http://aaroncrane.co.uk/2009/02/perl_safe_signals/
-
 */
 package otto
 
 import (
+	"encoding/json"
 	"fmt"
 	"strings"
 
@@ -273,7 +273,6 @@ func (otto *Otto) clone() *Otto {
 // src may also be a Script.
 //
 // src may also be a Program, but if the AST has been modified, then runtime behavior is undefined.
-//
 func Run(src interface{}) (*Otto, Value, error) {
 	otto := New()
 	value, err := otto.Run(src) // This already does safety checking
@@ -290,7 +289,6 @@ func Run(src interface{}) (*Otto, Value, error) {
 // src may also be a Script.
 //
 // src may also be a Program, but if the AST has been modified, then runtime behavior is undefined.
-//
 func (self Otto) Run(src interface{}) (Value, error) {
 	value, err := self.runtime.cmpl_run(src, nil)
 	if !value.safe() {
@@ -524,16 +522,15 @@ func (self Otto) ContextSkip(limit int, skipNative bool) (ctx Context) {
 // Call will invoke the function constructor rather than performing a function call.
 // In this case, the this argument has no effect.
 //
-//      // value is a String object
-//      value, _ := vm.Call("Object", nil, "Hello, World.")
+//	// value is a String object
+//	value, _ := vm.Call("Object", nil, "Hello, World.")
 //
-//      // Likewise...
-//      value, _ := vm.Call("new Object", nil, "Hello, World.")
-//
-//      // This will perform a concat on the given array and return the result
-//      // value is [ 1, 2, 3, undefined, 4, 5, 6, 7, "abc" ]
-//      value, _ := vm.Call(`[ 1, 2, 3, undefined, 4 ].concat`, nil, 5, 6, 7, "abc")
+//	// Likewise...
+//	value, _ := vm.Call("new Object", nil, "Hello, World.")
 //
+//	// This will perform a concat on the given array and return the result
+//	// value is [ 1, 2, 3, undefined, 4, 5, 6, 7, "abc" ]
+//	value, _ := vm.Call(`[ 1, 2, 3, undefined, 4 ].concat`, nil, 5, 6, 7, "abc")
 func (self Otto) Call(source string, this interface{}, argumentList ...interface{}) (Value, error) {
 
 	thisValue := Value{}
@@ -602,16 +599,16 @@ func (self Otto) Call(source string, this interface{}, argumentList ...interface
 //
 // For example, accessing an existing object:
 //
-//		object, _ := vm.Object(`Number`)
+//	object, _ := vm.Object(`Number`)
 //
 // Or, creating a new object:
 //
-//		object, _ := vm.Object(`({ xyzzy: "Nothing happens." })`)
+//	object, _ := vm.Object(`({ xyzzy: "Nothing happens." })`)
 //
 // Or, creating and assigning an object:
 //
-//		object, _ := vm.Object(`xyzzy = {}`)
-//		object.Set("volume", 11)
+//	object, _ := vm.Object(`xyzzy = {}`)
+//	object.Set("volume", 11)
 //
 // If there is an error (like the source does not result in an object), then
 // nil and an error is returned.
@@ -667,15 +664,14 @@ func _newObject(object *_object, value Value) *Object {
 //
 // It is essentially equivalent to:
 //
-//		var method, _ := object.Get(name)
-//		method.Call(object, argumentList...)
+//	var method, _ := object.Get(name)
+//	method.Call(object, argumentList...)
 //
 // An undefined value and an error will result if:
 //
-//		1. There is an error during conversion of the argument list
-//		2. The property is not actually a function
-//		3. An (uncaught) exception is thrown
-//
+//  1. There is an error during conversion of the argument list
+//  2. The property is not actually a function
+//  3. An (uncaught) exception is thrown
 func (self Object) Call(name string, argumentList ...interface{}) (Value, error) {
 	// TODO: Insert an example using JavaScript below...
 	// e.g., Object("JSON").Call("stringify", ...)
@@ -756,15 +752,40 @@ func (self Object) KeysByParent() [][]string {
 //
 // The return value will (generally) be one of:
 //
-//		Object
-//		Function
-//		Array
-//		String
-//		Number
-//		Boolean
-//		Date
-//		RegExp
-//
+//	Object
+//	Function
+//	Array
+//	String
+//	Number
+//	Boolean
+//	Date
+//	RegExp
 func (self Object) Class() string {
 	return self.object.class
 }
+
+func (self Object) MarshalJSON() ([]byte, error) {
+	var goValue interface{}
+	switch value := self.object.value.(type) {
+	case *_goStructObject:
+		goValue = value.value.Interface()
+	case *_goMapObject:
+		goValue = value.value.Interface()
+	case *_goArrayObject:
+		goValue = value.value.Interface()
+	case *_goSliceObject:
+		goValue = value.value.Interface()
+	default:
+		// It's a JS object; pass it to JSON.stringify:
+		var result []byte
+		err := catchPanic(func() {
+			resultVal := builtinJSON_stringify(FunctionCall{
+				runtime:      self.object.runtime,
+				ArgumentList: []Value{self.value},
+			})
+			result = []byte(resultVal.String())
+		})
+		return result, err
+	}
+	return json.Marshal(goValue)
+}
diff --git a/otto_.go b/otto_.go
index 4d720c6..f4f9193 100644
--- a/otto_.go
+++ b/otto_.go
@@ -134,7 +134,7 @@ func rangeStartLength(source []Value, size int64) (start, length int64) {
 }
 
 func hereBeDragons(arguments ...interface{}) string {
-	pc, _, _, _ := runtime_.Caller(1) // nolint: dogsled
+	pc, _, _, _ := runtime_.Caller(1) //nolint: dogsled
 	name := runtime_.FuncForPC(pc).Name()
 	message := fmt.Sprintf("Here be dragons -- %s", name)
 	if len(arguments) > 0 {
diff --git a/parser/error.go b/parser/error.go
index 084a081..3fb5838 100644
--- a/parser/error.go
+++ b/parser/error.go
@@ -119,7 +119,7 @@ func (self *_parser) errorUnexpectedToken(tkn token.Token) error {
 }
 
 // ErrorList is a list of *Errors.
-type ErrorList []*Error // nolint: errname
+type ErrorList []*Error //nolint: errname
 
 // Add adds an Error with given position and message to an ErrorList.
 func (self *ErrorList) Add(position file.Position, msg string) {
diff --git a/parser/parser.go b/parser/parser.go
index 75b7c50..9230f12 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -1,35 +1,34 @@
 /*
 Package parser implements a parser for JavaScript.
 
-    import (
-        "github.com/robertkrimen/otto/parser"
-    )
+	import (
+	    "github.com/robertkrimen/otto/parser"
+	)
 
 Parse and return an AST
 
-    filename := "" // A filename is optional
-    src := `
-        // Sample xyzzy example
-        (function(){
-            if (3.14159 > 0) {
-                console.log("Hello, World.");
-                return;
-            }
+	filename := "" // A filename is optional
+	src := `
+	    // Sample xyzzy example
+	    (function(){
+	        if (3.14159 > 0) {
+	            console.log("Hello, World.");
+	            return;
+	        }
 
-            var xyzzy = NaN;
-            console.log("Nothing happens.");
-            return xyzzy;
-        })();
-    `
+	        var xyzzy = NaN;
+	        console.log("Nothing happens.");
+	        return xyzzy;
+	    })();
+	`
 
-    // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList
-    program, err := parser.ParseFile(nil, filename, src, 0)
+	// Parse some JavaScript, yielding a *ast.Program and/or an ErrorList
+	program, err := parser.ParseFile(nil, filename, src, 0)
 
-Warning
+# Warning
 
 The parser and AST interfaces are still works-in-progress (particularly where
 node types are concerned) and may change in the future.
-
 */
 package parser
 
@@ -131,7 +130,7 @@ func ReadSource(filename string, src interface{}) ([]byte, error) {
 
 func ReadSourceMap(filename string, src interface{}) (*sourcemap.Consumer, error) {
 	if src == nil {
-		return nil, nil
+		return nil, nil //nolint: nilnil
 	}
 
 	switch src := src.(type) {
@@ -203,9 +202,8 @@ func ParseFileWithSourceMap(fileSet *file.FileSet, filename string, javascriptSo
 //
 // src may be a string, a byte slice, a bytes.Buffer, or an io.Reader, but it MUST always be in UTF-8.
 //
-//      // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList
-//      program, err := parser.ParseFile(nil, "", `if (abc > 1) {}`, 0)
-//
+//	// Parse some JavaScript, yielding a *ast.Program and/or an ErrorList
+//	program, err := parser.ParseFile(nil, "", `if (abc > 1) {}`, 0)
 func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mode) (*ast.Program, error) {
 	return ParseFileWithSourceMap(fileSet, filename, src, nil, mode)
 }
@@ -214,7 +212,6 @@ func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mod
 // corresponding ast.FunctionLiteral node.
 //
 // The parameter list, if any, should be a comma-separated list of identifiers.
-//
 func ParseFunction(parameterList, body string) (*ast.FunctionLiteral, error) {
 
 	src := "(function(" + parameterList + ") {\n" + body + "\n})"
diff --git a/repl/autocompleter.go b/repl/autocompleter.go
index e0b649c..7e6d470 100644
--- a/repl/autocompleter.go
+++ b/repl/autocompleter.go
@@ -41,7 +41,7 @@ func (a *autoCompleter) Do(line []rune, pos int) ([][]rune, int) {
 
 		if o := r.Object(); o != nil {
 			for _, v := range o.KeysByParent() {
-				l = append(l, v...) // nolint: makezero
+				l = append(l, v...) //nolint: makezero
 			}
 		}
 	}
diff --git a/script.go b/script.go
index cfb2268..8280d41 100644
--- a/script.go
+++ b/script.go
@@ -12,7 +12,6 @@ var scriptVersion = "2014-04-13/1"
 
 // Script is a handle for some (reusable) JavaScript.
 // Passing a Script value to a run method will evaluate the JavaScript.
-//
 type Script struct {
 	version  string
 	program  *_nodeProgram
@@ -23,9 +22,8 @@ type Script struct {
 // Compile will parse the given source and return a Script value or nil and
 // an error if there was a problem during compilation.
 //
-//      script, err := vm.Compile("", `var abc; if (!abc) abc = 0; abc += 2; abc;`)
-//      vm.Run(script)
-//
+//	script, err := vm.Compile("", `var abc; if (!abc) abc = 0; abc += 2; abc;`)
+//	vm.Run(script)
 func (self *Otto) Compile(filename string, src interface{}) (*Script, error) {
 	return self.CompileWithSourceMap(filename, src, nil)
 }
@@ -58,7 +56,6 @@ func (self *Script) String() string {
 // that is later unmarshalled can be executed on the same version of the otto runtime.
 //
 // The binary format can change at any time and should be considered unspecified and opaque.
-//
 func (self *Script) marshalBinary() ([]byte, error) {
 	var bfr bytes.Buffer
 	encoder := gob.NewEncoder(&bfr)
diff --git a/token/token.go b/token/token.go
index 0e941ac..2f624ef 100644
--- a/token/token.go
+++ b/token/token.go
@@ -13,7 +13,6 @@ type Token int
 // token string (e.g., for the token PLUS, the String() is
 // "+"). For all other tokens the string corresponds to the token
 // name (e.g. for the token IDENTIFIER, the string is "IDENTIFIER").
-//
 func (tkn Token) String() string {
 	if 0 == tkn {
 		return "UNKNOWN"
@@ -86,25 +85,24 @@ type _keyword struct {
 //
 // 7.6.1.2 Future Reserved Words:
 //
-//       const
-//       class
-//       enum
-//       export
-//       extends
-//       import
-//       super
+//	const
+//	class
+//	enum
+//	export
+//	extends
+//	import
+//	super
 //
 // 7.6.1.2 Future Reserved Words (strict):
 //
-//       implements
-//       interface
-//       let
-//       package
-//       private
-//       protected
-//       public
-//       static
-//
+//	implements
+//	interface
+//	let
+//	package
+//	private
+//	protected
+//	public
+//	static
 func IsKeyword(literal string) (Token, bool) {
 	if keyword, exists := keywordTable[literal]; exists {
 		if keyword.futureKeyword {
diff --git a/token/token_const.go b/token/token_const.go
index 738dff3..323f4a0 100644
--- a/token/token_const.go
+++ b/token/token_const.go
@@ -74,7 +74,7 @@ const (
 	COLON             // :
 	QUESTION_MARK     // ?
 
-	firstKeyword // nolint: deadcode
+	firstKeyword //nolint: deadcode
 	IF
 	IN
 	DO
@@ -108,7 +108,7 @@ const (
 	DEBUGGER
 
 	INSTANCEOF
-	lastKeyword // nolint: deadcode
+	lastKeyword //nolint: deadcode
 )
 
 var token2string = [...]string{
diff --git a/underscore/underscore.go b/underscore/underscore.go
index d5bd0ca..4bc89c2 100644
--- a/underscore/underscore.go
+++ b/underscore/underscore.go
@@ -21,7 +21,6 @@ To prevent this behavior, you can do the following:
 	func init() {
 		underscore.Disable()
 	}
-
 */
 package underscore
 
diff --git a/value.go b/value.go
index 576f4c7..1c1e7f9 100644
--- a/value.go
+++ b/value.go
@@ -1,6 +1,7 @@
 package otto
 
 import (
+	"encoding/json"
 	"fmt"
 	"math"
 	"reflect"
@@ -94,14 +95,13 @@ func (value Value) isCallable() bool {
 // Call the value as a function with the given this value and argument list and
 // return the result of invocation. It is essentially equivalent to:
 //
-//		value.apply(thisValue, argumentList)
+//	value.apply(thisValue, argumentList)
 //
 // An undefined value and an error will result if:
 //
-//		1. There is an error during conversion of the argument list
-//		2. The value is not actually a function
-//		3. An (uncaught) exception is thrown
-//
+//  1. There is an error during conversion of the argument list
+//  2. The value is not actually a function
+//  3. An (uncaught) exception is thrown
 func (value Value) Call(this Value, argumentList ...interface{}) (Value, error) {
 	result := Value{}
 	err := catchPanic(func() {
@@ -195,15 +195,14 @@ func (value Value) IsFunction() bool {
 //
 // The return value will (generally) be one of:
 //
-//		Object
-//		Function
-//		Array
-//		String
-//		Number
-//		Boolean
-//		Date
-//		RegExp
-//
+//	Object
+//	Function
+//	Array
+//	String
+//	Number
+//	Boolean
+//	Date
+//	RegExp
 func (value Value) Class() string {
 	if value.kind != valueObject {
 		return ""
@@ -386,11 +385,11 @@ func (value Value) String() string {
 
 // ToBoolean will convert the value to a boolean (bool).
 //
-//		ToValue(0).ToBoolean() => false
-//		ToValue("").ToBoolean() => false
-//		ToValue(true).ToBoolean() => true
-//		ToValue(1).ToBoolean() => true
-//		ToValue("Nothing happens").ToBoolean() => true
+//	ToValue(0).ToBoolean() => false
+//	ToValue("").ToBoolean() => false
+//	ToValue(true).ToBoolean() => true
+//	ToValue(1).ToBoolean() => true
+//	ToValue("Nothing happens").ToBoolean() => true
 //
 // If there is an error during the conversion process (like an uncaught exception), then the result will be false and an error.
 func (value Value) ToBoolean() (bool, error) {
@@ -410,9 +409,9 @@ func (value Value) numberValue() Value {
 
 // ToFloat will convert the value to a number (float64).
 //
-//		ToValue(0).ToFloat() => 0.
-//		ToValue(1.1).ToFloat() => 1.1
-//		ToValue("11").ToFloat() => 11.
+//	ToValue(0).ToFloat() => 0.
+//	ToValue(1.1).ToFloat() => 1.1
+//	ToValue("11").ToFloat() => 11.
 //
 // If there is an error during the conversion process (like an uncaught exception), then the result will be 0 and an error.
 func (value Value) ToFloat() (float64, error) {
@@ -425,9 +424,9 @@ func (value Value) ToFloat() (float64, error) {
 
 // ToInteger will convert the value to a number (int64).
 //
-//		ToValue(0).ToInteger() => 0
-//		ToValue(1.1).ToInteger() => 1
-//		ToValue("11").ToInteger() => 11
+//	ToValue(0).ToInteger() => 0
+//	ToValue(1.1).ToInteger() => 1
+//	ToValue("11").ToInteger() => 11
 //
 // If there is an error during the conversion process (like an uncaught exception), then the result will be 0 and an error.
 func (value Value) ToInteger() (int64, error) {
@@ -440,11 +439,11 @@ func (value Value) ToInteger() (int64, error) {
 
 // ToString will convert the value to a string (string).
 //
-//		ToValue(0).ToString() => "0"
-//		ToValue(false).ToString() => "false"
-//		ToValue(1.1).ToString() => "1.1"
-//		ToValue("11").ToString() => "11"
-//		ToValue('Nothing happens.').ToString() => "Nothing happens."
+//	ToValue(0).ToString() => "0"
+//	ToValue(false).ToString() => "false"
+//	ToValue(1.1).ToString() => "1.1"
+//	ToValue("11").ToString() => "11"
+//	ToValue('Nothing happens.').ToString() => "Nothing happens."
 //
 // If there is an error during the conversion process (like an uncaught exception), then the result will be the empty string ("") and an error.
 func (value Value) ToString() (string, error) {
@@ -506,8 +505,7 @@ func negativeZero() float64 {
 //
 // It is equivalent to:
 //
-//		ToValue(math.NaN())
-//
+//	ToValue(math.NaN())
 func NaNValue() Value {
 	return Value{valueNumber, __NaN__}
 }
@@ -532,8 +530,7 @@ func negativeZeroValue() Value {
 //
 // It is equivalent to:
 //
-//		ToValue(true)
-//
+//	ToValue(true)
 func TrueValue() Value {
 	return Value{valueBoolean, true}
 }
@@ -542,8 +539,7 @@ func TrueValue() Value {
 //
 // It is equivalent to:
 //
-//		ToValue(false)
-//
+//	ToValue(false)
 func FalseValue() Value {
 	return Value{valueBoolean, false}
 }
@@ -617,14 +613,13 @@ func strictEqualityComparison(x Value, y Value) bool {
 // If a reasonable conversion is not possible, then the original
 // value is returned.
 //
-//      undefined   -> nil (FIXME?: Should be Value{})
-//      null        -> nil
-//      boolean     -> bool
-//      number      -> A number type (int, float32, uint64, ...)
-//      string      -> string
-//      Array       -> []interface{}
-//      Object      -> map[string]interface{}
-//
+//	undefined   -> nil (FIXME?: Should be Value{})
+//	null        -> nil
+//	boolean     -> bool
+//	number      -> A number type (int, float32, uint64, ...)
+//	string      -> string
+//	Array       -> []interface{}
+//	Object      -> map[string]interface{}
 func (self Value) Export() (interface{}, error) {
 	return self.export(), nil
 }
@@ -1023,3 +1018,17 @@ func stringToReflectValue(value string, kind reflect.Kind) (reflect.Value, error
 	// FIXME This should end up as a TypeError?
 	panic(fmt.Errorf("invalid conversion of %q to reflect.Kind: %v", value, kind))
 }
+
+func (self Value) MarshalJSON() ([]byte, error) {
+	switch self.kind {
+	case valueUndefined, valueNull:
+		return []byte("null"), nil
+	case valueBoolean, valueNumber:
+		return json.Marshal(self.value)
+	case valueString:
+		return json.Marshal(self.string())
+	case valueObject:
+		return self.Object().MarshalJSON()
+	}
+	return nil, fmt.Errorf("invalid type %v", self.kind)
+}


Test patch:
diff --git a/.github/workflows/test-lint.yml b/.github/workflows/test-lint.yml
index 3afd8c9..7bc2a0b 100644
--- a/.github/workflows/test-lint.yml
+++ b/.github/workflows/test-lint.yml
@@ -1,24 +1,59 @@
 name: Go test and lint
 
 on:
-  push:
-    branches: '**'
+  pull_request:
+    branches: 'master'
 
 jobs:
   go-test-lint:
-    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        go: [1.18]
+        golangcli: [v1.50.0]
+        os: [ubuntu-latest]
+    runs-on: ${{ matrix.os }}
     steps:
-      - uses: actions/checkout@v2
-
       - name: Set up Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
-          go-version: 1.17
+          go-version: ${{ matrix.go }}
+
+      - name: Checkout
+        uses: actions/checkout@v3
 
-      - name: Test
-        run: go test -v ./...
+      - name: Go Cache Paths
+        id: go-cache-paths
+        run: |
+          echo "::set-output name=go-build::$(go env GOCACHE)"
+          echo "::set-output name=go-mod::$(go env GOMODCACHE)"
 
-      - name: golangci-lint
-        uses: golangci/golangci-lint-action@v2
+      - name: Go Build Cache
+        uses: actions/cache@v3
         with:
-          version: latest
+          path: ${{ steps.go-cache-paths.outputs.go-build }}
+          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}
+
+      - name: Go Mod Cache
+        uses: actions/cache@v3
+        with:
+          path: ${{ steps.go-cache-paths.outputs.go-mod }}
+          key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}
+
+      - name: Validate go mod
+        run: |
+          go mod tidy
+          git --no-pager diff && [[ 0 -eq $(git status --porcelain | wc -l) ]]
+
+      - name: Go Lint
+        uses: golangci/golangci-lint-action@v3
+        with:
+          version: ${{ matrix.golangci }}
+          args: "--out-${NO_FUTURE}format colored-line-number"
+          skip-pkg-cache: true
+          skip-build-cache: true
+
+      - name: Go Build
+        run: go build ./...
+
+      - name: Go Test
+        run: go test -race -v ./...
diff --git a/documentation_test.go b/documentation_test.go
index 4e453f7..c9a7625 100644
--- a/documentation_test.go
+++ b/documentation_test.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 )
 
-func ExampleSynopsis() { // nolint: govet
+func ExampleSynopsis() { //nolint: govet
 	vm := New()
 	vm.Run(`
         abc = 2 + 2;
@@ -69,7 +69,7 @@ func ExampleSynopsis() { // nolint: govet
 	// 4
 }
 
-func ExampleConsole() { // nolint: govet
+func ExampleConsole() { //nolint: govet
 	vm := New()
 	console := map[string]interface{}{
 		"log": func(call FunctionCall) Value {
diff --git a/otto_test.go b/otto_test.go
index cdbf95c..fc30cd6 100644
--- a/otto_test.go
+++ b/otto_test.go
@@ -2,10 +2,14 @@ package otto
 
 import (
 	"bytes"
+	"errors"
+	"fmt"
 	"io"
 	"testing"
+	"time"
 
 	"github.com/robertkrimen/otto/parser"
+	"github.com/stretchr/testify/require"
 )
 
 func TestOtto(t *testing.T) {
@@ -1738,6 +1742,56 @@ func Test_stackLimit(t *testing.T) {
 	})
 }
 
+func TestOttoInterrupt(t *testing.T) {
+	tests := []struct {
+		name   string
+		script string
+	}{
+		{
+			name:   "empty-for-loop",
+			script: "for(;;) {}",
+		},
+		{
+			name:   "empty-do-while",
+			script: "do{} while(true)",
+		},
+	}
+
+	halt := errors.New("interrupt")
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			vm := New()
+			vm.Interrupt = make(chan func(), 1)
+			ec := make(chan error, 1)
+			go func() {
+				defer func() {
+					if caught := recover(); caught != nil {
+						if caught == halt {
+							fmt.Println(caught)
+							ec <- nil
+							return
+						}
+						panic(caught)
+					}
+				}()
+				_, err := vm.Run(tc.script)
+				ec <- err
+			}()
+
+			// Give the vm chance to execute the loop.
+			time.Sleep(time.Millisecond * 100)
+			vm.Interrupt <- func() { panic(halt) }
+
+			select {
+			case <-time.After(time.Second):
+				t.Fatal("timeout")
+			case err := <-ec:
+				require.NoError(t, err)
+			}
+		})
+	}
+}
+
 func BenchmarkNew(b *testing.B) {
 	for i := 0; i < b.N; i++ {
 		New()
diff --git a/reflect_test.go b/reflect_test.go
index 38b622b..41842da 100644
--- a/reflect_test.go
+++ b/reflect_test.go
@@ -761,11 +761,11 @@ func TestPassthrough(t *testing.T) {
 	})
 }
 
-type TestDynamicFunctionReturningInterface_MyStruct1 struct{} // nolint: errname
+type TestDynamicFunctionReturningInterface_MyStruct1 struct{} //nolint: errname
 
 func (m *TestDynamicFunctionReturningInterface_MyStruct1) Error() string { return "MyStruct1" }
 
-type TestDynamicFunctionReturningInterface_MyStruct2 struct{} // nolint: errname
+type TestDynamicFunctionReturningInterface_MyStruct2 struct{} //nolint: errname
 
 func (m *TestDynamicFunctionReturningInterface_MyStruct2) Error() string { return "MyStruct2" }
 
diff --git a/test/tester.go b/test/tester.go
index 05d02c3..e3f5a24 100644
--- a/test/tester.go
+++ b/test/tester.go
@@ -42,7 +42,7 @@ var target = map[string]string{
 // http://api.cdnjs.com/libraries
 
 func fetch(name, location string) error {
-	response, err := http.Get(location) // nolint: noctx
+	response, err := http.Get(location) //nolint: noctx
 	if err != nil {
 		return err
 	}
@@ -110,7 +110,7 @@ func main() {
 	err := func() error {
 
 		if flag.Arg(0) == "fetch" {
-			response, err := http.Get("http://api.cdnjs.com/libraries") // nolint: noctx
+			response, err := http.Get("http://api.cdnjs.com/libraries") //nolint: noctx
 			if err != nil {
 				return err
 			}
diff --git a/value_test.go b/value_test.go
index 5d6792d..82463f9 100644
--- a/value_test.go
+++ b/value_test.go
@@ -312,3 +312,74 @@ func Test_toReflectValue(t *testing.T) {
 		is(err, nil)
 	})
 }
+
+func TestJSONMarshaling(t *testing.T) {
+	tt(t, func() {
+		eval, tester := test()
+		toJSON := func(val interface{}) string {
+			j, err := json.Marshal(val)
+			is(err, nil)
+			return string(j)
+		}
+
+		is(toJSON(UndefinedValue()), `null`)
+		is(toJSON(NullValue()), `null`)
+		is(toJSON(FalseValue()), `false`)
+		is(toJSON(TrueValue()), `true`)
+
+		is(toJSON(toValue(0)), `0`)
+		is(toJSON(toValue(1234)), `1234`)
+		is(toJSON(toValue(1234.125)), `1234.125`)
+		is(toJSON(toValue(-1234)), `-1234`)
+		is(toJSON(toValue(-1234.125)), `-1234.125`)
+
+		is(toJSON(toValue("")), `""`)
+		is(toJSON(toValue("Otto")), `"Otto"`)
+		is(toJSON(eval(`String.fromCharCode(97,98,99,100,101,102)`)), `"abcdef"`)
+
+		is(toJSON(eval("[]")), `[]`)
+		is(toJSON(eval("[1, 2, 3]")), `[1,2,3]`)
+		is(toJSON(eval("new Array(1,2,3)")), `[1,2,3]`)
+
+		is(toJSON(eval(`({a:1, b:"hi", c:[true,false]})`)), `{"a":1,"b":"hi","c":[true,false]}`)
+
+		goArray := []string{"foo", "bar"}
+		val, _ := tester.vm.ToValue(goArray)
+		is(toJSON(val), `["foo","bar"]`)
+
+		goMap := map[string]interface{}{
+			"bar": []int{1, 2, 3},
+			"foo": 17,
+		}
+		val, _ = tester.vm.ToValue(goMap)
+		is(toJSON(val), `{"bar":[1,2,3],"foo":17}`)
+	})
+}
+
+func TestNestedJSONMarshaling(t *testing.T) {
+	tt(t, func() {
+		eval, _ := test()
+		toJSON := func(val interface{}) string {
+			j, err := json.Marshal(val)
+			is(err, nil)
+			return string(j)
+		}
+
+		goMap := map[string]interface{}{
+			"foo": 17,
+		}
+
+		fn := eval(`(function(obj) {obj.jsVal = "hi"; obj.jsArray = [17,true]; return obj;})`)
+		result, err := fn.Call(fn, goMap)
+		is(err, nil)
+		exported, err := result.Export()
+		is(err, nil)
+
+		is(toJSON(exported), `{"foo":17,"jsArray":[17,true],"jsVal":"hi"}`)
+
+		// Before MarshalJSON was implemented, this last assertion would fail:
+		// FAIL (==)
+		//      got: {"foo":17,"jsArray":{},"jsVal":"hi"}
+		// expected: {"foo":17,"jsArray":[17,true],"jsVal":"hi"}
+	})
+}