Repository: robertkrimen/otto Branch: master Commit: 3ca729876b89 Files: 167 Total size: 1.2 MB Directory structure: gitextract_6msnb3d6/ ├── .clog.toml ├── .github/ │ └── workflows/ │ ├── release-build.yml │ └── test-lint.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── DESIGN.markdown ├── LICENSE ├── README.md ├── array_test.go ├── ast/ │ ├── comments.go │ ├── comments_test.go │ ├── node.go │ ├── walk.go │ ├── walk_example_test.go │ └── walk_test.go ├── builtin.go ├── builtin_array.go ├── builtin_boolean.go ├── builtin_date.go ├── builtin_error.go ├── builtin_function.go ├── builtin_json.go ├── builtin_math.go ├── builtin_number.go ├── builtin_object.go ├── builtin_regexp.go ├── builtin_string.go ├── builtin_test.go ├── call_test.go ├── clone.go ├── clone_test.go ├── cmpl.go ├── cmpl_evaluate.go ├── cmpl_evaluate_expression.go ├── cmpl_evaluate_statement.go ├── cmpl_parse.go ├── cmpl_test.go ├── console.go ├── consts.go ├── date_test.go ├── dbg/ │ └── dbg.go ├── dbg.go ├── documentation_test.go ├── error.go ├── error_native_test.go ├── error_test.go ├── evaluate.go ├── file/ │ └── file.go ├── function_stack_test.go ├── function_test.go ├── functional_benchmark_test.go ├── generate.go ├── global.go ├── global_test.go ├── go.mod ├── go.sum ├── inline.go ├── inline_test.go ├── issue_test.go ├── json_test.go ├── locale.go ├── math_test.go ├── native_stack_test.go ├── number_test.go ├── object.go ├── object_class.go ├── object_test.go ├── otto/ │ └── main.go ├── otto.go ├── otto_.go ├── otto_error_test.go ├── otto_test.go ├── panic_test.go ├── parser/ │ ├── comments_test.go │ ├── error.go │ ├── expression.go │ ├── lexer.go │ ├── lexer_test.go │ ├── marshal_test.go │ ├── parser.go │ ├── parser_test.go │ ├── regexp.go │ ├── regexp_test.go │ ├── scope.go │ └── statement.go ├── parser_test.go ├── property.go ├── reflect_test.go ├── regexp_test.go ├── registry/ │ └── registry.go ├── repl/ │ ├── autocompleter.go │ └── repl.go ├── result.go ├── runtime.go ├── runtime_test.go ├── scope.go ├── script.go ├── script_test.go ├── sourcemap_test.go ├── stash.go ├── string_test.go ├── terst/ │ └── terst.go ├── testing_test.go ├── token/ │ ├── generate.go │ ├── token.go │ └── token_const.go ├── tools/ │ ├── gen-jscore/ │ │ ├── .gen-jscore.yaml │ │ ├── helpers.go │ │ ├── main.go │ │ └── templates/ │ │ ├── constructor.tmpl │ │ ├── core-prototype-property.tmpl │ │ ├── definition.tmpl │ │ ├── function.tmpl │ │ ├── global.tmpl │ │ ├── name.tmpl │ │ ├── property-entry.tmpl │ │ ├── property-fields.tmpl │ │ ├── property-order.tmpl │ │ ├── property-value.tmpl │ │ ├── property.tmpl │ │ ├── prototype.tmpl │ │ ├── root.tmpl │ │ ├── type.tmpl │ │ └── value.tmpl │ ├── gen-tokens/ │ │ ├── .gen-tokens.yaml │ │ ├── main.go │ │ └── templates/ │ │ └── root.tmpl │ └── tester/ │ └── main.go ├── type_arguments.go ├── type_array.go ├── type_boolean.go ├── type_date.go ├── type_error.go ├── type_function.go ├── type_go_array.go ├── type_go_map.go ├── type_go_map_test.go ├── type_go_slice.go ├── type_go_slice_test.go ├── type_go_struct.go ├── type_go_struct_test.go ├── type_number.go ├── type_reference.go ├── type_regexp.go ├── type_string.go ├── underscore/ │ ├── LICENSE.underscorejs │ ├── README.md │ ├── download.go │ ├── generate.go │ ├── testify │ ├── underscore-min.js │ └── underscore.go ├── underscore_arrays_test.go ├── underscore_chaining_test.go ├── underscore_collections_test.go ├── underscore_functions_test.go ├── underscore_objects_test.go ├── underscore_test.go ├── underscore_utility_test.go ├── value.go ├── value_boolean.go ├── value_kind.gen.go ├── value_number.go ├── value_primitive.go ├── value_string.go └── value_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clog.toml ================================================ [clog] repository = "https://github.com/robertkrimen/otto" subtitle = "Release Notes" [sections] "Refactors" = ["refactor"] "Chores" = ["chore"] "Continuous Integration" = ["ci"] "Improvements" = ["imp", "improvement"] "Features" = ["feat", "feature"] "Legacy" = ["legacy"] "QA" = ["qa", "test", "tests"] "Documentation" = ["doc", "docs"] ================================================ FILE: .github/workflows/release-build.yml ================================================ name: Build Release on: push: tags: - 'v[0-9]+.[0-9]+.[0-9]+*' jobs: goreleaser: name: Release Go Binary runs-on: [ubuntu-latest] steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v5 with: go-version: 1.23 cache: true - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: # either 'goreleaser' (default) or 'goreleaser-pro' distribution: goreleaser version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} ================================================ FILE: .github/workflows/test-lint.yml ================================================ name: Go test and lint on: pull_request: branches: 'master' jobs: go-test-lint: strategy: matrix: go: [1.22, 1.23] golangcli: [v1.61.0] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} cache: true - name: Validate go mod / generate run: | go mod tidy go install golang.org/x/tools/cmd/stringer@latest go generate ./... git --no-pager diff && [[ 0 -eq $(git status --porcelain | wc -l) ]] - name: Go Lint uses: golangci/golangci-lint-action@v4 with: version: ${{ matrix.golangcli }} args: --out-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 ./... ================================================ FILE: .gitignore ================================================ .test otto/otto otto/otto-* tools/tester/testdata/ tools/tester/tester tools/gen-jscore/gen-jscore tools/gen-tokens/gen-tokens .idea dist/ .vscode/ ================================================ FILE: .golangci.yml ================================================ run: timeout: 6m linters-settings: govet: settings: shadow: strict: true enable-all: true goconst: min-len: 2 min-occurrences: 4 revive: enable-all-rules: false rules: - name: var-naming disabled: true gosec: excludes: - G115 # Too many false positives. linters: enable-all: true disable: - dupl - lll - gochecknoglobals - gochecknoinits - funlen - godox - err113 - wsl - nlreturn - gomnd - mnd - paralleltest - wrapcheck - testpackage - gocognit - nestif - exhaustive - forcetypeassert - gocyclo - cyclop - varnamelen - maintidx - ireturn - exhaustruct - dupword # Just causes noise - depguard # Deprecated - execinquery # Not needed in go 1.22+ - exportloopref issues: exclude-use-default: false max-same-issues: 0 exclude: - Deferring unsafe method "Close" on type "io\.ReadCloser" exclude-dirs: - terst exclude-files: - dbg/dbg.go - token/token_const.go exclude-rules: # Field alignment in tests isn't a performance issue. - text: fieldalignment path: _test\.go - text: Error return value of `fmt\.Fprint.*` is not checked path: tools/tester/main.go ================================================ FILE: .goreleaser.yaml ================================================ # When adding options check the documentation at https://goreleaser.com before: hooks: - go mod tidy builds: - env: - CGO_ENABLED=0 goos: - linux - darwin goarch: - amd64 - arm64 main: ./otto id: otto binary: otto universal_binaries: - replace: true id: otto checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ incpatch .Version }}-next" archives: - id: otto name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" release: header: | ### {{.Tag}} Release Notes ({{.Date}}) footer: | [Full Changelog](https://{{ .ModulePath }}/compare/{{ .PreviousTag }}...{{ .Tag }}) changelog: use: github sort: asc filters: exclude: - Merge pull request - Merge remote-tracking branch - Merge branch # Group commits messages by given regex and title. # Order value defines the order of the groups. # Proving no regex means all commits will be grouped under the default group. # Groups are disabled when using github-native, as it already groups things by itself. # Matches are performed against strings of the form: " ". # Regex use RE2 syntax as defined here: https://github.com/google/re2/wiki/Syntax. # # Default is no groups. groups: - title: Features regexp: '^.*?(feat|feature)(\([[:word:]]+\))??!?:.+$' order: 0 - title: 'Bug fixes' regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' order: 1 - title: 'Chores' regexp: '^.*?chore(\([[:word:]]+\))??!?:.+$' order: 2 - title: 'Quality' regexp: '^.*?(qa|test|tests)(\([[:word:]]+\))??!?:.+$' order: 3 - title: 'Documentation' regexp: '^.*?(doc|docs)(\([[:word:]]+\))??!?:.+$' order: 4 - title: 'Continuous Integration' regexp: '^.*?ci(\([[:word:]]+\))??!?:.+$' order: 5 - title: Other order: 999 ================================================ FILE: DESIGN.markdown ================================================ * Designate the filename of "anonymous" source code by the hash (md5/sha1, etc.) ================================================ FILE: LICENSE ================================================ Copyright (c) 2012 Robert Krimen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # otto [![GoDoc Reference](https://pkg.go.dev/badge/github.com/robertkrimen/otto.svg)](https://pkg.go.dev/github.com/robertkrimen/otto) ## Basic Usage Package otto is a JavaScript parser and interpreter written natively in Go. To use import it with the following: ```go import ( "github.com/robertkrimen/otto" ) ``` Run something in the VM ```go vm := otto.New() vm.Run(` abc = 2 + 2; console.log("The value of abc is " + abc); // 4 `) ``` Get a value out of the VM ```go if value, err := vm.Get("abc"); err == nil { if value_int, err := value.ToInteger(); err == nil { fmt.Println(value_int) } else { fmt.Printf("Error during conversion: %v\n", err) } } else { fmt.Printf("Error getting value: %v\n", err) } ``` Set a number ```go vm.Set("def", 11) vm.Run(` console.log("The value of def is " + def); // The value of def is 11 `) ``` Set a string ```go vm.Set("xyzzy", "Nothing happens.") vm.Run(` console.log(xyzzy.length); // 16 `) ``` Get the value of an expression ```go value, _ = vm.Run("xyzzy.length") { // value is an int64 with a value of 16 value, _ := value.ToInteger() } ``` An error happens ```go _, 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 ```go 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 ```go 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 ```go result, _ = vm.Run(` sayHello("Xyzzy"); // Hello, Xyzzy. sayHello(); // Hello, undefined result = twoPlus(2.0); // 4 `) ``` ## Parser A separate parser is available in the parser package if you're interested in only building an AST. [![GoDoc Reference](https://pkg.go.dev/badge/github.com/robertkrimen/otto/parser.svg)](https://pkg.go.dev/github.com/robertkrimen/otto/parser) Parse and return an AST ```go 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; })(); ` // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList program, err := parser.ParseFile(nil, filename, src, 0) ``` ## Setup You can run (Go) JavaScript from the command line with [otto](http://github.com/robertkrimen/otto/tree/master/otto). ```shell go install github.com/robertkrimen/otto/otto@latest ``` Run JavaScript by entering some source on stdin or by giving otto a filename: ```shell otto example.js ``` ## Underscore Optionally include the JavaScript utility-belt library, underscore, with this import: ```go import ( "github.com/robertkrimen/otto" _ "github.com/robertkrimen/otto/underscore" ) // Now every otto runtime will be initialized with Underscore. ``` For more information: [underscore](http://github.com/robertkrimen/otto/tree/master/underscore) ## Caveat Emptor The following are some limitations with otto: * `use strict` will parse, but does nothing. * The regular expression engine ([re2/regexp](https://pkg.go.dev/regexp)) is not fully compatible with the ECMA5 specification. * Otto targets ES5. Some ES6 features, e.g. Typed Arrays, are not supported. Pull requests to add functionality are always welcome. ### 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 Go [re2](https://github.com/google/re2/wiki/syntax). Therefore, the following syntax is incompatible: ```plaintext (?=) // 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) More information [about re2](https://github.com/google/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 If you want to stop long running executions (like third-party code), you can use the interrupt channel to do this: ```go 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 too 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 watchdogCleanup := make(chan struct{}) defer close(watchdogCleanup) go func() { select { case <-time.After(2 * time.Second): // Stop after two seconds vm.Interrupt <- func() { panic(halt) } case <-watchdogCleanup: } close(vm.Interrupt) }() vm.Run(unsafe) // Here be dragons (risky code) } ``` Where is `setTimeout` / `setInterval`? These timing functions are not part of the [ECMA-262 specification](https://ecma-international.org/publications-and-standards/standards/ecma-262/). They typically belong to the window object in a browser environment. While it is possible to implement similar functionality in Go, it generally requires wrapping Otto in an event loop. For an example of how this could be done in Go with otto, see [natto](http://github.com/robertkrimen/natto). Here is some more discussion of the issue: * [What is Node.js?](http://book.mixu.net/node/ch2.html) * [Reentrancy (computing)](http://en.wikipedia.org/wiki/Reentrancy_%28computing%29) * [Perl Safe Signals](https://metacpan.org/pod/Perl::Unsafe::Signals) ## Usage ```go var ErrVersion = errors.New("version mismatch") ``` ### type Error ```go type Error struct {} ``` An Error represents a runtime error, e.g. a `TypeError`, a `ReferenceError`, etc. ### func (Error) Error ```go func (err Error) Error() string ``` Error returns a string representation of the error ```plaintext TypeError: 'def' is not a function ``` ### func (Error) String ```go func (err Error) String() string ``` String returns a description of the error and a trace of where the error occurred. ```plaintext TypeError: 'def' is not a function at xyz (:3:9) at :7:1/ ``` ### type FunctionCall ```go type FunctionCall struct { This Value ArgumentList []Value Otto *Otto } ``` FunctionCall is an encapsulation of a JavaScript function call. ### func (FunctionCall) Argument ```go func (self FunctionCall) Argument(index int) Value ``` Argument will return the value of the argument at the given index. If no such argument exists, undefined is returned. ### type Object ```go type Object struct {} ``` Object is the representation of a JavaScript object. ### func (Object) Call ```go func (self Object) Call(name string, argumentList ...interface{}) (Value, error) ``` Call a method on the object. It is essentially equivalent to: ```go 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 ### func (Object) Class ```go func (self Object) Class() string ``` Class will return the class string of the object. The return value will (generally) be one of: ```plaintext Object Function Array String Number Boolean Date RegExp ``` ### func (Object) Get ```go func (self Object) Get(name string) (Value, error) ``` Get the value of the property with the given name. ### func (Object) Keys ```go func (self Object) Keys() []string ``` Get the keys for the object This is equivalent to calling Object.keys on the object. ### func (Object) Set ```go func (self Object) Set(name string, value interface{}) error ``` Set the property of the given name to the given value. An error will result if setting the property triggers an exception (e.g. read-only) or if there is an error during conversion of the given value. ### func (Object) Value ```go func (self Object) Value() Value ``` Value will return self as a value. ### type Otto ```go type Otto struct { // Interrupt is a channel for interrupting the runtime. You can use this to halt a long running execution, for example. // See "Halting Problem" for more information. Interrupt chan func() } ``` Otto is the representation of the JavaScript runtime. Each instance of Otto has a self-contained namespace. ### func New ```go func New() *Otto ``` New will allocate a new JavaScript runtime ### func Run ```go func Run(src interface{}) (*Otto, Value, error) ``` Run will allocate a new JavaScript runtime, run the given source on the allocated runtime, and return the runtime, resulting value, and error (if any). src may be a string, a byte slice, a bytes.Buffer, or an io.Reader, but it MUST always be in UTF-8. 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 (Otto) Call ```go func (self Otto) Call(source string, this interface{}, argumentList ...interface{}) (Value, error) ``` Call the given JavaScript with a given this and arguments. If this is nil, then some special handling takes place to determine the proper this value, falling back to a "standard" invocation if necessary (where this is undefined). If source begins with "new " (A lowercase new followed by a space), then Call will invoke the function constructor rather than performing a function call. In this case, the this argument has no effect. ```go // 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") ``` ### func (*Otto) Compile ```go func (self *Otto) Compile(filename string, src interface{}) (*Script, error) ``` Compile will parse the given source and return a Script value. If there is an error during compilation, it will return nil and an error. ```go script, err := vm.Compile("", `var abc; if (!abc) abc = 0; abc += 2; abc;`) vm.Run(script) ``` ### func (*Otto) Copy ```go func (in *Otto) Copy() *Otto ``` Copy will create a copy/clone of the runtime. Copy is useful for saving some time when creating many similar runtimes. This method works by walking the original runtime and cloning each object, scope, stash, etc. into a new runtime. Be on the lookout for memory leaks or inadvertent sharing of resources. ### func (Otto) Get ```go func (self Otto) Get(name string) (Value, error) ``` Get the value of the top-level binding of the given name. If there is an error (like the binding does not exist), then the value will be undefined. ### func (Otto) Object ```go func (self Otto) Object(source string) (*Object, error) ``` Object will run the given source and return the result as an object. For example, accessing an existing object: ```go object, _ := vm.Object(`Number`) ``` Or, creating a new object: ```go object, _ := vm.Object(`({ xyzzy: "Nothing happens." })`) ``` Or, creating and assigning an object: ```go 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. ### func (Otto) Run ```go func (self Otto) Run(src interface{}) (Value, error) ``` Run will run the given source (parsing it first if necessary), returning the resulting value and error (if any) src may be a string, a byte slice, a bytes.Buffer, or an io.Reader, but it MUST always be in UTF-8. If the runtime is unable to parse source, then this function will return undefined and the parse error (nothing will be evaluated in this case). 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 (Otto) Set ```go func (self Otto) Set(name string, value interface{}) error ``` Set the top-level binding of the given name to the given value. Set will automatically apply ToValue to the given value in order to convert it to a JavaScript value (type Value). If there is an error (like the binding is read-only, or the ToValue conversion fails), then an error is returned. If the top-level binding does not exist, it will be created. ### func (Otto) ToValue ```go func (self Otto) ToValue(value interface{}) (Value, error) ``` ToValue will convert an interface{} value to a value digestible by otto/JavaScript. ### type Script ```go type Script struct {} ``` Script is a handle for some (reusable) JavaScript. Passing a Script value to a run method will evaluate the JavaScript. ### func (*Script) String ```go func (self *Script) String() string ``` ### type Value ```go type Value struct {} ``` Value is the representation of a JavaScript value. ### func FalseValue ```go func FalseValue() Value ``` FalseValue will return a Value representing the bool value false. It is equivalent to: ```go ToValue(false) ``` ### func NaNValue ```go func NaNValue() Value ``` NaNValue will return a value representing NaN. It is equivalent to: ```go ToValue(math.NaN()) ``` ### func NullValue ```go func NullValue() Value ``` NullValue will return a Value representing null. ### func ToValue ```go func ToValue(value interface{}) (Value, error) ``` ToValue will convert an interface{} value to a value digestible by otto/JavaScript This function will not work for advanced types (struct, map, slice/array, etc.) and you should use Otto.ToValue instead. ### func TrueValue ```go func TrueValue() Value ``` TrueValue will return a value representing true. It is equivalent to: ```go ToValue(true) ``` ### func UndefinedValue ```go func UndefinedValue() Value ``` UndefinedValue will return a Value representing undefined. ### func (Value) Call ```go func (value Value) Call(this Value, argumentList ...interface{}) (Value, error) ``` 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: ```js value.apply(thisValue, argumentList) ``` A value of undefined 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 ### func (Value) Class ```go func (value Value) Class() string ``` Class will return the class string of the value or the empty string if value is not an object. The return value will (generally) be one of: ```plaintext Object Function Array String Number Boolean Date RegExp ``` ### func (Value) Export ```go func (self Value) Export() (interface{}, error) ``` Export will attempt to convert the value to a Go representation and return it via an interface{} kind. Export returns an error, which will always be nil. It is included for backwards compatibility. If a reasonable conversion is not possible, then the original value is returned. ```plaintext 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 (Value) IsBoolean ```go func (value Value) IsBoolean() bool ``` IsBoolean will return true if value is a boolean (primitive). ### func (Value) IsDefined ```go func (value Value) IsDefined() bool ``` IsDefined will return false if the value is undefined, and true otherwise. ### func (Value) IsFunction ```go func (value Value) IsFunction() bool ``` IsFunction will return true if value is a function. ### func (Value) IsNaN ```go func (value Value) IsNaN() bool ``` IsNaN will return true if value is NaN (or would convert to NaN). ### func (Value) IsNull ```go func (value Value) IsNull() bool ``` IsNull will return true if the value is null, and false otherwise. ### func (Value) IsNumber ```go func (value Value) IsNumber() bool ``` IsNumber will return true if value is a number (primitive). ### func (Value) IsObject ```go func (value Value) IsObject() bool ``` IsObject will return true if value is an object. ### func (Value) IsPrimitive ```go func (value Value) IsPrimitive() bool ``` IsPrimitive will return true if value is a primitive. ### func (Value) IsString ```go func (value Value) IsString() bool ``` IsString will return true if value is a string (primitive). ### func (Value) IsUndefined ```go func (value Value) IsUndefined() bool ``` IsUndefined will return true if the value is undefined, and false otherwise. ### func (Value) Object ```go func (value Value) Object() *Object ``` Object will return the object of the value, or nil if value is not an object. This method will not do any implicit conversion. For example, calling this method on a string primitive value will not return a String object. ### func (Value) String ```go func (value Value) String() string ``` String will return the value as a string. This method will return the empty string if there is an error. ### func (Value) ToBoolean ```go func (value Value) ToBoolean() (bool, error) ``` ToBoolean will convert the value to a boolean (bool). ```plaintext 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) ToFloat ```go func (value Value) ToFloat() (float64, error) ``` ToFloat will convert the value to a number (float64). ```plaintext 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) ToInteger ```go func (value Value) ToInteger() (int64, error) ``` ToInteger will convert the value to a number (int64). ```plaintext 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) ToString ```go func (value Value) ToString() (string, error) ``` ToString will convert the value to a string (string). ```plaintext 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. ================================================ FILE: array_test.go ================================================ package otto import ( "testing" ) func TestArray(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = [ undefined, "Nothing happens." ]; abc.length; `, 2) test(` abc = ""+[0, 1, 2, 3]; def = [].toString(); ghi = [null, 4, "null"].toString(); [ abc, def, ghi ]; `, "0,1,2,3,,,4,null") test(`new Array(0).length`, 0) test(`new Array(11).length`, 11) test(`new Array(11, 1).length`, 2) test(` abc = [0, 1, 2, 3]; abc.xyzzy = "Nothing happens."; delete abc[1]; var xyzzy = delete abc.xyzzy; [ abc, xyzzy, abc.xyzzy ]; `, "0,,2,3,true,") test(` var abc = [0, 1, 2, 3, 4]; abc.length = 2; abc; `, "0,1") test(`raise: [].length = 3.14159; `, "RangeError") test(`raise: new Array(3.14159); `, "RangeError") test(` Object.defineProperty(Array.prototype, "0", { value: 100, writable: false, configurable: true }); abc = [101]; abc.hasOwnProperty("0") && abc[0] === 101; `, true) test(` abc = [,,undefined]; [ abc.hasOwnProperty(0), abc.hasOwnProperty(1), abc.hasOwnProperty(2) ]; `, "false,false,true") test(` abc = Object.getOwnPropertyDescriptor(Array, "prototype"); [ [ typeof Array.prototype ], [ abc.writable, abc.enumerable, abc.configurable ] ]; `, "object,false,false,false") }) } func TestArray_toString(t *testing.T) { tt(t, func() { { test(` Array.prototype.toString = function() { return "Nothing happens."; } abc = Array.prototype.toString(); def = [].toString(); ghi = [null, 4, "null"].toString(); [ abc, def, ghi ].join(","); `, "Nothing happens.,Nothing happens.,Nothing happens.") } { test(` Array.prototype.join = undefined abc = Array.prototype.toString() def = [].toString() ghi = [null, 4, "null"].toString() abc + "," + def + "," + ghi; `, "[object Array],[object Array],[object Array]") } }) } func TestArray_toLocaleString(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` [ 3.14159, "abc", undefined, new Date(0) ].toLocaleString(); `, "3.142,abc,,1970-01-01 00:00:00") test(`raise: [ { toLocaleString: undefined } ].toLocaleString(); `, `TypeError: Array.toLocaleString index[0] "undefined" is not callable`) }) } func TestArray_concat(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = [0, 1, 2]; def = [-1, -2, -3]; ghi = abc.concat(def); jkl = abc.concat(def, 3, 4, 5); mno = def.concat(-4, -5, abc); [ ghi, jkl, mno ].join(";"); `, "0,1,2,-1,-2,-3;0,1,2,-1,-2,-3,3,4,5;-1,-2,-3,-4,-5,0,1,2") test(` var abc = [,1]; var def = abc.concat([], [,]); def.getClass = Object.prototype.toString; [ def.getClass(), typeof def[0], def[1], typeof def[2], def.length ]; `, "[object Array],undefined,1,undefined,3") test(` Object.defineProperty(Array.prototype, "0", { value: 100, writable: false, configurable: true }); var abc = Array.prototype.concat.call(101); var hasProperty = abc.hasOwnProperty("0"); var instanceOfVerify = typeof abc[0] === "object"; var verifyValue = false; verifyValue = abc[0] == 101; var verifyEnumerable = false; for (var property in abc) { if (property === "0" && abc.hasOwnProperty("0")) { verifyEnumerable = true; } } var verifyWritable = false; abc[0] = 12; verifyWritable = abc[0] === 12; var verifyConfigurable = false; delete abc[0]; verifyConfigurable = abc.hasOwnProperty("0"); [ hasProperty, instanceOfVerify, verifyValue, !verifyConfigurable, verifyEnumerable, verifyWritable ]; `, "true,true,true,true,true,true") }) } func TestArray_splice(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = [0, 1, 2]; def = abc.splice(1, 2, 3, 4, 5); ghi = [].concat(abc); jkl = ghi.splice(17, 21, 7, 8, 9); mno = [].concat(abc); pqr = mno.splice(2); [ abc, def, ghi, jkl, mno, pqr ].join(";"); `, "0,3,4,5;1,2;0,3,4,5,7,8,9;;0,3;4,5") }) } func TestArray_shift(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = [0, 1, 2]; def = abc.shift(); ghi = [].concat(abc); jkl = abc.shift(); mno = [].concat(abc); pqr = abc.shift(); stu = [].concat(abc); vwx = abc.shift(); [ abc, def, ghi, jkl, mno, pqr, stu, vwx ].join(";"); `, ";0;1,2;1;2;2;;") }) } func TestArray_push(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = [0]; def = abc.push(1); ghi = [].concat(abc); jkl = abc.push(2,3,4); [ abc, def, ghi, jkl ].join(";"); `, "0,1,2,3,4;2;0,1;5") }) } func TestArray_pop(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = [0,1]; def = abc.pop(); ghi = [].concat(abc); jkl = abc.pop(); mno = [].concat(abc); pqr = abc.pop(); [ abc, def, ghi, jkl, mno, pqr ].join(";"); `, ";1;0;0;;") }) } func TestArray_slice(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = [0,1,2,3]; def = abc.slice(); ghi = abc.slice(1); jkl = abc.slice(3,-1); mno = abc.slice(2,-1); pqr = abc.slice(-1, -10); [ abc, def, ghi, jkl, mno, pqr ].join(";"); `, "0,1,2,3;0,1,2,3;1,2,3;;2;") // Array.protoype.slice is generic test(` abc = { 0: 0, 1: 1, 2: 2, 3: 3 }; abc.length = 4; def = Array.prototype.slice.call(abc); ghi = Array.prototype.slice.call(abc,1); jkl = Array.prototype.slice.call(abc,3,-1); mno = Array.prototype.slice.call(abc,2,-1); pqr = Array.prototype.slice.call(abc,-1,-10); [ abc, def, ghi, jkl, pqr ].join(";"); `, "[object Object];0,1,2,3;1,2,3;;") }) } func TestArray_sliceArguments(t *testing.T) { tt(t, func() { test, _ := test() test(` (function(){ return Array.prototype.slice.call(arguments, 1) })({}, 1, 2, 3); `, "1,2,3") }) } func TestArray_unshift(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = []; def = abc.unshift(0); ghi = [].concat(abc); jkl = abc.unshift(1,2,3,4); [ abc, def, ghi, jkl ].join(";"); `, "1,2,3,4,0;1;0;5") }) } func TestArray_reverse(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = [0,1,2,3].reverse(); def = [0,1,2].reverse(); [ abc, def ]; `, "3,2,1,0,2,1,0") }) } func TestArray_sort(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = [0,1,2,3].sort(); def = [3,2,1,0].sort(); ghi = [].sort(); jkl = [0].sort(); mno = [1,0].sort(); pqr = [1,5,-10, 100, 8, 72, 401, 0.05].sort(); stu = [1,5,-10, 100, 8, 72, 401, 0.05].sort(function(x, y){ return x == y ? 0 : x < y ? -1 : 1 }); vwx = [1,2,3,1,2,3].sort(); yza = [1,2,3,1,0,1,-1,0].sort(); [ abc, def, ghi, jkl, mno, pqr, stu, vwx, yza ].join(";"); `, "0,1,2,3;0,1,2,3;;0;0,1;-10,0.05,1,100,401,5,72,8;-10,0.05,1,5,8,72,100,401;1,1,2,2,3,3;-1,0,0,1,1,1,2,3") test(`Array.prototype.sort.length`, 1) }) } func TestArray_isArray(t *testing.T) { tt(t, func() { test, _ := test() test(` [ Array.isArray.length, Array.isArray(), Array.isArray([]), Array.isArray({}) ]; `, "1,false,true,false") test(`Array.isArray(Math)`, false) }) } func TestArray_indexOf(t *testing.T) { tt(t, func() { test, _ := test() test(`['a', 'b', 'c', 'b'].indexOf('b')`, 1) test(`['a', 'b', 'c', 'b'].indexOf('b', 2)`, 3) test(`['a', 'b', 'c', 'b'].indexOf('b', -2)`, 3) test(` Object.prototype.indexOf = Array.prototype.indexOf; var abc = {0: 'a', 1: 'b', 2: 'c', length: 3}; abc.indexOf('c'); `, 2) test(`[true].indexOf(true, "-Infinity")`, 0) test(` var target = {}; Math[3] = target; Math.length = 5; Array.prototype.indexOf.call(Math, target) === 3; `, true) test(` var _NaN = NaN; var abc = new Array("NaN", undefined, 0, false, null, {toString:function(){return NaN}}, "false", _NaN, NaN); abc.indexOf(NaN); `, -1) test(` var abc = {toString:function (){return 0}}; var def = 1; var ghi = -(4/3); var jkl = new Array(false, undefined, null, "0", abc, -1.3333333333333, "string", -0, true, +0, def, 1, 0, false, ghi, -(4/3)); [ jkl.indexOf(-(4/3)), jkl.indexOf(0), jkl.indexOf(-0), jkl.indexOf(1) ]; `, "14,7,7,10") }) } func TestArray_lastIndexOf(t *testing.T) { tt(t, func() { test, _ := test() test(`['a', 'b', 'c', 'b'].lastIndexOf('b')`, 3) test(`['a', 'b', 'c', 'b'].lastIndexOf('b', 2)`, 1) test(`['a', 'b', 'c', 'b'].lastIndexOf('b', -2)`, 1) test(` Object.prototype.lastIndexOf = Array.prototype.lastIndexOf; var abc = {0: 'a', 1: 'b', 2: 'c', 3: 'b', length: 4}; abc.lastIndexOf('b'); `, 3) test(` var target = {}; Math[3] = target; Math.length = 5; [ Array.prototype.lastIndexOf.call(Math, target) === 3 ]; `, "true") test(` var _NaN = NaN; var abc = new Array("NaN", undefined, 0, false, null, {toString:function(){return NaN}}, "false", _NaN, NaN); abc.lastIndexOf(NaN); `, -1) test(` var abc = {toString:function (){return 0}}; var def = 1; var ghi = -(4/3); var jkl = new Array(false, undefined, null, "0", abc, -1.3333333333333, "string", -0, true, +0, def, 1, 0, false, ghi, -(4/3)); [ jkl.lastIndexOf(-(4/3)), jkl.indexOf(0), jkl.indexOf(-0), jkl.indexOf(1) ]; `, "15,7,7,10") }) } func TestArray_every(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: [].every()`, `TypeError: Array.every argument "undefined" is not callable`) test(`raise: [].every("abc")`, `TypeError: Array.every argument "abc" is not callable`) test(`[].every(function() { return false })`, true) test(`[1,2,3].every(function() { return false })`, false) test(`[1,2,3].every(function() { return true })`, true) test(`[1,2,3].every(function(_, index) { if (index === 1) return true })`, false) test(` var abc = function(value, index, object) { return ('[object Math]' !== Object.prototype.toString.call(object)); }; Math.length = 1; Math[0] = 1; !Array.prototype.every.call(Math, abc); `, true) test(` var def = false; var abc = function(value, index, object) { def = true; return this === Math; }; [11].every(abc, Math) && def; `, true) test(` var def = false; var abc = function(value, index, object) { def = true; return Math; }; [11].every(abc) && def; `, true) }) } func TestArray_some(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: [].some("abc")`, `TypeError: Array.some "abc" if not callable`) test(`[].some(function() { return true })`, false) test(`[1,2,3].some(function() { return false })`, false) test(`[1,2,3].some(function() { return true })`, true) test(`[1,2,3].some(function(_, index) { if (index === 1) return true })`, true) test(` var abc = function(value, index, object) { return ('[object Math]' !== Object.prototype.toString.call(object)); }; Math.length = 1; Math[0] = 1; !Array.prototype.some.call(Math, abc); `, true) test(` var abc = function(value, index, object) { return this === Math; }; [11].some(abc, Math); `, true) test(` var abc = function(value, index, object) { return Math; }; [11].some(abc); `, true) }) } func TestArray_forEach(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: [].forEach("abc")`, `TypeError: Array.foreach "abc" if not callable`) test(` var abc = 0; [].forEach(function(value) { abc += value; }); abc; `, 0) test(` abc = 0; var def = []; [1,2,3].forEach(function(value, index) { abc += value; def.push(index); }); [ abc, def ]; `, "6,0,1,2") test(` var def = false; var abc = function(value, index, object) { def = ('[object Math]' === Object.prototype.toString.call(object)); }; Math.length = 1; Math[0] = 1; Array.prototype.forEach.call(Math, abc); def; `, true) test(` var def = false; var abc = function(value, index, object) { def = this === Math; }; [11].forEach(abc, Math); def; `, true) }) } func TestArray_indexing(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = new Array(0, 1); var def = abc.length; abc[4294967296] = 10; // 2^32 => 0 abc[4294967297] = 11; // 2^32+1 => 1 [ def, abc.length, abc[0], abc[1], abc[4294967296] ]; `, "2,2,0,1,10") test(` abc = new Array(0, 1); def = abc.length; abc[4294967295] = 10; var ghi = abc.length; abc[4294967299] = 12; var jkl = abc.length; abc[4294967294] = 11; [ def, ghi, jkl, abc.length, abc[4294967295], abc[4294967299] ]; `, "2,2,2,4294967295,10,12") }) } func TestArray_map(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: [].map("abc")`, `TypeError: Array.foreach "abc" if not callable`) test(`[].map(function() { return 1 }).length`, 0) test(`[1,2,3].map(function(value) { return value * value })`, "1,4,9") test(`[1,2,3].map(function(value) { return 1 })`, "1,1,1") test(` var abc = function(value, index, object) { return ('[object Math]' === Object.prototype.toString.call(object)); }; Math.length = 1; Math[0] = 1; Array.prototype.map.call(Math, abc)[0]; `, true) test(` var abc = function(value, index, object) { return this === Math; }; [11].map(abc, Math)[0]; `, true) }) } func TestArray_filter(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: [].filter("abc")`, `TypeError: Array.filter "abc" if not callable`) test(`[].filter(function() { return 1 }).length`, 0) test(`[1,2,3].filter(function() { return false }).length`, 0) test(`[1,2,3].filter(function() { return true })`, "1,2,3") }) } func TestArray_reduce(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: [].reduce("abc")`, `TypeError: Array.reduce "abc" if not callable`) test(`raise: [].reduce(function() {})`, `TypeError: Array.reduce "function() {}" if not callable`) test(`[].reduce(function() {}, 0)`, 0) test(`[].reduce(function() {}, undefined)`, "undefined") test(`['a','b','c'].reduce(function(result, value) { return result+', '+value })`, "a, b, c") test(`[1,2,3].reduce(function(result, value) { return result + value }, 4)`, 10) test(`[1,2,3].reduce(function(result, value) { return result + value })`, 6) }) } func TestArray_reduceRight(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: [].reduceRight("abc")`, `TypeError: Array.reduceRight "abc" if not callable`) test(`raise: [].reduceRight(function() {})`, `TypeError: Array.reduceRight "function() {}" if not callable`) test(`[].reduceRight(function() {}, 0)`, 0) test(`[].reduceRight(function() {}, undefined)`, "undefined") test(`['a','b','c'].reduceRight(function(result, value) { return result+', '+value })`, "c, b, a") test(`[1,2,3].reduceRight(function(result, value) { return result + value }, 4)`, 10) test(`[1,2,3].reduceRight(function(result, value) { return result + value })`, 6) }) } func TestArray_defineOwnProperty(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = []; Object.defineProperty(abc, "length", { writable: false }); abc.length; `, 0) test(`raise: var abc = []; var exception; Object.defineProperty(abc, "length", { writable: false }); Object.defineProperty(abc, "length", { writable: true }); `, `TypeError: Object.DefineOwnProperty: property not configurable or writeable and descriptor not writeable`) }) } func TestArray_new(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = new Array(null); var def = new Array(undefined); [ abc.length, abc[0] === null, def.length, def[0] === undefined ] `, "1,true,1,true") test(` var abc = new Array(new Number(0)); var def = new Array(new Number(4294967295)); [ abc.length, typeof abc[0], abc[0] == 0, def.length, typeof def[0], def[0] == 4294967295 ] `, "1,object,true,1,object,true") }) } ================================================ FILE: ast/comments.go ================================================ package ast import ( "fmt" "github.com/robertkrimen/otto/file" ) // CommentPosition determines where the comment is in a given context. type CommentPosition int // Available comment positions. const ( _ CommentPosition = iota // LEADING is before the pertinent expression. LEADING // TRAILING is after the pertinent expression. TRAILING // KEY is before a key in an object. KEY // COLON is after a colon in a field declaration. COLON // FINAL is the final comments in a block, not belonging to a specific expression or the comment after a trailing , in an array or object literal. FINAL // IF is after an if keyword. IF // WHILE is after a while keyword. WHILE // DO is after do keyword. DO // FOR is after a for keyword. FOR // WITH is after a with keyword. WITH // TBD is unknown. TBD ) // Comment contains the data of the comment. type Comment struct { Text string Begin file.Idx Position CommentPosition } // NewComment creates a new comment. func NewComment(text string, idx file.Idx) *Comment { comment := &Comment{ Begin: idx, Text: text, Position: TBD, } return comment } // String returns a stringified version of the position. func (cp CommentPosition) String() string { switch cp { case LEADING: return "Leading" case TRAILING: return "Trailing" case KEY: return "Key" case COLON: return "Colon" case FINAL: return "Final" case IF: return "If" case WHILE: return "While" case DO: return "Do" case FOR: return "For" case WITH: return "With" default: return "???" } } // String returns a stringified version of the comment. func (c Comment) String() string { return fmt.Sprintf("Comment: %v", c.Text) } // Comments defines the current view of comments from the parser. type Comments struct { // CommentMap is a reference to the parser comment map CommentMap CommentMap // Comments lists the comments scanned, not linked to a node yet Comments []*Comment // Current is node for which comments are linked to Current Expression // future lists the comments after a line break during a sequence of comments future []*Comment // wasLineBreak determines if a line break occurred while scanning for comments wasLineBreak bool // primary determines whether or not processing a primary expression primary bool // afterBlock determines whether or not being after a block statement afterBlock bool } // NewComments returns a new Comments. func NewComments() *Comments { comments := &Comments{ CommentMap: CommentMap{}, } return comments } func (c *Comments) String() string { return fmt.Sprintf("NODE: %v, Comments: %v, Future: %v(LINEBREAK:%v)", c.Current, len(c.Comments), len(c.future), c.wasLineBreak) } // FetchAll returns all the currently scanned comments, // including those from the next line. func (c *Comments) FetchAll() []*Comment { defer func() { c.Comments = nil c.future = nil }() return append(c.Comments, c.future...) } // Fetch returns all the currently scanned comments. func (c *Comments) Fetch() []*Comment { defer func() { c.Comments = nil }() return c.Comments } // ResetLineBreak marks the beginning of a new statement. func (c *Comments) ResetLineBreak() { c.wasLineBreak = false } // MarkPrimary will mark the context as processing a primary expression. func (c *Comments) MarkPrimary() { c.primary = true c.wasLineBreak = false } // AfterBlock will mark the context as being after a block. func (c *Comments) AfterBlock() { c.afterBlock = true } // AddComment adds a comment to the view. // Depending on the context, comments are added normally or as post line break. func (c *Comments) AddComment(comment *Comment) { if c.primary { if !c.wasLineBreak { c.Comments = append(c.Comments, comment) } else { c.future = append(c.future, comment) } } else { if !c.wasLineBreak || (c.Current == nil && !c.afterBlock) { c.Comments = append(c.Comments, comment) } else { c.future = append(c.future, comment) } } } // MarkComments will mark the found comments as the given position. func (c *Comments) MarkComments(position CommentPosition) { for _, comment := range c.Comments { if comment.Position == TBD { comment.Position = position } } for _, c := range c.future { if c.Position == TBD { c.Position = position } } } // Unset the current node and apply the comments to the current expression. // Resets context variables. func (c *Comments) Unset() { if c.Current != nil { c.applyComments(c.Current, c.Current, TRAILING) c.Current = nil } c.wasLineBreak = false c.primary = false c.afterBlock = false } // SetExpression sets the current expression. // It is applied the found comments, unless the previous expression has not been unset. // It is skipped if the node is already set or if it is a part of the previous node. func (c *Comments) SetExpression(node Expression) { // Skipping same node if c.Current == node { return } if c.Current != nil && c.Current.Idx1() == node.Idx1() { c.Current = node return } previous := c.Current c.Current = node // Apply the found comments and futures to the node and the previous. c.applyComments(node, previous, TRAILING) } // PostProcessNode applies all found comments to the given node. func (c *Comments) PostProcessNode(node Node) { c.applyComments(node, nil, TRAILING) } // applyComments applies both the comments and the future comments to the given node and the previous one, // based on the context. func (c *Comments) applyComments(node, previous Node, position CommentPosition) { if previous != nil { c.CommentMap.AddComments(previous, c.Comments, position) c.Comments = nil } else { c.CommentMap.AddComments(node, c.Comments, position) c.Comments = nil } // Only apply the future comments to the node if the previous is set. // This is for detecting end of line comments and which node comments on the following lines belongs to if previous != nil { c.CommentMap.AddComments(node, c.future, position) c.future = nil } } // AtLineBreak will mark a line break. func (c *Comments) AtLineBreak() { c.wasLineBreak = true } // CommentMap is the data structure where all found comments are stored. type CommentMap map[Node][]*Comment // AddComment adds a single comment to the map. func (cm CommentMap) AddComment(node Node, comment *Comment) { list := cm[node] list = append(list, comment) cm[node] = list } // AddComments adds a slice of comments, given a node and an updated position. func (cm CommentMap) AddComments(node Node, comments []*Comment, position CommentPosition) { for _, comment := range comments { if comment.Position == TBD { comment.Position = position } cm.AddComment(node, comment) } } // Size returns the size of the map. func (cm CommentMap) Size() int { size := 0 for _, comments := range cm { size += len(comments) } return size } // MoveComments moves comments with a given position from a node to another. func (cm CommentMap) MoveComments(from, to Node, position CommentPosition) { for i, c := range cm[from] { if c.Position == position { cm.AddComment(to, c) // Remove the comment from the "from" slice cm[from][i] = cm[from][len(cm[from])-1] cm[from][len(cm[from])-1] = nil cm[from] = cm[from][:len(cm[from])-1] } } } ================================================ FILE: ast/comments_test.go ================================================ package ast import ( "testing" "github.com/robertkrimen/otto/file" ) func TestCommentMap(t *testing.T) { statement := &EmptyStatement{file.Idx(1)} comment := &Comment{Begin: 1, Text: "test", Position: LEADING} cm := CommentMap{} cm.AddComment(statement, comment) if cm.Size() != 1 { t.Errorf("the number of comments is %v, not 1", cm.Size()) } if len(cm[statement]) != 1 { t.Errorf("the number of comments is %v, not 1", cm.Size()) } if cm[statement][0].Text != "test" { t.Errorf("the text is %v, not \"test\"", cm[statement][0].Text) } } func TestCommentMap_move(t *testing.T) { statement1 := &EmptyStatement{file.Idx(1)} statement2 := &EmptyStatement{file.Idx(2)} comment := &Comment{Begin: 1, Text: "test", Position: LEADING} cm := CommentMap{} cm.AddComment(statement1, comment) if cm.Size() != 1 { t.Errorf("the number of comments is %v, not 1", cm.Size()) } if len(cm[statement1]) != 1 { t.Errorf("the number of comments is %v, not 1", cm.Size()) } if len(cm[statement2]) != 0 { t.Errorf("the number of comments is %v, not 0", cm.Size()) } cm.MoveComments(statement1, statement2, LEADING) if cm.Size() != 1 { t.Errorf("the number of comments is %v, not 1", cm.Size()) } if len(cm[statement2]) != 1 { t.Errorf("the number of comments is %v, not 1", cm.Size()) } if len(cm[statement1]) != 0 { t.Errorf("the number of comments is %v, not 0", cm.Size()) } } ================================================ FILE: ast/node.go ================================================ // Package ast declares types representing a JavaScript AST. // // # 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 import ( "github.com/robertkrimen/otto/file" "github.com/robertkrimen/otto/token" ) // Node is implemented by types that represent a node. type Node interface { Idx0() file.Idx // The index of the first character belonging to the node Idx1() file.Idx // The index of the first character immediately after the node } // Expression is implemented by types that represent an Expression. type Expression interface { Node expression() } // ArrayLiteral represents an array literal. type ArrayLiteral struct { Value []Expression LeftBracket file.Idx RightBracket file.Idx } // Idx0 implements Node. func (al *ArrayLiteral) Idx0() file.Idx { return al.LeftBracket } // Idx1 implements Node. func (al *ArrayLiteral) Idx1() file.Idx { return al.RightBracket + 1 } // expression implements Expression. func (*ArrayLiteral) expression() {} // AssignExpression represents an assignment expression. type AssignExpression struct { Left Expression Right Expression Operator token.Token } // Idx0 implements Node. func (ae *AssignExpression) Idx0() file.Idx { return ae.Left.Idx0() } // Idx1 implements Node. func (ae *AssignExpression) Idx1() file.Idx { return ae.Right.Idx1() } // expression implements Expression. func (*AssignExpression) expression() {} // BadExpression represents a bad expression. type BadExpression struct { From file.Idx To file.Idx } // Idx0 implements Node. func (be *BadExpression) Idx0() file.Idx { return be.From } // Idx1 implements Node. func (be *BadExpression) Idx1() file.Idx { return be.To } // expression implements Expression. func (*BadExpression) expression() {} // BinaryExpression represents a binary expression. type BinaryExpression struct { Left Expression Right Expression Operator token.Token Comparison bool } // Idx0 implements Node. func (be *BinaryExpression) Idx0() file.Idx { return be.Left.Idx0() } // Idx1 implements Node. func (be *BinaryExpression) Idx1() file.Idx { return be.Right.Idx1() } // expression implements Expression. func (*BinaryExpression) expression() {} // BooleanLiteral represents a boolean expression. type BooleanLiteral struct { Literal string Idx file.Idx Value bool } // Idx0 implements Node. func (bl *BooleanLiteral) Idx0() file.Idx { return bl.Idx } // Idx1 implements Node. func (bl *BooleanLiteral) Idx1() file.Idx { return file.Idx(int(bl.Idx) + len(bl.Literal)) } // expression implements Expression. func (*BooleanLiteral) expression() {} // BracketExpression represents a bracketed expression. type BracketExpression struct { Left Expression Member Expression LeftBracket file.Idx RightBracket file.Idx } // Idx0 implements Node. func (be *BracketExpression) Idx0() file.Idx { return be.Left.Idx0() } // Idx1 implements Node. func (be *BracketExpression) Idx1() file.Idx { return be.RightBracket + 1 } // expression implements Expression. func (*BracketExpression) expression() {} // CallExpression represents a call expression. type CallExpression struct { Callee Expression ArgumentList []Expression LeftParenthesis file.Idx RightParenthesis file.Idx } // Idx0 implements Node. func (ce *CallExpression) Idx0() file.Idx { return ce.Callee.Idx0() } // Idx1 implements Node. func (ce *CallExpression) Idx1() file.Idx { return ce.RightParenthesis + 1 } // expression implements Expression. func (*CallExpression) expression() {} // ConditionalExpression represents a conditional expression. type ConditionalExpression struct { Test Expression Consequent Expression Alternate Expression } // Idx0 implements Node. func (ce *ConditionalExpression) Idx0() file.Idx { return ce.Test.Idx0() } // Idx1 implements Node. func (ce *ConditionalExpression) Idx1() file.Idx { return ce.Alternate.Idx1() } // expression implements Expression. func (*ConditionalExpression) expression() {} // DotExpression represents a dot expression. type DotExpression struct { Left Expression Identifier *Identifier } // Idx0 implements Node. func (de *DotExpression) Idx0() file.Idx { return de.Left.Idx0() } // Idx1 implements Node. func (de *DotExpression) Idx1() file.Idx { return de.Identifier.Idx1() } // expression implements Expression. func (*DotExpression) expression() {} // EmptyExpression represents an empty expression. type EmptyExpression struct { Begin file.Idx End file.Idx } // Idx0 implements Node. func (ee *EmptyExpression) Idx0() file.Idx { return ee.Begin } // Idx1 implements Node. func (ee *EmptyExpression) Idx1() file.Idx { return ee.End } // expression implements Expression. func (*EmptyExpression) expression() {} // FunctionLiteral represents a function literal. type FunctionLiteral struct { Body Statement Name *Identifier ParameterList *ParameterList Source string DeclarationList []Declaration Function file.Idx } // Idx0 implements Node. func (fl *FunctionLiteral) Idx0() file.Idx { return fl.Function } // Idx1 implements Node. func (fl *FunctionLiteral) Idx1() file.Idx { return fl.Body.Idx1() } // expression implements Expression. func (*FunctionLiteral) expression() {} // Identifier represents an identifier. type Identifier struct { Name string Idx file.Idx } // Idx0 implements Node. func (i *Identifier) Idx0() file.Idx { return i.Idx } // Idx1 implements Node. func (i *Identifier) Idx1() file.Idx { return file.Idx(int(i.Idx) + len(i.Name)) } // expression implements Expression. func (*Identifier) expression() {} // NewExpression represents a new expression. type NewExpression struct { Callee Expression ArgumentList []Expression New file.Idx LeftParenthesis file.Idx RightParenthesis file.Idx } // Idx0 implements Node. func (ne *NewExpression) Idx0() file.Idx { return ne.New } // Idx1 implements Node. func (ne *NewExpression) Idx1() file.Idx { if ne.RightParenthesis > 0 { return ne.RightParenthesis + 1 } return ne.Callee.Idx1() } // expression implements Expression. func (*NewExpression) expression() {} // NullLiteral represents a null literal. type NullLiteral struct { Literal string Idx file.Idx } // Idx0 implements Node. func (nl *NullLiteral) Idx0() file.Idx { return nl.Idx } // Idx1 implements Node. func (nl *NullLiteral) Idx1() file.Idx { return file.Idx(int(nl.Idx) + 4) } // expression implements Expression. func (*NullLiteral) expression() {} // NumberLiteral represents a number literal. type NumberLiteral struct { Value interface{} Literal string Idx file.Idx } // Idx0 implements Node. func (nl *NumberLiteral) Idx0() file.Idx { return nl.Idx } // Idx1 implements Node. func (nl *NumberLiteral) Idx1() file.Idx { return file.Idx(int(nl.Idx) + len(nl.Literal)) } // expression implements Expression. func (*NumberLiteral) expression() {} // ObjectLiteral represents an object literal. type ObjectLiteral struct { Value []Property LeftBrace file.Idx RightBrace file.Idx } // Idx0 implements Node. func (ol *ObjectLiteral) Idx0() file.Idx { return ol.LeftBrace } // Idx1 implements Node. func (ol *ObjectLiteral) Idx1() file.Idx { return ol.RightBrace + 1 } // expression implements Expression. func (*ObjectLiteral) expression() {} // ParameterList represents a parameter list. type ParameterList struct { List []*Identifier Opening file.Idx Closing file.Idx } // Property represents a property. type Property struct { Value Expression Key string Kind string } // RegExpLiteral represents a regular expression literal. type RegExpLiteral struct { Literal string Pattern string Flags string Value string Idx file.Idx } // Idx0 implements Node. func (rl *RegExpLiteral) Idx0() file.Idx { return rl.Idx } // Idx1 implements Node. func (rl *RegExpLiteral) Idx1() file.Idx { return file.Idx(int(rl.Idx) + len(rl.Literal)) } // expression implements Expression. func (*RegExpLiteral) expression() {} // SequenceExpression represents a sequence literal. type SequenceExpression struct { Sequence []Expression } // Idx0 implements Node. func (se *SequenceExpression) Idx0() file.Idx { return se.Sequence[0].Idx0() } // Idx1 implements Node. func (se *SequenceExpression) Idx1() file.Idx { return se.Sequence[len(se.Sequence)-1].Idx1() } // expression implements Expression. func (*SequenceExpression) expression() {} // StringLiteral represents a string literal. type StringLiteral struct { Literal string Value string Idx file.Idx } // Idx0 implements Node. func (sl *StringLiteral) Idx0() file.Idx { return sl.Idx } // Idx1 implements Node. func (sl *StringLiteral) Idx1() file.Idx { return file.Idx(int(sl.Idx) + len(sl.Literal)) } // expression implements Expression. func (*StringLiteral) expression() {} // ThisExpression represents a this expression. type ThisExpression struct { Idx file.Idx } // Idx0 implements Node. func (te *ThisExpression) Idx0() file.Idx { return te.Idx } // Idx1 implements Node. func (te *ThisExpression) Idx1() file.Idx { return te.Idx + 4 } // expression implements Expression. func (*ThisExpression) expression() {} // UnaryExpression represents a unary expression. type UnaryExpression struct { Operand Expression Operator token.Token Idx file.Idx Postfix bool } // Idx0 implements Node. func (ue *UnaryExpression) Idx0() file.Idx { if ue.Postfix { return ue.Operand.Idx0() } return ue.Idx } // Idx1 implements Node. func (ue *UnaryExpression) Idx1() file.Idx { if ue.Postfix { return ue.Operand.Idx1() + 2 // ++ -- } return ue.Operand.Idx1() } // expression implements Expression. func (*UnaryExpression) expression() {} // VariableExpression represents a variable expression. type VariableExpression struct { Initializer Expression Name string Idx file.Idx } // Idx0 implements Node. func (ve *VariableExpression) Idx0() file.Idx { return ve.Idx } // Idx1 implements Node. func (ve *VariableExpression) Idx1() file.Idx { if ve.Initializer == nil { return file.Idx(int(ve.Idx) + len(ve.Name)) } return ve.Initializer.Idx1() } // expression implements Expression. func (*VariableExpression) expression() {} // Statement is implemented by types which represent a statement. type Statement interface { Node statement() } // BadStatement represents a bad statement. type BadStatement struct { From file.Idx To file.Idx } // Idx0 implements Node. func (bs *BadStatement) Idx0() file.Idx { return bs.From } // Idx1 implements Node. func (bs *BadStatement) Idx1() file.Idx { return bs.To } // expression implements Statement. func (*BadStatement) statement() {} // BlockStatement represents a block statement. type BlockStatement struct { List []Statement LeftBrace file.Idx RightBrace file.Idx } // Idx0 implements Node. func (bs *BlockStatement) Idx0() file.Idx { return bs.LeftBrace } // Idx1 implements Node. func (bs *BlockStatement) Idx1() file.Idx { return bs.RightBrace + 1 } // expression implements Statement. func (*BlockStatement) statement() {} // BranchStatement represents a branch statement. type BranchStatement struct { Label *Identifier Idx file.Idx Token token.Token } // Idx0 implements Node. func (bs *BranchStatement) Idx0() file.Idx { return bs.Idx } // Idx1 implements Node. func (bs *BranchStatement) Idx1() file.Idx { if bs.Label == nil { return file.Idx(int(bs.Idx) + len(bs.Token.String())) } return bs.Label.Idx1() } // expression implements Statement. func (*BranchStatement) statement() {} // CaseStatement represents a case statement. type CaseStatement struct { Test Expression Consequent []Statement Case file.Idx } // Idx0 implements Node. func (cs *CaseStatement) Idx0() file.Idx { return cs.Case } // Idx1 implements Node. func (cs *CaseStatement) Idx1() file.Idx { return cs.Consequent[len(cs.Consequent)-1].Idx1() } // expression implements Statement. func (*CaseStatement) statement() {} // CatchStatement represents a catch statement. type CatchStatement struct { Body Statement Parameter *Identifier Catch file.Idx } // Idx0 implements Node. func (cs *CatchStatement) Idx0() file.Idx { return cs.Catch } // Idx1 implements Node. func (cs *CatchStatement) Idx1() file.Idx { return cs.Body.Idx1() } // expression implements Statement. func (*CatchStatement) statement() {} // DebuggerStatement represents a debugger statement. type DebuggerStatement struct { Debugger file.Idx } // Idx0 implements Node. func (ds *DebuggerStatement) Idx0() file.Idx { return ds.Debugger } // Idx1 implements Node. func (ds *DebuggerStatement) Idx1() file.Idx { return ds.Debugger + 8 } // expression implements Statement. func (*DebuggerStatement) statement() {} // DoWhileStatement represents a do while statement. type DoWhileStatement struct { Test Expression Body Statement Do file.Idx RightParenthesis file.Idx } // Idx0 implements Node. func (dws *DoWhileStatement) Idx0() file.Idx { return dws.Do } // Idx1 implements Node. func (dws *DoWhileStatement) Idx1() file.Idx { return dws.RightParenthesis + 1 } // expression implements Statement. func (*DoWhileStatement) statement() {} // EmptyStatement represents a empty statement. type EmptyStatement struct { Semicolon file.Idx } // Idx0 implements Node. func (es *EmptyStatement) Idx0() file.Idx { return es.Semicolon } // Idx1 implements Node. func (es *EmptyStatement) Idx1() file.Idx { return es.Semicolon + 1 } // expression implements Statement. func (*EmptyStatement) statement() {} // ExpressionStatement represents a expression statement. type ExpressionStatement struct { Expression Expression } // Idx0 implements Node. func (es *ExpressionStatement) Idx0() file.Idx { return es.Expression.Idx0() } // Idx1 implements Node. func (es *ExpressionStatement) Idx1() file.Idx { return es.Expression.Idx1() } // expression implements Statement. func (*ExpressionStatement) statement() {} // ForInStatement represents a for in statement. type ForInStatement struct { Into Expression Source Expression Body Statement For file.Idx } // Idx0 implements Node. func (fis *ForInStatement) Idx0() file.Idx { return fis.For } // Idx1 implements Node. func (fis *ForInStatement) Idx1() file.Idx { return fis.Body.Idx1() } // expression implements Statement. func (*ForInStatement) statement() {} // ForStatement represents a for statement. type ForStatement struct { Initializer Expression Update Expression Test Expression Body Statement For file.Idx } // Idx0 implements Node. func (fs *ForStatement) Idx0() file.Idx { return fs.For } // Idx1 implements Node. func (fs *ForStatement) Idx1() file.Idx { return fs.Body.Idx1() } // expression implements Statement. func (*ForStatement) statement() {} // FunctionStatement represents a function statement. type FunctionStatement struct { Function *FunctionLiteral } // Idx0 implements Node. func (fs *FunctionStatement) Idx0() file.Idx { return fs.Function.Idx0() } // Idx1 implements Node. func (fs *FunctionStatement) Idx1() file.Idx { return fs.Function.Idx1() } // expression implements Statement. func (*FunctionStatement) statement() {} // IfStatement represents a if statement. type IfStatement struct { Test Expression Consequent Statement Alternate Statement If file.Idx } // Idx0 implements Node. func (is *IfStatement) Idx0() file.Idx { return is.If } // Idx1 implements Node. func (is *IfStatement) Idx1() file.Idx { if is.Alternate != nil { return is.Alternate.Idx1() } return is.Consequent.Idx1() } // expression implements Statement. func (*IfStatement) statement() {} // LabelledStatement represents a labelled statement. type LabelledStatement struct { Statement Statement Label *Identifier Colon file.Idx } // Idx0 implements Node. func (ls *LabelledStatement) Idx0() file.Idx { return ls.Label.Idx0() } // Idx1 implements Node. func (ls *LabelledStatement) Idx1() file.Idx { return ls.Statement.Idx1() } // expression implements Statement. func (*LabelledStatement) statement() {} // ReturnStatement represents a return statement. type ReturnStatement struct { Argument Expression Return file.Idx } // Idx0 implements Node. func (rs *ReturnStatement) Idx0() file.Idx { return rs.Return } // Idx1 implements Node. func (rs *ReturnStatement) Idx1() file.Idx { if rs.Argument != nil { return rs.Argument.Idx1() } return rs.Return + 6 } // expression implements Statement. func (*ReturnStatement) statement() {} // SwitchStatement represents a switch statement. type SwitchStatement struct { Discriminant Expression Body []*CaseStatement Switch file.Idx Default int RightBrace file.Idx } // Idx0 implements Node. func (ss *SwitchStatement) Idx0() file.Idx { return ss.Switch } // Idx1 implements Node. func (ss *SwitchStatement) Idx1() file.Idx { return ss.RightBrace + 1 } // expression implements Statement. func (*SwitchStatement) statement() {} // ThrowStatement represents a throw statement. type ThrowStatement struct { Argument Expression Throw file.Idx } // Idx0 implements Node. func (ts *ThrowStatement) Idx0() file.Idx { return ts.Throw } // Idx1 implements Node. func (ts *ThrowStatement) Idx1() file.Idx { return ts.Argument.Idx1() } // expression implements Statement. func (*ThrowStatement) statement() {} // TryStatement represents a try statement. type TryStatement struct { Body Statement Finally Statement Catch *CatchStatement Try file.Idx } // Idx0 implements Node. func (ts *TryStatement) Idx0() file.Idx { return ts.Try } // Idx1 implements Node. func (ts *TryStatement) Idx1() file.Idx { if ts.Finally != nil { return ts.Finally.Idx1() } return ts.Catch.Idx1() } // expression implements Statement. func (*TryStatement) statement() {} // VariableStatement represents a variable statement. type VariableStatement struct { List []Expression Var file.Idx } // Idx0 implements Node. func (vs *VariableStatement) Idx0() file.Idx { return vs.Var } // Idx1 implements Node. func (vs *VariableStatement) Idx1() file.Idx { return vs.List[len(vs.List)-1].Idx1() } // expression implements Statement. func (*VariableStatement) statement() {} // WhileStatement represents a while statement. type WhileStatement struct { Test Expression Body Statement While file.Idx } // Idx0 implements Node. func (ws *WhileStatement) Idx0() file.Idx { return ws.While } // Idx1 implements Node. func (ws *WhileStatement) Idx1() file.Idx { return ws.Body.Idx1() } // expression implements Statement. func (*WhileStatement) statement() {} // WithStatement represents a with statement. type WithStatement struct { Object Expression Body Statement With file.Idx } // Idx0 implements Node. func (ws *WithStatement) Idx0() file.Idx { return ws.With } // Idx1 implements Node. func (ws *WithStatement) Idx1() file.Idx { return ws.Body.Idx1() } // expression implements Statement. func (*WithStatement) statement() {} // Declaration is implemented by type which represent declarations. type Declaration interface { declaration() } // FunctionDeclaration represents a function declaration. type FunctionDeclaration struct { Function *FunctionLiteral } func (*FunctionDeclaration) declaration() {} // VariableDeclaration represents a variable declaration. type VariableDeclaration struct { List []*VariableExpression Var file.Idx } // declaration implements Declaration. func (*VariableDeclaration) declaration() {} // Program represents a full program. type Program struct { File *file.File Comments CommentMap Body []Statement DeclarationList []Declaration } // Idx0 implements Node. func (p *Program) Idx0() file.Idx { return p.Body[0].Idx0() } // Idx1 implements Node. func (p *Program) Idx1() file.Idx { return p.Body[len(p.Body)-1].Idx1() } ================================================ FILE: ast/walk.go ================================================ package ast import "fmt" // Visitor Enter method is invoked for each node encountered by Walk. // If the result visitor w is not nil, Walk visits each of the children // of node with the visitor v, followed by a call of the Exit method. type Visitor interface { Enter(n Node) (v Visitor) Exit(n Node) } // Walk traverses an AST in depth-first order: It starts by calling // v.Enter(node); node must not be nil. If the visitor v returned by // v.Enter(node) is not nil, Walk is invoked recursively with visitor // v for each of the non-nil children of node, followed by a call // of v.Exit(node). func Walk(v Visitor, n Node) { if n == nil { return } if v = v.Enter(n); v == nil { return } defer v.Exit(n) switch n := n.(type) { case *ArrayLiteral: if n != nil { for _, ex := range n.Value { Walk(v, ex) } } case *AssignExpression: if n != nil { Walk(v, n.Left) Walk(v, n.Right) } case *BadExpression: case *BadStatement: case *BinaryExpression: if n != nil { Walk(v, n.Left) Walk(v, n.Right) } case *BlockStatement: if n != nil { for _, s := range n.List { Walk(v, s) } } case *BooleanLiteral: case *BracketExpression: if n != nil { Walk(v, n.Left) Walk(v, n.Member) } case *BranchStatement: if n != nil { Walk(v, n.Label) } case *CallExpression: if n != nil { Walk(v, n.Callee) for _, a := range n.ArgumentList { Walk(v, a) } } case *CaseStatement: if n != nil { Walk(v, n.Test) for _, c := range n.Consequent { Walk(v, c) } } case *CatchStatement: if n != nil { Walk(v, n.Parameter) Walk(v, n.Body) } case *ConditionalExpression: if n != nil { Walk(v, n.Test) Walk(v, n.Consequent) Walk(v, n.Alternate) } case *DebuggerStatement: case *DoWhileStatement: if n != nil { Walk(v, n.Test) Walk(v, n.Body) } case *DotExpression: if n != nil { Walk(v, n.Left) Walk(v, n.Identifier) } case *EmptyExpression: case *EmptyStatement: case *ExpressionStatement: if n != nil { Walk(v, n.Expression) } case *ForInStatement: if n != nil { Walk(v, n.Into) Walk(v, n.Source) Walk(v, n.Body) } case *ForStatement: if n != nil { Walk(v, n.Initializer) Walk(v, n.Update) Walk(v, n.Test) Walk(v, n.Body) } case *FunctionLiteral: if n != nil { Walk(v, n.Name) for _, p := range n.ParameterList.List { Walk(v, p) } Walk(v, n.Body) } case *FunctionStatement: if n != nil { Walk(v, n.Function) } case *Identifier: case *IfStatement: if n != nil { Walk(v, n.Test) Walk(v, n.Consequent) Walk(v, n.Alternate) } case *LabelledStatement: if n != nil { Walk(v, n.Label) Walk(v, n.Statement) } case *NewExpression: if n != nil { Walk(v, n.Callee) for _, a := range n.ArgumentList { Walk(v, a) } } case *NullLiteral: case *NumberLiteral: case *ObjectLiteral: if n != nil { for _, p := range n.Value { Walk(v, p.Value) } } case *Program: if n != nil { for _, b := range n.Body { Walk(v, b) } } case *RegExpLiteral: case *ReturnStatement: if n != nil { Walk(v, n.Argument) } case *SequenceExpression: if n != nil { for _, e := range n.Sequence { Walk(v, e) } } case *StringLiteral: case *SwitchStatement: if n != nil { Walk(v, n.Discriminant) for _, c := range n.Body { Walk(v, c) } } case *ThisExpression: case *ThrowStatement: if n != nil { Walk(v, n.Argument) } case *TryStatement: if n != nil { Walk(v, n.Body) Walk(v, n.Catch) Walk(v, n.Finally) } case *UnaryExpression: if n != nil { Walk(v, n.Operand) } case *VariableExpression: if n != nil { Walk(v, n.Initializer) } case *VariableStatement: if n != nil { for _, e := range n.List { Walk(v, e) } } case *WhileStatement: if n != nil { Walk(v, n.Test) Walk(v, n.Body) } case *WithStatement: if n != nil { Walk(v, n.Object) Walk(v, n.Body) } default: panic(fmt.Sprintf("Walk: unexpected node type %T", n)) } } ================================================ FILE: ast/walk_example_test.go ================================================ package ast_test import ( "fmt" "log" "github.com/robertkrimen/otto/ast" "github.com/robertkrimen/otto/file" "github.com/robertkrimen/otto/parser" ) type walkExample struct { source string shift file.Idx } func (w *walkExample) Enter(n ast.Node) ast.Visitor { if id, ok := n.(*ast.Identifier); ok && id != nil { idx := n.Idx0() + w.shift - 1 s := w.source[:idx] + "new_" + w.source[idx:] w.source = s w.shift += 4 } if v, ok := n.(*ast.VariableExpression); ok && v != nil { idx := n.Idx0() + w.shift - 1 s := w.source[:idx] + "varnew_" + w.source[idx:] w.source = s w.shift += 7 } return w } func (w *walkExample) Exit(n ast.Node) { // AST node n has had all its children walked. Pop it out of your // stack, or do whatever processing you need to do, if any. } func ExampleVisitor_codeRewrite() { source := `var b = function() {test(); try {} catch(e) {} var test = "test(); var test = 1"} // test` program, err := parser.ParseFile(nil, "", source, 0) if err != nil { log.Fatal(err) } w := &walkExample{source: source} ast.Walk(w, program) fmt.Println(w.source) // Output: var varnew_b = function() {new_test(); try {} catch(new_e) {} var varnew_test = "test(); var test = 1"} // test } ================================================ FILE: ast/walk_test.go ================================================ package ast_test import ( "testing" "github.com/robertkrimen/otto/ast" "github.com/robertkrimen/otto/file" "github.com/robertkrimen/otto/parser" "github.com/stretchr/testify/require" ) type walker struct { seen map[ast.Node]struct{} source string stack []ast.Node shift file.Idx duplicate int newExpressionIdx1 file.Idx } // push and pop below are to prove the symmetry of Enter/Exit calls func (w *walker) push(n ast.Node) { w.stack = append(w.stack, n) } func (w *walker) pop(n ast.Node) { size := len(w.stack) if size <= 0 { panic("pop of empty stack") } if toPop := w.stack[size-1]; toPop != n { panic("pop: nodes do not equal") } w.stack[size-1] = nil w.stack = w.stack[:size-1] } func (w *walker) Enter(n ast.Node) ast.Visitor { w.push(n) if _, ok := w.seen[n]; ok { // Skip items we've already seen which occurs due to declarations. w.duplicate++ return w } w.seen[n] = struct{}{} switch t := n.(type) { case *ast.Identifier: if t != nil { idx := n.Idx0() + w.shift - 1 s := w.source[:idx] + "IDENT_" + w.source[idx:] w.source = s w.shift += 6 } case *ast.VariableExpression: if t != nil { idx := n.Idx0() + w.shift - 1 s := w.source[:idx] + "VAR_" + w.source[idx:] w.source = s w.shift += 4 } case *ast.NewExpression: w.newExpressionIdx1 = n.Idx1() } return w } func (w *walker) Exit(n ast.Node) { w.pop(n) } func TestVisitorRewrite(t *testing.T) { source := `var b = function() { test(); try {} catch(e) {} var test = "test(); var test = 1" } // test` program, err := parser.ParseFile(nil, "", source, 0) require.NoError(t, err) w := &walker{ source: source, seen: make(map[ast.Node]struct{}), } ast.Walk(w, program) xformed := `var VAR_b = function() { IDENT_test(); try {} catch(IDENT_e) {} var VAR_test = "test(); var test = 1" } // test` require.Equal(t, xformed, w.source) require.Empty(t, w.stack) require.Zero(t, w.duplicate) } func Test_issue261(t *testing.T) { tests := map[string]struct { code string want file.Idx }{ "no-parenthesis": { code: `var i = new Image;`, want: 18, }, "no-args": { code: `var i = new Image();`, want: 20, }, "two-args": { code: `var i = new Image(1, 2);`, want: 24, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { prog, err := parser.ParseFile(nil, "", tt.code, 0) require.NoError(t, err) w := &walker{ source: tt.code, seen: make(map[ast.Node]struct{}), } ast.Walk(w, prog) require.Equal(t, tt.want, w.newExpressionIdx1) require.Empty(t, w.stack) require.Zero(t, w.duplicate) }) } } func TestBadStatement(t *testing.T) { source := ` var abc; break; do { } while(true); ` program, err := parser.ParseFile(nil, "", source, 0) require.ErrorContains(t, err, "Illegal break statement") w := &walker{ source: source, seen: make(map[ast.Node]struct{}), } require.NotPanics(t, func() { ast.Walk(w, program) }) } ================================================ FILE: builtin.go ================================================ package otto import ( "encoding/hex" "errors" "math" "net/url" "regexp" "strconv" "strings" "unicode/utf16" "unicode/utf8" ) // Global. func builtinGlobalEval(call FunctionCall) Value { src := call.Argument(0) if !src.IsString() { return src } rt := call.runtime program := rt.cmplParseOrThrow(src.string(), nil) if !call.eval { // Not a direct call to eval, so we enter the global ExecutionContext rt.enterGlobalScope() defer rt.leaveScope() } returnValue := rt.cmplEvaluateNodeProgram(program, true) if returnValue.isEmpty() { return Value{} } return returnValue } func builtinGlobalIsNaN(call FunctionCall) Value { value := call.Argument(0).float64() return boolValue(math.IsNaN(value)) } func builtinGlobalIsFinite(call FunctionCall) Value { value := call.Argument(0).float64() return boolValue(!math.IsNaN(value) && !math.IsInf(value, 0)) } func digitValue(chr rune) int { switch { case '0' <= chr && chr <= '9': return int(chr - '0') case 'a' <= chr && chr <= 'z': return int(chr - 'a' + 10) case 'A' <= chr && chr <= 'Z': return int(chr - 'A' + 10) } return 36 // Larger than any legal digit value } func builtinGlobalParseInt(call FunctionCall) Value { input := strings.Trim(call.Argument(0).string(), builtinStringTrimWhitespace) if len(input) == 0 { return NaNValue() } radix := int(toInt32(call.Argument(1))) negative := false switch input[0] { case '+': input = input[1:] case '-': negative = true input = input[1:] } strip := true if radix == 0 { radix = 10 } else { if radix < 2 || radix > 36 { return NaNValue() } else if radix != 16 { strip = false } } switch len(input) { case 0: return NaNValue() case 1: default: if strip { if input[0] == '0' && (input[1] == 'x' || input[1] == 'X') { input = input[2:] radix = 16 } } } base := radix index := 0 for ; index < len(input); index++ { digit := digitValue(rune(input[index])) // If not ASCII, then an error anyway if digit >= base { break } } input = input[0:index] value, err := strconv.ParseInt(input, radix, 64) if err != nil { if errors.Is(err, strconv.ErrRange) { base := float64(base) // Could just be a very large number (e.g. 0x8000000000000000) var value float64 for _, chr := range input { digit := float64(digitValue(chr)) if digit >= base { return NaNValue() } value = value*base + digit } if negative { value *= -1 } return float64Value(value) } return NaNValue() } if negative { value *= -1 } return int64Value(value) } var ( parseFloatMatchBadSpecial = regexp.MustCompile(`[\+\-]?(?:[Ii]nf$|infinity)`) parseFloatMatchValid = regexp.MustCompile(`[0-9eE\+\-\.]|Infinity`) ) func builtinGlobalParseFloat(call FunctionCall) Value { // Caveat emptor: This implementation does NOT match the specification input := strings.Trim(call.Argument(0).string(), builtinStringTrimWhitespace) if parseFloatMatchBadSpecial.MatchString(input) { return NaNValue() } value, err := strconv.ParseFloat(input, 64) if err != nil { for end := len(input); end > 0; end-- { val := input[0:end] if !parseFloatMatchValid.MatchString(val) { return NaNValue() } value, err = strconv.ParseFloat(val, 64) if err == nil { break } } if err != nil { return NaNValue() } } return float64Value(value) } // encodeURI/decodeURI func encodeDecodeURI(call FunctionCall, escape *regexp.Regexp) Value { value := call.Argument(0) var input []uint16 switch vl := value.value.(type) { case []uint16: input = vl default: input = utf16.Encode([]rune(value.string())) } if len(input) == 0 { return stringValue("") } output := []byte{} length := len(input) encode := make([]byte, 4) for index := 0; index < length; { value := input[index] decode := utf16.Decode(input[index : index+1]) if value >= 0xDC00 && value <= 0xDFFF { panic(call.runtime.panicURIError("URI malformed")) } if value >= 0xD800 && value <= 0xDBFF { index++ if index >= length { panic(call.runtime.panicURIError("URI malformed")) } // input = ..., value, value1, ... value1 := input[index] if value1 < 0xDC00 || value1 > 0xDFFF { panic(call.runtime.panicURIError("URI malformed")) } decode = []rune{((rune(value) - 0xD800) * 0x400) + (rune(value1) - 0xDC00) + 0x10000} } index++ size := utf8.EncodeRune(encode, decode[0]) output = append(output, encode[0:size]...) } bytes := escape.ReplaceAllFunc(output, func(target []byte) []byte { // Probably a better way of doing this if target[0] == ' ' { return []byte("%20") } return []byte(url.QueryEscape(string(target))) }) return stringValue(string(bytes)) } var encodeURIRegexp = regexp.MustCompile(`([^~!@#$&*()=:/,;?+'])`) func builtinGlobalEncodeURI(call FunctionCall) Value { return encodeDecodeURI(call, encodeURIRegexp) } var encodeURIComponentRegexp = regexp.MustCompile(`([^~!*()'])`) func builtinGlobalEncodeURIComponent(call FunctionCall) Value { return encodeDecodeURI(call, encodeURIComponentRegexp) } // 3B/2F/3F/3A/40/26/3D/2B/24/2C/23. var decodeURIGuard = regexp.MustCompile(`(?i)(?:%)(3B|2F|3F|3A|40|26|3D|2B|24|2C|23)`) func decodeURI(input string, reserve bool) (string, bool) { if reserve { input = decodeURIGuard.ReplaceAllString(input, "%25$1") } input = strings.ReplaceAll(input, "+", "%2B") // Ugly hack to make QueryUnescape work with our use case output, err := url.QueryUnescape(input) if err != nil || !utf8.ValidString(output) { return "", true } return output, false } func builtinGlobalDecodeURI(call FunctionCall) Value { output, err := decodeURI(call.Argument(0).string(), true) if err { panic(call.runtime.panicURIError("URI malformed")) } return stringValue(output) } func builtinGlobalDecodeURIComponent(call FunctionCall) Value { output, err := decodeURI(call.Argument(0).string(), false) if err { panic(call.runtime.panicURIError("URI malformed")) } return stringValue(output) } // escape/unescape func builtinShouldEscape(chr byte) bool { if 'A' <= chr && chr <= 'Z' || 'a' <= chr && chr <= 'z' || '0' <= chr && chr <= '9' { return false } return !strings.ContainsRune("*_+-./", rune(chr)) } const escapeBase16 = "0123456789ABCDEF" func builtinEscape(input string) string { output := make([]byte, 0, len(input)) length := len(input) for index := 0; index < length; { if builtinShouldEscape(input[index]) { chr, width := utf8.DecodeRuneInString(input[index:]) chr16 := utf16.Encode([]rune{chr})[0] if 256 > chr16 { output = append(output, '%', escapeBase16[chr16>>4], escapeBase16[chr16&15], ) } else { output = append(output, '%', 'u', escapeBase16[chr16>>12], escapeBase16[(chr16>>8)&15], escapeBase16[(chr16>>4)&15], escapeBase16[chr16&15], ) } index += width } else { output = append(output, input[index]) index++ } } return string(output) } func builtinUnescape(input string) string { output := make([]rune, 0, len(input)) length := len(input) for index := 0; index < length; { if input[index] == '%' { if index <= length-6 && input[index+1] == 'u' { byte16, err := hex.DecodeString(input[index+2 : index+6]) if err == nil { value := uint16(byte16[0])<<8 + uint16(byte16[1]) chr := utf16.Decode([]uint16{value})[0] output = append(output, chr) index += 6 continue } } if index <= length-3 { byte8, err := hex.DecodeString(input[index+1 : index+3]) if err == nil { value := uint16(byte8[0]) chr := utf16.Decode([]uint16{value})[0] output = append(output, chr) index += 3 continue } } } output = append(output, rune(input[index])) index++ } return string(output) } func builtinGlobalEscape(call FunctionCall) Value { return stringValue(builtinEscape(call.Argument(0).string())) } func builtinGlobalUnescape(call FunctionCall) Value { return stringValue(builtinUnescape(call.Argument(0).string())) } ================================================ FILE: builtin_array.go ================================================ package otto import ( "strconv" "strings" ) // Array func builtinArray(call FunctionCall) Value { return objectValue(builtinNewArrayNative(call.runtime, call.ArgumentList)) } func builtinNewArray(obj *object, argumentList []Value) Value { return objectValue(builtinNewArrayNative(obj.runtime, argumentList)) } func builtinNewArrayNative(rt *runtime, argumentList []Value) *object { if len(argumentList) == 1 { firstArgument := argumentList[0] if firstArgument.IsNumber() { return rt.newArray(arrayUint32(rt, firstArgument)) } } return rt.newArrayOf(argumentList) } func builtinArrayToString(call FunctionCall) Value { thisObject := call.thisObject() join := thisObject.get("join") if join.isCallable() { join := join.object() return join.call(call.This, call.ArgumentList, false, nativeFrame) } return builtinObjectToString(call) } func builtinArrayToLocaleString(call FunctionCall) Value { separator := "," thisObject := call.thisObject() length := int64(toUint32(thisObject.get(propertyLength))) if length == 0 { return stringValue("") } stringList := make([]string, 0, length) for index := range length { value := thisObject.get(arrayIndexToString(index)) stringValue := "" switch value.kind { case valueEmpty, valueUndefined, valueNull: default: obj := call.runtime.toObject(value) toLocaleString := obj.get("toLocaleString") if !toLocaleString.isCallable() { panic(call.runtime.panicTypeError("Array.toLocaleString index[%d] %q is not callable", index, toLocaleString)) } stringValue = toLocaleString.call(call.runtime, objectValue(obj)).string() } stringList = append(stringList, stringValue) } return stringValue(strings.Join(stringList, separator)) } func builtinArrayConcat(call FunctionCall) Value { thisObject := call.thisObject() valueArray := []Value{} source := append([]Value{objectValue(thisObject)}, call.ArgumentList...) for _, item := range source { switch item.kind { case valueObject: obj := item.object() if isArray(obj) { length := obj.get(propertyLength).number().int64 for index := range length { name := strconv.FormatInt(index, 10) if obj.hasProperty(name) { valueArray = append(valueArray, obj.get(name)) } else { valueArray = append(valueArray, Value{}) } } continue } fallthrough default: valueArray = append(valueArray, item) } } return objectValue(call.runtime.newArrayOf(valueArray)) } func builtinArrayShift(call FunctionCall) Value { thisObject := call.thisObject() length := int64(toUint32(thisObject.get(propertyLength))) if length == 0 { thisObject.put(propertyLength, int64Value(0), true) return Value{} } first := thisObject.get("0") for index := int64(1); index < length; index++ { from := arrayIndexToString(index) to := arrayIndexToString(index - 1) if thisObject.hasProperty(from) { thisObject.put(to, thisObject.get(from), true) } else { thisObject.delete(to, true) } } thisObject.delete(arrayIndexToString(length-1), true) thisObject.put(propertyLength, int64Value(length-1), true) return first } func builtinArrayPush(call FunctionCall) Value { thisObject := call.thisObject() itemList := call.ArgumentList index := int64(toUint32(thisObject.get(propertyLength))) for len(itemList) > 0 { thisObject.put(arrayIndexToString(index), itemList[0], true) itemList = itemList[1:] index++ } length := int64Value(index) thisObject.put(propertyLength, length, true) return length } func builtinArrayPop(call FunctionCall) Value { thisObject := call.thisObject() length := int64(toUint32(thisObject.get(propertyLength))) if length == 0 { thisObject.put(propertyLength, uint32Value(0), true) return Value{} } last := thisObject.get(arrayIndexToString(length - 1)) thisObject.delete(arrayIndexToString(length-1), true) thisObject.put(propertyLength, int64Value(length-1), true) return last } func builtinArrayJoin(call FunctionCall) Value { separator := "," argument := call.Argument(0) if argument.IsDefined() { separator = argument.string() } thisObject := call.thisObject() length := int64(toUint32(thisObject.get(propertyLength))) if length == 0 { return stringValue("") } stringList := make([]string, 0, length) for index := range length { value := thisObject.get(arrayIndexToString(index)) stringValue := "" switch value.kind { case valueEmpty, valueUndefined, valueNull: default: stringValue = value.string() } stringList = append(stringList, stringValue) } return stringValue(strings.Join(stringList, separator)) } func builtinArraySplice(call FunctionCall) Value { thisObject := call.thisObject() length := int64(toUint32(thisObject.get(propertyLength))) start := valueToRangeIndex(call.Argument(0), length, false) deleteCount := length - start if arg, ok := call.getArgument(1); ok { deleteCount = valueToRangeIndex(arg, length-start, true) } valueArray := make([]Value, deleteCount) for index := range deleteCount { indexString := arrayIndexToString(start + index) if thisObject.hasProperty(indexString) { valueArray[index] = thisObject.get(indexString) } } // 0, <1, 2, 3, 4>, 5, 6, 7 // a, b // length 8 - delete 4 @ start 1 itemList := []Value{} itemCount := int64(len(call.ArgumentList)) if itemCount > 2 { itemCount -= 2 // Less the first two arguments itemList = call.ArgumentList[2:] } else { itemCount = 0 } if itemCount < deleteCount { // The Object/Array is shrinking stop := length - deleteCount // The new length of the Object/Array before // appending the itemList remainder // Stopping at the lower bound of the insertion: // Move an item from the after the deleted portion // to a position after the inserted portion for index := start; index < stop; index++ { from := arrayIndexToString(index + deleteCount) // Position just after deletion to := arrayIndexToString(index + itemCount) // Position just after splice (insertion) if thisObject.hasProperty(from) { thisObject.put(to, thisObject.get(from), true) } else { thisObject.delete(to, true) } } // Delete off the end // We don't bother to delete below (if any) since those // will be overwritten anyway for index := length; index > (stop + itemCount); index-- { thisObject.delete(arrayIndexToString(index-1), true) } } else if itemCount > deleteCount { // The Object/Array is growing // The itemCount is greater than the deleteCount, so we do // not have to worry about overwriting what we should be moving // --- // Starting from the upper bound of the deletion: // Move an item from the after the deleted portion // to a position after the inserted portion for index := length - deleteCount; index > start; index-- { from := arrayIndexToString(index + deleteCount - 1) to := arrayIndexToString(index + itemCount - 1) if thisObject.hasProperty(from) { thisObject.put(to, thisObject.get(from), true) } else { thisObject.delete(to, true) } } } for index := range itemCount { thisObject.put(arrayIndexToString(index+start), itemList[index], true) } thisObject.put(propertyLength, int64Value(length+itemCount-deleteCount), true) return objectValue(call.runtime.newArrayOf(valueArray)) } func builtinArraySlice(call FunctionCall) Value { thisObject := call.thisObject() length := int64(toUint32(thisObject.get(propertyLength))) start, end := rangeStartEnd(call.ArgumentList, length, false) if start >= end { // Always an empty array return objectValue(call.runtime.newArray(0)) } sliceLength := end - start sliceValueArray := make([]Value, sliceLength) for index := range sliceLength { from := arrayIndexToString(index + start) if thisObject.hasProperty(from) { sliceValueArray[index] = thisObject.get(from) } } return objectValue(call.runtime.newArrayOf(sliceValueArray)) } func builtinArrayUnshift(call FunctionCall) Value { thisObject := call.thisObject() length := int64(toUint32(thisObject.get(propertyLength))) itemList := call.ArgumentList itemCount := int64(len(itemList)) for index := length; index > 0; index-- { from := arrayIndexToString(index - 1) to := arrayIndexToString(index + itemCount - 1) if thisObject.hasProperty(from) { thisObject.put(to, thisObject.get(from), true) } else { thisObject.delete(to, true) } } for index := range itemCount { thisObject.put(arrayIndexToString(index), itemList[index], true) } newLength := int64Value(length + itemCount) thisObject.put(propertyLength, newLength, true) return newLength } func builtinArrayReverse(call FunctionCall) Value { thisObject := call.thisObject() length := int64(toUint32(thisObject.get(propertyLength))) lower := struct { name string index int64 exists bool }{} upper := lower lower.index = 0 middle := length / 2 // Division will floor for lower.index != middle { lower.name = arrayIndexToString(lower.index) upper.index = length - lower.index - 1 upper.name = arrayIndexToString(upper.index) lower.exists = thisObject.hasProperty(lower.name) upper.exists = thisObject.hasProperty(upper.name) switch { case lower.exists && upper.exists: lowerValue := thisObject.get(lower.name) upperValue := thisObject.get(upper.name) thisObject.put(lower.name, upperValue, true) thisObject.put(upper.name, lowerValue, true) case !lower.exists && upper.exists: value := thisObject.get(upper.name) thisObject.delete(upper.name, true) thisObject.put(lower.name, value, true) case lower.exists && !upper.exists: value := thisObject.get(lower.name) thisObject.delete(lower.name, true) thisObject.put(upper.name, value, true) } lower.index++ } return call.This } func sortCompare(thisObject *object, index0, index1 uint, compare *object) int { j := struct { name string value string exists bool defined bool }{} k := j j.name = arrayIndexToString(int64(index0)) j.exists = thisObject.hasProperty(j.name) k.name = arrayIndexToString(int64(index1)) k.exists = thisObject.hasProperty(k.name) switch { case !j.exists && !k.exists: return 0 case !j.exists: return 1 case !k.exists: return -1 } x := thisObject.get(j.name) y := thisObject.get(k.name) j.defined = x.IsDefined() k.defined = y.IsDefined() switch { case !j.defined && !k.defined: return 0 case !j.defined: return 1 case !k.defined: return -1 } if compare == nil { j.value = x.string() k.value = y.string() if j.value == k.value { return 0 } else if j.value < k.value { return -1 } return 1 } return toIntSign(compare.call(Value{}, []Value{x, y}, false, nativeFrame)) } func arraySortSwap(thisObject *object, index0, index1 uint) { j := struct { name string exists bool }{} k := j j.name = arrayIndexToString(int64(index0)) j.exists = thisObject.hasProperty(j.name) k.name = arrayIndexToString(int64(index1)) k.exists = thisObject.hasProperty(k.name) switch { case j.exists && k.exists: jv := thisObject.get(j.name) kv := thisObject.get(k.name) thisObject.put(j.name, kv, true) thisObject.put(k.name, jv, true) case !j.exists && k.exists: value := thisObject.get(k.name) thisObject.delete(k.name, true) thisObject.put(j.name, value, true) case j.exists && !k.exists: value := thisObject.get(j.name) thisObject.delete(j.name, true) thisObject.put(k.name, value, true) } } func arraySortQuickPartition(thisObject *object, left, right, pivot uint, compare *object) (uint, uint) { arraySortSwap(thisObject, pivot, right) // Right is now the pivot value cursor := left cursor2 := left for index := left; index < right; index++ { comparison := sortCompare(thisObject, index, right, compare) // Compare to the pivot value if comparison < 0 { arraySortSwap(thisObject, index, cursor) if cursor < cursor2 { arraySortSwap(thisObject, index, cursor2) } cursor++ cursor2++ } else if comparison == 0 { arraySortSwap(thisObject, index, cursor2) cursor2++ } } arraySortSwap(thisObject, cursor2, right) return cursor, cursor2 } func arraySortQuickSort(thisObject *object, left, right uint, compare *object) { if left < right { middle := left + (right-left)/2 pivot, pivot2 := arraySortQuickPartition(thisObject, left, right, middle, compare) if pivot > 0 { arraySortQuickSort(thisObject, left, pivot-1, compare) } arraySortQuickSort(thisObject, pivot2+1, right, compare) } } func builtinArraySort(call FunctionCall) Value { thisObject := call.thisObject() length := uint(toUint32(thisObject.get(propertyLength))) compareValue := call.Argument(0) compare := compareValue.object() if compareValue.IsUndefined() { } else if !compareValue.isCallable() { panic(call.runtime.panicTypeError("Array.sort value %q is not callable", compareValue)) } if length > 1 { arraySortQuickSort(thisObject, 0, length-1, compare) } return call.This } func builtinArrayIsArray(call FunctionCall) Value { return boolValue(isArray(call.Argument(0).object())) } func builtinArrayIndexOf(call FunctionCall) Value { thisObject, matchValue := call.thisObject(), call.Argument(0) if length := int64(toUint32(thisObject.get(propertyLength))); length > 0 { index := int64(0) if len(call.ArgumentList) > 1 { index = call.Argument(1).number().int64 } if index < 0 { if index += length; index < 0 { index = 0 } } else if index >= length { index = -1 } for ; index >= 0 && index < length; index++ { name := arrayIndexToString(index) if !thisObject.hasProperty(name) { continue } value := thisObject.get(name) if strictEqualityComparison(matchValue, value) { return uint32Value(uint32(index)) } } } return intValue(-1) } func builtinArrayLastIndexOf(call FunctionCall) Value { thisObject, matchValue := call.thisObject(), call.Argument(0) length := int64(toUint32(thisObject.get(propertyLength))) index := length - 1 if len(call.ArgumentList) > 1 { index = call.Argument(1).number().int64 } if 0 > index { index += length } if index > length { index = length - 1 } else if 0 > index { return intValue(-1) } for ; index >= 0; index-- { name := arrayIndexToString(index) if !thisObject.hasProperty(name) { continue } value := thisObject.get(name) if strictEqualityComparison(matchValue, value) { return uint32Value(uint32(index)) } } return intValue(-1) } func builtinArrayEvery(call FunctionCall) Value { thisObject := call.thisObject() this := objectValue(thisObject) if iterator := call.Argument(0); iterator.isCallable() { length := int64(toUint32(thisObject.get(propertyLength))) callThis := call.Argument(1) for index := range length { if key := arrayIndexToString(index); thisObject.hasProperty(key) { if value := thisObject.get(key); iterator.call(call.runtime, callThis, value, int64Value(index), this).bool() { continue } return falseValue } } return trueValue } panic(call.runtime.panicTypeError("Array.every argument %q is not callable", call.Argument(0))) } func builtinArraySome(call FunctionCall) Value { thisObject := call.thisObject() this := objectValue(thisObject) if iterator := call.Argument(0); iterator.isCallable() { length := int64(toUint32(thisObject.get(propertyLength))) callThis := call.Argument(1) for index := range length { if key := arrayIndexToString(index); thisObject.hasProperty(key) { if value := thisObject.get(key); iterator.call(call.runtime, callThis, value, int64Value(index), this).bool() { return trueValue } } } return falseValue } panic(call.runtime.panicTypeError("Array.some %q if not callable", call.Argument(0))) } func builtinArrayForEach(call FunctionCall) Value { thisObject := call.thisObject() this := objectValue(thisObject) if iterator := call.Argument(0); iterator.isCallable() { length := int64(toUint32(thisObject.get(propertyLength))) callThis := call.Argument(1) for index := range length { if key := arrayIndexToString(index); thisObject.hasProperty(key) { iterator.call(call.runtime, callThis, thisObject.get(key), int64Value(index), this) } } return Value{} } panic(call.runtime.panicTypeError("Array.foreach %q if not callable", call.Argument(0))) } func builtinArrayMap(call FunctionCall) Value { thisObject := call.thisObject() this := objectValue(thisObject) if iterator := call.Argument(0); iterator.isCallable() { length := int64(toUint32(thisObject.get(propertyLength))) callThis := call.Argument(1) values := make([]Value, length) for index := range length { if key := arrayIndexToString(index); thisObject.hasProperty(key) { values[index] = iterator.call(call.runtime, callThis, thisObject.get(key), index, this) } else { values[index] = Value{} } } return objectValue(call.runtime.newArrayOf(values)) } panic(call.runtime.panicTypeError("Array.foreach %q if not callable", call.Argument(0))) } func builtinArrayFilter(call FunctionCall) Value { thisObject := call.thisObject() this := objectValue(thisObject) if iterator := call.Argument(0); iterator.isCallable() { length := int64(toUint32(thisObject.get(propertyLength))) callThis := call.Argument(1) values := make([]Value, 0) for index := range length { if key := arrayIndexToString(index); thisObject.hasProperty(key) { value := thisObject.get(key) if iterator.call(call.runtime, callThis, value, index, this).bool() { values = append(values, value) } } } return objectValue(call.runtime.newArrayOf(values)) } panic(call.runtime.panicTypeError("Array.filter %q if not callable", call.Argument(0))) } func builtinArrayReduce(call FunctionCall) Value { thisObject := call.thisObject() this := objectValue(thisObject) if iterator := call.Argument(0); iterator.isCallable() { initial := len(call.ArgumentList) > 1 start := call.Argument(1) length := int64(toUint32(thisObject.get(propertyLength))) index := int64(0) if length > 0 || initial { var accumulator Value if !initial { for ; index < length; index++ { if key := arrayIndexToString(index); thisObject.hasProperty(key) { accumulator = thisObject.get(key) index++ break } } } else { accumulator = start } for ; index < length; index++ { if key := arrayIndexToString(index); thisObject.hasProperty(key) { accumulator = iterator.call(call.runtime, Value{}, accumulator, thisObject.get(key), index, this) } } return accumulator } } panic(call.runtime.panicTypeError("Array.reduce %q if not callable", call.Argument(0))) } func builtinArrayReduceRight(call FunctionCall) Value { thisObject := call.thisObject() this := objectValue(thisObject) if iterator := call.Argument(0); iterator.isCallable() { initial := len(call.ArgumentList) > 1 start := call.Argument(1) length := int64(toUint32(thisObject.get(propertyLength))) if length > 0 || initial { index := length - 1 var accumulator Value if !initial { for ; index >= 0; index-- { if key := arrayIndexToString(index); thisObject.hasProperty(key) { accumulator = thisObject.get(key) index-- break } } } else { accumulator = start } for ; index >= 0; index-- { if key := arrayIndexToString(index); thisObject.hasProperty(key) { accumulator = iterator.call(call.runtime, Value{}, accumulator, thisObject.get(key), key, this) } } return accumulator } } panic(call.runtime.panicTypeError("Array.reduceRight %q if not callable", call.Argument(0))) } ================================================ FILE: builtin_boolean.go ================================================ package otto // Boolean func builtinBoolean(call FunctionCall) Value { return boolValue(call.Argument(0).bool()) } func builtinNewBoolean(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newBoolean(valueOfArrayIndex(argumentList, 0))) } func builtinBooleanToString(call FunctionCall) Value { value := call.This if !value.IsBoolean() { // Will throw a TypeError if ThisObject is not a Boolean value = call.thisClassObject(classBooleanName).primitiveValue() } return stringValue(value.string()) } func builtinBooleanValueOf(call FunctionCall) Value { value := call.This if !value.IsBoolean() { value = call.thisClassObject(classBooleanName).primitiveValue() } return value } ================================================ FILE: builtin_date.go ================================================ package otto import ( "math" "time" ) // Date const ( // TODO Be like V8? // builtinDateDateTimeLayout = "Mon Jan 2 2006 15:04:05 GMT-0700 (MST)". builtinDateDateTimeLayout = time.RFC1123 // "Mon, 02 Jan 2006 15:04:05 MST" builtinDateDateLayout = "Mon, 02 Jan 2006" builtinDateTimeLayout = "15:04:05 MST" ) // utcTimeZone is the time zone used for UTC calculations. // It is GMT not UTC as that's what Javascript does because toUTCString is // actually an alias to toGMTString. var utcTimeZone = time.FixedZone("GMT", 0) func builtinDate(call FunctionCall) Value { date := &dateObject{} date.Set(newDateTime([]Value{}, time.Local)) //nolint:gosmopolitan return stringValue(date.Time().Format(builtinDateDateTimeLayout)) } func builtinNewDate(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newDate(newDateTime(argumentList, time.Local))) //nolint:gosmopolitan } func builtinDateToString(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return stringValue("Invalid Date") } return stringValue(date.Time().Local().Format(builtinDateDateTimeLayout)) //nolint:gosmopolitan } func builtinDateToDateString(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return stringValue("Invalid Date") } return stringValue(date.Time().Local().Format(builtinDateDateLayout)) //nolint:gosmopolitan } func builtinDateToTimeString(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return stringValue("Invalid Date") } return stringValue(date.Time().Local().Format(builtinDateTimeLayout)) //nolint:gosmopolitan } func builtinDateToUTCString(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return stringValue("Invalid Date") } return stringValue(date.Time().In(utcTimeZone).Format(builtinDateDateTimeLayout)) } func builtinDateToISOString(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return stringValue("Invalid Date") } return stringValue(date.Time().Format("2006-01-02T15:04:05.000Z")) } func builtinDateToJSON(call FunctionCall) Value { obj := call.thisObject() value := obj.DefaultValue(defaultValueHintNumber) // FIXME object.primitiveNumberValue // FIXME fv.isFinite if fv := value.float64(); math.IsNaN(fv) || math.IsInf(fv, 0) { return nullValue } toISOString := obj.get("toISOString") if !toISOString.isCallable() { // FIXME panic(call.runtime.panicTypeError("Date.toJSON toISOString %q is not callable", toISOString)) } return toISOString.call(call.runtime, objectValue(obj), []Value{}) } func builtinDateToGMTString(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return stringValue("Invalid Date") } return stringValue(date.Time().Format("Mon, 02 Jan 2006 15:04:05 GMT")) } func builtinDateGetTime(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } // We do this (convert away from a float) so the user // does not get something back in exponential notation return int64Value(date.Epoch()) } func builtinDateSetTime(call FunctionCall) Value { obj := call.thisObject() date := dateObjectOf(call.runtime, call.thisObject()) date.Set(call.Argument(0).float64()) obj.value = date return date.Value() } func builtinDateBeforeSet(call FunctionCall, argumentLimit int, timeLocal bool) (*object, *dateObject, *ecmaTime, []int) { obj := call.thisObject() date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return nil, nil, nil, nil } if argumentLimit > len(call.ArgumentList) { argumentLimit = len(call.ArgumentList) } if argumentLimit == 0 { obj.value = invalidDateObject return nil, nil, nil, nil } valueList := make([]int, argumentLimit) for index := range argumentLimit { value := call.ArgumentList[index] nm := value.number() switch nm.kind { case numberInteger, numberFloat: default: obj.value = invalidDateObject return nil, nil, nil, nil } valueList[index] = int(nm.int64) } baseTime := date.Time() if timeLocal { baseTime = baseTime.Local() //nolint:gosmopolitan } ecmaTime := newEcmaTime(baseTime) return obj, &date, &ecmaTime, valueList } func builtinDateParse(call FunctionCall) Value { date := call.Argument(0).string() return float64Value(dateParse(date)) } func builtinDateUTC(call FunctionCall) Value { return float64Value(newDateTime(call.ArgumentList, time.UTC)) } func builtinDateNow(call FunctionCall) Value { call.ArgumentList = []Value(nil) return builtinDateUTC(call) } // This is a placeholder. func builtinDateToLocaleString(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return stringValue("Invalid Date") } return stringValue(date.Time().Local().Format("2006-01-02 15:04:05")) //nolint:gosmopolitan } // This is a placeholder. func builtinDateToLocaleDateString(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return stringValue("Invalid Date") } return stringValue(date.Time().Local().Format("2006-01-02")) //nolint:gosmopolitan } // This is a placeholder. func builtinDateToLocaleTimeString(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return stringValue("Invalid Date") } return stringValue(date.Time().Local().Format("15:04:05")) //nolint:gosmopolitan } func builtinDateValueOf(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return date.Value() } func builtinDateGetYear(call FunctionCall) Value { // Will throw a TypeError is ThisObject is nil or // does not have Class of "Date" date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Local().Year() - 1900) //nolint:gosmopolitan } func builtinDateGetFullYear(call FunctionCall) Value { // Will throw a TypeError is ThisObject is nil or // does not have Class of "Date" date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Local().Year()) //nolint:gosmopolitan } func builtinDateGetUTCFullYear(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Year()) } func builtinDateGetMonth(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(dateFromGoMonth(date.Time().Local().Month())) //nolint:gosmopolitan } func builtinDateGetUTCMonth(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(dateFromGoMonth(date.Time().Month())) } func builtinDateGetDate(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Local().Day()) //nolint:gosmopolitan } func builtinDateGetUTCDate(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Day()) } func builtinDateGetDay(call FunctionCall) Value { // Actually day of the week date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(dateFromGoDay(date.Time().Local().Weekday())) //nolint:gosmopolitan } func builtinDateGetUTCDay(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(dateFromGoDay(date.Time().Weekday())) } func builtinDateGetHours(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Local().Hour()) //nolint:gosmopolitan } func builtinDateGetUTCHours(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Hour()) } func builtinDateGetMinutes(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Local().Minute()) //nolint:gosmopolitan } func builtinDateGetUTCMinutes(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Minute()) } func builtinDateGetSeconds(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Local().Second()) //nolint:gosmopolitan } func builtinDateGetUTCSeconds(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Second()) } func builtinDateGetMilliseconds(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Local().Nanosecond() / (100 * 100 * 100)) //nolint:gosmopolitan } func builtinDateGetUTCMilliseconds(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } return intValue(date.Time().Nanosecond() / (100 * 100 * 100)) } func builtinDateGetTimezoneOffset(call FunctionCall) Value { date := dateObjectOf(call.runtime, call.thisObject()) if date.isNaN { return NaNValue() } timeLocal := date.Time().Local() //nolint:gosmopolitan // Is this kosher? timeLocalAsUTC := time.Date( timeLocal.Year(), timeLocal.Month(), timeLocal.Day(), timeLocal.Hour(), timeLocal.Minute(), timeLocal.Second(), timeLocal.Nanosecond(), time.UTC, ) return float64Value(date.Time().Sub(timeLocalAsUTC).Seconds() / 60) } func builtinDateSetMilliseconds(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 1, true) if ecmaTime == nil { return NaNValue() } ecmaTime.millisecond = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetUTCMilliseconds(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 1, false) if ecmaTime == nil { return NaNValue() } ecmaTime.millisecond = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetSeconds(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 2, true) if ecmaTime == nil { return NaNValue() } if len(value) > 1 { ecmaTime.millisecond = value[1] } ecmaTime.second = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetUTCSeconds(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 2, false) if ecmaTime == nil { return NaNValue() } if len(value) > 1 { ecmaTime.millisecond = value[1] } ecmaTime.second = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetMinutes(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 3, true) if ecmaTime == nil { return NaNValue() } if len(value) > 2 { ecmaTime.millisecond = value[2] ecmaTime.second = value[1] } else if len(value) > 1 { ecmaTime.second = value[1] } ecmaTime.minute = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetUTCMinutes(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 3, false) if ecmaTime == nil { return NaNValue() } if len(value) > 2 { ecmaTime.millisecond = value[2] ecmaTime.second = value[1] } else if len(value) > 1 { ecmaTime.second = value[1] } ecmaTime.minute = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetHours(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 4, true) if ecmaTime == nil { return NaNValue() } switch { case len(value) > 3: ecmaTime.millisecond = value[3] fallthrough case len(value) > 2: ecmaTime.second = value[2] fallthrough case len(value) > 1: ecmaTime.minute = value[1] } ecmaTime.hour = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetUTCHours(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 4, false) if ecmaTime == nil { return NaNValue() } switch { case len(value) > 3: ecmaTime.millisecond = value[3] fallthrough case len(value) > 2: ecmaTime.second = value[2] fallthrough case len(value) > 1: ecmaTime.minute = value[1] } ecmaTime.hour = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetDate(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 1, true) if ecmaTime == nil { return NaNValue() } ecmaTime.day = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetUTCDate(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 1, false) if ecmaTime == nil { return NaNValue() } ecmaTime.day = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetMonth(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 2, true) if ecmaTime == nil { return NaNValue() } if len(value) > 1 { ecmaTime.day = value[1] } ecmaTime.month = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetUTCMonth(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 2, false) if ecmaTime == nil { return NaNValue() } if len(value) > 1 { ecmaTime.day = value[1] } ecmaTime.month = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetYear(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 1, true) if ecmaTime == nil { return NaNValue() } year := value[0] if 0 <= year && year <= 99 { year += 1900 } ecmaTime.year = year date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetFullYear(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 3, true) if ecmaTime == nil { return NaNValue() } if len(value) > 2 { ecmaTime.day = value[2] ecmaTime.month = value[1] } else if len(value) > 1 { ecmaTime.month = value[1] } ecmaTime.year = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } func builtinDateSetUTCFullYear(call FunctionCall) Value { obj, date, ecmaTime, value := builtinDateBeforeSet(call, 3, false) if ecmaTime == nil { return NaNValue() } if len(value) > 2 { ecmaTime.day = value[2] ecmaTime.month = value[1] } else if len(value) > 1 { ecmaTime.month = value[1] } ecmaTime.year = value[0] date.SetTime(ecmaTime.goTime()) obj.value = *date return date.Value() } // toUTCString // toISOString // toJSONString // toJSON ================================================ FILE: builtin_error.go ================================================ package otto import ( "fmt" ) func builtinError(call FunctionCall) Value { return objectValue(call.runtime.newError(classErrorName, call.Argument(0), 1)) } func builtinNewError(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newError(classErrorName, valueOfArrayIndex(argumentList, 0), 0)) } func builtinErrorToString(call FunctionCall) Value { thisObject := call.thisObject() if thisObject == nil { panic(call.runtime.panicTypeError("Error.toString is nil")) } name := classErrorName nameValue := thisObject.get("name") if nameValue.IsDefined() { name = nameValue.string() } message := "" messageValue := thisObject.get("message") if messageValue.IsDefined() { message = messageValue.string() } if len(name) == 0 { return stringValue(message) } if len(message) == 0 { return stringValue(name) } return stringValue(fmt.Sprintf("%s: %s", name, message)) } func (rt *runtime) newEvalError(message Value) *object { o := rt.newErrorObject("EvalError", message, 0) o.prototype = rt.global.EvalErrorPrototype return o } func builtinEvalError(call FunctionCall) Value { return objectValue(call.runtime.newEvalError(call.Argument(0))) } func builtinNewEvalError(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newEvalError(valueOfArrayIndex(argumentList, 0))) } func (rt *runtime) newTypeError(message Value) *object { o := rt.newErrorObject("TypeError", message, 0) o.prototype = rt.global.TypeErrorPrototype return o } func builtinTypeError(call FunctionCall) Value { return objectValue(call.runtime.newTypeError(call.Argument(0))) } func builtinNewTypeError(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newTypeError(valueOfArrayIndex(argumentList, 0))) } func (rt *runtime) newRangeError(message Value) *object { o := rt.newErrorObject("RangeError", message, 0) o.prototype = rt.global.RangeErrorPrototype return o } func builtinRangeError(call FunctionCall) Value { return objectValue(call.runtime.newRangeError(call.Argument(0))) } func builtinNewRangeError(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newRangeError(valueOfArrayIndex(argumentList, 0))) } func (rt *runtime) newURIError(message Value) *object { o := rt.newErrorObject("URIError", message, 0) o.prototype = rt.global.URIErrorPrototype return o } func (rt *runtime) newReferenceError(message Value) *object { o := rt.newErrorObject("ReferenceError", message, 0) o.prototype = rt.global.ReferenceErrorPrototype return o } func builtinReferenceError(call FunctionCall) Value { return objectValue(call.runtime.newReferenceError(call.Argument(0))) } func builtinNewReferenceError(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newReferenceError(valueOfArrayIndex(argumentList, 0))) } func (rt *runtime) newSyntaxError(message Value) *object { o := rt.newErrorObject("SyntaxError", message, 0) o.prototype = rt.global.SyntaxErrorPrototype return o } func builtinSyntaxError(call FunctionCall) Value { return objectValue(call.runtime.newSyntaxError(call.Argument(0))) } func builtinNewSyntaxError(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newSyntaxError(valueOfArrayIndex(argumentList, 0))) } func builtinURIError(call FunctionCall) Value { return objectValue(call.runtime.newURIError(call.Argument(0))) } func builtinNewURIError(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newURIError(valueOfArrayIndex(argumentList, 0))) } ================================================ FILE: builtin_function.go ================================================ package otto import ( "fmt" "strings" "unicode" "github.com/robertkrimen/otto/parser" ) // Function func builtinFunction(call FunctionCall) Value { return objectValue(builtinNewFunctionNative(call.runtime, call.ArgumentList)) } func builtinNewFunction(obj *object, argumentList []Value) Value { return objectValue(builtinNewFunctionNative(obj.runtime, argumentList)) } func argumentList2parameterList(argumentList []Value) []string { parameterList := make([]string, 0, len(argumentList)) for _, value := range argumentList { tmp := strings.FieldsFunc(value.string(), func(chr rune) bool { return chr == ',' || unicode.IsSpace(chr) }) parameterList = append(parameterList, tmp...) } return parameterList } func builtinNewFunctionNative(rt *runtime, argumentList []Value) *object { var parameterList, body string if count := len(argumentList); count > 0 { tmp := make([]string, 0, count-1) for _, value := range argumentList[0 : count-1] { tmp = append(tmp, value.string()) } parameterList = strings.Join(tmp, ",") body = argumentList[count-1].string() } // FIXME function, err := parser.ParseFunction(parameterList, body) rt.parseThrow(err) // Will panic/throw appropriately cmpl := compiler{} cmplFunction := cmpl.parseExpression(function) return rt.newNodeFunction(cmplFunction.(*nodeFunctionLiteral), rt.globalStash) } func builtinFunctionToString(call FunctionCall) Value { obj := call.thisClassObject(classFunctionName) // Should throw a TypeError unless Function switch fn := obj.value.(type) { case nativeFunctionObject: return stringValue(fmt.Sprintf("function %s() { [native code] }", fn.name)) case nodeFunctionObject: return stringValue(fn.node.source) case bindFunctionObject: return stringValue("function () { [native code] }") default: panic(call.runtime.panicTypeError("Function.toString unknown type %T", obj.value)) } } func builtinFunctionApply(call FunctionCall) Value { if !call.This.isCallable() { panic(call.runtime.panicTypeError("Function.apply %q is not callable", call.This)) } this := call.Argument(0) if this.IsUndefined() { // FIXME Not ECMA5 this = objectValue(call.runtime.globalObject) } argumentList := call.Argument(1) switch argumentList.kind { case valueUndefined, valueNull: return call.thisObject().call(this, nil, false, nativeFrame) case valueObject: default: panic(call.runtime.panicTypeError("Function.apply unknown type %T for second argument")) } arrayObject := argumentList.object() thisObject := call.thisObject() length := int64(toUint32(arrayObject.get(propertyLength))) valueArray := make([]Value, length) for index := range length { valueArray[index] = arrayObject.get(arrayIndexToString(index)) } return thisObject.call(this, valueArray, false, nativeFrame) } func builtinFunctionCall(call FunctionCall) Value { if !call.This.isCallable() { panic(call.runtime.panicTypeError("Function.call %q is not callable", call.This)) } thisObject := call.thisObject() this := call.Argument(0) if this.IsUndefined() { // FIXME Not ECMA5 this = objectValue(call.runtime.globalObject) } if len(call.ArgumentList) >= 1 { return thisObject.call(this, call.ArgumentList[1:], false, nativeFrame) } return thisObject.call(this, nil, false, nativeFrame) } func builtinFunctionBind(call FunctionCall) Value { target := call.This if !target.isCallable() { panic(call.runtime.panicTypeError("Function.bind %q is not callable", call.This)) } targetObject := target.object() this := call.Argument(0) argumentList := call.slice(1) if this.IsUndefined() { // FIXME Do this elsewhere? this = objectValue(call.runtime.globalObject) } return objectValue(call.runtime.newBoundFunction(targetObject, this, argumentList)) } ================================================ FILE: builtin_json.go ================================================ package otto import ( "bytes" "encoding/json" "fmt" "strings" ) type builtinJSONParseContext struct { reviver Value call FunctionCall } func builtinJSONParse(call FunctionCall) Value { ctx := builtinJSONParseContext{ call: call, } revive := false if reviver := call.Argument(1); reviver.isCallable() { revive = true ctx.reviver = reviver } var root interface{} err := json.Unmarshal([]byte(call.Argument(0).string()), &root) if err != nil { panic(call.runtime.panicSyntaxError(err.Error())) } value, exists := builtinJSONParseWalk(ctx, root) if !exists { value = Value{} } if revive { root := ctx.call.runtime.newObject() root.put("", value, false) return builtinJSONReviveWalk(ctx, root, "") } return value } func builtinJSONReviveWalk(ctx builtinJSONParseContext, holder *object, name string) Value { value := holder.get(name) if obj := value.object(); obj != nil { if isArray(obj) { length := int64(objectLength(obj)) for index := range length { idxName := arrayIndexToString(index) idxValue := builtinJSONReviveWalk(ctx, obj, idxName) if idxValue.IsUndefined() { obj.delete(idxName, false) } else { obj.defineProperty(idxName, idxValue, 0o111, false) } } } else { obj.enumerate(false, func(name string) bool { enumVal := builtinJSONReviveWalk(ctx, obj, name) if enumVal.IsUndefined() { obj.delete(name, false) } else { obj.defineProperty(name, enumVal, 0o111, false) } return true }) } } return ctx.reviver.call(ctx.call.runtime, objectValue(holder), name, value) } func builtinJSONParseWalk(ctx builtinJSONParseContext, rawValue interface{}) (Value, bool) { switch value := rawValue.(type) { case nil: return nullValue, true case bool: return boolValue(value), true case string: return stringValue(value), true case float64: return float64Value(value), true case []interface{}: arrayValue := make([]Value, len(value)) for index, rawValue := range value { if value, exists := builtinJSONParseWalk(ctx, rawValue); exists { arrayValue[index] = value } } return objectValue(ctx.call.runtime.newArrayOf(arrayValue)), true case map[string]interface{}: obj := ctx.call.runtime.newObject() for name, rawValue := range value { if value, exists := builtinJSONParseWalk(ctx, rawValue); exists { obj.put(name, value, false) } } return objectValue(obj), true } return Value{}, false } type builtinJSONStringifyContext struct { replacerFunction *Value gap string stack []*object propertyList []string call FunctionCall } func builtinJSONStringify(call FunctionCall) Value { ctx := builtinJSONStringifyContext{ call: call, stack: []*object{nil}, } replacer := call.Argument(1).object() if replacer != nil { if isArray(replacer) { length := objectLength(replacer) seen := map[string]bool{} propertyList := make([]string, length) length = 0 for index := range propertyList { value := replacer.get(arrayIndexToString(int64(index))) switch value.kind { case valueObject: switch value.value.(*object).class { case classStringName, classNumberName: default: continue } case valueString, valueNumber: default: continue } name := value.string() if seen[name] { continue } seen[name] = true length++ propertyList[index] = name } ctx.propertyList = propertyList[0:length] } else if replacer.class == classFunctionName { value := objectValue(replacer) ctx.replacerFunction = &value } } if spaceValue, exists := call.getArgument(2); exists { if spaceValue.kind == valueObject { switch spaceValue.value.(*object).class { case classStringName: spaceValue = stringValue(spaceValue.string()) case classNumberName: spaceValue = spaceValue.numberValue() } } switch spaceValue.kind { case valueString: value := spaceValue.string() if len(value) > 10 { ctx.gap = value[0:10] } else { ctx.gap = value } case valueNumber: value := spaceValue.number().int64 if value > 10 { value = 10 } else if value < 0 { value = 0 } ctx.gap = strings.Repeat(" ", int(value)) } } holder := call.runtime.newObject() holder.put("", call.Argument(0), false) value, exists := builtinJSONStringifyWalk(ctx, "", holder) if !exists { return Value{} } valueJSON, err := json.Marshal(value) if err != nil { panic(call.runtime.panicTypeError("JSON.stringify marshal: %s", err)) } if ctx.gap != "" { valueJSON1 := bytes.Buffer{} if err = json.Indent(&valueJSON1, valueJSON, "", ctx.gap); err != nil { panic(call.runtime.panicTypeError("JSON.stringify indent: %s", err)) } valueJSON = valueJSON1.Bytes() } return stringValue(string(valueJSON)) } func builtinJSONStringifyWalk(ctx builtinJSONStringifyContext, key string, holder *object) (interface{}, bool) { value := holder.get(key) if value.IsObject() { obj := value.object() if toJSON := obj.get("toJSON"); toJSON.IsFunction() { value = toJSON.call(ctx.call.runtime, value, key) } else if obj.objectClass.marshalJSON != nil { // If the object is a GoStruct or something that implements json.Marshaler marshaler := obj.objectClass.marshalJSON(obj) if marshaler != nil { return marshaler, true } } } if ctx.replacerFunction != nil { value = ctx.replacerFunction.call(ctx.call.runtime, objectValue(holder), key, value) } if value.kind == valueObject { switch value.value.(*object).class { case classBooleanName: value = value.object().value.(Value) case classStringName: value = stringValue(value.string()) case classNumberName: value = value.numberValue() } } switch value.kind { case valueBoolean: return value.bool(), true case valueString: return value.string(), true case valueNumber: integer := value.number() switch integer.kind { case numberInteger: return integer.int64, true case numberFloat: return integer.float64, true default: return nil, true } case valueNull: return nil, true case valueObject: objHolder := value.object() if value := value.object(); nil != value { for _, obj := range ctx.stack { if objHolder == obj { panic(ctx.call.runtime.panicTypeError("Converting circular structure to JSON")) } } ctx.stack = append(ctx.stack, value) defer func() { ctx.stack = ctx.stack[:len(ctx.stack)-1] }() } if isArray(objHolder) { var length uint32 switch value := objHolder.get(propertyLength).value.(type) { case uint32: length = value case int: if value >= 0 { length = uint32(value) } default: panic(ctx.call.runtime.panicTypeError(fmt.Sprintf("JSON.stringify: invalid length: %v (%[1]T)", value))) } array := make([]interface{}, length) for index := range array { name := arrayIndexToString(int64(index)) value, _ := builtinJSONStringifyWalk(ctx, name, objHolder) array[index] = value } return array, true } else if objHolder.class != classFunctionName { obj := map[string]interface{}{} if ctx.propertyList != nil { for _, name := range ctx.propertyList { value, exists := builtinJSONStringifyWalk(ctx, name, objHolder) if exists { obj[name] = value } } } else { // Go maps are without order, so this doesn't conform to the ECMA ordering // standard, but oh well... objHolder.enumerate(false, func(name string) bool { value, exists := builtinJSONStringifyWalk(ctx, name, objHolder) if exists { obj[name] = value } return true }) } return obj, true } } return nil, false } ================================================ FILE: builtin_math.go ================================================ package otto import ( "math" "math/rand" ) // Math func builtinMathAbs(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Abs(number)) } func builtinMathAcos(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Acos(number)) } func builtinMathAcosh(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Acosh(number)) } func builtinMathAsin(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Asin(number)) } func builtinMathAsinh(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Asinh(number)) } func builtinMathAtan(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Atan(number)) } func builtinMathAtan2(call FunctionCall) Value { y := call.Argument(0).float64() if math.IsNaN(y) { return NaNValue() } x := call.Argument(1).float64() if math.IsNaN(x) { return NaNValue() } return float64Value(math.Atan2(y, x)) } func builtinMathAtanh(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Atanh(number)) } func builtinMathCbrt(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Cbrt(number)) } func builtinMathCos(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Cos(number)) } func builtinMathCeil(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Ceil(number)) } func builtinMathCosh(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Cosh(number)) } func builtinMathExp(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Exp(number)) } func builtinMathExpm1(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Expm1(number)) } func builtinMathFloor(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Floor(number)) } func builtinMathLog(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Log(number)) } func builtinMathLog10(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Log10(number)) } func builtinMathLog1p(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Log1p(number)) } func builtinMathLog2(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Log2(number)) } func builtinMathMax(call FunctionCall) Value { switch len(call.ArgumentList) { case 0: return negativeInfinityValue() case 1: return float64Value(call.ArgumentList[0].float64()) } result := call.ArgumentList[0].float64() if math.IsNaN(result) { return NaNValue() } for _, value := range call.ArgumentList[1:] { value := value.float64() if math.IsNaN(value) { return NaNValue() } result = math.Max(result, value) } return float64Value(result) } func builtinMathMin(call FunctionCall) Value { switch len(call.ArgumentList) { case 0: return positiveInfinityValue() case 1: return float64Value(call.ArgumentList[0].float64()) } result := call.ArgumentList[0].float64() if math.IsNaN(result) { return NaNValue() } for _, value := range call.ArgumentList[1:] { value := value.float64() if math.IsNaN(value) { return NaNValue() } result = math.Min(result, value) } return float64Value(result) } func builtinMathPow(call FunctionCall) Value { // TODO Make sure this works according to the specification (15.8.2.13) x := call.Argument(0).float64() y := call.Argument(1).float64() if math.Abs(x) == 1 && math.IsInf(y, 0) { return NaNValue() } return float64Value(math.Pow(x, y)) } func builtinMathRandom(call FunctionCall) Value { var v float64 if call.runtime.random != nil { v = call.runtime.random() } else { v = rand.Float64() //nolint:gosec } return float64Value(v) } func builtinMathRound(call FunctionCall) Value { number := call.Argument(0).float64() value := math.Floor(number + 0.5) if value == 0 { value = math.Copysign(0, number) } return float64Value(value) } func builtinMathSin(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Sin(number)) } func builtinMathSinh(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Sinh(number)) } func builtinMathSqrt(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Sqrt(number)) } func builtinMathTan(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Tan(number)) } func builtinMathTanh(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Tanh(number)) } func builtinMathTrunc(call FunctionCall) Value { number := call.Argument(0).float64() return float64Value(math.Trunc(number)) } ================================================ FILE: builtin_number.go ================================================ package otto import ( "math" "strconv" "golang.org/x/text/language" "golang.org/x/text/message" "golang.org/x/text/number" ) // Number func numberValueFromNumberArgumentList(argumentList []Value) Value { if len(argumentList) > 0 { return argumentList[0].numberValue() } return intValue(0) } func builtinNumber(call FunctionCall) Value { return numberValueFromNumberArgumentList(call.ArgumentList) } func builtinNewNumber(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newNumber(numberValueFromNumberArgumentList(argumentList))) } func builtinNumberToString(call FunctionCall) Value { // Will throw a TypeError if ThisObject is not a Number value := call.thisClassObject(classNumberName).primitiveValue() radix := 10 radixArgument := call.Argument(0) if radixArgument.IsDefined() { integer := toIntegerFloat(radixArgument) if integer < 2 || integer > 36 { panic(call.runtime.panicRangeError("toString() radix must be between 2 and 36")) } radix = int(integer) } if radix == 10 { return stringValue(value.string()) } return stringValue(numberToStringRadix(value, radix)) } func builtinNumberValueOf(call FunctionCall) Value { return call.thisClassObject(classNumberName).primitiveValue() } func builtinNumberToFixed(call FunctionCall) Value { precision := toIntegerFloat(call.Argument(0)) if 20 < precision || 0 > precision { panic(call.runtime.panicRangeError("toFixed() precision must be between 0 and 20")) } if call.This.IsNaN() { return stringValue("NaN") } if value := call.This.float64(); math.Abs(value) >= 1e21 { return stringValue(floatToString(value, 64)) } return stringValue(strconv.FormatFloat(call.This.float64(), 'f', int(precision), 64)) } func builtinNumberToExponential(call FunctionCall) Value { if call.This.IsNaN() { return stringValue("NaN") } precision := float64(-1) if value := call.Argument(0); value.IsDefined() { precision = toIntegerFloat(value) if 0 > precision { panic(call.runtime.panicRangeError("toString() radix must be between 2 and 36")) } } return stringValue(strconv.FormatFloat(call.This.float64(), 'e', int(precision), 64)) } func builtinNumberToPrecision(call FunctionCall) Value { if call.This.IsNaN() { return stringValue("NaN") } value := call.Argument(0) if value.IsUndefined() { return stringValue(call.This.string()) } precision := toIntegerFloat(value) if 1 > precision { panic(call.runtime.panicRangeError("toPrecision() precision must be greater than 1")) } return stringValue(strconv.FormatFloat(call.This.float64(), 'g', int(precision), 64)) } func builtinNumberIsNaN(call FunctionCall) Value { if len(call.ArgumentList) < 1 { return boolValue(false) } return boolValue(call.Argument(0).IsNaN()) } func builtinNumberToLocaleString(call FunctionCall) Value { value := call.thisClassObject(classNumberName).primitiveValue() locale := call.Argument(0) lang := defaultLanguage if locale.IsDefined() { lang = language.MustParse(locale.string()) } p := message.NewPrinter(lang) return stringValue(p.Sprintf("%v", number.Decimal(value.value))) } ================================================ FILE: builtin_object.go ================================================ package otto import ( "fmt" "strconv" ) // Object func builtinObject(call FunctionCall) Value { value := call.Argument(0) switch value.kind { case valueUndefined, valueNull: return objectValue(call.runtime.newObject()) } return objectValue(call.runtime.toObject(value)) } func builtinNewObject(obj *object, argumentList []Value) Value { value := valueOfArrayIndex(argumentList, 0) switch value.kind { case valueNull, valueUndefined: case valueNumber, valueString, valueBoolean: return objectValue(obj.runtime.toObject(value)) case valueObject: return value default: } return objectValue(obj.runtime.newObject()) } func builtinObjectValueOf(call FunctionCall) Value { return objectValue(call.thisObject()) } func builtinObjectHasOwnProperty(call FunctionCall) Value { propertyName := call.Argument(0).string() thisObject := call.thisObject() return boolValue(thisObject.hasOwnProperty(propertyName)) } func builtinObjectIsPrototypeOf(call FunctionCall) Value { value := call.Argument(0) if !value.IsObject() { return falseValue } prototype := call.toObject(value).prototype thisObject := call.thisObject() for prototype != nil { if thisObject == prototype { return trueValue } prototype = prototype.prototype } return falseValue } func builtinObjectPropertyIsEnumerable(call FunctionCall) Value { propertyName := call.Argument(0).string() thisObject := call.thisObject() prop := thisObject.getOwnProperty(propertyName) if prop != nil && prop.enumerable() { return trueValue } return falseValue } func builtinObjectToString(call FunctionCall) Value { var result string switch { case call.This.IsUndefined(): result = "[object Undefined]" case call.This.IsNull(): result = "[object Null]" default: result = fmt.Sprintf("[object %s]", call.thisObject().class) } return stringValue(result) } func builtinObjectToLocaleString(call FunctionCall) Value { toString := call.thisObject().get("toString") if !toString.isCallable() { panic(call.runtime.panicTypeError("Object.toLocaleString %q is not callable", toString)) } return toString.call(call.runtime, call.This) } func builtinObjectGetPrototypeOf(call FunctionCall) Value { val := call.Argument(0) obj := val.object() if obj == nil { panic(call.runtime.panicTypeError("Object.GetPrototypeOf is nil")) } if obj.prototype == nil { return nullValue } return objectValue(obj.prototype) } func builtinObjectGetOwnPropertyDescriptor(call FunctionCall) Value { val := call.Argument(0) obj := val.object() if obj == nil { panic(call.runtime.panicTypeError("Object.GetOwnPropertyDescriptor is nil")) } name := call.Argument(1).string() descriptor := obj.getOwnProperty(name) if descriptor == nil { return Value{} } return objectValue(call.runtime.fromPropertyDescriptor(*descriptor)) } func builtinObjectDefineProperty(call FunctionCall) Value { val := call.Argument(0) obj := val.object() if obj == nil { panic(call.runtime.panicTypeError("Object.DefineProperty is nil")) } name := call.Argument(1).string() descriptor := toPropertyDescriptor(call.runtime, call.Argument(2)) obj.defineOwnProperty(name, descriptor, true) return val } func builtinObjectDefineProperties(call FunctionCall) Value { val := call.Argument(0) obj := val.object() if obj == nil { panic(call.runtime.panicTypeError("Object.DefineProperties is nil")) } properties := call.runtime.toObject(call.Argument(1)) properties.enumerate(false, func(name string) bool { descriptor := toPropertyDescriptor(call.runtime, properties.get(name)) obj.defineOwnProperty(name, descriptor, true) return true }) return val } func builtinObjectCreate(call FunctionCall) Value { prototypeValue := call.Argument(0) if !prototypeValue.IsNull() && !prototypeValue.IsObject() { panic(call.runtime.panicTypeError("Object.Create is nil")) } obj := call.runtime.newObject() obj.prototype = prototypeValue.object() propertiesValue := call.Argument(1) if propertiesValue.IsDefined() { properties := call.runtime.toObject(propertiesValue) properties.enumerate(false, func(name string) bool { descriptor := toPropertyDescriptor(call.runtime, properties.get(name)) obj.defineOwnProperty(name, descriptor, true) return true }) } return objectValue(obj) } func builtinObjectIsExtensible(call FunctionCall) Value { val := call.Argument(0) if obj := val.object(); obj != nil { return boolValue(obj.extensible) } panic(call.runtime.panicTypeError("Object.IsExtensible is nil")) } func builtinObjectPreventExtensions(call FunctionCall) Value { val := call.Argument(0) if obj := val.object(); obj != nil { obj.extensible = false return val } panic(call.runtime.panicTypeError("Object.PreventExtensions is nil")) } func builtinObjectAssign(call FunctionCall) Value { if len(call.ArgumentList) < 1 { panic(call.runtime.panicTypeError("Object.assign TypeError: Cannot convert undefined or null to object")) } target := call.ArgumentList[0] targetObj := target.object() if !target.IsObject() && target.IsNull() && target.IsUndefined() { panic(call.runtime.panicTypeError("Object.assign TypeError: Cannot convert undefined or null to object")) } for _, source := range call.ArgumentList[1:] { if source.IsString() { sourceStr := source.string() for i, r := range sourceStr { targetObj.put(strconv.Itoa(i), stringValue(string(r)), true) } } else if source.IsObject() { sourceObj := source.object() sourceObj.enumerate(false, func(name string) bool { descriptor := sourceObj.getOwnProperty(name) ok := targetObj.defineOwnProperty(name, *descriptor, true) if !ok { panic(call.runtime.panicTypeError("Object.assign TypeError: Cannot convert undefined or null to object")) } return true }) } } return objectValue(targetObj) } func builtinObjectIsSealed(call FunctionCall) Value { val := call.Argument(0) if obj := val.object(); obj != nil { if obj.extensible { return boolValue(false) } result := true obj.enumerate(true, func(name string) bool { prop := obj.getProperty(name) if prop.configurable() { result = false } return true }) return boolValue(result) } panic(call.runtime.panicTypeError("Object.IsSealed is nil")) } func builtinObjectSeal(call FunctionCall) Value { val := call.Argument(0) if obj := val.object(); obj != nil { obj.enumerate(true, func(name string) bool { if prop := obj.getOwnProperty(name); nil != prop && prop.configurable() { prop.configureOff() obj.defineOwnProperty(name, *prop, true) } return true }) obj.extensible = false return val } panic(call.runtime.panicTypeError("Object.Seal is nil")) } func builtinObjectIsFrozen(call FunctionCall) Value { val := call.Argument(0) if obj := val.object(); obj != nil { if obj.extensible { return boolValue(false) } result := true obj.enumerate(true, func(name string) bool { prop := obj.getProperty(name) if prop.configurable() || prop.writable() { result = false } return true }) return boolValue(result) } panic(call.runtime.panicTypeError("Object.IsFrozen is nil")) } func builtinObjectFreeze(call FunctionCall) Value { val := call.Argument(0) if obj := val.object(); obj != nil { obj.enumerate(true, func(name string) bool { if prop, update := obj.getOwnProperty(name), false; nil != prop { if prop.isDataDescriptor() && prop.writable() { prop.writeOff() update = true } if prop.configurable() { prop.configureOff() update = true } if update { obj.defineOwnProperty(name, *prop, true) } } return true }) obj.extensible = false return val } panic(call.runtime.panicTypeError("Object.Freeze is nil")) } func builtinObjectKeys(call FunctionCall) Value { if obj, keys := call.Argument(0).object(), []Value(nil); nil != obj { obj.enumerate(false, func(name string) bool { keys = append(keys, stringValue(name)) return true }) return objectValue(call.runtime.newArrayOf(keys)) } panic(call.runtime.panicTypeError("Object.Keys is nil")) } func builtinObjectValues(call FunctionCall) Value { if obj, values := call.Argument(0).object(), []Value(nil); nil != obj { obj.enumerate(false, func(name string) bool { values = append(values, obj.get(name)) return true }) return objectValue(call.runtime.newArrayOf(values)) } panic(call.runtime.panicTypeError("Object.Values is nil")) } func builtinObjectGetOwnPropertyNames(call FunctionCall) Value { if obj, propertyNames := call.Argument(0).object(), []Value(nil); nil != obj { obj.enumerate(true, func(name string) bool { if obj.hasOwnProperty(name) { propertyNames = append(propertyNames, stringValue(name)) } return true }) return objectValue(call.runtime.newArrayOf(propertyNames)) } // Default to empty array for non object types. return objectValue(call.runtime.newArray(0)) } ================================================ FILE: builtin_regexp.go ================================================ package otto import ( "fmt" ) // RegExp func builtinRegExp(call FunctionCall) Value { pattern := call.Argument(0) flags := call.Argument(1) if obj := pattern.object(); obj != nil { if obj.class == classRegExpName && flags.IsUndefined() { return pattern } } return objectValue(call.runtime.newRegExp(pattern, flags)) } func builtinNewRegExp(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newRegExp( valueOfArrayIndex(argumentList, 0), valueOfArrayIndex(argumentList, 1), )) } func builtinRegExpToString(call FunctionCall) Value { thisObject := call.thisObject() source := thisObject.get("source").string() flags := []byte{} if thisObject.get("global").bool() { flags = append(flags, 'g') } if thisObject.get("ignoreCase").bool() { flags = append(flags, 'i') } if thisObject.get("multiline").bool() { flags = append(flags, 'm') } return stringValue(fmt.Sprintf("/%s/%s", source, flags)) } func builtinRegExpExec(call FunctionCall) Value { thisObject := call.thisObject() target := call.Argument(0).string() match, result := execRegExp(thisObject, target) if !match { return nullValue } return objectValue(execResultToArray(call.runtime, target, result)) } func builtinRegExpTest(call FunctionCall) Value { thisObject := call.thisObject() target := call.Argument(0).string() match, result := execRegExp(thisObject, target) if !match { return boolValue(match) } // Match extract and assign input, $_ and $1 -> $9 on global RegExp. input := stringValue(target) call.runtime.global.RegExp.defineProperty("$_", input, 0o100, false) call.runtime.global.RegExp.defineProperty("input", input, 0o100, false) var start int n := 1 re := call.runtime.global.RegExp empty := stringValue("") for i, v := range result[2:] { if i%2 == 0 { start = v } else { if v == -1 { // No match for this part. re.defineProperty(fmt.Sprintf("$%d", n), empty, 0o100, false) } else { re.defineProperty(fmt.Sprintf("$%d", n), stringValue(target[start:v]), 0o100, false) } n++ if n == 10 { break } } } if n <= 9 { // Erase remaining. for i := n; i <= 9; i++ { re.defineProperty(fmt.Sprintf("$%d", i), empty, 0o100, false) } } return boolValue(match) } func builtinRegExpCompile(call FunctionCall) Value { // This (useless) function is deprecated, but is here to provide some // semblance of compatibility. // Caveat emptor: it may not be around for long. return Value{} } ================================================ FILE: builtin_string.go ================================================ package otto import ( "bytes" "regexp" "strconv" "strings" "unicode/utf16" "unicode/utf8" ) // String func stringValueFromStringArgumentList(argumentList []Value) Value { if len(argumentList) > 0 { return stringValue(argumentList[0].string()) } return stringValue("") } func builtinString(call FunctionCall) Value { return stringValueFromStringArgumentList(call.ArgumentList) } func builtinNewString(obj *object, argumentList []Value) Value { return objectValue(obj.runtime.newString(stringValueFromStringArgumentList(argumentList))) } func builtinStringToString(call FunctionCall) Value { return call.thisClassObject(classStringName).primitiveValue() } func builtinStringValueOf(call FunctionCall) Value { return call.thisClassObject(classStringName).primitiveValue() } func builtinStringFromCharCode(call FunctionCall) Value { chrList := make([]uint16, len(call.ArgumentList)) for index, value := range call.ArgumentList { chrList[index] = toUint16(value) } return string16Value(chrList) } func builtinStringCharAt(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) idx := int(call.Argument(0).number().int64) chr := stringAt(call.This.object().stringValue(), idx) if chr == utf8.RuneError { return stringValue("") } return stringValue(string(chr)) } func builtinStringCharCodeAt(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) idx := int(call.Argument(0).number().int64) chr := stringAt(call.This.object().stringValue(), idx) if chr == utf8.RuneError { return NaNValue() } return uint16Value(uint16(chr)) } func builtinStringConcat(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) var value bytes.Buffer value.WriteString(call.This.string()) for _, item := range call.ArgumentList { value.WriteString(item.string()) } return stringValue(value.String()) } func lastIndexRune(s, substr string) int { if i := strings.LastIndex(s, substr); i >= 0 { return utf16Length(s[:i]) } return -1 } func indexRune(s, substr string) int { if i := strings.Index(s, substr); i >= 0 { return utf16Length(s[:i]) } return -1 } func utf16Length(s string) int { return len(utf16.Encode([]rune(s))) } func builtinStringIndexOf(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) value := call.This.string() target := call.Argument(0).string() if 2 > len(call.ArgumentList) { return intValue(indexRune(value, target)) } start := toIntegerFloat(call.Argument(1)) if 0 > start { start = 0 } else if start >= float64(len(value)) { if target == "" { return intValue(len(value)) } return intValue(-1) } index := indexRune(value[int(start):], target) if index >= 0 { index += int(start) } return intValue(index) } func builtinStringLastIndexOf(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) value := call.This.string() target := call.Argument(0).string() if 2 > len(call.ArgumentList) || call.ArgumentList[1].IsUndefined() { return intValue(lastIndexRune(value, target)) } length := len(value) if length == 0 { return intValue(lastIndexRune(value, target)) } start := call.ArgumentList[1].number() if start.kind == numberInfinity { // FIXME // startNumber is infinity, so start is the end of string (start = length) return intValue(lastIndexRune(value, target)) } if 0 > start.int64 { start.int64 = 0 } end := int(start.int64) + len(target) if end > length { end = length } return intValue(lastIndexRune(value[:end], target)) } func builtinStringMatch(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := call.This.string() matcherValue := call.Argument(0) matcher := matcherValue.object() if !matcherValue.IsObject() || matcher.class != classRegExpName { matcher = call.runtime.newRegExp(matcherValue, Value{}) } global := matcher.get("global").bool() if !global { match, result := execRegExp(matcher, target) if !match { return nullValue } return objectValue(execResultToArray(call.runtime, target, result)) } result := matcher.regExpValue().regularExpression.FindAllStringIndex(target, -1) if result == nil { matcher.put("lastIndex", intValue(0), true) return Value{} // !match } matchCount := len(result) valueArray := make([]Value, matchCount) for index := range matchCount { valueArray[index] = stringValue(target[result[index][0]:result[index][1]]) } matcher.put("lastIndex", intValue(result[matchCount-1][1]), true) return objectValue(call.runtime.newArrayOf(valueArray)) } var builtinStringReplaceRegexp = regexp.MustCompile("\\$(?:[\\$\\&\\'\\`1-9]|0[1-9]|[1-9][0-9])") func builtinStringFindAndReplaceString(input []byte, lastIndex int, match []int, target []byte, replaceValue []byte) []byte { matchCount := len(match) / 2 output := input if match[0] != lastIndex { output = append(output, target[lastIndex:match[0]]...) } replacement := builtinStringReplaceRegexp.ReplaceAllFunc(replaceValue, func(part []byte) []byte { // TODO Check if match[0] or match[1] can be -1 in this scenario switch part[1] { case '$': return []byte{'$'} case '&': return target[match[0]:match[1]] case '`': return target[:match[0]] case '\'': return target[match[1]:] } matchNumberParse, err := strconv.ParseInt(string(part[1:]), 10, 64) if err != nil { return nil } matchNumber := int(matchNumberParse) if matchNumber >= matchCount { return nil } offset := 2 * matchNumber if match[offset] != -1 { return target[match[offset]:match[offset+1]] } return nil // The empty string }) return append(output, replacement...) } func builtinStringReplace(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := []byte(call.This.string()) searchValue := call.Argument(0) searchObject := searchValue.object() // TODO If a capture is -1? var search *regexp.Regexp global := false find := 1 if searchValue.IsObject() && searchObject.class == classRegExpName { regExp := searchObject.regExpValue() search = regExp.regularExpression if regExp.global { find = -1 global = true } } else { search = regexp.MustCompile(regexp.QuoteMeta(searchValue.string())) } found := search.FindAllSubmatchIndex(target, find) if found == nil { return stringValue(string(target)) // !match } lastIndex := 0 result := []byte{} replaceValue := call.Argument(1) if replaceValue.isCallable() { target := string(target) replace := replaceValue.object() for _, match := range found { if match[0] != lastIndex { result = append(result, target[lastIndex:match[0]]...) } matchCount := len(match) / 2 argumentList := make([]Value, matchCount+2) for index := range matchCount { offset := 2 * index if match[offset] != -1 { argumentList[index] = stringValue(target[match[offset]:match[offset+1]]) } else { argumentList[index] = Value{} } } // Replace expects rune offsets not byte offsets. startIndex := utf8.RuneCountInString(target[0:match[0]]) argumentList[matchCount+0] = intValue(startIndex) argumentList[matchCount+1] = stringValue(target) replacement := replace.call(Value{}, argumentList, false, nativeFrame).string() result = append(result, []byte(replacement)...) lastIndex = match[1] } } else { replace := []byte(replaceValue.string()) for _, match := range found { result = builtinStringFindAndReplaceString(result, lastIndex, match, target, replace) lastIndex = match[1] } } if lastIndex != len(target) { result = append(result, target[lastIndex:]...) } if global && searchObject != nil { searchObject.put("lastIndex", intValue(lastIndex), true) } return stringValue(string(result)) } func builtinStringSearch(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := call.This.string() searchValue := call.Argument(0) search := searchValue.object() if !searchValue.IsObject() || search.class != classRegExpName { search = call.runtime.newRegExp(searchValue, Value{}) } result := search.regExpValue().regularExpression.FindStringIndex(target) if result == nil { return intValue(-1) } return intValue(result[0]) } func builtinStringSplit(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := call.This.string() separatorValue := call.Argument(0) limitValue := call.Argument(1) limit := -1 if limitValue.IsDefined() { limit = int(toUint32(limitValue)) } if limit == 0 { return objectValue(call.runtime.newArray(0)) } if separatorValue.IsUndefined() { return objectValue(call.runtime.newArrayOf([]Value{stringValue(target)})) } if separatorValue.isRegExp() { targetLength := len(target) search := separatorValue.object().regExpValue().regularExpression valueArray := []Value{} result := search.FindAllStringSubmatchIndex(target, -1) lastIndex := 0 found := 0 for _, match := range result { if match[0] == match[1] { // FIXME Ugh, this is a hack if match[0] == 0 || match[0] == targetLength { continue } } if lastIndex != match[0] { valueArray = append(valueArray, stringValue(target[lastIndex:match[0]])) found++ } else if lastIndex == match[0] { if lastIndex != -1 { valueArray = append(valueArray, stringValue("")) found++ } } lastIndex = match[1] if found == limit { goto RETURN } captureCount := len(match) / 2 for index := 1; index < captureCount; index++ { offset := index * 2 value := Value{} if match[offset] != -1 { value = stringValue(target[match[offset]:match[offset+1]]) } valueArray = append(valueArray, value) found++ if found == limit { goto RETURN } } } if found != limit { if lastIndex != targetLength { valueArray = append(valueArray, stringValue(target[lastIndex:targetLength])) } else { valueArray = append(valueArray, stringValue("")) } } RETURN: return objectValue(call.runtime.newArrayOf(valueArray)) } else { separator := separatorValue.string() splitLimit := limit excess := false if limit > 0 { splitLimit = limit + 1 excess = true } split := strings.SplitN(target, separator, splitLimit) if excess && len(split) > limit { split = split[:limit] } valueArray := make([]Value, len(split)) for index, value := range split { valueArray[index] = stringValue(value) } return objectValue(call.runtime.newArrayOf(valueArray)) } } // builtinStringSlice returns the string sliced by the given values // which are rune not byte offsets, as per String.prototype.slice. func builtinStringSlice(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := []rune(call.This.string()) length := int64(len(target)) start, end := rangeStartEnd(call.ArgumentList, length, false) if end-start <= 0 { return stringValue("") } return stringValue(string(target[start:end])) } func builtinStringSubstring(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := []rune(call.This.string()) length := int64(len(target)) start, end := rangeStartEnd(call.ArgumentList, length, true) if start > end { start, end = end, start } return stringValue(string(target[start:end])) } func builtinStringSubstr(call FunctionCall) Value { target := []rune(call.This.string()) size := int64(len(target)) start, length := rangeStartLength(call.ArgumentList, size) if start >= size { return stringValue("") } if length <= 0 { return stringValue("") } if start+length >= size { // Cap length to be to the end of the string // start = 3, length = 5, size = 4 [0, 1, 2, 3] // 4 - 3 = 1 // target[3:4] length = size - start } return stringValue(string(target[start : start+length])) } func builtinStringStartsWith(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) target := call.This.string() search := call.Argument(0).string() length := len(search) if length > len(target) { return boolValue(false) } return boolValue(target[:length] == search) } func builtinStringToLowerCase(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) return stringValue(strings.ToLower(call.This.string())) } func builtinStringToUpperCase(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) return stringValue(strings.ToUpper(call.This.string())) } // 7.2 Table 2 — Whitespace Characters & 7.3 Table 3 - Line Terminator Characters. const builtinStringTrimWhitespace = "\u0009\u000A\u000B\u000C\u000D\u0020\u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF" func builtinStringTrim(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) return toValue(strings.Trim(call.This.string(), builtinStringTrimWhitespace)) } func builtinStringTrimStart(call FunctionCall) Value { return builtinStringTrimLeft(call) } func builtinStringTrimEnd(call FunctionCall) Value { return builtinStringTrimRight(call) } // Mozilla extension, not ECMAScript 5. func builtinStringTrimLeft(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) return toValue(strings.TrimLeft(call.This.string(), builtinStringTrimWhitespace)) } // Mozilla extension, not ECMAScript 5. func builtinStringTrimRight(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) return toValue(strings.TrimRight(call.This.string(), builtinStringTrimWhitespace)) } func builtinStringLocaleCompare(call FunctionCall) Value { checkObjectCoercible(call.runtime, call.This) this := call.This.string() //nolint:ifshort that := call.Argument(0).string() if this < that { return intValue(-1) } else if this == that { return intValue(0) } return intValue(1) } func builtinStringToLocaleLowerCase(call FunctionCall) Value { return builtinStringToLowerCase(call) } func builtinStringToLocaleUpperCase(call FunctionCall) Value { return builtinStringToUpperCase(call) } ================================================ FILE: builtin_test.go ================================================ package otto import ( "testing" ) func TestString_substr(t *testing.T) { tt(t, func() { test, _ := test() test(` [ "uñiçode".substr(0,1), // "u" "uñiçode".substr(0,2), // "uñ" "uñiçode".substr(0,3), // "uñi" "uñiçode".substr(0,4), // "uñiç" "uñiçode".substr(0,9), // "uñiçode" ]; `, "u,uñ,uñi,uñiç,uñiçode") test(` [ "abc".substr(0,1), // "a" "abc".substr(0,2), // "ab" "abc".substr(0,3), // "abc" "abc".substr(0,4), // "abc" "abc".substr(0,9), // "abc" ]; `, "a,ab,abc,abc,abc") test(` [ "abc".substr(1,1), // "b" "abc".substr(1,2), // "bc" "abc".substr(1,3), // "bc" "abc".substr(1,4), // "bc" "abc".substr(1,9), // "bc" ]; `, "b,bc,bc,bc,bc") test(` [ "abc".substr(2,1), // "c" "abc".substr(2,2), // "c" "abc".substr(2,3), // "c" "abc".substr(2,4), // "c" "abc".substr(2,9), // "c" ]; `, "c,c,c,c,c") test(` [ "abc".substr(3,1), // "" "abc".substr(3,2), // "" "abc".substr(3,3), // "" "abc".substr(3,4), // "" "abc".substr(3,9), // "" ]; `, ",,,,") test(` [ "abc".substr(0), // "abc" "abc".substr(1), // "bc" "abc".substr(2), // "c" "abc".substr(3), // "" "abc".substr(9), // "" ]; `, "abc,bc,c,,") test(` [ "abc".substr(-9), // "abc" "abc".substr(-3), // "abc" "abc".substr(-2), // "bc" "abc".substr(-1), // "c" ]; `, "abc,abc,bc,c") test(` [ "abc".substr(-9, 1), // "a" "abc".substr(-3, 1), // "a" "abc".substr(-2, 1), // "b" "abc".substr(-1, 1), // "c" "abc".substr(-1, 2), // "c" ]; `, "a,a,b,c,c") test(`"abcd".substr(3, 5)`, "d") }) } func Test_builtin_escape(t *testing.T) { tt(t, func() { is(builtinEscape("abc"), "abc") is(builtinEscape("="), "%3D") is(builtinEscape("abc=%+32"), "abc%3D%25+32") is(builtinEscape("世界"), "%u4E16%u754C") }) } func Test_builtin_unescape(t *testing.T) { tt(t, func() { is(builtinUnescape("abc"), "abc") is(builtinUnescape("=%3D"), "==") is(builtinUnescape("abc%3D%25+32"), "abc=%+32") is(builtinUnescape("%u4E16%u754C"), "世界") }) } func TestGlobal_escape(t *testing.T) { tt(t, func() { test, _ := test() test(` [ escape("abc"), // "abc" escape("="), // "%3D" escape("abc=%+32"), // "abc%3D%25+32" escape("\u4e16\u754c"), // "%u4E16%u754C" ]; `, "abc,%3D,abc%3D%25+32,%u4E16%u754C") }) } func TestGlobal_unescape(t *testing.T) { tt(t, func() { test, _ := test() test(` [ unescape("abc"), // "abc" unescape("=%3D"), // "==" unescape("abc%3D%25+32"), // "abc=%+32" unescape("%u4E16%u754C"), // "世界" ]; `, "abc,==,abc=%+32,世界") }) } func TestNumber_isNaN(t *testing.T) { tt(t, func() { test, _ := test() test(`Number.isNaN(1)`, false) test(`Number.isNaN(null)`, false) test(`Number.isNaN()`, false) test(`Number.isNaN(Number.NaN)`, true) test(`Number.isNaN(0+undefined)`, true) }) } ================================================ FILE: call_test.go ================================================ package otto import ( "reflect" "testing" "github.com/stretchr/testify/require" ) const ( testAb = "ab" ) func BenchmarkNativeCallWithString(b *testing.B) { vm := New() err := vm.Set("x", func(a1 string) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x("zzz")`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithFloat32(b *testing.B) { vm := New() err := vm.Set("x", func(a1 float32) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1.1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithFloat64(b *testing.B) { vm := New() err := vm.Set("x", func(a1 float64) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1.1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithInt(b *testing.B) { vm := New() err := vm.Set("x", func(a1 int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithUint(b *testing.B) { vm := New() err := vm.Set("x", func(a1 uint) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithInt8(b *testing.B) { vm := New() err := vm.Set("x", func(a1 int8) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithUint8(b *testing.B) { vm := New() err := vm.Set("x", func(a1 uint8) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithInt16(b *testing.B) { vm := New() err := vm.Set("x", func(a1 int16) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithUint16(b *testing.B) { vm := New() err := vm.Set("x", func(a1 uint16) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithInt32(b *testing.B) { vm := New() err := vm.Set("x", func(a1 int32) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithUint32(b *testing.B) { vm := New() err := vm.Set("x", func(a1 uint32) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithInt64(b *testing.B) { vm := New() err := vm.Set("x", func(a1 int64) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithUint64(b *testing.B) { vm := New() err := vm.Set("x", func(a1 uint64) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithStringInt(b *testing.B) { vm := New() err := vm.Set("x", func(a1 string, a2 int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x("zzz", 1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntVariadic0(b *testing.B) { vm := New() err := vm.Set("x", func(a ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x()`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntVariadic1(b *testing.B) { vm := New() err := vm.Set("x", func(a ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntVariadic3(b *testing.B) { vm := New() err := vm.Set("x", func(a ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1, 2, 3)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntVariadic10(b *testing.B) { vm := New() err := vm.Set("x", func(a ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntArray0(b *testing.B) { vm := New() err := vm.Set("x", func(a []int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x([])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntArray1(b *testing.B) { vm := New() err := vm.Set("x", func(a []int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x([1])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntArray3(b *testing.B) { vm := New() err := vm.Set("x", func(a []int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x([1, 2, 3])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntArray10(b *testing.B) { vm := New() err := vm.Set("x", func(a []int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntVariadicArray0(b *testing.B) { vm := New() err := vm.Set("x", func(a ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x([])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntVariadicArray1(b *testing.B) { vm := New() err := vm.Set("x", func(a ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x([1])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntVariadicArray3(b *testing.B) { vm := New() err := vm.Set("x", func(a ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x([1, 2, 3])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithIntVariadicArray10(b *testing.B) { vm := New() err := vm.Set("x", func(a ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithStringIntVariadic0(b *testing.B) { vm := New() err := vm.Set("x", func(a1 string, a2 ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x("a")`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithStringIntVariadic1(b *testing.B) { vm := New() err := vm.Set("x", func(a1 string, a2 ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x("a", 1)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithStringIntVariadic3(b *testing.B) { vm := New() err := vm.Set("x", func(a1 string, a2 ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x("a", 1, 2, 3)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithStringIntVariadic10(b *testing.B) { vm := New() err := vm.Set("x", func(a1 string, a2 ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x("a", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithStringIntVariadicArray0(b *testing.B) { vm := New() err := vm.Set("x", func(a1 string, a2 ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x("a", [])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithStringIntVariadicArray1(b *testing.B) { vm := New() err := vm.Set("x", func(a1 string, a2 ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x("a", [1])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithStringIntVariadicArray3(b *testing.B) { vm := New() err := vm.Set("x", func(a1 string, a2 ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x("a", [1, 2, 3])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithStringIntVariadicArray10(b *testing.B) { vm := New() err := vm.Set("x", func(a1 string, a2 ...int) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x("a", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithMap(b *testing.B) { vm := New() err := vm.Set("x", func(a map[string]string) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x({a: "b", c: "d"})`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithMapVariadic(b *testing.B) { vm := New() err := vm.Set("x", func(a ...map[string]string) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x({a: "b", c: "d"}, {w: "x", y: "z"})`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithMapVariadicArray(b *testing.B) { vm := New() err := vm.Set("x", func(a ...map[string]string) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x([{a: "b", c: "d"}, {w: "x", y: "z"}])`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithFunction(b *testing.B) { vm := New() err := vm.Set("x", func(a func()) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(function() {})`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithFunctionInt(b *testing.B) { vm := New() err := vm.Set("x", func(a func(int)) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(function(n) {})`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkNativeCallWithFunctionString(b *testing.B) { vm := New() err := vm.Set("x", func(a func(string)) {}) require.NoError(b, err) s, err := vm.Compile("test.js", `x(function(n) {})`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func TestNativeCallWithString(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 string) { if a1 != "zzz" { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x("zzz")`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithFloat32(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 float32) { if a1 != 1.1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1.1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithFloat64(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 float64) { if a1 != 1.1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1.1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithInt(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 int) { if a1 != 1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithUint(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 uint) { if a1 != 1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithInt8(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 int8) { if a1 != 1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithUint8(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 uint8) { if a1 != 1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithInt16(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 int16) { if a1 != 1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithUint16(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 uint16) { if a1 != 1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithInt32(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 int32) { if a1 != 1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithUint32(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 uint32) { if a1 != 1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithInt64(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 int64) { if a1 != 1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithUint64(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 uint64) { if a1 != 1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithStringInt(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 string, a2 int) { if a1 != "zzz" || a2 != 1 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x("zzz", 1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntVariadic0(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a ...int) { if !reflect.DeepEqual(a, []int{}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x()`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntVariadic1(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a ...int) { if !reflect.DeepEqual(a, []int{1}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntVariadic3(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a ...int) { if !reflect.DeepEqual(a, []int{1, 2, 3}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1, 2, 3)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntVariadic10(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a ...int) { if !reflect.DeepEqual(a, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntArray0(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a []int) { if !reflect.DeepEqual(a, []int{}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x([])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntArray1(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a []int) { if !reflect.DeepEqual(a, []int{1}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x([1])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntArray3(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a []int) { if !reflect.DeepEqual(a, []int{1, 2, 3}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x([1, 2, 3])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntArray10(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a []int) { if !reflect.DeepEqual(a, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntVariadicArray0(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a ...int) { if !reflect.DeepEqual(a, []int{}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x([])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntVariadicArray1(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a ...int) { if !reflect.DeepEqual(a, []int{1}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x([1])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntVariadicArray3(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a ...int) { if !reflect.DeepEqual(a, []int{1, 2, 3}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x([1, 2, 3])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithIntVariadicArray10(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a ...int) { if !reflect.DeepEqual(a, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithStringIntVariadic0(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 string, a2 ...int) { if a1 != "a" || !reflect.DeepEqual(a2, []int{}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x("a")`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithStringIntVariadic1(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 string, a2 ...int) { if a1 != "a" || !reflect.DeepEqual(a2, []int{1}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x("a", 1)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithStringIntVariadic3(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 string, a2 ...int) { if a1 != "a" || !reflect.DeepEqual(a2, []int{1, 2, 3}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x("a", 1, 2, 3)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithStringIntVariadic10(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 string, a2 ...int) { if a1 != "a" || !reflect.DeepEqual(a2, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x("a", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithStringIntVariadicArray0(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 string, a2 ...int) { if a1 != "a" || !reflect.DeepEqual(a2, []int{}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x("a", [])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithStringIntVariadicArray1(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 string, a2 ...int) { if a1 != "a" || !reflect.DeepEqual(a2, []int{1}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x("a", [1])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithStringIntVariadicArray3(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 string, a2 ...int) { if a1 != "a" || !reflect.DeepEqual(a2, []int{1, 2, 3}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x("a", [1, 2, 3])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithStringIntVariadicArray10(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a1 string, a2 ...int) { if a1 != "a" || !reflect.DeepEqual(a2, []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x("a", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithMap(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a map[string]string) { if !reflect.DeepEqual(a, map[string]string{"a": "b", "c": "d"}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x({a: "b", c: "d"})`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithMapVariadic(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a ...map[string]string) { if !reflect.DeepEqual(a, []map[string]string{{"a": "b", "c": "d"}, {"w": "x", "y": "z"}}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x({a: "b", c: "d"}, {w: "x", y: "z"})`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithMapVariadicArray(t *testing.T) { vm := New() called := false err := vm.Set("x", func(a ...map[string]string) { if !reflect.DeepEqual(a, []map[string]string{{"a": "b", "c": "d"}, {"w": "x", "y": "z"}}) { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x([{a: "b", c: "d"}, {w: "x", y: "z"}])`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithFunctionVoidBool(t *testing.T) { vm := New() called := false err := vm.Set("x", func(fn func() bool) { if !fn() { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(function() { return true; })`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithFunctionIntInt(t *testing.T) { vm := New() called := false err := vm.Set("x", func(fn func(int) int) { if fn(5) != 5 { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(function(n) { return n; })`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallWithFunctionStringString(t *testing.T) { vm := New() called := false err := vm.Set("x", func(fn func(string) string) { if fn("zzz") != "zzz" { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `x(function(n) { return n; })`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } type testNativeCallWithStruct struct { Prefix string } type testNativeCallWithStructArg struct { Text string } func (t testNativeCallWithStruct) MakeStruct(s string) testNativeCallWithStructArg { return testNativeCallWithStructArg{Text: s} } func (t testNativeCallWithStruct) MakeStructPointer(s string) *testNativeCallWithStructArg { return &testNativeCallWithStructArg{Text: s} } func (t testNativeCallWithStruct) CallWithStruct(a testNativeCallWithStructArg) string { return t.Prefix + a.Text } func (t *testNativeCallWithStruct) CallPointerWithStruct(a testNativeCallWithStructArg) string { return t.Prefix + a.Text } func (t testNativeCallWithStruct) CallWithStructPointer(a *testNativeCallWithStructArg) string { return t.Prefix + a.Text } func (t *testNativeCallWithStruct) CallPointerWithStructPointer(a *testNativeCallWithStructArg) string { return t.Prefix + a.Text } func TestNativeCallMethodWithStruct(t *testing.T) { vm := New() called := false err := vm.Set("x", testNativeCallWithStruct{Prefix: "a"}) require.NoError(t, err) err = vm.Set("t", func(s string) { if s != testAb { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `t(x.CallWithStruct(x.MakeStruct("b")))`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallPointerMethodWithStruct(t *testing.T) { vm := New() called := false err := vm.Set("x", &testNativeCallWithStruct{Prefix: "a"}) require.NoError(t, err) err = vm.Set("t", func(s string) { if s != testAb { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `t(x.CallPointerWithStruct(x.MakeStruct("b")))`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallMethodWithStructPointer(t *testing.T) { vm := New() called := false err := vm.Set("x", testNativeCallWithStruct{Prefix: "a"}) require.NoError(t, err) err = vm.Set("t", func(s string) { if s != testAb { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `t(x.CallWithStructPointer(x.MakeStructPointer("b")))`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallPointerMethodWithStructPointer(t *testing.T) { vm := New() called := false err := vm.Set("x", &testNativeCallWithStruct{Prefix: "a"}) require.NoError(t, err) err = vm.Set("t", func(s string) { if s != testAb { t.Fail() } called = true }) require.NoError(t, err) s, err := vm.Compile("test.js", `t(x.CallPointerWithStructPointer(x.MakeStructPointer("b")))`) require.NoError(t, err) if _, err = vm.Run(s); err != nil { t.Logf("err should have been nil; was %s\n", err.Error()) t.Fail() } if !called { t.Fail() } } func TestNativeCallNilInterfaceArg(t *testing.T) { vm := New() err := vm.Set("f1", func(v interface{}) {}) require.NoError(t, err) _, err = vm.Call("f1", nil, nil) require.NoError(t, err) } ================================================ FILE: clone.go ================================================ package otto import ( "fmt" ) type cloner struct { runtime *runtime obj map[*object]*object objectstash map[*objectStash]*objectStash dclstash map[*dclStash]*dclStash fnstash map[*fnStash]*fnStash } func (rt *runtime) clone() *runtime { rt.lck.Lock() defer rt.lck.Unlock() out := &runtime{ debugger: rt.debugger, random: rt.random, stackLimit: rt.stackLimit, traceLimit: rt.traceLimit, } c := cloner{ runtime: out, obj: make(map[*object]*object), objectstash: make(map[*objectStash]*objectStash), dclstash: make(map[*dclStash]*dclStash), fnstash: make(map[*fnStash]*fnStash), } globalObject := c.object(rt.globalObject) out.globalStash = out.newObjectStash(globalObject, nil) out.globalObject = globalObject out.global = global{ c.object(rt.global.Object), c.object(rt.global.Function), c.object(rt.global.Array), c.object(rt.global.String), c.object(rt.global.Boolean), c.object(rt.global.Number), c.object(rt.global.Math), c.object(rt.global.Date), c.object(rt.global.RegExp), c.object(rt.global.Error), c.object(rt.global.EvalError), c.object(rt.global.TypeError), c.object(rt.global.RangeError), c.object(rt.global.ReferenceError), c.object(rt.global.SyntaxError), c.object(rt.global.URIError), c.object(rt.global.JSON), c.object(rt.global.ObjectPrototype), c.object(rt.global.FunctionPrototype), c.object(rt.global.ArrayPrototype), c.object(rt.global.StringPrototype), c.object(rt.global.BooleanPrototype), c.object(rt.global.NumberPrototype), c.object(rt.global.DatePrototype), c.object(rt.global.RegExpPrototype), c.object(rt.global.ErrorPrototype), c.object(rt.global.EvalErrorPrototype), c.object(rt.global.TypeErrorPrototype), c.object(rt.global.RangeErrorPrototype), c.object(rt.global.ReferenceErrorPrototype), c.object(rt.global.SyntaxErrorPrototype), c.object(rt.global.URIErrorPrototype), } out.eval = out.globalObject.property["eval"].value.(Value).value.(*object) out.globalObject.prototype = out.global.ObjectPrototype // Not sure if this is necessary, but give some help to the GC c.runtime = nil c.obj = nil c.objectstash = nil c.dclstash = nil c.fnstash = nil return out } func (c *cloner) object(in *object) *object { if out, exists := c.obj[in]; exists { return out } out := &object{} c.obj[in] = out return in.objectClass.clone(in, out, c) } func (c *cloner) dclStash(in *dclStash) (*dclStash, bool) { if out, exists := c.dclstash[in]; exists { return out, true } out := &dclStash{} c.dclstash[in] = out return out, false } func (c *cloner) objectStash(in *objectStash) (*objectStash, bool) { if out, exists := c.objectstash[in]; exists { return out, true } out := &objectStash{} c.objectstash[in] = out return out, false } func (c *cloner) fnStash(in *fnStash) (*fnStash, bool) { if out, exists := c.fnstash[in]; exists { return out, true } out := &fnStash{} c.fnstash[in] = out return out, false } func (c *cloner) value(in Value) Value { out := in if value, ok := in.value.(*object); ok { out.value = c.object(value) } return out } func (c *cloner) valueArray(in []Value) []Value { out := make([]Value, len(in)) for index, value := range in { out[index] = c.value(value) } return out } func (c *cloner) stash(in stasher) stasher { if in == nil { return nil } return in.clone(c) } func (c *cloner) property(in property) property { out := in switch value := in.value.(type) { case Value: out.value = c.value(value) case propertyGetSet: p := propertyGetSet{} if value[0] != nil { p[0] = c.object(value[0]) } if value[1] != nil { p[1] = c.object(value[1]) } out.value = p default: panic(fmt.Errorf("in.value.(Value) != true; in.value is %T", in.value)) } return out } func (c *cloner) dclProperty(in dclProperty) dclProperty { out := in out.value = c.value(in.value) return out } ================================================ FILE: clone_test.go ================================================ package otto import ( "testing" "github.com/stretchr/testify/require" ) func TestCloneGetterSetter(t *testing.T) { vm := New() _, err := vm.Run(`var x = Object.create(null, { x: { get: function() {}, set: function() {}, }, })`) require.NoError(t, err) require.NotPanics(t, func() { vm.Copy() }) } ================================================ FILE: cmpl.go ================================================ package otto import ( "github.com/robertkrimen/otto/ast" "github.com/robertkrimen/otto/file" ) type compiler struct { file *file.File program *ast.Program } ================================================ FILE: cmpl_evaluate.go ================================================ package otto import ( "strconv" ) func (rt *runtime) cmplEvaluateNodeProgram(node *nodeProgram, eval bool) Value { if !eval { rt.enterGlobalScope() defer rt.leaveScope() } rt.cmplFunctionDeclaration(node.functionList) rt.cmplVariableDeclaration(node.varList) rt.scope.frame.file = node.file return rt.cmplEvaluateNodeStatementList(node.body) } func (rt *runtime) cmplCallNodeFunction(function *object, stash *fnStash, node *nodeFunctionLiteral, argumentList []Value) Value { indexOfParameterName := make([]string, len(argumentList)) // function(abc, def, ghi) // indexOfParameterName[0] = "abc" // indexOfParameterName[1] = "def" // indexOfParameterName[2] = "ghi" // ... argumentsFound := false for index, name := range node.parameterList { if name == "arguments" { argumentsFound = true } value := Value{} if index < len(argumentList) { value = argumentList[index] indexOfParameterName[index] = name } // strict = false rt.scope.lexical.setValue(name, value, false) } if !argumentsFound { arguments := rt.newArgumentsObject(indexOfParameterName, stash, len(argumentList)) arguments.defineProperty("callee", objectValue(function), 0o101, false) stash.arguments = arguments // strict = false rt.scope.lexical.setValue("arguments", objectValue(arguments), false) for index := range argumentList { if index < len(node.parameterList) { continue } indexAsString := strconv.FormatInt(int64(index), 10) arguments.defineProperty(indexAsString, argumentList[index], 0o111, false) } } rt.cmplFunctionDeclaration(node.functionList) rt.cmplVariableDeclaration(node.varList) result := rt.cmplEvaluateNodeStatement(node.body) if result.kind == valueResult { return result } return Value{} } func (rt *runtime) cmplFunctionDeclaration(list []*nodeFunctionLiteral) { executionContext := rt.scope eval := executionContext.eval stash := executionContext.variable for _, function := range list { name := function.name value := rt.cmplEvaluateNodeExpression(function) if !stash.hasBinding(name) { stash.createBinding(name, eval, value) } else { // TODO 10.5.5.e stash.setBinding(name, value, false) // TODO strict } } } func (rt *runtime) cmplVariableDeclaration(list []string) { executionContext := rt.scope eval := executionContext.eval stash := executionContext.variable for _, name := range list { if !stash.hasBinding(name) { stash.createBinding(name, eval, Value{}) // TODO strict? } } } ================================================ FILE: cmpl_evaluate_expression.go ================================================ package otto import ( "fmt" "math" goruntime "runtime" "github.com/robertkrimen/otto/token" ) func (rt *runtime) cmplEvaluateNodeExpression(node nodeExpression) Value { // Allow interpreter interruption // If the Interrupt channel is nil, then // we avoid runtime.Gosched() overhead (if any) // FIXME: Test this if rt.otto.Interrupt != nil { goruntime.Gosched() select { case value := <-rt.otto.Interrupt: value() default: } } switch node := node.(type) { case *nodeArrayLiteral: return rt.cmplEvaluateNodeArrayLiteral(node) case *nodeAssignExpression: return rt.cmplEvaluateNodeAssignExpression(node) case *nodeBinaryExpression: if node.comparison { return rt.cmplEvaluateNodeBinaryExpressionComparison(node) } return rt.cmplEvaluateNodeBinaryExpression(node) case *nodeBracketExpression: return rt.cmplEvaluateNodeBracketExpression(node) case *nodeCallExpression: return rt.cmplEvaluateNodeCallExpression(node, nil) case *nodeConditionalExpression: return rt.cmplEvaluateNodeConditionalExpression(node) case *nodeDotExpression: return rt.cmplEvaluateNodeDotExpression(node) case *nodeFunctionLiteral: local := rt.scope.lexical if node.name != "" { local = rt.newDeclarationStash(local) } value := objectValue(rt.newNodeFunction(node, local)) if node.name != "" { local.createBinding(node.name, false, value) } return value case *nodeIdentifier: name := node.name // TODO Should be true or false (strictness) depending on context // getIdentifierReference should not return nil, but we check anyway and panic // so as not to propagate the nil into something else reference := getIdentifierReference(rt, rt.scope.lexical, name, false, at(node.idx)) if reference == nil { // Should never get here! panic(hereBeDragons("referenceError == nil: " + name)) } return toValue(reference) case *nodeLiteral: return node.value case *nodeNewExpression: return rt.cmplEvaluateNodeNewExpression(node) case *nodeObjectLiteral: return rt.cmplEvaluateNodeObjectLiteral(node) case *nodeRegExpLiteral: return objectValue(rt.newRegExpDirect(node.pattern, node.flags)) case *nodeSequenceExpression: return rt.cmplEvaluateNodeSequenceExpression(node) case *nodeThisExpression: return objectValue(rt.scope.this) case *nodeUnaryExpression: return rt.cmplEvaluateNodeUnaryExpression(node) case *nodeVariableExpression: return rt.cmplEvaluateNodeVariableExpression(node) default: panic(fmt.Sprintf("unknown node type: %T", node)) } } func (rt *runtime) cmplEvaluateNodeArrayLiteral(node *nodeArrayLiteral) Value { valueArray := []Value{} for _, node := range node.value { if node == nil { valueArray = append(valueArray, emptyValue) } else { valueArray = append(valueArray, rt.cmplEvaluateNodeExpression(node).resolve()) } } result := rt.newArrayOf(valueArray) return objectValue(result) } func (rt *runtime) cmplEvaluateNodeAssignExpression(node *nodeAssignExpression) Value { left := rt.cmplEvaluateNodeExpression(node.left) right := rt.cmplEvaluateNodeExpression(node.right) rightValue := right.resolve() result := rightValue if node.operator != token.ASSIGN { result = rt.calculateBinaryExpression(node.operator, left, rightValue) } rt.putValue(left.reference(), result) return result } func (rt *runtime) cmplEvaluateNodeBinaryExpression(node *nodeBinaryExpression) Value { left := rt.cmplEvaluateNodeExpression(node.left) leftValue := left.resolve() switch node.operator { // Logical case token.LOGICAL_AND: if !leftValue.bool() { return leftValue } right := rt.cmplEvaluateNodeExpression(node.right) return right.resolve() case token.LOGICAL_OR: if leftValue.bool() { return leftValue } right := rt.cmplEvaluateNodeExpression(node.right) return right.resolve() } return rt.calculateBinaryExpression(node.operator, leftValue, rt.cmplEvaluateNodeExpression(node.right)) } func (rt *runtime) cmplEvaluateNodeBinaryExpressionComparison(node *nodeBinaryExpression) Value { left := rt.cmplEvaluateNodeExpression(node.left).resolve() right := rt.cmplEvaluateNodeExpression(node.right).resolve() return boolValue(rt.calculateComparison(node.operator, left, right)) } func (rt *runtime) cmplEvaluateNodeBracketExpression(node *nodeBracketExpression) Value { target := rt.cmplEvaluateNodeExpression(node.left) targetValue := target.resolve() member := rt.cmplEvaluateNodeExpression(node.member) memberValue := member.resolve() // TODO Pass in base value as-is, and defer toObject till later? obj, err := rt.objectCoerce(targetValue) if err != nil { panic(rt.panicTypeError("Cannot access member %q of %s", memberValue.string(), err, at(node.idx))) } return toValue(newPropertyReference(rt, obj, memberValue.string(), false, at(node.idx))) } func (rt *runtime) cmplEvaluateNodeCallExpression(node *nodeCallExpression, withArgumentList []interface{}) Value { this := Value{} callee := rt.cmplEvaluateNodeExpression(node.callee) argumentList := []Value{} if withArgumentList != nil { argumentList = rt.toValueArray(withArgumentList...) } else { for _, argumentNode := range node.argumentList { argumentList = append(argumentList, rt.cmplEvaluateNodeExpression(argumentNode).resolve()) } } eval := false // Whether this call is a (candidate for) direct call to eval name := "" if rf := callee.reference(); rf != nil { switch rf := rf.(type) { case *propertyReference: name = rf.name this = objectValue(rf.base) eval = rf.name == "eval" // Possible direct eval case *stashReference: // TODO ImplicitThisValue name = rf.name eval = rf.name == "eval" // Possible direct eval default: // FIXME? panic(rt.panicTypeError("unexpected callee type %T to node call expression", rf)) } } atv := at(-1) switch callee := node.callee.(type) { case *nodeIdentifier: atv = at(callee.idx) case *nodeDotExpression: atv = at(callee.idx) case *nodeBracketExpression: atv = at(callee.idx) } frm := frame{ callee: name, file: rt.scope.frame.file, } vl := callee.resolve() if !vl.IsFunction() { if name == "" { // FIXME Maybe typeof? panic(rt.panicTypeError("%v is not a function", vl, atv)) } panic(rt.panicTypeError("%q is not a function", name, atv)) } rt.scope.frame.offset = int(atv) return vl.object().call(this, argumentList, eval, frm) } func (rt *runtime) cmplEvaluateNodeConditionalExpression(node *nodeConditionalExpression) Value { test := rt.cmplEvaluateNodeExpression(node.test) testValue := test.resolve() if testValue.bool() { return rt.cmplEvaluateNodeExpression(node.consequent) } return rt.cmplEvaluateNodeExpression(node.alternate) } func (rt *runtime) cmplEvaluateNodeDotExpression(node *nodeDotExpression) Value { target := rt.cmplEvaluateNodeExpression(node.left) targetValue := target.resolve() // TODO Pass in base value as-is, and defer toObject till later? obj, err := rt.objectCoerce(targetValue) if err != nil { panic(rt.panicTypeError("Cannot access member %q of %s", node.identifier, err, at(node.idx))) } return toValue(newPropertyReference(rt, obj, node.identifier, false, at(node.idx))) } func (rt *runtime) cmplEvaluateNodeNewExpression(node *nodeNewExpression) Value { callee := rt.cmplEvaluateNodeExpression(node.callee) argumentList := []Value{} for _, argumentNode := range node.argumentList { argumentList = append(argumentList, rt.cmplEvaluateNodeExpression(argumentNode).resolve()) } var name string if rf := callee.reference(); rf != nil { switch rf := rf.(type) { case *propertyReference: name = rf.name case *stashReference: name = rf.name default: panic(rt.panicTypeError("node new expression unexpected callee type %T", rf)) } } atv := at(-1) switch callee := node.callee.(type) { case *nodeIdentifier: atv = at(callee.idx) case *nodeDotExpression: atv = at(callee.idx) case *nodeBracketExpression: atv = at(callee.idx) } vl := callee.resolve() if !vl.IsFunction() { if name == "" { // FIXME Maybe typeof? panic(rt.panicTypeError("%v is not a function", vl, atv)) } panic(rt.panicTypeError("'%s' is not a function", name, atv)) } rt.scope.frame.offset = int(atv) return vl.object().construct(argumentList) } func (rt *runtime) cmplEvaluateNodeObjectLiteral(node *nodeObjectLiteral) Value { result := rt.newObject() for _, prop := range node.value { switch prop.kind { case "value": result.defineProperty(prop.key, rt.cmplEvaluateNodeExpression(prop.value).resolve(), 0o111, false) case "get": getter := rt.newNodeFunction(prop.value.(*nodeFunctionLiteral), rt.scope.lexical) descriptor := property{} descriptor.mode = 0o211 descriptor.value = propertyGetSet{getter, nil} result.defineOwnProperty(prop.key, descriptor, false) case "set": setter := rt.newNodeFunction(prop.value.(*nodeFunctionLiteral), rt.scope.lexical) descriptor := property{} descriptor.mode = 0o211 descriptor.value = propertyGetSet{nil, setter} result.defineOwnProperty(prop.key, descriptor, false) default: panic(fmt.Sprintf("unknown node object literal property kind %T", prop.kind)) } } return objectValue(result) } func (rt *runtime) cmplEvaluateNodeSequenceExpression(node *nodeSequenceExpression) Value { var result Value for _, node := range node.sequence { result = rt.cmplEvaluateNodeExpression(node) result = result.resolve() } return result } func (rt *runtime) cmplEvaluateNodeUnaryExpression(node *nodeUnaryExpression) Value { target := rt.cmplEvaluateNodeExpression(node.operand) switch node.operator { case token.TYPEOF, token.DELETE: if target.kind == valueReference && target.reference().invalid() { if node.operator == token.TYPEOF { return stringValue("undefined") } return trueValue } } switch node.operator { case token.NOT: targetValue := target.resolve() if targetValue.bool() { return falseValue } return trueValue case token.BITWISE_NOT: targetValue := target.resolve() integerValue := toInt32(targetValue) return int32Value(^integerValue) case token.PLUS: targetValue := target.resolve() return float64Value(targetValue.float64()) case token.MINUS: targetValue := target.resolve() value := targetValue.float64() // TODO Test this sign := float64(-1) if math.Signbit(value) { sign = 1 } return float64Value(math.Copysign(value, sign)) case token.INCREMENT: targetValue := target.resolve() if node.postfix { // Postfix++ oldValue := targetValue.float64() newValue := float64Value(+1 + oldValue) rt.putValue(target.reference(), newValue) return float64Value(oldValue) } // ++Prefix newValue := float64Value(+1 + targetValue.float64()) rt.putValue(target.reference(), newValue) return newValue case token.DECREMENT: targetValue := target.resolve() if node.postfix { // Postfix-- oldValue := targetValue.float64() newValue := float64Value(-1 + oldValue) rt.putValue(target.reference(), newValue) return float64Value(oldValue) } // --Prefix newValue := float64Value(-1 + targetValue.float64()) rt.putValue(target.reference(), newValue) return newValue case token.VOID: target.resolve() // FIXME Side effect? return Value{} case token.DELETE: reference := target.reference() if reference == nil { return trueValue } return boolValue(target.reference().delete()) case token.TYPEOF: targetValue := target.resolve() switch targetValue.kind { case valueUndefined: return stringValue("undefined") case valueNull: return stringValue("object") case valueBoolean: return stringValue("boolean") case valueNumber: return stringValue("number") case valueString: return stringValue("string") case valueObject: if targetValue.object().isCall() { return stringValue("function") } return stringValue("object") default: // FIXME ? } } panic(hereBeDragons()) } func (rt *runtime) cmplEvaluateNodeVariableExpression(node *nodeVariableExpression) Value { if node.initializer != nil { // FIXME If reference is nil left := getIdentifierReference(rt, rt.scope.lexical, node.name, false, at(node.idx)) right := rt.cmplEvaluateNodeExpression(node.initializer) rightValue := right.resolve() rt.putValue(left, rightValue) } return stringValue(node.name) } ================================================ FILE: cmpl_evaluate_statement.go ================================================ package otto import ( "fmt" goruntime "runtime" "github.com/robertkrimen/otto/token" ) func (rt *runtime) cmplEvaluateNodeStatement(node nodeStatement) Value { // Allow interpreter interruption // If the Interrupt channel is nil, then // we avoid runtime.Gosched() overhead (if any) // FIXME: Test this if rt.otto.Interrupt != nil { goruntime.Gosched() select { case value := <-rt.otto.Interrupt: value() default: } } switch node := node.(type) { case *nodeBlockStatement: labels := rt.labels rt.labels = nil value := rt.cmplEvaluateNodeStatementList(node.list) if value.kind == valueResult { if value.evaluateBreak(labels) == resultBreak { return emptyValue } } return value case *nodeBranchStatement: target := node.label switch node.branch { // FIXME Maybe node.kind? node.operator? case token.BREAK: return toValue(newBreakResult(target)) case token.CONTINUE: return toValue(newContinueResult(target)) default: panic(fmt.Errorf("unknown node branch token %T", node)) } case *nodeDebuggerStatement: if rt.debugger != nil { rt.debugger(rt.otto) } return emptyValue // Nothing happens. case *nodeDoWhileStatement: return rt.cmplEvaluateNodeDoWhileStatement(node) case *nodeEmptyStatement: return emptyValue case *nodeExpressionStatement: return rt.cmplEvaluateNodeExpression(node.expression) case *nodeForInStatement: return rt.cmplEvaluateNodeForInStatement(node) case *nodeForStatement: return rt.cmplEvaluateNodeForStatement(node) case *nodeIfStatement: return rt.cmplEvaluateNodeIfStatement(node) case *nodeLabelledStatement: rt.labels = append(rt.labels, node.label) defer func() { if len(rt.labels) > 0 { rt.labels = rt.labels[:len(rt.labels)-1] // Pop the label } else { rt.labels = nil } }() return rt.cmplEvaluateNodeStatement(node.statement) case *nodeReturnStatement: if node.argument != nil { return toValue(newReturnResult(rt.cmplEvaluateNodeExpression(node.argument).resolve())) } return toValue(newReturnResult(Value{})) case *nodeSwitchStatement: return rt.cmplEvaluateNodeSwitchStatement(node) case *nodeThrowStatement: value := rt.cmplEvaluateNodeExpression(node.argument).resolve() panic(newException(value)) case *nodeTryStatement: return rt.cmplEvaluateNodeTryStatement(node) case *nodeVariableStatement: // Variables are already defined, this is initialization only for _, variable := range node.list { rt.cmplEvaluateNodeVariableExpression(variable.(*nodeVariableExpression)) } return emptyValue case *nodeWhileStatement: return rt.cmplEvaluateModeWhileStatement(node) case *nodeWithStatement: return rt.cmplEvaluateNodeWithStatement(node) default: panic(fmt.Errorf("unknown node statement type %T", node)) } } func (rt *runtime) cmplEvaluateNodeStatementList(list []nodeStatement) Value { var result Value for _, node := range list { value := rt.cmplEvaluateNodeStatement(node) switch value.kind { case valueResult: return value case valueEmpty: default: // We have getValue here to (for example) trigger a // ReferenceError (of the not defined variety) // Not sure if this is the best way to error out early // for such errors or if there is a better way // TODO Do we still need this? result = value.resolve() } } return result } func (rt *runtime) cmplEvaluateNodeDoWhileStatement(node *nodeDoWhileStatement) Value { labels := append(rt.labels, "") //nolint:gocritic rt.labels = nil test := node.test result := emptyValue resultBreak: for { for _, node := range node.body { value := rt.cmplEvaluateNodeStatement(node) switch value.kind { case valueResult: switch value.evaluateBreakContinue(labels) { case resultReturn: return value case resultBreak: break resultBreak case resultContinue: goto resultContinue } case valueEmpty: default: result = value } } resultContinue: if !rt.cmplEvaluateNodeExpression(test).resolve().bool() { // Stahp: do ... while (false) break } } return result } func (rt *runtime) cmplEvaluateNodeForInStatement(node *nodeForInStatement) Value { labels := append(rt.labels, "") //nolint:gocritic rt.labels = nil source := rt.cmplEvaluateNodeExpression(node.source) sourceValue := source.resolve() switch sourceValue.kind { case valueUndefined, valueNull: return emptyValue } sourceObject := rt.toObject(sourceValue) into := node.into body := node.body result := emptyValue obj := sourceObject for obj != nil { enumerateValue := emptyValue obj.enumerate(false, func(name string) bool { into := rt.cmplEvaluateNodeExpression(into) // In the case of: for (var abc in def) ... if into.reference() == nil { identifier := into.string() // TODO Should be true or false (strictness) depending on context into = toValue(getIdentifierReference(rt, rt.scope.lexical, identifier, false, -1)) } rt.putValue(into.reference(), stringValue(name)) for _, node := range body { value := rt.cmplEvaluateNodeStatement(node) switch value.kind { case valueResult: switch value.evaluateBreakContinue(labels) { case resultReturn: enumerateValue = value return false case resultBreak: obj = nil return false case resultContinue: return true } case valueEmpty: default: enumerateValue = value } } return true }) if obj == nil { break } obj = obj.prototype if !enumerateValue.isEmpty() { result = enumerateValue } } return result } func (rt *runtime) cmplEvaluateNodeForStatement(node *nodeForStatement) Value { labels := append(rt.labels, "") //nolint:gocritic rt.labels = nil initializer := node.initializer test := node.test update := node.update body := node.body if initializer != nil { initialResult := rt.cmplEvaluateNodeExpression(initializer) initialResult.resolve() // Side-effect trigger } result := emptyValue resultBreak: for { if test != nil { testResult := rt.cmplEvaluateNodeExpression(test) testResultValue := testResult.resolve() if !testResultValue.bool() { break } } // this is to prevent for cycles with no body from running forever if len(body) == 0 && rt.otto.Interrupt != nil { goruntime.Gosched() select { case value := <-rt.otto.Interrupt: value() default: } } for _, node := range body { value := rt.cmplEvaluateNodeStatement(node) switch value.kind { case valueResult: switch value.evaluateBreakContinue(labels) { case resultReturn: return value case resultBreak: break resultBreak case resultContinue: goto resultContinue } case valueEmpty: default: result = value } } resultContinue: if update != nil { updateResult := rt.cmplEvaluateNodeExpression(update) updateResult.resolve() // Side-effect trigger } } return result } func (rt *runtime) cmplEvaluateNodeIfStatement(node *nodeIfStatement) Value { test := rt.cmplEvaluateNodeExpression(node.test) testValue := test.resolve() if testValue.bool() { return rt.cmplEvaluateNodeStatement(node.consequent) } else if node.alternate != nil { return rt.cmplEvaluateNodeStatement(node.alternate) } return emptyValue } func (rt *runtime) cmplEvaluateNodeSwitchStatement(node *nodeSwitchStatement) Value { labels := append(rt.labels, "") //nolint:gocritic rt.labels = nil discriminantResult := rt.cmplEvaluateNodeExpression(node.discriminant) target := node.defaultIdx for index, clause := range node.body { test := clause.test if test != nil { if rt.calculateComparison(token.STRICT_EQUAL, discriminantResult, rt.cmplEvaluateNodeExpression(test)) { target = index break } } } result := emptyValue if target != -1 { for _, clause := range node.body[target:] { for _, statement := range clause.consequent { value := rt.cmplEvaluateNodeStatement(statement) switch value.kind { case valueResult: switch value.evaluateBreak(labels) { case resultReturn: return value case resultBreak: return emptyValue } case valueEmpty: default: result = value } } } } return result } func (rt *runtime) cmplEvaluateNodeTryStatement(node *nodeTryStatement) Value { tryCatchValue, exep := rt.tryCatchEvaluate(func() Value { return rt.cmplEvaluateNodeStatement(node.body) }) if exep && node.catch != nil { outer := rt.scope.lexical rt.scope.lexical = rt.newDeclarationStash(outer) defer func() { rt.scope.lexical = outer }() // TODO If necessary, convert TypeError => TypeError // That, is, such errors can be thrown despite not being JavaScript "native" // strict = false rt.scope.lexical.setValue(node.catch.parameter, tryCatchValue, false) // FIXME node.CatchParameter // FIXME node.Catch tryCatchValue, exep = rt.tryCatchEvaluate(func() Value { return rt.cmplEvaluateNodeStatement(node.catch.body) }) } if node.finally != nil { finallyValue := rt.cmplEvaluateNodeStatement(node.finally) if finallyValue.kind == valueResult { return finallyValue } } if exep { panic(newException(tryCatchValue)) } return tryCatchValue } func (rt *runtime) cmplEvaluateModeWhileStatement(node *nodeWhileStatement) Value { test := node.test body := node.body labels := append(rt.labels, "") //nolint:gocritic rt.labels = nil result := emptyValue resultBreakContinue: for { if !rt.cmplEvaluateNodeExpression(test).resolve().bool() { // Stahp: while (false) ... break } for _, node := range body { value := rt.cmplEvaluateNodeStatement(node) switch value.kind { case valueResult: switch value.evaluateBreakContinue(labels) { case resultReturn: return value case resultBreak: break resultBreakContinue case resultContinue: continue resultBreakContinue } case valueEmpty: default: result = value } } } return result } func (rt *runtime) cmplEvaluateNodeWithStatement(node *nodeWithStatement) Value { obj := rt.cmplEvaluateNodeExpression(node.object) outer := rt.scope.lexical lexical := rt.newObjectStash(rt.toObject(obj.resolve()), outer) rt.scope.lexical = lexical defer func() { rt.scope.lexical = outer }() return rt.cmplEvaluateNodeStatement(node.body) } ================================================ FILE: cmpl_parse.go ================================================ package otto import ( "fmt" "github.com/robertkrimen/otto/ast" "github.com/robertkrimen/otto/file" "github.com/robertkrimen/otto/token" ) var ( trueLiteral = &nodeLiteral{value: boolValue(true)} falseLiteral = &nodeLiteral{value: boolValue(false)} nullLiteral = &nodeLiteral{value: nullValue} emptyStatement = &nodeEmptyStatement{} ) func (cmpl *compiler) parseExpression(expr ast.Expression) nodeExpression { if expr == nil { return nil } switch expr := expr.(type) { case *ast.ArrayLiteral: out := &nodeArrayLiteral{ value: make([]nodeExpression, len(expr.Value)), } for i, value := range expr.Value { out.value[i] = cmpl.parseExpression(value) } return out case *ast.AssignExpression: return &nodeAssignExpression{ operator: expr.Operator, left: cmpl.parseExpression(expr.Left), right: cmpl.parseExpression(expr.Right), } case *ast.BinaryExpression: return &nodeBinaryExpression{ operator: expr.Operator, left: cmpl.parseExpression(expr.Left), right: cmpl.parseExpression(expr.Right), comparison: expr.Comparison, } case *ast.BooleanLiteral: if expr.Value { return trueLiteral } return falseLiteral case *ast.BracketExpression: return &nodeBracketExpression{ idx: expr.Left.Idx0(), left: cmpl.parseExpression(expr.Left), member: cmpl.parseExpression(expr.Member), } case *ast.CallExpression: out := &nodeCallExpression{ callee: cmpl.parseExpression(expr.Callee), argumentList: make([]nodeExpression, len(expr.ArgumentList)), } for i, value := range expr.ArgumentList { out.argumentList[i] = cmpl.parseExpression(value) } return out case *ast.ConditionalExpression: return &nodeConditionalExpression{ test: cmpl.parseExpression(expr.Test), consequent: cmpl.parseExpression(expr.Consequent), alternate: cmpl.parseExpression(expr.Alternate), } case *ast.DotExpression: return &nodeDotExpression{ idx: expr.Left.Idx0(), left: cmpl.parseExpression(expr.Left), identifier: expr.Identifier.Name, } case *ast.EmptyExpression: return nil case *ast.FunctionLiteral: name := "" if expr.Name != nil { name = expr.Name.Name } out := &nodeFunctionLiteral{ name: name, body: cmpl.parseStatement(expr.Body), source: expr.Source, file: cmpl.file, } if expr.ParameterList != nil { list := expr.ParameterList.List out.parameterList = make([]string, len(list)) for i, value := range list { out.parameterList[i] = value.Name } } for _, value := range expr.DeclarationList { switch value := value.(type) { case *ast.FunctionDeclaration: out.functionList = append(out.functionList, cmpl.parseExpression(value.Function).(*nodeFunctionLiteral)) case *ast.VariableDeclaration: for _, value := range value.List { out.varList = append(out.varList, value.Name) } default: panic(fmt.Sprintf("parse expression unknown function declaration type %T", value)) } } return out case *ast.Identifier: return &nodeIdentifier{ idx: expr.Idx, name: expr.Name, } case *ast.NewExpression: out := &nodeNewExpression{ callee: cmpl.parseExpression(expr.Callee), argumentList: make([]nodeExpression, len(expr.ArgumentList)), } for i, value := range expr.ArgumentList { out.argumentList[i] = cmpl.parseExpression(value) } return out case *ast.NullLiteral: return nullLiteral case *ast.NumberLiteral: return &nodeLiteral{ value: toValue(expr.Value), } case *ast.ObjectLiteral: out := &nodeObjectLiteral{ value: make([]nodeProperty, len(expr.Value)), } for i, value := range expr.Value { out.value[i] = nodeProperty{ key: value.Key, kind: value.Kind, value: cmpl.parseExpression(value.Value), } } return out case *ast.RegExpLiteral: return &nodeRegExpLiteral{ flags: expr.Flags, pattern: expr.Pattern, } case *ast.SequenceExpression: out := &nodeSequenceExpression{ sequence: make([]nodeExpression, len(expr.Sequence)), } for i, value := range expr.Sequence { out.sequence[i] = cmpl.parseExpression(value) } return out case *ast.StringLiteral: return &nodeLiteral{ value: stringValue(expr.Value), } case *ast.ThisExpression: return &nodeThisExpression{} case *ast.UnaryExpression: return &nodeUnaryExpression{ operator: expr.Operator, operand: cmpl.parseExpression(expr.Operand), postfix: expr.Postfix, } case *ast.VariableExpression: return &nodeVariableExpression{ idx: expr.Idx0(), name: expr.Name, initializer: cmpl.parseExpression(expr.Initializer), } default: panic(fmt.Errorf("parse expression unknown node type %T", expr)) } } func (cmpl *compiler) parseStatement(stmt ast.Statement) nodeStatement { if stmt == nil { return nil } switch stmt := stmt.(type) { case *ast.BlockStatement: out := &nodeBlockStatement{ list: make([]nodeStatement, len(stmt.List)), } for i, value := range stmt.List { out.list[i] = cmpl.parseStatement(value) } return out case *ast.BranchStatement: out := &nodeBranchStatement{ branch: stmt.Token, } if stmt.Label != nil { out.label = stmt.Label.Name } return out case *ast.DebuggerStatement: return &nodeDebuggerStatement{} case *ast.DoWhileStatement: out := &nodeDoWhileStatement{ test: cmpl.parseExpression(stmt.Test), } body := cmpl.parseStatement(stmt.Body) if block, ok := body.(*nodeBlockStatement); ok { out.body = block.list } else { out.body = append(out.body, body) } return out case *ast.EmptyStatement: return emptyStatement case *ast.ExpressionStatement: return &nodeExpressionStatement{ expression: cmpl.parseExpression(stmt.Expression), } case *ast.ForInStatement: out := &nodeForInStatement{ into: cmpl.parseExpression(stmt.Into), source: cmpl.parseExpression(stmt.Source), } body := cmpl.parseStatement(stmt.Body) if block, ok := body.(*nodeBlockStatement); ok { out.body = block.list } else { out.body = append(out.body, body) } return out case *ast.ForStatement: out := &nodeForStatement{ initializer: cmpl.parseExpression(stmt.Initializer), update: cmpl.parseExpression(stmt.Update), test: cmpl.parseExpression(stmt.Test), } body := cmpl.parseStatement(stmt.Body) if block, ok := body.(*nodeBlockStatement); ok { out.body = block.list } else { out.body = append(out.body, body) } return out case *ast.FunctionStatement: return emptyStatement case *ast.IfStatement: return &nodeIfStatement{ test: cmpl.parseExpression(stmt.Test), consequent: cmpl.parseStatement(stmt.Consequent), alternate: cmpl.parseStatement(stmt.Alternate), } case *ast.LabelledStatement: return &nodeLabelledStatement{ label: stmt.Label.Name, statement: cmpl.parseStatement(stmt.Statement), } case *ast.ReturnStatement: return &nodeReturnStatement{ argument: cmpl.parseExpression(stmt.Argument), } case *ast.SwitchStatement: out := &nodeSwitchStatement{ discriminant: cmpl.parseExpression(stmt.Discriminant), defaultIdx: stmt.Default, body: make([]*nodeCaseStatement, len(stmt.Body)), } for i, clause := range stmt.Body { out.body[i] = &nodeCaseStatement{ test: cmpl.parseExpression(clause.Test), consequent: make([]nodeStatement, len(clause.Consequent)), } for j, value := range clause.Consequent { out.body[i].consequent[j] = cmpl.parseStatement(value) } } return out case *ast.ThrowStatement: return &nodeThrowStatement{ argument: cmpl.parseExpression(stmt.Argument), } case *ast.TryStatement: out := &nodeTryStatement{ body: cmpl.parseStatement(stmt.Body), finally: cmpl.parseStatement(stmt.Finally), } if stmt.Catch != nil { out.catch = &nodeCatchStatement{ parameter: stmt.Catch.Parameter.Name, body: cmpl.parseStatement(stmt.Catch.Body), } } return out case *ast.VariableStatement: out := &nodeVariableStatement{ list: make([]nodeExpression, len(stmt.List)), } for i, value := range stmt.List { out.list[i] = cmpl.parseExpression(value) } return out case *ast.WhileStatement: out := &nodeWhileStatement{ test: cmpl.parseExpression(stmt.Test), } body := cmpl.parseStatement(stmt.Body) if block, ok := body.(*nodeBlockStatement); ok { out.body = block.list } else { out.body = append(out.body, body) } return out case *ast.WithStatement: return &nodeWithStatement{ object: cmpl.parseExpression(stmt.Object), body: cmpl.parseStatement(stmt.Body), } default: panic(fmt.Sprintf("parse statement: unknown type %T", stmt)) } } func cmplParse(in *ast.Program) *nodeProgram { cmpl := compiler{ program: in, } if cmpl.program != nil { cmpl.file = cmpl.program.File } return cmpl.parse() } func (cmpl *compiler) parse() *nodeProgram { out := &nodeProgram{ body: make([]nodeStatement, len(cmpl.program.Body)), file: cmpl.program.File, } for i, value := range cmpl.program.Body { out.body[i] = cmpl.parseStatement(value) } for _, value := range cmpl.program.DeclarationList { switch value := value.(type) { case *ast.FunctionDeclaration: out.functionList = append(out.functionList, cmpl.parseExpression(value.Function).(*nodeFunctionLiteral)) case *ast.VariableDeclaration: for _, value := range value.List { out.varList = append(out.varList, value.Name) } default: panic(fmt.Sprintf("Here be dragons: cmpl.parseProgram.DeclarationList(%T)", value)) } } return out } type nodeProgram struct { file *file.File body []nodeStatement varList []string functionList []*nodeFunctionLiteral } type node interface{} type ( nodeExpression interface { node expressionNode() } nodeArrayLiteral struct { value []nodeExpression } nodeAssignExpression struct { left nodeExpression right nodeExpression operator token.Token } nodeBinaryExpression struct { left nodeExpression right nodeExpression operator token.Token comparison bool } nodeBracketExpression struct { left nodeExpression member nodeExpression idx file.Idx } nodeCallExpression struct { callee nodeExpression argumentList []nodeExpression } nodeConditionalExpression struct { test nodeExpression consequent nodeExpression alternate nodeExpression } nodeDotExpression struct { left nodeExpression identifier string idx file.Idx } nodeFunctionLiteral struct { body nodeStatement file *file.File name string source string parameterList []string varList []string functionList []*nodeFunctionLiteral } nodeIdentifier struct { name string idx file.Idx } nodeLiteral struct { value Value } nodeNewExpression struct { callee nodeExpression argumentList []nodeExpression } nodeObjectLiteral struct { value []nodeProperty } nodeProperty struct { value nodeExpression key string kind string } nodeRegExpLiteral struct { flags string pattern string // Value? } nodeSequenceExpression struct { sequence []nodeExpression } nodeThisExpression struct{} nodeUnaryExpression struct { operand nodeExpression operator token.Token postfix bool } nodeVariableExpression struct { initializer nodeExpression name string idx file.Idx } ) type ( nodeStatement interface { node statementNode() } nodeBlockStatement struct { list []nodeStatement } nodeBranchStatement struct { label string branch token.Token } nodeCaseStatement struct { test nodeExpression consequent []nodeStatement } nodeCatchStatement struct { body nodeStatement parameter string } nodeDebuggerStatement struct{} nodeDoWhileStatement struct { test nodeExpression body []nodeStatement } nodeEmptyStatement struct{} nodeExpressionStatement struct { expression nodeExpression } nodeForInStatement struct { into nodeExpression source nodeExpression body []nodeStatement } nodeForStatement struct { initializer nodeExpression update nodeExpression test nodeExpression body []nodeStatement } nodeIfStatement struct { test nodeExpression consequent nodeStatement alternate nodeStatement } nodeLabelledStatement struct { statement nodeStatement label string } nodeReturnStatement struct { argument nodeExpression } nodeSwitchStatement struct { discriminant nodeExpression body []*nodeCaseStatement defaultIdx int } nodeThrowStatement struct { argument nodeExpression } nodeTryStatement struct { body nodeStatement catch *nodeCatchStatement finally nodeStatement } nodeVariableStatement struct { list []nodeExpression } nodeWhileStatement struct { test nodeExpression body []nodeStatement } nodeWithStatement struct { object nodeExpression body nodeStatement } ) // expressionNode. func (*nodeArrayLiteral) expressionNode() {} func (*nodeAssignExpression) expressionNode() {} func (*nodeBinaryExpression) expressionNode() {} func (*nodeBracketExpression) expressionNode() {} func (*nodeCallExpression) expressionNode() {} func (*nodeConditionalExpression) expressionNode() {} func (*nodeDotExpression) expressionNode() {} func (*nodeFunctionLiteral) expressionNode() {} func (*nodeIdentifier) expressionNode() {} func (*nodeLiteral) expressionNode() {} func (*nodeNewExpression) expressionNode() {} func (*nodeObjectLiteral) expressionNode() {} func (*nodeRegExpLiteral) expressionNode() {} func (*nodeSequenceExpression) expressionNode() {} func (*nodeThisExpression) expressionNode() {} func (*nodeUnaryExpression) expressionNode() {} func (*nodeVariableExpression) expressionNode() {} // statementNode func (*nodeBlockStatement) statementNode() {} func (*nodeBranchStatement) statementNode() {} func (*nodeCaseStatement) statementNode() {} func (*nodeCatchStatement) statementNode() {} func (*nodeDebuggerStatement) statementNode() {} func (*nodeDoWhileStatement) statementNode() {} func (*nodeEmptyStatement) statementNode() {} func (*nodeExpressionStatement) statementNode() {} func (*nodeForInStatement) statementNode() {} func (*nodeForStatement) statementNode() {} func (*nodeIfStatement) statementNode() {} func (*nodeLabelledStatement) statementNode() {} func (*nodeReturnStatement) statementNode() {} func (*nodeSwitchStatement) statementNode() {} func (*nodeThrowStatement) statementNode() {} func (*nodeTryStatement) statementNode() {} func (*nodeVariableStatement) statementNode() {} func (*nodeWhileStatement) statementNode() {} func (*nodeWithStatement) statementNode() {} ================================================ FILE: cmpl_test.go ================================================ package otto import ( "testing" "github.com/robertkrimen/otto/parser" ) func Test_cmpl(t *testing.T) { tt(t, func() { vm := New() test := func(src string, expect ...interface{}) { program, err := parser.ParseFile(nil, "", src, 0) is(err, nil) { program := cmplParse(program) value := vm.runtime.cmplEvaluateNodeProgram(program, false) if len(expect) > 0 { is(value, expect[0]) } } } test(``, Value{}) test(`var abc = 1; abc;`, 1) test(`var abc = 1 + 1; abc;`, 2) test(`1 + 2;`, 3) }) } func TestParse_cmpl(t *testing.T) { tt(t, func() { test := func(src string) { program, err := parser.ParseFile(nil, "", src, 0) is(err, nil) is(cmplParse(program), "!=", nil) } test(``) test(`var abc = 1; abc;`) test(` function abc() { return; } `) }) } ================================================ FILE: console.go ================================================ package otto import ( "fmt" "os" "strings" ) func formatForConsole(argumentList []Value) string { output := []string{} for _, argument := range argumentList { output = append(output, fmt.Sprintf("%v", argument)) } return strings.Join(output, " ") } func builtinConsoleLog(call FunctionCall) Value { fmt.Fprintln(os.Stdout, formatForConsole(call.ArgumentList)) //nolint:errcheck // Nothing we can do if this fails. return Value{} } func builtinConsoleError(call FunctionCall) Value { fmt.Fprintln(os.Stdout, formatForConsole(call.ArgumentList)) //nolint:errcheck // Nothing we can do if this fails. return Value{} } // Nothing happens. func builtinConsoleDir(call FunctionCall) Value { return Value{} } func builtinConsoleTime(call FunctionCall) Value { return Value{} } func builtinConsoleTimeEnd(call FunctionCall) Value { return Value{} } func builtinConsoleTrace(call FunctionCall) Value { return Value{} } func builtinConsoleAssert(call FunctionCall) Value { return Value{} } ================================================ FILE: consts.go ================================================ package otto const ( // Common classes. classStringName = "String" classGoArrayName = "GoArray" classGoSliceName = "GoSlice" classNumberName = "Number" classDateName = "Date" classArrayName = "Array" classFunctionName = "Function" classObjectName = "Object" classRegExpName = "RegExp" classBooleanName = "Boolean" classMathName = "Math" classJSONName = "JSON" // Error classes. classErrorName = "Error" classEvalErrorName = "EvalError" classTypeErrorName = "TypeError" classRangeErrorName = "RangeError" classReferenceErrorName = "ReferenceError" classSyntaxErrorName = "SyntaxError" classURIErrorName = "URIError" // Common properties. propertyName = "name" propertyLength = "length" propertyPrototype = "prototype" propertyConstructor = "constructor" // Common methods. methodToString = "toString" ) ================================================ FILE: date_test.go ================================================ package otto import ( "math" "testing" "time" ) func mockTimeLocal(location *time.Location) func() { local := time.Local time.Local = location return func() { time.Local = local } } // Passing or failing should not be dependent on what time zone we're in. func mockUTC() func() { return mockTimeLocal(time.UTC) } func TestDate(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() time0 := time.Unix(1348616313, 47*1000*1000).Local() test(`Date`, "function Date() { [native code] }") test(`new Date(0).toUTCString()`, "Thu, 01 Jan 1970 00:00:00 GMT") test(`new Date(0).toGMTString()`, "Thu, 01 Jan 1970 00:00:00 GMT") test(`new Date('2023').toGMTString()`, "Sun, 01 Jan 2023 00:00:00 GMT") test(`new Date('2023/02').toGMTString()`, "Wed, 01 Feb 2023 00:00:00 GMT") test(`new Date('2023/02/23').toGMTString()`, "Thu, 23 Feb 2023 00:00:00 GMT") test(`new Date('2023/02/23 11:23:57').toGMTString()`, "Thu, 23 Feb 2023 11:23:57 GMT") if false { // TODO toLocale{Date,Time}String test(`new Date(0).toLocaleString()`, "") test(`new Date(0).toLocaleDateString()`, "") test(`new Date(0).toLocaleTimeString()`, "") } test(`new Date(1348616313).getTime()`, 1348616313) test(`new Date(1348616313).toUTCString()`, "Fri, 16 Jan 1970 14:36:56 GMT") test(`abc = new Date(1348616313047); abc.toUTCString()`, "Tue, 25 Sep 2012 23:38:33 GMT") test(`abc.getYear()`, time0.Year()-1900) test(`abc.getFullYear()`, time0.Year()) test(`abc.getUTCFullYear()`, 2012) test(`abc.getMonth()`, int(time0.Month())-1) // Remember, the JavaScript month is 0-based test(`abc.getUTCMonth()`, 8) test(`abc.getDate()`, time0.Day()) test(`abc.getUTCDate()`, 25) test(`abc.getDay()`, int(time0.Weekday())) test(`abc.getUTCDay()`, 2) test(`abc.getHours()`, time0.Hour()) test(`abc.getUTCHours()`, 23) test(`abc.getMinutes()`, time0.Minute()) test(`abc.getUTCMinutes()`, 38) test(`abc.getSeconds()`, time0.Second()) test(`abc.getUTCSeconds()`, 33) test(`abc.getMilliseconds()`, time0.Nanosecond()/(1000*1000)) // In honor of the 47% test(`abc.getUTCMilliseconds()`, 47) _, offset := time0.Zone() test(`abc.getTimezoneOffset()`, offset/-60) test(`new Date("Xyzzy").getTime()`, math.NaN()) test(`abc.setFullYear(2011); abc.toUTCString()`, "Sun, 25 Sep 2011 23:38:33 GMT") test(`new Date(12564504e5).toUTCString()`, "Sun, 25 Oct 2009 06:00:00 GMT") test(`new Date(2009, 9, 25).toUTCString()`, "Sun, 25 Oct 2009 00:00:00 GMT") test(`+(new Date(2009, 9, 25))`, int64(1256428800000)) format := "Mon, 2 Jan 2006 15:04:05 MST" time1 := time.Unix(1256450400, 0) time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), 2001*1000*1000, time1.Location()).In(utcTimeZone) test(`abc = new Date(12564504e5); abc.setMilliseconds(2001); abc.toUTCString()`, time0.Format(format)) time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), time1.Hour(), time1.Minute(), 61, time1.Nanosecond(), time1.Location()).In(utcTimeZone) test(`abc = new Date(12564504e5); abc.setSeconds("61"); abc.toUTCString()`, time0.Format(format)) time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), time1.Hour(), 61, time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone) test(`abc = new Date(12564504e5); abc.setMinutes("61"); abc.toUTCString()`, time0.Format(format)) time0 = time.Date(time1.Year(), time1.Month(), time1.Day(), 5, time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone) test(`abc = new Date(12564504e5); abc.setHours("5"); abc.toUTCString()`, time0.Format(format)) time0 = time.Date(time1.Year(), time1.Month(), 26, time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone) test(`abc = new Date(12564504e5); abc.setDate("26"); abc.toUTCString()`, time0.Format(format)) time0 = time.Date(time1.Year(), 10, time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone) test(`abc = new Date(12564504e5); abc.setMonth(9); abc.toUTCString()`, time0.Format(format)) test(`abc = new Date(12564504e5); abc.setMonth("09"); abc.toUTCString()`, time0.Format(format)) time0 = time.Date(time1.Year(), 11, time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone) test(`abc = new Date(12564504e5); abc.setMonth("10"); abc.toUTCString()`, time0.Format(format)) time0 = time.Date(2010, time1.Month(), time1.Day(), time1.Hour(), time1.Minute(), time1.Second(), time1.Nanosecond(), time1.Location()).In(utcTimeZone) test(`abc = new Date(12564504e5); abc.setFullYear(2010); abc.toUTCString()`, time0.Format(format)) test(`new Date("2001-01-01T10:01:02.000").getTime()`, int64(978343262000)) // Date() test(`typeof Date()`, "string") test(`typeof Date(2006, 1, 2)`, "string") test(` abc = Object.getOwnPropertyDescriptor(Date, "parse"); [ abc.value === Date.parse, abc.writable, abc.enumerable, abc.configurable ]; `, "true,true,false,true") test(` abc = Object.getOwnPropertyDescriptor(Date.prototype, "toTimeString"); [ abc.value === Date.prototype.toTimeString, abc.writable, abc.enumerable, abc.configurable ]; `, "true,true,false,true") test(` var abc = Object.getOwnPropertyDescriptor(Date, "prototype"); [ [ typeof Date.prototype ], [ abc.writable, abc.enumerable, abc.configurable ] ]; `, "object,false,false,false") }) } func TestDate_parse(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(`Date.parse("2001-01-01T10:01:02.000")`, int64(978343262000)) test(`Date.parse("2006-01-02T15:04:05.000")`, int64(1136214245000)) test(`Date.parse("2006")`, int64(1136073600000)) test(`Date.parse("1970-01-16T14:36:56+00:00")`, 1348616000) test(`Date.parse("1970-01-16T14:36:56.313+00:00")`, 1348616313) test(`Date.parse("1970-01-16T14:36:56.000")`, 1348616000) test(`Date.parse.length`, 1) }) } func TestDate_UTC(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(`Date.UTC(2009, 9, 25)`, int64(1256428800000)) test(`Date.UTC.length`, 7) }) } func TestDate_now(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() // FIXME I think this too risky test(`+(""+Date.now()).substr(0, 10)`, float64(epochToInteger(timeToEpoch(time.Now()))/1000)) test(`Date.now() - Date.now(1,2,3) < 24 * 60 * 60`, true) }) } func TestDate_toISOString(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(`new Date(0).toISOString()`, "1970-01-01T00:00:00.000Z") }) } func TestDate_toJSON(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(`new Date(0).toJSON()`, "1970-01-01T00:00:00.000Z") }) } func TestDate_setYear(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(`new Date(12564504e5).setYear(96)`, int64(846223200000)) test(`new Date(12564504e5).setYear(1996)`, int64(846223200000)) test(`new Date(12564504e5).setYear(2000)`, int64(972453600000)) }) } func TestDateDefaultValue(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` var date = new Date(); date + 0 === date.toString() + "0"; `, true) }) } func TestDate_April1978(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` var abc = new Date(1978,3); [ abc.getYear(), abc.getMonth(), abc.valueOf() ]; `, "78,3,260236800000") }) } func TestDate_setMilliseconds(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` abc = new Date(); def = abc.setMilliseconds(); [ abc, def ]; `, "Invalid Date,NaN") }) } func TestDate_new(t *testing.T) { // FIXME? // This is probably incorrect, due to differences in Go date/time handling // versus ECMA date/time handling, but we'll leave this here for // future reference if true { return } tt(t, func() { test, _ := test() defer mockUTC()() test(` [ new Date(1899, 11).valueOf(), new Date(1899, 12).valueOf(), new Date(1900, 0).valueOf() ] `, "-2211638400000,-2208960000000,-2208960000000") }) } func TestDateComparison(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` var now0 = Date.now(); var now1 = (new Date()).toString(); [ now0 === now1, Math.abs(now0 - Date.parse(now1)) <= 1000 ]; `, "false,true") }) } func TestDate_setSeconds(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` abc = new Date(1980, 10); def = new Date(abc); abc.setSeconds(10, 12); def.setSeconds(10); def.setMilliseconds(12); abc.valueOf() === def.valueOf(); `, true) test(` abc = new Date(1980, 10); def = new Date(abc); abc.setUTCSeconds(10, 12); def.setUTCSeconds(10); def.setUTCMilliseconds(12); abc.valueOf() === def.valueOf(); `, true) test(`Date.prototype.setSeconds.length`, 2) test(`Date.prototype.setUTCSeconds.length`, 2) }) } func TestDate_setMinutes(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` abc = new Date(1980, 10); def = new Date(abc); abc.setMinutes(8, 10, 12); def.setMinutes(8); def.setSeconds(10); def.setMilliseconds(12); abc.valueOf() === def.valueOf(); `, true) test(` abc = new Date(1980, 10); def = new Date(abc); abc.setUTCMinutes(8, 10, 12); def.setUTCMinutes(8); def.setUTCSeconds(10); def.setUTCMilliseconds(12); abc.valueOf() === def.valueOf(); `, true) test(`Date.prototype.setMinutes.length`, 3) test(`Date.prototype.setUTCMinutes.length`, 3) }) } func TestDate_setHours(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` abc = new Date(1980, 10); def = new Date(abc); abc.setHours(6, 8, 10, 12); def.setHours(6); def.setMinutes(8); def.setSeconds(10); def.setMilliseconds(12); abc.valueOf() === def.valueOf(); `, true) test(` abc = new Date(1980, 10); def = new Date(abc); abc.setUTCHours(6, 8, 10, 12); def.setUTCHours(6); def.setUTCMinutes(8); def.setUTCSeconds(10); def.setUTCMilliseconds(12); abc.valueOf() === def.valueOf(); `, true) test(`Date.prototype.setHours.length`, 4) test(`Date.prototype.setUTCHours.length`, 4) }) } func TestDate_setMonth(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` abc = new Date(1980, 10); def = new Date(abc); abc.setMonth(6, 8); def.setMonth(6); def.setDate(8); abc.valueOf() === def.valueOf(); `, true) test(` abc = new Date(1980, 10); def = new Date(abc); abc.setUTCMonth(6, 8); def.setUTCMonth(6); def.setUTCDate(8); abc.valueOf() === def.valueOf(); `, true) test(`Date.prototype.setMonth.length`, 2) test(`Date.prototype.setUTCMonth.length`, 2) }) } func TestDate_setFullYear(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` abc = new Date(1980, 10); def = new Date(abc); abc.setFullYear(1981, 6, 8); def.setFullYear(1981); def.setMonth(6); def.setDate(8); abc.valueOf() === def.valueOf(); `, true) test(` abc = new Date(1980, 10); def = new Date(abc); abc.setUTCFullYear(1981, 6, 8); def.setUTCFullYear(1981); def.setUTCMonth(6); def.setUTCDate(8); abc.valueOf() === def.valueOf(); `, true) test(`Date.prototype.setFullYear.length`, 3) test(`Date.prototype.setUTCFullYear.length`, 3) }) } func TestDate_setTime(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` var abc = new Date(1999, 6, 1); var def = new Date(); def.setTime(abc.getTime()); [ def, abc.valueOf() == def.valueOf() ]; `, "Thu, 01 Jul 1999 00:00:00 UTC,true") test(`Date.prototype.setTime.length`, 1) }) } ================================================ FILE: dbg/dbg.go ================================================ // This file was AUTOMATICALLY GENERATED by dbg-import (smuggol) from github.com/robertkrimen/dbg /* Package dbg is a println/printf/log-debugging utility library. import ( Dbg "github.com/robertkrimen/dbg" ) dbg, dbgf := Dbg.New() dbg("Emit some debug stuff", []byte{120, 121, 122, 122, 121}, math.Pi) # "2013/01/28 16:50:03 Emit some debug stuff [120 121 122 122 121] 3.141592653589793" dbgf("With a %s formatting %.2f", "little", math.Pi) # "2013/01/28 16:51:55 With a little formatting (3.14)" dbgf("%/fatal//A fatal debug statement: should not be here") # "A fatal debug statement: should not be here" # ...and then, os.Exit(1) dbgf("%/panic//Can also panic %s", "this") # "Can also panic this" # ...as a panic, equivalent to: panic("Can also panic this") dbgf("Any %s arguments without a corresponding %%", "extra", "are treated like arguments to dbg()") # "2013/01/28 17:14:40 Any extra arguments (without a corresponding %) are treated like arguments to dbg()" dbgf("%d %d", 1, 2, 3, 4, 5) # "2013/01/28 17:16:32 Another example: 1 2 3 4 5" dbgf("%@: Include the function name for a little context (via %s)", "%@") # "2013... github.com/robertkrimen/dbg.TestSynopsis: Include the function name for a little context (via %@)" By default, dbg uses log (log.Println, log.Printf, log.Panic, etc.) for output. However, you can also provide your own output destination by invoking dbg.New with a customization function: import ( "bytes" Dbg "github.com/robertkrimen/dbg" "os" ) # dbg to os.Stderr dbg, dbgf := Dbg.New(func(dbgr *Dbgr) { dbgr.SetOutput(os.Stderr) }) # A slightly contrived example: var buffer bytes.Buffer dbg, dbgf := New(func(dbgr *Dbgr) { dbgr.SetOutput(&buffer) }) */ package dbg import ( "bytes" "fmt" "io" "log" "os" "regexp" goruntime "runtime" "strings" "unicode" ) type _frmt struct { ctl string format string operandCount int panic bool fatal bool check bool } var ( ctlTest = regexp.MustCompile(`^\s*%/`) ctlScan = regexp.MustCompile(`%?/(panic|fatal|check)(?:\s|$)`) ) func operandCount(format string) int { count := 0 end := len(format) for at := 0; at < end; { for at < end && format[at] != '%' { at++ } at++ if at < end { if format[at] != '%' && format[at] != '@' { count++ } at++ } } return count } func parseFormat(format string) (frmt _frmt) { if ctlTest.MatchString(format) { format = strings.TrimLeftFunc(format, unicode.IsSpace) index := strings.Index(format, "//") if index != -1 { frmt.ctl = format[0:index] format = format[index+2:] // Skip the second slash via +2 (instead of +1) } else { frmt.ctl = format format = "" } for _, tmp := range ctlScan.FindAllStringSubmatch(frmt.ctl, -1) { for _, value := range tmp[1:] { switch value { case "panic": frmt.panic = true case "fatal": frmt.fatal = true case "check": frmt.check = true } } } } frmt.format = format frmt.operandCount = operandCount(format) return } type Dbgr struct { emit emit } type DbgFunction func(values ...interface{}) func NewDbgr() *Dbgr { return &Dbgr{} } /* New will create and return a pair of debugging functions. You can customize where they output to by passing in an (optional) customization function: import ( Dbg "github.com/robertkrimen/dbg" "os" ) # dbg to os.Stderr dbg, dbgf := Dbg.New(func(dbgr *Dbgr) { dbgr.SetOutput(os.Stderr) }) */ func New(options ...interface{}) (dbg DbgFunction, dbgf DbgFunction) { dbgr := NewDbgr() if len(options) > 0 { if fn, ok := options[0].(func(*Dbgr)); ok { fn(dbgr) } } return dbgr.DbgDbgf() } func (d Dbgr) Dbg(values ...interface{}) { d.getEmit().emit(_frmt{}, "", values...) } func (d Dbgr) Dbgf(values ...interface{}) { d.dbgf(values...) } func (d Dbgr) DbgDbgf() (dbg DbgFunction, dbgf DbgFunction) { dbg = func(vl ...interface{}) { d.Dbg(vl...) } dbgf = func(vl ...interface{}) { d.dbgf(vl...) } return dbg, dbgf // Redundant, but... } func (d Dbgr) dbgf(values ...interface{}) { var frmt _frmt if len(values) > 0 { tmp := fmt.Sprint(values[0]) frmt = parseFormat(tmp) values = values[1:] } buf := bytes.Buffer{} format := frmt.format end := len(format) for at := 0; at < end; { last := at for at < end && format[at] != '%' { at++ } if at > last { buf.WriteString(format[last:at]) } if at >= end { break } // format[at] == '%' at++ // format[at] == ? if format[at] == '@' { depth := 2 pc, _, _, _ := goruntime.Caller(depth) name := goruntime.FuncForPC(pc).Name() buf.WriteString(name) } else { buf.WriteString(format[at-1 : at+1]) } at++ } //valuesF := append([]interface{}{}, values[0:frmt.operandCount]...) valuesF := values[0:frmt.operandCount] valuesDbg := values[frmt.operandCount:] if len(valuesDbg) > 0 { // Adjust frmt.format: // (%v instead of %s because: frmt.check) tmp := format if len(tmp) > 0 { if unicode.IsSpace(rune(tmp[len(tmp)-1])) { buf.WriteString("%v") } else { buf.WriteString(" %v") } } else if frmt.check { // Performing a check, so no output } else { buf.WriteString("%v") } // Adjust valuesF: if !frmt.check { tmp := []string{} for _, value := range valuesDbg { tmp = append(tmp, fmt.Sprintf("%v", value)) } // First, make a copy of valuesF, so we avoid overwriting valuesDbg when appending valuesF = append([]interface{}{}, valuesF...) valuesF = append(valuesF, strings.Join(tmp, " ")) } } format = buf.String() if frmt.check { // We do not actually emit to the log, but panic if // a non-nil value is detected (e.g. a non-nil error) for _, value := range valuesDbg { if value != nil { if format == "" { panic(value) } else { panic(fmt.Sprintf(format, append(valuesF, value)...)) } } } } else { d.getEmit().emit(frmt, format, valuesF...) } } // Idiot-proof &Dbgr{}, etc. func (d *Dbgr) getEmit() emit { if d.emit == nil { d.emit = standardEmit() } return d.emit } // SetOutput will accept the following as a destination for output: // // *log.Logger Print*/Panic*/Fatal* of the logger // io.Writer - // nil Reset to the default output (os.Stderr) // "log" Print*/Panic*/Fatal* via the "log" package func (d *Dbgr) SetOutput(output interface{}) { if output == nil { d.emit = standardEmit() return } switch output := output.(type) { case *log.Logger: d.emit = emitLogger{ logger: output, } return case io.Writer: d.emit = emitWriter{ writer: output, } return case string: if output == "log" { d.emit = emitLog{} return } } panic(output) } // ======== // // = emit = // // ======== // func standardEmit() emit { return emitWriter{ writer: os.Stderr, } } func ln(tmp string) string { length := len(tmp) if length > 0 && tmp[length-1] != '\n' { return tmp + "\n" } return tmp } type emit interface { emit(_frmt, string, ...interface{}) } type emitWriter struct { writer io.Writer } func (ew emitWriter) emit(frmt _frmt, format string, values ...interface{}) { if format == "" { fmt.Fprintln(ew.writer, values...) } else { if frmt.panic { panic(fmt.Sprintf(format, values...)) } fmt.Fprintf(ew.writer, ln(format), values...) if frmt.fatal { os.Exit(1) } } } type emitLogger struct { logger *log.Logger } func (el emitLogger) emit(frmt _frmt, format string, values ...interface{}) { if format == "" { el.logger.Println(values...) } else { if frmt.panic { el.logger.Panicf(format, values...) } else if frmt.fatal { el.logger.Fatalf(format, values...) } else { el.logger.Printf(format, values...) } } } type emitLog struct { } func (el emitLog) emit(frmt _frmt, format string, values ...interface{}) { if format == "" { log.Println(values...) } else { if frmt.panic { log.Panicf(format, values...) } else if frmt.fatal { log.Fatalf(format, values...) } else { log.Printf(format, values...) } } } ================================================ FILE: dbg.go ================================================ // This file was AUTOMATICALLY GENERATED by dbg-import (smuggol) for github.com/robertkrimen/dbg package otto import ( Dbg "github.com/robertkrimen/otto/dbg" ) var dbg, dbgf = Dbg.New() ================================================ FILE: documentation_test.go ================================================ package otto import ( "fmt" "os" ) func ExampleSynopsis() { //nolint:govet vm := New() _, err := vm.Run(` abc = 2 + 2; console.log("The value of abc is " + abc); // 4 `) if err != nil { fmt.Fprintln(os.Stderr, err) return } value, err := vm.Get("abc") if err != nil { fmt.Fprintln(os.Stderr, err) return } iv, err := value.ToInteger() if err != nil { fmt.Fprintln(os.Stderr, err) return } fmt.Println(iv) err = vm.Set("def", 11) if err != nil { fmt.Fprintln(os.Stderr, err) return } _, err = vm.Run(` console.log("The value of def is " + def); `) if err != nil { fmt.Fprintln(os.Stderr, err) return } err = vm.Set("xyzzy", "Nothing happens.") if err != nil { fmt.Fprintln(os.Stderr, err) return } _, err = vm.Run(` console.log(xyzzy.length); `) if err != nil { fmt.Fprintln(os.Stderr, err) return } value, err = vm.Run("xyzzy.length") if err != nil { fmt.Fprintln(os.Stderr, err) return } iv, err = value.ToInteger() if err != nil { fmt.Fprintln(os.Stderr, err) return } fmt.Println(iv) value, err = vm.Run("abcdefghijlmnopqrstuvwxyz.length") fmt.Println(value) fmt.Println(err) // Expected error. err = vm.Set("sayHello", func(call FunctionCall) Value { fmt.Printf("Hello, %s.\n", call.Argument(0).String()) return UndefinedValue() }) if err != nil { fmt.Fprintln(os.Stderr, err) return } err = vm.Set("twoPlus", func(call FunctionCall) Value { right, _ := call.Argument(0).ToInteger() result, _ := vm.ToValue(2 + right) return result }) if err != nil { fmt.Fprintln(os.Stderr, err) return } value, err = vm.Run(` sayHello("Xyzzy"); sayHello(); result = twoPlus(2.0); `) if err != nil { fmt.Fprintln(os.Stderr, err) return } fmt.Println(value) // Output: // The value of abc is 4 // 4 // The value of def is 11 // 16 // 16 // undefined // ReferenceError: 'abcdefghijlmnopqrstuvwxyz' is not defined // Hello, Xyzzy. // Hello, undefined. // 4 } func ExampleConsole() { //nolint:govet vm := New() console := map[string]interface{}{ "log": func(call FunctionCall) Value { fmt.Println("console.log:", formatForConsole(call.ArgumentList)) return UndefinedValue() }, } err := vm.Set("console", console) if err != nil { panic(fmt.Errorf("console error: %w", err)) } value, err := vm.Run(`console.log("Hello, World.");`) fmt.Println(value) fmt.Println(err) // Output: // console.log: Hello, World. // undefined // } ================================================ FILE: error.go ================================================ package otto import ( "errors" "fmt" "github.com/robertkrimen/otto/file" ) type exception struct { value interface{} } func newException(value interface{}) *exception { return &exception{ value: value, } } func (e *exception) eject() interface{} { value := e.value e.value = nil // Prevent Go from holding on to the value, whatever it is return value } type ottoError struct { name string message string trace []frame offset int } func (e ottoError) format() string { if len(e.name) == 0 { return e.message } if len(e.message) == 0 { return e.name } return fmt.Sprintf("%s: %s", e.name, e.message) } func (e ottoError) formatWithStack() string { str := e.format() + "\n" for _, frm := range e.trace { str += " at " + frm.location() + "\n" } return str } type frame struct { fn interface{} file *file.File nativeFile string callee string nativeLine int offset int native bool } var nativeFrame = frame{} type at int func (fr frame) location() string { str := "" switch { case fr.native: str = "" if fr.nativeFile != "" && fr.nativeLine != 0 { str = fmt.Sprintf("%s:%d", fr.nativeFile, fr.nativeLine) } case fr.file != nil: if p := fr.file.Position(file.Idx(fr.offset)); p != nil { path, line, column := p.Filename, p.Line, p.Column if path == "" { path = "" } str = fmt.Sprintf("%s:%d:%d", path, line, column) } } if fr.callee != "" { str = fmt.Sprintf("%s (%s)", fr.callee, str) } return str } // An Error represents a runtime error, e.g. a TypeError, a ReferenceError, etc. type Error struct { ottoError } // Error returns a description of the error // // TypeError: 'def' is not a function func (e Error) Error() string { return e.format() } // String returns a description of the error and a trace of where the // error occurred. // // TypeError: 'def' is not a function // at xyz (:3:9) // at :7:1/ func (e Error) String() string { return e.formatWithStack() } // GoString returns a description of the error and a trace of where the // error occurred. Printing with %#v will trigger this behaviour. func (e Error) GoString() string { return e.formatWithStack() } func (e ottoError) describe(format string, in ...interface{}) string { return fmt.Sprintf(format, in...) } func (e ottoError) messageValue() Value { if e.message == "" { return Value{} } return stringValue(e.message) } func (rt *runtime) typeErrorResult(throw bool) bool { if throw { panic(rt.panicTypeError()) } return false } func newError(rt *runtime, name string, stackFramesToPop int, in ...interface{}) ottoError { err := ottoError{ name: name, offset: -1, } description := "" length := len(in) if rt != nil && rt.scope != nil { curScope := rt.scope for range stackFramesToPop { if curScope.outer != nil { curScope = curScope.outer } } frm := curScope.frame if length > 0 { if atv, ok := in[length-1].(at); ok { in = in[0 : length-1] if curScope != nil { frm.offset = int(atv) } length-- } if length > 0 { description, in = in[0].(string), in[1:] } } limit := rt.traceLimit err.trace = append(err.trace, frm) if curScope != nil { for curScope = curScope.outer; curScope != nil; curScope = curScope.outer { if limit--; limit == 0 { break } if curScope.frame.offset >= 0 { err.trace = append(err.trace, curScope.frame) } } } } else if length > 0 { description, in = in[0].(string), in[1:] } err.message = err.describe(description, in...) return err } func (rt *runtime) panicTypeError(argumentList ...interface{}) *exception { return &exception{ value: newError(rt, "TypeError", 0, argumentList...), } } func (rt *runtime) panicReferenceError(argumentList ...interface{}) *exception { return &exception{ value: newError(rt, "ReferenceError", 0, argumentList...), } } func (rt *runtime) panicURIError(argumentList ...interface{}) *exception { return &exception{ value: newError(rt, "URIError", 0, argumentList...), } } func (rt *runtime) panicSyntaxError(argumentList ...interface{}) *exception { return &exception{ value: newError(rt, "SyntaxError", 0, argumentList...), } } func (rt *runtime) panicRangeError(argumentList ...interface{}) *exception { return &exception{ value: newError(rt, "RangeError", 0, argumentList...), } } func catchPanic(function func()) (err error) { defer func() { if caught := recover(); caught != nil { if excep, ok := caught.(*exception); ok { caught = excep.eject() } switch caught := caught.(type) { case *Error: err = caught return case ottoError: err = &Error{caught} return case Value: if vl := caught.object(); vl != nil { if vl, ok := vl.value.(ottoError); ok { err = &Error{vl} return } } err = errors.New(caught.string()) return } panic(caught) } }() function() return nil } ================================================ FILE: error_native_test.go ================================================ package otto import ( "testing" "github.com/stretchr/testify/require" ) // this is its own file because the tests in it rely on the line numbers of // some of the functions defined here. putting it in with the rest of the // tests would probably be annoying. func TestErrorContextNative(t *testing.T) { tt(t, func() { vm := New() err := vm.Set("N", func(c FunctionCall) Value { v, err := c.Argument(0).Call(NullValue()) if err != nil { panic(err) } return v }) require.NoError(t, err) s, err := vm.Compile("test.js", ` function F() { throw new Error('wow'); } function G() { return N(F); } `) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) f1, err := vm.Get("G") require.NoError(t, err) _, err = f1.Call(NullValue()) require.Error(t, err) err1 := asError(t, err) is(err1.message, "wow") is(len(err1.trace), 3) is(err1.trace[0].location(), "F (test.js:2:29)") is(err1.trace[1].location(), "github.com/robertkrimen/otto.TestErrorContextNative.func1.1 (error_native_test.go:17)") is(err1.trace[2].location(), "G (test.js:3:26)") }) } ================================================ FILE: error_test.go ================================================ package otto import ( "testing" "github.com/stretchr/testify/require" ) func TestError(t *testing.T) { tt(t, func() { test, _ := test() test(` [ Error.prototype.name, Error.prototype.message, Error.prototype.hasOwnProperty("message") ]; `, "Error,,true") }) } func TestError_instanceof(t *testing.T) { tt(t, func() { test, _ := test() test(`(new TypeError()) instanceof Error`, true) }) } func TestPanicValue(t *testing.T) { tt(t, func() { test, vm := test() vm.Set("abc", func(call FunctionCall) Value { value, err := call.Otto.Run(`({ def: 3.14159 })`) is(err, nil) panic(value) }) test(` try { abc(); } catch (err) { error = err; } [ error instanceof Error, error.message, error.def ]; `, "false,,3.14159") }) } func Test_catchPanic(t *testing.T) { tt(t, func() { vm := New() _, err := vm.Run(` A syntax error that does not define var; abc; `) is(err, "!=", nil) _, err = vm.Call(`abc.def`, nil) is(err, "!=", nil) }) } func asError(t *testing.T, err error) *Error { t.Helper() var oerr *Error require.ErrorAs(t, err, &oerr) return oerr } func TestErrorContext(t *testing.T) { tt(t, func() { vm := New() _, err := vm.Run(` undefined(); `) { err := asError(t, err) is(err.message, `"undefined" is not a function`) is(len(err.trace), 1) is(err.trace[0].location(), ":2:13") } _, err = vm.Run(` ({}).abc(); `) { err := asError(t, err) is(err.message, `"abc" is not a function`) is(len(err.trace), 1) is(err.trace[0].location(), ":2:14") } _, err = vm.Run(` ("abc").abc(); `) { err := asError(t, err) is(err.message, `"abc" is not a function`) is(len(err.trace), 1) is(err.trace[0].location(), ":2:14") } _, err = vm.Run(` var ghi = "ghi"; ghi(); `) { err := asError(t, err) is(err.message, `"ghi" is not a function`) is(len(err.trace), 1) is(err.trace[0].location(), ":3:13") } _, err = vm.Run(` function def() { undefined(); } function abc() { def(); } abc(); `) { err := asError(t, err) is(err.message, `"undefined" is not a function`) is(len(err.trace), 3) is(err.trace[0].location(), "def (:3:17)") is(err.trace[1].location(), "abc (:6:17)") is(err.trace[2].location(), ":8:13") } _, err = vm.Run(` function abc() { xyz(); } abc(); `) { err := asError(t, err) is(err.message, "'xyz' is not defined") is(len(err.trace), 2) is(err.trace[0].location(), "abc (:3:17)") is(err.trace[1].location(), ":5:13") } _, err = vm.Run(` mno + 1; `) { err := asError(t, err) is(err.message, "'mno' is not defined") is(len(err.trace), 1) is(err.trace[0].location(), ":2:13") } _, err = vm.Run(` eval("xyz();"); `) { err := asError(t, err) is(err.message, "'xyz' is not defined") is(len(err.trace), 1) is(err.trace[0].location(), ":1:1") } _, err = vm.Run(` xyzzy = "Nothing happens." eval("xyzzy();"); `) { err := asError(t, err) is(err.message, `"xyzzy" is not a function`) is(len(err.trace), 1) is(err.trace[0].location(), ":1:1") } _, err = vm.Run(` throw Error("xyzzy"); `) { err := asError(t, err) is(err.message, "xyzzy") is(len(err.trace), 1) is(err.trace[0].location(), ":2:19") } _, err = vm.Run(` throw new Error("xyzzy"); `) { err := asError(t, err) is(err.message, "xyzzy") is(len(err.trace), 1) is(err.trace[0].location(), ":2:23") } script1, err := vm.Compile("file1.js", `function A() { throw new Error("test"); } function C() { var o = null; o.prop = 1; } `) is(err, nil) _, err = vm.Run(script1) is(err, nil) script2, err := vm.Compile("file2.js", `function B() { A() } `) is(err, nil) _, err = vm.Run(script2) is(err, nil) script3, err := vm.Compile("file3.js", "B()") is(err, nil) _, err = vm.Run(script3) { err := asError(t, err) is(err.message, "test") is(len(err.trace), 3) is(err.trace[0].location(), "A (file1.js:2:15)") is(err.trace[1].location(), "B (file2.js:2:5)") is(err.trace[2].location(), "file3.js:1:1") } { f, _ := vm.Get("B") _, err = f.Call(UndefinedValue()) err1 := asError(t, err) is(err1.message, "test") is(len(err1.trace), 2) is(err1.trace[0].location(), "A (file1.js:2:15)") is(err1.trace[1].location(), "B (file2.js:2:5)") } { f, _ := vm.Get("C") _, err = f.Call(UndefinedValue()) err1 := asError(t, err) is(err1.message, `Cannot access member "prop" of null`) is(len(err1.trace), 1) is(err1.trace[0].location(), "C (file1.js:7:5)") } }) } func TestMakeCustomErrorReturn(t *testing.T) { tt(t, func() { vm := New() err := vm.Set("A", func(c FunctionCall) Value { return vm.MakeCustomError("CarrotError", "carrots is life, carrots is love") }) require.NoError(t, err) s, err := vm.Compile("test.js", ` function B() { return A(); } function C() { return B(); } function D() { return C(); } `) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) v, err := vm.Call("D", nil) require.NoError(t, err) is(v.Class(), classErrorName) name, err := v.Object().Get("name") require.NoError(t, err) is(name.String(), "CarrotError") message, err := v.Object().Get("message") require.NoError(t, err) is(message.String(), "carrots is life, carrots is love") str, err := v.Object().Call("toString") require.NoError(t, err) is(str, "CarrotError: carrots is life, carrots is love") i, err := v.Export() require.NoError(t, err) t.Logf("%#v\n", i) }) } func TestMakeCustomError(t *testing.T) { tt(t, func() { vm := New() err := vm.Set("A", func(c FunctionCall) Value { panic(vm.MakeCustomError("CarrotError", "carrots is life, carrots is love")) }) require.NoError(t, err) s, err := vm.Compile("test.js", ` function B() { A(); } function C() { B(); } function D() { C(); } `) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) _, err = vm.Call("D", nil) require.EqualError(t, err, "CarrotError: carrots is life, carrots is love") er := asError(t, err) is(er.name, "CarrotError") is(er.message, "carrots is life, carrots is love") }) } func TestMakeCustomErrorFreshVM(t *testing.T) { tt(t, func() { vm := New() e := vm.MakeCustomError("CarrotError", "carrots is life, carrots is love") str, err := e.ToString() require.NoError(t, err) is(str, "CarrotError: carrots is life, carrots is love") }) } func TestMakeTypeError(t *testing.T) { tt(t, func() { vm := New() err := vm.Set("A", func(c FunctionCall) Value { panic(vm.MakeTypeError("these aren't my glasses")) }) require.NoError(t, err) s, err := vm.Compile("test.js", ` function B() { A(); } function C() { B(); } function D() { C(); } `) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) _, err = vm.Call("D", nil) require.EqualError(t, err, "TypeError: these aren't my glasses") er := asError(t, err) is(er.name, "TypeError") is(er.message, "these aren't my glasses") }) } func TestMakeRangeError(t *testing.T) { tt(t, func() { vm := New() err := vm.Set("A", func(c FunctionCall) Value { panic(vm.MakeRangeError("too many")) }) require.NoError(t, err) s, err := vm.Compile("test.js", ` function B() { A(); } function C() { B(); } function D() { C(); } `) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) _, err = vm.Call("D", nil) require.EqualError(t, err, "RangeError: too many") er := asError(t, err) is(er.name, "RangeError") is(er.message, "too many") }) } func TestMakeSyntaxError(t *testing.T) { tt(t, func() { vm := New() err := vm.Set("A", func(c FunctionCall) Value { panic(vm.MakeSyntaxError("i think you meant \"you're\"")) }) require.NoError(t, err) s, err := vm.Compile("test.js", ` function B() { A(); } function C() { B(); } function D() { C(); } `) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) _, err = vm.Call("D", nil) require.EqualError(t, err, "SyntaxError: i think you meant \"you're\"") er := asError(t, err) is(er.name, "SyntaxError") is(er.message, "i think you meant \"you're\"") }) } func TestErrorStackProperty(t *testing.T) { tt(t, func() { vm := New() s, err := vm.Compile("test.js", ` function A() { throw new TypeError('uh oh'); } function B() { return A(); } function C() { return B(); } var s = null; try { C(); } catch (e) { s = e.stack; } s; `) require.NoError(t, err) v, err := vm.Run(s) require.NoError(t, err) is(v.String(), "TypeError: uh oh\n at A (test.js:2:29)\n at B (test.js:3:26)\n at C (test.js:4:26)\n at test.js:8:10\n") }) } func TestErrorMessageContainingFormatCharacters(t *testing.T) { tt(t, func() { test, tester := test() tester.Set("F", func(call FunctionCall) Value { return call.Otto.MakeCustomError(call.ArgumentList[0].String(), call.ArgumentList[1].String()) }) test("Error('literal percent-s: %s')", "Error: literal percent-s: %s") test("new Error('literal percent-s: %s')", "Error: literal percent-s: %s") test("F('TestError', 'literal percent-s: %s')", "TestError: literal percent-s: %s") test("raise: throw Error('literal percent-s: %s')", "Error: literal percent-s: %s") test("raise: throw new Error('literal percent-s: %s')", "Error: literal percent-s: %s") test("raise: throw F('TestError', 'literal percent-s: %s')", "TestError: literal percent-s: %s") }) } ================================================ FILE: evaluate.go ================================================ package otto import ( "fmt" "math" "strings" "github.com/robertkrimen/otto/token" ) func (rt *runtime) evaluateMultiply(left float64, right float64) Value { //nolint:unused // TODO 11.5.1 return Value{} } func (rt *runtime) evaluateDivide(left float64, right float64) Value { if math.IsNaN(left) || math.IsNaN(right) { return NaNValue() } if math.IsInf(left, 0) && math.IsInf(right, 0) { return NaNValue() } if left == 0 && right == 0 { return NaNValue() } if math.IsInf(left, 0) { if math.Signbit(left) == math.Signbit(right) { return positiveInfinityValue() } return negativeInfinityValue() } if math.IsInf(right, 0) { if math.Signbit(left) == math.Signbit(right) { return positiveZeroValue() } return negativeZeroValue() } if right == 0 { if math.Signbit(left) == math.Signbit(right) { return positiveInfinityValue() } return negativeInfinityValue() } return float64Value(left / right) } func (rt *runtime) evaluateModulo(left float64, right float64) Value { //nolint:unused // TODO 11.5.3 return Value{} } func (rt *runtime) calculateBinaryExpression(operator token.Token, left Value, right Value) Value { leftValue := left.resolve() switch operator { // Additive case token.PLUS: leftValue = toPrimitiveValue(leftValue) rightValue := right.resolve() rightValue = toPrimitiveValue(rightValue) if leftValue.IsString() || rightValue.IsString() { return stringValue(strings.Join([]string{leftValue.string(), rightValue.string()}, "")) } return float64Value(leftValue.float64() + rightValue.float64()) case token.MINUS: rightValue := right.resolve() return float64Value(leftValue.float64() - rightValue.float64()) // Multiplicative case token.MULTIPLY: rightValue := right.resolve() return float64Value(leftValue.float64() * rightValue.float64()) case token.SLASH: rightValue := right.resolve() return rt.evaluateDivide(leftValue.float64(), rightValue.float64()) case token.REMAINDER: rightValue := right.resolve() return float64Value(math.Mod(leftValue.float64(), rightValue.float64())) // Logical case token.LOGICAL_AND: left := leftValue.bool() if !left { return falseValue } return boolValue(right.resolve().bool()) case token.LOGICAL_OR: left := leftValue.bool() if left { return trueValue } return boolValue(right.resolve().bool()) // Bitwise case token.AND: rightValue := right.resolve() return int32Value(toInt32(leftValue) & toInt32(rightValue)) case token.OR: rightValue := right.resolve() return int32Value(toInt32(leftValue) | toInt32(rightValue)) case token.EXCLUSIVE_OR: rightValue := right.resolve() return int32Value(toInt32(leftValue) ^ toInt32(rightValue)) // Shift // (Masking of 0x1f is to restrict the shift to a maximum of 31 places) case token.SHIFT_LEFT: rightValue := right.resolve() return int32Value(toInt32(leftValue) << (toUint32(rightValue) & 0x1f)) case token.SHIFT_RIGHT: rightValue := right.resolve() return int32Value(toInt32(leftValue) >> (toUint32(rightValue) & 0x1f)) case token.UNSIGNED_SHIFT_RIGHT: rightValue := right.resolve() // Shifting an unsigned integer is a logical shift return uint32Value(toUint32(leftValue) >> (toUint32(rightValue) & 0x1f)) case token.INSTANCEOF: rightValue := right.resolve() if !rightValue.IsObject() { panic(rt.panicTypeError("invalid kind %s for instanceof (expected object)", rightValue.kind)) } return boolValue(rightValue.object().hasInstance(leftValue)) case token.IN: rightValue := right.resolve() if !rightValue.IsObject() { panic(rt.panicTypeError("invalid kind %s for in (expected object)", rightValue.kind)) } return boolValue(rightValue.object().hasProperty(leftValue.string())) } panic(hereBeDragons(operator)) } type lessThanResult int const ( lessThanFalse lessThanResult = iota lessThanTrue lessThanUndefined ) func calculateLessThan(left Value, right Value, leftFirst bool) lessThanResult { var x, y Value if leftFirst { x = toNumberPrimitive(left) y = toNumberPrimitive(right) } else { y = toNumberPrimitive(right) x = toNumberPrimitive(left) } var result bool if x.kind != valueString || y.kind != valueString { x, y := x.float64(), y.float64() if math.IsNaN(x) || math.IsNaN(y) { return lessThanUndefined } result = x < y } else { x, y := x.string(), y.string() result = x < y } if result { return lessThanTrue } return lessThanFalse } // FIXME Probably a map is not the most efficient way to do this. var lessThanTable [4](map[lessThanResult]bool) = [4](map[lessThanResult]bool){ // < map[lessThanResult]bool{ lessThanFalse: false, lessThanTrue: true, lessThanUndefined: false, }, // > map[lessThanResult]bool{ lessThanFalse: false, lessThanTrue: true, lessThanUndefined: false, }, // <= map[lessThanResult]bool{ lessThanFalse: true, lessThanTrue: false, lessThanUndefined: false, }, // >= map[lessThanResult]bool{ lessThanFalse: true, lessThanTrue: false, lessThanUndefined: false, }, } func (rt *runtime) calculateComparison(comparator token.Token, left Value, right Value) bool { // FIXME Use strictEqualityComparison? // TODO This might be redundant now (with regards to evaluateComparison) x := left.resolve() y := right.resolve() var kindEqualKind bool var negate bool result := true switch comparator { case token.LESS: result = lessThanTable[0][calculateLessThan(x, y, true)] case token.GREATER: result = lessThanTable[1][calculateLessThan(y, x, false)] case token.LESS_OR_EQUAL: result = lessThanTable[2][calculateLessThan(y, x, false)] case token.GREATER_OR_EQUAL: result = lessThanTable[3][calculateLessThan(x, y, true)] case token.STRICT_NOT_EQUAL: negate = true fallthrough case token.STRICT_EQUAL: if x.kind != y.kind { result = false } else { kindEqualKind = true } case token.NOT_EQUAL: negate = true fallthrough case token.EQUAL: switch { case x.kind == y.kind: kindEqualKind = true case x.kind <= valueNull && y.kind <= valueNull: result = true case x.kind <= valueNull || y.kind <= valueNull: result = false case x.kind <= valueString && y.kind <= valueString: result = x.float64() == y.float64() case x.kind == valueBoolean: result = rt.calculateComparison(token.EQUAL, float64Value(x.float64()), y) case y.kind == valueBoolean: result = rt.calculateComparison(token.EQUAL, x, float64Value(y.float64())) case x.kind == valueObject: result = rt.calculateComparison(token.EQUAL, toPrimitiveValue(x), y) case y.kind == valueObject: result = rt.calculateComparison(token.EQUAL, x, toPrimitiveValue(y)) default: panic(fmt.Sprintf("unknown types for equal: %v ==? %v", x, y)) } default: panic("unknown comparator " + comparator.String()) } if kindEqualKind { switch x.kind { case valueUndefined, valueNull: result = true case valueNumber: x := x.float64() y := y.float64() if math.IsNaN(x) || math.IsNaN(y) { result = false } else { result = x == y } case valueString: result = x.string() == y.string() case valueBoolean: result = x.bool() == y.bool() case valueObject: result = x.object() == y.object() default: goto ERROR } } if negate { result = !result } return result ERROR: panic(hereBeDragons("%v (%v) %s %v (%v)", x, x.kind, comparator, y, y.kind)) } ================================================ FILE: file/file.go ================================================ // Package file encapsulates the file abstractions used by the ast & parser. package file import ( "fmt" "strings" "gopkg.in/sourcemap.v1" ) // Idx is a compact encoding of a source position within a file set. // It can be converted into a Position for a more convenient, but much // larger, representation. type Idx int // Position describes an arbitrary source position // including the filename, line, and column location. type Position struct { Filename string // The filename where the error occurred, if any Offset int // The src offset Line int // The line number, starting at 1 Column int // The column number, starting at 1 (The character count) } // A Position is valid if the line number is > 0. func (p *Position) isValid() bool { return p.Line > 0 } // String returns a string in one of several forms: // // file:line:column A valid position with filename // line:column A valid position without filename // file An invalid position with filename // - An invalid position without filename func (p *Position) String() string { str := p.Filename if p.isValid() { if str != "" { str += ":" } str += fmt.Sprintf("%d:%d", p.Line, p.Column) } if str == "" { str = "-" } return str } // A FileSet represents a set of source files. type FileSet struct { last *File files []*File } // AddFile adds a new file with the given filename and src. // // This an internal method, but exported for cross-package use. func (fs *FileSet) AddFile(filename, src string) int { base := fs.nextBase() file := &File{ name: filename, src: src, base: base, } fs.files = append(fs.files, file) fs.last = file return base } func (fs *FileSet) nextBase() int { if fs.last == nil { return 1 } return fs.last.base + len(fs.last.src) + 1 } // File returns the File at idx or nil if not found. func (fs *FileSet) File(idx Idx) *File { for _, file := range fs.files { if idx <= Idx(file.base+len(file.src)) { return file } } return nil } // Position converts an Idx in the FileSet into a Position. func (fs *FileSet) Position(idx Idx) *Position { for _, file := range fs.files { if idx <= Idx(file.base+len(file.src)) { return file.Position(idx - Idx(file.base)) } } return nil } // File represents a file to parse. type File struct { sm *sourcemap.Consumer name string src string base int } // NewFile returns a new file with the given filename, src and base. func NewFile(filename, src string, base int) *File { return &File{ name: filename, src: src, base: base, } } // WithSourceMap sets the source map of fl. func (fl *File) WithSourceMap(sm *sourcemap.Consumer) *File { fl.sm = sm return fl } // Name returns the name of fl. func (fl *File) Name() string { return fl.name } // Source returns the source of fl. func (fl *File) Source() string { return fl.src } // Base returns the base of fl. func (fl *File) Base() int { return fl.base } // Position returns the position at idx or nil if not valid. func (fl *File) Position(idx Idx) *Position { position := &Position{} offset := int(idx) - fl.base if offset >= len(fl.src) || offset < 0 { return nil } src := fl.src[:offset] position.Filename = fl.name position.Offset = offset position.Line = strings.Count(src, "\n") + 1 if index := strings.LastIndex(src, "\n"); index >= 0 { position.Column = offset - index } else { position.Column = len(src) + 1 } if fl.sm != nil { if f, _, l, c, ok := fl.sm.Source(position.Line, position.Column); ok { position.Filename, position.Line, position.Column = f, l, c } } return position } ================================================ FILE: function_stack_test.go ================================================ package otto import ( "testing" "github.com/stretchr/testify/require" ) // this is its own file because the tests in it rely on the line numbers of // some of the functions defined here. putting it in with the rest of the // tests would probably be annoying. func TestFunction_stack(t *testing.T) { tt(t, func() { vm := New() s, err := vm.Compile("fake.js", `function X(fn1, fn2, fn3) { fn1(fn2, fn3); }`) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) expected := []frame{ {native: true, nativeFile: "function_stack_test.go", nativeLine: 36, offset: 0, callee: "github.com/robertkrimen/otto.TestFunction_stack.func1.2"}, {native: true, nativeFile: "function_stack_test.go", nativeLine: 29, offset: 0, callee: "github.com/robertkrimen/otto.TestFunction_stack.func1.1"}, {native: false, nativeFile: "", nativeLine: 0, offset: 29, callee: "X", file: s.program.file}, {native: false, nativeFile: "", nativeLine: 0, offset: 29, callee: "X", file: s.program.file}, } err = vm.Set("A", func(c FunctionCall) Value { _, err = c.Argument(0).Call(UndefinedValue()) require.NoError(t, err) return UndefinedValue() }) require.NoError(t, err) err = vm.Set("B", func(c FunctionCall) Value { depth := 0 for s := c.Otto.runtime.scope; s != nil; s = s.outer { // these properties are tested explicitly so that we don't test `.fn`, // which will differ from run to run is(s.frame.native, expected[depth].native) is(s.frame.nativeFile, expected[depth].nativeFile) is(s.frame.nativeLine, expected[depth].nativeLine) is(s.frame.offset, expected[depth].offset) is(s.frame.callee, expected[depth].callee) is(s.frame.file, expected[depth].file) depth++ } is(depth, 4) return UndefinedValue() }) require.NoError(t, err) x, err := vm.Get("X") require.NoError(t, err) a, err := vm.Get("A") require.NoError(t, err) b, err := vm.Get("B") require.NoError(t, err) _, err = x.Call(UndefinedValue(), x, a, b) require.NoError(t, err) }) } ================================================ FILE: function_test.go ================================================ package otto import ( "testing" ) func TestFunction(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = Object.getOwnPropertyDescriptor(Function, "prototype"); [ [ typeof Function.prototype, typeof Function.prototype.length, Function.prototype.length ], [ abc.writable, abc.enumerable, abc.configurable ] ]; `, "function,number,0,false,false,false") }) } func Test_argumentList2parameterList(t *testing.T) { tt(t, func() { is(argumentList2parameterList([]Value{toValue("abc, def"), toValue("ghi")}), []string{"abc", "def", "ghi"}) }) } func TestFunction_new(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: new Function({}); `, "SyntaxError: (anonymous): Line 2:9 Unexpected identifier") test(` var abc = Function("def, ghi", "jkl", "return def+ghi+jkl"); [ typeof abc, abc instanceof Function, abc("ab", "ba", 1) ]; `, "function,true,abba1") test(`raise: var abc = { toString: function() { throw 1; } }; var def = { toString: function() { throw 2; } }; var ghi = new Function(abc, def); ghi; `, "1") // S15.3.2.1_A3_T10 test(`raise: var abc = { toString: function() { return "z;x"; } }; var def = "return this"; var ghi = new Function(abc, def); ghi; `, "SyntaxError: (anonymous): Line 1:12 Unexpected token ;") test(`raise: var abc; var def = "return true"; var ghi = new Function(null, def); ghi; `, "SyntaxError: (anonymous): Line 1:11 Unexpected token null") }) } func TestFunction_apply(t *testing.T) { tt(t, func() { test, _ := test() test(`Function.prototype.apply.length`, 2) test(`String.prototype.substring.apply("abc", [1, 11])`, "bc") }) } func TestFunction_call(t *testing.T) { tt(t, func() { test, _ := test() test(`Function.prototype.call.length`, 1) test(`String.prototype.substring.call("abc", 1, 11)`, "bc") }) } func TestFunctionArguments(t *testing.T) { tt(t, func() { test, _ := test() // Should not be able to delete arguments test(` function abc(def, arguments){ delete def; return def; } abc(1); `, 1) // Again, should not be able to delete arguments test(` function abc(def){ delete def; return def; } abc(1); `, 1) // Test typeof of a function argument test(` function abc(def, ghi, jkl){ return typeof jkl } abc("1st", "2nd", "3rd", "4th", "5th"); `, "string") test(` function abc(def, ghi, jkl){ arguments[0] = 3.14; arguments[1] = 'Nothing happens'; arguments[2] = 42; if (3.14 === def && 'Nothing happens' === ghi && 42 === jkl) return true; } abc(-1, 4.2, 314); `, true) }) } func TestFunctionDeclarationInFunction(t *testing.T) { tt(t, func() { test, _ := test() // Function declarations happen AFTER parameter/argument declarations // That is, a function declared within a function will shadow/overwrite // declared parameters test(` function abc(def){ return def; function def(){ return 1; } } typeof abc(); `, "function") }) } func TestArguments_defineOwnProperty(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc; var def = true; var ghi = {}; (function (a, b, c) { Object.defineProperty(arguments, "0", { value: 42, writable: false, enumerable: false, configurable: false }); Object.defineProperty(arguments, "1", { value: 3.14, configurable: true, enumerable: true }); abc = Object.getOwnPropertyDescriptor(arguments, "0"); for (var name in arguments) { ghi[name] = (ghi[name] || 0) + 1; if (name === "0") { def = false; } } }(0, 1, 2)); [ abc.value, abc.writable, abc.enumerable, abc.configurable, def, ghi["1"] ]; `, "42,false,false,false,true,1") }) } func TestFunction_bind(t *testing.T) { tt(t, func() { test, _ := test() defer mockUTC()() test(` abc = function(){ return "abc"; }; def = abc.bind(); [ typeof def.prototype, typeof def.hasOwnProperty, def.hasOwnProperty("caller"), def.hasOwnProperty("arguments"), def() ]; `, "object,function,true,true,abc") test(` abc = function(){ return arguments[1]; }; def = abc.bind(undefined, "abc"); ghi = abc.bind(undefined, "abc", "ghi"); [ def(), def("def"), ghi("def") ]; `, ",def,ghi") test(` var abc = function () {}; var ghi; try { Object.defineProperty(Function.prototype, "xyzzy", { value: 1001, writable: true, enumerable: true, configurable: true }); var def = abc.bind({}); ghi = !def.hasOwnProperty("xyzzy") && ghi.xyzzy === 1001; } finally { delete Function.prototype.xyzzy; } [ ghi ]; `, "true") test(` var abc = function (def, ghi) {}; var jkl = abc.bind({}); var mno = abc.bind({}, 1, 2); [ jkl.length, mno.length ]; `, "2,0") test(`raise: Math.bind(); `, `TypeError: "bind" is not a function`) test(` function construct(fn, arguments) { var bound = Function.prototype.bind.apply(fn, [null].concat(arguments)); return new bound(); } var abc = construct(Date, [1957, 4, 27]); Object.prototype.toString.call(abc); `, "[object Date]") test(` var fn = function (x, y, z) { var result = {}; result.abc = x + y + z; result.def = arguments[0] === "a" && arguments.length === 3; return result; }; var newFn = Function.prototype.bind.call(fn, {}, "a", "b", "c"); var result = new newFn(); [ result.hasOwnProperty("abc"), result.hasOwnProperty("def"), result.abc, result.def ]; `, "true,true,abc,true") test(` abc = function(){ return "abc"; }; def = abc.bind(); def.toString(); `, "function () { [native code] }") }) } func TestFunction_toString(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: Function.prototype.toString.call(undefined); `, "TypeError: Function.Class environment != Function") test(` abc = function() { return -1 ; } 1; abc.toString(); `, "function() { return -1 ;\n}") }) } func TestFunction_length(t *testing.T) { tt(t, func() { test, _ := test() test(`function a(x, y) {}; a.length`, 2) }) } func TestFunction_name(t *testing.T) { tt(t, func() { test, _ := test() test(`function a() {}; a.name`, "a") test(`function a() {}; var b = a.bind(); b.name`, "bound a") }) } func TestFunction_caller(t *testing.T) { tt(t, func() { test, vm := test() vm.Set("n", func(v Value) Value { r, err := v.Call(UndefinedValue()) if err != nil { panic(err) } return r }) test(` function a() { return a.caller; }; a() `, NullValue()) test(` function a() { return a.caller === b; }; function b() { return a(); } b() `, true) test(` function a() { return a.caller === b && b.caller === c; } function b() { return a(); } function c() { return b(); } c(); `, true) test(` function a() { return a.caller === b && b.caller === n && n.caller === c; } function b() { return a(); } function c() { return n(b); } c() `, true) test(` function e() { return e.caller === g && f.caller === g && g.caller === f; } function f(n) { return g(n - 1); } function g(n) { return n > 0 ? f(n) : e(); } f(2); `, true) }) } ================================================ FILE: functional_benchmark_test.go ================================================ package otto import ( "math/rand" "strconv" "strings" "testing" "github.com/stretchr/testify/require" ) func TestGoSliceQuickSort(t *testing.T) { testGoSliceSort(t, "quickSort(testSlice, 0, testSlice.length-1);", jsQuickSort) } func TestGoSliceHeapSort(t *testing.T) { testGoSliceSort(t, "heapSort(testSlice)", jsHeapSort) } func TestJsArrayQuicksort(t *testing.T) { testJsArraySort(t, "quickSort(testSlice, 0, testSlice.length-1);", jsQuickSort) } func TestJsArrayHeapSort(t *testing.T) { testJsArraySort(t, "heapSort(testSlice)", jsHeapSort) } func TestJsArrayMergeSort(t *testing.T) { testJsArraySort(t, "testSlice = mergeSort(testSlice)", jsMergeSort) } func TestCryptoAes(t *testing.T) { tt(t, func() { _, vm := test() _, err := vm.Run(jsCryptoAES) is(err, nil) }) } func BenchmarkGoSliceQuickSort100000000(b *testing.B) { benchmarkGoSliceSort(b, 100000000, "quickSort(testSlice, 0, testSlice.length-1);", jsQuickSort) } func BenchmarkGoSliceHeapSort100000000(b *testing.B) { benchmarkGoSliceSort(b, 100000000, "heapSort(testSlice);", jsHeapSort) } func BenchmarkJsArrayQuickSort500(b *testing.B) { benchmarkJsArraySort(b, 500, "quickSort(testSlice, 0, testSlice.length-1);", jsQuickSort) } func BenchmarkJsArrayMergeSort500(b *testing.B) { benchmarkJsArraySort(b, 500, "mergeSort(testSlice);", jsMergeSort) } func BenchmarkJsArrayHeapSort500(b *testing.B) { benchmarkJsArraySort(b, 500, "heapSort(testSlice);", jsHeapSort) } func BenchmarkCryptoAES(b *testing.B) { vm := New() // Make sure VM creation time is not counted in runtime test b.ResetTimer() for i := 0; i < b.N; i++ { _, err := vm.Run(jsCryptoAES) require.NoError(b, err) } } func testGoSliceSort(t *testing.T, sortFuncCall string, sortCode string) { t.Helper() tt(t, func() { test, vm := test() // inject quicksort code _, err := vm.Run(sortCode) is(err, nil) testSlice := []int{5, 3, 2, 4, 1} vm.Set("testSlice", testSlice) _, err = vm.Run(sortFuncCall) is(err, nil) is(test(`testSlice[0]`).export(), 1) is(test(`testSlice[1]`).export(), 2) is(test(`testSlice[2]`).export(), 3) is(test(`testSlice[3]`).export(), 4) is(test(`testSlice[4]`).export(), 5) is(testSlice[0], 1) is(testSlice[1], 2) is(testSlice[2], 3) is(testSlice[3], 4) is(testSlice[4], 5) }) } func testJsArraySort(t *testing.T, sortFuncCall string, sortCode string) { t.Helper() tt(t, func() { test, vm := test() // inject quicksort code _, err := vm.Run(sortCode) require.NoError(t, err) _, err = vm.Run("var testSlice = [5, 3, 2, 4, 1];") require.NoError(t, err) _, err = vm.Run(sortFuncCall) require.NoError(t, err) is(test(`testSlice[0]`).export(), 1) is(test(`testSlice[1]`).export(), 2) is(test(`testSlice[2]`).export(), 3) is(test(`testSlice[3]`).export(), 4) is(test(`testSlice[4]`).export(), 5) }) } func benchmarkGoSliceSort(b *testing.B, size int, sortFuncCall string, sortCode string) { b.Helper() // generate arbitrary slice of 'size' testSlice := make([]int, size) for i := range size { testSlice[i] = rand.Int() //nolint:gosec } vm := New() // inject the sorting code _, err := vm.Run(sortCode) require.NoError(b, err) // Reset timer - everything until this point may have taken a long time b.ResetTimer() for i := 0; i < b.N; i++ { _, err = vm.Run(sortFuncCall) require.NoError(b, err) } } func benchmarkJsArraySort(b *testing.B, size int, sortFuncCall string, sortCode string) { b.Helper() // generate arbitrary slice of 'size' testSlice := make([]string, size) for i := range testSlice { testSlice[i] = strconv.Itoa(rand.Int()) //nolint:gosec } jsArrayString := "[" + strings.Join(testSlice, ",") + "]" vm := New() // inject the test array _, err := vm.Run("testSlice = " + jsArrayString) require.NoError(b, err) // inject the sorting code _, err = vm.Run(sortCode) require.NoError(b, err) // Reset timer - everything until this point may have taken a long time b.ResetTimer() for i := 0; i < b.N; i++ { _, err = vm.Run(sortFuncCall) require.NoError(b, err) } } /**********************************************************************************************************************/ // Appendix - all the Javascript algorithm code constants const jsQuickSort = ` function quickSort(arr, left, right){ var len = arr.length, pivot, partitionIndex; if(left < right){ pivot = right; partitionIndex = partition(arr, pivot, left, right); // sort left and right quickSort(arr, left, partitionIndex - 1); quickSort(arr, partitionIndex + 1, right); } return arr; } function partition(arr, pivot, left, right){ var pivotValue = arr[pivot], partitionIndex = left; for(var i = left; i < right; i++){ if(arr[i] < pivotValue){ swap(arr, i, partitionIndex); partitionIndex++; } } swap(arr, right, partitionIndex); return partitionIndex; } function swap(arr, i, j){ var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } ` const jsMergeSort = ` function mergeSort(arr){ var len = arr.length; if(len <2) return arr; var mid = Math.floor(len/2), left = arr.slice(0,mid), right =arr.slice(mid); // send left and right to the mergeSort to broke it down into pieces // then merge those return merge(mergeSort(left),mergeSort(right)); } function merge(left, right){ var result = [], lLen = left.length, rLen = right.length, l = 0, r = 0; while(l < lLen && r < rLen){ if(left[l] < right[r]){ result.push(left[l++]); } else{ result.push(right[r++]); } } // remaining part needs to be addred to the result return result.concat(left.slice(l)).concat(right.slice(r)); } ` const jsHeapSort = ` function heapSort(arr){ var len = arr.length, end = len-1; heapify(arr, len); while(end > 0){ swap(arr, end--, 0); siftDown(arr, 0, end); } return arr; } function heapify(arr, len){ // break the array into root + two sides, to create tree (heap) var mid = Math.floor((len-2)/2); while(mid >= 0){ siftDown(arr, mid--, len-1); } } function siftDown(arr, start, end){ var root = start, child = root*2 + 1, toSwap = root; while(child <= end){ if(arr[toSwap] < arr[child]){ swap(arr, toSwap, child); } if(child+1 <= end && arr[toSwap] < arr[child+1]){ swap(arr, toSwap, child+1) } if(toSwap != root){ swap(arr, root, toSwap); root = toSwap; } else{ return; } toSwap = root; child = root*2+1 } } function swap(arr, i, j){ var temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } ` // Copied from JetStream benchmarking suite // http://browserbench.org/JetStream/sources/crypto-aes.js const jsCryptoAES = ` /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* * AES Cipher function: encrypt 'input' with Rijndael algorithm * * takes byte-array 'input' (16 bytes) * 2D byte-array key schedule 'w' (Nr+1 x Nb bytes) * * applies Nr rounds (10/12/14) using key schedule w for 'add round key' stage * * returns byte-array encrypted value (16 bytes) */ function Cipher(input, w) { // main Cipher function [§5.1] var Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES) var Nr = w.length/Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys var state = [[],[],[],[]]; // initialise 4xNb byte-array 'state' with input [§3.4] for (var i=0; i<4*Nb; i++) state[i%4][Math.floor(i/4)] = input[i]; state = AddRoundKey(state, w, 0, Nb); for (var round=1; round 6 && i%Nk == 4) { temp = SubWord(temp); } for (var t=0; t<4; t++) w[i][t] = w[i-Nk][t] ^ temp[t]; } return w; } function SubWord(w) { // apply SBox to 4-byte word w for (var i=0; i<4; i++) w[i] = Sbox[w[i]]; return w; } function RotWord(w) { // rotate 4-byte word w left by one byte w[4] = w[0]; for (var i=0; i<4; i++) w[i] = w[i+1]; return w; } // Sbox is pre-computed multiplicative inverse in GF(2^8) used in SubBytes and KeyExpansion [§5.1.1] var Sbox = [0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16]; // Rcon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [§5.2] var Rcon = [ [0x00, 0x00, 0x00, 0x00], [0x01, 0x00, 0x00, 0x00], [0x02, 0x00, 0x00, 0x00], [0x04, 0x00, 0x00, 0x00], [0x08, 0x00, 0x00, 0x00], [0x10, 0x00, 0x00, 0x00], [0x20, 0x00, 0x00, 0x00], [0x40, 0x00, 0x00, 0x00], [0x80, 0x00, 0x00, 0x00], [0x1b, 0x00, 0x00, 0x00], [0x36, 0x00, 0x00, 0x00] ]; /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* * Use AES to encrypt 'plaintext' with 'password' using 'nBits' key, in 'Counter' mode of operation * - see http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf * for each block * - outputblock = cipher(counter, key) * - cipherblock = plaintext xor outputblock */ function AESEncryptCtr(plaintext, password, nBits) { if (!(nBits==128 || nBits==192 || nBits==256)) return ''; // standard allows 128/192/256 bit keys // for this example script, generate the key by applying Cipher to 1st 16/24/32 chars of password; // for real-world applications, a more secure approach would be to hash the password e.g. with SHA-1 var nBytes = nBits/8; // no bytes in key var pwBytes = new Array(nBytes); for (var i=0; i>> i*8) & 0xff; for (var i=0; i<4; i++) counterBlock[i+4] = (nonce/0x100000000 >>> i*8) & 0xff; // generate key schedule - an expansion of the key into distinct Key Rounds for each round var keySchedule = KeyExpansion(key); var blockCount = Math.ceil(plaintext.length/blockSize); var ciphertext = new Array(blockCount); // ciphertext as array of strings for (var b=0; b>> c*8) & 0xff; for (var c=0; c<4; c++) counterBlock[15-c-4] = (b/0x100000000 >>> c*8) var cipherCntr = Cipher(counterBlock, keySchedule); // -- encrypt counter block -- // calculate length of final block: var blockLength = b>> c*8) & 0xff; for (var c=0; c<4; c++) counterBlock[15-c-4] = ((b/0x100000000-1) >>> c*8) & 0xff; var cipherCntr = Cipher(counterBlock, keySchedule); // encrypt counter block ciphertext[b] = unescCtrlChars(ciphertext[b]); var pt = ''; for (var i=0; i>18 & 0x3f; h2 = bits>>12 & 0x3f; h3 = bits>>6 & 0x3f; h4 = bits & 0x3f; // end of string? index to '=' in b64 if (isNaN(o3)) h4 = 64; if (isNaN(o2)) h3 = 64; // use hexets to index into b64, and append result to encoded string enc += b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); } while (i < str.length); return enc; } function decodeBase64(str) { var o1, o2, o3, h1, h2, h3, h4, bits, i=0, enc=''; do { // unpack four hexets into three octets using index points in b64 h1 = b64.indexOf(str.charAt(i++)); h2 = b64.indexOf(str.charAt(i++)); h3 = b64.indexOf(str.charAt(i++)); h4 = b64.indexOf(str.charAt(i++)); bits = h1<<18 | h2<<12 | h3<<6 | h4; o1 = bits>>16 & 0xff; o2 = bits>>8 & 0xff; o3 = bits & 0xff; if (h3 == 64) enc += String.fromCharCode(o1); else if (h4 == 64) enc += String.fromCharCode(o1, o2); else enc += String.fromCharCode(o1, o2, o3); } while (i < str.length); return decodeUTF8(enc); // decode UTF-8 byte-array back to Unicode } function encodeUTF8(str) { // encode multi-byte string into utf-8 multiple single-byte characters str = str.replace( /[\u0080-\u07ff]/g, // U+0080 - U+07FF = 2-byte chars function(c) { var cc = c.charCodeAt(0); return String.fromCharCode(0xc0 | cc>>6, 0x80 | cc&0x3f); } ); str = str.replace( /[\u0800-\uffff]/g, // U+0800 - U+FFFF = 3-byte chars function(c) { var cc = c.charCodeAt(0); return String.fromCharCode(0xe0 | cc>>12, 0x80 | cc>>6&0x3F, 0x80 | cc&0x3f); } ); return str; } function decodeUTF8(str) { // decode utf-8 encoded string back into multi-byte characters str = str.replace( /[\u00c0-\u00df][\u0080-\u00bf]/g, // 2-byte chars function(c) { var cc = (c.charCodeAt(0)&0x1f)<<6 | c.charCodeAt(1)&0x3f; return String.fromCharCode(cc); } ); str = str.replace( /[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g, // 3-byte chars function(c) { var cc = (c.charCodeAt(0)&0x0f)<<12 | (c.charCodeAt(1)&0x3f<<6) | c.charCodeAt(2)&0x3f; return String.fromCharCode(cc); } ); return str; } function byteArrayToHexStr(b) { // convert byte array to hex string for displaying test vectors var s = ''; for (var i=0; i>>2]|=(d[e>>>2]>>>24-8*(e%4)&255)<<24-8*((b+e)%4);else if(65535>>2]=d[e>>>2];else c.push.apply(c,d);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< 32-8*(c%4);a.length=h.ceil(c/4)},clone:function(){var a=m.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],d=0;d>>2]>>>24-8*(b%4)&255;d.push((e>>>4).toString(16));d.push((e&15).toString(16))}return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>3]|=parseInt(a.substr(b, 2),16)<<24-4*(b%8);return new r.init(d,c/2)}},n=l.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var d=[],b=0;b>>2]>>>24-8*(b%4)&255));return d.join("")},parse:function(a){for(var c=a.length,d=[],b=0;b>>2]|=(a.charCodeAt(b)&255)<<24-8*(b%4);return new r.init(d,c)}},j=l.Utf8={stringify:function(a){try{return decodeURIComponent(escape(n.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return n.parse(unescape(encodeURIComponent(a)))}}, u=g.BufferedBlockAlgorithm=m.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=j.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,d=c.words,b=c.sigBytes,e=this.blockSize,f=b/(4*e),f=a?h.ceil(f):h.max((f|0)-this._minBufferSize,0);a=f*e;b=h.min(4*a,b);if(a){for(var g=0;gn;){var j;a:{j=k;for(var u=h.sqrt(j),t=2;t<=u;t++)if(!(j%t)){j=!1;break a}j=!0}j&&(8>n&&(m[n]=l(h.pow(k,0.5))),r[n]=l(h.pow(k,1/3)),n++);k++}var a=[],f=f.SHA256=q.extend({_doReset:function(){this._hash=new g.init(m.slice(0))},_doProcessBlock:function(c,d){for(var b=this._hash.words,e=b[0],f=b[1],g=b[2],j=b[3],h=b[4],m=b[5],n=b[6],q=b[7],p=0;64>p;p++){if(16>p)a[p]= c[d+p]|0;else{var k=a[p-15],l=a[p-2];a[p]=((k<<25|k>>>7)^(k<<14|k>>>18)^k>>>3)+a[p-7]+((l<<15|l>>>17)^(l<<13|l>>>19)^l>>>10)+a[p-16]}k=q+((h<<26|h>>>6)^(h<<21|h>>>11)^(h<<7|h>>>25))+(h&m^~h&n)+r[p]+a[p];l=((e<<30|e>>>2)^(e<<19|e>>>13)^(e<<10|e>>>22))+(e&f^e&g^f&g);q=n;n=m;m=h;h=j+k|0;j=g;g=f;f=e;e=k+l|0}b[0]=b[0]+e|0;b[1]=b[1]+f|0;b[2]=b[2]+g|0;b[3]=b[3]+j|0;b[4]=b[4]+h|0;b[5]=b[5]+m|0;b[6]=b[6]+n|0;b[7]=b[7]+q|0},_doFinalize:function(){var a=this._data,d=a.words,b=8*this._nDataBytes,e=8*a.sigBytes; d[e>>>5]|=128<<24-e%32;d[(e+64>>>9<<4)+14]=h.floor(b/4294967296);d[(e+64>>>9<<4)+15]=b;a.sigBytes=4*d.length;this._process();return this._hash},clone:function(){var a=q.clone.call(this);a._hash=this._hash.clone();return a}});s.SHA256=q._createHelper(f);s.HmacSHA256=q._createHmacHelper(f)})(Math); (function(){var h=CryptoJS,s=h.enc.Utf8;h.algo.HMAC=h.lib.Base.extend({init:function(f,g){f=this._hasher=new f.init;"string"==typeof g&&(g=s.parse(g));var h=f.blockSize,m=4*h;g.sigBytes>m&&(g=f.finalize(g));g.clamp();for(var r=this._oKey=g.clone(),l=this._iKey=g.clone(),k=r.words,n=l.words,j=0;j 0.4636476090008061 // darwin 1.03 => 0.46364760900080604 test(`Math.atan(0.5).toPrecision(10)`, "0.463647609") }) } func TestMath_atan2(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.atan2()`, naN) test(`Math.atan2(NaN)`, naN) test(`Math.atan2(0, NaN)`, naN) test(`Math.atan2(1, 0)`, 1.5707963267948966) test(`Math.atan2(1, -0)`, 1.5707963267948966) test(`1/Math.atan2(0, 1)`, infinity) test(`1/Math.atan2(0, 0)`, infinity) test(`Math.atan2(0, -0)`, 3.141592653589793) test(`Math.atan2(0, -1)`, 3.141592653589793) test(`1/Math.atan2(-0, 1)`, -infinity) test(`1/Math.atan2(-0, 0)`, -infinity) test(`Math.atan2(-0, -0)`, -3.141592653589793) test(`Math.atan2(-0, -1)`, -3.141592653589793) test(`Math.atan2(-1, 0)`, -1.5707963267948966) test(`Math.atan2(-1, -0)`, -1.5707963267948966) test(`1/Math.atan2(1, Infinity)`, infinity) test(`Math.atan2(1, -Infinity)`, 3.141592653589793) test(`1/Math.atan2(-1, Infinity)`, -infinity) test(`Math.atan2(-1, -Infinity)`, -3.141592653589793) test(`Math.atan2(Infinity, 1)`, 1.5707963267948966) test(`Math.atan2(-Infinity, 1)`, -1.5707963267948966) test(`Math.atan2(Infinity, Infinity)`, 0.7853981633974483) test(`Math.atan2(Infinity, -Infinity)`, 2.356194490192345) test(`Math.atan2(-Infinity, Infinity)`, -0.7853981633974483) test(`Math.atan2(-Infinity, -Infinity)`, -2.356194490192345) }) } func TestMath_atanh(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.atanh(-2)`, naN) test(`Math.atanh(2)`, naN) test(`Math.atanh(-1)`, -infinity) test(`Math.atanh(1)`, infinity) test(`Math.atanh(0)`, 0) test(`Math.atanh(0.5)`, 0.5493061443340548) }) } func TestMath_cbrt(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.cbrt(NaN)`, naN) test(`Math.cbrt(-1)`, -1) test(`Math.cbrt(1)`, 1) test(`Math.cbrt(-0)`, -0) test(`Math.cbrt(0)`, 0) test(`Math.cbrt(-Infinity)`, -infinity) test(`Math.cbrt(Infinity)`, infinity) test(`Math.cbrt(null)`, 0) test(`Math.cbrt(2)`, 1.2599210498948732) }) } func TestMath_ceil(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.ceil(NaN)`, naN) test(`Math.ceil(+0)`, 0) test(`1/Math.ceil(-0)`, -infinity) test(`Math.ceil(Infinity)`, infinity) test(`Math.ceil(-Infinity)`, -infinity) test(`1/Math.ceil(-0.5)`, -infinity) test(`Math.ceil(-11)`, -11) test(`Math.ceil(-0.5)`, 0) test(`Math.ceil(1.5)`, 2) }) } func TestMath_cos(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.cos(NaN)`, naN) test(`Math.cos(+0)`, 1) test(`Math.cos(-0)`, 1) test(`Math.cos(Infinity)`, naN) test(`Math.cos(-Infinity)`, naN) test(`Math.cos(0.5)`, 0.8775825618903728) }) } func TestMath_cosh(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.cosh(0)`, 1) test(`Math.cosh(1)`, 1.5430806348152437) test(`Math.cosh(-1)`, 1.5430806348152437) }) } func TestMath_exp(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.exp(NaN)`, naN) test(`Math.exp(+0)`, 1) test(`Math.exp(-0)`, 1) test(`Math.exp(Infinity)`, infinity) test(`Math.exp(-Infinity)`, 0) }) } func TestMath_expm1(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.expm1(0)`, 0) test(`Math.expm1(1)`, 1.718281828459045) test(`Math.expm1(-1)`, -0.6321205588285577) test(`Math.expm1(2)`, 6.38905609893065) test(`Math.expm1("foo")`, naN) }) } func TestMath_floor(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.floor(NaN)`, naN) test(`Math.floor(+0)`, 0) test(`1/Math.floor(-0)`, -infinity) test(`Math.floor(Infinity)`, infinity) test(`Math.floor(-Infinity)`, -infinity) test(`Math.floor(-11)`, -11) test(`Math.floor(-0.5)`, -1) test(`Math.floor(1.5)`, 1) }) } func TestMath_log(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.log(NaN)`, naN) test(`Math.log(-1)`, naN) test(`Math.log(+0)`, -infinity) test(`Math.log(-0)`, -infinity) test(`1/Math.log(1)`, infinity) test(`Math.log(Infinity)`, infinity) test(`Math.log(0.5)`, -0.6931471805599453) }) } func TestMath_log10(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.log10(100000)`, 5) test(`Math.log10(-2)`, naN) test(`Math.log10(2)`, 0.3010299956639812) test(`Math.log10(1)`, 0) test(`Math.log10(-0)`, -infinity) test(`Math.log10(0)`, -infinity) test(`Math.log10(Infinity)`, infinity) }) } func TestMath_log1p(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.log1p(-2)`, naN) test(`Math.log1p(-1)`, -infinity) test(`Math.log1p(1)`, 0.6931471805599453) test(`Math.log1p(-0)`, -0) test(`Math.log1p(0)`, 0) test(`Math.log1p(Infinity)`, infinity) }) } func TestMath_log2(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.log2(-2)`, naN) test(`Math.log2(-0)`, -infinity) test(`Math.log2(0)`, -infinity) test(`Math.log2(1)`, 0) test(`Math.log2(2)`, 1) test(`Math.log2(5)`, 2.321928094887362) test(`Math.log2(1024)`, 10) test(`Math.log2(Infinity)`, infinity) }) } func TestMath_max(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.max(-11, -1, 0, 1, 2, 3, 11)`, 11) }) } func TestMath_min(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.min(-11, -1, 0, 1, 2, 3, 11)`, -11) }) } func TestMath_pow(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.pow(0, NaN)`, naN) test(`Math.pow(0, 0)`, 1) test(`Math.pow(NaN, 0)`, 1) test(`Math.pow(0, -0)`, 1) test(`Math.pow(NaN, -0)`, 1) test(`Math.pow(NaN, 1)`, naN) test(`Math.pow(2, Infinity)`, infinity) test(`1/Math.pow(2, -Infinity)`, infinity) test(`Math.pow(1, Infinity)`, naN) test(`Math.pow(1, -Infinity)`, naN) test(`1/Math.pow(0.1, Infinity)`, infinity) test(`Math.pow(0.1, -Infinity)`, infinity) test(`Math.pow(Infinity, 1)`, infinity) test(`1/Math.pow(Infinity, -1)`, infinity) test(`Math.pow(-Infinity, 1)`, -infinity) test(`Math.pow(-Infinity, 2)`, infinity) test(`1/Math.pow(-Infinity, -1)`, -infinity) test(`1/Math.pow(-Infinity, -2)`, infinity) test(`1/Math.pow(0, 1)`, infinity) test(`Math.pow(0, -1)`, infinity) test(`1/Math.pow(-0, 1)`, -infinity) test(`1/Math.pow(-0, 2)`, infinity) test(`Math.pow(-0, -1)`, -infinity) test(`Math.pow(-0, -2)`, infinity) test(`Math.pow(-1, 0.1)`, naN) test(` [ Math.pow(-1, +Infinity), Math.pow(1, Infinity) ]; `, "NaN,NaN") }) } func TestMath_round(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.round(NaN)`, naN) test(`1/Math.round(0)`, infinity) test(`1/Math.round(-0)`, -infinity) test(`Math.round(Infinity)`, infinity) test(`Math.round(-Infinity)`, -infinity) test(`1/Math.round(0.1)`, infinity) test(`1/Math.round(-0.1)`, -infinity) test(`Math.round(3.5)`, 4) test(`Math.round(-3.5)`, -3) }) } func TestMath_sin(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.sin(NaN)`, naN) test(`1/Math.sin(+0)`, infinity) test(`1/Math.sin(-0)`, -infinity) test(`Math.sin(Infinity)`, naN) test(`Math.sin(-Infinity)`, naN) test(`Math.sin(0.5)`, 0.479425538604203) }) } func TestMath_sinh(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.sinh(-Infinity)`, -infinity) test(`Math.sinh(Infinity)`, infinity) test(`Math.sinh(-0)`, -0) test(`Math.sinh(0)`, 0) test(`Math.sinh(-1)`, -1.1752011936438014) test(`Math.sinh(1)`, 1.1752011936438014) test(`Math.sinh(2)`, 3.626860407847019) }) } func TestMath_sqrt(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.sqrt(NaN)`, naN) test(`Math.sqrt(-1)`, naN) test(`1/Math.sqrt(+0)`, infinity) test(`1/Math.sqrt(-0)`, -infinity) test(`Math.sqrt(Infinity)`, infinity) test(`Math.sqrt(2)`, 1.4142135623730951) }) } func TestMath_tan(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.tan(NaN)`, naN) test(`1/Math.tan(+0)`, infinity) test(`1/Math.tan(-0)`, -infinity) test(`Math.tan(Infinity)`, naN) test(`Math.tan(-Infinity)`, naN) test(`Math.tan(0.5)`, 0.5463024898437905) }) } func TestMath_tanh(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.tanh(Infinity)`, 1) test(`Math.tanh(-Infinity)`, -1) test(`Math.tanh(-1)`, -0.7615941559557649) test(`Math.tanh(1)`, 0.7615941559557649) test(`Math.tanh(-0)`, -0) test(`Math.tanh(0)`, 0) }) } func TestMath_trunc(t *testing.T) { tt(t, func() { test, _ := test() test(`Math.trunc(-Infinity)`, -infinity) test(`Math.trunc(Infinity)`, infinity) test(`Math.trunc(-0.123)`, -0) test(`Math.trunc(0.123)`, 0) test(`Math.trunc(-0)`, -0) test(`Math.trunc(0)`, 0) test(`Math.trunc("-1.123")`, -1) test(`Math.trunc(13.37)`, 13) test(`Math.trunc(42.84)`, 42) }) } ================================================ FILE: native_stack_test.go ================================================ package otto import ( "testing" "github.com/stretchr/testify/require" ) func TestNativeStackFrames(t *testing.T) { tt(t, func() { vm := New() s, err := vm.Compile("input.js", ` function A() { ext1(); } function B() { ext2(); } A(); `) require.NoError(t, err) err = vm.Set("ext1", func(c FunctionCall) Value { if _, err = c.Otto.Eval("B()"); err != nil { panic(err) } return UndefinedValue() }) require.NoError(t, err) err = vm.Set("ext2", func(c FunctionCall) Value { { // no limit, include innermost native frames ctx := c.Otto.ContextSkip(-1, false) is(ctx.Stacktrace, []string{ "github.com/robertkrimen/otto.TestNativeStackFrames.func1.2 (native_stack_test.go:29)", "B (input.js:3:19)", "github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)", "A (input.js:2:19)", "input.js:4:4", }) is(ctx.Callee, "github.com/robertkrimen/otto.TestNativeStackFrames.func1.2") is(ctx.Filename, "native_stack_test.go") is(ctx.Line, 29) is(ctx.Column, 0) } { // no limit, skip innermost native frames ctx := c.Otto.ContextSkip(-1, true) is(ctx.Stacktrace, []string{ "B (input.js:3:19)", "github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)", "A (input.js:2:19)", "input.js:4:4", }) is(ctx.Callee, "B") is(ctx.Filename, "input.js") is(ctx.Line, 3) is(ctx.Column, 19) } if _, err = c.Otto.Eval("ext3()"); err != nil { panic(err) } return UndefinedValue() }) require.NoError(t, err) err = vm.Set("ext3", func(c FunctionCall) Value { { // no limit, include innermost native frames ctx := c.Otto.ContextSkip(-1, false) is(ctx.Stacktrace, []string{ "github.com/robertkrimen/otto.TestNativeStackFrames.func1.3 (native_stack_test.go:71)", "github.com/robertkrimen/otto.TestNativeStackFrames.func1.2 (native_stack_test.go:29)", "B (input.js:3:19)", "github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)", "A (input.js:2:19)", "input.js:4:4", }) is(ctx.Callee, "github.com/robertkrimen/otto.TestNativeStackFrames.func1.3") is(ctx.Filename, "native_stack_test.go") is(ctx.Line, 71) is(ctx.Column, 0) } { // no limit, skip innermost native frames ctx := c.Otto.ContextSkip(-1, true) is(ctx.Stacktrace, []string{ "B (input.js:3:19)", "github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)", "A (input.js:2:19)", "input.js:4:4", }) is(ctx.Callee, "B") is(ctx.Filename, "input.js") is(ctx.Line, 3) is(ctx.Column, 19) } return UndefinedValue() }) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) }) } ================================================ FILE: number_test.go ================================================ package otto import ( "testing" ) func TestNumber(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = Object.getOwnPropertyDescriptor(Number, "prototype"); [ [ typeof Number.prototype ], [ abc.writable, abc.enumerable, abc.configurable ] ]; `, "object,false,false,false") }) } func TestNumber_toString(t *testing.T) { tt(t, func() { test, _ := test() test(` new Number(451).toString(); `, "451") test(` new Number(451).toString(10); `, "451") test(` new Number(451).toString(8); `, "703") test(`raise: new Number(451).toString(1); `, "RangeError: toString() radix must be between 2 and 36") test(`raise: new Number(451).toString(Infinity); `, "RangeError: toString() radix must be between 2 and 36") test(` new Number(NaN).toString() `, "NaN") test(` new Number(Infinity).toString() `, "Infinity") test(` new Number(Infinity).toString(16) `, "Infinity") test(` [ Number.prototype.toString(undefined), new Number().toString(undefined), new Number(0).toString(undefined), new Number(-1).toString(undefined), new Number(1).toString(undefined), new Number(Number.NaN).toString(undefined), new Number(Number.POSITIVE_INFINITY).toString(undefined), new Number(Number.NEGATIVE_INFINITY).toString(undefined) ] `, "0,0,0,-1,1,NaN,Infinity,-Infinity") }) } func TestNumber_toFixed(t *testing.T) { tt(t, func() { test, _ := test() test(`new Number(451).toFixed(2)`, "451.00") test(`12345.6789.toFixed()`, "12346") test(`12345.6789.toFixed(1)`, "12345.7") test(`12345.6789.toFixed(6)`, "12345.678900") test(`(1.23e-20).toFixed(2)`, "0.00") test(`2.34.toFixed(1)`, "2.3") // FIXME Wtf? "2.3" test(`-2.34.toFixed(1)`, -2.3) // FIXME Wtf? -2.3 test(`(-2.34).toFixed(1)`, "-2.3") test(`raise: new Number("a").toFixed(Number.POSITIVE_INFINITY); `, "RangeError: toFixed() precision must be between 0 and 20") test(` [ new Number(1e21).toFixed(), new Number(1e21).toFixed(0), new Number(1e21).toFixed(1), new Number(1e21).toFixed(1.1), new Number(1e21).toFixed(0.9), new Number(1e21).toFixed("1"), new Number(1e21).toFixed("1.1"), new Number(1e21).toFixed("0.9"), new Number(1e21).toFixed(Number.NaN), new Number(1e21).toFixed("some string") ]; `, "1e+21,1e+21,1e+21,1e+21,1e+21,1e+21,1e+21,1e+21,1e+21,1e+21") test(`raise: new Number(1e21).toFixed(Number.POSITIVE_INFINITY); `, "RangeError: toFixed() precision must be between 0 and 20") }) } func TestNumber_toExponential(t *testing.T) { tt(t, func() { test, _ := test() test(`new Number(451).toExponential(2)`, "4.51e+02") test(`77.1234.toExponential()`, "7.71234e+01") test(`77.1234.toExponential(4)`, "7.7123e+01") test(`77.1234.toExponential(2)`, "7.71e+01") test(`77 .toExponential()`, "7.7e+01") }) } func TestNumber_toPrecision(t *testing.T) { tt(t, func() { test, _ := test() test(`new Number(451).toPrecision()`, "451") test(`new Number(451).toPrecision(1)`, "5e+02") test(`5.123456.toPrecision()`, "5.123456") test(`5.123456.toPrecision(5)`, "5.1235") test(`5.123456.toPrecision(2)`, "5.1") test(`5.123456.toPrecision(1)`, "5") }) } func TestNumber_toLocaleString(t *testing.T) { tt(t, func() { test, _ := test() test(` [ new Number(4510).toLocaleString(), new Number(4510).toLocaleString('en-US'), new Number(4510).toLocaleString('nl-NL') ]; `, "4,510,4,510,4.510") }) } func TestValue_number(t *testing.T) { tt(t, func() { nm := toValue(0.0).number() is(nm.kind, numberInteger) nm = toValue(3.14159).number() is(nm.kind, numberFloat) }) } func Test_NaN(t *testing.T) { tt(t, func() { test, _ := test() test(` [ NaN === NaN, NaN == NaN ]; `, "false,false") }) } ================================================ FILE: object.go ================================================ package otto type object struct { value interface{} runtime *runtime objectClass *objectClass prototype *object property map[string]property class string propertyOrder []string extensible bool } func newObject(rt *runtime, class string) *object { o := &object{ runtime: rt, class: class, objectClass: classObject, property: make(map[string]property), extensible: true, } return o } // 8.12 // 8.12.1. func (o *object) getOwnProperty(name string) *property { return o.objectClass.getOwnProperty(o, name) } // 8.12.2. func (o *object) getProperty(name string) *property { return o.objectClass.getProperty(o, name) } // 8.12.3. func (o *object) get(name string) Value { return o.objectClass.get(o, name) } // 8.12.4. func (o *object) canPut(name string) bool { return o.objectClass.canPut(o, name) } // 8.12.5. func (o *object) put(name string, value Value, throw bool) { o.objectClass.put(o, name, value, throw) } // 8.12.6. func (o *object) hasProperty(name string) bool { return o.objectClass.hasProperty(o, name) } func (o *object) hasOwnProperty(name string) bool { return o.objectClass.hasOwnProperty(o, name) } type defaultValueHint int const ( defaultValueNoHint defaultValueHint = iota defaultValueHintString defaultValueHintNumber ) // 8.12.8. func (o *object) DefaultValue(hint defaultValueHint) Value { if hint == defaultValueNoHint { if o.class == classDateName { // Date exception hint = defaultValueHintString } else { hint = defaultValueHintNumber } } methodSequence := []string{"valueOf", "toString"} if hint == defaultValueHintString { methodSequence = []string{"toString", "valueOf"} } for _, methodName := range methodSequence { method := o.get(methodName) // FIXME This is redundant... if method.isCallable() { result := method.object().call(objectValue(o), nil, false, nativeFrame) if result.IsPrimitive() { return result } } } panic(o.runtime.panicTypeError("Object.DefaultValue unknown")) } func (o *object) String() string { return o.DefaultValue(defaultValueHintString).string() } func (o *object) defineProperty(name string, value Value, mode propertyMode, throw bool) bool { //nolint:unparam return o.defineOwnProperty(name, property{value, mode}, throw) } // 8.12.9. func (o *object) defineOwnProperty(name string, descriptor property, throw bool) bool { return o.objectClass.defineOwnProperty(o, name, descriptor, throw) } func (o *object) delete(name string, throw bool) bool { return o.objectClass.delete(o, name, throw) } func (o *object) enumerate(all bool, each func(string) bool) { o.objectClass.enumerate(o, all, each) } func (o *object) readProperty(name string) (property, bool) { prop, exists := o.property[name] return prop, exists } func (o *object) writeProperty(name string, value interface{}, mode propertyMode) { if value == nil { value = Value{} } if _, exists := o.property[name]; !exists { o.propertyOrder = append(o.propertyOrder, name) } o.property[name] = property{value, mode} } func (o *object) deleteProperty(name string) { if _, exists := o.property[name]; !exists { return } delete(o.property, name) for index, prop := range o.propertyOrder { if name == prop { if index == len(o.propertyOrder)-1 { o.propertyOrder = o.propertyOrder[:index] } else { o.propertyOrder = append(o.propertyOrder[:index], o.propertyOrder[index+1:]...) } } } } ================================================ FILE: object_class.go ================================================ package otto import ( "encoding/json" ) type objectClass struct { getOwnProperty func(*object, string) *property getProperty func(*object, string) *property get func(*object, string) Value canPut func(*object, string) bool put func(*object, string, Value, bool) hasProperty func(*object, string) bool hasOwnProperty func(*object, string) bool defineOwnProperty func(*object, string, property, bool) bool delete func(*object, string, bool) bool enumerate func(*object, bool, func(string) bool) clone func(*object, *object, *cloner) *object marshalJSON func(*object) json.Marshaler } func objectEnumerate(obj *object, all bool, each func(string) bool) { for _, name := range obj.propertyOrder { if all || obj.property[name].enumerable() { if !each(name) { return } } } } var classObject, classArray, classString, classArguments, classGoStruct, classGoMap, classGoArray, classGoSlice *objectClass func init() { classObject = &objectClass{ objectGetOwnProperty, objectGetProperty, objectGet, objectCanPut, objectPut, objectHasProperty, objectHasOwnProperty, objectDefineOwnProperty, objectDelete, objectEnumerate, objectClone, nil, } classArray = &objectClass{ objectGetOwnProperty, objectGetProperty, objectGet, objectCanPut, objectPut, objectHasProperty, objectHasOwnProperty, arrayDefineOwnProperty, objectDelete, objectEnumerate, objectClone, nil, } classString = &objectClass{ stringGetOwnProperty, objectGetProperty, objectGet, objectCanPut, objectPut, objectHasProperty, objectHasOwnProperty, objectDefineOwnProperty, objectDelete, stringEnumerate, objectClone, nil, } classArguments = &objectClass{ argumentsGetOwnProperty, objectGetProperty, argumentsGet, objectCanPut, objectPut, objectHasProperty, objectHasOwnProperty, argumentsDefineOwnProperty, argumentsDelete, objectEnumerate, objectClone, nil, } classGoStruct = &objectClass{ goStructGetOwnProperty, objectGetProperty, objectGet, goStructCanPut, goStructPut, objectHasProperty, objectHasOwnProperty, objectDefineOwnProperty, objectDelete, goStructEnumerate, objectClone, goStructMarshalJSON, } classGoMap = &objectClass{ goMapGetOwnProperty, objectGetProperty, objectGet, objectCanPut, objectPut, objectHasProperty, objectHasOwnProperty, goMapDefineOwnProperty, goMapDelete, goMapEnumerate, objectClone, nil, } classGoArray = &objectClass{ goArrayGetOwnProperty, objectGetProperty, objectGet, objectCanPut, objectPut, objectHasProperty, objectHasOwnProperty, goArrayDefineOwnProperty, goArrayDelete, goArrayEnumerate, objectClone, nil, } classGoSlice = &objectClass{ goSliceGetOwnProperty, objectGetProperty, objectGet, objectCanPut, objectPut, objectHasProperty, objectHasOwnProperty, goSliceDefineOwnProperty, goSliceDelete, goSliceEnumerate, objectClone, nil, } } // Allons-y // 8.12.1. func objectGetOwnProperty(obj *object, name string) *property { // Return a _copy_ of the prop prop, exists := obj.readProperty(name) if !exists { return nil } return &prop } // 8.12.2. func objectGetProperty(obj *object, name string) *property { prop := obj.getOwnProperty(name) if prop != nil { return prop } if obj.prototype != nil { return obj.prototype.getProperty(name) } return nil } // 8.12.3. func objectGet(obj *object, name string) Value { if prop := obj.getProperty(name); prop != nil { return prop.get(obj) } return Value{} } // 8.12.4. func objectCanPut(obj *object, name string) bool { canPut, _, _ := objectCanPutDetails(obj, name) return canPut } func objectCanPutDetails(obj *object, name string) (canPut bool, prop *property, setter *object) { //nolint:nonamedreturns prop = obj.getOwnProperty(name) if prop != nil { switch propertyValue := prop.value.(type) { case Value: return prop.writable(), prop, nil case propertyGetSet: setter = propertyValue[1] return setter != nil, prop, setter default: panic(obj.runtime.panicTypeError("unexpected type %T to Object.CanPutDetails", prop.value)) } } if obj.prototype == nil { return obj.extensible, nil, nil } prop = obj.prototype.getProperty(name) if prop == nil { return obj.extensible, nil, nil } switch propertyValue := prop.value.(type) { case Value: if !obj.extensible { return false, nil, nil } return prop.writable(), nil, nil case propertyGetSet: setter = propertyValue[1] return setter != nil, prop, setter default: panic(obj.runtime.panicTypeError("unexpected type %T to Object.CanPutDetails", prop.value)) } } // 8.12.5. func objectPut(obj *object, name string, value Value, throw bool) { if true { // Shortcut... // // So, right now, every class is using objectCanPut and every class // is using objectPut. // // If that were to no longer be the case, we would have to have // something to detect that here, so that we do not use an // incompatible canPut routine canPut, prop, setter := objectCanPutDetails(obj, name) switch { case !canPut: obj.runtime.typeErrorResult(throw) case setter != nil: setter.call(toValue(obj), []Value{value}, false, nativeFrame) case prop != nil: prop.value = value obj.defineOwnProperty(name, *prop, throw) default: obj.defineProperty(name, value, 0o111, throw) } return } // The long way... // // Right now, code should never get here, see above if !obj.canPut(name) { obj.runtime.typeErrorResult(throw) return } prop := obj.getOwnProperty(name) if prop == nil { prop = obj.getProperty(name) if prop != nil { if getSet, isAccessor := prop.value.(propertyGetSet); isAccessor { getSet[1].call(toValue(obj), []Value{value}, false, nativeFrame) return } } obj.defineProperty(name, value, 0o111, throw) return } switch propertyValue := prop.value.(type) { case Value: prop.value = value obj.defineOwnProperty(name, *prop, throw) case propertyGetSet: if propertyValue[1] != nil { propertyValue[1].call(toValue(obj), []Value{value}, false, nativeFrame) return } if throw { panic(obj.runtime.panicTypeError("Object.Put nil second parameter to propertyGetSet")) } default: panic(obj.runtime.panicTypeError("Object.Put unexpected type %T", prop.value)) } } // 8.12.6. func objectHasProperty(obj *object, name string) bool { return obj.getProperty(name) != nil } func objectHasOwnProperty(obj *object, name string) bool { return obj.getOwnProperty(name) != nil } // 8.12.9. func objectDefineOwnProperty(obj *object, name string, descriptor property, throw bool) bool { reject := func(reason string) bool { if throw { panic(obj.runtime.panicTypeError("Object.DefineOwnProperty: %s", reason)) } return false } prop, exists := obj.readProperty(name) if !exists { if !obj.extensible { return reject("not exists and not extensible") } if newGetSet, isAccessor := descriptor.value.(propertyGetSet); isAccessor { if newGetSet[0] == &nilGetSetObject { newGetSet[0] = nil } if newGetSet[1] == &nilGetSetObject { newGetSet[1] = nil } descriptor.value = newGetSet } obj.writeProperty(name, descriptor.value, descriptor.mode) return true } if descriptor.isEmpty() { return true } // TODO Per 8.12.9.6 - We should shortcut here (returning true) if // the current and new (define) properties are the same configurable := prop.configurable() if !configurable { if descriptor.configurable() { return reject("property and descriptor not configurable") } // Test that, if enumerable is set on the property descriptor, then it should // be the same as the existing property if descriptor.enumerateSet() && descriptor.enumerable() != prop.enumerable() { return reject("property not configurable and enumerable miss match") } } value, isDataDescriptor := prop.value.(Value) getSet, _ := prop.value.(propertyGetSet) switch { case descriptor.isGenericDescriptor(): // GenericDescriptor case isDataDescriptor != descriptor.isDataDescriptor(): // DataDescriptor <=> AccessorDescriptor if !configurable { return reject("property descriptor not configurable") } case isDataDescriptor && descriptor.isDataDescriptor(): // DataDescriptor <=> DataDescriptor if !configurable { if !prop.writable() && descriptor.writable() { return reject("property not configurable or writeable and descriptor not writeable") } if !prop.writable() { if descriptor.value != nil && !sameValue(value, descriptor.value.(Value)) { return reject("property not configurable or writeable and descriptor not the same") } } } default: // AccessorDescriptor <=> AccessorDescriptor newGetSet, _ := descriptor.value.(propertyGetSet) presentGet, presentSet := true, true if newGetSet[0] == &nilGetSetObject { // Present, but nil newGetSet[0] = nil } else if newGetSet[0] == nil { // Missing, not even nil newGetSet[0] = getSet[0] presentGet = false } if newGetSet[1] == &nilGetSetObject { // Present, but nil newGetSet[1] = nil } else if newGetSet[1] == nil { // Missing, not even nil newGetSet[1] = getSet[1] presentSet = false } if !configurable { if (presentGet && (getSet[0] != newGetSet[0])) || (presentSet && (getSet[1] != newGetSet[1])) { return reject("access descriptor not configurable") } } descriptor.value = newGetSet } // This section will preserve attributes of // the original property, if necessary value1 := descriptor.value if value1 == nil { value1 = prop.value } else if newGetSet, isAccessor := descriptor.value.(propertyGetSet); isAccessor { if newGetSet[0] == &nilGetSetObject { newGetSet[0] = nil } if newGetSet[1] == &nilGetSetObject { newGetSet[1] = nil } value1 = newGetSet } mode1 := descriptor.mode if mode1&0o222 != 0 { // TODO Factor this out into somewhere testable // (Maybe put into switch ...) mode0 := prop.mode if mode1&0o200 != 0 { if descriptor.isDataDescriptor() { mode1 &= ^0o200 // Turn off "writable" missing mode1 |= (mode0 & 0o100) } } if mode1&0o20 != 0 { mode1 |= (mode0 & 0o10) } if mode1&0o2 != 0 { mode1 |= (mode0 & 0o1) } mode1 &= 0o311 // 0311 to preserve the non-setting on "writable" } obj.writeProperty(name, value1, mode1) return true } func objectDelete(obj *object, name string, throw bool) bool { prop := obj.getOwnProperty(name) if prop == nil { return true } if prop.configurable() { obj.deleteProperty(name) return true } return obj.runtime.typeErrorResult(throw) } func objectClone(in *object, out *object, clone *cloner) *object { *out = *in out.runtime = clone.runtime if out.prototype != nil { out.prototype = clone.object(in.prototype) } out.property = make(map[string]property, len(in.property)) out.propertyOrder = make([]string, len(in.propertyOrder)) copy(out.propertyOrder, in.propertyOrder) for index, prop := range in.property { out.property[index] = clone.property(prop) } switch value := in.value.(type) { case nativeFunctionObject: out.value = value case bindFunctionObject: out.value = bindFunctionObject{ target: clone.object(value.target), this: clone.value(value.this), argumentList: clone.valueArray(value.argumentList), } case nodeFunctionObject: out.value = nodeFunctionObject{ node: value.node, stash: clone.stash(value.stash), } case argumentsObject: out.value = value.clone(clone) } return out } ================================================ FILE: object_test.go ================================================ package otto import ( "testing" ) func TestObject_(t *testing.T) { tt(t, func() { test, _ := test() obj := newObject(nil, "") is(obj != nil, true) obj.put("xyzzy", toValue("Nothing happens."), true) is(obj.get("xyzzy"), "Nothing happens.") test(` var abc = Object.getOwnPropertyDescriptor(Object, "prototype"); [ [ typeof Object.prototype, abc.writable, abc.enumerable, abc.configurable ], ]; `, "object,false,false,false") }) } func TestStringObject(t *testing.T) { tt(t, func() { obj := New().runtime.newStringObject(toValue("xyzzy")) is(obj.get("1"), "y") is(obj.get("10"), "undefined") is(obj.get("2"), "z") }) } func TestObject_getPrototypeOf(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = {}; def = Object.getPrototypeOf(abc); ghi = Object.getPrototypeOf(def); [abc,def,ghi,ghi+""]; `, "[object Object],[object Object],,null") test(` abc = Object.getOwnPropertyDescriptor(Object, "getPrototypeOf"); [ abc.value === Object.getPrototypeOf, abc.writable, abc.enumerable, abc.configurable ]; `, "true,true,false,true") }) } func TestObject_new(t *testing.T) { tt(t, func() { test, _ := test() test(` [ new Object("abc"), new Object(2+2) ]; `, "abc,4") }) } func TestObject_create(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: Object.create()`, "TypeError: Object.Create is nil") test(` var abc = Object.create(null) var def = Object.create({x: 10, y: 20}) var ghi = Object.create(Object.prototype) var jkl = Object.create({x: 10, y: 20}, { z: { value: 30, writable: true }, // sum: { // get: function() { // return this.x + this.y + this.z // } // } }); [ abc.prototype, def.x, def.y, ghi, jkl.x, jkl.y, jkl.z ] `, ",10,20,[object Object],10,20,30") test(` var properties = {}; Object.defineProperty(properties, "abc", { value: {}, enumerable: false }); var mno = Object.create({}, properties); mno.hasOwnProperty("abc"); `, false) }) } func TestObject_toLocaleString(t *testing.T) { tt(t, func() { test, _ := test() test(` ({}).toLocaleString(); `, "[object Object]") test(` object = { toString: function() { return "Nothing happens."; } }; object.toLocaleString(); `, "Nothing happens.") }) } func TestObject_isExtensible(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: Object.isExtensible(); `, "TypeError: Object.IsExtensible is nil") // FIXME terst, Why raise? test(`raise: Object.isExtensible({}); `, true) test(`Object.isExtensible.length`, 1) test(`Object.isExtensible.prototype`, "undefined") }) } func TestObject_preventExtensions(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: Object.preventExtensions() `, "TypeError: Object.PreventExtensions is nil") test(`raise: var abc = { def: true }; var ghi = Object.preventExtensions(abc); [ ghi.def === true, Object.isExtensible(abc), Object.isExtensible(ghi) ]; `, "true,false,false") test(` var abc = new String(); var def = Object.isExtensible(abc); Object.preventExtensions(abc); var ghi = false; try { Object.defineProperty(abc, "0", { value: "~" }); } catch (err) { ghi = err instanceof TypeError; } [ def, ghi, abc.hasOwnProperty("0"), typeof abc[0] ]; `, "true,true,false,undefined") test(`Object.preventExtensions.length`, 1) test(`Object.preventExtensions.prototype`, "undefined") }) } func TestObject_isSealed(t *testing.T) { tt(t, func() { test, _ := test() test(`Object.isSealed.length`, 1) test(`Object.isSealed.prototype`, "undefined") }) } func TestObject_seal(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: Object.seal()`, "TypeError: Object.Seal is nil") test(` var abc = {a:1,b:1,c:3}; var sealed = Object.isSealed(abc); Object.seal(abc); [sealed, Object.isSealed(abc)]; `, "false,true") test(` var abc = {a:1,b:1,c:3}; var sealed = Object.isSealed(abc); var caught = false; Object.seal(abc); abc.b = 5; Object.defineProperty(abc, "a", {value:4}); try { Object.defineProperty(abc, "a", {value:42,enumerable:false}); } catch (e) { caught = e instanceof TypeError; } [sealed, Object.isSealed(abc), caught, abc.a, abc.b]; `, "false,true,true,4,5") test(`Object.seal.length`, 1) test(`Object.seal.prototype`, "undefined") }) } func TestObject_isFrozen(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: Object.isFrozen()`, "TypeError: Object.IsFrozen is nil") test(`Object.isFrozen(Object.preventExtensions({a:1}))`, false) test(`Object.isFrozen({})`, false) test(` var abc = {}; Object.defineProperty(abc, "def", { value: "def", writable: true, configurable: false }); Object.preventExtensions(abc); !Object.isFrozen(abc); `, true) test(`Object.isFrozen.length`, 1) test(`Object.isFrozen.prototype`, "undefined") }) } func TestObject_freeze(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: Object.freeze()`, "TypeError: Object.Freeze is nil") test(` var abc = {a:1,b:2,c:3}; var frozen = Object.isFrozen(abc); Object.freeze(abc); abc.b = 5; [frozen, Object.isFrozen(abc), abc.b]; `, "false,true,2") test(` var abc = {a:1,b:2,c:3}; var frozen = Object.isFrozen(abc); var caught = false; Object.freeze(abc); abc.b = 5; try { Object.defineProperty(abc, "a", {value:4}); } catch (e) { caught = e instanceof TypeError; } [frozen, Object.isFrozen(abc), caught, abc.a, abc.b]; `, "false,true,true,1,2") test(`Object.freeze.length`, 1) test(`Object.freeze.prototype`, "undefined") }) } func TestObject_defineProperty(t *testing.T) { tt(t, func() { test, _ := test() test(` (function(abc, def, ghi){ Object.defineProperty(arguments, "0", { enumerable: false }); return true; })(0, 1, 2); `, true) test(` var abc = {}; abc.def = 3.14; // Default: writable: true, enumerable: true, configurable: true Object.defineProperty(abc, "def", { value: 42 }); var ghi = Object.getOwnPropertyDescriptor(abc, "def"); [ ghi.value, ghi.writable, ghi.enumerable, ghi.configurable ]; `, "42,true,true,true") // Test that we handle the case of DefineOwnProperty // where [[Writable]] is something but [[Value]] is not test(` var abc = []; Object.defineProperty(abc, "0", { writable: false }); 0 in abc; `, true) // Test that we handle the case of DefineOwnProperty // where [[Writable]] is something but [[Value]] is not // (and the property originally had something for [[Value]] test(` abc = { def: 42 }; Object.defineProperty(abc, "def", { writable: false }); abc.def; `, 42) }) } func TestObject_assign(t *testing.T) { tt(t, func() { test, _ := test() // Test 1: Assigning two empty objects should return an empty object. test(`JSON.stringify(Object.assign({}, {}))`, "{}") // Test 2: Single source assignment. test(`JSON.stringify(Object.assign({}, {a: 1}))`, "{\"a\":1}") // Test 3: Multiple sources with later properties overriding earlier ones. test(`JSON.stringify(Object.assign({a: 1, c: 5}, {a: 2}, {b: 3}))`, "{\"a\":2,\"b\":3,\"c\":5}") // Test 4: Merging objects with overlapping keys. test(`JSON.stringify(Object.assign({a: 1, b: 2}, {b: 3, c: 4}))`, "{\"a\":1,\"b\":3,\"c\":4}") // Test 5: Sources that are null or undefined should be ignored. test(`JSON.stringify(Object.assign({a: 1}, null, undefined, {b: 2}))`, "{\"a\":1,\"b\":2}") // Test 6: When a string is used as a source, its characters are assigned as indexed properties. test(`JSON.stringify(Object.assign({}, "abc"))`, "{\"0\":\"a\",\"1\":\"b\",\"2\":\"c\"}") // // Test 7: The return value should be the target object. test(`(function(){ var o = {x:1}; var r = Object.assign(o, {y:2}); return (o === r).toString(); })()`, "true") // Test 8: Non-enumerable properties should not be copied. test(`(function(){ var target = {}; var source = {}; Object.defineProperty(source, "hidden", { value: 42, enumerable: false }); Object.assign(target, source); return target.hasOwnProperty("hidden").toString(); })()`, "false") // Test 9: Using a number as a source should not add any properties. test(`JSON.stringify(Object.assign({}, 123))`, "{}") // Test 10: Using a boolean as a source should not add any properties. test(`JSON.stringify(Object.assign({}, true))`, "{}") // Test 11: Arrays are objects, so their indexed elements are copied. test(`JSON.stringify(Object.assign({}, [1,2,3]))`, "{\"0\":1,\"1\":2,\"2\":3}") }) } func TestObject_keys(t *testing.T) { tt(t, func() { test, _ := test() test(`Object.keys({ abc:undefined, def:undefined })`, "abc,def") test(` function abc() { this.abc = undefined; this.def = undefined; } Object.keys(new abc()) `, "abc,def") test(` function def() { this.ghi = undefined; } def.prototype = new abc(); Object.keys(new def()); `, "ghi") test(` var ghi = Object.create( { abc: undefined, def: undefined }, { ghi: { value: undefined, enumerable: true }, jkl: { value: undefined, enumerable: false } } ); Object.keys(ghi); `, "ghi") test(` (function(abc, def, ghi){ return Object.keys(arguments) })(undefined, undefined); `, "0,1") test(` (function(abc, def, ghi){ return Object.keys(arguments) })(undefined, undefined, undefined, undefined); `, "0,1,2,3") }) } func TestObject_values(t *testing.T) { tt(t, func() { test, _ := test() test(`Object.values({ abc:"first_example", def:"second_example" })`, "first_example,second_example") test(` function abc() { this.abc = "first_example"; this.def = "second_example"; } Object.values(new abc()) `, "first_example,second_example") test(` function def() { this.ghi = "third_example" } def.prototype = new abc(); Object.values(new def()); `, "third_example") test(` var arr = [1, 2, 3]; Object.values(arr); `, "1,2,3") test(` var arr = [{"abc": "first_example"}, {"def": "second_example"}]; Object.values(arr); `, "[object Object],[object Object]") }) } func TestObject_getOwnPropertyNames(t *testing.T) { tt(t, func() { test, _ := test() test(`Object.getOwnPropertyNames({ abc:undefined, def:undefined })`, "abc,def") test(` var ghi = Object.create( { abc: undefined, def: undefined }, { ghi: { value: undefined, enumerable: true }, jkl: { value: undefined, enumerable: false } } ); Object.getOwnPropertyNames(ghi) `, "ghi,jkl") }) } func TestObjectGetterSetter(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: Object.create({}, { abc: { get: function(){ return "true"; }, writable: true } }).abc; `, "TypeError: toPropertyDescriptor descriptor writeSet") test(`raise: Object.create({}, { abc: { get: function(){ return "true"; }, writable: false } }).abc; `, "TypeError: toPropertyDescriptor descriptor writeSet") test(` Object.create({}, { abc: { get: function(){ return "true"; } } }).abc; `, "true") test(` Object.create({xyz:true},{abc:{get:function(){return this.xyx}}}).abc; Object.create({ xyz: true }, { abc: { get: function(){ return this.xyz; } } }).abc; `, true) test(` var abc = false; var def = Object.create({}, { xyz: { set: function(value) { abc = value; } } }); def.xyz = true; [ abc ]; `, "true") test(` var abc = {}; Object.defineProperty(abc, "def", { value: "xyzzy", configurable: true }); Object.preventExtensions(abc); Object.defineProperty(abc, "def", { get: function() { return 5; } }); var def = Object.getOwnPropertyDescriptor(abc, "def"); [ abc.def, typeof def.get, typeof def.set, typeof def.value, def.configurable, def.enumerable, typeof def.writable ]; `, "5,function,undefined,undefined,true,false,undefined") test(` var abc = {}; Object.defineProperty(abc, "def", { get: function() { return 5; } configurable: true }); Object.preventExtensions(abc); Object.defineProperty(abc, "def", { value: "xyzzy", }); var def = Object.getOwnPropertyDescriptor(abc, "def"); [ abc.def, typeof def.get, typeof def.set, def.value, def.configurable, def.enumerable, def.writable ]; `, "xyzzy,undefined,undefined,xyzzy,true,false,false") test(` var abc = {}; function _get0() { return 10; } function _set(value) { abc.def = value; } Object.defineProperty(abc, "ghi", { get: _get0, set: _set, configurable: true }); function _get1() { return 20; } Object.defineProperty(abc, "ghi", { get: _get0 }); var descriptor = Object.getOwnPropertyDescriptor(abc, "ghi"); [ typeof descriptor.set ]; `, "function") test(`raise: var abc = []; Object.defineProperty(abc, "length", { get: function () { return 2; } }); `, `TypeError: Array.DefineOwnProperty ["function () {\n return 2;\n }" ] is not a value`) test(` var abc = {}; var getter = function() { return 1; } Object.defineProperty(abc, "def", { get: getter, configurable: false }); var jkl = undefined; try { Object.defineProperty(abc, "def", { get: undefined }); } catch (err) { jkl = err; } var ghi = Object.getOwnPropertyDescriptor(abc, "def"); [ jkl instanceof TypeError, ghi.get === getter, ghi.configurable, ghi.enumerable ]; `, "true,true,false,false") test(` var abc = {}; var getter = function() { return 1; }; Object.defineProperty(abc, "def", { get: getter }); Object.defineProperty(abc, "def", { set: undefined }); var ghi = Object.getOwnPropertyDescriptor(abc, "def"); [ ghi.get === getter, ghi.set === undefined, ghi.configurable, ghi.enumerable ]; `, "true,true,false,false") test(` var abc = {}; var getter = function() { return 1; }; Object.defineProperty(abc, "def", { get: getter }); var jkl = undefined; try { Object.defineProperty(abc, "def", { set: function() {} }); } catch (err) { jkl = err; } var ghi = Object.getOwnPropertyDescriptor(abc, "def"); [ jkl instanceof TypeError, ghi.get === getter, ghi.set, ghi.configurable, ghi.enumerable ]; `, "true,true,,false,false") test(` var abc = {}; var def = "xyzzy"; Object.defineProperty(abc, "ghi", { get: undefined, set: function(value) { def = value; }, enumerable: true, configurable: true }); var hasOwn = abc.hasOwnProperty("ghi"); var descriptor = Object.getOwnPropertyDescriptor(abc, "ghi"); [ hasOwn, typeof descriptor.get ]; `, "true,undefined") test(` var abc = "xyzzy"; Object.defineProperty(Array.prototype, "abc", { get: function () { return abc; }, set: function (value) { abc = value; }, enumerable: true, configurable: true }); var def = []; def.abc = 3.14159; [ def.hasOwnProperty("abc"), def.abc, abc ]; `, "false,3.14159,3.14159") }) } func TestProperty(t *testing.T) { tt(t, func() { prop := property{} prop.writeOn() is(prop.writeSet(), true) prop.writeClear() is(prop.writeSet(), false) prop.writeOff() is(prop.writeSet(), true) prop.writeClear() is(prop.writeSet(), false) }) } ================================================ FILE: otto/main.go ================================================ package main import ( "errors" "flag" "fmt" "io" "os" "github.com/robertkrimen/otto" "github.com/robertkrimen/otto/underscore" ) var flagUnderscore *bool = flag.Bool("underscore", true, "Load underscore into the runtime environment") func readSource(filename string) ([]byte, error) { if filename == "" || filename == "-" { return io.ReadAll(os.Stdin) } return os.ReadFile(filename) //nolint:gosec } func main() { flag.Parse() if !*flagUnderscore { underscore.Disable() } err := func() error { src, err := readSource(flag.Arg(0)) if err != nil { return err } vm := otto.New() _, err = vm.Run(src) return err }() if err != nil { var oerr *otto.Error if errors.As(err, &oerr) { fmt.Fprint(os.Stderr, oerr.String()) } else { fmt.Fprintln(os.Stderr, err) } os.Exit(64) } } ================================================ FILE: otto.go ================================================ /* Package otto is a JavaScript parser and interpreter written natively in Go. http://godoc.org/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 `) Get a value out of the VM 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 `) Set a string vm.Set("xyzzy", "Nothing happens.") vm.Run(` console.log(xyzzy.length); // 16 `) Get the value of an expression value, _ = vm.Run("xyzzy.length") // iv is an int64 with a value of 16 iv, _ := 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 ... } 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{} }) 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 }) Use the functions in JavaScript result, _ = vm.Run(` sayHello("Xyzzy"); // Hello, Xyzzy. sayHello(); // Hello, undefined result = twoPlus(2.0); // 4 `) # Parser A separate parser is available in the parser package if you're just interested in building an AST. 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; } 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) otto You can run (Go) JavaScript from the commandline with: http://github.com/robertkrimen/otto/tree/master/otto $ go get -v github.com/robertkrimen/otto/otto Run JavaScript by entering some source on stdin or by giving otto a filename: $ otto example.js underscore Optionally include the JavaScript utility-belt library, underscore, with this import: import ( "github.com/robertkrimen/otto" _ "github.com/robertkrimen/otto/underscore" ) // Now every otto runtime will come loaded with underscore For more information: http://github.com/robertkrimen/otto/tree/master/underscore # 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. # 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 A brief discussion of these limitations: "Regexp (?!re)" https://groups.google.com/forum/?fromgroups=#%21topic/golang-nuts/7qgSDWPIh_E 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 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) } Where is setTimeout/setInterval? These timing functions are not actually part of the ECMA-262 specification. Typically, they belong to the `windows` object (in the browser). It would not be difficult to provide something like these via Go, but you probably want to wrap otto in an event loop in that case. For an example of how this could be done in Go with otto, see natto: http://github.com/robertkrimen/natto Here is some more discussion of the issue: * http://book.mixu.net/node/ch2.html * http://en.wikipedia.org/wiki/Reentrancy_%28computing%29 * http://aaroncrane.co.uk/2009/02/perl_safe_signals/ */ package otto import ( "encoding/json" "errors" "strings" "github.com/robertkrimen/otto/file" "github.com/robertkrimen/otto/registry" ) // Otto is the representation of the JavaScript runtime. // Each instance of Otto has a self-contained namespace. type Otto struct { // Interrupt is a channel for interrupting the runtime. You can use this to halt a long running execution, for example. // See "Halting Problem" for more information. Interrupt chan func() runtime *runtime } // New will allocate a new JavaScript runtime. func New() *Otto { o := &Otto{ runtime: newContext(), } o.runtime.otto = o o.runtime.traceLimit = 10 if err := o.Set("console", o.runtime.newConsole()); err != nil { panic(err) } registry.Apply(func(entry registry.Entry) { if _, err := o.Run(entry.Source()); err != nil { panic(err) } }) return o } func (o *Otto) clone() *Otto { n := &Otto{ runtime: o.runtime.clone(), } n.runtime.otto = n return n } // Run will allocate a new JavaScript runtime, run the given source // on the allocated runtime, and return the runtime, resulting value, and // error (if any). // // src may be a string, a byte slice, a bytes.Buffer, or an io.Reader, but it MUST always be in UTF-8. // // 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 return otto, value, err } // Run will run the given source (parsing it first if necessary), returning the resulting value and error (if any) // // src may be a string, a byte slice, a bytes.Buffer, or an io.Reader, but it MUST always be in UTF-8. // // If the runtime is unable to parse source, then this function will return undefined and the parse error (nothing // will be evaluated in this case). // // 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 (o Otto) Run(src interface{}) (Value, error) { value, err := o.runtime.cmplRun(src, nil) if !value.safe() { value = Value{} } return value, err } // Eval will do the same thing as Run, except without leaving the current scope. // // By staying in the same scope, the code evaluated has access to everything // already defined in the current stack frame. This is most useful in, for // example, a debugger call. func (o Otto) Eval(src interface{}) (Value, error) { if o.runtime.scope == nil { o.runtime.enterGlobalScope() defer o.runtime.leaveScope() } value, err := o.runtime.cmplEval(src, nil) if !value.safe() { value = Value{} } return value, err } // Get the value of the top-level binding of the given name. // // If there is an error (like the binding does not exist), then the value // will be undefined. func (o Otto) Get(name string) (Value, error) { value := Value{} err := catchPanic(func() { value = o.getValue(name) }) if !value.safe() { value = Value{} } return value, err } func (o Otto) getValue(name string) Value { return o.runtime.globalStash.getBinding(name, false) } // Set the top-level binding of the given name to the given value. // // Set will automatically apply ToValue to the given value in order // to convert it to a JavaScript value (type Value). // // If there is an error (like the binding is read-only, or the ToValue conversion // fails), then an error is returned. // // If the top-level binding does not exist, it will be created. func (o Otto) Set(name string, value interface{}) error { val, err := o.ToValue(value) if err != nil { return err } return catchPanic(func() { o.setValue(name, val) }) } func (o Otto) setValue(name string, value Value) { o.runtime.globalStash.setValue(name, value, false) } // SetDebuggerHandler sets the debugger handler to fn. func (o Otto) SetDebuggerHandler(fn func(vm *Otto)) { o.runtime.debugger = fn } // SetRandomSource sets the random source to fn. func (o Otto) SetRandomSource(fn func() float64) { o.runtime.random = fn } // SetStackDepthLimit sets an upper limit to the depth of the JavaScript // stack. In simpler terms, this limits the number of "nested" function calls // you can make in a particular interpreter instance. // // Note that this doesn't take into account the Go stack depth. If your // JavaScript makes a call to a Go function, otto won't keep track of what // happens outside the interpreter. So if your Go function is infinitely // recursive, you're still in trouble. func (o Otto) SetStackDepthLimit(limit int) { o.runtime.stackLimit = limit } // SetStackTraceLimit sets an upper limit to the number of stack frames that // otto will use when formatting an error's stack trace. By default, the limit // is 10. This is consistent with V8 and SpiderMonkey. // // TODO: expose via `Error.stackTraceLimit`. func (o Otto) SetStackTraceLimit(limit int) { o.runtime.traceLimit = limit } // MakeCustomError creates a new Error object with the given name and message, // returning it as a Value. func (o Otto) MakeCustomError(name, message string) Value { return o.runtime.toValue(o.runtime.newError(name, o.runtime.toValue(message), 0)) } // MakeRangeError creates a new RangeError object with the given message, // returning it as a Value. func (o Otto) MakeRangeError(message string) Value { return o.runtime.toValue(o.runtime.newRangeError(o.runtime.toValue(message))) } // MakeSyntaxError creates a new SyntaxError object with the given message, // returning it as a Value. func (o Otto) MakeSyntaxError(message string) Value { return o.runtime.toValue(o.runtime.newSyntaxError(o.runtime.toValue(message))) } // MakeTypeError creates a new TypeError object with the given message, // returning it as a Value. func (o Otto) MakeTypeError(message string) Value { return o.runtime.toValue(o.runtime.newTypeError(o.runtime.toValue(message))) } // Context is a structure that contains information about the current execution // context. type Context struct { This Value Symbols map[string]Value Filename string Callee string Stacktrace []string Line int Column int } // Context returns the current execution context of the vm, traversing up to // ten stack frames, and skipping any innermost native function stack frames. func (o Otto) Context() Context { return o.ContextSkip(10, true) } // ContextLimit returns the current execution context of the vm, with a // specific limit on the number of stack frames to traverse, skipping any // innermost native function stack frames. func (o Otto) ContextLimit(limit int) Context { return o.ContextSkip(limit, true) } // ContextSkip returns the current execution context of the vm, with a // specific limit on the number of stack frames to traverse, optionally // skipping any innermost native function stack frames. func (o Otto) ContextSkip(limit int, skipNative bool) Context { // Ensure we are operating in a scope if o.runtime.scope == nil { o.runtime.enterGlobalScope() defer o.runtime.leaveScope() } curScope := o.runtime.scope frm := curScope.frame for skipNative && frm.native && curScope.outer != nil { curScope = curScope.outer frm = curScope.frame } // Get location information var ctx Context ctx.Filename = "" ctx.Callee = frm.callee switch { case frm.native: ctx.Filename = frm.nativeFile ctx.Line = frm.nativeLine ctx.Column = 0 case frm.file != nil: ctx.Filename = "" if p := frm.file.Position(file.Idx(frm.offset)); p != nil { ctx.Line = p.Line ctx.Column = p.Column if p.Filename != "" { ctx.Filename = p.Filename } } } // Get the current scope this Value ctx.This = objectValue(curScope.this) // Build stacktrace (up to 10 levels deep) ctx.Symbols = make(map[string]Value) ctx.Stacktrace = append(ctx.Stacktrace, frm.location()) for limit != 0 { // Get variables stash := curScope.lexical for { for _, name := range getStashProperties(stash) { if _, ok := ctx.Symbols[name]; !ok { ctx.Symbols[name] = stash.getBinding(name, true) } } stash = stash.outer() if stash == nil || stash.outer() == nil { break } } curScope = curScope.outer if curScope == nil { break } if curScope.frame.offset >= 0 { ctx.Stacktrace = append(ctx.Stacktrace, curScope.frame.location()) } limit-- } return ctx } // Call the given JavaScript with a given this and arguments. // // If this is nil, then some special handling takes place to determine the proper // this value, falling back to a "standard" invocation if necessary (where this is // undefined). // // If source begins with "new " (A lowercase new followed by a space), then // 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.") // // // 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 (o Otto) Call(source string, this interface{}, argumentList ...interface{}) (Value, error) { thisValue := Value{} construct := false if strings.HasPrefix(source, "new ") { source = source[4:] construct = true } // FIXME enterGlobalScope o.runtime.enterGlobalScope() defer func() { o.runtime.leaveScope() }() if !construct && this == nil { program, err := o.runtime.cmplParse("", source+"()", nil) if err == nil { if node, ok := program.body[0].(*nodeExpressionStatement); ok { if node, ok2 := node.expression.(*nodeCallExpression); ok2 { var value Value if err = catchPanic(func() { value = o.runtime.cmplEvaluateNodeCallExpression(node, argumentList) }); err != nil { return Value{}, err } return value, nil } } } } else { value, err := o.ToValue(this) if err != nil { return Value{}, err } thisValue = value } val := thisValue fn, err := o.Run(source) if err != nil { return Value{}, err } if construct { result, err2 := fn.constructSafe(o.runtime, val, argumentList...) if err2 != nil { return Value{}, err2 } return result, nil } result, err := fn.Call(val, argumentList...) if err != nil { return Value{}, err } return result, nil } // Object will run the given source and return the result as an object. // // For example, accessing an existing object: // // object, _ := vm.Object(`Number`) // // Or, creating a new object: // // object, _ := vm.Object(`({ xyzzy: "Nothing happens." })`) // // Or, creating and assigning an object: // // 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. func (o Otto) Object(source string) (*Object, error) { value, err := o.runtime.cmplRun(source, nil) if err != nil { return nil, err } if value.IsObject() { return value.Object(), nil } return nil, errors.New("value is not an object") } // ToValue will convert an interface{} value to a value digestible by otto/JavaScript. func (o Otto) ToValue(value interface{}) (Value, error) { return o.runtime.safeToValue(value) } // Copy will create a copy/clone of the runtime. // // Copy is useful for saving some time when creating many similar runtimes. // // This method works by walking the original runtime and cloning each object, scope, stash, // etc. into a new runtime. // // Be on the lookout for memory leaks or inadvertent sharing of resources. func (o *Otto) Copy() *Otto { out := &Otto{ runtime: o.runtime.clone(), } out.runtime.otto = out return out } // Object is the representation of a JavaScript object. type Object struct { object *object value Value } // Call a method on the object. // // It is essentially equivalent to: // // 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 func (o Object) Call(name string, argumentList ...interface{}) (Value, error) { // TODO: Insert an example using JavaScript below... // e.g., Object("JSON").Call("stringify", ...) function, err := o.Get(name) if err != nil { return Value{}, err } return function.Call(o.Value(), argumentList...) } // Value returns the value of o. func (o Object) Value() Value { return o.value } // Get the value of the property with the given name. func (o Object) Get(name string) (Value, error) { value := Value{} err := catchPanic(func() { value = o.object.get(name) }) if !value.safe() { value = Value{} } return value, err } // Set the property of the given name to the given value. // // An error will result if the setting the property triggers an exception (i.e. read-only), // or there is an error during conversion of the given value. func (o Object) Set(name string, value interface{}) error { val, err := o.object.runtime.safeToValue(value) if err != nil { return err } return catchPanic(func() { o.object.put(name, val, true) }) } // Keys gets the keys for the given object. // // Equivalent to calling Object.keys on the object. func (o Object) Keys() []string { var keys []string o.object.enumerate(false, func(name string) bool { keys = append(keys, name) return true }) return keys } // KeysByParent gets the keys (and those of the parents) for the given object, // in order of "closest" to "furthest". func (o Object) KeysByParent() [][]string { var a [][]string for o := o.object; o != nil; o = o.prototype { var l []string o.enumerate(false, func(name string) bool { l = append(l, name) return true }) a = append(a, l) } return a } // Class will return the class string of the object. // // The return value will (generally) be one of: // // Object // Function // Array // String // Number // Boolean // Date // RegExp func (o Object) Class() string { return o.object.class } // MarshalJSON implements json.Marshaller. func (o Object) MarshalJSON() ([]byte, error) { var goValue interface{} switch value := o.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 := builtinJSONStringify(FunctionCall{ runtime: o.object.runtime, ArgumentList: []Value{o.value}, }) result = []byte(resultVal.String()) }) return result, err } return json.Marshal(goValue) } ================================================ FILE: otto_.go ================================================ package otto import ( "fmt" "regexp" goruntime "runtime" "strconv" ) var isIdentifierRegexp *regexp.Regexp = regexp.MustCompile(`^[a-zA-Z\$][a-zA-Z0-9\$]*$`) func isIdentifier(value string) bool { return isIdentifierRegexp.MatchString(value) } func (rt *runtime) toValueArray(arguments ...interface{}) []Value { length := len(arguments) if length == 1 { if valueArray, ok := arguments[0].([]Value); ok { return valueArray } return []Value{rt.toValue(arguments[0])} } valueArray := make([]Value, length) for index, value := range arguments { valueArray[index] = rt.toValue(value) } return valueArray } func stringToArrayIndex(name string) int64 { index, err := strconv.ParseInt(name, 10, 64) if err != nil { return -1 } if index < 0 { return -1 } if index >= maxUint32 { // The value 2^32 (or above) is not a valid index because // you cannot store a uint32 length for an index of uint32 return -1 } return index } func isUint32(value int64) bool { return value >= 0 && value <= maxUint32 } func arrayIndexToString(index int64) string { return strconv.FormatInt(index, 10) } func valueOfArrayIndex(array []Value, index int) Value { value, _ := getValueOfArrayIndex(array, index) return value } func getValueOfArrayIndex(array []Value, index int) (Value, bool) { if index >= 0 && index < len(array) { value := array[index] if !value.isEmpty() { return value, true } } return Value{}, false } // A range index can be anything from 0 up to length. It is NOT safe to use as an index // to an array, but is useful for slicing and in some ECMA algorithms. func valueToRangeIndex(indexValue Value, length int64, negativeIsZero bool) int64 { index := indexValue.number().int64 if negativeIsZero { if index < 0 { index = 0 } // minimum(index, length) if index >= length { index = length } return index } if index < 0 { index += length if index < 0 { index = 0 } } else if index > length { index = length } return index } func rangeStartEnd(array []Value, size int64, negativeIsZero bool) (start, end int64) { //nolint:nonamedreturns start = valueToRangeIndex(valueOfArrayIndex(array, 0), size, negativeIsZero) if len(array) == 1 { // If there is only the start argument, then end = size end = size return } // Assuming the argument is undefined... end = size endValue := valueOfArrayIndex(array, 1) if !endValue.IsUndefined() { // Which it is not, so get the value as an array index end = valueToRangeIndex(endValue, size, negativeIsZero) } return } func rangeStartLength(source []Value, size int64) (start, length int64) { //nolint:nonamedreturns start = valueToRangeIndex(valueOfArrayIndex(source, 0), size, false) // Assume the second argument is missing or undefined length = size if len(source) == 1 { // If there is only the start argument, then length = size return start, length } lengthValue := valueOfArrayIndex(source, 1) if !lengthValue.IsUndefined() { // Which it is not, so get the value as an array index length = lengthValue.number().int64 } return start, length } func hereBeDragons(arguments ...interface{}) string { pc, _, _, _ := goruntime.Caller(1) //nolint:dogsled name := goruntime.FuncForPC(pc).Name() message := "Here be dragons -- " + name if len(arguments) > 0 { message += ": " argument0 := fmt.Sprintf("%s", arguments[0]) if len(arguments) == 1 { message += argument0 } else { message += fmt.Sprintf(argument0, arguments[1:]...) } } else { message += "." } return message } ================================================ FILE: otto_error_test.go ================================================ package otto import ( "testing" ) func TestOttoError(t *testing.T) { tt(t, func() { vm := New() _, err := vm.Run(`throw "Xyzzy"`) is(err, "Xyzzy") _, err = vm.Run(`throw new TypeError()`) is(err, "TypeError") _, err = vm.Run(`throw new TypeError("Nothing happens.")`) is(err, "TypeError: Nothing happens.") _, err = ToValue([]byte{}) is(err, "TypeError: invalid value (slice): missing runtime: [] ([]uint8)") _, err = vm.Run(` (function(){ return abcdef.length })() `) is(err, "ReferenceError: 'abcdef' is not defined") _, err = vm.Run(` function start() { } start() xyzzy() `) is(err, "ReferenceError: 'xyzzy' is not defined") _, err = vm.Run(` // Just a comment xyzzy `) is(err, "ReferenceError: 'xyzzy' is not defined") }) } ================================================ FILE: otto_test.go ================================================ package otto import ( "bytes" "errors" "io" "testing" "time" "github.com/robertkrimen/otto/parser" "github.com/stretchr/testify/require" ) func TestOtto(t *testing.T) { tt(t, func() { test, _ := test() test("xyzzy = 2", 2) test("xyzzy + 2", 4) test("xyzzy += 16", 18) test("xyzzy", 18) test(` (function(){ return 1 })() `, 1) test(` (function(){ return 1 }).call(this) `, 1) test(` (function(){ var result (function(){ result = -1 })() return result })() `, -1) test(` var abc = 1 abc || (abc = -1) abc `, 1) test(` var abc = (function(){ 1 === 1 })(); abc; `, "undefined") }) } func TestFunction__(t *testing.T) { tt(t, func() { test, _ := test() test(` function abc() { return 1; }; abc(); `, 1) }) } func TestIf(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = undefined; def = undefined; if (true) abc = 1 else abc = 2; if (false) { def = 3; } else def = 4; [ abc, def ]; `, "1,4") test(` if (1) { abc = 1; } else { abc = 0; } abc; `, 1) test(` if (0) { abc = 1; } else { abc = 0; } abc; `, 0) test(` abc = 0; if (0) { abc = 1; } abc; `, 0) test(` abc = 0; if (abc) { abc = 1; } abc; `, 0) }) } func TestSequence(t *testing.T) { tt(t, func() { test, _ := test() test(` 1, 2, 3; `, 3) }) } func TestCall(t *testing.T) { tt(t, func() { test, _ := test() test(` Math.pow(3, 2); `, 9) }) } func TestRunFunctionWithSetArguments(t *testing.T) { tt(t, func() { vm := New() _, err := vm.Run(`var sillyFunction = function(record){record.silly = true; record.answer *= -1};`) require.NoError(t, err) record := map[string]interface{}{"foo": "bar", "answer": 42} // Set performs a conversion that allows the map to be addressed as a Javascript object err = vm.Set("argument", record) require.NoError(t, err) _, err = vm.Run("sillyFunction(argument)") require.NoError(t, err) is(record["answer"].(float64), -42) is(record["silly"].(bool), true) }) } func TestRunFunctionWithArgumentsPassedToCall(t *testing.T) { tt(t, func() { vm := New() _, err := vm.Run(`var sillyFunction = function(record){record.silly = true; record.answer *= -1};`) require.NoError(t, err) record := map[string]interface{}{"foo": "bar", "answer": 42} _, err = vm.Call("sillyFunction", nil, record) require.NoError(t, err) is(record["answer"].(float64), -42) is(record["silly"].(bool), true) }) } func TestMember(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = [ 0, 1, 2 ]; def = { "abc": 0, "def": 1, "ghi": 2, }; [ abc[2], def.abc, abc[1], def.def ]; `, "2,0,1,1") }) } func Test_this(t *testing.T) { tt(t, func() { test, _ := test() test(` typeof this; `, "object") }) } func TestWhile(t *testing.T) { tt(t, func() { test, _ := test() test(` limit = 4 abc = 0 while (limit) { abc = abc + 1 limit = limit - 1 } abc; `, 4) }) } func TestSwitch_break(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = true; var ghi = "Xyzzy"; while (abc) { switch ('def') { case 'def': break; } ghi = "Nothing happens."; abc = false; } ghi; `, "Nothing happens.") test(` var abc = true; var ghi = "Xyzzy"; WHILE: while (abc) { switch ('def') { case 'def': break WHILE; } ghi = "Nothing happens." abc = false } ghi; `, "Xyzzy") test(` var ghi = "Xyzzy"; FOR: for (;;) { switch ('def') { case 'def': break FOR; ghi = ""; } ghi = "Nothing happens."; } ghi; `, "Xyzzy") test(` var ghi = "Xyzzy"; FOR: for (var jkl in {}) { switch ('def') { case 'def': break FOR; ghi = "Something happens."; } ghi = "Nothing happens."; } ghi; `, "Xyzzy") test(` var ghi = "Xyzzy"; function jkl() { switch ('def') { case 'def': break; ghi = ""; } ghi = "Nothing happens."; } while (abc) { jkl(); abc = false; ghi = "Something happens."; } ghi; `, "Something happens.") }) } func TestTryFinally(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc; try { abc = 1; } finally { abc = 2; } abc; `, 2) test(` var abc = false, def = 0; do { def += 1; if (def > 100) { break; } try { continue; } finally { abc = true; } } while(!abc && def < 10) def; `, 1) test(` var abc = false, def = 0, ghi = 0; do { def += 1; if (def > 100) { break; } try { throw 0; } catch (jkl) { continue; } finally { abc = true; ghi = 11; } ghi -= 1; } while(!abc && def < 10) ghi; `, 11) test(` var abc = 0, def = 0; do { try { abc += 1; throw "ghi"; } finally { def = 1; continue; } def -= 1; } while (abc < 2) [ abc, def ]; `, "2,1") }) } func TestTryCatch(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = 1; try { throw 4; abc = -1; } catch (xyzzy) { abc += xyzzy + 1; } abc; `, 6) test(` abc = 1; var def; try { try { throw 4; abc = -1; } catch (xyzzy) { abc += xyzzy + 1; throw 64; } } catch (xyzzy) { def = xyzzy; abc = -2; } [ def, abc ]; `, "64,-2") }) } func TestWith(t *testing.T) { tt(t, func() { test, _ := test() test(` var def; with({ abc: 9 }) { def = abc; } def; `, 9) test(` var def; with({ abc: function(){ return 11; } }) { def = abc(); } def; `, 11) }) } func TestSwitch(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = 0; switch (0) { default: abc += 1; case 1: abc += 2; case 2: abc += 4; case 3: abc += 8; } abc; `, 15) test(` abc = 0; switch (3) { default: abc += 1; case 1: abc += 2; case 2: abc += 4; case 3: abc += 8; } abc; `, 8) test(` abc = 0; switch (60) { case 1: abc += 2; case 2: abc += 4; case 3: abc += 8; } abc; `, 0) }) } func TestForIn(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc; for (property in { a: 1 }) { abc = property; } abc; `, "a") test(` var ghi; for (property in new String("xyzzy")) { ghi = property; } ghi; `, "4") }) } func TestFor(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = 7; for (i = 0; i < 3; i += 1) { abc += 1; } abc; `, 10) test(` abc = 7; for (i = 0; i < 3; i += 1) { abc += 1; if (i == 1) { break; } } abc; `, 9) test(` abc = 7; for (i = 0; i < 3; i += 1) { if (i == 2) { continue; } abc += 1; } abc; `, 9) test(` abc = 0; for (;;) { abc += 1; if (abc == 3) break; } abc; `, 3) test(` for (abc = 0; ;) { abc += 1; if (abc == 3) break; } abc; `, 3) test(` for (abc = 0; ; abc += 1) { abc += 1; if (abc == 3) break; } abc; `, 3) }) } func TestLabelled(t *testing.T) { tt(t, func() { test, _ := test() // TODO Add emergency break test(` xyzzy: for (var abc = 0; abc <= 0; abc++) { for (var def = 0; def <= 1; def++) { if (def === 0) { continue xyzzy; } else { } } } `) test(` abc = 0 def: while (true) { while (true) { abc = abc + 1 if (abc > 11) { break def; } } } abc; `, 12) test(` abc = 0 def: do { do { abc = abc + 1 if (abc > 11) { break def; } } while (true) } while (true) abc; `, 12) }) } func TestConditional(t *testing.T) { tt(t, func() { test, _ := test() test(` [ true ? false : true, true ? 1 : 0, false ? 3.14159 : "abc" ]; `, "false,1,abc") }) } func TestArrayLiteral(t *testing.T) { tt(t, func() { test, _ := test() test(` [ 1, , 3.14159 ]; `, "1,,3.14159") }) } func TestAssignment(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = 1; abc; `, 1) test(` abc += 2; abc; `, 3) }) } func TestBinaryOperation(t *testing.T) { tt(t, func() { test, _ := test() test(`0 == 1`, false) test(`1 == "1"`, true) test(`0 === 1`, false) test(`1 === "1"`, false) test(`"1" === "1"`, true) }) } func Test_typeof(t *testing.T) { tt(t, func() { test, _ := test() test(`typeof abc`, "undefined") test(`typeof abc === 'undefined'`, true) test(`typeof {}`, "object") test(`typeof null`, "object") }) } func Test_PrimitiveValueObjectValue(t *testing.T) { tt(t, func() { test, _ := test() Number11 := test(`new Number(11)`) is(Number11.float64(), 11) }) } func Test_eval(t *testing.T) { tt(t, func() { test, _ := test() // FIXME terst, Is this correct? test(` var abc = 1; `, "undefined") test(` eval("abc += 1"); `, 2) test(` (function(){ var abc = 11; eval("abc += 1"); return abc; })(); `, 12) test(`abc`, 2) test(` (function(){ try { eval("var prop = \\u2029;"); return false; } catch (abc) { return [ abc instanceof SyntaxError, abc.toString() ]; } })(); `, "true,SyntaxError: (anonymous): Line 1:12 Unexpected token ILLEGAL (and 1 more errors)") test(` function abc(){ this.THIS = eval("this"); } var def = new abc(); def === def.THIS; `, true) }) } func Test_evalDirectIndirect(t *testing.T) { tt(t, func() { test, _ := test() // (function () {return this;}()).abc = "global"; test(` var abc = "global"; (function(){ try { var _eval = eval; var abc = "function"; return [ _eval("\'global\' === abc"), // eval (Indirect) eval("\'function\' === abc"), // eval (Direct) ]; } finally { delete this.abc; } })(); `, "true,true") }) } func TestError_URIError(t *testing.T) { tt(t, func() { test, _ := test() test(`new URIError() instanceof URIError`, true) test(` var abc try { decodeURI("http://example.com/ _^#%") } catch (def) { abc = def instanceof URIError } abc `, true) }) } func TestTo(t *testing.T) { tt(t, func() { test, _ := test() { value, _ := test(`"11"`).ToFloat() is(value, float64(11)) } { value, _ := test(`"11"`).ToInteger() is(value, int64(11)) value, _ = test(`1.1`).ToInteger() is(value, int64(1)) } }) } func TestShouldError(t *testing.T) { tt(t, func() { test, _ := test() test(`raise: xyzzy throw new TypeError("Nothing happens.") `, "ReferenceError: 'xyzzy' is not defined") }) } func TestAPI(t *testing.T) { tt(t, func() { test, vm := test() test(` String.prototype.xyzzy = function(){ return this.length + 11 + (arguments[0] || 0) } abc = new String("xyzzy") def = "Nothing happens." abc.xyzzy() `, 16) abc, _ := vm.Get("abc") def, _ := vm.Get("def") obj := abc.Object() result, _ := obj.Call("xyzzy") is(result, 16) result, _ = obj.Call("xyzzy", 1) is(result, 17) value, _ := obj.Get("xyzzy") result, _ = value.Call(def) is(result, 27) result, _ = value.Call(def, 3) is(result, 30) obj = value.Object() // Object xyzzy result, _ = obj.Value().Call(def, 3) is(result, 30) test(` abc = { 'abc': 1, 'def': false, 3.14159: NaN, }; abc['abc']; `, 1) abc, err := vm.Get("abc") require.NoError(t, err) obj = abc.Object() // Object abc value, err = obj.Get("abc") require.NoError(t, err) is(value, 1) is(obj.Keys(), []string{"abc", "def", "3.14159"}) test(` abc = [ 0, 1, 2, 3.14159, "abc", , ]; abc.def = true; `) abc, err = vm.Get("abc") require.NoError(t, err) obj = abc.Object() // Object abc is(obj.Keys(), []string{"0", "1", "2", "3", "4", "def"}) }) } func TestObjectKeys(t *testing.T) { tt(t, func() { vm := New() _, err := vm.Eval(`var x = Object.create(null); x.a = 1`) require.NoError(t, err) _, err = vm.Eval(`var y = Object.create(x); y.b = 2`) require.NoError(t, err) o1, err := vm.Object("x") require.NoError(t, err) is(o1.Keys(), []string{"a"}) is(o1.KeysByParent(), [][]string{{"a"}}) o2, err := vm.Object("y") require.NoError(t, err) is(o2.Keys(), []string{"b"}) is(o2.KeysByParent(), [][]string{{"b"}, {"a"}}) }) } func TestUnicode(t *testing.T) { tt(t, func() { test, _ := test() test(`var abc = eval("\"a\uFFFFa\"");`, "undefined") test(`abc.length`, 3) test(`abc != "aa"`, true) test("abc[1] === \"\uFFFF\"", true) }) } func TestDotMember(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = { ghi: 11, } abc.def = "Xyzzy" abc.null = "Nothing happens." `) test(`abc.def`, "Xyzzy") test(`abc.null`, "Nothing happens.") test(`abc.ghi`, 11) test(` abc = { null: 11, } `) test(`abc.def`, "undefined") test(`abc.null`, 11) test(`abc.ghi`, "undefined") }) } func Test_stringToFloat(t *testing.T) { tt(t, func() { is(parseNumber("10e10000"), infinity) is(parseNumber("10e10_."), naN) }) } func Test_delete(t *testing.T) { tt(t, func() { test, _ := test() test(` delete 42; `, true) test(` var abc = delete $_undefined_$; abc = abc && delete ($_undefined_$); abc; `, true) // delete should not trigger get() test(` var abc = { get def() { throw "Test_delete: delete should not trigger get()" } }; delete abc.def `, true) }) } func TestObject_defineOwnProperty(t *testing.T) { tt(t, func() { test, _ := test() test(` var object = {}; var descriptor = new Boolean(false); descriptor.configurable = true; Object.defineProperties(object, { property: descriptor }); var abc = object.hasOwnProperty("property"); delete object.property; var def = object.hasOwnProperty("property"); [ abc, def ]; `, "true,false") test(` var object = [0, 1, 2]; Object.defineProperty(object, "0", { value: 42, writable: false, enumerable: false, configurable: false }); var abc = Object.getOwnPropertyDescriptor(object, "0"); [ abc.value, abc.writable, abc.enumerable, abc.configurable ]; `, "42,false,false,false") test(` var abc = { "xyzzy": 42 }; var def = Object.defineProperties(abc, ""); abc === def; `, true) }) } func Test_assignmentEvaluationOrder(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = 0; ((abc = 1) & abc); `, 1) test(` var abc = 0; (abc & (abc = 1)); `, 0) }) } func TestOttoCall(t *testing.T) { tt(t, func() { vm := New() _, err := vm.Run(` var abc = { ghi: 1, def: function(def){ var ghi = 0; if (this.ghi) { ghi = this.ghi; } return "def: " + (def + 3.14159 + ghi); } }; function structFunc(s) { return s.Val; } `) require.NoError(t, err) value, err := vm.Call(`abc.def`, nil, 2) require.NoError(t, err) is(value, "def: 6.14159") value, err = vm.Call(`abc.def`, "", 2) require.NoError(t, err) is(value, "def: 5.14159") // Do not attempt to do a ToValue on a this of nil value, err = vm.Call(`jkl.def`, nil, 1, 2, 3) is(err, "!=", nil) is(value, "undefined") value, err = vm.Call(`[ 1, 2, 3, undefined, 4 ].concat`, nil, 5, 6, 7, "abc") require.NoError(t, err) is(value, "1,2,3,,4,5,6,7,abc") s := struct{ Val int }{Val: 10} value, err = vm.Call("structFunc", nil, s) require.NoError(t, err) is(value, 10) }) } func TestOttoCall_new(t *testing.T) { tt(t, func() { test, vm := test() vm.Set("abc", func(call FunctionCall) Value { value, err := call.Otto.Call(`new Object`, nil, "Nothing happens.") require.NoError(t, err) return value }) test(` def = abc(); [ def, def instanceof String ]; `, "Nothing happens.,true") }) } func TestOttoCall_newWithBrackets(t *testing.T) { tt(t, func() { test, vm := test() _, err := vm.Run(`var a = {default: function B(x) { this.x = x; } }`) require.NoError(t, err) test(`(new a['default'](1)).x`, 1) }) } func TestOttoCall_throw(t *testing.T) { // FIXME? (Been broken for a while) // Looks like this has been broken for a while... what // behavior do we want here? if true { return } tt(t, func() { test, vm := test() vm.Set("abc", func(call FunctionCall) Value { if false { _, err := call.Otto.Call(`throw eval`, nil, "({ def: 3.14159 })") require.NoError(t, err) } _, err := call.Otto.Call(`throw Error`, nil, "abcdef") require.NoError(t, err) return Value{} }) // TODO try { abc(); } catch (err) { error = err } // Possible unrelated error case: // If error is not declared beforehand, is later referencing it a ReferenceError? // Should the catch { } declare error in the outer scope? test(` var error; try { abc(); } catch (err) { error = err; } [ error instanceof Error, error.message, error.def ]; `, "true,abcdef,") vm.Set("def", func(call FunctionCall) Value { _, err := call.Otto.Call(`throw new Object`, nil, 3.14159) require.NoError(t, err) return UndefinedValue() }) test(` try { def(); } catch (err) { error = err; } [ error instanceof Error, error.message, error.def, typeof error, error, error instanceof Number ]; `, "false,,,object,3.14159,true") }) } func TestOttoCopy(t *testing.T) { tt(t, func() { vm0 := New() _, err := vm0.Run(` var abc = function() { return "Xyzzy"; }; function def() { return abc() + (0 + {}); } `) require.NoError(t, err) value, err := vm0.Run(` def(); `) require.NoError(t, err) is(value, "Xyzzy0[object Object]") vm1 := vm0.Copy() value, err = vm1.Run(` def(); `) require.NoError(t, err) is(value, "Xyzzy0[object Object]") _, err = vm1.Run(` abc = function() { return 3.14159; }; `) require.NoError(t, err) value, err = vm1.Run(` def(); `) require.NoError(t, err) is(value, "3.141590[object Object]") value, err = vm0.Run(` def(); `) require.NoError(t, err) is(value, "Xyzzy0[object Object]") { vm01 := New() _, err2 := vm01.Run(` var global = (function () {return this;}()) var abc = 0; var vm = "vm0"; var def = (function(){ var jkl = 0; var abc = function() { global.abc += 1; jkl += 1; return 1; }; return function() { return [ vm, global.abc, jkl, abc() ]; }; })(); `) require.NoError(t, err2) value2, err2 := vm01.Run(` def(); `) require.NoError(t, err2) is(value2, "vm0,0,0,1") vm11 := vm01.Copy() err2 = vm11.Set("vm", "vm1") require.NoError(t, err2) value2, err2 = vm11.Run(` def(); `) require.NoError(t, err2) is(value2, "vm1,1,1,1") value2, err2 = vm01.Run(` def(); `) require.NoError(t, err2) is(value2, "vm0,1,1,1") value2, err2 = vm11.Run(` def(); `) require.NoError(t, err2) is(value2, "vm1,2,2,1") } }) } func TestOttoCall_clone(t *testing.T) { tt(t, func() { vm := New().clone() rt := vm.runtime { // FIXME terst, Check how this comparison is done is(rt.global.Array.prototype, rt.global.FunctionPrototype) is(rt.global.ArrayPrototype, "!=", nil) is(rt.global.Array.runtime, rt) is(rt.global.Array.prototype.runtime, rt) is(rt.global.Array.get("prototype").object().runtime, rt) } { value, err := vm.Run(`[ 1, 2, 3 ].toString()`) require.NoError(t, err) is(value, "1,2,3") } { value, err := vm.Run(`[ 1, 2, 3 ]`) require.NoError(t, err) is(value, "1,2,3") obj := value.object() is(obj, "!=", nil) is(obj.prototype, rt.global.ArrayPrototype) value, err = vm.Run(`Array.prototype`) require.NoError(t, err) obj = value.object() is(obj.runtime, rt) is(obj, "!=", nil) is(obj, rt.global.ArrayPrototype) } { otto1 := New() _, err := otto1.Run(` var abc = 1; var def = 2; `) require.NoError(t, err) otto2 := otto1.clone() value, err := otto2.Run(`abc += 1; abc;`) require.NoError(t, err) is(value, 2) value, err = otto1.Run(`abc += 4; abc;`) require.NoError(t, err) is(value, 5) } { vm1 := New() _, err := vm1.Run(` var abc = 1; var def = function(value) { abc += value; return abc; } `) require.NoError(t, err) vm2 := vm1.clone() value, err := vm2.Run(`def(1)`) require.NoError(t, err) is(value, 2) value, err = vm1.Run(`def(4)`) require.NoError(t, err) is(value, 5) } { vm1 := New() _, err := vm1.Run(` var abc = { ghi: 1, jkl: function(value) { this.ghi += value; return this.ghi; } }; var def = { abc: abc }; `) require.NoError(t, err) otto2 := vm1.clone() value, err := otto2.Run(`def.abc.jkl(1)`) require.NoError(t, err) is(value, 2) value, err = vm1.Run(`def.abc.jkl(4)`) require.NoError(t, err) is(value, 5) } { vm1 := New() _, err := vm1.Run(` var abc = function() { return "abc"; }; var def = function() { return "def"; }; `) require.NoError(t, err) vm2 := vm1.clone() value, err := vm2.Run(` [ abc.toString(), def.toString() ]; `) require.NoError(t, err) is(value, `function() { return "abc"; },function() { return "def"; }`) _, err = vm2.Run(` var def = function() { return "ghi"; }; `) require.NoError(t, err) value, err = vm1.Run(` [ abc.toString(), def.toString() ]; `) require.NoError(t, err) is(value, `function() { return "abc"; },function() { return "def"; }`) value, err = vm2.Run(` [ abc.toString(), def.toString() ]; `) require.NoError(t, err) is(value, `function() { return "abc"; },function() { return "ghi"; }`) } }) } func TestOttoRun(t *testing.T) { tt(t, func() { vm := New() program, err := parser.ParseFile(nil, "", "", 0) require.NoError(t, err) value, err := vm.Run(program) require.NoError(t, err) is(value, UndefinedValue()) program, err = parser.ParseFile(nil, "", "2 + 2", 0) require.NoError(t, err) value, err = vm.Run(program) require.NoError(t, err) is(value, 4) value, err = vm.Run(program) require.NoError(t, err) is(value, 4) program, err = parser.ParseFile(nil, "", "var abc; if (!abc) abc = 0; abc += 2; abc;", 0) require.NoError(t, err) value, err = vm.Run(program) require.NoError(t, err) is(value, 2) value, err = vm.Run(program) require.NoError(t, err) is(value, 4) value, err = vm.Run(program) require.NoError(t, err) is(value, 6) { src := []byte("var abc; if (!abc) abc = 0; abc += 2; abc;") value, err = vm.Run(src) require.NoError(t, err) is(value, 8) value, err = vm.Run(bytes.NewBuffer(src)) require.NoError(t, err) is(value, 10) value, err = vm.Run(io.Reader(bytes.NewBuffer(src))) require.NoError(t, err) is(value, 12) } { script, err2 := vm.Compile("", `var abc; if (!abc) abc = 0; abc += 2; abc;`) require.NoError(t, err2) value, err2 = vm.Run(script) require.NoError(t, err2) is(value, 14) value, err2 = vm.Run(script) require.NoError(t, err2) is(value, 16) is(script.String(), "// \nvar abc; if (!abc) abc = 0; abc += 2; abc;") } }) } // This generates functions to be used by the test below. The arguments are // `src`, which is something that otto can execute, and `expected`, which is // what the result of executing `src` should be. func makeTestOttoEvalFunction(t *testing.T, src, expected interface{}) func(c FunctionCall) Value { t.Helper() return func(c FunctionCall) Value { v, err := c.Otto.Eval(src) require.NoError(t, err) i, err := v.Export() require.NoError(t, err) is(i, expected) return v } } func TestOttoEval(t *testing.T) { tt(t, func() { vm := New() err := vm.Set("x1", makeTestOttoEvalFunction(t, `a`, 1)) require.NoError(t, err) err = vm.Set("y1", makeTestOttoEvalFunction(t, `b`, "hello")) require.NoError(t, err) err = vm.Set("z1", makeTestOttoEvalFunction(t, `c`, true)) require.NoError(t, err) err = vm.Set("w", makeTestOttoEvalFunction(t, `a = 2; b = 'what'; c = false; null`, nil)) require.NoError(t, err) err = vm.Set("x2", makeTestOttoEvalFunction(t, `a`, 2)) require.NoError(t, err) err = vm.Set("y2", makeTestOttoEvalFunction(t, `b`, "what")) require.NoError(t, err) err = vm.Set("z2", makeTestOttoEvalFunction(t, `c`, false)) require.NoError(t, err) // note that these variables are defined in the scope of function `t`, // so would not usually be available to the functions called below. // // this is _not_ the recommended use case for `Eval` - instead it's // intended to be used in `debugger` handlers. this code here is the // equivalent of reading behind the current stack frame in C... // technically valid, but completely insane. // // makes for a good test case though. _, err = vm.Run(`(function t() { var a = 1; var b = 'hello'; var c = true; x1(); y1(); z1(); w(); x2(); y2(); z2(); }())`) require.NoError(t, err) }) // this test makes sure that `Eval` doesn't explode if the VM doesn't have // a scope other than global defined. tt(t, func() { vm := New() _, err := vm.Eval("null") require.NoError(t, err) err = vm.Set("a", 1) require.NoError(t, err) err = vm.Set("b", 2) require.NoError(t, err) v, err := vm.Eval("a + b") require.NoError(t, err) r, err := v.Export() require.NoError(t, err) is(r, 3) }) } func TestOttoContext(t *testing.T) { // These are all the builtin global scope symbols builtins := []string{ "escape", "URIError", classRegExpName, "ReferenceError", "parseFloat", "parseInt", "SyntaxError", "decodeURIComponent", "encodeURIComponent", "Infinity", "JSON", "isNaN", "unescape", "decodeURI", classObjectName, classFunctionName, "RangeError", classErrorName, "get_context", "eval", classNumberName, "Math", "NaN", classDateName, classBooleanName, "console", "encodeURI", "EvalError", classArrayName, "TypeError", classStringName, "isFinite", "undefined", } tt(t, func() { vm := New() err := vm.Set("get_context", func(c FunctionCall) Value { ctx := c.Otto.Context() is(ctx.Callee, "f1") is(ctx.Filename, "") is(ctx.Line, 8) is(ctx.Column, 5) is(ctx.Stacktrace, []string{ "f1 (:8:5)", "f2 (:15:5)", "f3 (:19:5)", "t (:22:4)", }) is(len(ctx.Symbols), 9+len(builtins)) is(ctx.Symbols["a"], 1) is(ctx.Symbols["b"], "hello") is(ctx.Symbols["c"], true) is(ctx.Symbols["j"], 2) is(ctx.Symbols["f1"].IsFunction(), true) is(ctx.Symbols["f2"].IsFunction(), true) is(ctx.Symbols["f3"].IsFunction(), true) is(ctx.Symbols["t"].IsFunction(), true) callee, _ := ctx.Symbols["arguments"].Object().Get("callee") is(callee.IsDefined(), true) return Value{} }) require.NoError(t, err) _, err = vm.Run(`(function t() { var a = 1; var b = 'hello'; var c = true; function f1() { var j = 2; get_context(); (function() { var d = 4; })() } function f2() { f1(); } function f3() { f2(); } f3(); a = 2; b = 'goodbye'; c = false; }())`) require.NoError(t, err) }) // this test makes sure that `Context` works on global scope by default, if // there is not a current scope. tt(t, func() { vm := New() err := vm.Set("get_context", func(c FunctionCall) Value { ctx := c.Otto.Context() is(ctx.Callee, "") is(ctx.Filename, "") is(ctx.Line, 3) is(ctx.Column, 4) is(ctx.Stacktrace, []string{":3:4"}) is(len(ctx.Symbols), 2+len(builtins)) is(ctx.Symbols["a"], 1) is(ctx.Symbols["b"], UndefinedValue()) return Value{} }) require.NoError(t, err) _, err = vm.Run(` var a = 1; get_context() var b = 2; `) require.NoError(t, err) }) // this test makes sure variables are shadowed correctly. tt(t, func() { vm := New() err := vm.Set("check_context", func(c FunctionCall) Value { n, err := c.Argument(0).ToInteger() require.NoError(t, err) ctx := c.Otto.Context() is(ctx.Symbols["a"], n) return Value{} }) require.NoError(t, err) _, err = vm.Run(` var a = 1; check_context(1); (function() { var a = 2; check_context(2); }()); (function(a) { check_context(3); }(3)); (function(a) { check_context(4); }).call(null, 4); check_context(1); `) require.NoError(t, err) }) } func Test_objectLength(t *testing.T) { tt(t, func() { _, vm := test() value := vm.Set("abc", []string{"jkl", "mno"}) is(objectLength(value.object()), 2) value, _ = vm.Run(`[1, 2, 3]`) is(objectLength(value.object()), 3) value, _ = vm.Run(`new String("abcdefghi")`) is(objectLength(value.object()), 9) value, _ = vm.Run(`"abcdefghi"`) is(objectLength(value.object()), 0) }) } func Test_stackLimit(t *testing.T) { // JavaScript stack depth before entering `a` is 5; becomes 6 after // entering. setting the maximum stack depth to 5 should result in an // error ocurring at that 5 -> 6 boundary. code := ` function a() {} function b() { a(); } function c() { b(); } function d() { c(); } function e() { d(); } e(); ` // has no error tt(t, func() { _, vm := test() _, err := vm.Run(code) require.NoError(t, err) }) // has error tt(t, func() { _, vm := test() vm.vm.SetStackDepthLimit(2) _, err := vm.Run(code) require.Error(t, err) }) // has error tt(t, func() { _, vm := test() vm.vm.SetStackDepthLimit(5) _, err := vm.Run(code) require.Error(t, err) }) // has no error tt(t, func() { _, vm := test() vm.vm.SetStackDepthLimit(6) _, err := vm.Run(code) require.NoError(t, err) }) // has no error tt(t, func() { _, vm := test() vm.vm.SetStackDepthLimit(1) vm.vm.SetStackDepthLimit(0) _, err := vm.Run(code) require.NoError(t, err) }) } 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 { //nolint:errorlint 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() } } func BenchmarkClone(b *testing.B) { vm := New() b.ResetTimer() for i := 0; i < b.N; i++ { vm.clone() } } ================================================ FILE: panic_test.go ================================================ package otto import ( "testing" ) func Test_panic(t *testing.T) { tt(t, func() { test, _ := test() // Test that property.value is set to something if writable is set // to something test(` var abc = []; Object.defineProperty(abc, "0", { writable: false }); Object.defineProperty(abc, "0", { writable: false }); "0" in abc; `, true) test(`raise: var abc = []; Object.defineProperty(abc, "0", { writable: false }); Object.defineProperty(abc, "0", { value: false, writable: false }); `, "TypeError: Array.DefineOwnProperty Object.DefineOwnProperty failed") // Test that a regular expression can contain \c0410 (CYRILLIC CAPITAL LETTER A) // without panicking test(` var abc = 0x0410; var def = String.fromCharCode(abc); new RegExp("\\c" + def).exec(def); `, "null") // Test transforming a transformable regular expression without a panic test(` new RegExp("\\u0000"); new RegExp("\\undefined").test("undefined"); `, true) }) } ================================================ FILE: parser/comments_test.go ================================================ package parser import ( "fmt" "reflect" "testing" "github.com/robertkrimen/otto/ast" ) func checkComments(actual []*ast.Comment, expected []string, position ast.CommentPosition) error { var comments []*ast.Comment for _, c := range actual { if c.Position == position { comments = append(comments, c) } } if len(comments) != len(expected) { return fmt.Errorf("the number of comments is not correct. %v != %v", len(comments), len(expected)) } for i, v := range comments { if v.Text != expected[i] { return fmt.Errorf("comments do not match: %q != %q", v.Text, expected[i]) } if v.Position != position { return fmt.Errorf("comment positions do not match: %d != %d", position, v.Position) } } return nil } func displayComments(m ast.CommentMap) { fmt.Printf("Displaying comments:\n") //nolint:forbidigo for n, comments := range m { fmt.Printf("%v %v:\n", reflect.TypeOf(n), n) //nolint:forbidigo for i, comment := range comments { fmt.Printf(" [%v] %v @ %v\n", i, comment.Text, comment.Position) //nolint:forbidigo } } } func TestParser_comments(t *testing.T) { tt(t, func() { test := func(source string, chk interface{}) (*parser, *ast.Program) { parser, program, err := testParseWithMode(source, StoreComments) is(firstErr(err), chk) // Check unresolved comments return parser, program } var err error var p *parser var program *ast.Program p, program = test("q=2;// Hej\nv = 0", nil) is(len(program.Body), 2) err = checkComments((p.comments.CommentMap)[program.Body[1]], []string{" Hej"}, ast.LEADING) is(err, nil) // Assignment p, program = test("i = /*test=*/ 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right], []string{"test="}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Conditional, before consequent p, program = test("i ? /*test?*/ 2 : 3", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ConditionalExpression).Consequent], []string{"test?"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Conditional, after consequent p, program = test("i ? 2 /*test?*/ : 3", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ConditionalExpression).Consequent], []string{"test?"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // Conditional, before alternate p, program = test("i ? 2 : /*test:*/ 3", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ConditionalExpression).Alternate], []string{"test:"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Logical OR p, program = test("i || /*test||*/ 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"test||"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Logical AND p, program = test("i && /*test&&*/ 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"test&&"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Bitwise OR p, program = test("i | /*test|*/ 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"test|"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Exclusive OR p, program = test("i ^ /*test^*/ 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"test^"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Bitwise AND p, program = test("i & /*test&*/ 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"test&"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Equality p, program = test("i == /*test==*/ 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"test=="}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Relational, < p, program = test("i < /*test<*/ 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"test<"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Relational, instanceof p, program = test("i instanceof /*testinstanceof*/ thing", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"testinstanceof"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Shift left p, program = test("i << /*test<<*/ 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"test<<"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // + p, program = test("i + /*test+*/ 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"test+"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // * p, program = test("i * /*test**/ 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"test*"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Unary prefix, ++ p, program = test("++/*test++*/i", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.UnaryExpression).Operand], []string{"test++"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Unary prefix, delete p, program = test("delete /*testdelete*/ i", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.UnaryExpression).Operand], []string{"testdelete"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Unary postfix, ++ p, program = test("i/*test++*/++", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.UnaryExpression).Operand], []string{"test++"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // + pt 2 p, program = test("i /*test+*/ + 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Left], []string{"test+"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // Multiple comments for a single node p, program = test("i /*test+*/ /*test+2*/ + 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0]], []string{}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Left], []string{"test+", "test+2"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 2) // Multiple comments for multiple nodes p, program = test("i /*test1*/ + 2 /*test2*/ + a /*test3*/ * x /*test4*/", nil) is(len(program.Body), 1) is(p.comments.CommentMap.Size(), 4) // Leading comment p, program = test("/*leadingtest*/i + 2", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement)], []string{"leadingtest"}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Left], []string{}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // Leading comment, with semicolon p, program = test("/*leadingtest;*/;i + 2", nil) is(len(program.Body), 2) is(checkComments((p.comments.CommentMap)[program.Body[1]], []string{"leadingtest;"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Arrays p, program = test("[1, 2 /*test2*/, 3]", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ArrayLiteral).Value[1]], []string{"test2"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // Function calls p, program = test("fun(a,b) //test", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.CallExpression)], []string{"test"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // Function calls, pt 2 p, program = test("fun(a/*test1*/,b)", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.CallExpression).ArgumentList[0]], []string{"test1"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // Function calls, pt 3 p, program = test("fun(/*test1*/a,b)", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.CallExpression).ArgumentList[0]], []string{"test1"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Arrays pt 2 p, program = test(`["abc".substr(0,1)/*testa*/, "abc.substr(0,2)"];`, nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ArrayLiteral).Value[0]], []string{"testa"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // Arrays pt 3 p, program = test(`[a, //test b];`, nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ArrayLiteral).Value[1]], []string{"test"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Arrays pt 4 p, program = test(`[a, //test b, c];`, nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ArrayLiteral).Value[1]], []string{"test"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Arrays pt 5 p, program = test(` [ "a1", // "a" "a2", // "ab" ]; `, nil) is(p.comments.CommentMap.Size(), 2) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ArrayLiteral).Value[1]], []string{" \"a\""}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ArrayLiteral)], []string{" \"ab\""}, ast.FINAL), nil) // Arrays pt 6 p, program = test(`[a, /*test*/ b, c];`, nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ArrayLiteral).Value[1]], []string{"test"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Arrays pt 7 - Empty node p, program = test(`[a,,/*test2*/,];`, nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ArrayLiteral).Value[2]], []string{"test2"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // Arrays pt 8 - Trailing node p, program = test(`[a,,,/*test2*/];`, nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ArrayLiteral)], []string{"test2"}, ast.FINAL), nil) is(p.comments.CommentMap.Size(), 1) // Arrays pt 9 - Leading node p, program = test(`[/*test2*/a,,,];`, nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.ArrayLiteral).Value[0]], []string{"test2"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Object literal p, program = test("obj = {a: 1, b: 2 /*test2*/, c: 3}", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.ObjectLiteral).Value[1].Value], []string{"test2"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // Object literal, pt 2 p, program = test("obj = {/*test2*/a: 1, b: 2, c: 3}", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.ObjectLiteral).Value[0].Value], []string{"test2"}, ast.KEY), nil) is(p.comments.CommentMap.Size(), 1) // Object literal, pt 3 p, program = test("obj = {x/*test2*/: 1, y: 2, z: 3}", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.ObjectLiteral).Value[0].Value], []string{"test2"}, ast.COLON), nil) is(p.comments.CommentMap.Size(), 1) // Object literal, pt 4 p, program = test("obj = {x: /*test2*/1, y: 2, z: 3}", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.ObjectLiteral).Value[0].Value], []string{"test2"}, ast.LEADING), nil) is(p.comments.CommentMap.Size(), 1) // Object literal, pt 5 p, program = test("obj = {x: 1/*test2*/, y: 2, z: 3}", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.ObjectLiteral).Value[0].Value], []string{"test2"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // Object literal, pt 6 p, program = test("obj = {x: 1, y: 2, z: 3/*test2*/}", nil) is(len(program.Body), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.ObjectLiteral).Value[2].Value], []string{"test2"}, ast.TRAILING), nil) is(p.comments.CommentMap.Size(), 1) // Object literal, pt 7 - trailing comment p, program = test("obj = {x: 1, y: 2, z: 3,/*test2*/}", nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.ObjectLiteral)], []string{"test2"}, ast.FINAL), nil) // Line breaks p, program = test(` t1 = "BLA DE VLA" /*Test*/ t2 = "Nothing happens." `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[1]], []string{"Test"}, ast.LEADING), nil) // Line breaks pt 2 p, program = test(` t1 = "BLA DE VLA" /*Test*/ t2 = "Nothing happens." `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.StringLiteral)], []string{"Test"}, ast.TRAILING), nil) is(checkComments((p.comments.CommentMap)[program.Body[1].(*ast.ExpressionStatement)], []string{}, ast.LEADING), nil) // Line breaks pt 3 p, program = test(` t1 = "BLA DE VLA" /*Test*/ /*Test2*/ t2 = "Nothing happens." `, nil) is(p.comments.CommentMap.Size(), 2) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.StringLiteral)], []string{"Test", "Test2"}, ast.TRAILING), nil) is(checkComments((p.comments.CommentMap)[program.Body[1].(*ast.ExpressionStatement)], []string{}, ast.LEADING), nil) // Line breaks pt 4 p, program = test(` t1 = "BLA DE VLA" /*Test*/ /*Test2*/ t2 = "Nothing happens." `, nil) is(p.comments.CommentMap.Size(), 2) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.StringLiteral)], []string{"Test"}, ast.TRAILING), nil) is(checkComments((p.comments.CommentMap)[program.Body[1]], []string{"Test2"}, ast.LEADING), nil) // Line breaks pt 5 p, program = test(` t1 = "BLA DE VLA"; /*Test*/ t2 = "Nothing happens." `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[1]], []string{"Test"}, ast.LEADING), nil) // Line breaks pt 6 p, program = test(` t1 = "BLA DE VLA"; /*Test*/ /*Test2*/ t2 = "Nothing happens." `, nil) is(p.comments.CommentMap.Size(), 2) is(checkComments((p.comments.CommentMap)[program.Body[1]], []string{"Test", "Test2"}, ast.LEADING), nil) // Misc p, _ = test(` var x = Object.create({y: { }, // a }); `, nil) is(p.comments.CommentMap.Size(), 1) // Misc 2 p, _ = test(` var x = Object.create({y: { }, // a // b a: 2}); `, nil) is(p.comments.CommentMap.Size(), 2) // Statement blocks p, program = test(` (function() { // Baseline setup }) `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body], []string{" Baseline setup"}, ast.FINAL), nil) // Switches p, program = test(` switch (switcha) { // switch comment case "switchb": a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.SwitchStatement).Body[0]], []string{" switch comment"}, ast.LEADING), nil) // Switches pt 2 p, program = test(` switch (switcha) { case /*switch comment*/ "switchb": a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.SwitchStatement).Body[0].Test], []string{"switch comment"}, ast.LEADING), nil) // Switches pt 3 p, program = test(` switch (switcha) { case "switchb" /*switch comment*/: a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.SwitchStatement).Body[0].Test], []string{"switch comment"}, ast.TRAILING), nil) // Switches pt 4 p, program = test(` switch (switcha) { case "switchb": /*switch comment*/ a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.SwitchStatement).Body[0].Consequent[0]], []string{"switch comment"}, ast.LEADING), nil) // Switches pt 5 - default p, program = test(` switch (switcha) { default: /*switch comment*/ a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.SwitchStatement).Body[0].Consequent[0]], []string{"switch comment"}, ast.LEADING), nil) // Switches pt 6 p, program = test(` switch (switcha) { case "switchb": /*switch comment*/a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.SwitchStatement).Body[0].Consequent[0]], []string{"switch comment"}, ast.LEADING), nil) // Switches pt 7 p, program = test(` switch (switcha) { case "switchb": /*switch comment*/ { a } } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.SwitchStatement).Body[0].Consequent[0]], []string{"switch comment"}, ast.LEADING), nil) // Switches pt 8 p, program = test(` switch (switcha) { case "switchb": { a }/*switch comment*/ } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.SwitchStatement).Body[0].Consequent[0]], []string{"switch comment"}, ast.TRAILING), nil) // Switches pt 9 p, program = test(` switch (switcha) { case "switchb": /*switch comment*/ { a } } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.SwitchStatement).Body[0].Consequent[0]], []string{"switch comment"}, ast.LEADING), nil) // Switches pt 10 p, program = test(` switch (switcha) { case "switchb": { /*switch comment*/a } } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.SwitchStatement).Body[0].Consequent[0].(*ast.BlockStatement).List[0]], []string{"switch comment"}, ast.LEADING), nil) // For loops p, program = test(` for(/*comment*/i = 0 ; i < 1 ; i++) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForStatement).Initializer.(*ast.SequenceExpression).Sequence[0].(*ast.AssignExpression).Left], []string{"comment"}, ast.LEADING), nil) // For loops pt 2 p, program = test(` for(i/*comment*/ = 0 ; i < 1 ; i++) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForStatement).Initializer.(*ast.SequenceExpression).Sequence[0].(*ast.AssignExpression).Left], []string{"comment"}, ast.TRAILING), nil) // For loops pt 3 p, program = test(` for(i = 0 ; /*comment*/i < 1 ; i++) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForStatement).Test.(*ast.BinaryExpression).Left], []string{"comment"}, ast.LEADING), nil) // For loops pt 4 p, program = test(` for(i = 0 ;i /*comment*/ < 1 ; i++) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForStatement).Test.(*ast.BinaryExpression).Left], []string{"comment"}, ast.TRAILING), nil) // For loops pt 5 p, program = test(` for(i = 0 ;i < 1 /*comment*/ ; i++) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForStatement).Test.(*ast.BinaryExpression).Right], []string{"comment"}, ast.TRAILING), nil) // For loops pt 6 p, program = test(` for(i = 0 ;i < 1 ; /*comment*/ i++) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForStatement).Update.(*ast.UnaryExpression).Operand], []string{"comment"}, ast.LEADING), nil) // For loops pt 7 p, program = test(` for(i = 0 ;i < 1 ; i++) /*comment*/ { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForStatement).Body], []string{"comment"}, ast.LEADING), nil) // For loops pt 8 p, program = test(` for(i = 0 ;i < 1 ; i++) { a }/*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForStatement).Body], []string{"comment"}, ast.TRAILING), nil) // For loops pt 9 p, program = test(` for(i = 0 ;i < 1 ; /*comment*/i++) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForStatement).Update.(*ast.UnaryExpression).Operand], []string{"comment"}, ast.LEADING), nil) // For loops pt 10 p, program = test(` for(i = 0 ;i < 1 ; i/*comment*/++) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForStatement).Update.(*ast.UnaryExpression).Operand.(*ast.Identifier)], []string{"comment"}, ast.TRAILING), nil) // For loops pt 11 p, program = test(` for(i = 0 ;i < 1 ; i++/*comment*/) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForStatement).Update.(*ast.UnaryExpression)], []string{"comment"}, ast.TRAILING), nil) // ForIn p, program = test(` for(/*comment*/var i = 0 in obj) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForInStatement).Into], []string{"comment"}, ast.LEADING), nil) // ForIn pt 2 p, program = test(` for(var i = 0 /*comment*/in obj) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForInStatement).Into.(*ast.VariableExpression).Initializer], []string{"comment"}, ast.TRAILING), nil) // ForIn pt 3 p, program = test(` for(var i = 0 in /*comment*/ obj) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForInStatement).Source], []string{"comment"}, ast.LEADING), nil) // ForIn pt 4 p, program = test(` for(var i = 0 in obj/*comment*/) { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForInStatement).Source], []string{"comment"}, ast.TRAILING), nil) // ForIn pt 5 p, program = test(` for(var i = 0 in obj) /*comment*/ { a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForInStatement).Body], []string{"comment"}, ast.LEADING), nil) // ForIn pt 6 p, program = test(` for(var i = 0 in obj) { a }/*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ForInStatement).Body], []string{"comment"}, ast.TRAILING), nil) // ForIn pt 7 p, program = test(` for(var i = 0 in obj) { a } // comment `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program], []string{" comment"}, ast.TRAILING), nil) // ForIn pt 8 p, program = test(` for(var i = 0 in obj) { a } // comment c `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[1]], []string{" comment"}, ast.LEADING), nil) // Block p, program = test(` /*comment*/{ a } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.BlockStatement)], []string{"comment"}, ast.LEADING), nil) // Block pt 2 p, program = test(` { a }/*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.BlockStatement)], []string{"comment"}, ast.TRAILING), nil) // If then else p, program = test(` /*comment*/ if(a) { b } else { c } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.IfStatement)], []string{"comment"}, ast.LEADING), nil) // If then else pt 2 p, program = test(` if/*comment*/(a) { b } else { c } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.IfStatement)], []string{"comment"}, ast.IF), nil) // If then else pt 3 p, program = test(` if(/*comment*/a) { b } else { c } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.IfStatement).Test], []string{"comment"}, ast.LEADING), nil) // If then else pt 4 p, program = test(` if(a/*comment*/) { b } else { c } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.IfStatement).Test], []string{"comment"}, ast.TRAILING), nil) // If then else pt 4 p, program = test(` if(a)/*comment*/ { b } else { c } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.IfStatement).Consequent], []string{"comment"}, ast.LEADING), nil) // If then else pt 5 p, program = test(` if(a) { b } /*comment*/else { c } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.IfStatement).Consequent], []string{"comment"}, ast.TRAILING), nil) // If then else pt 6 p, program = test(` if(a) { b } else/*comment*/ { c } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.IfStatement).Alternate], []string{"comment"}, ast.LEADING), nil) // If then else pt 7 p, program = test(` if(a) { b } else { c }/*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.IfStatement).Alternate], []string{"comment"}, ast.TRAILING), nil) // If then else pt 8 p, _ = test(` if /*comment*/ (a) { b } else { c } `, nil) is(p.comments.CommentMap.Size(), 1) // If then else pt 9 p, _ = test(` if (a) /*comment*/{ b } else { c } `, nil) is(p.comments.CommentMap.Size(), 1) // If then else pt 10 p, _ = test(` if(a){ b } /*comment*/ else { c } `, nil) is(p.comments.CommentMap.Size(), 1) // Do while p, program = test(` /*comment*/do { a } while(b) `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.DoWhileStatement)], []string{"comment"}, ast.LEADING), nil) // Do while pt 2 p, program = test(` do /*comment*/ { a } while(b) `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.DoWhileStatement)], []string{"comment"}, ast.DO), nil) // Do while pt 3 p, program = test(` do { a } /*comment*/ while(b) `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.DoWhileStatement).Body], []string{"comment"}, ast.TRAILING), nil) // Do while pt 4 p, program = test(` do { a } while/*comment*/(b) `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.DoWhileStatement)], []string{"comment"}, ast.WHILE), nil) // Do while pt 5 p, program = test(` do { a } while(b)/*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.DoWhileStatement)], []string{"comment"}, ast.TRAILING), nil) // While p, program = test(` /*comment*/while(a) { b } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement)], []string{"comment"}, ast.LEADING), nil) // While pt 2 p, program = test(` while/*comment*/(a) { b } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement)], []string{"comment"}, ast.WHILE), nil) // While pt 3 p, program = test(` while(/*comment*/a) { b } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement).Test], []string{"comment"}, ast.LEADING), nil) // While pt 4 p, program = test(` while(a/*comment*/) { b } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement).Test], []string{"comment"}, ast.TRAILING), nil) // While pt 5 p, program = test(` while(a) /*comment*/ { c } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement).Body], []string{"comment"}, ast.LEADING), nil) // While pt 6 p, program = test(` while(a) { c }/*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement).Body], []string{"comment"}, ast.TRAILING), nil) // While pt 7 p, program = test(` while(a) { c/*comment*/ } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement).Body.(*ast.BlockStatement).List[0].(*ast.ExpressionStatement).Expression.(*ast.Identifier)], []string{"comment"}, ast.TRAILING), nil) // While pt 7 p, program = test(` while(a) { /*comment*/ } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement).Body.(*ast.BlockStatement)], []string{"comment"}, ast.FINAL), nil) // While pt 8 p, _ = test(` while /*comment*/(a) { } `, nil) is(p.comments.CommentMap.Size(), 1) // While pt 9 p, _ = test(` while (a) /*comment*/{ } `, nil) is(p.comments.CommentMap.Size(), 1) // Break p, program = test(` while(a) { break/*comment*/; } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement).Body.(*ast.BlockStatement).List[0].(*ast.BranchStatement)], []string{"comment"}, ast.TRAILING), nil) // Break pt 2 p, program = test(` while(a) { next/*comment*/: break next; } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement).Body.(*ast.BlockStatement).List[0].(*ast.LabelledStatement).Label], []string{"comment"}, ast.TRAILING), nil) // Break pt 3 p, program = test(` while(a) { next:/*comment*/ break next; } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement).Body.(*ast.BlockStatement).List[0].(*ast.LabelledStatement)], []string{"comment"}, ast.LEADING), nil) // Break pt 4 p, program = test(` while(a) { next: break /*comment*/next; } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement).Body.(*ast.BlockStatement).List[0].(*ast.LabelledStatement).Statement.(*ast.BranchStatement).Label], []string{"comment"}, ast.LEADING), nil) // Break pt 5 p, program = test(` while(a) { next: break next/*comment*/; } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WhileStatement).Body.(*ast.BlockStatement).List[0].(*ast.LabelledStatement).Statement.(*ast.BranchStatement).Label], []string{"comment"}, ast.TRAILING), nil) // Debugger p, program = test(` debugger // comment `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.DebuggerStatement)], []string{" comment"}, ast.TRAILING), nil) // Debugger pt 2 p, program = test(` debugger; // comment `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program], []string{" comment"}, ast.TRAILING), nil) // Debugger pt 3 p, program = test(` debugger; // comment `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program], []string{" comment"}, ast.TRAILING), nil) // With p, program = test(` /*comment*/with(a) { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WithStatement)], []string{"comment"}, ast.LEADING), nil) // With pt 2 p, program = test(` with/*comment*/(a) { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WithStatement)], []string{"comment"}, ast.WITH), nil) // With pt 3 p, program = test(` with(/*comment*/a) { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WithStatement).Object], []string{"comment"}, ast.LEADING), nil) // With pt 4 p, program = test(` with(a/*comment*/) { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WithStatement).Object], []string{"comment"}, ast.TRAILING), nil) // With pt 5 p, program = test(` with(a) /*comment*/ { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WithStatement).Body], []string{"comment"}, ast.LEADING), nil) // With pt 6 p, program = test(` with(a) { }/*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.WithStatement).Body], []string{"comment"}, ast.TRAILING), nil) // With pt 7 p, _ = test(` with /*comment*/(a) { } `, nil) is(p.comments.CommentMap.Size(), 1) // With pt 8 p, _ = test(` with (a) /*comment*/{ } `, nil) is(p.comments.CommentMap.Size(), 1) // Var p, program = test(` /*comment*/var a `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.VariableStatement)], []string{"comment"}, ast.LEADING), nil) // Var pt 2 p, program = test(` var/*comment*/ a `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.VariableStatement).List[0]], []string{"comment"}, ast.LEADING), nil) // Var pt 3 p, program = test(` var a/*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.VariableStatement).List[0]], []string{"comment"}, ast.TRAILING), nil) // Var pt 4 p, program = test(` var a/*comment*/, b `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.VariableStatement).List[0].(*ast.VariableExpression)], []string{"comment"}, ast.TRAILING), nil) // Var pt 5 p, program = test(` var a, /*comment*/b `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.VariableStatement).List[1].(*ast.VariableExpression)], []string{"comment"}, ast.LEADING), nil) // Var pt 6 p, program = test(` var a, b/*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.VariableStatement).List[1]], []string{"comment"}, ast.TRAILING), nil) // Var pt 7 p, program = test(` var a, b; /*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program], []string{"comment"}, ast.TRAILING), nil) // Return p, _ = test(` function f() { /*comment*/return o } `, nil) is(p.comments.CommentMap.Size(), 1) // Try catch p, program = test(` /*comment*/try { a } catch(b) { c } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement)], []string{"comment"}, ast.LEADING), nil) // Try catch pt 2 p, program = test(` try/*comment*/ { a } catch(b) { c } finally { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement).Body], []string{"comment"}, ast.LEADING), nil) // Try catch pt 3 p, program = test(` try { a }/*comment*/ catch(b) { c } finally { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement).Body], []string{"comment"}, ast.TRAILING), nil) // Try catch pt 4 p, program = test(` try { a } catch(/*comment*/b) { c } finally { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement).Catch.Parameter], []string{"comment"}, ast.LEADING), nil) // Try catch pt 5 p, program = test(` try { a } catch(b/*comment*/) { c } finally { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement).Catch.Parameter], []string{"comment"}, ast.TRAILING), nil) // Try catch pt 6 p, program = test(` try { a } catch(b) /*comment*/{ c } finally { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement).Catch.Body], []string{"comment"}, ast.LEADING), nil) // Try catch pt 7 p, program = test(` try { a } catch(b){ c } /*comment*/ finally { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement).Catch.Body], []string{"comment"}, ast.TRAILING), nil) // Try catch pt 8 p, program = test(` try { a } catch(b){ c } finally /*comment*/ { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement).Finally], []string{"comment"}, ast.LEADING), nil) // Try catch pt 9 p, program = test(` try { a } catch(b){ c } finally { }/*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement).Finally], []string{"comment"}, ast.TRAILING), nil) // Try catch pt 11 p, program = test(` try { a } /*comment*/ catch(b){ c } finally { d } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement).Body], []string{"comment"}, ast.TRAILING), nil) // Throw p, program = test(` throw a/*comment*/ `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ThrowStatement).Argument], []string{"comment"}, ast.TRAILING), nil) // Throw pt 2 p, program = test(` /*comment*/throw a `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ThrowStatement)], []string{"comment"}, ast.LEADING), nil) // Throw pt 3 p, program = test(` throw /*comment*/a `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ThrowStatement).Argument], []string{"comment"}, ast.LEADING), nil) // Try catch pt 10 p, program = test(` try { a } catch(b){ c } /*comment*/finally { } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement).Catch.Body], []string{"comment"}, ast.TRAILING), nil) // Try catch pt 11 p, program = test(` try { a } catch(b){ c } finally /*comment*/ { d } `, nil) is(p.comments.CommentMap.Size(), 1) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.TryStatement).Finally], []string{"comment"}, ast.LEADING), nil) // Switch / comment p, _ = test(` var volvo = 1 //comment switch(abra) { } `, nil) is(p.comments.CommentMap.Size(), 1) // Switch / comment p, _ = test(` f("string",{ key: "val" //comment }); `, nil) is(p.comments.CommentMap.Size(), 1) // Switch / comment p, program = test(` function f() { /*comment*/if(true){a++} } `, nil) is(p.comments.CommentMap.Size(), 1) n := program.Body[0].(*ast.FunctionStatement).Function.Body.(*ast.BlockStatement).List[0] is(checkComments((p.comments.CommentMap)[n], []string{"comment"}, ast.LEADING), nil) // Function in function p, program = test(` function f() { /*comment*/function f2() { } } `, nil) is(p.comments.CommentMap.Size(), 1) n = program.Body[0].(*ast.FunctionStatement).Function.Body.(*ast.BlockStatement).List[0] is(checkComments((p.comments.CommentMap)[n], []string{"comment"}, ast.LEADING), nil) p, program = test(` a + /*comment1*/ /*comment2*/ b/*comment3*/; /*comment4*/c `, nil) is(p.comments.CommentMap.Size(), 4) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"comment1", "comment2"}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"comment3"}, ast.TRAILING), nil) is(checkComments((p.comments.CommentMap)[program.Body[1]], []string{"comment4"}, ast.LEADING), nil) p, program = test(` a + /*comment1*/ /*comment2*/ b/*comment3*/ /*comment4*/c `, nil) is(p.comments.CommentMap.Size(), 4) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"comment1", "comment2"}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.BinaryExpression).Right], []string{"comment3"}, ast.TRAILING), nil) is(checkComments((p.comments.CommentMap)[program.Body[1]], []string{"comment4"}, ast.LEADING), nil) // New p, program = test(` a = /*comment1*/new /*comment2*/ obj/*comment3*/() `, nil) is(p.comments.CommentMap.Size(), 3) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right], []string{"comment1"}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.NewExpression).Callee], []string{"comment2"}, ast.LEADING), nil) is(checkComments((p.comments.CommentMap)[program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.NewExpression).Callee], []string{"comment3"}, ast.TRAILING), nil) }) } func TestParser_comments2(t *testing.T) { tt(t, func() { test := func(source string, chk interface{}) (*parser, *ast.Program) { parser, program, err := testParseWithMode(source, StoreComments) is(firstErr(err), chk) // Check unresolved comments is(len(parser.comments.Comments), 0) return parser, program } parser, program := test(` a = /*comment1*/new /*comment2*/ obj/*comment3*/() `, nil) n := program.Body[0] fmt.Printf("FOUND NODE: %v, number of comments: %v\n", reflect.TypeOf(n), len(parser.comments.CommentMap[n])) //nolint:forbidigo displayComments(parser.comments.CommentMap) }) } ================================================ FILE: parser/error.go ================================================ package parser import ( "fmt" "sort" "github.com/robertkrimen/otto/file" "github.com/robertkrimen/otto/token" ) const ( errUnexpectedToken = "Unexpected token %v" errUnexpectedEndOfInput = "Unexpected end of input" ) // UnexpectedNumber: 'Unexpected number', // UnexpectedString: 'Unexpected string', // UnexpectedIdentifier: 'Unexpected identifier', // UnexpectedReserved: 'Unexpected reserved word', // NewlineAfterThrow: 'Illegal newline after throw', // InvalidRegExp: 'Invalid regular expression', // UnterminatedRegExp: 'Invalid regular expression: missing /', // InvalidLHSInAssignment: 'invalid left-hand side in assignment', // InvalidLHSInForIn: 'Invalid left-hand side in for-in', // MultipleDefaultsInSwitch: 'More than one default clause in switch statement', // NoCatchOrFinally: 'Missing catch or finally after try', // UnknownLabel: 'Undefined label \'%0\'', // Redeclaration: '%0 \'%1\' has already been declared', // IllegalContinue: 'Illegal continue statement', // IllegalBreak: 'Illegal break statement', // IllegalReturn: 'Illegal return statement', // StrictModeWith: 'Strict mode code may not include a with statement', // StrictCatchVariable: 'Catch variable may not be eval or arguments in strict mode', // StrictVarName: 'Variable name may not be eval or arguments in strict mode', // StrictParamName: 'Parameter name eval or arguments is not allowed in strict mode', // StrictParamDupe: 'Strict mode function may not have duplicate parameter names', // StrictFunctionName: 'Function name may not be eval or arguments in strict mode', // StrictOctalLiteral: 'Octal literals are not allowed in strict mode.', // StrictDelete: 'Delete of an unqualified identifier in strict mode.', // StrictDuplicateProperty: 'Duplicate data property in object literal not allowed in strict mode', // AccessorDataProperty: 'Object literal may not have data and accessor property with the same name', // AccessorGetSet: 'Object literal may not have multiple get/set accessors with the same name', // StrictLHSAssignment: 'Assignment to eval or arguments is not allowed in strict mode', // StrictLHSPostfix: 'Postfix increment/decrement may not have eval or arguments operand in strict mode', // StrictLHSPrefix: 'Prefix increment/decrement may not have eval or arguments operand in strict mode', // StrictReservedWord: 'Use of future reserved word in strict mode' // A SyntaxError is a description of an ECMAScript syntax error. // An Error represents a parsing error. It includes the position where the error occurred and a message/description. type Error struct { Message string Position file.Position } // FIXME Should this be "SyntaxError"? func (e Error) Error() string { filename := e.Position.Filename if filename == "" { filename = "(anonymous)" } return fmt.Sprintf("%s: Line %d:%d %s", filename, e.Position.Line, e.Position.Column, e.Message, ) } func (p *parser) error(place interface{}, msg string, msgValues ...interface{}) { var idx file.Idx switch place := place.(type) { case int: idx = p.idxOf(place) case file.Idx: if place == 0 { idx = p.idxOf(p.chrOffset) } else { idx = place } default: panic(fmt.Errorf("error(%T, ...)", place)) } position := p.position(idx) msg = fmt.Sprintf(msg, msgValues...) p.errors.Add(position, msg) } func (p *parser) errorUnexpected(idx file.Idx, chr rune) { if chr == -1 { p.error(idx, errUnexpectedEndOfInput) return } p.error(idx, errUnexpectedToken, token.ILLEGAL) } func (p *parser) errorUnexpectedToken(tkn token.Token) { if tkn == token.EOF { p.error(file.Idx(0), errUnexpectedEndOfInput) return } value := tkn.String() switch tkn { case token.BOOLEAN, token.NULL: p.error(p.idx, errUnexpectedToken, p.literal) case token.IDENTIFIER: p.error(p.idx, "Unexpected identifier") case token.KEYWORD: // TODO Might be a future reserved word p.error(p.idx, "Unexpected reserved word") case token.NUMBER: p.error(p.idx, "Unexpected number") case token.STRING: p.error(p.idx, "Unexpected string") default: p.error(p.idx, errUnexpectedToken, value) } } // ErrorList is a list of *Errors. type ErrorList []*Error //nolint:errname // Add adds an Error with given position and message to an ErrorList. func (el *ErrorList) Add(position file.Position, msg string) { *el = append(*el, &Error{Position: position, Message: msg}) } // Reset resets an ErrorList to no errors. func (el *ErrorList) Reset() { *el = (*el)[0:0] } // Len implement sort.Interface. func (el *ErrorList) Len() int { return len(*el) } // Swap implement sort.Interface. func (el *ErrorList) Swap(i, j int) { (*el)[i], (*el)[j] = (*el)[j], (*el)[i] } // Less implement sort.Interface. func (el *ErrorList) Less(i, j int) bool { x := (*el)[i].Position y := (*el)[j].Position if x.Filename < y.Filename { return true } if x.Filename == y.Filename { if x.Line < y.Line { return true } if x.Line == y.Line { return x.Column < y.Column } } return false } // Sort sorts el. func (el *ErrorList) Sort() { sort.Sort(el) } // Error implements the Error interface. func (el *ErrorList) Error() string { switch len(*el) { case 0: return "no errors" case 1: return (*el)[0].Error() default: return fmt.Sprintf("%s (and %d more errors)", (*el)[0].Error(), len(*el)-1) } } // Err returns an error equivalent to this ErrorList. // If the list is empty, Err returns nil. func (el *ErrorList) Err() error { if len(*el) == 0 { return nil } return el } ================================================ FILE: parser/expression.go ================================================ package parser import ( "regexp" "github.com/robertkrimen/otto/ast" "github.com/robertkrimen/otto/file" "github.com/robertkrimen/otto/token" ) func (p *parser) parseIdentifier() *ast.Identifier { literal := p.literal idx := p.idx if p.mode&StoreComments != 0 { p.comments.MarkComments(ast.LEADING) } p.next() exp := &ast.Identifier{ Name: literal, Idx: idx, } if p.mode&StoreComments != 0 { p.comments.SetExpression(exp) } return exp } func (p *parser) parsePrimaryExpression() ast.Expression { literal := p.literal idx := p.idx switch p.token { case token.IDENTIFIER: p.next() if len(literal) > 1 { tkn, strict := token.IsKeyword(literal) if tkn == token.KEYWORD { if !strict { p.error(idx, "Unexpected reserved word") } } } return &ast.Identifier{ Name: literal, Idx: idx, } case token.NULL: p.next() return &ast.NullLiteral{ Idx: idx, Literal: literal, } case token.BOOLEAN: p.next() value := false switch literal { case "true": value = true case "false": value = false default: p.error(idx, "Illegal boolean literal") } return &ast.BooleanLiteral{ Idx: idx, Literal: literal, Value: value, } case token.STRING: p.next() value, err := parseStringLiteral(literal[1 : len(literal)-1]) if err != nil { p.error(idx, err.Error()) } return &ast.StringLiteral{ Idx: idx, Literal: literal, Value: value, } case token.NUMBER: p.next() value, err := parseNumberLiteral(literal) if err != nil { p.error(idx, err.Error()) value = 0 } return &ast.NumberLiteral{ Idx: idx, Literal: literal, Value: value, } case token.SLASH, token.QUOTIENT_ASSIGN: return p.parseRegExpLiteral() case token.LEFT_BRACE: return p.parseObjectLiteral() case token.LEFT_BRACKET: return p.parseArrayLiteral() case token.LEFT_PARENTHESIS: p.expect(token.LEFT_PARENTHESIS) expression := p.parseExpression() if p.mode&StoreComments != 0 { p.comments.Unset() } p.expect(token.RIGHT_PARENTHESIS) return expression case token.THIS: p.next() return &ast.ThisExpression{ Idx: idx, } case token.FUNCTION: return p.parseFunction(false) } p.errorUnexpectedToken(p.token) p.nextStatement() return &ast.BadExpression{From: idx, To: p.idx} } func (p *parser) parseRegExpLiteral() *ast.RegExpLiteral { offset := p.chrOffset - 1 // Opening slash already gotten if p.token == token.QUOTIENT_ASSIGN { offset-- // = } idx := p.idxOf(offset) pattern, err := p.scanString(offset) endOffset := p.chrOffset p.next() if err == nil { pattern = pattern[1 : len(pattern)-1] } flags := "" if p.token == token.IDENTIFIER { // gim flags = p.literal endOffset = p.chrOffset p.next() } var value string // TODO 15.10 // Test during parsing that this is a valid regular expression // Sorry, (?=) and (?!) are invalid (for now) pat, err := TransformRegExp(pattern) if err != nil { if pat == "" || p.mode&IgnoreRegExpErrors == 0 { p.error(idx, "Invalid regular expression: %s", err.Error()) } } else { _, err = regexp.Compile(pat) if err != nil { // We should not get here, ParseRegExp should catch any errors p.error(idx, "Invalid regular expression: %s", err.Error()[22:]) // Skip redundant "parse regexp error" } else { value = pat } } literal := p.str[offset:endOffset] return &ast.RegExpLiteral{ Idx: idx, Literal: literal, Pattern: pattern, Flags: flags, Value: value, } } func (p *parser) parseVariableDeclaration(declarationList *[]*ast.VariableExpression) ast.Expression { if p.token != token.IDENTIFIER { idx := p.expect(token.IDENTIFIER) p.nextStatement() return &ast.BadExpression{From: idx, To: p.idx} } literal := p.literal idx := p.idx p.next() node := &ast.VariableExpression{ Name: literal, Idx: idx, } if p.mode&StoreComments != 0 { p.comments.SetExpression(node) } if declarationList != nil { *declarationList = append(*declarationList, node) } if p.token == token.ASSIGN { if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() node.Initializer = p.parseAssignmentExpression() } return node } func (p *parser) parseVariableDeclarationList(idx file.Idx) []ast.Expression { var declarationList []*ast.VariableExpression // Avoid bad expressions var list []ast.Expression for { if p.mode&StoreComments != 0 { p.comments.MarkComments(ast.LEADING) } decl := p.parseVariableDeclaration(&declarationList) list = append(list, decl) if p.token != token.COMMA { break } if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() } p.scope.declare(&ast.VariableDeclaration{ Var: idx, List: declarationList, }) return list } func (p *parser) parseObjectPropertyKey() (string, string) { idx, tkn, literal := p.idx, p.token, p.literal value := "" if p.mode&StoreComments != 0 { p.comments.MarkComments(ast.KEY) } p.next() switch tkn { case token.IDENTIFIER: value = literal case token.NUMBER: var err error _, err = parseNumberLiteral(literal) if err != nil { p.error(idx, err.Error()) } else { value = literal } case token.STRING: var err error value, err = parseStringLiteral(literal[1 : len(literal)-1]) if err != nil { p.error(idx, err.Error()) } default: // null, false, class, etc. if matchIdentifier.MatchString(literal) { value = literal } } return literal, value } func (p *parser) parseObjectProperty() ast.Property { literal, value := p.parseObjectPropertyKey() if literal == "get" && p.token != token.COLON { idx := p.idx _, value = p.parseObjectPropertyKey() parameterList := p.parseFunctionParameterList() node := &ast.FunctionLiteral{ Function: idx, ParameterList: parameterList, } p.parseFunctionBlock(node) return ast.Property{ Key: value, Kind: "get", Value: node, } } else if literal == "set" && p.token != token.COLON { idx := p.idx _, value = p.parseObjectPropertyKey() parameterList := p.parseFunctionParameterList() node := &ast.FunctionLiteral{ Function: idx, ParameterList: parameterList, } p.parseFunctionBlock(node) return ast.Property{ Key: value, Kind: "set", Value: node, } } if p.mode&StoreComments != 0 { p.comments.MarkComments(ast.COLON) } p.expect(token.COLON) exp := ast.Property{ Key: value, Kind: "value", Value: p.parseAssignmentExpression(), } if p.mode&StoreComments != 0 { p.comments.SetExpression(exp.Value) } return exp } func (p *parser) parseObjectLiteral() ast.Expression { var value []ast.Property idx0 := p.expect(token.LEFT_BRACE) for p.token != token.RIGHT_BRACE && p.token != token.EOF { value = append(value, p.parseObjectProperty()) if p.token == token.COMMA { if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() continue } } if p.mode&StoreComments != 0 { p.comments.MarkComments(ast.FINAL) } idx1 := p.expect(token.RIGHT_BRACE) return &ast.ObjectLiteral{ LeftBrace: idx0, RightBrace: idx1, Value: value, } } func (p *parser) parseArrayLiteral() ast.Expression { idx0 := p.expect(token.LEFT_BRACKET) var value []ast.Expression for p.token != token.RIGHT_BRACKET && p.token != token.EOF { if p.token == token.COMMA { // This kind of comment requires a special empty expression node. empty := &ast.EmptyExpression{Begin: p.idx, End: p.idx} if p.mode&StoreComments != 0 { p.comments.SetExpression(empty) p.comments.Unset() } value = append(value, empty) p.next() continue } exp := p.parseAssignmentExpression() value = append(value, exp) if p.token != token.RIGHT_BRACKET { if p.mode&StoreComments != 0 { p.comments.Unset() } p.expect(token.COMMA) } } if p.mode&StoreComments != 0 { p.comments.MarkComments(ast.FINAL) } idx1 := p.expect(token.RIGHT_BRACKET) return &ast.ArrayLiteral{ LeftBracket: idx0, RightBracket: idx1, Value: value, } } func (p *parser) parseArgumentList() (argumentList []ast.Expression, idx0, idx1 file.Idx) { //nolint:nonamedreturns if p.mode&StoreComments != 0 { p.comments.Unset() } idx0 = p.expect(token.LEFT_PARENTHESIS) for p.token != token.RIGHT_PARENTHESIS { exp := p.parseAssignmentExpression() if p.mode&StoreComments != 0 { p.comments.SetExpression(exp) } argumentList = append(argumentList, exp) if p.token != token.COMMA { break } if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() } if p.mode&StoreComments != 0 { p.comments.Unset() } idx1 = p.expect(token.RIGHT_PARENTHESIS) return } func (p *parser) parseCallExpression(left ast.Expression) ast.Expression { argumentList, idx0, idx1 := p.parseArgumentList() exp := &ast.CallExpression{ Callee: left, LeftParenthesis: idx0, ArgumentList: argumentList, RightParenthesis: idx1, } if p.mode&StoreComments != 0 { p.comments.SetExpression(exp) } return exp } func (p *parser) parseDotMember(left ast.Expression) ast.Expression { period := p.expect(token.PERIOD) literal := p.literal idx := p.idx if !matchIdentifier.MatchString(literal) { p.expect(token.IDENTIFIER) p.nextStatement() return &ast.BadExpression{From: period, To: p.idx} } p.next() return &ast.DotExpression{ Left: left, Identifier: &ast.Identifier{ Idx: idx, Name: literal, }, } } func (p *parser) parseBracketMember(left ast.Expression) ast.Expression { idx0 := p.expect(token.LEFT_BRACKET) member := p.parseExpression() idx1 := p.expect(token.RIGHT_BRACKET) return &ast.BracketExpression{ LeftBracket: idx0, Left: left, Member: member, RightBracket: idx1, } } func (p *parser) parseNewExpression() ast.Expression { idx := p.expect(token.NEW) callee := p.parseLeftHandSideExpression() node := &ast.NewExpression{ New: idx, Callee: callee, } if p.token == token.LEFT_PARENTHESIS { argumentList, idx0, idx1 := p.parseArgumentList() node.ArgumentList = argumentList node.LeftParenthesis = idx0 node.RightParenthesis = idx1 } if p.mode&StoreComments != 0 { p.comments.SetExpression(node) } return node } func (p *parser) parseLeftHandSideExpression() ast.Expression { var left ast.Expression if p.token == token.NEW { left = p.parseNewExpression() } else { if p.mode&StoreComments != 0 { p.comments.MarkComments(ast.LEADING) p.comments.MarkPrimary() } left = p.parsePrimaryExpression() } if p.mode&StoreComments != 0 { p.comments.SetExpression(left) } for { switch p.token { case token.PERIOD: left = p.parseDotMember(left) case token.LEFT_BRACKET: left = p.parseBracketMember(left) default: return left } } } func (p *parser) parseLeftHandSideExpressionAllowCall() ast.Expression { allowIn := p.scope.allowIn p.scope.allowIn = true defer func() { p.scope.allowIn = allowIn }() var left ast.Expression if p.token == token.NEW { var newComments []*ast.Comment if p.mode&StoreComments != 0 { newComments = p.comments.FetchAll() p.comments.MarkComments(ast.LEADING) p.comments.MarkPrimary() } left = p.parseNewExpression() if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(left, newComments, ast.LEADING) } } else { if p.mode&StoreComments != 0 { p.comments.MarkComments(ast.LEADING) p.comments.MarkPrimary() } left = p.parsePrimaryExpression() } if p.mode&StoreComments != 0 { p.comments.SetExpression(left) } for { switch p.token { case token.PERIOD: left = p.parseDotMember(left) case token.LEFT_BRACKET: left = p.parseBracketMember(left) case token.LEFT_PARENTHESIS: left = p.parseCallExpression(left) default: return left } } } func (p *parser) parsePostfixExpression() ast.Expression { operand := p.parseLeftHandSideExpressionAllowCall() switch p.token { case token.INCREMENT, token.DECREMENT: // Make sure there is no line terminator here if p.implicitSemicolon { break } tkn := p.token idx := p.idx if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() switch operand.(type) { case *ast.Identifier, *ast.DotExpression, *ast.BracketExpression: default: p.error(idx, "invalid left-hand side in assignment") p.nextStatement() return &ast.BadExpression{From: idx, To: p.idx} } exp := &ast.UnaryExpression{ Operator: tkn, Idx: idx, Operand: operand, Postfix: true, } if p.mode&StoreComments != 0 { p.comments.SetExpression(exp) } return exp } return operand } func (p *parser) parseUnaryExpression() ast.Expression { switch p.token { case token.PLUS, token.MINUS, token.NOT, token.BITWISE_NOT: fallthrough case token.DELETE, token.VOID, token.TYPEOF: tkn := p.token idx := p.idx if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() return &ast.UnaryExpression{ Operator: tkn, Idx: idx, Operand: p.parseUnaryExpression(), } case token.INCREMENT, token.DECREMENT: tkn := p.token idx := p.idx if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() operand := p.parseUnaryExpression() switch operand.(type) { case *ast.Identifier, *ast.DotExpression, *ast.BracketExpression: default: p.error(idx, "invalid left-hand side in assignment") p.nextStatement() return &ast.BadExpression{From: idx, To: p.idx} } return &ast.UnaryExpression{ Operator: tkn, Idx: idx, Operand: operand, } } return p.parsePostfixExpression() } func (p *parser) parseMultiplicativeExpression() ast.Expression { next := p.parseUnaryExpression left := next() for p.token == token.MULTIPLY || p.token == token.SLASH || p.token == token.REMAINDER { tkn := p.token if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() left = &ast.BinaryExpression{ Operator: tkn, Left: left, Right: next(), } } return left } func (p *parser) parseAdditiveExpression() ast.Expression { next := p.parseMultiplicativeExpression left := next() for p.token == token.PLUS || p.token == token.MINUS { tkn := p.token if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() left = &ast.BinaryExpression{ Operator: tkn, Left: left, Right: next(), } } return left } func (p *parser) parseShiftExpression() ast.Expression { next := p.parseAdditiveExpression left := next() for p.token == token.SHIFT_LEFT || p.token == token.SHIFT_RIGHT || p.token == token.UNSIGNED_SHIFT_RIGHT { tkn := p.token if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() left = &ast.BinaryExpression{ Operator: tkn, Left: left, Right: next(), } } return left } func (p *parser) parseRelationalExpression() ast.Expression { next := p.parseShiftExpression left := next() allowIn := p.scope.allowIn p.scope.allowIn = true defer func() { p.scope.allowIn = allowIn }() switch p.token { case token.LESS, token.LESS_OR_EQUAL, token.GREATER, token.GREATER_OR_EQUAL: tkn := p.token if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() exp := &ast.BinaryExpression{ Operator: tkn, Left: left, Right: p.parseRelationalExpression(), Comparison: true, } return exp case token.INSTANCEOF: tkn := p.token if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() exp := &ast.BinaryExpression{ Operator: tkn, Left: left, Right: p.parseRelationalExpression(), } return exp case token.IN: if !allowIn { return left } tkn := p.token if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() exp := &ast.BinaryExpression{ Operator: tkn, Left: left, Right: p.parseRelationalExpression(), } return exp } return left } func (p *parser) parseEqualityExpression() ast.Expression { next := p.parseRelationalExpression left := next() for p.token == token.EQUAL || p.token == token.NOT_EQUAL || p.token == token.STRICT_EQUAL || p.token == token.STRICT_NOT_EQUAL { tkn := p.token if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() left = &ast.BinaryExpression{ Operator: tkn, Left: left, Right: next(), Comparison: true, } } return left } func (p *parser) parseBitwiseAndExpression() ast.Expression { next := p.parseEqualityExpression left := next() for p.token == token.AND { if p.mode&StoreComments != 0 { p.comments.Unset() } tkn := p.token p.next() left = &ast.BinaryExpression{ Operator: tkn, Left: left, Right: next(), } } return left } func (p *parser) parseBitwiseExclusiveOrExpression() ast.Expression { next := p.parseBitwiseAndExpression left := next() for p.token == token.EXCLUSIVE_OR { if p.mode&StoreComments != 0 { p.comments.Unset() } tkn := p.token p.next() left = &ast.BinaryExpression{ Operator: tkn, Left: left, Right: next(), } } return left } func (p *parser) parseBitwiseOrExpression() ast.Expression { next := p.parseBitwiseExclusiveOrExpression left := next() for p.token == token.OR { if p.mode&StoreComments != 0 { p.comments.Unset() } tkn := p.token p.next() left = &ast.BinaryExpression{ Operator: tkn, Left: left, Right: next(), } } return left } func (p *parser) parseLogicalAndExpression() ast.Expression { next := p.parseBitwiseOrExpression left := next() for p.token == token.LOGICAL_AND { if p.mode&StoreComments != 0 { p.comments.Unset() } tkn := p.token p.next() left = &ast.BinaryExpression{ Operator: tkn, Left: left, Right: next(), } } return left } func (p *parser) parseLogicalOrExpression() ast.Expression { next := p.parseLogicalAndExpression left := next() for p.token == token.LOGICAL_OR { if p.mode&StoreComments != 0 { p.comments.Unset() } tkn := p.token p.next() left = &ast.BinaryExpression{ Operator: tkn, Left: left, Right: next(), } } return left } func (p *parser) parseConditionalExpression() ast.Expression { left := p.parseLogicalOrExpression() if p.token == token.QUESTION_MARK { if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() consequent := p.parseAssignmentExpression() if p.mode&StoreComments != 0 { p.comments.Unset() } p.expect(token.COLON) exp := &ast.ConditionalExpression{ Test: left, Consequent: consequent, Alternate: p.parseAssignmentExpression(), } return exp } return left } func (p *parser) parseAssignmentExpression() ast.Expression { left := p.parseConditionalExpression() var operator token.Token switch p.token { case token.ASSIGN: operator = p.token case token.ADD_ASSIGN: operator = token.PLUS case token.SUBTRACT_ASSIGN: operator = token.MINUS case token.MULTIPLY_ASSIGN: operator = token.MULTIPLY case token.QUOTIENT_ASSIGN: operator = token.SLASH case token.REMAINDER_ASSIGN: operator = token.REMAINDER case token.AND_ASSIGN: operator = token.AND case token.AND_NOT_ASSIGN: operator = token.AND_NOT case token.OR_ASSIGN: operator = token.OR case token.EXCLUSIVE_OR_ASSIGN: operator = token.EXCLUSIVE_OR case token.SHIFT_LEFT_ASSIGN: operator = token.SHIFT_LEFT case token.SHIFT_RIGHT_ASSIGN: operator = token.SHIFT_RIGHT case token.UNSIGNED_SHIFT_RIGHT_ASSIGN: operator = token.UNSIGNED_SHIFT_RIGHT } if operator != 0 { idx := p.idx if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() switch left.(type) { case *ast.Identifier, *ast.DotExpression, *ast.BracketExpression: default: p.error(left.Idx0(), "invalid left-hand side in assignment") p.nextStatement() return &ast.BadExpression{From: idx, To: p.idx} } exp := &ast.AssignExpression{ Left: left, Operator: operator, Right: p.parseAssignmentExpression(), } if p.mode&StoreComments != 0 { p.comments.SetExpression(exp) } return exp } return left } func (p *parser) parseExpression() ast.Expression { next := p.parseAssignmentExpression left := next() if p.token == token.COMMA { sequence := []ast.Expression{left} for { if p.token != token.COMMA { break } p.next() sequence = append(sequence, next()) } return &ast.SequenceExpression{ Sequence: sequence, } } return left } ================================================ FILE: parser/lexer.go ================================================ package parser import ( "bytes" "errors" "fmt" "regexp" "strconv" "strings" "unicode" "unicode/utf8" "github.com/robertkrimen/otto/ast" "github.com/robertkrimen/otto/file" "github.com/robertkrimen/otto/token" ) type chr struct { //nolint:unused value rune width int } var matchIdentifier = regexp.MustCompile(`^[$_\p{L}][$_\p{L}\d}]*$`) func isDecimalDigit(chr rune) bool { return '0' <= chr && chr <= '9' } func digitValue(chr rune) int { switch { case '0' <= chr && chr <= '9': return int(chr - '0') case 'a' <= chr && chr <= 'f': return int(chr - 'a' + 10) case 'A' <= chr && chr <= 'F': return int(chr - 'A' + 10) } return 16 // Larger than any legal digit value } // See https://www.unicode.org/reports/tr31/ for reference on ID_Start and ID_Continue. var includeIDStart = []*unicode.RangeTable{ unicode.Lu, unicode.Ll, unicode.Lt, unicode.Lm, unicode.Lo, unicode.Nl, unicode.Other_ID_Start, } var includeIDContinue = []*unicode.RangeTable{ unicode.Lu, unicode.Ll, unicode.Lt, unicode.Lm, unicode.Lo, unicode.Nl, unicode.Other_ID_Start, unicode.Mn, unicode.Mc, unicode.Nd, unicode.Pc, unicode.Other_ID_Continue, } var exclude = []*unicode.RangeTable{ unicode.Pattern_Syntax, unicode.Pattern_White_Space, } func unicodeIDStart(r rune) bool { if unicode.In(r, exclude...) { return false } return unicode.In(r, includeIDStart...) } func unicodeIDContinue(r rune) bool { if unicode.In(r, exclude...) { return false } return unicode.In(r, includeIDContinue...) } func isDigit(chr rune, base int) bool { return digitValue(chr) < base } func isIdentifierStart(chr rune) bool { return chr == '$' || chr == '_' || chr == '\\' || 'a' <= chr && chr <= 'z' || 'A' <= chr && chr <= 'Z' || chr >= utf8.RuneSelf && unicodeIDStart(chr) } func isIdentifierPart(chr rune) bool { return chr == '$' || chr == '_' || chr == '\\' || 'a' <= chr && chr <= 'z' || 'A' <= chr && chr <= 'Z' || '0' <= chr && chr <= '9' || chr >= utf8.RuneSelf && unicodeIDContinue(chr) } func (p *parser) scanIdentifier() (string, error) { offset := p.chrOffset parse := false for isIdentifierPart(p.chr) { if p.chr == '\\' { distance := p.chrOffset - offset p.read() if p.chr != 'u' { return "", fmt.Errorf("invalid identifier escape character: %c (%s)", p.chr, string(p.chr)) } parse = true var value rune for range 4 { p.read() decimal, ok := hex2decimal(byte(p.chr)) if !ok { return "", fmt.Errorf("invalid identifier escape character: %c (%s)", p.chr, string(p.chr)) } value = value<<4 | decimal } switch { case value == '\\': return "", fmt.Errorf("invalid identifier escape value: %c (%s)", value, string(value)) case distance == 0: if !isIdentifierStart(value) { return "", fmt.Errorf("invalid identifier escape value: %c (%s)", value, string(value)) } case distance > 0: if !isIdentifierPart(value) { return "", fmt.Errorf("invalid identifier escape value: %c (%s)", value, string(value)) } } } p.read() } literal := p.str[offset:p.chrOffset] if parse { return parseStringLiteral(literal) } return literal, nil } // 7.2. func isLineWhiteSpace(chr rune) bool { //nolint:unused, deadcode switch chr { case '\u0009', '\u000b', '\u000c', '\u0020', '\u00a0', '\ufeff': return true case '\u000a', '\u000d', '\u2028', '\u2029': return false case '\u0085': return false } return unicode.IsSpace(chr) } // 7.3. func isLineTerminator(chr rune) bool { switch chr { case '\u000a', '\u000d', '\u2028', '\u2029': return true } return false } func (p *parser) scan() (tkn token.Token, literal string, idx file.Idx) { //nolint:nonamedreturns p.implicitSemicolon = false for { p.skipWhiteSpace() idx = p.idxOf(p.chrOffset) insertSemicolon := false switch chr := p.chr; { case isIdentifierStart(chr): var err error literal, err = p.scanIdentifier() if err != nil { tkn = token.ILLEGAL break } if len(literal) > 1 { // Keywords are longer than 1 character, avoid lookup otherwise var strict bool tkn, strict = token.IsKeyword(literal) switch tkn { case 0: // Not a keyword switch literal { case "true", "false": p.insertSemicolon = true return token.BOOLEAN, literal, idx case "null": p.insertSemicolon = true return token.NULL, literal, idx } case token.KEYWORD: if strict { // TODO If strict and in strict mode, then this is not a break break } return token.KEYWORD, literal, idx case token.THIS, token.BREAK, token.THROW, // A newline after a throw is not allowed, but we need to detect it token.RETURN, token.CONTINUE, token.DEBUGGER: p.insertSemicolon = true return tkn, literal, idx default: return tkn, literal, idx } } p.insertSemicolon = true return token.IDENTIFIER, literal, idx case '0' <= chr && chr <= '9': p.insertSemicolon = true tkn, literal = p.scanNumericLiteral(false) return tkn, literal, idx default: p.read() switch chr { case -1: if p.insertSemicolon { p.insertSemicolon = false p.implicitSemicolon = true } tkn = token.EOF case '\r', '\n', '\u2028', '\u2029': p.insertSemicolon = false p.implicitSemicolon = true p.comments.AtLineBreak() continue case ':': tkn = token.COLON case '.': if digitValue(p.chr) < 10 { insertSemicolon = true tkn, literal = p.scanNumericLiteral(true) } else { tkn = token.PERIOD } case ',': tkn = token.COMMA case ';': tkn = token.SEMICOLON case '(': tkn = token.LEFT_PARENTHESIS case ')': tkn = token.RIGHT_PARENTHESIS insertSemicolon = true case '[': tkn = token.LEFT_BRACKET case ']': tkn = token.RIGHT_BRACKET insertSemicolon = true case '{': tkn = token.LEFT_BRACE case '}': tkn = token.RIGHT_BRACE insertSemicolon = true case '+': tkn = p.switch3(token.PLUS, token.ADD_ASSIGN, '+', token.INCREMENT) if tkn == token.INCREMENT { insertSemicolon = true } case '-': tkn = p.switch3(token.MINUS, token.SUBTRACT_ASSIGN, '-', token.DECREMENT) if tkn == token.DECREMENT { insertSemicolon = true } case '*': tkn = p.switch2(token.MULTIPLY, token.MULTIPLY_ASSIGN) case '/': switch p.chr { case '/': if p.mode&StoreComments != 0 { comment := string(p.readSingleLineComment()) p.comments.AddComment(ast.NewComment(comment, idx)) continue } p.skipSingleLineComment() continue case '*': if p.mode&StoreComments != 0 { comment := string(p.readMultiLineComment()) p.comments.AddComment(ast.NewComment(comment, idx)) continue } p.skipMultiLineComment() continue default: // Could be division, could be RegExp literal tkn = p.switch2(token.SLASH, token.QUOTIENT_ASSIGN) insertSemicolon = true } case '%': tkn = p.switch2(token.REMAINDER, token.REMAINDER_ASSIGN) case '^': tkn = p.switch2(token.EXCLUSIVE_OR, token.EXCLUSIVE_OR_ASSIGN) case '<': tkn = p.switch4(token.LESS, token.LESS_OR_EQUAL, '<', token.SHIFT_LEFT, token.SHIFT_LEFT_ASSIGN) case '>': tkn = p.switch6(token.GREATER, token.GREATER_OR_EQUAL, '>', token.SHIFT_RIGHT, token.SHIFT_RIGHT_ASSIGN, '>', token.UNSIGNED_SHIFT_RIGHT, token.UNSIGNED_SHIFT_RIGHT_ASSIGN) case '=': tkn = p.switch2(token.ASSIGN, token.EQUAL) if tkn == token.EQUAL && p.chr == '=' { p.read() tkn = token.STRICT_EQUAL } case '!': tkn = p.switch2(token.NOT, token.NOT_EQUAL) if tkn == token.NOT_EQUAL && p.chr == '=' { p.read() tkn = token.STRICT_NOT_EQUAL } case '&': if p.chr == '^' { p.read() tkn = p.switch2(token.AND_NOT, token.AND_NOT_ASSIGN) } else { tkn = p.switch3(token.AND, token.AND_ASSIGN, '&', token.LOGICAL_AND) } case '|': tkn = p.switch3(token.OR, token.OR_ASSIGN, '|', token.LOGICAL_OR) case '~': tkn = token.BITWISE_NOT case '?': tkn = token.QUESTION_MARK case '"', '\'': insertSemicolon = true tkn = token.STRING var err error literal, err = p.scanString(p.chrOffset - 1) if err != nil { tkn = token.ILLEGAL } default: p.errorUnexpected(idx, chr) tkn = token.ILLEGAL } } p.insertSemicolon = insertSemicolon return tkn, literal, idx } } func (p *parser) switch2(tkn0, tkn1 token.Token) token.Token { if p.chr == '=' { p.read() return tkn1 } return tkn0 } func (p *parser) switch3(tkn0, tkn1 token.Token, chr2 rune, tkn2 token.Token) token.Token { if p.chr == '=' { p.read() return tkn1 } if p.chr == chr2 { p.read() return tkn2 } return tkn0 } func (p *parser) switch4(tkn0, tkn1 token.Token, chr2 rune, tkn2, tkn3 token.Token) token.Token { if p.chr == '=' { p.read() return tkn1 } if p.chr == chr2 { p.read() if p.chr == '=' { p.read() return tkn3 } return tkn2 } return tkn0 } func (p *parser) switch6(tkn0, tkn1 token.Token, chr2 rune, tkn2, tkn3 token.Token, chr3 rune, tkn4, tkn5 token.Token) token.Token { if p.chr == '=' { p.read() return tkn1 } if p.chr == chr2 { p.read() if p.chr == '=' { p.read() return tkn3 } if p.chr == chr3 { p.read() if p.chr == '=' { p.read() return tkn5 } return tkn4 } return tkn2 } return tkn0 } func (p *parser) chrAt(index int) chr { //nolint:unused value, width := utf8.DecodeRuneInString(p.str[index:]) return chr{ value: value, width: width, } } func (p *parser) peek() rune { if p.offset+1 < p.length { return rune(p.str[p.offset+1]) } return -1 } func (p *parser) read() { if p.offset < p.length { p.chrOffset = p.offset chr, width := rune(p.str[p.offset]), 1 if chr >= utf8.RuneSelf { // !ASCII chr, width = utf8.DecodeRuneInString(p.str[p.offset:]) if chr == utf8.RuneError && width == 1 { p.error(p.chrOffset, "Invalid UTF-8 character") } } p.offset += width p.chr = chr } else { p.chrOffset = p.length p.chr = -1 // EOF } } // This is here since the functions are so similar. func (p *regExpParser) read() { if p.offset < p.length { p.chrOffset = p.offset chr, width := rune(p.str[p.offset]), 1 if chr >= utf8.RuneSelf { // !ASCII chr, width = utf8.DecodeRuneInString(p.str[p.offset:]) if chr == utf8.RuneError && width == 1 { p.error(p.chrOffset, "Invalid UTF-8 character") } } p.offset += width p.chr = chr } else { p.chrOffset = p.length p.chr = -1 // EOF } } func (p *parser) readSingleLineComment() []rune { var result []rune for p.chr != -1 { p.read() if isLineTerminator(p.chr) { return result } result = append(result, p.chr) } // Get rid of the trailing -1 return result[:len(result)-1] } func (p *parser) readMultiLineComment() []rune { var result []rune p.read() for p.chr >= 0 { chr := p.chr p.read() if chr == '*' && p.chr == '/' { p.read() return result } result = append(result, chr) } p.errorUnexpected(0, p.chr) return result } func (p *parser) skipSingleLineComment() { for p.chr != -1 { p.read() if isLineTerminator(p.chr) { return } } } func (p *parser) skipMultiLineComment() { p.read() for p.chr >= 0 { chr := p.chr p.read() if chr == '*' && p.chr == '/' { p.read() return } } p.errorUnexpected(0, p.chr) } func (p *parser) skipWhiteSpace() { for { switch p.chr { case ' ', '\t', '\f', '\v', '\u00a0', '\ufeff': p.read() continue case '\r': if p.peek() == '\n' { p.comments.AtLineBreak() p.read() } fallthrough case '\u2028', '\u2029', '\n': if p.insertSemicolon { return } p.comments.AtLineBreak() p.read() continue } if p.chr >= utf8.RuneSelf { if unicode.IsSpace(p.chr) { p.read() continue } } break } } func (p *parser) scanMantissa(base int) { for digitValue(p.chr) < base { p.read() } } func (p *parser) scanEscape(quote rune) { var length, base uint32 switch p.chr { // Octal: // length, base, limit = 3, 8, 255 case 'a', 'b', 'f', 'n', 'r', 't', 'v', '\\', '"', '\'', '0': p.read() return case '\r', '\n', '\u2028', '\u2029': p.scanNewline() return case 'x': p.read() length, base = 2, 16 case 'u': p.read() length, base = 4, 16 default: p.read() // Always make progress return } var value uint32 for ; length > 0 && p.chr != quote && p.chr >= 0; length-- { digit := uint32(digitValue(p.chr)) if digit >= base { break } value = value*base + digit p.read() } } func (p *parser) scanString(offset int) (string, error) { // " ' / quote := rune(p.str[offset]) for p.chr != quote { chr := p.chr if chr == '\n' || chr == '\r' || chr == '\u2028' || chr == '\u2029' || chr < 0 { goto newline } p.read() switch { case chr == '\\': if quote == '/' { if p.chr == '\n' || p.chr == '\r' || p.chr == '\u2028' || p.chr == '\u2029' || p.chr < 0 { goto newline } p.read() } else { p.scanEscape(quote) } case chr == '[' && quote == '/': // Allow a slash (/) in a bracket character class ([...]) // TODO Fix this, this is hacky... quote = -1 case chr == ']' && quote == -1: quote = '/' } } // " ' / p.read() return p.str[offset:p.chrOffset], nil newline: p.scanNewline() err := "String not terminated" if quote == '/' { err = "Invalid regular expression: missing /" p.error(p.idxOf(offset), err) } return "", errors.New(err) } func (p *parser) scanNewline() { if p.chr == '\r' { p.read() if p.chr != '\n' { return } } p.read() } func hex2decimal(chr byte) (rune, bool) { r := rune(chr) switch { case '0' <= r && r <= '9': return r - '0', true case 'a' <= r && r <= 'f': return r - 'a' + 10, true case 'A' <= r && r <= 'F': return r - 'A' + 10, true default: return 0, false } } func parseNumberLiteral(literal string) (value interface{}, err error) { //nolint:nonamedreturns // TODO Is Uint okay? What about -MAX_UINT value, err = strconv.ParseInt(literal, 0, 64) if err == nil { return value, nil } parseIntErr := err // Save this first error, just in case value, err = strconv.ParseFloat(literal, 64) if err == nil { return value, nil } else if errors.Is(err, strconv.ErrRange) { // Infinity, etc. return value, nil } // TODO(steve): Fix as this is assigning to err so we know the type. // Need to understand what this was trying to do? err = parseIntErr if errors.Is(err, strconv.ErrRange) { if len(literal) > 2 && literal[0] == '0' && (literal[1] == 'X' || literal[1] == 'x') { // Could just be a very large number (e.g. 0x8000000000000000) var value float64 literal = literal[2:] for _, chr := range literal { digit := digitValue(chr) if digit >= 16 { return nil, fmt.Errorf("illegal numeric literal: %v (>= 16)", digit) } value = value*16 + float64(digit) } return value, nil } } return nil, errors.New("illegal numeric literal") } func parseStringLiteral(literal string) (string, error) { // Best case scenario... if literal == "" { return "", nil } // Slightly less-best case scenario... if !strings.ContainsRune(literal, '\\') { return literal, nil } str := literal buffer := bytes.NewBuffer(make([]byte, 0, 3*len(literal)/2)) for len(str) > 0 { switch chr := str[0]; { // We do not explicitly handle the case of the quote // value, which can be: " ' / // This assumes we're already passed a partially well-formed literal case chr >= utf8.RuneSelf: chr, size := utf8.DecodeRuneInString(str) buffer.WriteRune(chr) str = str[size:] continue case chr != '\\': buffer.WriteByte(chr) str = str[1:] continue } if len(str) <= 1 { panic("len(str) <= 1") } chr := str[1] var value rune if chr >= utf8.RuneSelf { str = str[1:] var size int value, size = utf8.DecodeRuneInString(str) str = str[size:] // \ + } else { str = str[2:] // \ switch chr { case 'b': value = '\b' case 'f': value = '\f' case 'n': value = '\n' case 'r': value = '\r' case 't': value = '\t' case 'v': value = '\v' case 'x', 'u': size := 0 switch chr { case 'x': size = 2 case 'u': size = 4 } if len(str) < size { return "", fmt.Errorf("invalid escape: \\%s: len(%q) != %d", string(chr), str, size) } for j := range size { decimal, ok := hex2decimal(str[j]) if !ok { return "", fmt.Errorf("invalid escape: \\%s: %q", string(chr), str[:size]) } value = value<<4 | decimal } str = str[size:] if chr == 'x' { break } if value > utf8.MaxRune { panic("value > utf8.MaxRune") } case '0': if len(str) == 0 || '0' > str[0] || str[0] > '7' { value = 0 break } fallthrough case '1', '2', '3', '4', '5', '6', '7': // TODO strict value = rune(chr) - '0' j := 0 for ; j < 2; j++ { if len(str) < j+1 { break } if ch := str[j]; '0' > ch || ch > '7' { break } decimal := rune(str[j]) - '0' value = (value << 3) | decimal } str = str[j:] case '\\': value = '\\' case '\'', '"': value = rune(chr) case '\r': if len(str) > 0 { if str[0] == '\n' { str = str[1:] } } fallthrough case '\n': continue default: value = rune(chr) } } buffer.WriteRune(value) } return buffer.String(), nil } func (p *parser) scanNumericLiteral(decimalPoint bool) (token.Token, string) { offset := p.chrOffset tkn := token.NUMBER if decimalPoint { offset-- p.scanMantissa(10) goto exponent } if p.chr == '0' { chrOffset := p.chrOffset p.read() switch p.chr { case 'x', 'X': // Hexadecimal p.read() if isDigit(p.chr, 16) { p.read() } else { return token.ILLEGAL, p.str[chrOffset:p.chrOffset] } p.scanMantissa(16) if p.chrOffset-chrOffset <= 2 { // Only "0x" or "0X" p.error(0, "Illegal hexadecimal number") } goto hexadecimal case '.': // Float goto float default: // Octal, Float if p.chr == 'e' || p.chr == 'E' { goto exponent } p.scanMantissa(8) if p.chr == '8' || p.chr == '9' { return token.ILLEGAL, p.str[chrOffset:p.chrOffset] } goto octal } } p.scanMantissa(10) float: if p.chr == '.' { p.read() p.scanMantissa(10) } exponent: if p.chr == 'e' || p.chr == 'E' { p.read() if p.chr == '-' || p.chr == '+' { p.read() } if isDecimalDigit(p.chr) { p.read() p.scanMantissa(10) } else { return token.ILLEGAL, p.str[offset:p.chrOffset] } } hexadecimal: octal: if isIdentifierStart(p.chr) || isDecimalDigit(p.chr) { return token.ILLEGAL, p.str[offset:p.chrOffset] } return tkn, p.str[offset:p.chrOffset] } ================================================ FILE: parser/lexer_test.go ================================================ package parser import ( "testing" "github.com/robertkrimen/otto/file" "github.com/robertkrimen/otto/terst" "github.com/robertkrimen/otto/token" ) var ( tt = terst.Terst is = terst.Is ) func TestLexer(t *testing.T) { tt(t, func() { setup := func(src string) *parser { parser := newParser("", src, 1, nil) return parser } test := func(src string, test ...interface{}) { parser := setup(src) for len(test) > 0 { tkn, literal, idx := parser.scan() if len(test) > 0 { is(tkn, test[0].(token.Token)) test = test[1:] } if len(test) > 0 { is(literal, test[0].(string)) test = test[1:] } if len(test) > 0 { // FIXME terst, Fix this so that cast to file.Idx is not necessary? is(idx, file.Idx(test[0].(int))) test = test[1:] } } } test("", token.EOF, "", 1, ) test("1", token.NUMBER, "1", 1, token.EOF, "", 2, ) test(".0", token.NUMBER, ".0", 1, token.EOF, "", 3, ) test("abc", token.IDENTIFIER, "abc", 1, token.EOF, "", 4, ) test("abc(1)", token.IDENTIFIER, "abc", 1, token.LEFT_PARENTHESIS, "", 4, token.NUMBER, "1", 5, token.RIGHT_PARENTHESIS, "", 6, token.EOF, "", 7, ) test(".", token.PERIOD, "", 1, token.EOF, "", 2, ) test("===.", token.STRICT_EQUAL, "", 1, token.PERIOD, "", 4, token.EOF, "", 5, ) test(">>>=.0", token.UNSIGNED_SHIFT_RIGHT_ASSIGN, "", 1, token.NUMBER, ".0", 5, token.EOF, "", 7, ) test(">>>=0.0.", token.UNSIGNED_SHIFT_RIGHT_ASSIGN, "", 1, token.NUMBER, "0.0", 5, token.PERIOD, "", 8, token.EOF, "", 9, ) test("\"abc\"", token.STRING, "\"abc\"", 1, token.EOF, "", 6, ) test("abc = //", token.IDENTIFIER, "abc", 1, token.ASSIGN, "", 5, token.EOF, "", 9, ) test("abc = /*test*/", token.IDENTIFIER, "abc", 1, token.ASSIGN, "", 5, token.EOF, "", 15, ) test("abc = 1 / 2", token.IDENTIFIER, "abc", 1, token.ASSIGN, "", 5, token.NUMBER, "1", 7, token.SLASH, "", 9, token.NUMBER, "2", 11, token.EOF, "", 12, ) test("xyzzy = 'Nothing happens.'", token.IDENTIFIER, "xyzzy", 1, token.ASSIGN, "", 7, token.STRING, "'Nothing happens.'", 9, token.EOF, "", 27, ) test("abc = !false", token.IDENTIFIER, "abc", 1, token.ASSIGN, "", 5, token.NOT, "", 7, token.BOOLEAN, "false", 8, token.EOF, "", 13, ) test("abc = !!true", token.IDENTIFIER, "abc", 1, token.ASSIGN, "", 5, token.NOT, "", 7, token.NOT, "", 8, token.BOOLEAN, "true", 9, token.EOF, "", 13, ) test("abc *= 1", token.IDENTIFIER, "abc", 1, token.MULTIPLY_ASSIGN, "", 5, token.NUMBER, "1", 8, token.EOF, "", 9, ) test("if 1 else", token.IF, "if", 1, token.NUMBER, "1", 4, token.ELSE, "else", 6, token.EOF, "", 10, ) test("null", token.NULL, "null", 1, token.EOF, "", 5, ) test(`"\u007a\x79\u000a\x78"`, token.STRING, "\"\\u007a\\x79\\u000a\\x78\"", 1, token.EOF, "", 23, ) test(`"[First line \ Second line \ Third line\ . ]" `, token.STRING, "\"[First line \\\nSecond line \\\n Third line\\\n. ]\"", 1, token.EOF, "", 53, ) test("/", token.SLASH, "", 1, token.EOF, "", 2, ) test("var abc = \"abc\uFFFFabc\"", token.VAR, "var", 1, token.IDENTIFIER, "abc", 5, token.ASSIGN, "", 9, token.STRING, "\"abc\uFFFFabc\"", 11, token.EOF, "", 22, ) test(`'\t' === '\r'`, token.STRING, "'\\t'", 1, token.STRICT_EQUAL, "", 6, token.STRING, "'\\r'", 10, token.EOF, "", 14, ) test(`var \u0024 = 1`, token.VAR, "var", 1, token.IDENTIFIER, "$", 5, token.ASSIGN, "", 12, token.NUMBER, "1", 14, token.EOF, "", 15, ) test("10e10000", token.NUMBER, "10e10000", 1, token.EOF, "", 9, ) test(`var if var class`, token.VAR, "var", 1, token.IF, "if", 5, token.VAR, "var", 8, token.KEYWORD, "class", 12, token.EOF, "", 17, ) test(`-0`, token.MINUS, "", 1, token.NUMBER, "0", 2, token.EOF, "", 3, ) test(`.01`, token.NUMBER, ".01", 1, token.EOF, "", 4, ) test(`.01e+2`, token.NUMBER, ".01e+2", 1, token.EOF, "", 7, ) test(";", token.SEMICOLON, "", 1, token.EOF, "", 2, ) test(";;", token.SEMICOLON, "", 1, token.SEMICOLON, "", 2, token.EOF, "", 3, ) test("//", token.EOF, "", 3, ) test(";;//test", token.SEMICOLON, "", 1, token.SEMICOLON, "", 2, token.EOF, "", 9, ) test("1", token.NUMBER, "1", 1, ) test("12 123", token.NUMBER, "12", 1, token.NUMBER, "123", 4, ) test("1.2 12.3", token.NUMBER, "1.2", 1, token.NUMBER, "12.3", 5, ) test("/ /=", token.SLASH, "", 1, token.QUOTIENT_ASSIGN, "", 3, ) test(`"abc"`, token.STRING, `"abc"`, 1, ) test(`'abc'`, token.STRING, `'abc'`, 1, ) test("++", token.INCREMENT, "", 1, ) test(">", token.GREATER, "", 1, ) test(">=", token.GREATER_OR_EQUAL, "", 1, ) test(">>", token.SHIFT_RIGHT, "", 1, ) test(">>=", token.SHIFT_RIGHT_ASSIGN, "", 1, ) test(">>>", token.UNSIGNED_SHIFT_RIGHT, "", 1, ) test(">>>=", token.UNSIGNED_SHIFT_RIGHT_ASSIGN, "", 1, ) test("1 \"abc\"", token.NUMBER, "1", 1, token.STRING, "\"abc\"", 3, ) test(",", token.COMMA, "", 1, ) test("1, \"abc\"", token.NUMBER, "1", 1, token.COMMA, "", 2, token.STRING, "\"abc\"", 4, ) test("new abc(1, 3.14159);", token.NEW, "new", 1, token.IDENTIFIER, "abc", 5, token.LEFT_PARENTHESIS, "", 8, token.NUMBER, "1", 9, token.COMMA, "", 10, token.NUMBER, "3.14159", 12, token.RIGHT_PARENTHESIS, "", 19, token.SEMICOLON, "", 20, ) test("1 == \"1\"", token.NUMBER, "1", 1, token.EQUAL, "", 3, token.STRING, "\"1\"", 6, ) test("1\n[]\n", token.NUMBER, "1", 1, token.LEFT_BRACKET, "", 3, token.RIGHT_BRACKET, "", 4, ) test("1\ufeff[]\ufeff", token.NUMBER, "1", 1, token.LEFT_BRACKET, "", 5, token.RIGHT_BRACKET, "", 6, ) // Identifier from Unicode Nl test("\u16ee", token.IDENTIFIER, "ᛮ", 1, ) // Identifier from Unicode Other_ID_Start test("\u212e", token.IDENTIFIER, "℮", 1, ) // Using char from ID_Continue after valid start char test("a\u0300", token.IDENTIFIER, "à", 1, ) // ILLEGAL test(`3ea`, token.ILLEGAL, "3e", 1, token.IDENTIFIER, "a", 3, token.EOF, "", 4, ) test(`3in`, token.ILLEGAL, "3", 1, token.IN, "in", 2, token.EOF, "", 4, ) test("\"Hello\nWorld\"", token.ILLEGAL, "", 1, token.IDENTIFIER, "World", 8, token.ILLEGAL, "", 13, token.EOF, "", 14, ) test("\u203f = 10", token.ILLEGAL, "", 1, token.ASSIGN, "", 5, token.NUMBER, "10", 7, token.EOF, "", 9, ) test(`"\x0G"`, token.STRING, "\"\\x0G\"", 1, token.EOF, "", 7, ) // Starting identifier with ID_Continue char from Nm test("\u0300", token.ILLEGAL, ) // Starting identifier with Pattern_Syntax test("'", token.ILLEGAL, ) }) } ================================================ FILE: parser/marshal_test.go ================================================ package parser import ( "bytes" "encoding/json" "fmt" "os" "reflect" "strings" "testing" "github.com/robertkrimen/otto/ast" "github.com/stretchr/testify/require" ) func marshal(name string, children ...interface{}) interface{} { if len(children) == 1 { if name == "" { return testMarshalNode(children[0]) } return map[string]interface{}{ name: children[0], } } ret := map[string]interface{}{} length := len(children) / 2 for i := range length { name := children[i*2].(string) value := children[i*2+1] ret[name] = value } if name == "" { return ret } return map[string]interface{}{ name: ret, } } func testMarshalNode(node interface{}) interface{} { switch node := node.(type) { // Expression case *ast.ArrayLiteral: return marshal("Array", testMarshalNode(node.Value)) case *ast.AssignExpression: return marshal("Assign", "Left", testMarshalNode(node.Left), "Right", testMarshalNode(node.Right), ) case *ast.BinaryExpression: return marshal("BinaryExpression", "Operator", node.Operator.String(), "Left", testMarshalNode(node.Left), "Right", testMarshalNode(node.Right), ) case *ast.BooleanLiteral: return marshal("Literal", node.Value) case *ast.CallExpression: return marshal("Call", "Callee", testMarshalNode(node.Callee), "ArgumentList", testMarshalNode(node.ArgumentList), ) case *ast.ConditionalExpression: return marshal("Conditional", "Test", testMarshalNode(node.Test), "Consequent", testMarshalNode(node.Consequent), "Alternate", testMarshalNode(node.Alternate), ) case *ast.DotExpression: return marshal("Dot", "Left", testMarshalNode(node.Left), "Member", node.Identifier.Name, ) case *ast.NewExpression: return marshal("New", "Callee", testMarshalNode(node.Callee), "ArgumentList", testMarshalNode(node.ArgumentList), ) case *ast.NullLiteral: return marshal("Literal", nil) case *ast.NumberLiteral: return marshal("Literal", node.Value) case *ast.ObjectLiteral: return marshal("Object", testMarshalNode(node.Value)) case *ast.RegExpLiteral: return marshal("Literal", node.Literal) case *ast.StringLiteral: return marshal("Literal", node.Literal) case *ast.VariableExpression: return []interface{}{node.Name, testMarshalNode(node.Initializer)} // Statement case *ast.Program: return testMarshalNode(node.Body) case *ast.BlockStatement: return marshal("BlockStatement", testMarshalNode(node.List)) case *ast.EmptyStatement: return "EmptyStatement" case *ast.ExpressionStatement: return testMarshalNode(node.Expression) case *ast.ForInStatement: return marshal("ForIn", "Into", marshal("", node.Into), "Source", marshal("", node.Source), "Body", marshal("", node.Body), ) case *ast.FunctionLiteral: return marshal("Function", testMarshalNode(node.Body)) case *ast.Identifier: return marshal("Identifier", node.Name) case *ast.IfStatement: ret := marshal("", "Test", testMarshalNode(node.Test), "Consequent", testMarshalNode(node.Consequent), ).(map[string]interface{}) if node.Alternate != nil { ret["Alternate"] = testMarshalNode(node.Alternate) } return marshal("If", ret) case *ast.LabelledStatement: return marshal("Label", "Name", node.Label.Name, "Statement", testMarshalNode(node.Statement), ) case ast.Property: return marshal("", "Key", node.Key, "Value", testMarshalNode(node.Value), ) case *ast.ReturnStatement: return marshal("Return", testMarshalNode(node.Argument)) case *ast.SequenceExpression: return marshal("Sequence", testMarshalNode(node.Sequence)) case *ast.ThrowStatement: return marshal("Throw", testMarshalNode(node.Argument)) case *ast.VariableStatement: return marshal("Var", testMarshalNode(node.List)) } { value := reflect.ValueOf(node) if value.Kind() == reflect.Slice { tmp0 := []interface{}{} for index := range value.Len() { tmp0 = append(tmp0, testMarshalNode(value.Index(index).Interface())) } return tmp0 } } if node != nil { fmt.Fprintf(os.Stderr, "testMarshalNode(%T)\n", node) } return nil } func testMarshal(node interface{}) string { value, err := json.Marshal(testMarshalNode(node)) if err != nil { panic(err) } return string(value) } func TestParserAST(t *testing.T) { tt(t, func() { test := func(inputOutput string) { match := matchBeforeAfterSeparator.FindStringIndex(inputOutput) input := strings.TrimSpace(inputOutput[0:match[0]]) wantOutput := strings.TrimSpace(inputOutput[match[1]:]) _, program, err := testParse(input) is(err, nil) haveOutput := testMarshal(program) tmp0, tmp1 := bytes.Buffer{}, bytes.Buffer{} err = json.Indent(&tmp0, []byte(haveOutput), "\t\t", " ") require.NoError(t, err) err = json.Indent(&tmp1, []byte(wantOutput), "\t\t", " ") require.NoError(t, err) is("\n\t\t"+tmp0.String(), "\n\t\t"+tmp1.String()) } test(` --- [] `) test(` ; --- [ "EmptyStatement" ] `) test(` ;;; --- [ "EmptyStatement", "EmptyStatement", "EmptyStatement" ] `) test(` 1; true; abc; "abc"; null; --- [ { "Literal": 1 }, { "Literal": true }, { "Identifier": "abc" }, { "Literal": "\"abc\"" }, { "Literal": null } ] `) test(` { 1; null; 3.14159; ; } --- [ { "BlockStatement": [ { "Literal": 1 }, { "Literal": null }, { "Literal": 3.14159 }, "EmptyStatement" ] } ] `) test(` new abc(); --- [ { "New": { "ArgumentList": [], "Callee": { "Identifier": "abc" } } } ] `) test(` new abc(1, 3.14159) --- [ { "New": { "ArgumentList": [ { "Literal": 1 }, { "Literal": 3.14159 } ], "Callee": { "Identifier": "abc" } } } ] `) test(` true ? false : true --- [ { "Conditional": { "Alternate": { "Literal": true }, "Consequent": { "Literal": false }, "Test": { "Literal": true } } } ] `) test(` true || false --- [ { "BinaryExpression": { "Left": { "Literal": true }, "Operator": "||", "Right": { "Literal": false } } } ] `) test(` 0 + { abc: true } --- [ { "BinaryExpression": { "Left": { "Literal": 0 }, "Operator": "+", "Right": { "Object": [ { "Key": "abc", "Value": { "Literal": true } } ] } } } ] `) test(` 1 == "1" --- [ { "BinaryExpression": { "Left": { "Literal": 1 }, "Operator": "==", "Right": { "Literal": "\"1\"" } } } ] `) test(` abc(1) --- [ { "Call": { "ArgumentList": [ { "Literal": 1 } ], "Callee": { "Identifier": "abc" } } } ] `) test(` Math.pow(3, 2) --- [ { "Call": { "ArgumentList": [ { "Literal": 3 }, { "Literal": 2 } ], "Callee": { "Dot": { "Left": { "Identifier": "Math" }, "Member": "pow" } } } } ] `) test(` 1, 2, 3 --- [ { "Sequence": [ { "Literal": 1 }, { "Literal": 2 }, { "Literal": 3 } ] } ] `) test(` / abc / gim; --- [ { "Literal": "/ abc / gim" } ] `) test(` if (0) 1; --- [ { "If": { "Consequent": { "Literal": 1 }, "Test": { "Literal": 0 } } } ] `) test(` 0+function(){ return; } --- [ { "BinaryExpression": { "Left": { "Literal": 0 }, "Operator": "+", "Right": { "Function": { "BlockStatement": [ { "Return": null } ] } } } } ] `) test(` xyzzy // Ignore it // Ignore this // And this /* And all.. ... of this! */ "Nothing happens." // And finally this --- [ { "Identifier": "xyzzy" }, { "Literal": "\"Nothing happens.\"" } ] `) test(` ((x & (x = 1)) !== 0) --- [ { "BinaryExpression": { "Left": { "BinaryExpression": { "Left": { "Identifier": "x" }, "Operator": "\u0026", "Right": { "Assign": { "Left": { "Identifier": "x" }, "Right": { "Literal": 1 } } } } }, "Operator": "!==", "Right": { "Literal": 0 } } } ] `) test(` { abc: 'def' } --- [ { "BlockStatement": [ { "Label": { "Name": "abc", "Statement": { "Literal": "'def'" } } } ] } ] `) test(` // This is not an object, this is a string literal with a label! ({ abc: 'def' }) --- [ { "Object": [ { "Key": "abc", "Value": { "Literal": "'def'" } } ] } ] `) test(` [,] --- [ { "Array": [ null ] } ] `) test(` [,,] --- [ { "Array": [ null, null ] } ] `) test(` ({ get abc() {} }) --- [ { "Object": [ { "Key": "abc", "Value": { "Function": { "BlockStatement": [] } } } ] } ] `) test(` /abc/.source --- [ { "Dot": { "Left": { "Literal": "/abc/" }, "Member": "source" } } ] `) test(` xyzzy throw new TypeError("Nothing happens.") --- [ { "Identifier": "xyzzy" }, { "Throw": { "New": { "ArgumentList": [ { "Literal": "\"Nothing happens.\"" } ], "Callee": { "Identifier": "TypeError" } } } } ] `) // When run, this will call a type error to be thrown // This is essentially the same as: // // var abc = 1(function(){})() // test(` var abc = 1 (function(){ })() --- [ { "Var": [ [ "abc", { "Call": { "ArgumentList": [], "Callee": { "Call": { "ArgumentList": [ { "Function": { "BlockStatement": [] } } ], "Callee": { "Literal": 1 } } } } } ] ] } ] `) test(` "use strict" --- [ { "Literal": "\"use strict\"" } ] `) test(` "use strict" abc = 1 + 2 + 11 --- [ { "Literal": "\"use strict\"" }, { "Assign": { "Left": { "Identifier": "abc" }, "Right": { "BinaryExpression": { "Left": { "BinaryExpression": { "Left": { "Literal": 1 }, "Operator": "+", "Right": { "Literal": 2 } } }, "Operator": "+", "Right": { "Literal": 11 } } } } } ] `) test(` abc = function() { 'use strict' } --- [ { "Assign": { "Left": { "Identifier": "abc" }, "Right": { "Function": { "BlockStatement": [ { "Literal": "'use strict'" } ] } } } } ] `) test(` for (var abc in def) { } --- [ { "ForIn": { "Body": { "BlockStatement": [] }, "Into": [ "abc", null ], "Source": { "Identifier": "def" } } } ] `) test(` abc = { '"': "'", "'": '"', } --- [ { "Assign": { "Left": { "Identifier": "abc" }, "Right": { "Object": [ { "Key": "\"", "Value": { "Literal": "\"'\"" } }, { "Key": "'", "Value": { "Literal": "'\"'" } } ] } } } ] `) test(` if (!abc && abc.jkl(def) && abc[0] === +abc[0] && abc.length < ghi) { } --- [ { "If": { "Consequent": { "BlockStatement": [] }, "Test": { "BinaryExpression": { "Left": { "BinaryExpression": { "Left": { "BinaryExpression": { "Left": null, "Operator": "\u0026\u0026", "Right": { "Call": { "ArgumentList": [ { "Identifier": "def" } ], "Callee": { "Dot": { "Left": { "Identifier": "abc" }, "Member": "jkl" } } } } } }, "Operator": "\u0026\u0026", "Right": { "BinaryExpression": { "Left": null, "Operator": "===", "Right": null } } } }, "Operator": "\u0026\u0026", "Right": { "BinaryExpression": { "Left": { "Dot": { "Left": { "Identifier": "abc" }, "Member": "length" } }, "Operator": "\u003c", "Right": { "Identifier": "ghi" } } } } } } } ] `) }) } ================================================ FILE: parser/parser.go ================================================ /* Package parser implements a parser for JavaScript. 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; } 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) # 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 import ( "bytes" "encoding/base64" "fmt" "io" "os" "github.com/robertkrimen/otto/ast" "github.com/robertkrimen/otto/file" "github.com/robertkrimen/otto/token" "gopkg.in/sourcemap.v1" ) // A Mode value is a set of flags (or 0). They control optional parser functionality. type Mode uint const ( // IgnoreRegExpErrors ignores RegExp compatibility errors (allow backtracking). IgnoreRegExpErrors Mode = 1 << iota // StoreComments stores the comments from source to the comments map. StoreComments ) type parser struct { comments *ast.Comments file *file.File scope *scope literal string str string errors ErrorList recover struct { idx file.Idx count int } idx file.Idx token token.Token offset int chrOffset int mode Mode base int length int chr rune insertSemicolon bool implicitSemicolon bool // Scratch when trying to seek to the next statement, etc. } // Parser is implemented by types which can parse JavaScript Code. type Parser interface { Scan() (tkn token.Token, literal string, idx file.Idx) } func newParser(filename, src string, base int, sm *sourcemap.Consumer) *parser { return &parser{ chr: ' ', // This is set so we can start scanning by skipping whitespace str: src, length: len(src), base: base, file: file.NewFile(filename, src, base).WithSourceMap(sm), comments: ast.NewComments(), } } // NewParser returns a new Parser. func NewParser(filename, src string) Parser { return newParser(filename, src, 1, nil) } // ReadSource reads code from src if not nil, otherwise reads from filename. func ReadSource(filename string, src interface{}) ([]byte, error) { if src != nil { switch src := src.(type) { case string: return []byte(src), nil case []byte: return src, nil case *bytes.Buffer: if src != nil { return src.Bytes(), nil } case io.Reader: var bfr bytes.Buffer if _, err := io.Copy(&bfr, src); err != nil { return nil, err } return bfr.Bytes(), nil default: return nil, fmt.Errorf("invalid src type %T", src) } } return os.ReadFile(filename) //nolint:gosec } // ReadSourceMap reads the source map from src if not nil, otherwise is a noop. func ReadSourceMap(filename string, src interface{}) (*sourcemap.Consumer, error) { if src == nil { return nil, nil //nolint:nilnil } switch src := src.(type) { case string: return sourcemap.Parse(filename, []byte(src)) case []byte: return sourcemap.Parse(filename, src) case *bytes.Buffer: return sourcemap.Parse(filename, src.Bytes()) case io.Reader: var bfr bytes.Buffer if _, err := io.Copy(&bfr, src); err != nil { return nil, err } return sourcemap.Parse(filename, bfr.Bytes()) case *sourcemap.Consumer: return src, nil default: return nil, fmt.Errorf("invalid sourcemap type %T", src) } } // ParseFileWithSourceMap parses the sourcemap returning the resulting Program. func ParseFileWithSourceMap(fileSet *file.FileSet, filename string, javascriptSource, sourcemapSource interface{}, mode Mode) (*ast.Program, error) { src, err := ReadSource(filename, javascriptSource) if err != nil { return nil, err } if sourcemapSource == nil { lines := bytes.Split(src, []byte("\n")) lastLine := lines[len(lines)-1] if bytes.HasPrefix(lastLine, []byte("//# sourceMappingURL=data:application/json")) { bits := bytes.SplitN(lastLine, []byte(","), 2) if len(bits) == 2 { if d, errDecode := base64.StdEncoding.DecodeString(string(bits[1])); errDecode == nil { sourcemapSource = d } } } } sm, err := ReadSourceMap(filename, sourcemapSource) if err != nil { return nil, err } base := 1 if fileSet != nil { base = fileSet.AddFile(filename, string(src)) } p := newParser(filename, string(src), base, sm) p.mode = mode program, err := p.parse() program.Comments = p.comments.CommentMap return program, err } // ParseFile parses the source code of a single JavaScript/ECMAScript source file and returns // the corresponding ast.Program node. // // If fileSet == nil, ParseFile parses source without a FileSet. // If fileSet != nil, ParseFile first adds filename and src to fileSet. // // The filename argument is optional and is used for labelling errors, etc. // // 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) func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mode) (*ast.Program, error) { return ParseFileWithSourceMap(fileSet, filename, src, nil, mode) } // ParseFunction parses a given parameter list and body as a function and returns the // 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})" p := newParser("", src, 1, nil) program, err := p.parse() if err != nil { return nil, err } return program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral), nil } // Scan reads a single token from the source at the current offset, increments the offset and // returns the token.Token token, a string literal representing the value of the token (if applicable) // and it's current file.Idx index. func (p *parser) Scan() (token.Token, string, file.Idx) { return p.scan() } func (p *parser) slice(idx0, idx1 file.Idx) string { from := int(idx0) - p.base to := int(idx1) - p.base if from >= 0 && to <= len(p.str) { return p.str[from:to] } return "" } func (p *parser) parse() (*ast.Program, error) { p.next() program := p.parseProgram() if false { p.errors.Sort() } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(program, p.comments.FetchAll(), ast.TRAILING) } return program, p.errors.Err() } func (p *parser) next() { p.token, p.literal, p.idx = p.scan() } func (p *parser) optionalSemicolon() { if p.token == token.SEMICOLON { p.next() return } if p.implicitSemicolon { p.implicitSemicolon = false return } if p.token != token.EOF && p.token != token.RIGHT_BRACE { p.expect(token.SEMICOLON) } } func (p *parser) semicolon() { if p.token != token.RIGHT_PARENTHESIS && p.token != token.RIGHT_BRACE { if p.implicitSemicolon { p.implicitSemicolon = false return } p.expect(token.SEMICOLON) } } func (p *parser) idxOf(offset int) file.Idx { return file.Idx(p.base + offset) } func (p *parser) expect(value token.Token) file.Idx { idx := p.idx if p.token != value { p.errorUnexpectedToken(p.token) } p.next() return idx } func lineCount(str string) (int, int) { line, last := 0, -1 pair := false for index, chr := range str { switch chr { case '\r': line++ last = index pair = true continue case '\n': if !pair { line++ } last = index case '\u2028', '\u2029': line++ last = index + 2 } pair = false } return line, last } func (p *parser) position(idx file.Idx) file.Position { position := file.Position{} offset := int(idx) - p.base str := p.str[:offset] position.Filename = p.file.Name() line, last := lineCount(str) position.Line = 1 + line if last >= 0 { position.Column = offset - last } else { position.Column = 1 + len(str) } return position } ================================================ FILE: parser/parser_test.go ================================================ package parser import ( "errors" "regexp" "strings" "testing" "github.com/robertkrimen/otto/ast" "github.com/robertkrimen/otto/file" "github.com/robertkrimen/otto/underscore" "github.com/stretchr/testify/require" ) func firstErr(err error) error { var lerr *ErrorList if errors.As(err, &lerr) { return (*lerr)[0] } return err } var matchBeforeAfterSeparator = regexp.MustCompile(`(?m)^[ \t]*---$`) func testParse(src string) (*parser, *ast.Program, error) { return testParseWithMode(src, 0) } func testParseWithMode(src string, mode Mode) (parser *parser, program *ast.Program, err error) { //nolint:nonamedreturns defer func() { if tmp := recover(); tmp != nil { if tmp, ok := tmp.(string); ok { if strings.HasPrefix(tmp, "SyntaxError:") { parser = nil program = nil err = errors.New(tmp) return } } panic(tmp) } }() parser = newParser("", src, 1, nil) parser.mode = mode program, err = parser.parse() return parser, program, err } func TestParseFile(t *testing.T) { tt(t, func() { _, err := ParseFile(nil, "", `/abc/`, 0) is(err, nil) _, err = ParseFile(nil, "", `/(?!def)abc/`, IgnoreRegExpErrors) is(err, nil) _, err = ParseFile(nil, "", `/(?!def)abc/`, 0) is(err, "(anonymous): Line 1:1 Invalid regular expression: re2: Invalid (?!) ") _, err = ParseFile(nil, "", `/(?!def)abc/; return`, IgnoreRegExpErrors) is(err, "(anonymous): Line 1:15 Illegal return statement") _, err = ParseFile(nil, "/make-sure-file-path-is-returned-not-anonymous", `a..`, 0) is(err, "/make-sure-file-path-is-returned-not-anonymous: Line 1:3 Unexpected token .") }) } func TestParseFunction(t *testing.T) { tt(t, func() { test := func(prm, bdy string, expect interface{}) *ast.FunctionLiteral { function, err := ParseFunction(prm, bdy) is(firstErr(err), expect) return function } test("a, b,c,d", "", nil) test("a, b;,c,d", "", "(anonymous): Line 1:15 Unexpected token ;") test("this", "", "(anonymous): Line 1:11 Unexpected token this") test("a, b, c, null", "", "(anonymous): Line 1:20 Unexpected token null") test("a, b,c,d", "return;", nil) test("a, b,c,d", "break;", "(anonymous): Line 2:1 Illegal break statement") test("a, b,c,d", "{}", nil) }) } func TestParserErr(t *testing.T) { tt(t, func() { test := func(input string, expect interface{}) (*ast.Program, *parser) { parser := newParser("", input, 1, nil) program, err := parser.parse() is(firstErr(err), expect) return program, parser } test("", nil) program, parser := test(` var abc; break; do { } while(true); `, "(anonymous): Line 3:9 Illegal break statement") { stmt := program.Body[1].(*ast.BadStatement) is(parser.position(stmt.From).Column, 9) is(parser.position(stmt.To).Column, 16) is(parser.slice(stmt.From, stmt.To), "break; ") } test("{", "(anonymous): Line 1:2 Unexpected end of input") test("}", "(anonymous): Line 1:1 Unexpected token }") test("3ea", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("3in", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("3in []", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("3e", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("3e+", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("3e-", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("3x", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("3x0", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("0x", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("09", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("018", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("01.0", "(anonymous): Line 1:3 Unexpected number") test("01a", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("0x3in[]", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("\"Hello\nWorld\"", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("\u203f = 10", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("x\\", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("x\\\\", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("x\\u005c", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("x\\u002a", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("x\\\\u002a", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("/\n", "(anonymous): Line 1:1 Invalid regular expression: missing /") test("var x = /(s/g", "(anonymous): Line 1:9 Invalid regular expression: Unterminated group") test("0 = 1", "(anonymous): Line 1:1 invalid left-hand side in assignment") test("func() = 1", "(anonymous): Line 1:1 invalid left-hand side in assignment") test("(1 + 1) = 2", "(anonymous): Line 1:2 invalid left-hand side in assignment") test("1++", "(anonymous): Line 1:2 invalid left-hand side in assignment") test("1--", "(anonymous): Line 1:2 invalid left-hand side in assignment") test("--1", "(anonymous): Line 1:1 invalid left-hand side in assignment") test("for((1 + 1) in abc) def();", "(anonymous): Line 1:1 Invalid left-hand side in for-in") test("[", "(anonymous): Line 1:2 Unexpected end of input") test("[,", "(anonymous): Line 1:3 Unexpected end of input") test("1 + {", "(anonymous): Line 1:6 Unexpected end of input") test("1 + { abc:abc", "(anonymous): Line 1:14 Unexpected end of input") test("1 + { abc:abc,", "(anonymous): Line 1:15 Unexpected end of input") test("var abc = /\n/", "(anonymous): Line 1:11 Invalid regular expression: missing /") test("var abc = \"\n", "(anonymous): Line 1:11 Unexpected token ILLEGAL") test("var if = 0", "(anonymous): Line 1:5 Unexpected token if") test("abc + 0 = 1", "(anonymous): Line 1:1 invalid left-hand side in assignment") test("+abc = 1", "(anonymous): Line 1:1 invalid left-hand side in assignment") test("1 + (", "(anonymous): Line 1:6 Unexpected end of input") test("\n\n\n{", "(anonymous): Line 4:2 Unexpected end of input") test("\n/* Some multiline\ncomment */\n)", "(anonymous): Line 4:1 Unexpected token )") // TODO // { set 1 } // { get 2 } // ({ set: s(if) { } }) // ({ set s(.) { } }) // ({ set: s() { } }) // ({ set: s(a, b) { } }) // ({ get: g(d) { } }) // ({ get i() { }, i: 42 }) // ({ i: 42, get i() { } }) // ({ set i(x) { }, i: 42 }) // ({ i: 42, set i(x) { } }) // ({ get i() { }, get i() { } }) // ({ set i(x) { }, set i(x) { } }) test("function abc(if) {}", "(anonymous): Line 1:14 Unexpected token if") test("function abc(true) {}", "(anonymous): Line 1:14 Unexpected token true") test("function abc(false) {}", "(anonymous): Line 1:14 Unexpected token false") test("function abc(null) {}", "(anonymous): Line 1:14 Unexpected token null") test("function null() {}", "(anonymous): Line 1:10 Unexpected token null") test("function true() {}", "(anonymous): Line 1:10 Unexpected token true") test("function false() {}", "(anonymous): Line 1:10 Unexpected token false") test("function if() {}", "(anonymous): Line 1:10 Unexpected token if") test("a b;", "(anonymous): Line 1:3 Unexpected identifier") test("if.a", "(anonymous): Line 1:3 Unexpected token .") test("a if", "(anonymous): Line 1:3 Unexpected token if") test("a class", "(anonymous): Line 1:3 Unexpected reserved word") test("break\n", "(anonymous): Line 1:1 Illegal break statement") test("break 1;", "(anonymous): Line 1:7 Unexpected number") test("for (;;) { break 1; }", "(anonymous): Line 1:18 Unexpected number") test("continue\n", "(anonymous): Line 1:1 Illegal continue statement") test("continue 1;", "(anonymous): Line 1:10 Unexpected number") test("for (;;) { continue 1; }", "(anonymous): Line 1:21 Unexpected number") test("throw", "(anonymous): Line 1:1 Unexpected end of input") test("throw;", "(anonymous): Line 1:6 Unexpected token ;") test("throw \n", "(anonymous): Line 1:1 Unexpected end of input") test("for (var abc, def in {});", "(anonymous): Line 1:19 Unexpected token in") test("for ((abc in {});;);", nil) test("for ((abc in {}));", "(anonymous): Line 1:17 Unexpected token )") test("for (+abc in {});", "(anonymous): Line 1:1 Invalid left-hand side in for-in") test("if (false)", "(anonymous): Line 1:11 Unexpected end of input") test("if (false) abc(); else", "(anonymous): Line 1:23 Unexpected end of input") test("do", "(anonymous): Line 1:3 Unexpected end of input") test("while (false)", "(anonymous): Line 1:14 Unexpected end of input") test("for (;;)", "(anonymous): Line 1:9 Unexpected end of input") test("with (abc)", "(anonymous): Line 1:11 Unexpected end of input") test("try {}", "(anonymous): Line 1:1 Missing catch or finally after try") test("try {} catch {}", "(anonymous): Line 1:14 Unexpected token {") test("try {} catch () {}", "(anonymous): Line 1:15 Unexpected token )") test("\u203f = 1", "(anonymous): Line 1:1 Unexpected token ILLEGAL") // TODO // const x = 12, y; // const x, y = 12; // const x; // if(true) let a = 1; // if(true) const a = 1; test(`new abc()."def"`, "(anonymous): Line 1:11 Unexpected string") test("/*", "(anonymous): Line 1:3 Unexpected end of input") test("/**", "(anonymous): Line 1:4 Unexpected end of input") test("/*\n\n\n", "(anonymous): Line 4:1 Unexpected end of input") test("/*\n\n\n*", "(anonymous): Line 4:2 Unexpected end of input") test("/*abc", "(anonymous): Line 1:6 Unexpected end of input") test("/*abc *", "(anonymous): Line 1:9 Unexpected end of input") test("\n]", "(anonymous): Line 2:1 Unexpected token ]") test("\r\n]", "(anonymous): Line 2:1 Unexpected token ]") test("\n\r]", "(anonymous): Line 3:1 Unexpected token ]") test("//\r\n]", "(anonymous): Line 2:1 Unexpected token ]") test("//\n\r]", "(anonymous): Line 3:1 Unexpected token ]") test("/abc\\\n/", "(anonymous): Line 1:1 Invalid regular expression: missing /") test("//\r \n]", "(anonymous): Line 3:1 Unexpected token ]") test("/*\r\n*/]", "(anonymous): Line 2:3 Unexpected token ]") test("/*\r \n*/]", "(anonymous): Line 3:3 Unexpected token ]") test("\\\\", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("\\u005c", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("\\abc", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("\\u0000", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("\\u200c = []", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("\\u200D = []", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test(`"\`, "(anonymous): Line 1:1 Unexpected token ILLEGAL") test(`"\u`, "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("return", "(anonymous): Line 1:1 Illegal return statement") test("continue", "(anonymous): Line 1:1 Illegal continue statement") test("break", "(anonymous): Line 1:1 Illegal break statement") test("switch (abc) { default: continue; }", "(anonymous): Line 1:25 Illegal continue statement") test("do { abc } *", "(anonymous): Line 1:12 Unexpected token *") test("while (true) { break abc; }", "(anonymous): Line 1:16 Undefined label 'abc'") test("while (true) { continue abc; }", "(anonymous): Line 1:16 Undefined label 'abc'") test("abc: while (true) { (function(){ break abc; }); }", "(anonymous): Line 1:34 Undefined label 'abc'") test("abc: while (true) { (function(){ abc: break abc; }); }", nil) test("abc: while (true) { (function(){ continue abc; }); }", "(anonymous): Line 1:34 Undefined label 'abc'") test(`abc: if (0) break abc; else {}`, nil) test(`abc: if (0) { break abc; } else {}`, nil) test(`abc: if (0) { break abc } else {}`, nil) test("abc: while (true) { abc: while (true) {} }", "(anonymous): Line 1:21 Label 'abc' already exists") if false { // TODO When strict mode is implemented test("(function () { 'use strict'; delete abc; }())", "") } test("_: _: while (true) {]", "(anonymous): Line 1:4 Label '_' already exists") test("_:\n_:\nwhile (true) {]", "(anonymous): Line 2:1 Label '_' already exists") test("_:\n _:\nwhile (true) {]", "(anonymous): Line 2:4 Label '_' already exists") test("/Xyzzy(?!Nothing happens)/", "(anonymous): Line 1:1 Invalid regular expression: re2: Invalid (?!) ") test("function(){}", "(anonymous): Line 1:9 Unexpected token (") test("\n/*/", "(anonymous): Line 2:4 Unexpected end of input") test("/*/.source", "(anonymous): Line 1:11 Unexpected end of input") test("/\\1/.source", "(anonymous): Line 1:1 Invalid regular expression: re2: Invalid \\1 ") test("var class", "(anonymous): Line 1:5 Unexpected reserved word") test("var if", "(anonymous): Line 1:5 Unexpected token if") test("object Object", "(anonymous): Line 1:8 Unexpected identifier") test("[object Object]", "(anonymous): Line 1:9 Unexpected identifier") test("\\u0xyz", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test(`for (var abc, def in {}) {}`, "(anonymous): Line 1:19 Unexpected token in") test(`for (abc, def in {}) {}`, "(anonymous): Line 1:1 Invalid left-hand side in for-in") test(`for (var abc=def, ghi=("abc" in {}); true;) {}`, nil) { // Semicolon insertion test("this\nif (1);", nil) test("while (1) { break\nif (1); }", nil) test("throw\nif (1);", "(anonymous): Line 1:1 Illegal newline after throw") test("(function(){ return\nif (1); })", nil) test("while (1) { continue\nif (1); }", nil) test("debugger\nif (1);", nil) } { // Reserved words test("class", "(anonymous): Line 1:1 Unexpected reserved word") test("abc.class = 1", nil) test("var class;", "(anonymous): Line 1:5 Unexpected reserved word") test("const", "(anonymous): Line 1:1 Unexpected reserved word") test("abc.const = 1", nil) test("var const;", "(anonymous): Line 1:5 Unexpected reserved word") test("enum", "(anonymous): Line 1:1 Unexpected reserved word") test("abc.enum = 1", nil) test("var enum;", "(anonymous): Line 1:5 Unexpected reserved word") test("export", "(anonymous): Line 1:1 Unexpected reserved word") test("abc.export = 1", nil) test("var export;", "(anonymous): Line 1:5 Unexpected reserved word") test("extends", "(anonymous): Line 1:1 Unexpected reserved word") test("abc.extends = 1", nil) test("var extends;", "(anonymous): Line 1:5 Unexpected reserved word") test("import", "(anonymous): Line 1:1 Unexpected reserved word") test("abc.import = 1", nil) test("var import;", "(anonymous): Line 1:5 Unexpected reserved word") test("super", "(anonymous): Line 1:1 Unexpected reserved word") test("abc.super = 1", nil) test("var super;", "(anonymous): Line 1:5 Unexpected reserved word") } { // Reserved words (strict) test(`implements`, nil) test(`abc.implements = 1`, nil) test(`var implements;`, nil) test(`interface`, nil) test(`abc.interface = 1`, nil) test(`var interface;`, nil) test(`let`, nil) test(`abc.let = 1`, nil) test(`var let;`, nil) test(`package`, nil) test(`abc.package = 1`, nil) test(`var package;`, nil) test(`private`, nil) test(`abc.private = 1`, nil) test(`var private;`, nil) test(`protected`, nil) test(`abc.protected = 1`, nil) test(`var protected;`, nil) test(`public`, nil) test(`abc.public = 1`, nil) test(`var public;`, nil) test(`static`, nil) test(`abc.static = 1`, nil) test(`var static;`, nil) test(`yield`, nil) test(`abc.yield = 1`, nil) test(`var yield;`, nil) } }) } func TestParser(t *testing.T) { tt(t, func() { test := func(source string, chk interface{}) *ast.Program { _, program, err := testParse(source) is(firstErr(err), chk) return program } test(` abc -- [] `, "(anonymous): Line 3:13 invalid left-hand side in assignment") test(` abc-- [] `, nil) test("1\n[]\n", "(anonymous): Line 2:2 Unexpected token ]") test(` function abc() { } abc() `, nil) test("", nil) test("//", nil) test("/* */", nil) test("/** **/", nil) test("/*****/", nil) test("/*", "(anonymous): Line 1:3 Unexpected end of input") test("#", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("/**/#", "(anonymous): Line 1:5 Unexpected token ILLEGAL") test("new +", "(anonymous): Line 1:5 Unexpected token +") program := test(";", nil) is(len(program.Body), 1) is(program.Body[0].(*ast.EmptyStatement).Semicolon, file.Idx(1)) program = test(";;", nil) is(len(program.Body), 2) is(program.Body[0].(*ast.EmptyStatement).Semicolon, file.Idx(1)) is(program.Body[1].(*ast.EmptyStatement).Semicolon, file.Idx(2)) program = test("1.2", nil) is(len(program.Body), 1) is(program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.NumberLiteral).Literal, "1.2") program = test("/* */1.2", nil) is(len(program.Body), 1) is(program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.NumberLiteral).Literal, "1.2") program = test("\n", nil) is(len(program.Body), 0) test(` if (0) { abc = 0 } else abc = 0 `, nil) test("if (0) abc = 0 else abc = 0", "(anonymous): Line 1:16 Unexpected token else") test(` if (0) { abc = 0 } else abc = 0 `, nil) test(` if (0) { abc = 1 } else { } `, nil) test(` do { } while (true) `, nil) test(` try { } finally { } `, nil) test(` try { } catch (abc) { } finally { } `, nil) test(` try { } catch (abc) { } finally { } `, nil) test(`try {} catch (abc) {} finally {}`, nil) test(` do { do { } while (0) } while (0) `, nil) test(`do do; while(0); while(0);`, nil) test(` (function(){ try { if ( 1 ) { return 1 } return 0 } finally { } })() `, nil) test("abc = ''\ndef", nil) test("abc = 1\ndef", nil) test("abc = Math\ndef", nil) test(`"\'"`, nil) test(` abc = function(){ } abc = 0 `, nil) test("abc.null = 0", nil) test("0x41", nil) test(`"\d"`, nil) test(`(function(){return this})`, nil) test(` Object.defineProperty(Array.prototype, "0", { value: 100, writable: false, configurable: true }); abc = [101]; abc.hasOwnProperty("0") && abc[0] === 101; `, nil) test(`new abc()`, nil) test(`new {}`, nil) test(` limit = 4 result = 0 while (limit) { limit = limit - 1 if (limit) { } else { break } result = result + 1 } `, nil) test(` while (0) { if (0) { continue } } `, nil) test("var \u0061\u0062\u0063 = 0", nil) // 7_3_1 test("var test7_3_1\nabc = 66;", nil) test("var test7_3_1\u2028abc = 66;", nil) // 7_3_3 test("//\u2028 =;", "(anonymous): Line 2:2 Unexpected token =") // 7_3_10 test("var abc = \u2029;", "(anonymous): Line 2:1 Unexpected token ;") test("var abc = \\u2029;", "(anonymous): Line 1:11 Unexpected token ILLEGAL") test("var \\u0061\\u0062\\u0063 = 0;", nil) test("'", "(anonymous): Line 1:1 Unexpected token ILLEGAL") test("'\nstr\ning\n'", "(anonymous): Line 1:1 Unexpected token ILLEGAL") // S7.6_A4.3_T1 test(`var $\u0030 = 0;`, nil) // S7.6.1.1_A1.1 test(`switch = 1`, "(anonymous): Line 1:8 Unexpected token =") // S7.8.3_A2.1_T1 test(`.0 === 0.0`, nil) // 7.8.5-1 test("var regExp = /\\\rn/;", "(anonymous): Line 1:14 Invalid regular expression: missing /") // S7.8.5_A1.1_T2 test("var regExp = /=/;", nil) // S7.8.5_A1.2_T1 test("/*/", "(anonymous): Line 1:4 Unexpected end of input") // Sbp_7.9_A9_T3 test(` do { ; } while (false) true `, nil) // S7.9_A10_T10 test(` {a:1 } 3 `, nil) test(` abc ++def `, nil) // S7.9_A5.2_T1 test(` for(false;false ) { break; } `, "(anonymous): Line 3:13 Unexpected token )") // S7.9_A9_T8 test(` do {}; while (false) `, "(anonymous): Line 2:18 Unexpected token ;") // S8.4_A5 test(` "x\0y" `, nil) // S9.3.1_A6_T1 test(` 10e10000 `, nil) // 10.4.2-1-5 test(` "abc\ def" `, nil) test("'\\\n'", nil) test("'\\\r\n'", nil) //// 11.13.1-1-1 test("42 = 42;", "(anonymous): Line 1:1 invalid left-hand side in assignment") // S11.13.2_A4.2_T1.3 test(` abc /= "1" `, nil) // 12.1-1 test(` try{};catch(){} `, "(anonymous): Line 2:13 Missing catch or finally after try") // 12.1-3 test(` try{};finally{} `, "(anonymous): Line 2:13 Missing catch or finally after try") // S12.6.3_A11.1_T3 test(` while (true) { break abc; } `, "(anonymous): Line 3:17 Undefined label 'abc'") // S15.3_A2_T1 test(`var x / = 1;`, "(anonymous): Line 1:7 Unexpected token /") test(` function abc() { if (0) return; else { } } `, nil) test("//\u2028 var =;", "(anonymous): Line 2:6 Unexpected token =") test(` throw {} `, "(anonymous): Line 2:13 Illegal newline after throw") // S7.6.1.1_A1.11 test(` function = 1 `, "(anonymous): Line 2:22 Unexpected token =") // S7.8.3_A1.2_T1 test(`0e1`, nil) test("abc = 1; abc\n++", "(anonymous): Line 2:3 Unexpected end of input") // --- test("({ get abc() {} })", nil) test(`for (abc.def in {}) {}`, nil) test(`while (true) { break }`, nil) test(`while (true) { continue }`, nil) test(`abc=/^(?:(\w+:)\/{2}(\w+(?:\.\w+)*\/?)|(.{0,2}\/{1}))?([/.]*?(?:[^?]+)?\/)?((?:[^/?]+)\.(\w+))(?:\?(\S+)?)?$/,def=/^(?:(\w+:)\/{2})|(.{0,2}\/{1})?([/.]*?(?:[^?]+)?\/?)?$/`, nil) test(`(function() { try {} catch (err) {} finally {} return })`, nil) test(`0xde0b6b3a7640080.toFixed(0)`, nil) test(`/[^-._0-9A-Za-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u37f-\u1fff\u200c-\u200d\u203f\u2040\u2070-\u218f]/`, nil) test(`/[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE-\uFFFF]/`, nil) test("var abc = 1;\ufeff", nil) test("\ufeff/* var abc = 1; */", nil) test(`if (-0x8000000000000000<=abc&&abc<=0x8000000000000000) {}`, nil) test(`(function(){debugger;return this;})`, nil) test(` `, nil) test(` var abc = "" debugger `, nil) test(` var abc = /\[\]$/ debugger `, nil) test(` var abc = 1 / 2 debugger `, nil) test(` function mergeObjects(x, y) { /* dummy body */} mergeObjects( { "duck": "quack" }, { "dog": "bark" }, // Allow trailing comma after the last argument. ); `, nil) }) } func Test_parseStringLiteral(t *testing.T) { tt(t, func() { test := func(have, want string) { have, err := parseStringLiteral(have) is(err, nil) is(have, want) } test("", "") test("1(\\\\d+)", "1(\\d+)") test("\\u2029", "\u2029") test("abc\\uFFFFabc", "abc\uFFFFabc") test("[First line \\\nSecond line \\\n Third line\\\n. ]", "[First line Second line Third line. ]") test("\\u007a\\x79\\u000a\\x78", "zy\nx") // S7.8.4_A4.2_T3 test("\\a", "a") test("\u0410", "\u0410") // S7.8.4_A5.1_T1 test("\\0", "\u0000") // S8.4_A5 test("\u0000", "\u0000") // 15.5.4.20 test("'abc'\\\n'def'", "'abc''def'") // 15.5.4.20-4-1 test("'abc'\\\r\n'def'", "'abc''def'") // Octal test("\\0", "\000") test("\\00", "\000") test("\\000", "\000") test("\\09", "\0009") test("\\009", "\0009") test("\\0009", "\0009") test("\\1", "\001") test("\\01", "\001") test("\\001", "\001") test("\\0011", "\0011") test("\\1abc", "\001abc") test("\\\u4e16", "\u4e16") // err test = func(have, want string) { have, err := parseStringLiteral(have) is(err.Error(), want) is(have, "") } test(`\u`, `invalid escape: \u: len("") != 4`) test(`\u0`, `invalid escape: \u: len("0") != 4`) test(`\u00`, `invalid escape: \u: len("00") != 4`) test(`\u000`, `invalid escape: \u: len("000") != 4`) test(`\x`, `invalid escape: \x: len("") != 2`) test(`\x0`, `invalid escape: \x: len("0") != 2`) test(`\x0`, `invalid escape: \x: len("0") != 2`) }) } func Test_parseNumberLiteral(t *testing.T) { tt(t, func() { test := func(input string, expect interface{}) { result, err := parseNumberLiteral(input) is(err, nil) is(result, expect) } test("0", 0) test("0x8000000000000000", float64(9.223372036854776e+18)) }) } func Test_praseRegExpLiteral(t *testing.T) { tt(t, func() { test := func(input, literal, pattern, flags string) { parser := newParser("", input, 1, nil) program, err := parser.parse() is(err, nil) regex := program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.RegExpLiteral) is(regex.Literal, literal) is(regex.Pattern, pattern) is(regex.Flags, flags) } test("/abc/", "/abc/", "abc", "") test("/abc/gim", "/abc/gim", "abc", "gim") test("/abc/ ", "/abc/", "abc", "") test("/abc/gim ", "/abc/gim", "abc", "gim") test("/abc/;", "/abc/", "abc", "") test("/abc/gim;", "/abc/gim", "abc", "gim") test("/abc/\n", "/abc/", "abc", "") test("/abc/gim\n", "/abc/gim", "abc", "gim") test("/abc/;\n", "/abc/", "abc", "") test("/abc/gim;\n", "/abc/gim", "abc", "gim") }) } func TestPosition(t *testing.T) { tt(t, func() { parser := newParser("", "// Lorem ipsum", 1, nil) // Out of range, idx0 (error condition) is(parser.slice(0, 1), "") is(parser.slice(0, 10), "") // Out of range, idx1 (error condition) is(parser.slice(1, 128), "") is(parser.str[0:0], "") is(parser.slice(1, 1), "") is(parser.str[0:1], "/") is(parser.slice(1, 2), "/") is(parser.str[0:14], "// Lorem ipsum") is(parser.slice(1, 15), "// Lorem ipsum") parser = newParser("", "(function(){ return 0; })", 1, nil) program, err := parser.parse() is(err, nil) var node ast.Node node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral) is(node.Idx0(), file.Idx(2)) is(node.Idx1(), file.Idx(25)) is(parser.slice(node.Idx0(), node.Idx1()), "function(){ return 0; }") is(parser.slice(node.Idx0(), node.Idx1()+1), "function(){ return 0; })") is(parser.slice(node.Idx0(), node.Idx1()+2), "") is(node.(*ast.FunctionLiteral).Source, "function(){ return 0; }") node = program is(node.Idx0(), file.Idx(2)) is(node.Idx1(), file.Idx(25)) is(parser.slice(node.Idx0(), node.Idx1()), "function(){ return 0; }") parser = newParser("", "(function(){ return abc; })", 1, nil) program, err = parser.parse() is(err, nil) node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral) is(node.(*ast.FunctionLiteral).Source, "function(){ return abc; }") parser = newParser("", "this.style", 1, nil) program, err = parser.parse() is(err, nil) node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.DotExpression).Left.(*ast.ThisExpression) is(node.Idx0(), file.Idx(1)) is(node.Idx1(), file.Idx(5)) parser = newParser("", "(function(){ if (abc) { throw 'failed'; } })", 1, nil) program, err = parser.parse() is(err, nil) block := program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.IfStatement) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "if (abc) { throw 'failed'; }") node = node.(*ast.IfStatement).Consequent.(*ast.BlockStatement).List[0].(*ast.ThrowStatement) is(node.Idx0(), 25) is(parser.slice(node.Idx0(), node.Idx1()), "throw 'failed'") parser = newParser("", "(function(){ for (x=1; x<=4; x++) { console.log(x); } })", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.ForStatement) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "for (x=1; x<=4; x++) { console.log(x); }") parser = newParser("", "(function(){ for (p in o) { console.log(p); } })", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.ForInStatement) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "for (p in o) { console.log(p); }") parser = newParser("", "x = {x: 1}", 1, nil) program, err = parser.parse() is(err, nil) node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.ObjectLiteral) is(node.Idx0(), 5) is(parser.slice(node.Idx0(), node.Idx1()), "{x: 1}") parser = newParser("", "x = [1, 2]", 1, nil) program, err = parser.parse() is(err, nil) node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.ArrayLiteral) is(node.Idx0(), 5) is(parser.slice(node.Idx0(), node.Idx1()), "[1, 2]") parser = newParser("", "x = true ? 1 : 2", 1, nil) program, err = parser.parse() is(err, nil) node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.ConditionalExpression) is(node.Idx0(), 5) is(parser.slice(node.Idx0(), node.Idx1()), "true ? 1 : 2") parser = newParser("", "(function(){ x = 1, y = 2; })", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.ExpressionStatement).Expression.(*ast.SequenceExpression) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "x = 1, y = 2") parser = newParser("", "x = ~x", 1, nil) program, err = parser.parse() is(err, nil) node = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.AssignExpression).Right.(*ast.UnaryExpression) is(node.Idx0(), 5) is(parser.slice(node.Idx0(), node.Idx1()), "~x") parser = newParser("", "(function(){ xyz++; })", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.ExpressionStatement).Expression.(*ast.UnaryExpression) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "xyz++") parser = newParser("", "(function(){ var abc, xyz = 1; })", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.VariableStatement) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "var abc, xyz = 1") node = block.List[0].(*ast.VariableStatement).List[0].(*ast.VariableExpression) is(node.Idx0(), 18) is(parser.slice(node.Idx0(), node.Idx1()), "abc") node = block.List[0].(*ast.VariableStatement).List[1].(*ast.VariableExpression) is(node.Idx0(), 23) is(parser.slice(node.Idx0(), node.Idx1()), "xyz = 1") parser = newParser("", "for (i = 0; i < 10; i++) { if (i == 5) break; }", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ForStatement).Body.(*ast.BlockStatement) node = block.List[0].(*ast.IfStatement).Consequent.(*ast.BranchStatement) is(node.Idx0(), 40) is(parser.slice(node.Idx0(), node.Idx1()), "break") parser = newParser("", "(function(){ xyz: for (i = 0; i < 10; i++) { if (i == 5) continue xyz; } })", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.LabelledStatement) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "xyz: for (i = 0; i < 10; i++) { if (i == 5) continue xyz; }") block = node.(*ast.LabelledStatement).Statement.(*ast.ForStatement).Body.(*ast.BlockStatement) node = block.List[0].(*ast.IfStatement).Consequent.(*ast.BranchStatement) is(node.Idx0(), 58) is(parser.slice(node.Idx0(), node.Idx1()), "continue xyz") parser = newParser("", "(function(){ return; })", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.ReturnStatement) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "return") parser = newParser("", "(function(){ return 10; })", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.ReturnStatement) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "return 10") parser = newParser("", "(function(){ switch (a) { default: return; }})", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.SwitchStatement) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "switch (a) { default: return; }") parser = newParser("", "(function(){ try { a(); } catch (error) { b(); } })", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.TryStatement) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "try { a(); } catch (error) { b(); }") node = block.List[0].(*ast.TryStatement).Catch is(node.Idx0(), 27) is(parser.slice(node.Idx0(), node.Idx1()), "catch (error) { b(); }") parser = newParser("", "(function(){ try { a(); } catch (error) { b(); } finally { c(); } })", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.TryStatement) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "try { a(); } catch (error) { b(); } finally { c(); }") parser = newParser("", "(function(){ with (1) {} })", 1, nil) program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) node = block.List[0].(*ast.WithStatement) is(node.Idx0(), 14) is(parser.slice(node.Idx0(), node.Idx1()), "with (1) {}") parser = newParser("", "while (i < 10) { i++; }", 1, nil) program, err = parser.parse() is(err, nil) node = program.Body[0].(*ast.WhileStatement) is(node.Idx0(), 1) is(parser.slice(node.Idx0(), node.Idx1()), "while (i < 10) { i++; }") parser = newParser("", "do { i++; } while (i < 10 )", 1, nil) program, err = parser.parse() is(err, nil) node = program.Body[0].(*ast.DoWhileStatement) is(node.Idx0(), 1) is(parser.slice(node.Idx0(), node.Idx1()), "do { i++; } while (i < 10 )") parser = newParser("", "(function() { // single-line comment\n })", 1, nil) parser.mode |= StoreComments program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) comment := parser.comments.CommentMap[block][0] is(comment.Begin, 15) is(parser.slice(comment.Begin, file.Idx(int(comment.Begin)+len(comment.Text)+2)), "// single-line comment") parser = newParser("", "(function() { /* multi-line comment */ })", 1, nil) parser.mode |= StoreComments program, err = parser.parse() is(err, nil) block = program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral).Body.(*ast.BlockStatement) comment = parser.comments.CommentMap[block][0] is(comment.Begin, 15) is(parser.slice(comment.Begin, file.Idx(int(comment.Begin)+len(comment.Text)+4)), "/* multi-line comment */") }) } func BenchmarkParser(b *testing.B) { src := underscore.Source() b.ResetTimer() for i := 0; i < b.N; i++ { parser := newParser("", src, 1, nil) _, err := parser.parse() require.NoError(b, err) } } ================================================ FILE: parser/regexp.go ================================================ package parser import ( "bytes" "fmt" "strconv" ) type regExpParser struct { goRegexp *bytes.Buffer str string errors []error length int chrOffset int offset int chr rune invalid bool } // TransformRegExp transforms a JavaScript pattern into a Go "regexp" pattern. // // re2 (Go) cannot do backtracking, so the presence of a lookahead (?=) (?!) or // backreference (\1, \2, ...) will cause an error. // // 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. // // If the pattern is invalid (not valid even in JavaScript), then this function // returns the empty string and an error. // // If the pattern is valid, but incompatible (contains a lookahead or backreference), // then this function returns the transformation (a non-empty string) AND an error. func TransformRegExp(pattern string) (string, error) { if pattern == "" { return "", nil } // TODO If without \, if without (?=, (?!, then another shortcut p := regExpParser{ str: pattern, length: len(pattern), goRegexp: bytes.NewBuffer(make([]byte, 0, 3*len(pattern)/2)), } p.read() // Pull in the first character p.scan() var err error if len(p.errors) > 0 { err = p.errors[0] } if p.invalid { return "", err } // Might not be re2 compatible, but is still a valid JavaScript RegExp return p.goRegexp.String(), err } func (p *regExpParser) scan() { for p.chr != -1 { switch p.chr { case '\\': p.read() p.scanEscape(false) case '(': p.pass() p.scanGroup() case '[': p.pass() p.scanBracket() case ')': p.error(-1, "Unmatched ')'") p.invalid = true p.pass() default: p.pass() } } } // (...) func (p *regExpParser) scanGroup() { str := p.str[p.chrOffset:] if len(str) > 1 { // A possibility of (?= or (?! if str[0] == '?' { if str[1] == '=' || str[1] == '!' { p.error(-1, "re2: Invalid (%s) ", p.str[p.chrOffset:p.chrOffset+2]) } } } for p.chr != -1 && p.chr != ')' { switch p.chr { case '\\': p.read() p.scanEscape(false) case '(': p.pass() p.scanGroup() case '[': p.pass() p.scanBracket() default: p.pass() continue } } if p.chr != ')' { p.error(-1, "Unterminated group") p.invalid = true return } p.pass() } // [...]. func (p *regExpParser) scanBracket() { for p.chr != -1 { if p.chr == ']' { break } else if p.chr == '\\' { p.read() p.scanEscape(true) continue } p.pass() } if p.chr != ']' { p.error(-1, "Unterminated character class") p.invalid = true return } p.pass() } // \... func (p *regExpParser) scanEscape(inClass bool) { offset := p.chrOffset var length, base uint32 switch p.chr { case '0', '1', '2', '3', '4', '5', '6', '7': var value int64 size := 0 for { digit := int64(digitValue(p.chr)) if digit >= 8 { // Not a valid digit break } value = value*8 + digit p.read() size++ } if size == 1 { // The number of characters read _, err := p.goRegexp.Write([]byte{'\\', byte(value) + '0'}) if err != nil { p.errors = append(p.errors, err) } if value != 0 { // An invalid backreference p.error(-1, "re2: Invalid \\%d ", value) } return } tmp := []byte{'\\', 'x', '0', 0} if value >= 16 { tmp = tmp[0:2] } else { tmp = tmp[0:3] } tmp = strconv.AppendInt(tmp, value, 16) _, err := p.goRegexp.Write(tmp) if err != nil { p.errors = append(p.errors, err) } return case '8', '9': size := 0 for { digit := digitValue(p.chr) if digit >= 10 { // Not a valid digit break } p.read() size++ } err := p.goRegexp.WriteByte('\\') if err != nil { p.errors = append(p.errors, err) } _, err = p.goRegexp.WriteString(p.str[offset:p.chrOffset]) if err != nil { p.errors = append(p.errors, err) } p.error(-1, "re2: Invalid \\%s ", p.str[offset:p.chrOffset]) return case 'x': p.read() length, base = 2, 16 case 'u': p.read() length, base = 4, 16 case 'b': if inClass { _, err := p.goRegexp.Write([]byte{'\\', 'x', '0', '8'}) if err != nil { p.errors = append(p.errors, err) } p.read() return } fallthrough case 'B': fallthrough case 'd', 'D', 's', 'S', 'w', 'W': // This is slightly broken, because ECMAScript // includes \v in \s, \S, while re2 does not fallthrough case '\\': fallthrough case 'f', 'n', 'r', 't', 'v': err := p.goRegexp.WriteByte('\\') if err != nil { p.errors = append(p.errors, err) } p.pass() return case 'c': p.read() var value int64 switch { case 'a' <= p.chr && p.chr <= 'z': value = int64(p.chr) - 'a' + 1 case 'A' <= p.chr && p.chr <= 'Z': value = int64(p.chr) - 'A' + 1 default: err := p.goRegexp.WriteByte('c') if err != nil { p.errors = append(p.errors, err) } return } tmp := []byte{'\\', 'x', '0', 0} if value >= 16 { tmp = tmp[0:2] } else { tmp = tmp[0:3] } tmp = strconv.AppendInt(tmp, value, 16) _, err := p.goRegexp.Write(tmp) if err != nil { p.errors = append(p.errors, err) } p.read() return default: // $ is an identifier character, so we have to have // a special case for it here if p.chr == '$' || !isIdentifierPart(p.chr) { // A non-identifier character needs escaping err := p.goRegexp.WriteByte('\\') if err != nil { p.errors = append(p.errors, err) } } else { //nolint:staticcheck // Unescape the character for re2 } p.pass() return } // Otherwise, we're a \u.... or \x... valueOffset := p.chrOffset var value uint32 for length := length; length > 0; length-- { digit := uint32(digitValue(p.chr)) if digit >= base { // Not a valid digit goto skip } value = value*base + digit p.read() } switch length { case 4: if _, err := p.goRegexp.Write([]byte{ '\\', 'x', '{', p.str[valueOffset+0], p.str[valueOffset+1], p.str[valueOffset+2], p.str[valueOffset+3], '}', }); err != nil { p.errors = append(p.errors, err) } case 2: if _, err := p.goRegexp.Write([]byte{ '\\', 'x', p.str[valueOffset+0], p.str[valueOffset+1], }); err != nil { p.errors = append(p.errors, err) } default: // Should never, ever get here... p.error(-1, "re2: Illegal branch in scanEscape") goto skip } return skip: _, err := p.goRegexp.WriteString(p.str[offset:p.chrOffset]) if err != nil { p.errors = append(p.errors, err) } } func (p *regExpParser) pass() { if p.chr != -1 { _, err := p.goRegexp.WriteRune(p.chr) if err != nil { p.errors = append(p.errors, err) } } p.read() } // TODO Better error reporting, use the offset, etc. func (p *regExpParser) error(offset int, msg string, msgValues ...interface{}) { //nolint:unparam err := fmt.Errorf(msg, msgValues...) p.errors = append(p.errors, err) } ================================================ FILE: parser/regexp_test.go ================================================ package parser import ( "regexp" "testing" ) func TestRegExp(t *testing.T) { tt(t, func() { { // err test := func(input string, expect interface{}) { _, err := TransformRegExp(input) is(err, expect) } test("[", "Unterminated character class") test("(", "Unterminated group") test("(?=)", "re2: Invalid (?=) ") test("(?=)", "re2: Invalid (?=) ") test("(?!)", "re2: Invalid (?!) ") // An error anyway test("(?=", "re2: Invalid (?=) ") test("\\1", "re2: Invalid \\1 ") test("\\90", "re2: Invalid \\90 ") test("\\9123456789", "re2: Invalid \\9123456789 ") test("\\(?=)", "Unmatched ')'") test(")", "Unmatched ')'") } { // err test := func(input, expect string, expectErr interface{}) { output, err := TransformRegExp(input) is(output, expect) is(err, expectErr) } test("(?!)", "(?!)", "re2: Invalid (?!) ") test(")", "", "Unmatched ')'") test("(?!))", "", "re2: Invalid (?!) ") test("\\0", "\\0", nil) test("\\1", "\\1", "re2: Invalid \\1 ") test("\\9123456789", "\\9123456789", "re2: Invalid \\9123456789 ") } { // err test := func(input string, expect string) { result, err := TransformRegExp(input) is(err, nil) if is(result, expect) { _, err = regexp.Compile(result) if !is(err, nil) { t.Log(result) } } } test("", "") test("abc", "abc") test(`\abc`, `abc`) test(`\a\b\c`, `a\bc`) test(`\x`, `x`) test(`\c`, `c`) test(`\cA`, `\x01`) test(`\cz`, `\x1a`) test(`\ca`, `\x01`) test(`\cj`, `\x0a`) test(`\ck`, `\x0b`) test(`\+`, `\+`) test(`[\b]`, `[\x08]`) test(`\u0z01\x\undefined`, `u0z01xundefined`) test(`\\|'|\r|\n|\t|\u2028|\u2029`, `\\|'|\r|\n|\t|\x{2028}|\x{2029}`) test("]", "]") test("}", "}") test("%", "%") test("(%)", "(%)") test("(?:[%\\s])", "(?:[%\\s])") test("[[]", "[[]") test("\\101", "\\x41") test("\\51", "\\x29") test("\\051", "\\x29") test("\\175", "\\x7d") test("\\04", "\\x04") test(`<%([\s\S]+?)%>`, `<%([\s\S]+?)%>`) test(`(.)^`, "(.)^") test(`<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$`, `<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$`) test(`\$`, `\$`) test(`[G-b]`, `[G-b]`) test(`[G-b\0]`, `[G-b\0]`) } }) } func TestTransformRegExp(t *testing.T) { tt(t, func() { pattern, err := TransformRegExp(`\s+abc\s+`) is(err, nil) is(pattern, `\s+abc\s+`) is(regexp.MustCompile(pattern).MatchString("\t abc def"), true) }) } ================================================ FILE: parser/scope.go ================================================ package parser import ( "github.com/robertkrimen/otto/ast" ) type scope struct { outer *scope declarationList []ast.Declaration labels []string allowIn bool inIteration bool inSwitch bool inFunction bool } func (p *parser) openScope() { p.scope = &scope{ outer: p.scope, allowIn: true, } } func (p *parser) closeScope() { p.scope = p.scope.outer } func (p *scope) declare(declaration ast.Declaration) { p.declarationList = append(p.declarationList, declaration) } func (p *scope) hasLabel(name string) bool { for _, label := range p.labels { if label == name { return true } } if p.outer != nil && !p.inFunction { // Crossing a function boundary to look for a label is verboten return p.outer.hasLabel(name) } return false } ================================================ FILE: parser/statement.go ================================================ package parser import ( "github.com/robertkrimen/otto/ast" "github.com/robertkrimen/otto/token" ) func (p *parser) parseBlockStatement() *ast.BlockStatement { node := &ast.BlockStatement{} // Find comments before the leading brace if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node, p.comments.FetchAll(), ast.LEADING) p.comments.Unset() } node.LeftBrace = p.expect(token.LEFT_BRACE) node.List = p.parseStatementList() if p.mode&StoreComments != 0 { p.comments.Unset() p.comments.CommentMap.AddComments(node, p.comments.FetchAll(), ast.FINAL) p.comments.AfterBlock() } node.RightBrace = p.expect(token.RIGHT_BRACE) // Find comments after the trailing brace if p.mode&StoreComments != 0 { p.comments.ResetLineBreak() p.comments.CommentMap.AddComments(node, p.comments.Fetch(), ast.TRAILING) } return node } func (p *parser) parseEmptyStatement() ast.Statement { idx := p.expect(token.SEMICOLON) return &ast.EmptyStatement{Semicolon: idx} } func (p *parser) parseStatementList() (list []ast.Statement) { //nolint:nonamedreturns for p.token != token.RIGHT_BRACE && p.token != token.EOF { statement := p.parseStatement() list = append(list, statement) } return list } func (p *parser) parseStatement() ast.Statement { if p.token == token.EOF { p.errorUnexpectedToken(p.token) return &ast.BadStatement{From: p.idx, To: p.idx + 1} } if p.mode&StoreComments != 0 { p.comments.ResetLineBreak() } switch p.token { case token.SEMICOLON: return p.parseEmptyStatement() case token.LEFT_BRACE: return p.parseBlockStatement() case token.IF: return p.parseIfStatement() case token.DO: statement := p.parseDoWhileStatement() p.comments.PostProcessNode(statement) return statement case token.WHILE: return p.parseWhileStatement() case token.FOR: return p.parseForOrForInStatement() case token.BREAK: return p.parseBreakStatement() case token.CONTINUE: return p.parseContinueStatement() case token.DEBUGGER: return p.parseDebuggerStatement() case token.WITH: return p.parseWithStatement() case token.VAR: return p.parseVariableStatement() case token.FUNCTION: return p.parseFunctionStatement() case token.SWITCH: return p.parseSwitchStatement() case token.RETURN: return p.parseReturnStatement() case token.THROW: return p.parseThrowStatement() case token.TRY: return p.parseTryStatement() } var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } expression := p.parseExpression() if identifier, isIdentifier := expression.(*ast.Identifier); isIdentifier && p.token == token.COLON { // LabelledStatement colon := p.idx if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() // : label := identifier.Name for _, value := range p.scope.labels { if label == value { p.error(identifier.Idx0(), "Label '%s' already exists", label) } } var labelComments []*ast.Comment if p.mode&StoreComments != 0 { labelComments = p.comments.FetchAll() } p.scope.labels = append(p.scope.labels, label) // Push the label statement := p.parseStatement() p.scope.labels = p.scope.labels[:len(p.scope.labels)-1] // Pop the label exp := &ast.LabelledStatement{ Label: identifier, Colon: colon, Statement: statement, } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(exp, labelComments, ast.LEADING) } return exp } p.optionalSemicolon() statement := &ast.ExpressionStatement{ Expression: expression, } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(statement, comments, ast.LEADING) } return statement } func (p *parser) parseTryStatement() ast.Statement { var tryComments []*ast.Comment if p.mode&StoreComments != 0 { tryComments = p.comments.FetchAll() } node := &ast.TryStatement{ Try: p.expect(token.TRY), Body: p.parseBlockStatement(), } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node, tryComments, ast.LEADING) p.comments.CommentMap.AddComments(node.Body, p.comments.FetchAll(), ast.TRAILING) } if p.token == token.CATCH { catch := p.idx if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() p.expect(token.LEFT_PARENTHESIS) if p.token != token.IDENTIFIER { p.expect(token.IDENTIFIER) p.nextStatement() return &ast.BadStatement{From: catch, To: p.idx} } identifier := p.parseIdentifier() p.expect(token.RIGHT_PARENTHESIS) node.Catch = &ast.CatchStatement{ Catch: catch, Parameter: identifier, Body: p.parseBlockStatement(), } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node.Catch.Body, p.comments.FetchAll(), ast.TRAILING) } } if p.token == token.FINALLY { if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() if p.mode&StoreComments != 0 { tryComments = p.comments.FetchAll() } node.Finally = p.parseBlockStatement() if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node.Finally, tryComments, ast.LEADING) } } if node.Catch == nil && node.Finally == nil { p.error(node.Try, "Missing catch or finally after try") return &ast.BadStatement{From: node.Try, To: node.Body.Idx1()} } return node } func (p *parser) parseFunctionParameterList() *ast.ParameterList { opening := p.expect(token.LEFT_PARENTHESIS) if p.mode&StoreComments != 0 { p.comments.Unset() } var list []*ast.Identifier for p.token != token.RIGHT_PARENTHESIS && p.token != token.EOF { if p.token != token.IDENTIFIER { p.expect(token.IDENTIFIER) } else { identifier := p.parseIdentifier() list = append(list, identifier) } if p.token != token.RIGHT_PARENTHESIS { if p.mode&StoreComments != 0 { p.comments.Unset() } p.expect(token.COMMA) } } closing := p.expect(token.RIGHT_PARENTHESIS) return &ast.ParameterList{ Opening: opening, List: list, Closing: closing, } } func (p *parser) parseFunctionStatement() *ast.FunctionStatement { var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } function := &ast.FunctionStatement{ Function: p.parseFunction(true), } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(function, comments, ast.LEADING) } return function } func (p *parser) parseFunction(declaration bool) *ast.FunctionLiteral { node := &ast.FunctionLiteral{ Function: p.expect(token.FUNCTION), } var name *ast.Identifier if p.token == token.IDENTIFIER { name = p.parseIdentifier() if declaration { p.scope.declare(&ast.FunctionDeclaration{ Function: node, }) } } else if declaration { // Use expect error handling p.expect(token.IDENTIFIER) } if p.mode&StoreComments != 0 { p.comments.Unset() } node.Name = name node.ParameterList = p.parseFunctionParameterList() p.parseFunctionBlock(node) node.Source = p.slice(node.Idx0(), node.Idx1()) return node } func (p *parser) parseFunctionBlock(node *ast.FunctionLiteral) { p.openScope() inFunction := p.scope.inFunction p.scope.inFunction = true defer func() { p.scope.inFunction = inFunction p.closeScope() }() node.Body = p.parseBlockStatement() node.DeclarationList = p.scope.declarationList } func (p *parser) parseDebuggerStatement() ast.Statement { idx := p.expect(token.DEBUGGER) node := &ast.DebuggerStatement{ Debugger: idx, } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node, p.comments.FetchAll(), ast.TRAILING) } p.semicolon() return node } func (p *parser) parseReturnStatement() ast.Statement { idx := p.expect(token.RETURN) var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } if !p.scope.inFunction { p.error(idx, "Illegal return statement") p.nextStatement() return &ast.BadStatement{From: idx, To: p.idx} } node := &ast.ReturnStatement{ Return: idx, } if !p.implicitSemicolon && p.token != token.SEMICOLON && p.token != token.RIGHT_BRACE && p.token != token.EOF { node.Argument = p.parseExpression() } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node, comments, ast.LEADING) } p.semicolon() return node } func (p *parser) parseThrowStatement() ast.Statement { var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } idx := p.expect(token.THROW) if p.implicitSemicolon { if p.chr == -1 { // Hackish p.error(idx, "Unexpected end of input") } else { p.error(idx, "Illegal newline after throw") } p.nextStatement() return &ast.BadStatement{From: idx, To: p.idx} } node := &ast.ThrowStatement{ Throw: idx, Argument: p.parseExpression(), } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node, comments, ast.LEADING) } p.semicolon() return node } func (p *parser) parseSwitchStatement() ast.Statement { var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } idx := p.expect(token.SWITCH) if p.mode&StoreComments != 0 { comments = append(comments, p.comments.FetchAll()...) } p.expect(token.LEFT_PARENTHESIS) node := &ast.SwitchStatement{ Switch: idx, Discriminant: p.parseExpression(), Default: -1, } p.expect(token.RIGHT_PARENTHESIS) if p.mode&StoreComments != 0 { comments = append(comments, p.comments.FetchAll()...) } p.expect(token.LEFT_BRACE) inSwitch := p.scope.inSwitch p.scope.inSwitch = true defer func() { p.scope.inSwitch = inSwitch }() for index := 0; p.token != token.EOF; index++ { if p.token == token.RIGHT_BRACE { node.RightBrace = p.idx p.next() break } clause := p.parseCaseStatement() if clause.Test == nil { if node.Default != -1 { p.error(clause.Case, "Already saw a default in switch") } node.Default = index } node.Body = append(node.Body, clause) } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node, comments, ast.LEADING) } return node } func (p *parser) parseWithStatement() ast.Statement { var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } idx := p.expect(token.WITH) var withComments []*ast.Comment if p.mode&StoreComments != 0 { withComments = p.comments.FetchAll() } p.expect(token.LEFT_PARENTHESIS) node := &ast.WithStatement{ With: idx, Object: p.parseExpression(), } p.expect(token.RIGHT_PARENTHESIS) if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node, comments, ast.LEADING) p.comments.CommentMap.AddComments(node, withComments, ast.WITH) } node.Body = p.parseStatement() return node } func (p *parser) parseCaseStatement() *ast.CaseStatement { node := &ast.CaseStatement{ Case: p.idx, } var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() p.comments.Unset() } if p.token == token.DEFAULT { p.next() } else { p.expect(token.CASE) node.Test = p.parseExpression() } if p.mode&StoreComments != 0 { p.comments.Unset() } p.expect(token.COLON) for { if p.token == token.EOF || p.token == token.RIGHT_BRACE || p.token == token.CASE || p.token == token.DEFAULT { break } consequent := p.parseStatement() node.Consequent = append(node.Consequent, consequent) } // Link the comments to the case statement if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node, comments, ast.LEADING) } return node } func (p *parser) parseIterationStatement() ast.Statement { inIteration := p.scope.inIteration p.scope.inIteration = true defer func() { p.scope.inIteration = inIteration }() return p.parseStatement() } func (p *parser) parseForIn(into ast.Expression) *ast.ForInStatement { // Already have consumed " in" source := p.parseExpression() p.expect(token.RIGHT_PARENTHESIS) body := p.parseIterationStatement() forin := &ast.ForInStatement{ Into: into, Source: source, Body: body, } return forin } func (p *parser) parseFor(initializer ast.Expression) *ast.ForStatement { // Already have consumed " ;" var test, update ast.Expression if p.token != token.SEMICOLON { test = p.parseExpression() } if p.mode&StoreComments != 0 { p.comments.Unset() } p.expect(token.SEMICOLON) if p.token != token.RIGHT_PARENTHESIS { update = p.parseExpression() } p.expect(token.RIGHT_PARENTHESIS) body := p.parseIterationStatement() forstatement := &ast.ForStatement{ Initializer: initializer, Test: test, Update: update, Body: body, } return forstatement } func (p *parser) parseForOrForInStatement() ast.Statement { var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } idx := p.expect(token.FOR) var forComments []*ast.Comment if p.mode&StoreComments != 0 { forComments = p.comments.FetchAll() } p.expect(token.LEFT_PARENTHESIS) var left []ast.Expression forIn := false if p.token != token.SEMICOLON { allowIn := p.scope.allowIn p.scope.allowIn = false if p.token == token.VAR { tokenIdx := p.idx var varComments []*ast.Comment if p.mode&StoreComments != 0 { varComments = p.comments.FetchAll() p.comments.Unset() } p.next() list := p.parseVariableDeclarationList(tokenIdx) if len(list) == 1 && p.token == token.IN { if p.mode&StoreComments != 0 { p.comments.Unset() } p.next() // in forIn = true left = []ast.Expression{list[0]} // There is only one declaration } else { left = list } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(left[0], varComments, ast.LEADING) } } else { left = append(left, p.parseExpression()) if p.token == token.IN { p.next() forIn = true } } p.scope.allowIn = allowIn } if forIn { switch left[0].(type) { case *ast.Identifier, *ast.DotExpression, *ast.BracketExpression, *ast.VariableExpression: // These are all acceptable default: p.error(idx, "Invalid left-hand side in for-in") p.nextStatement() return &ast.BadStatement{From: idx, To: p.idx} } forin := p.parseForIn(left[0]) forin.For = idx if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(forin, comments, ast.LEADING) p.comments.CommentMap.AddComments(forin, forComments, ast.FOR) } return forin } if p.mode&StoreComments != 0 { p.comments.Unset() } p.expect(token.SEMICOLON) initializer := &ast.SequenceExpression{Sequence: left} forstatement := p.parseFor(initializer) forstatement.For = idx if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(forstatement, comments, ast.LEADING) p.comments.CommentMap.AddComments(forstatement, forComments, ast.FOR) } return forstatement } func (p *parser) parseVariableStatement() *ast.VariableStatement { var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } idx := p.expect(token.VAR) list := p.parseVariableDeclarationList(idx) statement := &ast.VariableStatement{ Var: idx, List: list, } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(statement, comments, ast.LEADING) p.comments.Unset() } p.semicolon() return statement } func (p *parser) parseDoWhileStatement() ast.Statement { inIteration := p.scope.inIteration p.scope.inIteration = true defer func() { p.scope.inIteration = inIteration }() var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } idx := p.expect(token.DO) var doComments []*ast.Comment if p.mode&StoreComments != 0 { doComments = p.comments.FetchAll() } node := &ast.DoWhileStatement{Do: idx} if p.token == token.LEFT_BRACE { node.Body = p.parseBlockStatement() } else { node.Body = p.parseStatement() } p.expect(token.WHILE) var whileComments []*ast.Comment if p.mode&StoreComments != 0 { whileComments = p.comments.FetchAll() } p.expect(token.LEFT_PARENTHESIS) node.Test = p.parseExpression() node.RightParenthesis = p.expect(token.RIGHT_PARENTHESIS) p.implicitSemicolon = true p.optionalSemicolon() if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node, comments, ast.LEADING) p.comments.CommentMap.AddComments(node, doComments, ast.DO) p.comments.CommentMap.AddComments(node, whileComments, ast.WHILE) } return node } func (p *parser) parseWhileStatement() ast.Statement { var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } idx := p.expect(token.WHILE) var whileComments []*ast.Comment if p.mode&StoreComments != 0 { whileComments = p.comments.FetchAll() } p.expect(token.LEFT_PARENTHESIS) node := &ast.WhileStatement{ While: idx, Test: p.parseExpression(), } p.expect(token.RIGHT_PARENTHESIS) node.Body = p.parseIterationStatement() if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node, comments, ast.LEADING) p.comments.CommentMap.AddComments(node, whileComments, ast.WHILE) } return node } func (p *parser) parseIfStatement() ast.Statement { var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } pos := p.expect(token.IF) var ifComments []*ast.Comment if p.mode&StoreComments != 0 { ifComments = p.comments.FetchAll() } p.expect(token.LEFT_PARENTHESIS) node := &ast.IfStatement{ If: pos, Test: p.parseExpression(), } p.expect(token.RIGHT_PARENTHESIS) if p.token == token.LEFT_BRACE { node.Consequent = p.parseBlockStatement() } else { node.Consequent = p.parseStatement() } if p.token == token.ELSE { p.next() node.Alternate = p.parseStatement() } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(node, comments, ast.LEADING) p.comments.CommentMap.AddComments(node, ifComments, ast.IF) } return node } func (p *parser) parseSourceElement() ast.Statement { statement := p.parseStatement() return statement } func (p *parser) parseSourceElements() []ast.Statement { body := []ast.Statement(nil) for { if p.token != token.STRING { break } body = append(body, p.parseSourceElement()) } for p.token != token.EOF { body = append(body, p.parseSourceElement()) } return body } func (p *parser) parseProgram() *ast.Program { p.openScope() defer p.closeScope() return &ast.Program{ Body: p.parseSourceElements(), DeclarationList: p.scope.declarationList, File: p.file, } } func (p *parser) parseBreakStatement() ast.Statement { var comments []*ast.Comment if p.mode&StoreComments != 0 { comments = p.comments.FetchAll() } idx := p.expect(token.BREAK) semicolon := p.implicitSemicolon if p.token == token.SEMICOLON { semicolon = true p.next() } if semicolon || p.token == token.RIGHT_BRACE { p.implicitSemicolon = false if !p.scope.inIteration && !p.scope.inSwitch { goto illegal } breakStatement := &ast.BranchStatement{ Idx: idx, Token: token.BREAK, } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(breakStatement, comments, ast.LEADING) p.comments.CommentMap.AddComments(breakStatement, p.comments.FetchAll(), ast.TRAILING) } return breakStatement } if p.token == token.IDENTIFIER { identifier := p.parseIdentifier() if !p.scope.hasLabel(identifier.Name) { p.error(idx, "Undefined label '%s'", identifier.Name) return &ast.BadStatement{From: idx, To: identifier.Idx1()} } p.semicolon() breakStatement := &ast.BranchStatement{ Idx: idx, Token: token.BREAK, Label: identifier, } if p.mode&StoreComments != 0 { p.comments.CommentMap.AddComments(breakStatement, comments, ast.LEADING) } return breakStatement } p.expect(token.IDENTIFIER) illegal: p.error(idx, "Illegal break statement") p.nextStatement() return &ast.BadStatement{From: idx, To: p.idx} } func (p *parser) parseContinueStatement() ast.Statement { idx := p.expect(token.CONTINUE) semicolon := p.implicitSemicolon if p.token == token.SEMICOLON { semicolon = true p.next() } if semicolon || p.token == token.RIGHT_BRACE { p.implicitSemicolon = false if !p.scope.inIteration { goto illegal } return &ast.BranchStatement{ Idx: idx, Token: token.CONTINUE, } } if p.token == token.IDENTIFIER { identifier := p.parseIdentifier() if !p.scope.hasLabel(identifier.Name) { p.error(idx, "Undefined label '%s'", identifier.Name) return &ast.BadStatement{From: idx, To: identifier.Idx1()} } if !p.scope.inIteration { goto illegal } p.semicolon() return &ast.BranchStatement{ Idx: idx, Token: token.CONTINUE, Label: identifier, } } p.expect(token.IDENTIFIER) illegal: p.error(idx, "Illegal continue statement") p.nextStatement() return &ast.BadStatement{From: idx, To: p.idx} } // Find the next statement after an error (recover). func (p *parser) nextStatement() { for { switch p.token { case token.BREAK, token.CONTINUE, token.FOR, token.IF, token.RETURN, token.SWITCH, token.VAR, token.DO, token.TRY, token.WITH, token.WHILE, token.THROW, token.CATCH, token.FINALLY: // Return only if parser made some progress since last // sync or if it has not reached 10 next calls without // progress. Otherwise consume at least one token to // avoid an endless parser loop if p.idx == p.recover.idx && p.recover.count < 10 { p.recover.count++ return } if p.idx > p.recover.idx { p.recover.idx = p.idx p.recover.count = 0 return } // Reaching here indicates a parser bug, likely an // incorrect token list in this function, but it only // leads to skipping of possibly correct code if a // previous error is present, and thus is preferred // over a non-terminating parse. case token.EOF: return } p.next() } } ================================================ FILE: parser_test.go ================================================ package otto import ( "testing" ) func TestPersistence(t *testing.T) { tt(t, func() { test, _ := test() test(` function abc() { return 1; } abc.toString(); `, "function abc() { return 1; }") test(` function def() { return 3.14159; } [ abc.toString(), def.toString() ]; `, "function abc() { return 1; },function def() { return 3.14159; }") test(` eval("function ghi() { return 'ghi' }"); [ abc.toString(), def.toString(), ghi.toString() ]; `, "function abc() { return 1; },function def() { return 3.14159; },function ghi() { return 'ghi' }") test(` [ abc.toString(), def.toString(), ghi.toString() ]; `, "function abc() { return 1; },function def() { return 3.14159; },function ghi() { return 'ghi' }") test(`/* */`, UndefinedValue()) }) } ================================================ FILE: property.go ================================================ package otto // property type propertyMode int const ( modeWriteMask propertyMode = 0o700 modeEnumerateMask propertyMode = 0o070 modeConfigureMask propertyMode = 0o007 modeOnMask propertyMode = 0o111 modeSetMask propertyMode = 0o222 // If value is 2, then mode is neither "On" nor "Off" ) type propertyGetSet [2]*object var nilGetSetObject = object{} type property struct { value interface{} mode propertyMode } func (p property) writable() bool { return p.mode&modeWriteMask == modeWriteMask&modeOnMask } func (p *property) writeOn() { p.mode = (p.mode & ^modeWriteMask) | (modeWriteMask & modeOnMask) } func (p *property) writeOff() { p.mode &= ^modeWriteMask } func (p *property) writeClear() { p.mode = (p.mode & ^modeWriteMask) | (modeWriteMask & modeSetMask) } func (p property) writeSet() bool { return p.mode&modeWriteMask&modeSetMask == 0 } func (p property) enumerable() bool { return p.mode&modeEnumerateMask == modeEnumerateMask&modeOnMask } func (p *property) enumerateOn() { p.mode = (p.mode & ^modeEnumerateMask) | (modeEnumerateMask & modeOnMask) } func (p *property) enumerateOff() { p.mode &= ^modeEnumerateMask } func (p property) enumerateSet() bool { return p.mode&modeEnumerateMask&modeSetMask == 0 } func (p property) configurable() bool { return p.mode&modeConfigureMask == modeConfigureMask&modeOnMask } func (p *property) configureOn() { p.mode = (p.mode & ^modeConfigureMask) | (modeConfigureMask & modeOnMask) } func (p *property) configureOff() { p.mode &= ^modeConfigureMask } func (p property) configureSet() bool { //nolint:unused return p.mode&modeConfigureMask&modeSetMask == 0 } func (p property) copy() *property { //nolint:unused cpy := p return &cpy } func (p property) get(this *object) Value { switch value := p.value.(type) { case Value: return value case propertyGetSet: if value[0] != nil { return value[0].call(toValue(this), nil, false, nativeFrame) } } return Value{} } func (p property) isAccessorDescriptor() bool { setGet, test := p.value.(propertyGetSet) return test && (setGet[0] != nil || setGet[1] != nil) } func (p property) isDataDescriptor() bool { if p.writeSet() { // Either "On" or "Off" return true } value, valid := p.value.(Value) return valid && !value.isEmpty() } func (p property) isGenericDescriptor() bool { return !(p.isDataDescriptor() || p.isAccessorDescriptor()) } func (p property) isEmpty() bool { return p.mode == 0o222 && p.isGenericDescriptor() } // _enumerableValue, _enumerableTrue, _enumerableFalse? // .enumerableValue() .enumerableExists() func toPropertyDescriptor(rt *runtime, value Value) property { objectDescriptor := value.object() if objectDescriptor == nil { panic(rt.panicTypeError("toPropertyDescriptor on nil")) } var descriptor property descriptor.mode = modeSetMask // Initially nothing is set if objectDescriptor.hasProperty("enumerable") { if objectDescriptor.get("enumerable").bool() { descriptor.enumerateOn() } else { descriptor.enumerateOff() } } if objectDescriptor.hasProperty("configurable") { if objectDescriptor.get("configurable").bool() { descriptor.configureOn() } else { descriptor.configureOff() } } if objectDescriptor.hasProperty("writable") { if objectDescriptor.get("writable").bool() { descriptor.writeOn() } else { descriptor.writeOff() } } var getter, setter *object getterSetter := false if objectDescriptor.hasProperty("get") { val := objectDescriptor.get("get") if val.IsDefined() { if !val.isCallable() { panic(rt.panicTypeError("toPropertyDescriptor get not callable")) } getter = val.object() getterSetter = true } else { getter = &nilGetSetObject getterSetter = true } } if objectDescriptor.hasProperty("set") { val := objectDescriptor.get("set") if val.IsDefined() { if !val.isCallable() { panic(rt.panicTypeError("toPropertyDescriptor set not callable")) } setter = val.object() getterSetter = true } else { setter = &nilGetSetObject getterSetter = true } } if getterSetter { if descriptor.writeSet() { panic(rt.panicTypeError("toPropertyDescriptor descriptor writeSet")) } descriptor.value = propertyGetSet{getter, setter} } if objectDescriptor.hasProperty("value") { if getterSetter { panic(rt.panicTypeError("toPropertyDescriptor value getterSetter")) } descriptor.value = objectDescriptor.get("value") } return descriptor } func (rt *runtime) fromPropertyDescriptor(descriptor property) *object { obj := rt.newObject() if descriptor.isDataDescriptor() { obj.defineProperty("value", descriptor.value.(Value), 0o111, false) obj.defineProperty("writable", boolValue(descriptor.writable()), 0o111, false) } else if descriptor.isAccessorDescriptor() { getSet := descriptor.value.(propertyGetSet) get := Value{} if getSet[0] != nil { get = objectValue(getSet[0]) } set := Value{} if getSet[1] != nil { set = objectValue(getSet[1]) } obj.defineProperty("get", get, 0o111, false) obj.defineProperty("set", set, 0o111, false) } obj.defineProperty("enumerable", boolValue(descriptor.enumerable()), 0o111, false) obj.defineProperty("configurable", boolValue(descriptor.configurable()), 0o111, false) return obj } ================================================ FILE: reflect_test.go ================================================ package otto import ( "bytes" "encoding/json" "fmt" "math" "reflect" "strings" "testing" "github.com/stretchr/testify/require" ) type abcStruct struct { Jkl interface{} Pqr map[string]int8 Ghi string Mno _mnoStruct Def int Abc bool } func (abc abcStruct) String() string { return abc.Ghi } func (abc *abcStruct) FuncPointer() string { return "abc" } func (abc abcStruct) Func() { } func (abc abcStruct) FuncReturn1() string { return "abc" } func (abc abcStruct) FuncReturn2() (string, error) { return "def", nil } func (abc abcStruct) Func1Return1(a string) string { return a } func (abc abcStruct) Func2Return1(x, y string) string { return x + y } func (abc abcStruct) FuncEllipsis(xyz ...string) int { return len(xyz) } func (abc abcStruct) FuncReturnStruct() _mnoStruct { return _mnoStruct{} } func (abc abcStruct) Func1Int(i int) int { return i + 1 } func (abc abcStruct) Func1Int8(i int8) int8 { return i + 1 } func (abc abcStruct) Func1Int16(i int16) int16 { return i + 1 } func (abc abcStruct) Func1Int32(i int32) int32 { return i + 1 } func (abc abcStruct) Func1Int64(i int64) int64 { return i + 1 } func (abc abcStruct) Func1Uint(i uint) uint { return i + 1 } func (abc abcStruct) Func1Uint8(i uint8) uint8 { return i + 1 } func (abc abcStruct) Func1Uint16(i uint16) uint16 { return i + 1 } func (abc abcStruct) Func1Uint32(i uint32) uint32 { return i + 1 } func (abc abcStruct) Func1Uint64(i uint64) uint64 { return i + 1 } func (abc abcStruct) Func2Int(i, j int) int { return i + j } func (abc abcStruct) Func2StringInt(s string, i int) string { return fmt.Sprintf("%v:%v", s, i) } func (abc abcStruct) Func1IntVariadic(a ...int) int { t := 0 for _, i := range a { t += i } return t } func (abc abcStruct) Func2IntVariadic(s string, a ...int) string { t := 0 for _, i := range a { t += i } return fmt.Sprintf("%v:%v", s, t) } func (abc abcStruct) Func2IntArrayVariadic(s string, a ...[]int) string { t := 0 for _, i := range a { for _, j := range i { t += j } } return fmt.Sprintf("%v:%v", s, t) } type _mnoStruct struct { Ghi string } func (mno _mnoStruct) Func() string { return "mno" } func TestReflect(t *testing.T) { tt(t, func() { // Testing dbgf // These should panic str := "test" require.Panics(t, func() { _, err := toValue("Xyzzy").toReflectValue(reflect.ValueOf(&str).Type()) require.NoError(t, err) }) require.Panics(t, func() { _, err := stringToReflectValue("Xyzzy", reflect.Ptr) require.NoError(t, err) }) }) } func Test_reflectStruct(t *testing.T) { tt(t, func() { test, vm := test() // _abcStruct { abc := &abcStruct{} vm.Set("abc", abc) test(` [ abc.Abc, abc.Ghi ]; `, "false,") abc.Abc = true abc.Ghi = "Nothing happens." test(` [ abc.Abc, abc.Ghi ]; `, "true,Nothing happens.") *abc = abcStruct{} test(` [ abc.Abc, abc.Ghi ]; `, "false,") abc.Abc = true abc.Ghi = "Xyzzy" vm.Set("abc", abc) test(` [ abc.Abc, abc.Ghi ]; `, "true,Xyzzy") is(abc.Abc, true) test(` abc.Abc = false; abc.Def = 451; abc.Ghi = "Nothing happens."; abc.abc = "Something happens."; [ abc.Def, abc.abc ]; `, "451,Something happens.") is(abc.Abc, false) is(abc.Def, 451) is(abc.Ghi, "Nothing happens.") test(` delete abc.Def; delete abc.abc; [ abc.Def, abc.abc ]; `, "451,") is(abc.Def, 451) test(` abc.FuncPointer(); `, "abc") test(` abc.Func(); `, "undefined") test(` abc.FuncReturn1(); `, "abc") test(` abc.Func1Return1("abc"); `, "abc") test(` abc.Func2Return1("abc", "def"); `, "abcdef") test(` abc.FuncEllipsis("abc", "def", "ghi"); `, 3) test(` ret = abc.FuncReturn2(); if (ret && ret.length && ret.length == 2 && ret[0] == "def" && ret[1] === undefined) { true; } else { false; } `, true) test(` abc.FuncReturnStruct(); `, "[object Object]") test(` abc.FuncReturnStruct().Func(); `, "mno") test(` abc.Func1Int(1); `, 2) test(` abc.Func1Int(0x01 & 0x01); `, 2) test(`raise: abc.Func1Int(1.1); `, "RangeError: converting float64 to int would cause loss of precision") test(` var v = 1; abc.Func1Int(v + 1); `, 3) test(` abc.Func2Int(1, 2); `, 3) test(` abc.Func1Int8(1); `, 2) test(` abc.Func1Int16(1); `, 2) test(` abc.Func1Int32(1); `, 2) test(` abc.Func1Int64(1); `, 2) test(` abc.Func1Uint(1); `, 2) test(` abc.Func1Uint8(1); `, 2) test(` abc.Func1Uint16(1); `, 2) test(` abc.Func1Uint32(1); `, 2) test(` abc.Func1Uint64(1); `, 2) test(` abc.Func2StringInt("test", 1); `, "test:1") test(` abc.Func1IntVariadic(1, 2); `, 3) test(` abc.Func2IntVariadic("test", 1, 2); `, "test:3") test(` abc.Func2IntVariadic("test", [1, 2]); `, "test:3") test(` abc.Func2IntArrayVariadic("test", [1, 2]); `, "test:3") test(` abc.Func2IntArrayVariadic("test", [1, 2], [3, 4]); `, "test:10") test(` abc.Func2IntArrayVariadic("test", [[1, 2], [3, 4]]); `, "test:10") } }) } func Test_reflectMap(t *testing.T) { tt(t, func() { test, vm := test() // map[string]string { abc := map[string]string{ "Xyzzy": "Nothing happens.", "def": "1", } vm.Set("abc", abc) test(` abc.xyz = "pqr"; [ abc.Xyzzy, abc.def, abc.ghi ]; `, "Nothing happens.,1,") is(abc["xyz"], "pqr") } // map[string]float64 { abc := map[string]float64{ "Xyzzy": math.Pi, "def": 1, } vm.Set("abc", abc) test(` abc.xyz = "pqr"; abc.jkl = 10; [ abc.Xyzzy, abc.def, abc.ghi ]; `, "3.141592653589793,1,") is(abc["xyz"], math.NaN()) is(abc["jkl"], float64(10)) } // map[string]int32 { abc := map[string]int32{ "Xyzzy": 3, "def": 1, } vm.Set("abc", abc) test(` abc.xyz = "pqr"; abc.jkl = 10; [ abc.Xyzzy, abc.def, abc.ghi ]; `, "3,1,") is(abc["xyz"], 0) is(abc["jkl"], int32(10)) test(` delete abc["Xyzzy"]; `) _, exists := abc["Xyzzy"] is(exists, false) is(abc["Xyzzy"], 0) } // map[int32]string { abc := map[int32]string{ 0: "abc", 1: "def", } vm.Set("abc", abc) test(` abc[2] = "pqr"; //abc.jkl = 10; abc[3] = 10; [ abc[0], abc[1], abc[2], abc[3] ] `, "abc,def,pqr,10") is(abc[2], "pqr") is(abc[3], "10") test(` delete abc[2]; `) _, exists := abc[2] is(exists, false) } }) } func Test_reflectMapIterateKeys(t *testing.T) { tt(t, func() { test, vm := test() // map[string]interface{} { abc := map[string]interface{}{ "Xyzzy": "Nothing happens.", "def": 1, } vm.Set("abc", abc) test(` var keys = []; for (var key in abc) { keys.push(key); } keys.sort(); keys; `, "Xyzzy,def") } // map[uint]interface{} { abc := map[uint]interface{}{ 456: "Nothing happens.", 123: 1, } vm.Set("abc", abc) test(` var keys = []; for (var key in abc) { keys.push(key); } keys.sort(); keys; `, "123,456") } // map[byte]interface{} { abc := map[byte]interface{}{ 10: "Nothing happens.", 20: 1, } vm.Set("abc", abc) test(` for (var key in abc) { abc[key] = "123"; } `) is(abc[10], "123") is(abc[20], "123") } }) } func Test_reflectSlice(t *testing.T) { tt(t, func() { test, vm := test() // []bool { abc := []bool{ false, true, true, false, } vm.Set("abc", abc) test(` abc; `, "false,true,true,false") test(` abc[0] = true; abc[abc.length-1] = true; delete abc[2]; abc; `, "true,true,false,true") is(abc, []bool{true, true, false, true}) is(abc[len(abc)-1], true) } // []int32 { abc := make([]int32, 4) vm.Set("abc", abc) test(` abc; `, "0,0,0,0") test(`raise: abc[0] = "42"; abc[1] = 4.2; abc[2] = 3.14; abc; `, "RangeError: 4.2 to reflect.Kind: int32") is(abc, []int32{42, 0, 0, 0}) test(` delete abc[1]; delete abc[2]; `) is(abc[1], 0) is(abc[2], 0) } }) } func Test_reflectArray(t *testing.T) { tt(t, func() { test, vm := test() // []bool { abc := [4]bool{ false, true, true, false, } vm.Set("abc", abc) test(` abc; `, "false,true,true,false") // Unaddressable array test(` abc[0] = true; abc[abc.length-1] = true; abc; `, "false,true,true,false") // Again, unaddressable array is(abc, [4]bool{false, true, true, false}) is(abc[len(abc)-1], false) // ... } // []int32 { abc := make([]int32, 4) vm.Set("abc", abc) test(` abc; `, "0,0,0,0") test(`raise: abc[0] = "42"; abc[1] = 4.2; abc[2] = 3.14; abc; `, "RangeError: 4.2 to reflect.Kind: int32") is(abc, []int32{42, 0, 0, 0}) } // []bool { abc := [4]bool{ false, true, true, false, } vm.Set("abc", &abc) test(` abc; `, "false,true,true,false") test(` abc[0] = true; abc[abc.length-1] = true; delete abc[2]; abc; `, "true,true,false,true") is(abc, [4]bool{true, true, false, true}) is(abc[len(abc)-1], true) } // no common type { test(` abc = [1, 2.2, "str"]; abc; `, "1,2.2,str") val, err := vm.Get("abc") is(err, nil) abc, err := val.Export() is(err, nil) is(abc, []interface{}{int64(1), 2.2, "str"}) } // common type int { test(` abc = [1, 2, 3]; abc; `, "1,2,3") val, err := vm.Get("abc") is(err, nil) abc, err := val.Export() is(err, nil) is(abc, []int64{1, 2, 3}) } // common type string { test(` abc = ["str1", "str2", "str3"]; abc; `, "str1,str2,str3") val, err := vm.Get("abc") is(err, nil) abc, err := val.Export() is(err, nil) is(abc, []string{"str1", "str2", "str3"}) } // issue #269 { called := false vm.Set("blah", func(c FunctionCall) Value { v, err := c.Argument(0).Export() is(err, nil) is(v, []int64{3}) called = true return UndefinedValue() }) is(called, false) test(`var x = 3; blah([x])`) is(called, true) } }) } func Test_reflectArray_concat(t *testing.T) { tt(t, func() { test, vm := test() vm.Set("ghi", []string{"jkl", "mno"}) vm.Set("pqr", []interface{}{"jkl", 42, 3.14159, true}) test(` var def = { "abc": ["abc"], "xyz": ["xyz"] }; xyz = pqr.concat(ghi, def.abc, def, def.xyz); [ xyz, xyz.length ]; `, "jkl,42,3.14159,true,jkl,mno,abc,[object Object],xyz,9") }) } func Test_reflectMapInterface(t *testing.T) { tt(t, func() { test, vm := test() { abc := map[string]interface{}{ "Xyzzy": "Nothing happens.", "def": "1", "jkl": "jkl", } vm.Set("abc", abc) vm.Set("mno", &abcStruct{}) test(` abc.xyz = "pqr"; abc.ghi = {}; abc.jkl = 3.14159; abc.mno = mno; mno.Abc = true; mno.Ghi = "Something happens."; [ abc.Xyzzy, abc.def, abc.ghi, abc.mno ]; `, "Nothing happens.,1,[object Object],[object Object]") is(abc["xyz"], "pqr") is(abc["ghi"], map[string]interface{}{}) is(abc["jkl"], float64(3.14159)) mno, valid := abc["mno"].(*abcStruct) is(valid, true) is(mno.Abc, true) is(mno.Ghi, "Something happens.") } }) } func TestPassthrough(t *testing.T) { tt(t, func() { test, vm := test() { abc := &abcStruct{ Mno: _mnoStruct{ Ghi: "", }, } vm.Set("abc", abc) test(` abc.Mno.Ghi; `, "") vm.Set("pqr", map[string]int8{ "xyzzy": 0, "Nothing happens.": 1, }) test(` abc.Ghi = "abc"; abc.Pqr = pqr; abc.Pqr["Nothing happens."]; `, 1) mno := _mnoStruct{ Ghi: "", } vm.Set("mno", mno) test(` abc.Mno = mno; abc.Mno.Ghi; `, "") } }) } type TestDynamicFunctionReturningInterfaceMyStruct1 struct{} //nolint:errname func (m *TestDynamicFunctionReturningInterfaceMyStruct1) Error() string { return "MyStruct1" } type TestDynamicFunctionReturningInterfaceMyStruct2 struct{} //nolint:errname func (m *TestDynamicFunctionReturningInterfaceMyStruct2) Error() string { return "MyStruct2" } func TestDynamicFunctionReturningInterface(t *testing.T) { tt(t, func() { test, vm := test() var l []func() error vm.Set("r", func(cb func() error) { l = append(l, cb) }) vm.Set("e1", func() error { return &TestDynamicFunctionReturningInterfaceMyStruct1{} }) vm.Set("e2", func() error { return &TestDynamicFunctionReturningInterfaceMyStruct2{} }) vm.Set("e3", func() error { return nil }) test("r(function() { return e1(); })", UndefinedValue()) test("r(function() { return e2(); })", UndefinedValue()) test("r(function() { return e3(); })", UndefinedValue()) test("r(function() { return null; })", UndefinedValue()) if l[0]() == nil { t.Fail() } if l[1]() == nil { t.Fail() } if l[2]() != nil { t.Fail() } if l[3]() != nil { t.Fail() } }) } func TestStructCallParameterConversion(t *testing.T) { tt(t, func() { test, vm := test() type T struct { StringValue string `json:"s"` BooleanValue bool `json:"b"` IntegerValue int `json:"i"` } var x T vm.Set("f", func(t T) bool { return t == x }) // test set properties x = T{"A", true, 1} test("f({s: 'A', b: true, i: 1})", true) // test zero-value properties x = T{"", false, 0} test("f({s: '', b: false, i: 0})", true) // test missing properties x = T{"", true, 1} test("f({b: true, i: 1})", true) x = T{"A", false, 1} test("f({s: 'A', i: 1})", true) x = T{"A", true, 0} test("f({s: 'A', b: true})", true) x = T{"", false, 0} test("f({})", true) // make sure it fails with extra properties x = T{"", false, 0} if _, err := vm.Run("f({x: true})"); err == nil { t.Fail() } }) } type TestTextUnmarshallerCallParameterConversionMyStruct struct{} func (m *TestTextUnmarshallerCallParameterConversionMyStruct) UnmarshalText(b []byte) error { if string(b) == "good" { return nil } return fmt.Errorf("NOT_GOOD: %s", string(b)) } func TestTextUnmarshallerCallParameterConversion(t *testing.T) { tt(t, func() { test, vm := test() vm.Set("f", func(t TestTextUnmarshallerCallParameterConversionMyStruct) bool { return true }) // success test("f('good')", true) // explicit failure, should pass error message up if _, err := vm.Run("f('bad')"); err == nil || !strings.Contains(err.Error(), "NOT_GOOD: bad") { t.Fail() } // wrong input if _, err := vm.Run("f(null)"); err == nil { t.Fail() } // no input if _, err := vm.Run("f()"); err == nil { t.Fail() } }) } func TestJSONRawMessageCallParameterConversion(t *testing.T) { for _, e := range []struct { c string r string e bool }{ {"f({a:1})", `{"a":1}`, false}, {"f(null)", `null`, false}, {"f(1)", `1`, false}, {"f('x')", `"x"`, false}, {"f([1,2,3])", `[1,2,3]`, false}, {"f(function(){})", `{}`, false}, {"f()", `[1,2,3]`, true}, } { t.Run(e.c, func(t *testing.T) { vm := New() err := vm.Set("f", func(m json.RawMessage) json.RawMessage { return m }) require.NoError(t, err) r, err := vm.Run(e.c) if err != nil { if !e.e { t.Error("err should be nil") t.Fail() } return } if e.e { t.Error("err should not be nil") t.Fail() return } v, err := r.Export() if err != nil { t.Error(err) t.Fail() } m, ok := v.(json.RawMessage) if !ok { t.Error("result should be json.RawMessage") t.Fail() } if !bytes.Equal(m, json.RawMessage(e.r)) { t.Errorf("output is wrong\nexpected: %s\nactual: %s\n", e.r, m) t.Fail() } }) } } ================================================ FILE: regexp_test.go ================================================ package otto import ( "fmt" "testing" ) func TestRegExp(t *testing.T) { tt(t, func() { test, _ := test() test(` [ /abc/.toString(), /abc/gim.toString(), ""+/abc/gi.toString(), new RegExp("1(\\d+)").toString(), ]; `, "/abc/,/abc/gim,/abc/gi,/1(\\d+)/") test(` [ new RegExp("abc").exec("123abc456"), null === new RegExp("xyzzy").exec("123abc456"), new RegExp("1(\\d+)").exec("123abc456"), new RegExp("xyzzy").test("123abc456"), new RegExp("1(\\d+)").test("123abc456"), new RegExp("abc").exec("123abc456"), ]; `, "abc,true,123,23,false,true,abc") test(`new RegExp("abc").toString()`, "/abc/") test(`new RegExp("abc", "g").toString()`, "/abc/g") test(`new RegExp("abc", "mig").toString()`, "/abc/gim") result := test(`/(a)?/.exec('b')`, ",") is(result.object().get("0"), "") is(result.object().get("1"), "undefined") is(result.object().get(propertyLength), 2) result = test(`/(a)?(b)?/.exec('b')`, "b,,b") is(result.object().get("0"), "b") is(result.object().get("1"), "undefined") is(result.object().get("2"), "b") is(result.object().get(propertyLength), 3) test(`/\u0041/.source`, "\\u0041") test(`/\a/.source`, "\\a") test(`/\;/.source`, "\\;") test(`/a\a/.source`, "a\\a") test(`/,\;/.source`, ",\\;") test(`/ \ /.source`, " \\ ") // Start sanity check... test("eval(\"/abc/\").source", "abc") test("eval(\"/\u0023/\").source", "#") test("eval(\"/\u0058/\").source", "X") test("eval(\"/\\\u0023/\").source == \"\\\u0023\"", true) test("'0x' + '0058'", "0x0058") test("'\\\\' + '0x' + '0058'", "\\0x0058") // ...stop sanity check test(`abc = '\\' + String.fromCharCode('0x' + '0058'); eval('/' + abc + '/').source`, "\\X") test(`abc = '\\' + String.fromCharCode('0x0058'); eval('/' + abc + '/').source == "\\\u0058"`, true) test(`abc = '\\' + String.fromCharCode('0x0023'); eval('/' + abc + '/').source == "\\\u0023"`, true) test(`abc = '\\' + String.fromCharCode('0x0078'); eval('/' + abc + '/').source == "\\\u0078"`, true) test(` var abc = Object.getOwnPropertyDescriptor(RegExp, "prototype"); [ [ typeof RegExp.prototype ], [ abc.writable, abc.enumerable, abc.configurable ] ]; `, "object,false,false,false") }) } func TestRegExp_global(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = /(?:ab|cd)\d?/g; var found = []; do { match = abc.exec("ab cd2 ab34 cd"); if (match !== null) { found.push(match[0]); } else { break; } } while (true); found; `, "ab,cd2,ab3,cd") }) } func TestRegExp_exec(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = /./g; def = '123456'; ghi = 0; while (ghi < 100 && abc.exec(def) !== null) { ghi += 1; } [ ghi, def.length, ghi == def.length ]; `, "6,6,true") test(` abc = /[abc](\d)?/g; def = 'a0 b c1 d3'; ghi = 0; lastIndex = 0; while (ghi < 100 && abc.exec(def) !== null) { lastIndex = abc.lastIndex; ghi += 1; } [ ghi, lastIndex ]; `, "3,7") test(` abc = /(\d)?(s)/g; def = 's'; ghi = abc.exec(def); [ ghi[1] === undefined, ghi[2] === 's' ]; `, "true,true") test(` abc = /(\d)?(s)/g; def = '%s'; abc.lastIndex = 1; ghi = abc.exec(def); [ ghi[1] === undefined, ghi[2] === 's' ]; `, "true,true") test(` var abc = /[abc](\d)?/.exec("a0 b c1 d3"); [ abc.length, abc.input, abc.index, abc ]; `, "2,a0 b c1 d3,0,a0,0") test(`raise: var exec = RegExp.prototype.exec; exec("Xyzzy"); `, "TypeError: Calling RegExp.exec on a non-RegExp object") test(` var abc = /\w{3}\d?/.exec("CE\uFFFFL\uFFDDbox127"); [ abc.input.length, abc.length, abc.input, abc.index, abc ]; `, "11,1,CE\uFFFFL\uFFDDbox127,5,box1") test(` var abc = /\w{3}\d?/.exec("CE😋box127"); [ abc.input.length, abc.length, abc.input, abc.index, abc ]; `, "10,1,CE😋box127,4,box1") test(`RegExp.prototype.exec.length`, 1) test(`RegExp.prototype.exec.prototype`, "undefined") }) } func TestRegExp_test(t *testing.T) { tt(t, func() { test, _ := test() test(`RegExp.prototype.test.length`, 1) test(`RegExp.prototype.test.prototype`, "undefined") }) } func TestRegExp_toString(t *testing.T) { tt(t, func() { test, _ := test() test(`RegExp.prototype.toString.length`, 0) test(`RegExp.prototype.toString.prototype`, "undefined") }) } func TestRegExp_zaacbbbcac(t *testing.T) { if true { return } tt(t, func() { test, _ := test() // FIXME? TODO /(z)((a+)?(b+)?(c))*/.exec("zaacbbbcac") test(` var abc = /(z)((a+)?(b+)?(c))*/.exec("zaacbbbcac"); [ abc.length, abc.index, abc ]; `, "6,0,zaacbbbcac,z,ac,a,,c") }) } func TestRegExpCopying(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = /xyzzy/i; def = RegExp(abc); abc.indicator = 1; [ abc.indicator, def.indicator ]; `, "1,1") test(`raise: RegExp(new RegExp("\\d"), "1"); `, "TypeError: Cannot supply flags when constructing one RegExp from another") }) } func TestRegExp_multiline(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = /s$/m.exec("pairs\nmakes\tdouble"); [ abc.length, abc.index, abc ]; `, "1,4,s") }) } func TestRegExp_source(t *testing.T) { tt(t, func() { test, _ := test() test(` [ /xyzzy/i.source, /./i.source ]; `, "xyzzy,.") test(` var abc = /./i; var def = new RegExp(abc); [ abc.source, def.source, abc.source === def.source ]; `, ".,.,true") test(` var abc = /./i; var def = abc.hasOwnProperty("source"); var ghi = abc.source; abc.source = "xyzzy"; [ def, abc.source ]; `, "true,.") }) } func TestRegExp_newRegExp(t *testing.T) { tt(t, func() { test, _ := test() test(` Math.toString(); var abc = new RegExp(Math,eval("\"g\"")); [ abc, abc.global ]; `, "/[object Math]/g,true") }) } func TestRegExp_flags(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = /./i; var def = new RegExp(abc); [ abc.multiline == def.multiline, abc.global == def.global, abc.ignoreCase == def.ignoreCase ]; `, "true,true,true") }) } func TestRegExp_controlCharacter(t *testing.T) { tt(t, func() { test, _ := test() for code := 0x41; code < 0x5a; code++ { val := string(rune(code - 64)) test(fmt.Sprintf(` var code = 0x%x; var string = String.fromCharCode(code %% 32); var result = (new RegExp("\\c" + String.fromCharCode(code))).exec(string); [ code, string, result ]; `, code), fmt.Sprintf("%d,%s,%s", code, val, val)) } }) } func TestRegExp_notNotEmptyCharacterClass(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = /[\s\S]a/m.exec("a\naba"); [ abc.length, abc.input, abc ]; `, "1,a\naba,\na") }) } func TestRegExp_compile(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = /[\s\S]a/; abc.compile('^\w+'); `, "undefined") }) } ================================================ FILE: registry/registry.go ================================================ // Package registry is an experimental package to facilitate altering the otto runtime via import. // // This interface can change at any time. package registry var registry []*Entry = make([]*Entry, 0) // Entry represents a registry entry. type Entry struct { source func() string active bool } // newEntry returns a new Entry for source. func newEntry(source func() string) *Entry { return &Entry{ active: true, source: source, } } // Enable enables the entry. func (e *Entry) Enable() { e.active = true } // Disable disables the entry. func (e *Entry) Disable() { e.active = false } // Source returns the source of the entry. func (e Entry) Source() string { return e.source() } // Apply applies callback to all registry entries. func Apply(callback func(Entry)) { for _, entry := range registry { if !entry.active { continue } callback(*entry) } } // Register registers a new Entry for source. func Register(source func() string) *Entry { entry := newEntry(source) registry = append(registry, entry) return entry } ================================================ FILE: repl/autocompleter.go ================================================ package repl import ( "regexp" "strings" "github.com/robertkrimen/otto" ) type autoCompleter struct { vm *otto.Otto } var lastExpressionRegex = regexp.MustCompile(`[a-zA-Z0-9]([a-zA-Z0-9\.]*[a-zA-Z0-9])?\.?$`) func (a *autoCompleter) Do(line []rune, pos int) ([][]rune, int) { lastExpression := lastExpressionRegex.FindString(string(line)) bits := strings.Split(lastExpression, ".") var l []string if first := bits[:len(bits)-1]; len(first) == 0 { c := a.vm.Context() l = make([]string, len(c.Symbols)) i := 0 for k := range c.Symbols { l[i] = k i++ } } else { r, err := a.vm.Eval(strings.Join(bits[:len(bits)-1], ".")) if err != nil { return nil, 0 } if o := r.Object(); o != nil { for _, v := range o.KeysByParent() { l = append(l, v...) //nolint:makezero } } } last := bits[len(bits)-1] var r [][]rune for _, s := range l { if strings.HasPrefix(s, last) { r = append(r, []rune(strings.TrimPrefix(s, last))) } } return r, len(last) } ================================================ FILE: repl/repl.go ================================================ // Package repl implements a REPL (read-eval-print loop) for otto. package repl import ( "errors" "fmt" "io" "strings" "sync/atomic" "github.com/robertkrimen/otto" "gopkg.in/readline.v1" ) var counter uint32 // DebuggerHandler implements otto's debugger handler signature, providing a // simple drop-in debugger implementation. func DebuggerHandler(vm *otto.Otto) { i := atomic.AddUint32(&counter, 1) // purposefully ignoring the error here - we can't do anything useful with // it except panicking, and that'd be pretty rude. it'd be easy enough for a // consumer to define an equivalent function that _does_ panic if desired. _ = RunWithPrompt(vm, fmt.Sprintf("DEBUGGER[%d]> ", i)) } // Run creates a REPL with the default prompt and no prelude. func Run(vm *otto.Otto) error { return RunWithOptions(vm, Options{}) } // RunWithPrompt runs a REPL with the given prompt and no prelude. func RunWithPrompt(vm *otto.Otto, prompt string) error { return RunWithOptions(vm, Options{ Prompt: prompt, }) } // RunWithPrelude runs a REPL with the default prompt and the given prelude. func RunWithPrelude(vm *otto.Otto, prelude string) error { return RunWithOptions(vm, Options{ Prelude: prelude, }) } // RunWithPromptAndPrelude runs a REPL with the given prompt and prelude. func RunWithPromptAndPrelude(vm *otto.Otto, prompt, prelude string) error { return RunWithOptions(vm, Options{ Prompt: prompt, Prelude: prelude, }) } // Options contains parameters for configuring a REPL session. type Options struct { // Prompt controls what's shown at the beginning of the line. If not // specified, this defaults to "> " Prompt string // Prelude is some text that's printed before control is given to the user. // By default this is empty. If specified, this will be printed with a // newline following it. Prelude string // Autocomplete controls whether this REPL session has autocompletion // enabled. The way autocomplete is implemented can incur a performance // penalty, so it's turned off by default. Autocomplete bool } // RunWithOptions runs a REPL with the given options. func RunWithOptions(vm *otto.Otto, options Options) error { prompt := options.Prompt if prompt == "" { prompt = "> " } c := &readline.Config{ Prompt: prompt, } if options.Autocomplete { c.AutoComplete = &autoCompleter{vm} } rl, err := readline.NewEx(c) if err != nil { return err } prelude := options.Prelude if prelude != "" { if _, err = io.Copy(rl.Stderr(), strings.NewReader(prelude+"\n")); err != nil { return err } rl.Refresh() } var d []string for { l, errRL := rl.Readline() if errRL != nil { if errors.Is(errRL, readline.ErrInterrupt) { if d != nil { d = nil rl.SetPrompt(prompt) rl.Refresh() continue } break } return errRL } if l == "" { continue } d = append(d, l) s, errRL := vm.Compile("repl", strings.Join(d, "\n")) if errRL != nil { rl.SetPrompt(strings.Repeat(" ", len(prompt))) } else { rl.SetPrompt(prompt) d = nil v, errEval := vm.Eval(s) if errEval != nil { var oerr *otto.Error if errors.As(errEval, &oerr) { if _, err = io.Copy(rl.Stdout(), strings.NewReader(oerr.String())); err != nil { return fmt.Errorf("write out: %w", err) } } else { if _, err = io.Copy(rl.Stdout(), strings.NewReader(errEval.Error())); err != nil { return fmt.Errorf("write out: %w", err) } } } else { if _, err = rl.Stdout().Write([]byte(v.String() + "\n")); err != nil { return fmt.Errorf("write out: %w", err) } } } rl.Refresh() } return rl.Close() } ================================================ FILE: result.go ================================================ package otto type resultKind int const ( _ resultKind = iota resultReturn resultBreak resultContinue ) type result struct { value Value target string kind resultKind } func newReturnResult(value Value) result { return result{kind: resultReturn, value: value, target: ""} } func newContinueResult(target string) result { return result{kind: resultContinue, value: emptyValue, target: target} } func newBreakResult(target string) result { return result{kind: resultBreak, value: emptyValue, target: target} } ================================================ FILE: runtime.go ================================================ package otto import ( "encoding" "encoding/json" "errors" "fmt" "math" "path" "reflect" goruntime "runtime" "strconv" "strings" "sync" "github.com/robertkrimen/otto/ast" "github.com/robertkrimen/otto/parser" ) type global struct { Object *object // Object( ... ), new Object( ... ) - 1 (length) Function *object // Function( ... ), new Function( ... ) - 1 Array *object // Array( ... ), new Array( ... ) - 1 String *object // String( ... ), new String( ... ) - 1 Boolean *object // Boolean( ... ), new Boolean( ... ) - 1 Number *object // Number( ... ), new Number( ... ) - 1 Math *object Date *object // Date( ... ), new Date( ... ) - 7 RegExp *object // RegExp( ... ), new RegExp( ... ) - 2 Error *object // Error( ... ), new Error( ... ) - 1 EvalError *object TypeError *object RangeError *object ReferenceError *object SyntaxError *object URIError *object JSON *object ObjectPrototype *object // Object.prototype FunctionPrototype *object // Function.prototype ArrayPrototype *object // Array.prototype StringPrototype *object // String.prototype BooleanPrototype *object // Boolean.prototype NumberPrototype *object // Number.prototype DatePrototype *object // Date.prototype RegExpPrototype *object // RegExp.prototype ErrorPrototype *object // Error.prototype EvalErrorPrototype *object TypeErrorPrototype *object RangeErrorPrototype *object ReferenceErrorPrototype *object SyntaxErrorPrototype *object URIErrorPrototype *object } type runtime struct { global global globalObject *object globalStash *objectStash scope *scope otto *Otto eval *object debugger func(*Otto) random func() float64 labels []string stackLimit int traceLimit int lck sync.Mutex } func (rt *runtime) enterScope(scop *scope) { scop.outer = rt.scope if rt.scope != nil { if rt.stackLimit != 0 && rt.scope.depth+1 >= rt.stackLimit { panic(rt.panicRangeError("Maximum call stack size exceeded")) } scop.depth = rt.scope.depth + 1 } rt.scope = scop } func (rt *runtime) leaveScope() { rt.scope = rt.scope.outer } // FIXME This is used in two places (cloning). func (rt *runtime) enterGlobalScope() { rt.enterScope(newScope(rt.globalStash, rt.globalStash, rt.globalObject)) } func (rt *runtime) enterFunctionScope(outer stasher, this Value) *fnStash { if outer == nil { outer = rt.globalStash } stash := rt.newFunctionStash(outer) var thisObject *object switch this.kind { case valueUndefined, valueNull: thisObject = rt.globalObject default: thisObject = rt.toObject(this) } rt.enterScope(newScope(stash, stash, thisObject)) return stash } func (rt *runtime) putValue(reference referencer, value Value) { name := reference.putValue(value) if name != "" { // Why? -- If reference.base == nil // strict = false rt.globalObject.defineProperty(name, value, 0o111, false) } } func (rt *runtime) tryCatchEvaluate(inner func() Value) (tryValue Value, isException bool) { //nolint:nonamedreturns // resultValue = The value of the block (e.g. the last statement) // throw = Something was thrown // throwValue = The value of what was thrown // other = Something that changes flow (return, break, continue) that is not a throw // Otherwise, some sort of unknown panic happened, we'll just propagate it. defer func() { if caught := recover(); caught != nil { if excep, ok := caught.(*exception); ok { caught = excep.eject() } switch caught := caught.(type) { case ottoError: isException = true tryValue = objectValue(rt.newErrorObjectError(caught)) case Value: isException = true tryValue = caught default: isException = true tryValue = toValue(caught) } } }() return inner(), false } func (rt *runtime) toObject(value Value) *object { switch value.kind { case valueEmpty, valueUndefined, valueNull: panic(rt.panicTypeError("toObject unsupported kind %s", value.kind)) case valueBoolean: return rt.newBoolean(value) case valueString: return rt.newString(value) case valueNumber: return rt.newNumber(value) case valueObject: return value.object() default: panic(rt.panicTypeError("toObject unknown kind %s", value.kind)) } } func (rt *runtime) objectCoerce(value Value) (*object, error) { switch value.kind { case valueUndefined: return nil, errors.New("undefined") case valueNull: return nil, errors.New("null") case valueBoolean: return rt.newBoolean(value), nil case valueString: return rt.newString(value), nil case valueNumber: return rt.newNumber(value), nil case valueObject: return value.object(), nil default: panic(rt.panicTypeError("objectCoerce unknown kind %s", value.kind)) } } func checkObjectCoercible(rt *runtime, value Value) { isObject, mustCoerce := testObjectCoercible(value) if !isObject && !mustCoerce { panic(rt.panicTypeError("checkObjectCoercible not object or mustCoerce")) } } // testObjectCoercible. func testObjectCoercible(value Value) (isObject, mustCoerce bool) { //nolint:nonamedreturns switch value.kind { case valueReference, valueEmpty, valueNull, valueUndefined: return false, false case valueNumber, valueString, valueBoolean: return false, true case valueObject: return true, false default: panic(fmt.Sprintf("testObjectCoercible unknown kind %s", value.kind)) } } func (rt *runtime) safeToValue(value interface{}) (Value, error) { result := Value{} err := catchPanic(func() { result = rt.toValue(value) }) return result, err } // convertNumeric converts numeric parameter val from js to that of type t if it is safe to do so, otherwise it panics. // This allows literals (int64), bitwise values (int32) and the general form (float64) of javascript numerics to be passed as parameters to go functions easily. func (rt *runtime) convertNumeric(v Value, t reflect.Type) reflect.Value { val := reflect.ValueOf(v.export()) if val.Kind() == t.Kind() { return val } if val.Kind() == reflect.Interface { val = reflect.ValueOf(val.Interface()) } switch val.Kind() { case reflect.Float32, reflect.Float64: f64 := val.Float() switch t.Kind() { case reflect.Float64: return reflect.ValueOf(f64) case reflect.Float32: if reflect.Zero(t).OverflowFloat(f64) { panic(rt.panicRangeError("converting float64 to float32 would overflow")) } return val.Convert(t) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: i64 := int64(f64) if float64(i64) != f64 { panic(rt.panicRangeError(fmt.Sprintf("converting %v to %v would cause loss of precision", val.Type(), t))) } // The float represents an integer val = reflect.ValueOf(i64) default: panic(rt.panicTypeError(fmt.Sprintf("cannot convert %v to %v", val.Type(), t))) } } switch val.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: i64 := val.Int() switch t.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if reflect.Zero(t).OverflowInt(i64) { panic(rt.panicRangeError(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))) } return val.Convert(t) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if i64 < 0 { panic(rt.panicRangeError(fmt.Sprintf("converting %v to %v would underflow", val.Type(), t))) } if reflect.Zero(t).OverflowUint(uint64(i64)) { panic(rt.panicRangeError(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))) } return val.Convert(t) case reflect.Float32, reflect.Float64: return val.Convert(t) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: u64 := val.Uint() switch t.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if u64 > math.MaxInt64 || reflect.Zero(t).OverflowInt(int64(u64)) { panic(rt.panicRangeError(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))) } return val.Convert(t) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: if reflect.Zero(t).OverflowUint(u64) { panic(rt.panicRangeError(fmt.Sprintf("converting %v to %v would overflow", val.Type(), t))) } return val.Convert(t) case reflect.Float32, reflect.Float64: return val.Convert(t) } } panic(rt.panicTypeError(fmt.Sprintf("unsupported type %v -> %v for numeric conversion", val.Type(), t))) } func fieldIndexByName(t reflect.Type, name string) []int { for t.Kind() == reflect.Ptr { t = t.Elem() } for i := range t.NumField() { f := t.Field(i) if !validGoStructName(f.Name) { continue } if f.Anonymous { for t.Kind() == reflect.Ptr { t = t.Elem() } if f.Type.Kind() == reflect.Struct { if a := fieldIndexByName(f.Type, name); a != nil { return append([]int{i}, a...) } } } if a := strings.SplitN(f.Tag.Get("json"), ",", 2); a[0] != "" { if a[0] == "-" { continue } if a[0] == name { return []int{i} } } if f.Name == name { return []int{i} } } return nil } var ( typeOfValue = reflect.TypeOf(Value{}) typeOfJSONRawMessage = reflect.TypeOf(json.RawMessage{}) ) // convertCallParameter converts request val to type t if possible. // If the conversion fails due to overflow or type miss-match then it panics. // If no conversion is known then the original value is returned. func (rt *runtime) convertCallParameter(v Value, t reflect.Type) (reflect.Value, error) { if t == typeOfValue { return reflect.ValueOf(v), nil } if t == typeOfJSONRawMessage { if d, err := json.Marshal(v.export()); err == nil { return reflect.ValueOf(d), nil } } if v.kind == valueObject { if gso, ok := v.object().value.(*goStructObject); ok { if gso.value.Type().AssignableTo(t) { // please see TestDynamicFunctionReturningInterface for why this exists if t.Kind() == reflect.Interface && gso.value.Type().ConvertibleTo(t) { return gso.value.Convert(t), nil } return gso.value, nil } } if gao, ok := v.object().value.(*goArrayObject); ok { if gao.value.Type().AssignableTo(t) { // please see TestDynamicFunctionReturningInterface for why this exists if t.Kind() == reflect.Interface && gao.value.Type().ConvertibleTo(t) { return gao.value.Convert(t), nil } return gao.value, nil } } } tk := t.Kind() if tk == reflect.Interface { e := v.export() if e == nil { return reflect.Zero(t), nil } iv := reflect.ValueOf(e) if iv.Type().AssignableTo(t) { return iv, nil } } if tk == reflect.Ptr { switch v.kind { case valueEmpty, valueNull, valueUndefined: return reflect.Zero(t), nil default: var vv reflect.Value vv, err := rt.convertCallParameter(v, t.Elem()) if err != nil { return reflect.Zero(t), fmt.Errorf("can't convert to %s: %w", t, err) } if vv.CanAddr() { return vv.Addr(), nil } pv := reflect.New(vv.Type()) pv.Elem().Set(vv) return pv, nil } } switch tk { case reflect.Bool: return reflect.ValueOf(v.bool()), nil case reflect.String: switch v.kind { case valueString: return reflect.ValueOf(v.value), nil case valueNumber: return reflect.ValueOf(fmt.Sprintf("%v", v.value)), nil } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: if v.kind == valueNumber { return rt.convertNumeric(v, t), nil } case reflect.Slice: if o := v.object(); o != nil { if lv := o.get(propertyLength); lv.IsNumber() { l := lv.number().int64 s := reflect.MakeSlice(t, int(l), int(l)) tt := t.Elem() switch o.class { case classArrayName: for i := range l { p, ok := o.property[strconv.FormatInt(i, 10)] if !ok { continue } e, ok := p.value.(Value) if !ok { continue } ev, err := rt.convertCallParameter(e, tt) if err != nil { return reflect.Zero(t), fmt.Errorf("couldn't convert element %d of %s: %w", i, t, err) } s.Index(int(i)).Set(ev) } case classGoArrayName, classGoSliceName: var gslice bool switch o.value.(type) { case *goSliceObject: gslice = true case *goArrayObject: gslice = false } for i := range l { var p *property if gslice { p = goSliceGetOwnProperty(o, strconv.FormatInt(i, 10)) } else { p = goArrayGetOwnProperty(o, strconv.FormatInt(i, 10)) } if p == nil { continue } e, ok := p.value.(Value) if !ok { continue } ev, err := rt.convertCallParameter(e, tt) if err != nil { return reflect.Zero(t), fmt.Errorf("couldn't convert element %d of %s: %w", i, t, err) } s.Index(int(i)).Set(ev) } } return s, nil } } case reflect.Map: if o := v.object(); o != nil && t.Key().Kind() == reflect.String { m := reflect.MakeMap(t) var err error o.enumerate(false, func(k string) bool { v, verr := rt.convertCallParameter(o.get(k), t.Elem()) if verr != nil { err = fmt.Errorf("couldn't convert property %q of %s: %w", k, t, verr) return false } m.SetMapIndex(reflect.ValueOf(k), v) return true }) if err != nil { return reflect.Zero(t), err } return m, nil } case reflect.Func: if t.NumOut() > 1 { return reflect.Zero(t), errors.New("converting JavaScript values to Go functions with more than one return value is currently not supported") } if o := v.object(); o != nil && o.class == classFunctionName { return reflect.MakeFunc(t, func(args []reflect.Value) []reflect.Value { l := make([]interface{}, len(args)) for i, a := range args { if a.CanInterface() { l[i] = a.Interface() } } rv, err := v.Call(nullValue, l...) if err != nil { panic(err) } if t.NumOut() == 0 { return nil } r, err := rt.convertCallParameter(rv, t.Out(0)) if err != nil { panic(rt.panicTypeError("convertCallParameter Func: %s", err)) } return []reflect.Value{r} }), nil } case reflect.Struct: if o := v.object(); o != nil && o.class == classObjectName { s := reflect.New(t) for _, k := range o.propertyOrder { idx := fieldIndexByName(t, k) if idx == nil { return reflect.Zero(t), fmt.Errorf("can't convert property %q of %s: field does not exist", k, t) } ss := s for _, i := range idx { if ss.Kind() == reflect.Ptr { if ss.IsNil() { if !ss.CanSet() { return reflect.Zero(t), fmt.Errorf("can't convert property %q of %s: %s is unexported", k, t, ss.Type().Elem()) } ss.Set(reflect.New(ss.Type().Elem())) } ss = ss.Elem() } ss = ss.Field(i) } v, err := rt.convertCallParameter(o.get(k), ss.Type()) if err != nil { return reflect.Zero(t), fmt.Errorf("couldn't convert property %q of %s: %w", k, t, err) } ss.Set(v) } return s.Elem(), nil } } if tk == reflect.String { if o := v.object(); o != nil && o.hasProperty("toString") { if fn := o.get("toString"); fn.IsFunction() { sv, err := fn.Call(v) if err != nil { return reflect.Zero(t), fmt.Errorf("couldn't call toString: %w", err) } r, err := rt.convertCallParameter(sv, t) if err != nil { return reflect.Zero(t), fmt.Errorf("couldn't convert toString result: %w", err) } return r, nil } } return reflect.ValueOf(v.String()), nil } if v.kind == valueString { var s encoding.TextUnmarshaler if reflect.PointerTo(t).Implements(reflect.TypeOf(&s).Elem()) { r := reflect.New(t) if err := r.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(v.string())); err != nil { return reflect.Zero(t), fmt.Errorf("can't convert to %s as TextUnmarshaller: %w", t.String(), err) } return r.Elem(), nil } } s := "OTTO DOES NOT UNDERSTAND THIS TYPE" switch v.kind { case valueBoolean: s = "boolean" case valueNull: s = "null" case valueNumber: s = "number" case valueString: s = "string" case valueUndefined: s = "undefined" case valueObject: s = v.Class() } return reflect.Zero(t), fmt.Errorf("can't convert from %q to %q", s, t) } func (rt *runtime) toValue(value interface{}) Value { rv, ok := value.(reflect.Value) if ok { value = rv.Interface() } switch value := value.(type) { case Value: return value case func(FunctionCall) Value: var name, file string var line int pc := reflect.ValueOf(value).Pointer() fn := goruntime.FuncForPC(pc) if fn != nil { name = fn.Name() file, line = fn.FileLine(pc) file = path.Base(file) } return objectValue(rt.newNativeFunction(name, file, line, value)) case nativeFunction: var name, file string var line int pc := reflect.ValueOf(value).Pointer() fn := goruntime.FuncForPC(pc) if fn != nil { name = fn.Name() file, line = fn.FileLine(pc) file = path.Base(file) } return objectValue(rt.newNativeFunction(name, file, line, value)) case Object, *Object, object, *object: // Nothing happens. // FIXME We should really figure out what can come here. // This catch-all is ugly. default: val := reflect.ValueOf(value) if ok && val.Kind() == rv.Kind() { // Use passed in rv which may be writable. val = rv } switch val.Kind() { case reflect.Ptr: switch reflect.Indirect(val).Kind() { case reflect.Struct: return objectValue(rt.newGoStructObject(val)) case reflect.Array: return objectValue(rt.newGoArray(val)) } case reflect.Struct: return objectValue(rt.newGoStructObject(val)) case reflect.Map: return objectValue(rt.newGoMapObject(val)) case reflect.Slice: return objectValue(rt.newGoSlice(val)) case reflect.Array: return objectValue(rt.newGoArray(val)) case reflect.Func: var name, file string var line int if v := reflect.ValueOf(val); v.Kind() == reflect.Ptr { pc := v.Pointer() fn := goruntime.FuncForPC(pc) if fn != nil { name = fn.Name() file, line = fn.FileLine(pc) file = path.Base(file) } } typ := val.Type() return objectValue(rt.newNativeFunction(name, file, line, func(c FunctionCall) Value { nargs := typ.NumIn() if len(c.ArgumentList) != nargs { if typ.IsVariadic() { if len(c.ArgumentList) < nargs-1 { panic(rt.panicRangeError(fmt.Sprintf("expected at least %d arguments; got %d", nargs-1, len(c.ArgumentList)))) } } else { panic(rt.panicRangeError(fmt.Sprintf("expected %d argument(s); got %d", nargs, len(c.ArgumentList)))) } } in := make([]reflect.Value, len(c.ArgumentList)) callSlice := false for i, a := range c.ArgumentList { var t reflect.Type n := i if n >= nargs-1 && typ.IsVariadic() { if n > nargs-1 { n = nargs - 1 } t = typ.In(n).Elem() } else { t = typ.In(n) } // if this is a variadic Go function, and the caller has supplied // exactly the number of JavaScript arguments required, and this // is the last JavaScript argument, try treating the it as the // actual set of variadic Go arguments. if that succeeds, break // out of the loop. if typ.IsVariadic() && len(c.ArgumentList) == nargs && i == nargs-1 { if v, err := rt.convertCallParameter(a, typ.In(n)); err == nil { in[i] = v callSlice = true break } } v, err := rt.convertCallParameter(a, t) if err != nil { panic(rt.panicTypeError(err.Error())) } in[i] = v } var out []reflect.Value if callSlice { out = val.CallSlice(in) } else { out = val.Call(in) } switch len(out) { case 0: return Value{} case 1: return rt.toValue(out[0].Interface()) default: s := make([]interface{}, len(out)) for i, v := range out { s[i] = rt.toValue(v.Interface()) } return rt.toValue(s) } })) } } return toValue(value) } func (rt *runtime) newGoSlice(value reflect.Value) *object { obj := rt.newGoSliceObject(value) obj.prototype = rt.global.ArrayPrototype return obj } func (rt *runtime) newGoArray(value reflect.Value) *object { obj := rt.newGoArrayObject(value) obj.prototype = rt.global.ArrayPrototype return obj } func (rt *runtime) parse(filename string, src, sm interface{}) (*ast.Program, error) { return parser.ParseFileWithSourceMap(nil, filename, src, sm, 0) } func (rt *runtime) cmplParse(filename string, src, sm interface{}) (*nodeProgram, error) { program, err := parser.ParseFileWithSourceMap(nil, filename, src, sm, 0) if err != nil { return nil, err } return cmplParse(program), nil } func (rt *runtime) parseSource(src, sm interface{}) (*nodeProgram, *ast.Program, error) { switch src := src.(type) { case *ast.Program: return nil, src, nil case *Script: return src.program, nil, nil } program, err := rt.parse("", src, sm) return nil, program, err } func (rt *runtime) cmplRunOrEval(src, sm interface{}, eval bool) (Value, error) { result := Value{} node, program, err := rt.parseSource(src, sm) if err != nil { return result, err } if node == nil { node = cmplParse(program) } err = catchPanic(func() { result = rt.cmplEvaluateNodeProgram(node, eval) }) switch result.kind { case valueEmpty: result = Value{} case valueReference: result = result.resolve() } return result, err } func (rt *runtime) cmplRun(src, sm interface{}) (Value, error) { return rt.cmplRunOrEval(src, sm, false) } func (rt *runtime) cmplEval(src, sm interface{}) (Value, error) { return rt.cmplRunOrEval(src, sm, true) } func (rt *runtime) parseThrow(err error) { if err == nil { return } var errl parser.ErrorList if errors.Is(err, &errl) { err := errl[0] if err.Message == "invalid left-hand side in assignment" { panic(rt.panicReferenceError(err.Message)) } panic(rt.panicSyntaxError(err.Message)) } panic(rt.panicSyntaxError(err.Error())) } func (rt *runtime) cmplParseOrThrow(src, sm interface{}) *nodeProgram { program, err := rt.cmplParse("", src, sm) rt.parseThrow(err) // Will panic/throw appropriately return program } ================================================ FILE: runtime_test.go ================================================ package otto import ( "math" "testing" "github.com/stretchr/testify/require" ) // FIXME terst, Review tests func TestOperator(t *testing.T) { tt(t, func() { test, vm := test() test("xyzzy = 1") test("xyzzy", 1) if true { vm.Set("twoPlusTwo", func(FunctionCall) Value { return toValue(5) }) test("twoPlusTwo( 1 )", 5) test("1 + twoPlusTwo( 1 )", 6) test("-1 + twoPlusTwo( 1 )", 4) } test("result = 4") test("result", 4) test("result += 1") test("result", 5) test("result *= 2") test("result", 10) test("result /= 2") test("result", 5) test("result = 112.51 % 3.1") test("result", 0.9100000000000019) test("result = 'Xyzzy'") test("result", "Xyzzy") test("result = 'Xyz' + 'zy'") test("result", "Xyzzy") test("result = \"Xyzzy\"") test("result", "Xyzzy") test("result = 1; result = result") test("result", 1) test(` var result64 = 64 , result10 = 10 `) test("result64", 64) test("result10", 10) test(` result = 1; result += 1; `) test("result", 2) }) } func TestFunction_(t *testing.T) { tt(t, func() { test, _ := test() test(` result = 2 xyzzy = function() { result += 1 } xyzzy() result; `, 3) test(` xyzzy = function() { return 1 } result = xyzzy() `, 1) test(` xyzzy = function() {} result = xyzzy() `, "undefined") test(` xyzzy = function() { return 64 return 1 } result = xyzzy() `, 64) test(` result = 4 xyzzy = function() { result = 2 } xyzzy(); result; `, 2) test(` result = 4 xyzzy = function() { var result result = 2 } xyzzy(); result; `, 4) test(` xyzzy = function() { var result = 4 return result } result = xyzzy() `, 4) test(` xyzzy = function() { function test() { var result = 1 return result } return test() + 1 } result = xyzzy() + 1 `, 3) test(` xyzzy = function() { function test() { var result = 1 return result } _xyzzy = 2 var result = _xyzzy + test() + 1 return result } result = xyzzy() + 1; [ result, _xyzzy ]; `, "5,2") test(` xyzzy = function(apple) { return 1 } result = xyzzy(1) `, 1) test(` xyzzy = function(apple) { return apple + 1 } result = xyzzy(2) `, 3) test(` { result = 1 result += 1; } `, 2) test(` var global = 1 outer = function() { var global = 2 var inner = function(){ return global } return inner() } result = outer() `, 2) test(` var apple = 1 var banana = function() { return apple } var cherry = function() { var apple = 2 return banana() } result = cherry() `, 1) test(` function xyz() { }; delete xyz; `, false) test(` var abc = function __factorial(def){ if (def === 1) { return def; } else { return __factorial(def-1)*def; } }; abc(3); `, 6) }) } func TestDoWhile(t *testing.T) { tt(t, func() { test, _ := test() test(` limit = 4; result = 0; do { result = result + 1; limit = limit - 1; } while (limit); result; `, 4) test(` result = eval("do {abc=1; break; abc=2;} while (0);"); [ result, abc ]; `, "1,1") }) } func TestContinueBreak(t *testing.T) { tt(t, func() { test, _ := test() test(` limit = 4 result = 0 while (limit) { limit = limit - 1 if (limit) { } else { break } result = result + 1 } [ result, limit ]; `, "3,0") test(` limit = 4 result = 0 while (limit) { limit = limit - 1 if (limit) { continue } else { break } result = result + 1 } result; `, 0) test(` limit = 4 result = 0 do { limit = limit - 1 if (limit) { continue } else { break } result = result + 1 } while (limit) result; `, 0) }) } func TestTryCatchError(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc try { 1() } catch (def) { abc = def } abc; `, "TypeError: 1 is not a function") }) } func TestPositiveNegativeZero(t *testing.T) { tt(t, func() { test, _ := test() test(`1/0`, infinity) test(`1/-0`, -infinity) test(` abc = -0 1/abc `, -infinity) }) } func TestComparison(t *testing.T) { tt(t, func() { test, _ := test() test(` undefined = 1; undefined; `, "undefined") test("undefined == undefined", true) test("undefined != undefined", false) test("null == null", true) test("null != null", false) test("0 == 1", false) is(negativeZero, -0) is(positiveZero, 0) is(math.Signbit(negativeZero), true) is(positiveZero == negativeZero, true) test("1 == 1", true) test("'Hello, World.' == 'Goodbye, World.'", false) test("'Hello, World.' == true", false) test("'Hello, World.' == false", false) test("'Hello, World.' == 1", false) test("1 == 'Hello, World.'", false) is(parseNumber("-1"), -1) test("0+Object", "0function Object() { [native code] }") }) } func TestComparisonRelational(t *testing.T) { tt(t, func() { test, _ := test() test("0 < 0", false) test("0 > 0", false) test("0 <= 0", true) test("0 >= 0", true) test("' 0' >= 0", true) test("'_ 0' >= 0", false) }) } func TestArguments(t *testing.T) { tt(t, func() { test, _ := test() test(` xyzzy = function() { return arguments[0] } result = xyzzy("xyzzy"); `, "xyzzy") test(` xyzzy = function() { arguments[0] = "abcdef" return arguments[0] } result = xyzzy("xyzzy"); `, "abcdef") test(` xyzzy = function(apple) { apple = "abcdef" return arguments[0] } result = xyzzy("xyzzy"); `, "abcdef") test(` (function(){ return arguments })() `, "[object Arguments]") test(` (function(){ return arguments.length })() `, 0) test(` (function(){ return arguments.length })(1, 2, 4, 8, 10) `, 5) }) } func TestObjectLiteral(t *testing.T) { tt(t, func() { test, _ := test() test(` ({}); `, "[object Object]") test(` var abc = { xyzzy: "Nothing happens.", get 1e2() { return 3.14159; }, get null() { return true; }, get "[\n]"() { return "<>"; } }; [ abc["1e2"], abc.null, abc["[\n]"] ]; `, "3.14159,true,<>") test(` var abc = { xyzzy: "Nothing happens.", set 1e2() { this[3.14159] = 100; return Math.random(); }, set null(def) { this.def = def; return Math.random(); }, }; [ abc["1e2"] = Infinity, abc[3.14159], abc.null = "xyz", abc.def ]; `, "Infinity,100,xyz,xyz") }) } func TestUnaryPrefix(t *testing.T) { tt(t, func() { test, _ := test() test(` var result = 0; [++result, result]; `, "1,1") test(` result = 0; [--result, result]; `, "-1,-1") test(` var object = { valueOf: function() { return 1; } }; result = ++object; [ result, typeof result ]; `, "2,number") test(` var object = { valueOf: function() { return 1; } }; result = --object; [ result, typeof result ]; `, "0,number") }) } func TestUnaryPostfix(t *testing.T) { tt(t, func() { test, _ := test() test(` var result = 0; result++; [ result++, result ]; `, "1,2") test(` result = 0; result--; [ result--, result ]; `, "-1,-2") test(` var object = { valueOf: function() { return 1; } }; result = object++; [ result, typeof result ]; `, "1,number") test(` var object = { valueOf: function() { return 1; } }; result = object-- [ result, typeof result ]; `, "1,number") }) } func TestBinaryLogicalOperation(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = true def = false ghi = false jkl = false result = abc && def || ghi && jkl `, false) test(` abc = true def = true ghi = false jkl = false result = abc && def || ghi && jkl `, true) }) } func TestBinaryBitwiseOperation(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = 1 & 2; def = 1 & 3; ghi = 1 | 3; jkl = 1 ^ 2; mno = 1 ^ 3; [ abc, def, ghi, jkl, mno ]; `, "0,1,3,3,2") }) } func TestBinaryShiftOperation(t *testing.T) { tt(t, func() { test, _ := test() test(` high = (1 << 30) - 1 + (1 << 30) low = -high - 1 abc = 23 << 1 def = -105 >> 1 ghi = 23 << 2 jkl = 1 >>> 31 mno = 1 << 64 pqr = 1 >> 2 stu = -2 >> 4 vwx = low >> 1 yz = low >>> 1 `) test("abc", 46) test("def", -53) test("ghi", 92) test("jkl", 0) test("mno", 1) test("pqr", 0) test("stu", -1) test("vwx", -1073741824) test("yz", 1073741824) }) } func TestParenthesizing(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = 1 + 2 * 3 def = (1 + 2) * 3 ghi = !(false || true) jkl = !false || true `) test("abc", 7) test("def", 9) test("ghi", false) test("jkl", true) }) } func Test_instanceof(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = {} instanceof Object; `, true) test(` abc = "abc" instanceof Object; `, false) test(`raise: abc = {} instanceof "abc"; `, "TypeError: invalid kind String for instanceof (expected object)") test(`raise: "xyzzy" instanceof Math; `, "TypeError: Object.hasInstance not callable") }) } func TestIn(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = "prototype" in Object; def = "xyzzy" in Object; [ abc, def ]; `, "true,false") }) } func Test_new(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = new Boolean; def = new Boolean(1); [ abc, def ]; `, "false,true") }) } func TestNewFunction(t *testing.T) { tt(t, func() { test, _ := test() test(` new Function("return 11")() `, 11) test(` abc = 10 new Function("abc += 1")() abc `, 11) test(` new Function("a", "b", "c", "return b + 2")(10, 11, 12) `, 13) test(`raise: new 1 `, "TypeError: 1 is not a function") // TODO Better error reporting: new this test(`raise: new this `, "TypeError: [object environment] is not a function") test(`raise: new {} `, "TypeError: [object Object] is not a function") }) } func TestNewPrototype(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = { 'xyzzy': 'Nothing happens.' } function Xyzzy(){} Xyzzy.prototype = abc; (new Xyzzy()).xyzzy `, "Nothing happens.") }) } func TestBlock(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc=0; var ghi; def: { do { abc++; if (!(abc < 10)) { break def; ghi = "ghi"; } } while (true); } [ abc,ghi ]; `, "10,") }) } func Test_toString(t *testing.T) { tt(t, func() { test, _ := test() test(` [undefined+""] `, "undefined") }) } func TestEvaluationOrder(t *testing.T) { tt(t, func() { test, _ := test() test(` var abc = 0; abc < (abc = 1) === true; `, true) }) } func TestClone(t *testing.T) { tt(t, func() { vm1 := New() _, err := vm1.Run(` var abc = 1; `) require.NoError(t, err) vm2 := vm1.clone() _, err = vm1.Run(` abc += 2; `) require.NoError(t, err) _, err = vm2.Run(` abc += 4; `) require.NoError(t, err) is(vm1.getValue("abc"), 3) is(vm2.getValue("abc"), 5) }) } func Test_debugger(t *testing.T) { tt(t, func() { called := false vm := New() vm.SetDebuggerHandler(func(o *Otto) { is(o, vm) called = true }) _, err := vm.Run(`debugger`) require.NoError(t, err) is(called, true) }) tt(t, func() { called := false vm := New() vm.SetDebuggerHandler(func(o *Otto) { is(o, vm) called = true }) _, err := vm.Run(`null`) require.NoError(t, err) is(called, false) }) tt(t, func() { vm := New() _, err := vm.Run(`debugger`) require.NoError(t, err) }) } func Test_random(t *testing.T) { tt(t, func() { vm := New() vm.SetRandomSource(func() float64 { return 1 }) r, err := vm.Run(`Math.random()`) require.NoError(t, err) f, err := r.ToFloat() require.NoError(t, err) is(f, 1) }) tt(t, func() { vm := New() r1, err := vm.Run(`Math.random()`) require.NoError(t, err) f1, err := r1.ToFloat() require.NoError(t, err) r2, err := vm.Run(`Math.random()`) require.NoError(t, err) f2, err := r2.ToFloat() require.NoError(t, err) is(f1 == f2, false) }) } func Test_stringArray(t *testing.T) { getStrings := func() []string { return []string{"these", "are", "strings"} } concatStrings := func(a []string) string { if len(a) == 0 { return "" } r := a[0] for i := 1; i < len(a); i++ { r += " " r += a[i] } return r } tt(t, func() { vm := New() err := vm.Set("getStrings", getStrings) require.NoError(t, err) err = vm.Set("concatStrings", concatStrings) require.NoError(t, err) r1, err := vm.Run(`var a = getStrings(); concatStrings(a)`) require.NoError(t, err) is(r1, "these are strings") }) } type goByteArrayWithMethodsTest [8]byte func (g goByteArrayWithMethodsTest) S() string { return string(g[:]) } func (g goByteArrayWithMethodsTest) F(i int) byte { return g[i] } func Test_goByteArrayWithMethods_typeof_S(t *testing.T) { a := goByteArrayWithMethodsTest{97, 98, 99, 100, 101, 102, 103, 104} tt(t, func() { test, vm := test() vm.Set("a", a) is(test("typeof a.S").export(), "function") }) } func Test_goByteArrayWithMethods_S(t *testing.T) { a := goByteArrayWithMethodsTest{97, 98, 99, 100, 101, 102, 103, 104} tt(t, func() { test, vm := test() vm.Set("a", a) is(test("a.S()").export(), "abcdefgh") }) } func Test_goByteArrayWithMethods_F0(t *testing.T) { a := goByteArrayWithMethodsTest{97, 98, 99, 100, 101, 102, 103, 104} tt(t, func() { test, vm := test() vm.Set("a", a) is(test("a.F(0)").export(), 97) }) } func Test_goByteArrayWithMethods_F1(t *testing.T) { a := goByteArrayWithMethodsTest{97, 98, 99, 100, 101, 102, 103, 104} tt(t, func() { test, vm := test() vm.Set("a", a) is(test("a.F(1)").export(), 98) }) } ================================================ FILE: scope.go ================================================ package otto // An ECMA-262 ExecutionContext. type scope struct { lexical stasher variable stasher this *object outer *scope frame frame depth int eval bool } func newScope(lexical stasher, variable stasher, this *object) *scope { return &scope{ lexical: lexical, variable: variable, this: this, } } ================================================ FILE: script.go ================================================ package otto import ( "bytes" "encoding/gob" "errors" ) // ErrVersion is an error which represents a version mismatch. var ErrVersion = errors.New("version mismatch") 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 filename string src string } // 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) func (o *Otto) Compile(filename string, src interface{}) (*Script, error) { return o.CompileWithSourceMap(filename, src, nil) } // CompileWithSourceMap does the same thing as Compile, but with the obvious // difference of applying a source map. func (o *Otto) CompileWithSourceMap(filename string, src, sm interface{}) (*Script, error) { program, err := o.runtime.parse(filename, src, sm) if err != nil { return nil, err } node := cmplParse(program) script := &Script{ version: scriptVersion, program: node, filename: filename, src: program.File.Source(), } return script, nil } func (s *Script) String() string { return "// " + s.filename + "\n" + s.src } // MarshalBinary will marshal a script into a binary form. A marshalled script // 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 (s *Script) marshalBinary() ([]byte, error) { var bfr bytes.Buffer encoder := gob.NewEncoder(&bfr) err := encoder.Encode(s.version) if err != nil { return nil, err } err = encoder.Encode(s.program) if err != nil { return nil, err } err = encoder.Encode(s.filename) if err != nil { return nil, err } err = encoder.Encode(s.src) if err != nil { return nil, err } return bfr.Bytes(), nil } // UnmarshalBinary will vivify a marshalled script into something usable. If the script was // originally marshalled on a different version of the otto runtime, then this method // will return an error. // // The binary format can change at any time and should be considered unspecified and opaque. func (s *Script) unmarshalBinary(data []byte) (err error) { //nolint:nonamedreturns decoder := gob.NewDecoder(bytes.NewReader(data)) defer func() { if err != nil { s.version = "" s.program = nil s.filename = "" s.src = "" } }() if err = decoder.Decode(&s.version); err != nil { return err } if s.version != scriptVersion { return ErrVersion } if err = decoder.Decode(&s.program); err != nil { return err } if err = decoder.Decode(&s.filename); err != nil { return err } return decoder.Decode(&s.src) } ================================================ FILE: script_test.go ================================================ package otto import ( "testing" "github.com/stretchr/testify/require" ) func TestScript(t *testing.T) { tt(t, func() { vm := New() script, err := vm.Compile("xyzzy", `var abc; if (!abc) abc = 0; abc += 2; abc;`) require.NoError(t, err) str := script.String() is(str, "// xyzzy\nvar abc; if (!abc) abc = 0; abc += 2; abc;") val, err := vm.Run(script) require.NoError(t, err) is(val, 2) // TODO(steve): Fix the underlying issues as to why this returns early. if true { return } tmp, err := script.marshalBinary() require.NoError(t, err) is(len(tmp), 1228) { script2 := &Script{} err = script2.unmarshalBinary(tmp) require.NoError(t, err) is(script2.String(), str) val, err = vm.Run(script2) require.NoError(t, err) is(val, 4) tmp, err = script2.marshalBinary() require.NoError(t, err) is(len(tmp), 1228) } { script2 := &Script{} err = script2.unmarshalBinary(tmp) require.NoError(t, err) is(script2.String(), str) val2, err2 := vm.Run(script2) require.NoError(t, err2) is(val2, 6) tmp, err2 = script2.marshalBinary() require.NoError(t, err2) is(len(tmp), 1228) } { version := scriptVersion scriptVersion = "bogus" script2 := &Script{} err = script2.unmarshalBinary(tmp) is(err, "version mismatch") is(script2.String(), "// \n") is(script2.version, "") is(script2.program == nil, true) is(script2.filename, "") is(script2.src, "") scriptVersion = version } }) } func TestFunctionCall_CallerLocation(t *testing.T) { tt(t, func() { vm := New() err := vm.Set("loc", func(call FunctionCall) Value { return toValue(call.CallerLocation()) }) require.NoError(t, err) script, err := vm.Compile("somefile.js", `var where = loc();`) require.NoError(t, err) _, err = vm.Run(script) require.NoError(t, err) where, err := vm.Get("where") require.NoError(t, err) is(where, "somefile.js:1:13") }) } ================================================ FILE: sourcemap_test.go ================================================ package otto import ( "testing" "github.com/stretchr/testify/require" ) const ( testSourcemapCodeOriginal = "function functionA(argA, argB) {\n functionB(argA, argB);\n}\n\nfunction functionB(argA, argB) {\n functionExternal(argA, argB);\n}" testSourcemapCodeMangled = "function functionA(argA,argB){functionB(argA,argB)}function functionB(argA,argB){functionExternal(argA,argB)}" testSourcemapContent = `{"version":3,"sources":["hello.js"],"names":["functionA","argA","argB","functionB","functionExternal"],"mappings":"AAAA,QAASA,WAAUC,KAAMC,MACvBC,UAAUF,KAAMC,MAGlB,QAASC,WAAUF,KAAMC,MACvBE,iBAAiBH,KAAMC"}` testSourcemapInline = "function functionA(argA,argB){functionB(argA,argB)}function functionB(argA,argB){functionExternal(argA,argB)}\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImhlbGxvLmpzIl0sIm5hbWVzIjpbImZ1bmN0aW9uQSIsImFyZ0EiLCJhcmdCIiwiZnVuY3Rpb25CIiwiZnVuY3Rpb25FeHRlcm5hbCJdLCJtYXBwaW5ncyI6IkFBQUEsUUFBU0EsV0FBVUMsS0FBTUMsTUFDdkJDLFVBQVVGLEtBQU1DLE1BR2xCLFFBQVNDLFdBQVVGLEtBQU1DLE1BQ3ZCRSxpQkFBaUJILEtBQU1DIn0=" testSourcemapOriginalStack = "ReferenceError: 'functionExternal' is not defined\n at functionB (hello.js:6:3)\n at functionA (hello.js:2:3)\n at :1:1\n" testSourcemapMangledStack = "ReferenceError: 'functionExternal' is not defined\n at functionB (hello.js:1:82)\n at functionA (hello.js:1:31)\n at :1:1\n" testSourcemapMappedStack = "ReferenceError: 'functionExternal' is not defined\n at functionB (hello.js:6:2)\n at functionA (hello.js:2:2)\n at :1:1\n" ) func TestSourceMapOriginalWithNoSourcemap(t *testing.T) { tt(t, func() { vm := New() s, err := vm.Compile("hello.js", testSourcemapCodeOriginal) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) _, err = vm.Run(`functionA()`) require.Error(t, err) var oerr *Error require.ErrorAs(t, err, &oerr) require.Equal(t, testSourcemapOriginalStack, oerr.String()) }) } func TestSourceMapMangledWithNoSourcemap(t *testing.T) { tt(t, func() { vm := New() s, err := vm.Compile("hello.js", testSourcemapCodeMangled) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) _, err = vm.Run(`functionA()`) require.Error(t, err) var oerr *Error require.ErrorAs(t, err, &oerr) require.Equal(t, testSourcemapMangledStack, oerr.String()) }) } func TestSourceMapMangledWithSourcemap(t *testing.T) { tt(t, func() { vm := New() s, err := vm.CompileWithSourceMap("hello.js", testSourcemapCodeMangled, testSourcemapContent) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) _, err = vm.Run(`functionA()`) require.Error(t, err) var oerr *Error require.ErrorAs(t, err, &oerr) require.Equal(t, testSourcemapMappedStack, oerr.String()) }) } func TestSourceMapMangledWithInlineSourcemap(t *testing.T) { tt(t, func() { vm := New() s, err := vm.CompileWithSourceMap("hello.js", testSourcemapInline, nil) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) _, err = vm.Run(`functionA()`) require.Error(t, err) var oerr *Error require.ErrorAs(t, err, &oerr) require.Equal(t, testSourcemapMappedStack, oerr.String()) }) } func TestSourceMapContextPosition(t *testing.T) { tt(t, func() { vm := New() s, err := vm.CompileWithSourceMap("hello.js", testSourcemapCodeMangled, testSourcemapContent) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) err = vm.Set("functionExternal", func(c FunctionCall) Value { ctx := c.Otto.Context() is(ctx.Filename, "hello.js") is(ctx.Line, 6) is(ctx.Column, 2) return UndefinedValue() }) require.NoError(t, err) _, err = vm.Run(`functionA()`) require.NoError(t, err) }) } func TestSourceMapContextStacktrace(t *testing.T) { tt(t, func() { vm := New() s, err := vm.CompileWithSourceMap("hello.js", testSourcemapCodeMangled, testSourcemapContent) require.NoError(t, err) _, err = vm.Run(s) require.NoError(t, err) err = vm.Set("functionExternal", func(c FunctionCall) Value { ctx := c.Otto.Context() is(ctx.Stacktrace, []string{ "functionB (hello.js:6:2)", "functionA (hello.js:2:2)", ":1:1", }) return UndefinedValue() }) require.NoError(t, err) _, err = vm.Run(`functionA()`) require.NoError(t, err) }) } ================================================ FILE: stash.go ================================================ package otto import ( "fmt" ) // stasher is implemented by types which can stash data. type stasher interface { hasBinding(name string) bool // createBinding(name string, deletable bool, value Value) // CreateMutableBinding setBinding(name string, value Value, strict bool) // SetMutableBinding getBinding(name string, throw bool) Value // GetBindingValue deleteBinding(name string) bool // setValue(name string, value Value, throw bool) // createBinding + setBinding outer() stasher runtime() *runtime newReference(name string, strict bool, atv at) referencer clone(cloner *cloner) stasher } type objectStash struct { rt *runtime outr stasher object *object } func (s *objectStash) runtime() *runtime { return s.rt } func (rt *runtime) newObjectStash(obj *object, outer stasher) *objectStash { if obj == nil { obj = rt.newBaseObject() obj.class = "environment" } return &objectStash{ rt: rt, outr: outer, object: obj, } } func (s *objectStash) clone(c *cloner) stasher { out, exists := c.objectStash(s) if exists { return out } *out = objectStash{ c.runtime, c.stash(s.outr), c.object(s.object), } return out } func (s *objectStash) hasBinding(name string) bool { return s.object.hasProperty(name) } func (s *objectStash) createBinding(name string, deletable bool, value Value) { if s.object.hasProperty(name) { panic(hereBeDragons()) } mode := propertyMode(0o111) if !deletable { mode = propertyMode(0o110) } // TODO False? s.object.defineProperty(name, value, mode, false) } func (s *objectStash) setBinding(name string, value Value, strict bool) { s.object.put(name, value, strict) } func (s *objectStash) setValue(name string, value Value, throw bool) { if !s.hasBinding(name) { s.createBinding(name, true, value) // Configurable by default } else { s.setBinding(name, value, throw) } } func (s *objectStash) getBinding(name string, throw bool) Value { if s.object.hasProperty(name) { return s.object.get(name) } if throw { // strict? panic(s.rt.panicReferenceError("Not Defined", name)) } return Value{} } func (s *objectStash) deleteBinding(name string) bool { return s.object.delete(name, false) } func (s *objectStash) outer() stasher { return s.outr } func (s *objectStash) newReference(name string, strict bool, atv at) referencer { return newPropertyReference(s.rt, s.object, name, strict, atv) } type dclStash struct { rt *runtime outr stasher property map[string]dclProperty } type dclProperty struct { value Value mutable bool deletable bool readable bool } func (rt *runtime) newDeclarationStash(outer stasher) *dclStash { return &dclStash{ rt: rt, outr: outer, property: map[string]dclProperty{}, } } func (s *dclStash) clone(c *cloner) stasher { out, exists := c.dclStash(s) if exists { return out } prop := make(map[string]dclProperty, len(s.property)) for index, value := range s.property { prop[index] = c.dclProperty(value) } *out = dclStash{ c.runtime, c.stash(s.outr), prop, } return out } func (s *dclStash) hasBinding(name string) bool { _, exists := s.property[name] return exists } func (s *dclStash) runtime() *runtime { return s.rt } func (s *dclStash) createBinding(name string, deletable bool, value Value) { if _, exists := s.property[name]; exists { panic(fmt.Errorf("createBinding: %s: already exists", name)) } s.property[name] = dclProperty{ value: value, mutable: true, deletable: deletable, readable: false, } } func (s *dclStash) setBinding(name string, value Value, strict bool) { prop, exists := s.property[name] if !exists { panic(fmt.Errorf("setBinding: %s: missing", name)) } if prop.mutable { prop.value = value s.property[name] = prop } else { s.rt.typeErrorResult(strict) } } func (s *dclStash) setValue(name string, value Value, throw bool) { if !s.hasBinding(name) { s.createBinding(name, false, value) // NOT deletable by default } else { s.setBinding(name, value, throw) } } // FIXME This is called a __lot__. func (s *dclStash) getBinding(name string, throw bool) Value { prop, exists := s.property[name] if !exists { panic(fmt.Errorf("getBinding: %s: missing", name)) } if !prop.mutable && !prop.readable { if throw { // strict? panic(s.rt.panicTypeError("getBinding property %s not mutable and not readable", name)) } return Value{} } return prop.value } func (s *dclStash) deleteBinding(name string) bool { prop, exists := s.property[name] if !exists { return true } if !prop.deletable { return false } delete(s.property, name) return true } func (s *dclStash) outer() stasher { return s.outr } func (s *dclStash) newReference(name string, strict bool, _ at) referencer { return &stashReference{ name: name, base: s, } } // ======== // _fnStash // ======== type fnStash struct { dclStash arguments *object indexOfArgumentName map[string]string } func (rt *runtime) newFunctionStash(outer stasher) *fnStash { return &fnStash{ dclStash: dclStash{ rt: rt, outr: outer, property: map[string]dclProperty{}, }, } } func (s *fnStash) clone(c *cloner) stasher { out, exists := c.fnStash(s) if exists { return out } dclStash := s.dclStash.clone(c).(*dclStash) index := make(map[string]string, len(s.indexOfArgumentName)) for name, value := range s.indexOfArgumentName { index[name] = value } *out = fnStash{ dclStash: *dclStash, arguments: c.object(s.arguments), indexOfArgumentName: index, } return out } // getStashProperties returns the properties from stash. func getStashProperties(stash stasher) []string { switch vars := stash.(type) { case *dclStash: keys := make([]string, 0, len(vars.property)) for k := range vars.property { keys = append(keys, k) } return keys case *fnStash: keys := make([]string, 0, len(vars.property)) for k := range vars.property { keys = append(keys, k) } return keys case *objectStash: keys := make([]string, 0, len(vars.object.property)) for k := range vars.object.property { keys = append(keys, k) } return keys default: panic("unknown stash type") } } ================================================ FILE: string_test.go ================================================ package otto import ( "testing" "github.com/stretchr/testify/require" ) func TestString(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = (new String("xyzzy")).length; def = new String().length; ghi = new String("Nothing happens.").length; `) test("abc", 5) test("def", 0) test("ghi", 16) test(`"".length`, 0) test(`"a\uFFFFbc".length`, 4) test(`String(+0)`, "0") test(`String(-0)`, "0") test(`""+-0`, "0") test(` var abc = Object.getOwnPropertyDescriptor(String, "prototype"); [ [ typeof String.prototype ], [ abc.writable, abc.enumerable, abc.configurable ] ]; `, "object,false,false,false") }) } func TestString_charAt(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = "xyzzy".charAt(0) def = "xyzzy".charAt(11) `) test("abc", "x") test("def", "") }) } func TestString_charCodeAt(t *testing.T) { tt(t, func() { test, _ := test() test(` abc = "xyzzy".charCodeAt(0) def = "xyzzy".charCodeAt(11) `) test("abc", 120) test("def", naN) }) } func TestString_fromCharCode(t *testing.T) { tt(t, func() { test, _ := test() test(`String.fromCharCode()`, []uint16{}) test(`String.fromCharCode(88, 121, 122, 122, 121)`, []uint16{88, 121, 122, 122, 121}) // FIXME terst, Double-check these... test(`String.fromCharCode("88", 121, 122, 122.05, 121)`, []uint16{88, 121, 122, 122, 121}) test(`String.fromCharCode("88", 121, 122, NaN, 121)`, []uint16{88, 121, 122, 0, 121}) test(`String.fromCharCode("0x21")`, []uint16{33}) test(`String.fromCharCode(-1).charCodeAt(0)`, 65535) test(`String.fromCharCode(65535).charCodeAt(0)`, 65535) test(`String.fromCharCode(65534).charCodeAt(0)`, 65534) test(`String.fromCharCode(4294967295).charCodeAt(0)`, 65535) test(`String.fromCharCode(4294967294).charCodeAt(0)`, 65534) test(`String.fromCharCode(0x0024) === "$"`, true) }) } func TestString_concat(t *testing.T) { tt(t, func() { test, _ := test() test(`"".concat()`, "") test(`"".concat("abc", "def")`, "abcdef") test(`"".concat("abc", undefined, "def")`, "abcundefineddef") }) } func TestString_indexOf(t *testing.T) { tt(t, func() { test, _ := test() test(`"".indexOf("")`, 0) test(`"".indexOf("", 11)`, 0) test(`"abc".indexOf("")`, 0) test(`"abc".indexOf("", 11)`, 3) test(`"abc".indexOf("a")`, 0) test(`"abc".indexOf("bc")`, 1) test(`"abc".indexOf("bc", 11)`, -1) test(`"uñiçode".indexOf("ñ")`, 1) test(`"uñiçode".indexOf("ñ", 11)`, -1) test(`"uññiçode".indexOf("ç")`, 4) test(`"uññiçode".indexOf("ç", 11)`, -1) test(`"$$abcdabcd".indexOf("ab", function(){return -Infinity;}())`, 2) test(`"$$abcdabcd".indexOf("ab", function(){return NaN;}())`, 2) test(` var abc = {toString:function(){return "\u0041B";}} var def = {valueOf:function(){return true;}} var ghi = "ABB\u0041BABAB"; var jkl; with(ghi) { jkl = indexOf(abc, def); } jkl; `, 3) }) } func TestString_lastIndexOf(t *testing.T) { tt(t, func() { test, _ := test() test(`"".lastIndexOf("")`, 0) test(`"".lastIndexOf("", 11)`, 0) test(`"abc".lastIndexOf("")`, 3) test(`"abc".lastIndexOf("", 11)`, 3) test(`"abc".lastIndexOf("a")`, 0) test(`"abc".lastIndexOf("bc")`, 1) test(`"abc".lastIndexOf("bc", 11)`, 1) test(`"abc".lastIndexOf("bc", 0)`, -1) test(`"abc".lastIndexOf("abcabcabc", 2)`, -1) test(`"abc".lastIndexOf("abc", 0)`, 0) test(`"abc".lastIndexOf("abc", 1)`, 0) test(`"abc".lastIndexOf("abc", 2)`, 0) test(`"abc".lastIndexOf("abc", 3)`, 0) test(`"uñiçodeñ".lastIndexOf("ñ")`, 7) test(`"uñiçode".lastIndexOf("ñ")`, 1) test(`"uñiçodeñ".lastIndexOf("ç")`, 3) test(`"uñiçodeñ".lastIndexOf("aç")`, -1) test(` abc = new Object(true); abc.lastIndexOf = String.prototype.lastIndexOf; abc.lastIndexOf(true, false); `, 0) }) } func TestString_match(t *testing.T) { tt(t, func() { test, _ := test() test(`"abc____abc_abc___".match(/__abc/)`, "__abc") test(`"abc___abc_abc__abc__abc".match(/abc/g)`, "abc,abc,abc,abc,abc") test(`"abc____abc_abc___".match(/__abc/g)`, "__abc") test(` abc = /abc/g "abc___abc_abc__abc__abc".match(abc) `, "abc,abc,abc,abc,abc") test(`abc.lastIndex`, 23) }) } func BenchmarkString_match(b *testing.B) { vm := New() s, _ := vm.Compile("test.js", `"abc____abc_abc___".match(/__abc/g)`) for i := 0; i < b.N; i++ { _, e := vm.Run(s) if e != nil { b.Error(e.Error()) } } } func TestString_replace(t *testing.T) { tt(t, func() { test, _ := test() test(`"abc_abc".replace(/abc/, "$&123")`, "abc123_abc") test(`"abc_abc".replace(/abc/g, "$&123")`, "abc123_abc123") test(`"abc_abc_".replace(/abc/g, "$&123")`, "abc123_abc123_") test(`"_abc_abc_".replace(/abc/g, "$&123")`, "_abc123_abc123_") test(`"abc".replace(/abc/, "$&123")`, "abc123") test(`"abc_".replace(/abc/, "$&123")`, "abc123_") test("\"^abc$\".replace(/abc/, \"$`def\")", "^^def$") test("\"^abc$\".replace(/abc/, \"def$`\")", "^def^$") test(`"_abc_abd_".replace(/ab(c|d)/g, "$1")`, "_c_d_") test(` "_abc_abd_".replace(/ab(c|d)/g, function(){ }) `, "_undefined_undefined_") test(`"b".replace(/(a)?(b)?/, "_$1_")`, "__") test(` "b".replace(/(a)?(b)?/, function(a, b, c, d, e, f){ return [a, b, c, d, e, f] }) `, "b,,b,0,b,") test(` var abc = 'She sells seashells by the seashore.'; var def = /sh/; [ abc.replace(def, "$'" + 'sch') ]; `, "She sells seaells by the seashore.schells by the seashore.") }) } func BenchmarkString_replace(b *testing.B) { vm := New() s, _ := vm.Compile("test.js", `"_abc_abd_".replace(/ab(c|d)/g, "$1")`) for i := 0; i < b.N; i++ { _, e := vm.Run(s) if e != nil { b.Error(e.Error()) } } } func TestString_search(t *testing.T) { tt(t, func() { test, _ := test() test(`"abc".search(/abc/)`, 0) test(`"abc".search(/def/)`, -1) test(`"abc".search(/c$/)`, 2) test(`"abc".search(/$/)`, 3) }) } func BenchmarkString_search(b *testing.B) { vm := New() s, _ := vm.Compile("test.js", `"abc".search(/c$/)`) for i := 0; i < b.N; i++ { _, e := vm.Run(s) if e != nil { b.Error(e.Error()) } } } func TestString_split(t *testing.T) { tt(t, func() { test, _ := test() test(`"abc".split("", 1)`, "a") test(`"abc".split("", 2)`, "a,b") test(`"abc".split("", 3)`, "a,b,c") test(`"abc".split("", 4)`, "a,b,c") test(`"abc".split("", 11)`, "a,b,c") test(`"abc".split("", 0)`, "") test(`"abc".split("")`, "a,b,c") test(`"abc".split(undefined)`, "abc") test(`"__1__3_1__2__".split("_")`, ",,1,,3,1,,2,,") test(`"__1__3_1__2__".split(/_/)`, ",,1,,3,1,,2,,") test(`"ab".split(/a*/)`, ",b") test(`_ = "Aboldandcoded".split(/<(\/)?([^<>]+)>/)`, "A,,B,bold,/,B,and,,CODE,coded,/,CODE,") test(`_.length`, 13) test(`_[1] === undefined`, true) test(`_[12] === ""`, true) test(` var abc = new String("one-1 two-2 three-3"); var def = abc.split(new RegExp); [ def.constructor === Array, abc.length, def.length, def.join('') ]; `, "true,19,19,one-1 two-2 three-3") }) } func BenchmarkString_splitWithString(b *testing.B) { vm := New() err := vm.Set("data", "Lorem ipsum dolor sit amet, blandit nec elit. Ridiculous tortor wisi fusce vivamus") require.NoError(b, err) s, err := vm.Compile("test.js", `data.split(" ")`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func BenchmarkString_splitWithRegex(b *testing.B) { vm := New() err := vm.Set("data", "Lorem ipsum dolor sit amet, blandit nec elit. Ridiculous tortor wisi fusce vivamus") require.NoError(b, err) s, err := vm.Compile("test.js", `data.split(/ /)`) require.NoError(b, err) for i := 0; i < b.N; i++ { _, err = vm.Run(s) require.NoError(b, err) } } func TestString_slice(t *testing.T) { tt(t, func() { test, _ := test() test(`"abc".slice()`, "abc") test(`"abc".slice(0)`, "abc") test(`"abc".slice(0,11)`, "abc") test(`"abc".slice(0,-1)`, "ab") test(`"abc".slice(-1,11)`, "c") test(`abc = "abc"; abc.slice(abc.length+1, 0)`, "") }) } func TestString_length(t *testing.T) { tt(t, func() { test, _ := test() test(`"abc".length`, 3) test(`"uñiçode".length`, 7) test(`"😋".length`, 2) }) } func TestString_slice_unicode(t *testing.T) { tt(t, func() { test, _ := test() test(`"uñiçode".slice()`, "uñiçode") test(`"uñiçode".slice(0)`, "uñiçode") test(`"uñiçode".slice(0,11)`, "uñiçode") test(`"uñiçode".slice(0,-1)`, "uñiçod") test(`"uñiçode".slice(-1,11)`, "e") test(`"发送 213123".slice(0,2)`, "发送") }) } func TestString_substring_unicode(t *testing.T) { tt(t, func() { test, _ := test() test(`"uñiçode".substring()`, "uñiçode") test(`"uñiçode".substring(0)`, "uñiçode") test(`"uñiçode".substring(0,11)`, "uñiçode") test(`"uñiçode".substring(11,0)`, "uñiçode") test(`"uñiçode".substring(0,-1)`, "") test(`"uñiçode".substring(-1,11)`, "uñiçode") test(`"uñiçode".substring(1)`, "ñiçode") test(`"uñiçode".substring(Infinity, Infinity)`, "") }) } func TestString_substring(t *testing.T) { tt(t, func() { test, _ := test() test(`"abc".substring()`, "abc") test(`"abc".substring(0)`, "abc") test(`"abc".substring(0,11)`, "abc") test(`"abc".substring(11,0)`, "abc") test(`"abc".substring(0,-1)`, "") test(`"abc".substring(-1,11)`, "abc") test(`"abc".substring(11,1)`, "bc") test(`"abc".substring(1)`, "bc") test(`"abc".substring(Infinity, Infinity)`, "") }) } func TestString_toCase(t *testing.T) { tt(t, func() { test, _ := test() test(`"abc".toLowerCase()`, "abc") test(`"ABC".toLowerCase()`, "abc") test(`"abc".toLocaleLowerCase()`, "abc") test(`"ABC".toLocaleLowerCase()`, "abc") test(`"abc".toUpperCase()`, "ABC") test(`"ABC".toUpperCase()`, "ABC") test(`"abc".toLocaleUpperCase()`, "ABC") test(`"ABC".toLocaleUpperCase()`, "ABC") }) } func Test_floatToString(t *testing.T) { tt(t, func() { test, _ := test() test(`String(-1234567890)`, "-1234567890") test(`-+String(-(-1234567890))`, -1234567890) test(`String(-1e128)`, "-1e+128") test(`String(0.12345)`, "0.12345") test(`String(-0.00000012345)`, "-1.2345e-7") test(`String(0.0000012345)`, "0.0000012345") test(`String(1000000000000000000000)`, "1e+21") test(`String(1e21)`, "1e+21") test(`String(1E21)`, "1e+21") test(`String(-1000000000000000000000)`, "-1e+21") test(`String(-1e21)`, "-1e+21") test(`String(-1E21)`, "-1e+21") test(`String(0.0000001)`, "1e-7") test(`String(1e-7)`, "1e-7") test(`String(1E-7)`, "1e-7") test(`String(-0.0000001)`, "-1e-7") test(`String(-1e-7)`, "-1e-7") test(`String(-1E-7)`, "-1e-7") }) } func TestString_indexing(t *testing.T) { tt(t, func() { test, _ := test() // Actually a test of stringToArrayIndex, under the hood. test(` abc = new String("abc"); index = Math.pow(2, 32); [ abc.length, abc[index], abc[index+1], abc[index+2], abc[index+3] ]; `, "3,,,,") }) } func TestString_trim(t *testing.T) { tt(t, func() { test, _ := test() test(`' \n abc \t \n'.trim();`, "abc") test(`" abc\u000B".trim()`, "abc") test(`"abc ".trim()`, "abc") test(` var a = "\u180Eabc \u000B " var b = a.trim() a.length + b.length `, 10) }) } func TestString_trimLeft(t *testing.T) { tt(t, func() { test, _ := test() test(`" abc\u000B".trimLeft()`, "abc\u000B") test(`"abc ".trimLeft()`, "abc ") test(` var a = "\u180Eabc \u000B " var b = a.trimLeft() a.length + b.length `, 13) }) } func TestString_trimRight(t *testing.T) { tt(t, func() { test, _ := test() test(`" abc\u000B".trimRight()`, " abc") test(`" abc ".trimRight()`, " abc") test(` var a = "\u180Eabc \u000B " var b = a.trimRight() a.length + b.length `, 11) }) } func TestString_localeCompare(t *testing.T) { tt(t, func() { test, _ := test() test(`'a'.localeCompare('c');`, -1) test(`'c'.localeCompare('a');`, 1) test(`'a'.localeCompare('a');`, 0) }) } func TestString_startsWith(t *testing.T) { tt(t, func() { test, _ := test() test(`'a'.startsWith('c');`, false) test(`'aa'.startsWith('a');`, true) }) } func TestString_trimStart(t *testing.T) { tt(t, func() { test, _ := test() test(`" abc\u000B".trimStart()`, "abc\u000B") test(`"abc ".trimStart()`, "abc ") test(` var a = "\u180Eabc \u000B " var b = a.trimStart() a.length + b.length `, 13) }) } func TestString_trimEnd(t *testing.T) { tt(t, func() { test, _ := test() test(`" abc\u000B".trimEnd()`, " abc") test(`" abc ".trimEnd()`, " abc") test(` var a = "\u180Eabc \u000B " var b = a.trimEnd() a.length + b.length `, 11) }) } ================================================ FILE: terst/terst.go ================================================ // This file was AUTOMATICALLY GENERATED by terst-import (smuggol) from github.com/robertkrimen/terst /* Package terst is a terse (terst = test + terse), easy-to-use testing library for Go. terst is compatible with (and works via) the standard testing package: http://golang.org/pkg/testing var is = terst.Is func Test(t *testing.T) { terst.Terst(t, func() { is("abc", "abc") is(1, ">", 0) var abc []int is(abc, nil) } } Do not import terst directly, instead use `terst-import` to copy it into your testing environment: https://github.com/robertkrimen/terst/tree/master/terst-import $ go get github.com/robertkrimen/terst/terst-import $ terst-import */ package terst import ( "bytes" "errors" "fmt" "math/big" "reflect" "regexp" goruntime "runtime" "strings" "sync" "testing" "time" ) // Is compares two values (got & expect) and returns true if the comparison is true, // false otherwise. In addition, if the comparison is false, Is will report the error // in a manner similar to testing.T.Error(...). Is also takes an optional argument, // a comparator, that changes how the comparison is made. The following // comparators are available: // // == # got == expect (default) // != # got != expect // // > # got > expect (float32, uint, uint16, int, int64, ...) // >= # got >= expect // < # got < expect // <= # got <= expect // // =~ # regexp.MustCompile(expect).Match{String}(got) // !~ # !regexp.MustCompile(expect).Match{String}(got) // // Basic usage with the default comparator (==): // // Is(, ) // // Specifying a different comparator: // // Is(, , ) // // A simple comparison: // // Is(2 + 2, 4) // // A bit trickier: // // Is(1, ">", 0) // Is(2 + 2, "!=", 5) // Is("Nothing happens.", "=~", `ing(\s+)happens\.$`) // // Is should only be called under a Terst(t, ...) call. For a standalone version, // use IsErr. If no scope is found and the comparison is false, then Is will panic the error. func Is(arguments ...interface{}) bool { err := IsErr(arguments...) if err != nil { call := Caller() if call == nil { panic(err) } call.Error(err) return false } return true } type ( // ErrFail indicates a comparison failure (e.g. 0 > 1). ErrFail error // ErrInvalid indicates an invalid comparison (e.g. bool == string). ErrInvalid error ) var errInvalid = errors.New("invalid") var registry = struct { table map[uintptr]*_scope lock sync.RWMutex }{ table: map[uintptr]*_scope{}, } func registerScope(pc uintptr, scope *_scope) { registry.lock.Lock() defer registry.lock.Unlock() registry.table[pc] = scope } func getScope() *_scope { s, _ := findScope() return s } func floatCompare(a float64, b float64) int { if a > b { return 1 } else if a < b { return -1 } // NaN == NaN return 0 } func bigIntCompare(a *big.Int, b *big.Int) int { return a.Cmp(b) } func bigInt(value int64) *big.Int { return big.NewInt(value) } func bigUint(value uint64) *big.Int { return big.NewInt(0).SetUint64(value) } func toString(value interface{}) (string, error) { switch value := value.(type) { case string: return value, nil case fmt.Stringer: return value.String(), nil case error: return value.Error(), nil } return "", errInvalid } func matchString(got string, expect *regexp.Regexp) (int, error) { if expect.MatchString(got) { return 0, nil } return -1, nil } func match(got []byte, expect *regexp.Regexp) (int, error) { if expect.Match(got) { return 0, nil } return -1, nil } func compareMatch(got, expect interface{}) (int, error) { switch got := got.(type) { case []byte: switch expect := expect.(type) { case string: matcher, err := regexp.Compile(expect) if err != nil { return 0, err } return match(got, matcher) case *regexp.Regexp: return match(got, expect) } default: if got, err := toString(got); err == nil { switch expect := expect.(type) { case string: matcher, err := regexp.Compile(expect) if err != nil { return 0, err } return matchString(got, matcher) case *regexp.Regexp: return matchString(got, expect) } } else { return 0, err } } return 0, errInvalid } func floatPromote(value reflect.Value) (float64, error) { kind := value.Kind() if reflect.Int <= kind && kind <= reflect.Int64 { return float64(value.Int()), nil } if reflect.Uint <= kind && kind <= reflect.Uint64 { return float64(value.Uint()), nil } if reflect.Float32 <= kind && kind <= reflect.Float64 { return value.Float(), nil } return 0, errInvalid } func bigIntPromote(value reflect.Value) (*big.Int, error) { kind := value.Kind() if reflect.Int <= kind && kind <= reflect.Int64 { return bigInt(value.Int()), nil } if reflect.Uint <= kind && kind <= reflect.Uint64 { return bigUint(value.Uint()), nil } return nil, errInvalid } func compareOther(got, expect interface{}) (int, error) { switch expect.(type) { case float32, float64: return compareNumber(got, expect) case uint, uint8, uint16, uint32, uint64: return compareNumber(got, expect) case int, int8, int16, int32, int64: return compareNumber(got, expect) case string: var err error got, err = toString(got) if err != nil { return 0, err } case nil: got := reflect.ValueOf(got) switch got.Kind() { case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.Interface: if got.IsNil() { return 0, nil } return -1, nil case reflect.Invalid: // reflect.Invalid: var abc interface{} = nil return 0, nil } return 0, errInvalid } if reflect.ValueOf(got).Type() != reflect.ValueOf(expect).Type() { return 0, errInvalid } if reflect.DeepEqual(got, expect) { return 0, nil } return -1, nil } func compareNumber(got, expect interface{}) (int, error) { gotRv := reflect.ValueOf(got) gotKind := gotRv.Kind() expectRv := reflect.ValueOf(expect) expectKind := expectRv.Kind() if reflect.Float32 <= gotKind && gotKind <= reflect.Float64 || reflect.Float32 <= expectKind && expectKind <= reflect.Float64 { got, err := floatPromote(gotRv) if err != nil { return 0, err } expect, err := floatPromote(expectRv) if err != nil { return 0, err } return floatCompare(got, expect), nil } goBigInt, err := bigIntPromote(gotRv) if err != nil { return 0, err } expectBigInt, err := bigIntPromote(expectRv) if err != nil { return 0, err } return goBigInt.Cmp(expectBigInt), nil } // IsErr compares two values (got & expect) and returns nil if the comparison is true, an ErrFail if // the comparison is false, or an ErrInvalid if the comparison is invalid. IsErr also // takes an optional argument, a comparator, that changes how the comparison is made. // // Is & IsErr are similar but different: // // Is(...) // Should only be called within a Terst(...) call // IsErr(...) // A standalone comparator, the same as Is, just without the automatic reporting func IsErr(arguments ...interface{}) error { var got, expect interface{} comparator := "==" switch len(arguments) { case 0, 1: return fmt.Errorf("invalid number of arguments to IsErr: %d", len(arguments)) case 2: got, expect = arguments[0], arguments[1] default: if value, ok := arguments[1].(string); ok { comparator = value } else { return fmt.Errorf("invalid comparator: %v", arguments[1]) } got, expect = arguments[0], arguments[2] } var result int var err error switch comparator { case "<", "<=", ">", ">=": result, err = compareNumber(got, expect) case "=~", "!~": result, err = compareMatch(got, expect) case "==", "!=": result, err = compareOther(got, expect) default: return fmt.Errorf("invalid comparator: %s", comparator) } if err == errInvalid { return ErrInvalid(fmt.Errorf( "\nINVALID (%s):\n got: %v (%T)\n expected: %v (%T)", comparator, got, got, expect, expect, )) } else if err != nil { return err } equality, pass := false, false switch comparator { case "==", "=~": equality = true pass = result == 0 case "!=", "!~": equality = true pass = result != 0 case "<": pass = result < 0 case "<=": pass = result <= 0 case ">": pass = result > 0 case ">=": pass = result >= 0 } if !pass { if equality { if comparator[1] == '~' { if value, ok := got.([]byte); ok { return ErrFail(fmt.Errorf( "\nFAIL (%s)\n got: %s %v%s\nexpected: %v%s", comparator, value, got, typeKindString(got), expect, typeKindString(expect), )) } } return ErrFail(fmt.Errorf( "\nFAIL (%s)\n got: %v%s\nexpected: %v%s", comparator, got, typeKindString(got), expect, typeKindString(expect), )) } return ErrFail(fmt.Errorf( "\nFAIL (%s)\n got: %v%s\nexpected: %s %v%s", comparator, got, typeKindString(got), comparator, expect, typeKindString(expect), )) } return nil } func typeKindString(value interface{}) string { reflectValue := reflect.ValueOf(value) kind := reflectValue.Kind().String() result := fmt.Sprintf("%T", value) if kind == result { if kind == "string" { return "" } return fmt.Sprintf(" (%T)", value) } return fmt.Sprintf(" (%T=%s)", value, kind) } func (s *_scope) reset() { s.name = "" s.output = s.output[:0] s.start = time.Time{} s.duration = 0 } // Terst creates a testing scope, where Is can be called and errors will be reported // according to the top-level location of the comparison, and not where the Is call // actually takes place. For example: // // func test(value int) { // Is(value, 5) // <--- This failure is reported below. // } // // Terst(t, func(){ // // Is(2, ">", 3) // <--- An error is reported here. // // test(5) // <--- An error is reported here. // // }) func Terst(t *testing.T, arguments ...func()) { curScope := &_scope{ t: t, } pc, _, _, ok := goruntime.Caller(1) // TODO Associate with the Test... func if !ok { panic("Here be dragons.") } _, curScope.testFunc = findTestFunc() registerScope(pc, curScope) for _, fn := range arguments { func() { curScope.reset() name := curScope.testFunc.Name() index := strings.LastIndex(curScope.testFunc.Name(), ".") if index >= 0 { name = name[index+1:] + "(Terst)" } else { name = "(Terst)" } name = "(Terst)" curScope.name = name curScope.start = time.Now() defer func() { curScope.duration = time.Now().Sub(curScope.start) if err := recover(); err != nil { curScope.t.Fail() curScope.report() panic(err) } curScope.report() }() fn() }() } } // From "testing" func (s *_scope) report() { format := "~~~ %s: (Terst)\n%s" if s.t.Failed() { fmt.Printf(format, "FAIL", s.output) } else if testing.Verbose() && len(s.output) > 0 { fmt.Printf(format, "PASS", s.output) } } func (s *_scope) log(call entry, str string) { s.mu.Lock() defer s.mu.Unlock() s.output = append(s.output, decorate(call, str)...) } // decorate prefixes the string with the file and line of the call site // and inserts the final newline if needed and indentation tabs for formascing. func decorate(call entry, s string) string { file, line := call.File, call.Line if call.PC > 0 { // Truncate file name at last file name separator. if index := strings.LastIndex(file, "/"); index >= 0 { file = file[index+1:] } else if index = strings.LastIndex(file, "\\"); index >= 0 { file = file[index+1:] } } else { file = "???" line = 1 } buf := new(bytes.Buffer) // Every line is indented at least one tab. buf.WriteByte('\t') fmt.Fprintf(buf, "%s:%d: ", file, line) lines := strings.Split(s, "\n") if l := len(lines); l > 1 && lines[l-1] == "" { lines = lines[:l-1] } for i, line := range lines { if i > 0 { // Second and subsequent lines are indented an extra tab. buf.WriteString("\n\t\t") } buf.WriteString(line) } buf.WriteByte('\n') return buf.String() } func findScope() (*_scope, entry) { registry.lock.RLock() defer registry.lock.RUnlock() table := registry.table depth := 2 // Starting depth call := entry{} for { pc, _, _, ok := goruntime.Caller(depth) if !ok { break } if s, exists := table[pc]; exists { pc, file, line, _ := goruntime.Caller(depth - 3) // Terst(...) + func(){}() + fn() => ???() call.PC = pc call.File = file call.Line = line return s, call } depth++ } return nil, entry{} } // Call is a reference to a line immediately under a Terst testing scope. type Call struct { scope *_scope entry entry } // Caller will search the stack, looking for a Terst testing scope. If a scope // is found, then Caller returns a Call for logging errors, accessing testing.T, etc. // If no scope is found, Caller returns nil. func Caller() *Call { curScope, entry := findScope() if curScope == nil { return nil } return &Call{ scope: curScope, entry: entry, } } // TestFunc returns the *runtime.Func entry for the top-level Test...(t testing.T) // function. func (cl *Call) TestFunc() *goruntime.Func { return cl.scope.testFunc } // T returns the original testing.T passed to Terst(...) func (cl *Call) T() *testing.T { return cl.scope.t } // Log is the terst version of `testing.T.Log` func (cl *Call) Log(arguments ...interface{}) { cl.scope.log(cl.entry, fmt.Sprintln(arguments...)) } // Logf is the terst version of `testing.T.Logf` func (cl *Call) Logf(format string, arguments ...interface{}) { cl.scope.log(cl.entry, fmt.Sprintf(format, arguments...)) } // Error is the terst version of `testing.T.Error` func (cl *Call) Error(arguments ...interface{}) { cl.scope.log(cl.entry, fmt.Sprintln(arguments...)) cl.scope.t.Fail() } // Errorf is the terst version of `testing.T.Errorf` func (cl *Call) Errorf(format string, arguments ...interface{}) { cl.scope.log(cl.entry, fmt.Sprintf(format, arguments...)) cl.scope.t.Fail() } // Skip is the terst version of `testing.T.Skip` func (cl *Call) Skip(arguments ...interface{}) { cl.scope.log(cl.entry, fmt.Sprintln(arguments...)) cl.scope.t.SkipNow() } // Skipf is the terst version of `testing.T.Skipf` func (cl *Call) Skipf(format string, arguments ...interface{}) { cl.scope.log(cl.entry, fmt.Sprintf(format, arguments...)) cl.scope.t.SkipNow() } type _scope struct { t *testing.T testFunc *goruntime.Func name string mu sync.RWMutex output []byte start time.Time duration time.Duration } type entry struct { PC uintptr File string Line int Func *goruntime.Func } func findFunc(match string) (entry, *goruntime.Func) { depth := 2 // Starting depth for { pc, file, line, ok := goruntime.Caller(depth) if !ok { break } fn := goruntime.FuncForPC(pc) name := fn.Name() if index := strings.LastIndex(name, match); index >= 0 { // Assume we have an instance of TestXyzzy in a _test file return entry{ PC: pc, File: file, Line: line, Func: fn, }, fn } depth++ } return entry{}, nil } func findTestFunc() (entry, *goruntime.Func) { return findFunc(".Test") } func findTerstFunc() (entry, *goruntime.Func) { return findFunc(".Terst") } ================================================ FILE: testing_test.go ================================================ package otto import ( "errors" "strings" "testing" "time" "github.com/robertkrimen/otto/terst" ) func tt(t *testing.T, arguments ...func()) { t.Helper() halt := errors.New("A test was taking too long") timer := time.AfterFunc(20*time.Second, func() { panic(halt) }) defer func() { timer.Stop() }() terst.Terst(t, arguments...) } func is(arguments ...interface{}) bool { var got, expect interface{} switch len(arguments) { case 0, 1: return terst.Is(arguments...) case 2: got, expect = arguments[0], arguments[1] default: got, expect = arguments[0], arguments[2] } switch value := got.(type) { case Value: if value.value != nil { got = value.value } case *Error: if value != nil { got = value.Error() } if expect == nil { // FIXME This is weird expect = "" } } if len(arguments) == 2 { arguments[0] = got arguments[1] = expect } else { arguments[0] = got arguments[2] = expect } return terst.Is(arguments...) } func test(arguments ...interface{}) (func(string, ...interface{}) Value, *_tester) { tester := newTester() if len(arguments) > 0 { tester.test(arguments[0].(string)) } return tester.test, tester } type _tester struct { vm *Otto } func newTester() *_tester { return &_tester{ vm: New(), } } func (te *_tester) Get(name string) (Value, error) { return te.vm.Get(name) } func (te *_tester) Set(name string, value interface{}) Value { err := te.vm.Set(name, value) is(err, nil) if err != nil { terst.Caller().T().FailNow() } return te.vm.getValue(name) } func (te *_tester) Run(src interface{}) (Value, error) { return te.vm.Run(src) } func (te *_tester) test(name string, expect ...interface{}) Value { vm := te.vm raise := false defer func() { if caught := recover(); caught != nil { if exception, ok := caught.(*exception); ok { caught = exception.eject() } if raise { if len(expect) > 0 { is(caught, expect[0]) } } else { dbg("Panic, caught:", caught) panic(caught) } } }() var value Value var err error if isIdentifier(name) { value = vm.getValue(name) } else { source := name index := strings.Index(source, "raise:") if index == 0 { raise = true source = source[6:] source = strings.TrimLeft(source, " ") } value, err = vm.runtime.cmplRun(source, nil) if err != nil { panic(err) } } value = value.resolve() if len(expect) > 0 { is(value, expect[0]) } return value } ================================================ FILE: token/generate.go ================================================ package token //go:generate go run ../tools/gen-tokens -output token_const.go ================================================ FILE: token/token.go ================================================ // Package token defines constants representing the lexical tokens of JavaScript (ECMA5). package token import ( "strconv" ) // Token is the set of lexical tokens in JavaScript (ECMA5). type Token int // String returns the string corresponding to the token. // For operators, delimiters, and keywords the string is the actual // 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 { switch { case tkn == 0: return "UNKNOWN" case tkn < Token(len(token2string)): return token2string[tkn] default: return "token(" + strconv.Itoa(int(tkn)) + ")" } } type keyword struct { token Token futureKeyword bool strict bool } // IsKeyword returns the keyword token if literal is a keyword, a KEYWORD token // if the literal is a future keyword (const, let, class, super, ...), or 0 if the literal is not a keyword. // // If the literal is a keyword, IsKeyword returns a second value indicating if the literal // is considered a future keyword in strict-mode only. // // 7.6.1.2 Future Reserved Words: // // const // class // enum // export // extends // import // super // // 7.6.1.2 Future Reserved Words (strict): // // implements // interface // let // package // private // protected // public // static func IsKeyword(literal string) (Token, bool) { if kw, exists := keywordTable[literal]; exists { if kw.futureKeyword { return KEYWORD, kw.strict } return kw.token, false } return 0, false } ================================================ FILE: token/token_const.go ================================================ // Code generated by tools/gen-tokens. DO NOT EDIT. package token const ( _ Token = iota // Control. ILLEGAL EOF COMMENT KEYWORD // Types. STRING BOOLEAN NULL NUMBER IDENTIFIER // Maths. PLUS // + MINUS // - MULTIPLY // * SLASH // / REMAINDER // % // Logical and bitwise operators. AND // & OR // | EXCLUSIVE_OR // ^ SHIFT_LEFT // << SHIFT_RIGHT // >> UNSIGNED_SHIFT_RIGHT // >>> AND_NOT // &^ // Math assignments. ADD_ASSIGN // += SUBTRACT_ASSIGN // -= MULTIPLY_ASSIGN // *= QUOTIENT_ASSIGN // /= REMAINDER_ASSIGN // %= // Math and bitwise assignments. AND_ASSIGN // &= OR_ASSIGN // |= EXCLUSIVE_OR_ASSIGN // ^= SHIFT_LEFT_ASSIGN // <<= SHIFT_RIGHT_ASSIGN // >>= UNSIGNED_SHIFT_RIGHT_ASSIGN // >>>= AND_NOT_ASSIGN // &^= // Logical operators and decrement / increment. LOGICAL_AND // && LOGICAL_OR // || INCREMENT // ++ DECREMENT // -- // Comparison operators. EQUAL // == STRICT_EQUAL // === LESS // < GREATER // > ASSIGN // = NOT // ! // Bitwise not. BITWISE_NOT // ~ // Comparison operators. NOT_EQUAL // != STRICT_NOT_EQUAL // !== LESS_OR_EQUAL // <= GREATER_OR_EQUAL // >= // Left operators. LEFT_PARENTHESIS // ( LEFT_BRACKET // [ LEFT_BRACE // { COMMA // , PERIOD // . // Right operators. RIGHT_PARENTHESIS // ) RIGHT_BRACKET // ] RIGHT_BRACE // } SEMICOLON // ; COLON // : QUESTION_MARK // ? // Basic flow - keywords below here. _ IF IN DO // Declarations. VAR FOR NEW TRY // Advanced flow. THIS ELSE CASE VOID WITH // Loops. WHILE BREAK CATCH THROW // Functions. RETURN TYPEOF DELETE SWITCH // Fallback identifiers. DEFAULT FINALLY // Miscellaneous. FUNCTION CONTINUE DEBUGGER // Instance of. INSTANCEOF ) var token2string = [...]string{ ILLEGAL: "ILLEGAL", EOF: "EOF", COMMENT: "COMMENT", KEYWORD: "KEYWORD", STRING: "STRING", BOOLEAN: "BOOLEAN", NULL: "NULL", NUMBER: "NUMBER", IDENTIFIER: "IDENTIFIER", PLUS: "+", MINUS: "-", MULTIPLY: "*", SLASH: "/", REMAINDER: "%", AND: "&", OR: "|", EXCLUSIVE_OR: "^", SHIFT_LEFT: "<<", SHIFT_RIGHT: ">>", UNSIGNED_SHIFT_RIGHT: ">>>", AND_NOT: "&^", ADD_ASSIGN: "+=", SUBTRACT_ASSIGN: "-=", MULTIPLY_ASSIGN: "*=", QUOTIENT_ASSIGN: "/=", REMAINDER_ASSIGN: "%=", AND_ASSIGN: "&=", OR_ASSIGN: "|=", EXCLUSIVE_OR_ASSIGN: "^=", SHIFT_LEFT_ASSIGN: "<<=", SHIFT_RIGHT_ASSIGN: ">>=", UNSIGNED_SHIFT_RIGHT_ASSIGN: ">>>=", AND_NOT_ASSIGN: "&^=", LOGICAL_AND: "&&", LOGICAL_OR: "||", INCREMENT: "++", DECREMENT: "--", EQUAL: "==", STRICT_EQUAL: "===", LESS: "<", GREATER: ">", ASSIGN: "=", NOT: "!", BITWISE_NOT: "~", NOT_EQUAL: "!=", STRICT_NOT_EQUAL: "!==", LESS_OR_EQUAL: "<=", GREATER_OR_EQUAL: ">=", LEFT_PARENTHESIS: "(", LEFT_BRACKET: "[", LEFT_BRACE: "{", COMMA: ",", PERIOD: ".", RIGHT_PARENTHESIS: ")", RIGHT_BRACKET: "]", RIGHT_BRACE: "}", SEMICOLON: ";", COLON: ":", QUESTION_MARK: "?", IF: "if", IN: "in", DO: "do", VAR: "var", FOR: "for", NEW: "new", TRY: "try", THIS: "this", ELSE: "else", CASE: "case", VOID: "void", WITH: "with", WHILE: "while", BREAK: "break", CATCH: "catch", THROW: "throw", RETURN: "return", TYPEOF: "typeof", DELETE: "delete", SWITCH: "switch", DEFAULT: "default", FINALLY: "finally", FUNCTION: "function", CONTINUE: "continue", DEBUGGER: "debugger", INSTANCEOF: "instanceof", } var keywordTable = map[string]keyword{ "if": { token: IF, }, "in": { token: IN, }, "do": { token: DO, }, "var": { token: VAR, }, "for": { token: FOR, }, "new": { token: NEW, }, "try": { token: TRY, }, "this": { token: THIS, }, "else": { token: ELSE, }, "case": { token: CASE, }, "void": { token: VOID, }, "with": { token: WITH, }, "while": { token: WHILE, }, "break": { token: BREAK, }, "catch": { token: CATCH, }, "throw": { token: THROW, }, "return": { token: RETURN, }, "typeof": { token: TYPEOF, }, "delete": { token: DELETE, }, "switch": { token: SWITCH, }, "default": { token: DEFAULT, }, "finally": { token: FINALLY, }, "function": { token: FUNCTION, }, "continue": { token: CONTINUE, }, "debugger": { token: DEBUGGER, }, "instanceof": { token: INSTANCEOF, }, "const": { token: KEYWORD, futureKeyword: true, }, "class": { token: KEYWORD, futureKeyword: true, }, "enum": { token: KEYWORD, futureKeyword: true, }, "export": { token: KEYWORD, futureKeyword: true, }, "extends": { token: KEYWORD, futureKeyword: true, }, "import": { token: KEYWORD, futureKeyword: true, }, "super": { token: KEYWORD, futureKeyword: true, }, "implements": { token: KEYWORD, futureKeyword: true, strict: true, }, "interface": { token: KEYWORD, futureKeyword: true, strict: true, }, "let": { token: KEYWORD, futureKeyword: true, strict: true, }, "package": { token: KEYWORD, futureKeyword: true, strict: true, }, "private": { token: KEYWORD, futureKeyword: true, strict: true, }, "protected": { token: KEYWORD, futureKeyword: true, strict: true, }, "public": { token: KEYWORD, futureKeyword: true, strict: true, }, "static": { token: KEYWORD, futureKeyword: true, strict: true, }, } ================================================ FILE: tools/gen-jscore/.gen-jscore.yaml ================================================ types: - name: Object core: true properties: - name: length mode: 0 value: 1 - name: prototype mode: 0 value: rt.global.ObjectPrototype - name: getPrototypeOf function: 1 - name: assign function: 1 - name: getOwnPropertyDescriptor function: 2 - name: defineProperty function: 3 - name: defineProperties function: 2 - name: create function: 2 - name: isExtensible function: 1 - name: preventExtensions function: 1 - name: isSealed function: 1 - name: seal function: 1 - name: isFrozen function: 1 - name: freeze function: 1 - name: keys function: 1 - name: values function: 1 - name: getOwnPropertyNames function: 1 prototype: value: prototypeValueObject properties: - name: constructor value: rt.global.Object - name: hasOwnProperty function: 1 - name: isPrototypeOf function: 1 - name: propertyIsEnumerable function: 1 - name: toString function: -1 - name: valueOf function: -1 - name: toLocaleString function: -1 - name: Function core: true properties: - name: length value: 1 - name: prototype value: rt.global.FunctionPrototype prototype: prototype: Object value: prototypeValueFunction properties: - name: toString function: -1 - name: apply function: 2 - name: call function: 1 - name: bind function: 1 - name: constructor value: rt.global.Function - name: length kind: valueNumber value: 0 - name: Array objectClass: Object properties: - name: length value: 1 - name: prototype value: rt.global.ArrayPrototype - name: isArray function: 1 prototype: prototype: Object objectClass: Array value: nil properties: - name: length mode: 0o100 kind: valueNumber value: uint32(0) - name: constructor value: rt.global.Array - name: concat function: 1 - name: lastIndexOf function: 1 - name: pop function: -1 - name: push function: 1 - name: reverse function: -1 - name: shift function: -1 - name: unshift function: 1 - name: slice function: 2 - name: sort function: 1 - name: splice function: 2 - name: indexOf function: 1 - name: join function: 1 - name: forEach function: 1 - name: filter function: 1 - name: map function: 1 - name: every function: 1 - name: some function: 1 - name: reduce function: 1 - name: reduceRight function: 1 - name: toLocaleString function: -1 - name: toString function: -1 - name: String properties: - name: length value: 1 - name: prototype value: rt.global.StringPrototype - name: fromCharCode function: 1 prototype: objectClass: String prototype: Object value: prototypeValueString properties: - name: length kind: valueNumber value: int(0) - name: constructor value: rt.global.String - name: charAt function: 1 - name: charCodeAt function: 1 - name: concat function: 1 - name: indexOf function: 1 - name: lastIndexOf function: 1 - name: localeCompare function: 1 - name: match function: 1 - name: replace function: 2 - name: search function: 1 - name: slice function: 2 - name: split function: 2 - name: substr function: 2 - name: substring function: 2 - name: startsWith function: 1 - name: toString function: -1 - name: trim function: -1 - name: trimLeft function: -1 - name: trimRight function: -1 - name: trimStart function: -1 - name: trimEnd function: -1 - name: toLocaleLowerCase function: -1 - name: toLocaleUpperCase function: -1 - name: toLowerCase function: -1 - name: toUpperCase function: -1 - name: valueOf function: -1 - name: Boolean properties: - name: length value: 1 - name: prototype value: rt.global.BooleanPrototype prototype: prototype: Object value: prototypeValueBoolean properties: - name: constructor value: rt.global.Boolean - name: toString function: -1 - name: valueOf function: -1 globals: length: 1 - name: Number properties: - name: length value: 1 - name: prototype value: rt.global.NumberPrototype - name: isNaN function: 1 - name: MAX_VALUE value: math.MaxFloat64 kind: valueNumber - name: MIN_VALUE kind: valueNumber value: math.SmallestNonzeroFloat64 - name: NaN kind: valueNumber value: math.NaN() - name: NEGATIVE_INFINITY kind: valueNumber value: math.Inf(-1) - name: POSITIVE_INFINITY kind: valueNumber value: math.Inf(+1) prototype: prototype: Object value: prototypeValueNumber properties: - name: constructor value: rt.global.Number - name: toExponential function: 1 - name: toFixed function: 1 - name: toPrecision function: 1 - name: toString function: -1 - name: valueOf function: -1 - name: toLocaleString function: 1 - name: Math class: Math objectPrototype: Object properties: - name: abs function: 1 - name: acos function: 1 - name: acosh function: 1 - name: asin function: 1 - name: asinh function: 1 - name: atan function: 1 - name: atanh function: 1 - name: atan2 function: 1 - name: cbrt function: 1 - name: ceil function: 1 - name: cos function: 1 - name: cosh function: 1 - name: exp function: 1 - name: expm1 function: 1 - name: floor function: 1 - name: log function: 1 - name: log10 function: 1 - name: log1p function: 1 - name: log2 function: 1 - name: max function: 2 - name: min function: 2 - name: pow function: 2 - name: random function: -1 - name: round function: 1 - name: sin function: 1 - name: sinh function: 1 - name: sqrt function: 1 - name: tan function: 1 - name: tanh function: 1 - name: trunc function: 1 - name: E kind: valueNumber value: math.E - name: LN10 kind: valueNumber value: math.Ln10 - name: LN2 kind: valueNumber value: math.Ln2 - name: LOG10E kind: valueNumber value: math.Log10E - name: LOG2E kind: valueNumber value: math.Log2E - name: PI kind: valueNumber value: math.Pi - name: SQRT1_2 kind: valueNumber value: sqrt1_2 - name: SQRT2 kind: valueNumber value: math.Sqrt2 - name: Date properties: - name: length value: 7 - name: prototype value: rt.global.DatePrototype - name: parse function: 1 - name: UTC function: 7 - name: now function: -1 prototype: prototype: Object value: prototypeValueDate properties: - name: constructor value: rt.global.Date - name: toString function: -1 - name: toDateString function: -1 - name: toTimeString function: -1 - name: toISOString function: -1 - name: toUTCString function: -1 - name: toGMTString function: -1 - name: getDate function: -1 - name: setDate function: 1 - name: getDay function: -1 - name: getFullYear function: -1 - name: setFullYear function: 3 - name: getHours function: -1 - name: setHours function: 4 - name: getMilliseconds function: -1 - name: setMilliseconds function: 1 - name: getMinutes function: -1 - name: setMinutes function: 3 - name: getMonth function: -1 - name: setMonth function: 2 - name: getSeconds function: -1 - name: setSeconds function: 2 - name: getTime function: -1 - name: setTime function: 1 - name: getTimezoneOffset function: -1 - name: getUTCDate function: -1 - name: setUTCDate function: 1 - name: getUTCDay function: -1 - name: getUTCFullYear function: -1 - name: setUTCFullYear function: 3 - name: getUTCHours function: -1 - name: setUTCHours function: 4 - name: getUTCMilliseconds function: -1 - name: setUTCMilliseconds function: 1 - name: getUTCMinutes function: -1 - name: setUTCMinutes function: 3 - name: getUTCMonth function: -1 - name: setUTCMonth function: 2 - name: getUTCSeconds function: -1 - name: setUTCSeconds function: 2 - name: valueOf function: -1 - name: getYear function: -1 - name: setYear function: 1 - name: toJSON function: 1 - name: toLocaleString function: -1 - name: toLocaleDateString function: -1 - name: toLocaleTimeString function: -1 - name: RegExp properties: - name: length value: 2 - name: prototype value: rt.global.RegExpPrototype prototype: prototype: Object value: prototypeValueRegExp properties: - name: constructor value: rt.global.RegExp - name: exec function: 1 - name: compile function: 1 - name: toString function: -1 - name: test function: 1 - name: Error objectPrototype: Function properties: - name: length value: 1 - name: prototype value: rt.global.ErrorPrototype prototype: prototype: Object value: nil properties: - name: constructor value: rt.global.Error - name: name kind: valueString value: classErrorName mode: 0o101 - name: message kind: valueString value: '""' mode: 0o101 - name: toString function: -1 - name: EvalError objectPrototype: Function properties: - name: length value: 1 - name: prototype value: rt.global.EvalErrorPrototype prototype: prototype: Error value: nil properties: - name: constructor value: rt.global.EvalError - name: name kind: valueString value: classEvalErrorName mode: 0o101 - name: message kind: valueString value: '""' mode: 0o101 - name: toString function: -1 call: ErrorToString - name: TypeError objectPrototype: Function properties: - name: length value: 1 - name: prototype value: rt.global.TypeErrorPrototype prototype: prototype: Error value: nil properties: - name: constructor value: rt.global.TypeError - name: name kind: valueString value: classTypeErrorName mode: 0o101 - name: message kind: valueString value: '""' mode: 0o101 - name: toString function: -1 call: ErrorToString - name: RangeError objectPrototype: Function properties: - name: length value: 1 - name: prototype value: rt.global.RangeErrorPrototype prototype: prototype: Error value: nil properties: - name: constructor value: rt.global.RangeError - name: name kind: valueString value: classRangeErrorName mode: 0o101 - name: message kind: valueString value: '""' mode: 0o101 - name: toString function: -1 call: ErrorToString - name: ReferenceError objectPrototype: Function properties: - name: length value: 1 - name: prototype value: rt.global.ReferenceErrorPrototype prototype: prototype: Error value: nil properties: - name: constructor value: rt.global.ReferenceError - name: name kind: valueString value: classReferenceErrorName mode: 0o101 - name: message kind: valueString value: '""' mode: 0o101 - name: toString function: -1 call: ErrorToString - name: SyntaxError objectPrototype: Function properties: - name: length value: 1 - name: prototype value: rt.global.SyntaxErrorPrototype prototype: prototype: Error value: nil properties: - name: constructor value: rt.global.SyntaxError - name: name kind: valueString value: classSyntaxErrorName mode: 0o101 - name: message kind: valueString value: '""' mode: 0o101 - name: toString function: -1 call: ErrorToString - name: URIError objectPrototype: Function properties: - name: length value: 1 - name: prototype value: rt.global.URIErrorPrototype prototype: prototype: Error value: nil properties: - name: constructor value: rt.global.URIError - name: name kind: valueString value: classURIErrorName mode: 0o101 - name: message kind: valueString value: '""' mode: 0o101 - name: toString function: -1 call: ErrorToString - name: JSON class: JSON objectPrototype: Object properties: - name: parse function: 2 - name: stringify function: 3 - name: Global properties: - name: eval function: 1 - name: parseInt function: 2 - name: parseFloat function: 1 - name: isNaN function: 1 - name: isFinite function: 1 - name: decodeURI function: 1 - name: decodeURIComponent function: 1 - name: encodeURI function: 1 - name: encodeURIComponent function: 1 - name: escape function: 1 - name: unescape function: 1 - name: Object mode: 0o101 value: rt.global.Object - name: Function mode: 0o101 value: rt.global.Function - name: Array mode: 0o101 value: rt.global.Array - name: String mode: 0o101 value: rt.global.String - name: Boolean mode: 0o101 value: rt.global.Boolean - name: Number mode: 0o101 value: rt.global.Number - name: Math mode: 0o101 value: rt.global.Math - name: Date mode: 0o101 value: rt.global.Date - name: RegExp mode: 0o101 value: rt.global.RegExp - name: Error mode: 0o101 value: rt.global.Error - name: EvalError mode: 0o101 value: rt.global.EvalError - name: TypeError mode: 0o101 value: rt.global.TypeError - name: RangeError mode: 0o101 value: rt.global.RangeError - name: ReferenceError mode: 0o101 value: rt.global.ReferenceError - name: SyntaxError mode: 0o101 value: rt.global.SyntaxError - name: URIError mode: 0o101 value: rt.global.URIError - name: JSON mode: 0o101 value: rt.global.JSON - name: undefined kind: valueUndefined - name: NaN kind: valueNumber value: math.NaN() - name: Infinity kind: valueNumber value: math.Inf(+1) log: name: Console class: Object objectPrototype: Object properties: - name: log function: -1 - name: debug call: ConsoleLog function: -1 - name: info call: ConsoleLog function: -1 - name: error function: -1 - name: warn function: -1 call: ConsoleError - name: dir function: -1 - name: time function: -1 - name: timeEnd function: -1 - name: trace function: -1 - name: assert function: -1 values: - name: int - name: int32 - name: int64 - name: uint16 - name: uint32 - name: float64 - name: string - name: string16 type: "[]uint16" - name: bool - name: object type: "*object" ================================================ FILE: tools/gen-jscore/helpers.go ================================================ package main import ( "fmt" "unicode" ) // ucfirst converts the first rune of val to uppercase an returns the result. func ucfirst(val string) string { r := []rune(val) r[0] = unicode.ToUpper(r[0]) return string(r) } // dict is a template helper returns a map created from alternating values. // Values must be passed as key, value pairs with key being a string. // It can be used to pass the combination of multiple values to a template. func dict(values ...interface{}) (map[string]interface{}, error) { if len(values)%2 != 0 { return nil, fmt.Errorf("map requires parameters which are multiple of 2 got %d", len(values)) } m := make(map[string]interface{}, len(values)/2) for i := 0; i < len(values); i += 2 { key, ok := values[i].(string) if !ok { return nil, fmt.Errorf("map keys must be strings got %T", values[i]) } m[key] = values[i+1] } return m, nil } ================================================ FILE: tools/gen-jscore/main.go ================================================ // Command gen-jscore generates go representations of JavaScript core types file. package main import ( "embed" "flag" "fmt" "log" "os" "os/exec" "strings" "text/template" "gopkg.in/yaml.v3" ) //go:embed .gen-jscore.yaml var configData []byte //go:embed templates/* var templates embed.FS // jsType represents JavaScript type to generate. type jsType struct { Prototype *prototype `yaml:"prototype"` Name string `yaml:"name"` ObjectClass string `yaml:"objectClass"` ObjectPrototype string `yaml:"objectPrototype"` Class string `yaml:"class"` Value string `yaml:"value"` Properties []property `yaml:"properties"` Core bool `yaml:"core"` } // BlankConstructor is a default fallback returning false for templates. func (t jsType) BlankConstructor() bool { return false } // prototype represents a JavaScript prototype to generate. type prototype struct { Value string `yaml:"value"` ObjectClass string `yaml:"objectClass"` Prototype string `yaml:"prototype"` Properties []property `yaml:"properties"` } // Property returns the property with the given name. func (p prototype) Property(name string) (*property, error) { for _, prop := range p.Properties { if prop.Name == name { return &prop, nil } } return nil, fmt.Errorf("missing property %q", name) } // property represents a JavaScript property to generate. type property struct { Name string `yaml:"name"` Call string `yaml:"call"` Mode string `yaml:"mode"` Value string `yaml:"value"` Kind string `yaml:"kind"` Function int `yaml:"function"` } // value represents a JavaScript value to generate a Value creator for. type value struct { Name string `yaml:"name"` Type string `yaml:"type"` } // config represents our configuration. type config struct { Types []jsType `yaml:"types"` Values []value `yaml:"values"` Log jsType `yaml:"log"` } // Type returns the type for name. func (c config) Type(name string) (*jsType, error) { for _, t := range c.Types { if t.Name == name { return &t, nil } } return nil, fmt.Errorf("missing type %q", name) } // generate generates the context file writing the output to filename. func generate(filename string) (err error) { var cfg config if err = yaml.Unmarshal(configData, &cfg); err != nil { return fmt.Errorf("decode config: %w", err) } tmpl := template.New("base").Funcs(template.FuncMap{ "ucfirst": ucfirst, "dict": dict, "contains": strings.Contains, }) tmpl, err = tmpl.ParseFS(templates, "templates/*.tmpl") if err != nil { return fmt.Errorf("parse templates: %w", err) } output, err := os.Create(filename) //nolint:gosec if err != nil { return fmt.Errorf("open output: %w", err) } defer func() { if errc := output.Close(); err == nil && errc != nil { err = errc } }() if err = tmpl.ExecuteTemplate(output, "root.tmpl", cfg); err != nil { return fmt.Errorf("execute template: %w", err) } cmd := exec.Command("gofmt", "-w", filename) buf, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("format output %q: %w", string(buf), err) } return nil } func main() { var filename string flag.StringVar(&filename, "output", "inline.go", "the filename to write the generated code to") if err := generate(filename); err != nil { log.Fatal(err) } } ================================================ FILE: tools/gen-jscore/templates/constructor.tmpl ================================================ {{- with .Prototype.Property "constructor"}} // {{$.Name}} constructor definition. rt.global.{{$.Name}}Prototype.property[{{template "name.tmpl" .Name}}] = property{{template "property-value.tmpl" dict "Name" $.Name "Core" true "Property" .}} {{- end}} ================================================ FILE: tools/gen-jscore/templates/core-prototype-property.tmpl ================================================ {{/* Expects .(jsType) */}} // {{.Name}} prototype property definition. rt.global.{{.Name}}Prototype.property = {{template "property.tmpl" dict "Name" .Name "BlankConstructor" true "Properties" .Prototype.Properties}} rt.global.{{.Name}}Prototype.propertyOrder = {{template "property-order.tmpl" .Prototype}}{{/* No newline. */}} ================================================ FILE: tools/gen-jscore/templates/definition.tmpl ================================================ &object{ runtime: rt, class: class{{or .Class "Function"}}Name, objectClass: class{{or .ObjectClass "Object"}}, prototype: rt.global.{{or .ObjectPrototype "Function"}}Prototype, extensible: true, {{- if not .Class}} value: nativeFunctionObject{ name: class{{or .Value .Name}}Name, call: builtin{{or .Value .Name}}, construct: builtinNew{{or .Value .Name}}, }, {{- end}} {{- template "property-fields.tmpl" .}} }{{/* No newline */ -}} ================================================ FILE: tools/gen-jscore/templates/function.tmpl ================================================ &object{ runtime: rt, class: classFunctionName, objectClass: classObject, prototype: rt.global.FunctionPrototype, extensible: true, property: map[string]property{ propertyLength: { mode: 0, value: Value{ kind: valueNumber, value: {{if eq .Property.Function -1}}0{{else}}{{.Property.Function}}{{end}}, }, }, propertyName: { mode: 0, value: Value{ kind: valueString, value: "{{.Property.Name}}", }, }, }, propertyOrder: []string{ propertyLength, propertyName, }, value: nativeFunctionObject{ name: {{template "name.tmpl" .Property.Name}}, call: builtin{{if .Property.Call}}{{.Property.Call}}{{else}}{{.Name}}{{.Property.Name | ucfirst}}{{end}}, }, }{{/* No newline. */ -}} ================================================ FILE: tools/gen-jscore/templates/global.tmpl ================================================ // {{.Name}} properties. rt.globalObject.property = {{template "property.tmpl" .}} // {{.Name}} property order. rt.globalObject.propertyOrder = {{template "property-order.tmpl" . -}} ================================================ FILE: tools/gen-jscore/templates/name.tmpl ================================================ {{- if eq . "length" "prototype" "constructor" -}} property{{ucfirst .}} {{- else if eq . "toString" -}} methodToString {{- else if eq . "Object" "Function" "Array" "String" "Boolean" "Number" "Math" "Date" "RegExp" "Error" "EvalError" "TypeError" "RangeError" "ReferenceError" "SyntaxError" "URIError" "JSON" -}} class{{.}}Name {{- else -}} "{{.}}" {{- end -}} ================================================ FILE: tools/gen-jscore/templates/property-entry.tmpl ================================================ {{- template "name.tmpl" .Property.Name}}: {{- template "property-value.tmpl" .}},{{/* No newline */ -}} ================================================ FILE: tools/gen-jscore/templates/property-fields.tmpl ================================================ {{- /* expects .Name and .Properties */ -}} {{if .Properties}} property: {{template "property.tmpl" .}}, propertyOrder: {{template "property-order.tmpl" .}}, {{end -}} ================================================ FILE: tools/gen-jscore/templates/property-order.tmpl ================================================ {{- /* expects .Properties */ -}} []string{ {{range .Properties -}} {{template "name.tmpl" .Name}}, {{end -}} }{{/* No newline */ -}} ================================================ FILE: tools/gen-jscore/templates/property-value.tmpl ================================================ {{with .Property}} { mode: {{if .Mode}}{{.Mode}}{{else if or .Function (eq .Name "constructor")}}0o101{{else}}0{{end}}, {{- if eq .Name "constructor" | and $.BlankConstructor}} value: Value{}, {{- else}} value: Value{ kind: {{if .Kind}}{{.Kind}}{{else if eq .Name "length"}}valueNumber{{else}}valueObject{{end}}, {{- if .Function}} value: {{template "function.tmpl" $}}, {{- else if .Value}} value: {{.Value}}, {{- end}} }, {{- end}} }{{end -}} ================================================ FILE: tools/gen-jscore/templates/property.tmpl ================================================ {{- /* Expects .Name and .Properties */ -}} map[string]property{ {{range .Properties -}} {{- /* Skip constructor which is output later. */}} {{- if eq .Name "constructor" | not -}} {{template "property-entry.tmpl" dict "Name" $.Name "BlankConstructor" $.BlankConstructor "Property" .}} {{end -}} {{end -}} }{{/* No newline */ -}} ================================================ FILE: tools/gen-jscore/templates/prototype.tmpl ================================================ {{/* Expects .Name.(jsType.Name), .Prototype and optional .BlankConstructor */}} {{- with .Prototype}} // {{$.Name}} prototype. rt.global.{{$.Name}}Prototype = &object{ runtime: rt, class: class{{$.Name}}Name, objectClass: class{{or .ObjectClass "Object"}}, prototype: {{if .Prototype}}rt.global.{{.Prototype}}Prototype{{else}}nil{{end}}, extensible: true, value: {{or .Value (print "prototypeValue" $.Name)}}, {{- if not $.Core}} {{- template "property-fields.tmpl" dict "Name" $.Name "BlankConstructor" $.BlankConstructor "Properties" .Properties}} {{- end}} } {{- end -}} ================================================ FILE: tools/gen-jscore/templates/root.tmpl ================================================ // Code generated by tools/gen-jscore. DO NOT EDIT. package otto import ( "math" ) func (rt *runtime) newContext() { // Order here is import as definitions depend on each other. {{- $object := .Type "Object"}} {{- $function := .Type "Function"}} {{template "prototype.tmpl" $object}} {{template "prototype.tmpl" $function}} {{- template "core-prototype-property.tmpl" $object}} {{- template "core-prototype-property.tmpl" $function}} {{- template "type.tmpl" $object}} {{- template "type.tmpl" $function}} {{- range .Types}} {{- if eq .Name "Global"}} {{template "global.tmpl" . }} {{- else if not .Core}} {{template "type.tmpl" .}} {{- end}} {{- end}} } func (rt *runtime) newConsole() *object { return {{template "definition.tmpl" .Log}} } {{range .Values}} {{template "value.tmpl" .}} {{- end}} ================================================ FILE: tools/gen-jscore/templates/type.tmpl ================================================ {{if not .Core | and .Prototype}} {{template "prototype.tmpl" dict "Name" .Name "Prototype" .Prototype}} {{- end}} // {{.Name}} definition. rt.global.{{.Name}} = {{template "definition.tmpl" .}} {{- if .Prototype}} {{template "constructor.tmpl" .}} {{- end}} ================================================ FILE: tools/gen-jscore/templates/value.tmpl ================================================ func {{.Name}}Value(value {{or .Type .Name}}) Value { return Value{ kind: {{- if contains .Name "string"}} valueString {{- else if contains .Name "bool"}} valueBoolean {{- else if contains .Name "object"}} valueObject {{- else}} valueNumber {{- end}}, value: value, } } ================================================ FILE: tools/gen-tokens/.gen-tokens.yaml ================================================ tokens: - group: Control - name: ILLEGAL - name: EOF - name: COMMENT - name: KEYWORD - group: Types - name: STRING - name: BOOLEAN - name: "NULL" - name: NUMBER - name: IDENTIFIER - group: Maths - name: PLUS symbol: "+" - name: MINUS symbol: "-" - name: MULTIPLY symbol: "*" - name: SLASH symbol: "/" - name: REMAINDER symbol: "%" - group: Logical and bitwise operators - name: AND symbol: "&" - name: OR symbol: "|" - name: EXCLUSIVE_OR symbol: "^" - name: SHIFT_LEFT symbol: "<<" - name: SHIFT_RIGHT symbol: ">>" - name: UNSIGNED_SHIFT_RIGHT symbol: ">>>" - name: AND_NOT symbol: "&^" - group: Math assignments - name: ADD_ASSIGN symbol: "+=" - name: SUBTRACT_ASSIGN symbol: "-=" - name: MULTIPLY_ASSIGN symbol: "*=" - name: QUOTIENT_ASSIGN symbol: "/=" - name: REMAINDER_ASSIGN symbol: "%=" - group: Math and bitwise assignments - name: AND_ASSIGN symbol: "&=" - name: OR_ASSIGN symbol: "|=" - name: EXCLUSIVE_OR_ASSIGN symbol: "^=" - name: SHIFT_LEFT_ASSIGN symbol: "<<=" - name: SHIFT_RIGHT_ASSIGN symbol: ">>=" - name: UNSIGNED_SHIFT_RIGHT_ASSIGN symbol: ">>>=" - name: AND_NOT_ASSIGN symbol: "&^=" - group: Logical operators and decrement / increment - name: LOGICAL_AND symbol: "&&" - name: LOGICAL_OR symbol: "||" - name: INCREMENT symbol: "++" - name: DECREMENT symbol: "--" - group: Comparison operators - name: EQUAL symbol: "==" - name: STRICT_EQUAL symbol: "===" - name: LESS symbol: "<" - name: GREATER symbol: ">" - name: ASSIGN symbol: "=" - name: NOT symbol: "!" - group: Bitwise not - name: BITWISE_NOT symbol: "~" - group: Comparison operators - name: NOT_EQUAL symbol: "!=" - name: STRICT_NOT_EQUAL symbol: "!==" - name: LESS_OR_EQUAL symbol: "<=" - name: GREATER_OR_EQUAL symbol: ">=" - group: Left operators - name: LEFT_PARENTHESIS symbol: "(" - name: LEFT_BRACKET symbol: "[" - name: LEFT_BRACE symbol: "{" - name: COMMA symbol: "," - name: PERIOD symbol: "." - group: Right operators - name: RIGHT_PARENTHESIS symbol: ")" - name: RIGHT_BRACKET symbol: "]" - name: RIGHT_BRACE symbol: "}" - name: SEMICOLON symbol: ";" - name: COLON symbol: ":" - name: QUESTION_MARK symbol: "?" - group: Basic flow - keywords below here - name: _ - name: IF - name: IN - name: DO - group: Declarations - name: VAR - name: FOR - name: NEW - name: TRY - group: Advanced flow - name: THIS - name: ELSE - name: CASE - name: VOID - name: WITH - group: Loops - name: WHILE - name: BREAK - name: CATCH - name: THROW - group: Functions - name: RETURN - name: TYPEOF - name: DELETE - name: SWITCH - group: Fallback identifiers - name: DEFAULT - name: FINALLY - group: Miscellaneous - name: FUNCTION - name: CONTINUE - name: DEBUGGER - group: Instance of - name: INSTANCEOF # Future - name: const future: true - name: class future: true - name: enum future: true - name: export future: true - name: extends future: true - name: import future: true - name: super future: true # Future Strict items - name: implements future: true strict: true - name: interface future: true strict: true - name: let future: true strict: true - name: package future: true strict: true - name: private future: true strict: true - name: protected future: true strict: true - name: public future: true strict: true - name: static future: true strict: true ================================================ FILE: tools/gen-tokens/main.go ================================================ // Command gen-tokens generates go representations of JavaScript tokens. package main import ( "embed" "flag" "fmt" "log" "os" "os/exec" "strings" "text/template" "gopkg.in/yaml.v3" ) //go:embed .gen-tokens.yaml var configData []byte //go:embed templates/* var templates embed.FS // token represents a JavaScript token. type token struct { Group string `yaml:"group"` Name string `yaml:"name"` Symbol string `yaml:"symbol"` Future bool `yaml:"future"` Strict bool `yaml:"strict"` } // config represents our configuration. type config struct { Tokens []token `yaml:"tokens"` } // generate generates the context file writing the output to filename. func generate(filename string) (err error) { var cfg config if err = yaml.Unmarshal(configData, &cfg); err != nil { return fmt.Errorf("decode config: %w", err) } tmpl := template.New("base").Funcs(template.FuncMap{ "toLower": strings.ToLower, }) tmpl, err = tmpl.ParseFS(templates, "templates/*.tmpl") if err != nil { return fmt.Errorf("parse templates: %w", err) } output, err := os.Create(filename) //nolint:gosec if err != nil { return fmt.Errorf("open output: %w", err) } defer func() { if errc := output.Close(); err == nil && errc != nil { err = errc } }() if err = tmpl.ExecuteTemplate(output, "root.tmpl", cfg); err != nil { return fmt.Errorf("execute template: %w", err) } cmd := exec.Command("gofmt", "-w", filename) buf, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("format output %q: %w", string(buf), err) } return nil } func main() { var filename string flag.StringVar(&filename, "output", "token_const.go", "the filename to write the generated code to") if err := generate(filename); err != nil { log.Fatal(err) } } ================================================ FILE: tools/gen-tokens/templates/root.tmpl ================================================ // Code generated by tools/gen-tokens. DO NOT EDIT. package token const ( _ Token = iota {{range .Tokens}} {{- if not .Future}} {{- if .Group}} // {{.Group}}. {{- else}} {{.Name}} {{- if .Symbol}}// {{.Symbol}}{{end}} {{- end}} {{- end}} {{- end}} ) var token2string = [...]string{ {{- $lc := false -}} {{- range .Tokens -}} {{if or .Future .Group | not -}} {{- if eq .Name "_" -}} {{$lc = true}} {{- else}} {{- $symbol := or .Symbol .Name}} {{.Name}}: "{{if $lc}}{{toLower $symbol}}{{else}}{{$symbol}}{{end}}", {{- end}} {{- end -}} {{- end}} } var keywordTable = map[string]keyword{ {{- $keyword := false -}} {{range .Tokens}} {{- /* First keyword follows _ */ -}} {{- if eq .Name "_"}}{{$keyword = true}}{{continue}}{{end}} {{- if $keyword}} {{- if or .Symbol .Group | not}} "{{toLower .Name}}": { {{- if .Future}} token: KEYWORD, futureKeyword: true, {{- else}} token: {{.Name}}, {{- end}} {{- if .Strict}} strict: true, {{- end}} }, {{- end -}} {{end -}} {{- end}} } ================================================ FILE: tools/tester/main.go ================================================ // Command tester automates the ability to download a suite of JavaScript libraries from a CDN and check if otto can handle them. // // It provides two commands via flags: // * -fetch = Fetch all libraries from the CDN and store them in local testdata directory. // * -report [file1 file2 ... fileN] = Report the results of trying to run the given or if empty all libraries in the testdata directory. package main import ( "context" "encoding/json" "errors" "flag" "fmt" "io" "io/fs" "net/http" "os" "path/filepath" "regexp" "runtime" "strings" "sync" "text/tabwriter" "time" "github.com/robertkrimen/otto" "github.com/robertkrimen/otto/parser" ) const ( // dataDir is where the libraries are downloaded to. dataDir = "testdata" // downloadWorkers is the number of workers that process downloads. downloadWorkers = 40 // librariesURL is the source for JavaScript libraries for testing. librariesURL = "http://api.cdnjs.com/libraries" // requestTimeout is the maximum time we wait for a request to complete. requestTimeout = time.Second * 20 ) var ( // testWorkers is the number of workers that process report. testWorkers = min(10, runtime.GOMAXPROCS(0)) // noopConsole is a noopConsole which ignore log requests. noopConsole = map[string]interface{}{ "log": func(call otto.FunctionCall) otto.Value { return otto.UndefinedValue() }, } ) var ( matchReferenceErrorNotDefined = regexp.MustCompile(`^ReferenceError: \S+ is not defined$`) matchLookahead = regexp.MustCompile(`Invalid regular expression: re2: Invalid \(\?[=!]\) `) matchBackReference = regexp.MustCompile(`Invalid regular expression: re2: Invalid \\\d `) matchTypeErrorUndefined = regexp.MustCompile(`^TypeError: Cannot access member '[^']+' of undefined$`) ) // broken identifies libraries which fail with a fatal error, so must be skipped. var broken = map[string]string{ "lets-plot.js": "stack overflow", "knockout-es5.js": "stack overflow", "sendbird-calls.js": "runtime: out of memory", } // libraries represents fetch all libraries response. type libraries struct { Results []library `json:"results"` } // library represents a single library in a libraries response. type library struct { Name string `json:"name"` Latest string `json:"latest"` } // fetch fetches itself and stores it in the dataDir. func (l library) fetch() error { if !strings.HasSuffix(l.Latest, ".js") { return nil } ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, l.Latest, nil) if err != nil { return fmt.Errorf("request library %q: %w", l.Name, err) } resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("get library %q: %w", l.Name, err) } defer resp.Body.Close() //nolint:errcheck name := l.Name if !strings.HasSuffix(name, ".js") { name += ".js" } f, err := os.Create(filepath.Join(dataDir, name)) //nolint:gosec if err != nil { return fmt.Errorf("create library %q: %w", l.Name, err) } if _, err = io.Copy(f, resp.Body); err != nil { return fmt.Errorf("write library %q: %w", l.Name, err) } return nil } // test runs the code from filename returning the time it took and any error // encountered when running a full parse without IgnoreRegExpErrors in parseError. func test(filename string) (took time.Duration, parseError, err error) { //nolint:nonamedreturns defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic on %q: %v", filename, r) } }() now := time.Now() defer func() { // Always set took. took = time.Since(now) }() if val := broken[filepath.Base(filename)]; val != "" { return 0, nil, fmt.Errorf("fatal %q", val) } script, err := os.ReadFile(filename) //nolint:gosec if err != nil { return 0, nil, err } vm := otto.New() if err = vm.Set("console", noopConsole); err != nil { return 0, nil, fmt.Errorf("set console: %w", err) } prog, err := parser.ParseFile(nil, filename, string(script), 0) if err != nil { val := err.Error() switch { case matchReferenceErrorNotDefined.MatchString(val), matchTypeErrorUndefined.MatchString(val), matchLookahead.MatchString(val), matchBackReference.MatchString(val): // RegExp issues, retry with IgnoreRegExpErrors. parseError = err if _, err = parser.ParseFile(nil, filename, string(script), parser.IgnoreRegExpErrors); err != nil { return 0, nil, err } return 0, parseError, nil default: return 0, nil, err } } _, err = vm.Run(prog) return 0, nil, err } // fetchAll fetches all files from src. func fetchAll(src string) error { ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, librariesURL, nil) if err != nil { return fmt.Errorf("request libraries %q: %w", src, err) } resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("get libraries %q: %w", src, err) } defer resp.Body.Close() //nolint:errcheck var libs libraries dec := json.NewDecoder(resp.Body) if err = dec.Decode(&libs); err != nil { return fmt.Errorf("json decode: %w", err) } if err = os.Mkdir(dataDir, 0o750); err != nil && !errors.Is(err, fs.ErrExist) { return fmt.Errorf("mkdir: %w", err) } var wg sync.WaitGroup work := make(chan library, downloadWorkers) errs := make(chan error, len(libs.Results)) wg.Add(downloadWorkers) for range downloadWorkers { go func() { defer wg.Done() for lib := range work { fmt.Fprint(os.Stdout, ".") errs <- lib.fetch() } }() } fmt.Fprintf(os.Stdout, "Downloading %d libraries with %d workers ...", len(libs.Results), downloadWorkers) wg.Add(1) go func() { defer wg.Done() for _, lib := range libs.Results { work <- lib } close(work) }() wg.Wait() close(errs) fmt.Fprintln(os.Stdout, " done") for e := range errs { if e != nil { fmt.Fprintln(os.Stderr, e) err = e } } return err } // result represents the result from a test. type result struct { err error parseError error filename string took time.Duration } // report runs test for all specified files, if none a specified all // JavaScript files in our dataDir, outputting the results. func report(files []string) error { if len(files) == 0 { var err error files, err = filepath.Glob(filepath.Join(dataDir, "*.js")) if err != nil { return fmt.Errorf("read dir: %w", err) } } var wg sync.WaitGroup workers := min(testWorkers, len(files)) work := make(chan string, workers) results := make(chan result, len(files)) wg.Add(workers) for range workers { go func() { defer wg.Done() for f := range work { fmt.Fprint(os.Stdout, ".") took, parseError, err := test(f) results <- result{ filename: f, err: err, parseError: parseError, took: took, } } }() } fmt.Fprintf(os.Stdout, "Reporting on %d libs with %d workers...", len(files), workers) wg.Add(1) go func() { defer wg.Done() for _, f := range files { work <- f } close(work) }() wg.Wait() close(results) fmt.Fprintln(os.Stdout, " done") var fail, pass, parse int writer := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0) fmt.Fprintln(writer, "Library", "\t| Took", "\t| Status") fmt.Fprintln(writer, "-------", "\t| ----", "\t| ------") for res := range results { switch { case res.err != nil: fmt.Fprintf(writer, "%s\t| %v\t| fail: %v\n", res.filename, res.took, res.err) fail++ case res.parseError != nil: fmt.Fprintf(writer, "%s\t| %v\t| pass parse: %v\n", res.filename, res.took, res.parseError) parse++ default: fmt.Fprintf(writer, "%s\t| %v\t| pass\n", res.filename, res.took) pass++ } } if err := writer.Flush(); err != nil { return fmt.Errorf("flush: %w", err) } fmt.Fprintf(os.Stdout, "\nSummary:\n - %d passes\n - %d parse passes\n - %d fails\n", pass, parse, fail) return nil } func main() { flagFetch := flag.Bool("fetch", false, "fetch all libraries for testing") flagReport := flag.Bool("report", false, "test and report the named files or all libraries if non specified") flag.Parse() var err error switch { case *flagFetch: err = fetchAll(librariesURL) case *flagReport: err = report(flag.Args()) default: flag.PrintDefaults() err = errors.New("missing flag") } if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(64) } } ================================================ FILE: type_arguments.go ================================================ package otto import ( "strconv" ) func (rt *runtime) newArgumentsObject(indexOfParameterName []string, stash stasher, length int) *object { obj := rt.newClassObject("Arguments") for index := range indexOfParameterName { name := strconv.FormatInt(int64(index), 10) objectDefineOwnProperty(obj, name, property{Value{}, 0o111}, false) } obj.objectClass = classArguments obj.value = argumentsObject{ indexOfParameterName: indexOfParameterName, stash: stash, } obj.prototype = rt.global.ObjectPrototype obj.defineProperty(propertyLength, intValue(length), 0o101, false) return obj } type argumentsObject struct { stash stasher indexOfParameterName []string } func (o argumentsObject) clone(c *cloner) argumentsObject { indexOfParameterName := make([]string, len(o.indexOfParameterName)) copy(indexOfParameterName, o.indexOfParameterName) return argumentsObject{ indexOfParameterName: indexOfParameterName, stash: c.stash(o.stash), } } func (o argumentsObject) get(name string) (Value, bool) { index := stringToArrayIndex(name) if index >= 0 && index < int64(len(o.indexOfParameterName)) { if name = o.indexOfParameterName[index]; name == "" { return Value{}, false } return o.stash.getBinding(name, false), true } return Value{}, false } func (o argumentsObject) put(name string, value Value) { index := stringToArrayIndex(name) name = o.indexOfParameterName[index] o.stash.setBinding(name, value, false) } func (o argumentsObject) delete(name string) { index := stringToArrayIndex(name) o.indexOfParameterName[index] = "" } func argumentsGet(obj *object, name string) Value { if value, exists := obj.value.(argumentsObject).get(name); exists { return value } return objectGet(obj, name) } func argumentsGetOwnProperty(obj *object, name string) *property { prop := objectGetOwnProperty(obj, name) if value, exists := obj.value.(argumentsObject).get(name); exists { prop.value = value } return prop } func argumentsDefineOwnProperty(obj *object, name string, descriptor property, throw bool) bool { if _, exists := obj.value.(argumentsObject).get(name); exists { if !objectDefineOwnProperty(obj, name, descriptor, false) { return obj.runtime.typeErrorResult(throw) } if value, valid := descriptor.value.(Value); valid { obj.value.(argumentsObject).put(name, value) } return true } return objectDefineOwnProperty(obj, name, descriptor, throw) } func argumentsDelete(obj *object, name string, throw bool) bool { if !objectDelete(obj, name, throw) { return false } if _, exists := obj.value.(argumentsObject).get(name); exists { obj.value.(argumentsObject).delete(name) } return true } ================================================ FILE: type_array.go ================================================ package otto import ( "strconv" ) func (rt *runtime) newArrayObject(length uint32) *object { obj := rt.newObject() obj.class = classArrayName obj.defineProperty(propertyLength, uint32Value(length), 0o100, false) obj.objectClass = classArray return obj } func isArray(obj *object) bool { if obj == nil { return false } switch obj.class { case classArrayName, classGoArrayName, classGoSliceName: return true default: return false } } func objectLength(obj *object) uint32 { if obj == nil { return 0 } switch obj.class { case classArrayName: return obj.get(propertyLength).value.(uint32) case classStringName: return uint32(obj.get(propertyLength).value.(int)) case classGoArrayName, classGoSliceName: return uint32(obj.get(propertyLength).value.(int)) } return 0 } func arrayUint32(rt *runtime, value Value) uint32 { nm := value.number() if nm.kind != numberInteger || !isUint32(nm.int64) { // FIXME panic(rt.panicRangeError()) } return uint32(nm.int64) } func arrayDefineOwnProperty(obj *object, name string, descriptor property, throw bool) bool { lengthProperty := obj.getOwnProperty(propertyLength) lengthValue, valid := lengthProperty.value.(Value) if !valid { panic("Array.length != Value{}") } reject := func(reason string) bool { if throw { panic(obj.runtime.panicTypeError("Array.DefineOwnProperty %s", reason)) } return false } length := lengthValue.value.(uint32) if name == propertyLength { if descriptor.value == nil { return objectDefineOwnProperty(obj, name, descriptor, throw) } newLengthValue, isValue := descriptor.value.(Value) if !isValue { panic(obj.runtime.panicTypeError("Array.DefineOwnProperty %q is not a value", descriptor.value)) } newLength := arrayUint32(obj.runtime, newLengthValue) descriptor.value = uint32Value(newLength) if newLength > length { return objectDefineOwnProperty(obj, name, descriptor, throw) } if !lengthProperty.writable() { return reject("property length for not writable") } newWritable := true if descriptor.mode&0o700 == 0 { // If writable is off newWritable = false descriptor.mode |= 0o100 } if !objectDefineOwnProperty(obj, name, descriptor, throw) { return false } for newLength < length { length-- if !obj.delete(strconv.FormatInt(int64(length), 10), false) { descriptor.value = uint32Value(length + 1) if !newWritable { descriptor.mode &= 0o077 } objectDefineOwnProperty(obj, name, descriptor, false) return reject("delete failed") } } if !newWritable { descriptor.mode &= 0o077 objectDefineOwnProperty(obj, name, descriptor, false) } } else if index := stringToArrayIndex(name); index >= 0 { if index >= int64(length) && !lengthProperty.writable() { return reject("property length not writable") } if !objectDefineOwnProperty(obj, strconv.FormatInt(index, 10), descriptor, false) { return reject("Object.DefineOwnProperty failed") } if index >= int64(length) { lengthProperty.value = uint32Value(uint32(index + 1)) objectDefineOwnProperty(obj, propertyLength, *lengthProperty, false) return true } } return objectDefineOwnProperty(obj, name, descriptor, throw) } ================================================ FILE: type_boolean.go ================================================ package otto func (rt *runtime) newBooleanObject(value Value) *object { return rt.newPrimitiveObject(classBooleanName, boolValue(value.bool())) } ================================================ FILE: type_date.go ================================================ package otto import ( "fmt" "math" "regexp" Time "time" ) type dateObject struct { time Time.Time value Value epoch int64 isNaN bool } var invalidDateObject = dateObject{ time: Time.Time{}, epoch: -1, value: NaNValue(), isNaN: true, } type ecmaTime struct { location *Time.Location year int month int day int hour int minute int second int millisecond int } func newEcmaTime(goTime Time.Time) ecmaTime { return ecmaTime{ year: goTime.Year(), month: dateFromGoMonth(goTime.Month()), day: goTime.Day(), hour: goTime.Hour(), minute: goTime.Minute(), second: goTime.Second(), millisecond: goTime.Nanosecond() / (100 * 100 * 100), location: goTime.Location(), } } func (t *ecmaTime) goTime() Time.Time { return Time.Date( t.year, dateToGoMonth(t.month), t.day, t.hour, t.minute, t.second, t.millisecond*(100*100*100), t.location, ) } func (d *dateObject) Time() Time.Time { return d.time } func (d *dateObject) Epoch() int64 { return d.epoch } func (d *dateObject) Value() Value { return d.value } // FIXME A date should only be in the range of -100,000,000 to +100,000,000 (1970): 15.9.1.1. func (d *dateObject) SetNaN() { d.time = Time.Time{} d.epoch = -1 d.value = NaNValue() d.isNaN = true } func (d *dateObject) SetTime(time Time.Time) { d.Set(timeToEpoch(time)) } func (d *dateObject) Set(epoch float64) { // epoch d.epoch = epochToInteger(epoch) // time time, err := epochToTime(epoch) d.time = time // Is either a valid time, or the zero-value for time.Time // value & isNaN if err != nil { d.isNaN = true d.epoch = -1 d.value = NaNValue() } else { d.value = int64Value(d.epoch) } } func epochToInteger(value float64) int64 { if value > 0 { return int64(math.Floor(value)) } return int64(math.Ceil(value)) } func epochToTime(value float64) (Time.Time, error) { epochWithMilli := value if math.IsNaN(epochWithMilli) || math.IsInf(epochWithMilli, 0) { return Time.Time{}, fmt.Errorf("invalid time %v", value) } epoch := int64(epochWithMilli / 1000) milli := int64(epochWithMilli) % 1000 return Time.Unix(epoch, milli*1000000).In(utcTimeZone), nil } func timeToEpoch(time Time.Time) float64 { return float64(time.UnixMilli()) } func (rt *runtime) newDateObject(epoch float64) *object { obj := rt.newObject() obj.class = classDateName // FIXME This is ugly... date := dateObject{} date.Set(epoch) obj.value = date return obj } func (o *object) dateValue() dateObject { value, _ := o.value.(dateObject) return value } func dateObjectOf(rt *runtime, date *object) dateObject { if date == nil { panic(rt.panicTypeError("Date.ObjectOf is nil")) } if date.class != classDateName { panic(rt.panicTypeError("Date.ObjectOf %q != %q", date.class, classDateName)) } return date.dateValue() } // JavaScript is 0-based, Go is 1-based (15.9.1.4). func dateToGoMonth(month int) Time.Month { return Time.Month(month + 1) } func dateFromGoMonth(month Time.Month) int { return int(month) - 1 } func dateFromGoDay(day Time.Weekday) int { return int(day) } // newDateTime returns the epoch of date contained in argumentList for location. func newDateTime(argumentList []Value, location *Time.Location) float64 { pick := func(index int, default_ float64) (float64, bool) { if index >= len(argumentList) { return default_, false } value := argumentList[index].float64() if math.IsNaN(value) || math.IsInf(value, 0) { return 0, true } return value, false } switch len(argumentList) { case 0: // 0-argument time := Time.Now().In(utcTimeZone) return timeToEpoch(time) case 1: // 1-argument value := valueOfArrayIndex(argumentList, 0) value = toPrimitiveValue(value) if value.IsString() { return dateParse(value.string()) } return value.float64() default: // 2-argument, 3-argument, ... var year, month, day, hour, minute, second, millisecond float64 var invalid bool if year, invalid = pick(0, 1900.0); invalid { return math.NaN() } if month, invalid = pick(1, 0.0); invalid { return math.NaN() } if day, invalid = pick(2, 1.0); invalid { return math.NaN() } if hour, invalid = pick(3, 0.0); invalid { return math.NaN() } if minute, invalid = pick(4, 0.0); invalid { return math.NaN() } if second, invalid = pick(5, 0.0); invalid { return math.NaN() } if millisecond, invalid = pick(6, 0.0); invalid { return math.NaN() } if year >= 0 && year <= 99 { year += 1900 } time := Time.Date(int(year), dateToGoMonth(int(month)), int(day), int(hour), int(minute), int(second), int(millisecond)*1000*1000, location) return timeToEpoch(time) } } var ( dateLayoutList = []string{ "2006", "2006-01", "2006-01-02", "2006T15:04", "2006-01T15:04", "2006-01-02T15:04", "2006T15:04:05", "2006-01T15:04:05", "2006-01-02T15:04:05", "2006/01", "2006/01/02", "2006/01/02 15:04:05", "2006T15:04:05.000", "2006-01T15:04:05.000", "2006-01-02T15:04:05.000", "2006T15:04-0700", "2006-01T15:04-0700", "2006-01-02T15:04-0700", "2006T15:04:05-0700", "2006-01T15:04:05-0700", "2006-01-02T15:04:05-0700", "2006T15:04:05.000-0700", "2006-01T15:04:05.000-0700", "2006-01-02T15:04:05.000-0700", Time.RFC1123, } matchDateTimeZone = regexp.MustCompile(`^(.*)(?:(Z)|([\+\-]\d{2}):(\d{2}))$`) ) // dateParse returns the epoch of the parsed date. func dateParse(date string) float64 { // YYYY-MM-DDTHH:mm:ss.sssZ var time Time.Time var err error if match := matchDateTimeZone.FindStringSubmatch(date); match != nil { if match[2] == "Z" { date = match[1] + "+0000" } else { date = match[1] + match[3] + match[4] } } for _, layout := range dateLayoutList { time, err = Time.Parse(layout, date) if err == nil { break } } if err != nil { return math.NaN() } return float64(time.UnixMilli()) } ================================================ FILE: type_error.go ================================================ package otto func (rt *runtime) newErrorObject(name string, message Value, stackFramesToPop int) *object { obj := rt.newClassObject(classErrorName) if message.IsDefined() { err := newError(rt, name, stackFramesToPop, "%s", message.string()) obj.defineProperty("message", err.messageValue(), 0o111, false) obj.value = err } else { obj.value = newError(rt, name, stackFramesToPop) } obj.defineOwnProperty("stack", property{ value: propertyGetSet{ rt.newNativeFunction("get", "internal", 0, func(FunctionCall) Value { return stringValue(obj.value.(ottoError).formatWithStack()) }), &nilGetSetObject, }, mode: modeConfigureMask & modeOnMask, }, false) return obj } func (rt *runtime) newErrorObjectError(err ottoError) *object { obj := rt.newClassObject(classErrorName) obj.defineProperty("message", err.messageValue(), 0o111, false) obj.value = err switch err.name { case "EvalError": obj.prototype = rt.global.EvalErrorPrototype case "TypeError": obj.prototype = rt.global.TypeErrorPrototype case "RangeError": obj.prototype = rt.global.RangeErrorPrototype case "ReferenceError": obj.prototype = rt.global.ReferenceErrorPrototype case "SyntaxError": obj.prototype = rt.global.SyntaxErrorPrototype case "URIError": obj.prototype = rt.global.URIErrorPrototype default: obj.prototype = rt.global.ErrorPrototype } obj.defineOwnProperty("stack", property{ value: propertyGetSet{ rt.newNativeFunction("get", "internal", 0, func(FunctionCall) Value { return stringValue(obj.value.(ottoError).formatWithStack()) }), &nilGetSetObject, }, mode: modeConfigureMask & modeOnMask, }, false) return obj } ================================================ FILE: type_function.go ================================================ package otto // constructFunction. type constructFunction func(*object, []Value) Value // 13.2.2 [[Construct]]. func defaultConstruct(fn *object, argumentList []Value) Value { obj := fn.runtime.newObject() obj.class = classObjectName prototype := fn.get("prototype") if prototype.kind != valueObject { prototype = objectValue(fn.runtime.global.ObjectPrototype) } obj.prototype = prototype.object() this := objectValue(obj) value := fn.call(this, argumentList, false, nativeFrame) if value.kind == valueObject { return value } return this } // nativeFunction. type nativeFunction func(FunctionCall) Value // nativeFunctionObject. type nativeFunctionObject struct { call nativeFunction construct constructFunction name string file string line int } func (rt *runtime) newNativeFunctionProperty(name, file string, line int, native nativeFunction, length int) *object { o := rt.newClassObject(classFunctionName) o.value = nativeFunctionObject{ name: name, file: file, line: line, call: native, construct: defaultConstruct, } o.defineProperty("name", stringValue(name), 0o000, false) o.defineProperty(propertyLength, intValue(length), 0o000, false) return o } func (rt *runtime) newNativeFunctionObject(name, file string, line int, native nativeFunction, length int) *object { o := rt.newNativeFunctionProperty(name, file, line, native, length) o.defineOwnProperty("caller", property{ value: propertyGetSet{ rt.newNativeFunctionProperty("get", "internal", 0, func(fc FunctionCall) Value { for sc := rt.scope; sc != nil; sc = sc.outer { if sc.frame.fn == o { if sc.outer == nil || sc.outer.frame.fn == nil { return nullValue } return rt.toValue(sc.outer.frame.fn) } } return nullValue }, 0), &nilGetSetObject, }, mode: 0o000, }, false) return o } // bindFunctionObject. type bindFunctionObject struct { target *object this Value argumentList []Value } func (rt *runtime) newBoundFunctionObject(target *object, this Value, argumentList []Value) *object { o := rt.newClassObject(classFunctionName) o.value = bindFunctionObject{ target: target, this: this, argumentList: argumentList, } length := int(toInt32(target.get(propertyLength))) length -= len(argumentList) if length < 0 { length = 0 } o.defineProperty("name", stringValue("bound "+target.get("name").String()), 0o000, false) o.defineProperty(propertyLength, intValue(length), 0o000, false) o.defineProperty("caller", Value{}, 0o000, false) // TODO Should throw a TypeError o.defineProperty("arguments", Value{}, 0o000, false) // TODO Should throw a TypeError return o } // [[Construct]]. func (fn bindFunctionObject) construct(argumentList []Value) Value { obj := fn.target switch value := obj.value.(type) { case nativeFunctionObject: return value.construct(obj, fn.argumentList) case nodeFunctionObject: argumentList = append(fn.argumentList, argumentList...) return obj.construct(argumentList) default: panic(fn.target.runtime.panicTypeError("construct unknown type %T", obj.value)) } } // nodeFunctionObject. type nodeFunctionObject struct { node *nodeFunctionLiteral stash stasher } func (rt *runtime) newNodeFunctionObject(node *nodeFunctionLiteral, stash stasher) *object { o := rt.newClassObject(classFunctionName) o.value = nodeFunctionObject{ node: node, stash: stash, } o.defineProperty("name", stringValue(node.name), 0o000, false) o.defineProperty(propertyLength, intValue(len(node.parameterList)), 0o000, false) o.defineOwnProperty("caller", property{ value: propertyGetSet{ rt.newNativeFunction("get", "internal", 0, func(fc FunctionCall) Value { for sc := rt.scope; sc != nil; sc = sc.outer { if sc.frame.fn == o { if sc.outer == nil || sc.outer.frame.fn == nil { return nullValue } return rt.toValue(sc.outer.frame.fn) } } return nullValue }), &nilGetSetObject, }, mode: 0o000, }, false) return o } // _object. func (o *object) isCall() bool { switch fn := o.value.(type) { case nativeFunctionObject: return fn.call != nil case bindFunctionObject: return true case nodeFunctionObject: return true default: return false } } func (o *object) call(this Value, argumentList []Value, eval bool, frm frame) Value { //nolint:unparam // Isn't currently used except in recursive self. switch fn := o.value.(type) { case nativeFunctionObject: // Since eval is a native function, we only have to check for it here if eval { eval = o == o.runtime.eval // If eval is true, then it IS a direct eval } // Enter a scope, name from the native object... rt := o.runtime if rt.scope != nil && !eval { rt.enterFunctionScope(rt.scope.lexical, this) rt.scope.frame = frame{ native: true, nativeFile: fn.file, nativeLine: fn.line, callee: fn.name, file: nil, fn: o, } defer func() { rt.leaveScope() }() } return fn.call(FunctionCall{ runtime: o.runtime, eval: eval, This: this, ArgumentList: argumentList, Otto: o.runtime.otto, }) case bindFunctionObject: // TODO Passthrough site, do not enter a scope argumentList = append(fn.argumentList, argumentList...) return fn.target.call(fn.this, argumentList, false, frm) case nodeFunctionObject: rt := o.runtime stash := rt.enterFunctionScope(fn.stash, this) rt.scope.frame = frame{ callee: fn.node.name, file: fn.node.file, fn: o, } defer func() { rt.leaveScope() }() callValue := rt.cmplCallNodeFunction(o, stash, fn.node, argumentList) if value, valid := callValue.value.(result); valid { return value.value } return callValue } panic(o.runtime.panicTypeError("%v is not a function", objectValue(o))) } func (o *object) construct(argumentList []Value) Value { switch fn := o.value.(type) { case nativeFunctionObject: if fn.call == nil { panic(o.runtime.panicTypeError("%v is not a function", objectValue(o))) } if fn.construct == nil { panic(o.runtime.panicTypeError("%v is not a constructor", objectValue(o))) } return fn.construct(o, argumentList) case bindFunctionObject: return fn.construct(argumentList) case nodeFunctionObject: return defaultConstruct(o, argumentList) } panic(o.runtime.panicTypeError("%v is not a function", objectValue(o))) } // 15.3.5.3. func (o *object) hasInstance(of Value) bool { if !o.isCall() { // We should not have a hasInstance method panic(o.runtime.panicTypeError("Object.hasInstance not callable")) } if !of.IsObject() { return false } prototype := o.get("prototype") if !prototype.IsObject() { panic(o.runtime.panicTypeError("Object.hasInstance prototype %q is not an object", prototype)) } prototypeObject := prototype.object() value := of.object().prototype for value != nil { if value == prototypeObject { return true } value = value.prototype } return false } // FunctionCall is an encapsulation of a JavaScript function call. type FunctionCall struct { This Value runtime *runtime thisObj *object Otto *Otto ArgumentList []Value eval bool } // Argument will return the value of the argument at the given index. // // If no such argument exists, undefined is returned. func (f FunctionCall) Argument(index int) Value { return valueOfArrayIndex(f.ArgumentList, index) } func (f FunctionCall) getArgument(index int) (Value, bool) { return getValueOfArrayIndex(f.ArgumentList, index) } func (f FunctionCall) slice(index int) []Value { if index < len(f.ArgumentList) { return f.ArgumentList[index:] } return []Value{} } func (f *FunctionCall) thisObject() *object { if f.thisObj == nil { this := f.This.resolve() // FIXME Is this right? f.thisObj = f.runtime.toObject(this) } return f.thisObj } func (f *FunctionCall) thisClassObject(class string) *object { if o := f.thisObject(); o.class != class { panic(f.runtime.panicTypeError("Function.Class %s != %s", o.class, class)) } return f.thisObj } func (f FunctionCall) toObject(value Value) *object { return f.runtime.toObject(value) } // CallerLocation will return file location information (file:line:pos) where this function is being called. func (f FunctionCall) CallerLocation() string { // see error.go for location() return f.runtime.scope.outer.frame.location() } ================================================ FILE: type_go_array.go ================================================ package otto import ( "reflect" "strconv" ) func (rt *runtime) newGoArrayObject(value reflect.Value) *object { o := rt.newObject() o.class = classGoArrayName o.objectClass = classGoArray o.value = newGoArrayObject(value) return o } type goArrayObject struct { value reflect.Value writable bool propertyMode propertyMode } func newGoArrayObject(value reflect.Value) *goArrayObject { writable := value.Kind() == reflect.Ptr || value.CanSet() // The Array is addressable (like a Slice) mode := propertyMode(0o010) if writable { mode = 0o110 } return &goArrayObject{ value: value, writable: writable, propertyMode: mode, } } func (o goArrayObject) getValue(name string) (reflect.Value, bool) { //nolint:unused if index, err := strconv.ParseInt(name, 10, 64); err != nil { v, ok := o.getValueIndex(index) if ok { return v, ok } } if m := o.value.MethodByName(name); m.IsValid() { return m, true } return reflect.Value{}, false } func (o goArrayObject) getValueIndex(index int64) (reflect.Value, bool) { value := reflect.Indirect(o.value) if index < int64(value.Len()) { return value.Index(int(index)), true } return reflect.Value{}, false } func (o goArrayObject) setValue(index int64, value Value) bool { indexValue, exists := o.getValueIndex(index) if !exists { return false } reflectValue, err := value.toReflectValue(reflect.Indirect(o.value).Type().Elem()) if err != nil { panic(err) } indexValue.Set(reflectValue) return true } func goArrayGetOwnProperty(obj *object, name string) *property { // length if name == propertyLength { return &property{ value: toValue(reflect.Indirect(obj.value.(*goArrayObject).value).Len()), mode: 0, } } // .0, .1, .2, ... if index := stringToArrayIndex(name); index >= 0 { goObj := obj.value.(*goArrayObject) value := Value{} reflectValue, exists := goObj.getValueIndex(index) if exists { value = obj.runtime.toValue(reflectValue.Interface()) } return &property{ value: value, mode: goObj.propertyMode, } } if method := obj.value.(*goArrayObject).value.MethodByName(name); method.IsValid() { return &property{ obj.runtime.toValue(method.Interface()), 0o110, } } return objectGetOwnProperty(obj, name) } func goArrayEnumerate(obj *object, all bool, each func(string) bool) { goObj := obj.value.(*goArrayObject) // .0, .1, .2, ... for index, length := 0, goObj.value.Len(); index < length; index++ { name := strconv.FormatInt(int64(index), 10) if !each(name) { return } } objectEnumerate(obj, all, each) } func goArrayDefineOwnProperty(obj *object, name string, descriptor property, throw bool) bool { if name == propertyLength { return obj.runtime.typeErrorResult(throw) } else if index := stringToArrayIndex(name); index >= 0 { goObj := obj.value.(*goArrayObject) if goObj.writable { if obj.value.(*goArrayObject).setValue(index, descriptor.value.(Value)) { return true } } return obj.runtime.typeErrorResult(throw) } return objectDefineOwnProperty(obj, name, descriptor, throw) } func goArrayDelete(obj *object, name string, throw bool) bool { // length if name == propertyLength { return obj.runtime.typeErrorResult(throw) } // .0, .1, .2, ... index := stringToArrayIndex(name) if index >= 0 { goObj := obj.value.(*goArrayObject) if goObj.writable { indexValue, exists := goObj.getValueIndex(index) if exists { indexValue.Set(reflect.Zero(reflect.Indirect(goObj.value).Type().Elem())) return true } } return obj.runtime.typeErrorResult(throw) } return obj.delete(name, throw) } ================================================ FILE: type_go_map.go ================================================ package otto import ( "reflect" ) func (rt *runtime) newGoMapObject(value reflect.Value) *object { obj := rt.newObject() obj.class = classObjectName // TODO Should this be something else? obj.objectClass = classGoMap obj.value = newGoMapObject(value) return obj } type goMapObject struct { keyType reflect.Type valueType reflect.Type value reflect.Value } func newGoMapObject(value reflect.Value) *goMapObject { if value.Kind() != reflect.Map { dbgf("%/panic//%@: %v != reflect.Map", value.Kind()) } return &goMapObject{ value: value, keyType: value.Type().Key(), valueType: value.Type().Elem(), } } func (o goMapObject) toKey(name string) reflect.Value { reflectValue, err := stringToReflectValue(name, o.keyType.Kind()) if err != nil { panic(err) } return reflectValue } func (o goMapObject) toValue(value Value) reflect.Value { reflectValue, err := value.toReflectValue(o.valueType) if err != nil { panic(err) } return reflectValue } func goMapGetOwnProperty(obj *object, name string) *property { goObj := obj.value.(*goMapObject) // an error here means that the key referenced by `name` could not possibly // be a property of this object, so it should be safe to ignore this error // // TODO: figure out if any cases from // https://go.dev/ref/spec#Comparison_operators meet the criteria of 1) // being possible to represent as a string, 2) being possible to reconstruct // from a string, and 3) having a meaningful failure case in this context // other than "key does not exist" key, err := stringToReflectValue(name, goObj.keyType.Kind()) if err != nil { return nil } value := goObj.value.MapIndex(key) if value.IsValid() { return &property{obj.runtime.toValue(value.Interface()), 0o111} } // Other methods if method := obj.value.(*goMapObject).value.MethodByName(name); method.IsValid() { return &property{ value: obj.runtime.toValue(method.Interface()), mode: 0o110, } } return nil } func goMapEnumerate(obj *object, all bool, each func(string) bool) { goObj := obj.value.(*goMapObject) keys := goObj.value.MapKeys() for _, key := range keys { if !each(toValue(key).String()) { return } } } func goMapDefineOwnProperty(obj *object, name string, descriptor property, throw bool) bool { goObj := obj.value.(*goMapObject) // TODO ...or 0222 if descriptor.mode != 0o111 { return obj.runtime.typeErrorResult(throw) } if !descriptor.isDataDescriptor() { return obj.runtime.typeErrorResult(throw) } goObj.value.SetMapIndex(goObj.toKey(name), goObj.toValue(descriptor.value.(Value))) return true } func goMapDelete(obj *object, name string, throw bool) bool { goObj := obj.value.(*goMapObject) goObj.value.SetMapIndex(goObj.toKey(name), reflect.Value{}) // FIXME return true } ================================================ FILE: type_go_map_test.go ================================================ package otto import ( "sort" "strconv" "testing" ) type GoMapTest map[string]int func (s GoMapTest) Join() string { joinedStr := "" // Ordering the map takes some effort // because map iterators in golang are unordered by definition. // So we need to extract keys, sort them, and then generate K/V pairs // All of this is meant to ensure that the test is predictable. keys := make([]string, len(s)) i := 0 for key := range s { keys[i] = key i++ } sort.Strings(keys) for _, key := range keys { joinedStr += key + ": " + strconv.Itoa(s[key]) + " " } return joinedStr } func TestGoMap(t *testing.T) { tt(t, func() { test, vm := test() vm.Set("TestMap", GoMapTest{"one": 1, "two": 2, "three": 3}) is(test(`TestMap["one"]`).export(), 1) is(test(`TestMap.Join()`).export(), "one: 1 three: 3 two: 2 ") }) } ================================================ FILE: type_go_slice.go ================================================ package otto import ( "reflect" "strconv" ) func (rt *runtime) newGoSliceObject(value reflect.Value) *object { o := rt.newObject() o.class = classGoSliceName o.objectClass = classGoSlice o.value = newGoSliceObject(value) return o } type goSliceObject struct { value reflect.Value } func newGoSliceObject(value reflect.Value) *goSliceObject { return &goSliceObject{ value: value, } } func (o goSliceObject) getValue(index int64) (reflect.Value, bool) { if index < int64(o.value.Len()) { return o.value.Index(int(index)), true } return reflect.Value{}, false } func (o *goSliceObject) setLength(value Value) { want, err := value.ToInteger() if err != nil { panic(err) } wantInt := int(want) switch { case wantInt == o.value.Len(): // No change needed. case wantInt < o.value.Cap(): // Fits in current capacity. o.value.SetLen(wantInt) default: // Needs expanding. newSlice := reflect.MakeSlice(o.value.Type(), wantInt, wantInt) reflect.Copy(newSlice, o.value) o.value = newSlice } } func (o *goSliceObject) setValue(index int64, value Value) bool { reflectValue, err := value.toReflectValue(o.value.Type().Elem()) if err != nil { panic(err) } indexValue, exists := o.getValue(index) if !exists { if int64(o.value.Len()) == index { // Trying to append e.g. slice.push(...), allow it. o.value = reflect.Append(o.value, reflectValue) return true } return false } indexValue.Set(reflectValue) return true } func goSliceGetOwnProperty(obj *object, name string) *property { // length if name == propertyLength { return &property{ value: toValue(obj.value.(*goSliceObject).value.Len()), mode: 0o110, } } // .0, .1, .2, ... if index := stringToArrayIndex(name); index >= 0 { value := Value{} reflectValue, exists := obj.value.(*goSliceObject).getValue(index) if exists { value = obj.runtime.toValue(reflectValue.Interface()) } return &property{ value: value, mode: 0o110, } } // Other methods if method := obj.value.(*goSliceObject).value.MethodByName(name); method.IsValid() { return &property{ value: obj.runtime.toValue(method.Interface()), mode: 0o110, } } return objectGetOwnProperty(obj, name) } func goSliceEnumerate(obj *object, all bool, each func(string) bool) { goObj := obj.value.(*goSliceObject) // .0, .1, .2, ... for index, length := 0, goObj.value.Len(); index < length; index++ { name := strconv.FormatInt(int64(index), 10) if !each(name) { return } } objectEnumerate(obj, all, each) } func goSliceDefineOwnProperty(obj *object, name string, descriptor property, throw bool) bool { if name == propertyLength { obj.value.(*goSliceObject).setLength(descriptor.value.(Value)) return true } else if index := stringToArrayIndex(name); index >= 0 { if obj.value.(*goSliceObject).setValue(index, descriptor.value.(Value)) { return true } return obj.runtime.typeErrorResult(throw) } return objectDefineOwnProperty(obj, name, descriptor, throw) } func goSliceDelete(obj *object, name string, throw bool) bool { // length if name == propertyLength { return obj.runtime.typeErrorResult(throw) } // .0, .1, .2, ... index := stringToArrayIndex(name) if index >= 0 { goObj := obj.value.(*goSliceObject) indexValue, exists := goObj.getValue(index) if exists { indexValue.Set(reflect.Zero(goObj.value.Type().Elem())) return true } return obj.runtime.typeErrorResult(throw) } return obj.delete(name, throw) } ================================================ FILE: type_go_slice_test.go ================================================ package otto import "testing" type GoSliceTest []int func (s GoSliceTest) Sum() int { sum := 0 for _, v := range s { sum += v } return sum } func TestGoSlice(t *testing.T) { tt(t, func() { test, vm := test() vm.Set("TestSlice", GoSliceTest{1, 2, 3}) is(test(`TestSlice.length`).export(), 3) is(test(`TestSlice[1]`).export(), 2) is(test(`TestSlice.Sum()`).export(), 6) }) } ================================================ FILE: type_go_struct.go ================================================ package otto import ( "encoding/json" "reflect" ) // FIXME Make a note about not being able to modify a struct unless it was // passed as a pointer-to: &struct{ ... } // This seems to be a limitation of the reflect package. // This goes for the other Go constructs too. // I guess we could get around it by either: // 1. Creating a new struct every time // 2. Creating an addressable? struct in the constructor func (rt *runtime) newGoStructObject(value reflect.Value) *object { o := rt.newObject() o.class = classObjectName // TODO Should this be something else? o.objectClass = classGoStruct o.value = newGoStructObject(value) return o } type goStructObject struct { value reflect.Value } func newGoStructObject(value reflect.Value) *goStructObject { if reflect.Indirect(value).Kind() != reflect.Struct { dbgf("%/panic//%@: %v != reflect.Struct", value.Kind()) } return &goStructObject{ value: value, } } func (o goStructObject) getValue(name string) reflect.Value { if idx := fieldIndexByName(reflect.Indirect(o.value).Type(), name); len(idx) > 0 { return reflect.Indirect(o.value).FieldByIndex(idx) } if validGoStructName(name) { // Do not reveal hidden or unexported fields. if field := reflect.Indirect(o.value).FieldByName(name); field.IsValid() { return field } if method := o.value.MethodByName(name); method.IsValid() { return method } } return reflect.Value{} } func (o goStructObject) fieldIndex(name string) []int { //nolint:unused return fieldIndexByName(reflect.Indirect(o.value).Type(), name) } func (o goStructObject) method(name string) (reflect.Method, bool) { //nolint:unused return reflect.Indirect(o.value).Type().MethodByName(name) } func (o goStructObject) setValue(rt *runtime, name string, value Value) bool { if idx := fieldIndexByName(reflect.Indirect(o.value).Type(), name); len(idx) == 0 { return false } fieldValue := o.getValue(name) converted, err := rt.convertCallParameter(value, fieldValue.Type()) if err != nil { panic(rt.panicTypeError("Object.setValue convertCallParameter: %s", err)) } fieldValue.Set(converted) return true } func goStructGetOwnProperty(obj *object, name string) *property { goObj := obj.value.(*goStructObject) value := goObj.getValue(name) if value.IsValid() { return &property{obj.runtime.toValue(value), 0o110} } return objectGetOwnProperty(obj, name) } func validGoStructName(name string) bool { if name == "" { return false } return 'A' <= name[0] && name[0] <= 'Z' // TODO What about Unicode? } func goStructEnumerate(obj *object, all bool, each func(string) bool) { goObj := obj.value.(*goStructObject) // Enumerate fields for index := range reflect.Indirect(goObj.value).NumField() { name := reflect.Indirect(goObj.value).Type().Field(index).Name if validGoStructName(name) { if !each(name) { return } } } // Enumerate methods for index := range goObj.value.NumMethod() { name := goObj.value.Type().Method(index).Name if validGoStructName(name) { if !each(name) { return } } } objectEnumerate(obj, all, each) } func goStructCanPut(obj *object, name string) bool { goObj := obj.value.(*goStructObject) value := goObj.getValue(name) if value.IsValid() { return true } return objectCanPut(obj, name) } func goStructPut(obj *object, name string, value Value, throw bool) { goObj := obj.value.(*goStructObject) if goObj.setValue(obj.runtime, name, value) { return } objectPut(obj, name, value, throw) } func goStructMarshalJSON(obj *object) json.Marshaler { goObj := obj.value.(*goStructObject) goValue := reflect.Indirect(goObj.value).Interface() marshaler, _ := goValue.(json.Marshaler) return marshaler } ================================================ FILE: type_go_struct_test.go ================================================ package otto import ( "testing" ) func TestGoStructEmbeddedFields(t *testing.T) { type A struct { A1 string `json:"a1"` A2 string `json:"a2"` A3 string `json:"a3"` } type B struct { A B1 string `json:"b1"` } tt(t, func() { test, vm := test() vm.Set("v", B{A{"a1", "a2", "a3"}, "b1"}) test(`[v.a1,v.a2,v.a3,v.b1]`, "a1,a2,a3,b1") }) } func TestGoStructNilBoolPointerField(t *testing.T) { type S struct { C interface{} `json:"c"` B *bool `json:"b"` A int `json:"a"` } tt(t, func() { test, vm := test() vm.Set("s", S{A: 1, B: nil, C: nil}) test(`'a' in s`, true) test(`typeof s.a`, "number") test(`'b' in s`, true) test(`typeof s.b`, "undefined") test(`'c' in s`, true) test(`typeof s.c`, "undefined") }) } func TestGoStructError(t *testing.T) { type S1 struct { A string `json:"a"` B string `json:"b"` } type S2 struct { B S1 `json:"b"` A []S1 `json:"a"` } type S3 struct { B S2 `json:"b"` A []S2 `json:"a"` } tt(t, func() { test, vm := test() vm.Set("fn", func(s *S3) string { return "cool" }) test( `(function() { try { fn({a:[{a:[{c:"x"}]}]}) } catch (ex) { return ex } })()`, `TypeError: can't convert to *otto.S3: couldn't convert property "a" of otto.S3: couldn't convert element 0 of []otto.S2: couldn't convert property "a" of otto.S2: couldn't convert element 0 of []otto.S1: can't convert property "c" of otto.S1: field does not exist`, ) }) } ================================================ FILE: type_number.go ================================================ package otto func (rt *runtime) newNumberObject(value Value) *object { return rt.newPrimitiveObject(classNumberName, value.numberValue()) } ================================================ FILE: type_reference.go ================================================ package otto type referencer interface { invalid() bool // IsUnresolvableReference getValue() Value // getValue putValue(value Value) string // PutValue delete() bool } // PropertyReference type propertyReference struct { base *object runtime *runtime name string at at strict bool } func newPropertyReference(rt *runtime, base *object, name string, strict bool, atv at) *propertyReference { return &propertyReference{ runtime: rt, name: name, strict: strict, base: base, at: atv, } } func (pr *propertyReference) invalid() bool { return pr.base == nil } func (pr *propertyReference) getValue() Value { if pr.base == nil { panic(pr.runtime.panicReferenceError("'%s' is not defined", pr.name, pr.at)) } return pr.base.get(pr.name) } func (pr *propertyReference) putValue(value Value) string { if pr.base == nil { return pr.name } pr.base.put(pr.name, value, pr.strict) return "" } func (pr *propertyReference) delete() bool { if pr.base == nil { // TODO Throw an error if strict return true } return pr.base.delete(pr.name, pr.strict) } type stashReference struct { base stasher name string strict bool } func (sr *stashReference) invalid() bool { return false // The base (an environment) will never be nil } func (sr *stashReference) getValue() Value { return sr.base.getBinding(sr.name, sr.strict) } func (sr *stashReference) putValue(value Value) string { sr.base.setValue(sr.name, value, sr.strict) return "" } func (sr *stashReference) delete() bool { if sr.base == nil { // This should never be reached, but just in case return false } return sr.base.deleteBinding(sr.name) } // getIdentifierReference. func getIdentifierReference(rt *runtime, stash stasher, name string, strict bool, atv at) referencer { if stash == nil { return newPropertyReference(rt, nil, name, strict, atv) } if stash.hasBinding(name) { return stash.newReference(name, strict, atv) } return getIdentifierReference(rt, stash.outer(), name, strict, atv) } ================================================ FILE: type_regexp.go ================================================ package otto import ( "fmt" "regexp" "github.com/robertkrimen/otto/parser" ) type regExpObject struct { regularExpression *regexp.Regexp source string flags string global bool ignoreCase bool multiline bool } func (rt *runtime) newRegExpObject(pattern string, flags string) *object { o := rt.newObject() o.class = classRegExpName global := false ignoreCase := false multiline := false re2flags := "" // TODO Maybe clean up the panicking here... TypeError, SyntaxError, ? for _, chr := range flags { switch chr { case 'g': if global { panic(rt.panicSyntaxError("newRegExpObject: %s %s", pattern, flags)) } global = true case 'm': if multiline { panic(rt.panicSyntaxError("newRegExpObject: %s %s", pattern, flags)) } multiline = true re2flags += "m" case 'i': if ignoreCase { panic(rt.panicSyntaxError("newRegExpObject: %s %s", pattern, flags)) } ignoreCase = true re2flags += "i" } } re2pattern, err := parser.TransformRegExp(pattern) if err != nil { panic(rt.panicTypeError("Invalid regular expression: %s", err.Error())) } if len(re2flags) > 0 { re2pattern = fmt.Sprintf("(?%s:%s)", re2flags, re2pattern) } regularExpression, err := regexp.Compile(re2pattern) if err != nil { panic(rt.panicSyntaxError("Invalid regular expression: %s", err.Error()[22:])) } o.value = regExpObject{ regularExpression: regularExpression, global: global, ignoreCase: ignoreCase, multiline: multiline, source: pattern, flags: flags, } o.defineProperty("global", boolValue(global), 0, false) o.defineProperty("ignoreCase", boolValue(ignoreCase), 0, false) o.defineProperty("multiline", boolValue(multiline), 0, false) o.defineProperty("lastIndex", intValue(0), 0o100, false) o.defineProperty("source", stringValue(pattern), 0, false) return o } func (o *object) regExpValue() regExpObject { value, _ := o.value.(regExpObject) return value } func execRegExp(this *object, target string) (bool, []int) { if this.class != classRegExpName { panic(this.runtime.panicTypeError("Calling RegExp.exec on a non-RegExp object")) } lastIndex := this.get("lastIndex").number().int64 index := lastIndex global := this.get("global").bool() if !global { index = 0 } var result []int if 0 > index || index > int64(len(target)) { } else { result = this.regExpValue().regularExpression.FindStringSubmatchIndex(target[index:]) } if result == nil { this.put("lastIndex", intValue(0), true) return false, nil } startIndex := index endIndex := int(lastIndex) + result[1] // We do this shift here because the .FindStringSubmatchIndex above // was done on a local subordinate slice of the string, not the whole string for index, offset := range result { if offset != -1 { result[index] += int(startIndex) } } if global { this.put("lastIndex", intValue(endIndex), true) } return true, result } func execResultToArray(rt *runtime, target string, result []int) *object { captureCount := len(result) / 2 valueArray := make([]Value, captureCount) for index := range captureCount { offset := 2 * index if result[offset] != -1 { valueArray[index] = stringValue(target[result[offset]:result[offset+1]]) } else { valueArray[index] = Value{} } } matchIndex := result[0] if matchIndex != 0 { // Find the utf16 index in the string, not the byte index. matchIndex = utf16Length(target[:matchIndex]) } match := rt.newArrayOf(valueArray) match.defineProperty("input", stringValue(target), 0o111, false) match.defineProperty("index", intValue(matchIndex), 0o111, false) return match } ================================================ FILE: type_string.go ================================================ package otto import ( "strconv" "unicode/utf16" "unicode/utf8" ) type stringObjecter interface { Length() int At(at int) rune String() string } type stringASCII string func (str stringASCII) Length() int { return len(str) } func (str stringASCII) At(at int) rune { return rune(str[at]) } func (str stringASCII) String() string { return string(str) } type stringWide struct { string string value16 []uint16 } func (str stringWide) Length() int { if str.value16 == nil { str.value16 = utf16.Encode([]rune(str.string)) } return len(str.value16) } func (str stringWide) At(at int) rune { if str.value16 == nil { str.value16 = utf16.Encode([]rune(str.string)) } return rune(str.value16[at]) } func (str stringWide) String() string { return str.string } func newStringObject(str string) stringObjecter { for i := range len(str) { if str[i] >= utf8.RuneSelf { goto wide } } return stringASCII(str) wide: return &stringWide{ string: str, } } func stringAt(str stringObjecter, index int) rune { if 0 <= index && index < str.Length() { return str.At(index) } return utf8.RuneError } func (rt *runtime) newStringObject(value Value) *object { str := newStringObject(value.string()) obj := rt.newClassObject(classStringName) obj.defineProperty(propertyLength, intValue(str.Length()), 0, false) obj.objectClass = classString obj.value = str return obj } func (o *object) stringValue() stringObjecter { if str, ok := o.value.(stringObjecter); ok { return str } return nil } func stringEnumerate(obj *object, all bool, each func(string) bool) { if str := obj.stringValue(); str != nil { length := str.Length() for index := range length { if !each(strconv.FormatInt(int64(index), 10)) { return } } } objectEnumerate(obj, all, each) } func stringGetOwnProperty(obj *object, name string) *property { if prop := objectGetOwnProperty(obj, name); prop != nil { return prop } // TODO Test a string of length >= +int32 + 1? if index := stringToArrayIndex(name); index >= 0 { if chr := stringAt(obj.stringValue(), int(index)); chr != utf8.RuneError { return &property{stringValue(string(chr)), 0} } } return nil } ================================================ FILE: underscore/LICENSE.underscorejs ================================================ Copyright (c) 2009-2022 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: underscore/README.md ================================================ # underscore [![Reference](https://pkg.go.dev/badge/github.com/robertkrimen/otto/underscore.svg)](https://pkg.go.dev/github.com/robertkrimen/otto/underscore) [![License](https://img.shields.io/badge/MIT-blue.svg)](https://opensource.org/licenses/MIT) To update the version of underscore run: ```shell go generate ``` ================================================ FILE: underscore/download.go ================================================ //go:build generate package main import ( "context" "flag" "fmt" "io" "log" "net/http" "os" "time" ) var ( url = flag.String("url", "", "url to read from") output = flag.String("output", "", "output file to write the result too") ) func download(url, output string) (err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return fmt.Errorf("new request failed: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() var f *os.File if output != "" { if f, err = os.Create(output); err != nil { return fmt.Errorf("create file %q failed: %w", output, err) } defer f.Close() } else { f = os.Stdout } if _, err := io.Copy(f, resp.Body); err != nil { return fmt.Errorf("body save: %w", err) } return nil } func main() { flag.Parse() switch { case len(*url) == 0: log.Fatal("missing required --url parameter") } if err := download(*url, *output); err != nil { log.Fatal(err) } } ================================================ FILE: underscore/generate.go ================================================ package underscore //go:generate go run download.go --url https://underscorejs.org/underscore-min.js --output underscore-min.js //go:generate go run download.go --url https://raw.githubusercontent.com/jashkenas/underscore/master/LICENSE --output LICENSE.underscorejs ================================================ FILE: underscore/testify ================================================ #!/usr/bin/env perl use strict; use warnings; my $underscore_test = shift @ARGV || ""; if (!-d $underscore_test) { print <<_END_; Usage: testify ./underscore/test # Should look something like: arrays.js chaining.js collections.js functions.js index.html objects.js speed.js utility.js vendor _END_ if ($underscore_test) { die "!: Not a directory: $underscore_test\n" } exit; } chdir $underscore_test or die "!: $!"; my @js = <*.js>; for my $file (@js) { open my $fh, '<', $file or die "!: $!"; my $tests = join "", <$fh>; my @tests = $tests =~ m/ ^(\s{2}test\(.*? ^\s{2}}\);)$ /mgxs; close $fh; next unless @tests; print "$file: ", scalar(@tests), "\n"; my $underscore_name = "underscore_$file"; $underscore_name =~ s/.js$//; my $go_file = "${underscore_name}_test.go"; $go_file =~ s/.js$/.go/; open $fh, '>', $go_file or die "!: $!"; $fh->print(<<_END_); package otto import ( "testing" ) _END_ my $count = 0; for my $test (@tests) { $test =~ s/`([^`]+)`/<$1>/g; my ($name) = $test =~ m/^\s*test\(['"]([^'"]+)['"]/; $fh->print(<<_END_); // $name func Test_${underscore_name}_$count(t *testing.T) { tt(t, func(){ test := underscoreTest() test(` $test `) }) } _END_ $count++; } } # test('#779 - delimeters are applied to unescaped text.', 1, function() { # var template = _.template('<<\nx\n>>', null, {evaluate: /<<(.*?)>>/g}); # strictEqual(template(), '<<\nx\n>>'); # }); ================================================ FILE: underscore/underscore-min.js ================================================ !function(n,r){"object"==typeof exports&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define("underscore",r):(n="undefined"!=typeof globalThis?globalThis:n||self,function(){var t=n._,e=n._=r();e.noConflict=function(){return n._=t,e}}())}(this,(function(){ // Underscore.js 1.13.7 // https://underscorejs.org // (c) 2009-2024 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. var n="1.13.7",r="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global||Function("return this")()||{},t=Array.prototype,e=Object.prototype,u="undefined"!=typeof Symbol?Symbol.prototype:null,i=t.push,o=t.slice,a=e.toString,f=e.hasOwnProperty,c="undefined"!=typeof ArrayBuffer,l="undefined"!=typeof DataView,s=Array.isArray,p=Object.keys,v=Object.create,h=c&&ArrayBuffer.isView,y=isNaN,d=isFinite,g=!{toString:null}.propertyIsEnumerable("toString"),b=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"],m=Math.pow(2,53)-1;function j(n,r){return r=null==r?n.length-1:+r,function(){for(var t=Math.max(arguments.length-r,0),e=Array(t),u=0;u=0&&t<=m}}function J(n){return function(r){return null==r?void 0:r[n]}}var G=J("byteLength"),H=K(G),Q=/\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/;var X=c?function(n){return h?h(n)&&!q(n):H(n)&&Q.test(a.call(n))}:C(!1),Y=J("length");function Z(n,r){r=function(n){for(var r={},t=n.length,e=0;e":">",'"':""","'":"'","`":"`"},$n=zn(Ln),Cn=zn(wn(Ln)),Kn=tn.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g},Jn=/(.)^/,Gn={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},Hn=/\\|'|\r|\n|\u2028|\u2029/g;function Qn(n){return"\\"+Gn[n]}var Xn=/^\s*(\w|\$)+\s*$/;var Yn=0;function Zn(n,r,t,e,u){if(!(e instanceof r))return n.apply(t,u);var i=Mn(n.prototype),o=n.apply(i,u);return w(o)?o:i}var nr=j((function(n,r){var t=nr.placeholder,e=function(){for(var u=0,i=r.length,o=Array(i),a=0;a1)er(a,r-1,t,e),u=e.length;else for(var f=0,c=a.length;f0&&(t=r.apply(this,arguments)),n<=1&&(r=null),t}}var cr=nr(fr,2);function lr(n,r,t){r=Pn(r,t);for(var e,u=nn(n),i=0,o=u.length;i0?0:u-1;i>=0&&i0?a=i>=0?i:Math.max(i+f,a):f=i>=0?Math.min(i+1,f):i+f+1;else if(t&&i&&f)return e[i=t(e,u)]===u?i:-1;if(u!=u)return(i=r(o.call(e,a,f),$))>=0?i+a:-1;for(i=n>0?a:f-1;i>=0&&i0?0:o-1;for(u||(e=r[i?i[a]:a],a+=n);a>=0&&a=3;return r(n,Rn(t,u,4),e,i)}}var _r=wr(1),Ar=wr(-1);function xr(n,r,t){var e=[];return r=Pn(r,t),mr(n,(function(n,t,u){r(n,t,u)&&e.push(n)})),e}function Sr(n,r,t){r=Pn(r,t);for(var e=!tr(n)&&nn(n),u=(e||n).length,i=0;i=0}var Er=j((function(n,r,t){var e,u;return D(r)?u=r:(r=Bn(r),e=r.slice(0,-1),r=r[r.length-1]),jr(n,(function(n){var i=u;if(!i){if(e&&e.length&&(n=Nn(n,e)),null==n)return;i=n[r]}return null==i?i:i.apply(n,t)}))}));function Br(n,r){return jr(n,Dn(r))}function Nr(n,r,t){var e,u,i=-1/0,o=-1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var a=0,f=(n=tr(n)?n:jn(n)).length;ai&&(i=e);else r=Pn(r,t),mr(n,(function(n,t,e){((u=r(n,t,e))>o||u===-1/0&&i===-1/0)&&(i=n,o=u)}));return i}var Ir=/[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;function Tr(n){return n?U(n)?o.call(n):S(n)?n.match(Ir):tr(n)?jr(n,Tn):jn(n):[]}function kr(n,r,t){if(null==r||t)return tr(n)||(n=jn(n)),n[Un(n.length-1)];var e=Tr(n),u=Y(e);r=Math.max(Math.min(r,u),0);for(var i=u-1,o=0;o1&&(e=Rn(e,r[1])),r=an(n)):(e=qr,r=er(r,!1,!1),n=Object(n));for(var u=0,i=r.length;u1&&(t=r[1])):(r=jr(er(r,!1,!1),String),e=function(n,t){return!Mr(r,t)}),Ur(n,e,t)}));function zr(n,r,t){return o.call(n,0,Math.max(0,n.length-(null==r||t?1:r)))}function Lr(n,r,t){return null==n||n.length<1?null==r||t?void 0:[]:null==r||t?n[0]:zr(n,n.length-r)}function $r(n,r,t){return o.call(n,null==r||t?1:r)}var Cr=j((function(n,r){return r=er(r,!0,!0),xr(n,(function(n){return!Mr(r,n)}))})),Kr=j((function(n,r){return Cr(n,r)}));function Jr(n,r,t,e){A(r)||(e=t,t=r,r=!1),null!=t&&(t=Pn(t,e));for(var u=[],i=[],o=0,a=Y(n);or?(e&&(clearTimeout(e),e=null),a=c,o=n.apply(u,i),e||(u=i=null)):e||!1===t.trailing||(e=setTimeout(f,l)),o};return c.cancel=function(){clearTimeout(e),a=0,e=u=i=null},c},debounce:function(n,r,t){var e,u,i,o,a,f=function(){var c=Wn()-u;r>c?e=setTimeout(f,r-c):(e=null,t||(o=n.apply(a,i)),e||(i=a=null))},c=j((function(c){return a=this,i=c,u=Wn(),e||(e=setTimeout(f,r),t&&(o=n.apply(a,i))),o}));return c.cancel=function(){clearTimeout(e),e=i=a=null},c},wrap:function(n,r){return nr(r,n)},negate:ar,compose:function(){var n=arguments,r=n.length-1;return function(){for(var t=r,e=n[r].apply(this,arguments);t--;)e=n[t].call(this,e);return e}},after:function(n,r){return function(){if(--n<1)return r.apply(this,arguments)}},before:fr,once:cr,findKey:lr,findIndex:pr,findLastIndex:vr,sortedIndex:hr,indexOf:dr,lastIndexOf:gr,find:br,detect:br,findWhere:function(n,r){return br(n,kn(r))},each:mr,forEach:mr,map:jr,collect:jr,reduce:_r,foldl:_r,inject:_r,reduceRight:Ar,foldr:Ar,filter:xr,select:xr,reject:function(n,r,t){return xr(n,ar(Pn(r)),t)},every:Sr,all:Sr,some:Or,any:Or,contains:Mr,includes:Mr,include:Mr,invoke:Er,pluck:Br,where:function(n,r){return xr(n,kn(r))},max:Nr,min:function(n,r,t){var e,u,i=1/0,o=1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var a=0,f=(n=tr(n)?n:jn(n)).length;ae||void 0===t)return 1;if(t 2; }), 3, 'should return first found '); strictEqual(_.find(array, function() { return false; }), void 0, 'should return if is not found'); }); `) }) } // detect. func Test_underscore_collections_5(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('detect', function() { var result = _.detect([1, 2, 3], function(num){ return num * 2 == 4; }); equal(result, 2, 'found the first "2" and broke the loop'); }); `) }) } // select. func Test_underscore_collections_6(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('select', function() { var evens = _.select([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; }); equal(evens.join(', '), '2, 4, 6', 'selected each even number'); evens = _.filter([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; }); equal(evens.join(', '), '2, 4, 6', 'aliased as "filter"'); }); `) }) } // reject. func Test_underscore_collections_7(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('reject', function() { var odds = _.reject([1, 2, 3, 4, 5, 6], function(num){ return num % 2 == 0; }); equal(odds.join(', '), '1, 3, 5', 'rejected each even number'); var context = "obj"; var evens = _.reject([1, 2, 3, 4, 5, 6], function(num){ equal(context, "obj"); return num % 2 != 0; }, context); equal(evens.join(', '), '2, 4, 6', 'rejected each odd number'); }); `) }) } // all. func Test_underscore_collections_8(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('all', function() { ok(_.all([], _.identity), 'the empty set'); ok(_.all([true, true, true], _.identity), 'all true values'); ok(!_.all([true, false, true], _.identity), 'one false value'); ok(_.all([0, 10, 28], function(num){ return num % 2 == 0; }), 'even numbers'); ok(!_.all([0, 11, 28], function(num){ return num % 2 == 0; }), 'an odd number'); ok(_.all([1], _.identity) === true, 'cast to boolean - true'); ok(_.all([0], _.identity) === false, 'cast to boolean - false'); ok(_.every([true, true, true], _.identity), 'aliased as "every"'); ok(!_.all([undefined, undefined, undefined], _.identity), 'works with arrays of undefined'); }); `) }) } // any. func Test_underscore_collections_9(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('any', function() { var nativeSome = Array.prototype.some; Array.prototype.some = null; ok(!_.any([]), 'the empty set'); ok(!_.any([false, false, false]), 'all false values'); ok(_.any([false, false, true]), 'one true value'); ok(_.any([null, 0, 'yes', false]), 'a string'); ok(!_.any([null, 0, '', false]), 'falsy values'); ok(!_.any([1, 11, 29], function(num){ return num % 2 == 0; }), 'all odd numbers'); ok(_.any([1, 10, 29], function(num){ return num % 2 == 0; }), 'an even number'); ok(_.any([1], _.identity) === true, 'cast to boolean - true'); ok(_.any([0], _.identity) === false, 'cast to boolean - false'); ok(_.some([false, false, true]), 'aliased as "some"'); Array.prototype.some = nativeSome; }); `) }) } // include. func Test_underscore_collections_10(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('include', function() { ok(_.include([1,2,3], 2), 'two is in the array'); ok(!_.include([1,3,9], 2), 'two is not in the array'); ok(_.contains({moe:1, larry:3, curly:9}, 3) === true, '_.include on objects checks their values'); ok(_([1,2,3]).include(2), 'OO-style include'); }); `) }) } // invoke. func Test_underscore_collections_11(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('invoke', function() { var list = [[5, 1, 7], [3, 2, 1]]; var result = _.invoke(list, 'sort'); equal(result[0].join(', '), '1, 5, 7', 'first array sorted'); equal(result[1].join(', '), '1, 2, 3', 'second array sorted'); }); `) }) } // invoke w/ function reference. func Test_underscore_collections_12(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('invoke w/ function reference', function() { var list = [[5, 1, 7], [3, 2, 1]]; var result = _.invoke(list, Array.prototype.sort); equal(result[0].join(', '), '1, 5, 7', 'first array sorted'); equal(result[1].join(', '), '1, 2, 3', 'second array sorted'); }); `) }) } // invoke when strings have a call method. func Test_underscore_collections_13(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('invoke when strings have a call method', function() { String.prototype.call = function() { return 42; }; var list = [[5, 1, 7], [3, 2, 1]]; var s = "foo"; equal(s.call(), 42, "call function exists"); var result = _.invoke(list, 'sort'); equal(result[0].join(', '), '1, 5, 7', 'first array sorted'); equal(result[1].join(', '), '1, 2, 3', 'second array sorted'); delete String.prototype.call; equal(s.call, undefined, "call function removed"); }); `) }) } // pluck. func Test_underscore_collections_14(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('pluck', function() { var people = [{name : 'moe', age : 30}, {name : 'curly', age : 50}]; equal(_.pluck(people, 'name').join(', '), 'moe, curly', 'pulls names out of objects'); }); `) }) } // where. func Test_underscore_collections_15(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('where', function() { var list = [{a: 1, b: 2}, {a: 2, b: 2}, {a: 1, b: 3}, {a: 1, b: 4}]; var result = _.where(list, {a: 1}); equal(result.length, 3); equal(result[result.length - 1].b, 4); result = _.where(list, {b: 2}); equal(result.length, 2); equal(result[0].a, 1); }); `) }) } // findWhere. func Test_underscore_collections_16(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('findWhere', function() { var list = [{a: 1, b: 2}, {a: 2, b: 2}, {a: 1, b: 3}, {a: 1, b: 4}, {a: 2, b: 4}]; var result = _.findWhere(list, {a: 1}); deepEqual(result, {a: 1, b: 2}); result = _.findWhere(list, {b: 4}); deepEqual(result, {a: 1, b: 4}); }); `) }) } // max. func Test_underscore_collections_17(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('max', function() { equal(3, _.max([1, 2, 3]), 'can perform a regular Math.max'); var neg = _.max([1, 2, 3], function(num){ return -num; }); equal(neg, 1, 'can perform a computation-based max'); equal(-Infinity, _.max({}), 'Maximum value of an empty object'); equal(-Infinity, _.max([]), 'Maximum value of an empty array'); equal(_.max({'a': 'a'}), -Infinity, 'Maximum value of a non-numeric collection'); // TEST: Takes too long return; equal(299999, _.max(_.range(1,300000)), "Maximum value of a too-big array"); }); `) }) } // min. func Test_underscore_collections_18(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('min', function() { equal(1, _.min([1, 2, 3]), 'can perform a regular Math.min'); var neg = _.min([1, 2, 3], function(num){ return -num; }); equal(neg, 3, 'can perform a computation-based min'); equal(Infinity, _.min({}), 'Minimum value of an empty object'); equal(Infinity, _.min([]), 'Minimum value of an empty array'); equal(_.min({'a': 'a'}), Infinity, 'Minimum value of a non-numeric collection'); var now = new Date(9999999999); var then = new Date(0); equal(_.min([now, then]), then); // TEST: Takes too long return; equal(1, _.min(_.range(1,300000)), "Minimum value of a too-big array"); }); `) }) } // sortBy. func Test_underscore_collections_19(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('sortBy', function() { var people = [{name : 'curly', age : 50}, {name : 'moe', age : 30}]; people = _.sortBy(people, function(person){ return person.age; }); equal(_.pluck(people, 'name').join(', '), 'moe, curly', 'stooges sorted by age'); var list = [undefined, 4, 1, undefined, 3, 2]; equal(_.sortBy(list, _.identity).join(','), '1,2,3,4,,', 'sortBy with undefined values'); var list = ["one", "two", "three", "four", "five"]; var sorted = _.sortBy(list, 'length'); equal(sorted.join(' '), 'one two four five three', 'sorted by length'); function Pair(x, y) { this.x = x; this.y = y; } var collection = [ new Pair(1, 1), new Pair(1, 2), new Pair(1, 3), new Pair(1, 4), new Pair(1, 5), new Pair(1, 6), new Pair(2, 1), new Pair(2, 2), new Pair(2, 3), new Pair(2, 4), new Pair(2, 5), new Pair(2, 6), new Pair(undefined, 1), new Pair(undefined, 2), new Pair(undefined, 3), new Pair(undefined, 4), new Pair(undefined, 5), new Pair(undefined, 6) ]; var actual = _.sortBy(collection, function(pair) { return pair.x; }); deepEqual(actual, collection, 'sortBy should be stable'); }); `) }) } // groupBy. func Test_underscore_collections_20(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('groupBy', function() { var parity = _.groupBy([1, 2, 3, 4, 5, 6], function(num){ return num % 2; }); ok('0' in parity && '1' in parity, 'created a group for each value'); equal(parity[0].join(', '), '2, 4, 6', 'put each even number in the right group'); var list = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"]; var grouped = _.groupBy(list, 'length'); equal(grouped['3'].join(' '), 'one two six ten'); equal(grouped['4'].join(' '), 'four five nine'); equal(grouped['5'].join(' '), 'three seven eight'); var context = {}; _.groupBy([{}], function(){ ok(this === context); }, context); grouped = _.groupBy([4.2, 6.1, 6.4], function(num) { return Math.floor(num) > 4 ? 'hasOwnProperty' : 'constructor'; }); equal(grouped.constructor.length, 1); equal(grouped.hasOwnProperty.length, 2); var array = [{}]; _.groupBy(array, function(value, index, obj){ ok(obj === array); }); var array = [1, 2, 1, 2, 3]; var grouped = _.groupBy(array); equal(grouped['1'].length, 2); equal(grouped['3'].length, 1); }); `) }) } // countBy. func Test_underscore_collections_21(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('countBy', function() { var parity = _.countBy([1, 2, 3, 4, 5], function(num){ return num % 2 == 0; }); equal(parity['true'], 2); equal(parity['false'], 3); var list = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"]; var grouped = _.countBy(list, 'length'); equal(grouped['3'], 4); equal(grouped['4'], 3); equal(grouped['5'], 3); var context = {}; _.countBy([{}], function(){ ok(this === context); }, context); grouped = _.countBy([4.2, 6.1, 6.4], function(num) { return Math.floor(num) > 4 ? 'hasOwnProperty' : 'constructor'; }); equal(grouped.constructor, 1); equal(grouped.hasOwnProperty, 2); var array = [{}]; _.countBy(array, function(value, index, obj){ ok(obj === array); }); var array = [1, 2, 1, 2, 3]; var grouped = _.countBy(array); equal(grouped['1'], 2); equal(grouped['3'], 1); }); `) }) } // sortedIndex. func Test_underscore_collections_22(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('sortedIndex', function() { var numbers = [10, 20, 30, 40, 50], num = 35; var indexForNum = _.sortedIndex(numbers, num); equal(indexForNum, 3, '35 should be inserted at index 3'); var indexFor30 = _.sortedIndex(numbers, 30); equal(indexFor30, 2, '30 should be inserted at index 2'); var objects = [{x: 10}, {x: 20}, {x: 30}, {x: 40}]; var iterator = function(obj){ return obj.x; }; strictEqual(_.sortedIndex(objects, {x: 25}, iterator), 2); strictEqual(_.sortedIndex(objects, {x: 35}, 'x'), 3); var context = {1: 2, 2: 3, 3: 4}; iterator = function(obj){ return this[obj]; }; strictEqual(_.sortedIndex([1, 3], 2, iterator, context), 1); }); `) }) } // shuffle. func Test_underscore_collections_23(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('shuffle', function() { var numbers = _.range(10); var shuffled = _.shuffle(numbers).sort(); notStrictEqual(numbers, shuffled, 'original object is unmodified'); equal(shuffled.join(','), numbers.join(','), 'contains the same members before and after shuffle'); }); `) }) } // toArray. func Test_underscore_collections_24(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('toArray', function() { ok(!_.isArray(arguments), 'arguments object is not an array'); ok(_.isArray(_.toArray(arguments)), 'arguments object converted into array'); var a = [1,2,3]; ok(_.toArray(a) !== a, 'array is cloned'); equal(_.toArray(a).join(', '), '1, 2, 3', 'cloned array contains same elements'); var numbers = _.toArray({one : 1, two : 2, three : 3}); equal(numbers.join(', '), '1, 2, 3', 'object flattened into array'); // TEST: ReferenceError: document is not defined return; // test in IE < 9 try { var actual = _.toArray(document.childNodes); } catch(ex) { } ok(_.isArray(actual), 'should not throw converting a node list'); }); `) }) } // size. func Test_underscore_collections_25(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test('size', function() { equal(_.size({one : 1, two : 2, three : 3}), 3, 'can compute the size of an object'); equal(_.size([1, 2, 3]), 3, 'can compute the size of an array'); var func = function() { return _.size(arguments); }; equal(func(1, 2, 3, 4), 4, 'can test the size of the arguments object'); equal(_.size('hello'), 5, 'can compute the size of a string'); equal(_.size(null), 0, 'handles nulls'); }); `) }) } ================================================ FILE: underscore_functions_test.go ================================================ package otto import ( "testing" ) // bind. func Test_underscore_functions_0(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("bind", function() { var context = {name : 'moe'}; var func = function(arg) { return "name: " + (this.name || arg); }; var bound = _.bind(func, context); equal(bound(), 'name: moe', 'can bind a function to a context'); bound = _(func).bind(context); equal(bound(), 'name: moe', 'can do OO-style binding'); bound = _.bind(func, null, 'curly'); equal(bound(), 'name: curly', 'can bind without specifying a context'); func = function(salutation, name) { return salutation + ': ' + name; }; func = _.bind(func, this, 'hello'); equal(func('moe'), 'hello: moe', 'the function was partially applied in advance'); func = _.bind(func, this, 'curly'); equal(func(), 'hello: curly', 'the function was completely applied in advance'); func = function(salutation, firstname, lastname) { return salutation + ': ' + firstname + ' ' + lastname; }; func = _.bind(func, this, 'hello', 'moe', 'curly'); equal(func(), 'hello: moe curly', 'the function was partially applied in advance and can accept multiple arguments'); func = function(context, message) { equal(this, context, message); }; _.bind(func, 0, 0, 'can bind a function to <0>')(); _.bind(func, '', '', 'can bind a function to an empty string')(); _.bind(func, false, false, 'can bind a function to ')(); // These tests are only meaningful when using a browser without a native bind function // To test this with a modern browser, set underscore's nativeBind to undefined var F = function () { return this; }; var Boundf = _.bind(F, {hello: "moe curly"}); equal(Boundf().hello, "moe curly", "When called without the new operator, it's OK to be bound to the context"); }); `) }) } // partial. func Test_underscore_functions_1(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("partial", function() { var obj = {name: 'moe'}; var func = function() { return this.name + ' ' + _.toArray(arguments).join(' '); }; obj.func = _.partial(func, 'a', 'b'); equal(obj.func('c', 'd'), 'moe a b c d', 'can partially apply'); }); `) }) } // bindAll. func Test_underscore_functions_2(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("bindAll", function() { var curly = {name : 'curly'}, moe = { name : 'moe', getName : function() { return 'name: ' + this.name; }, sayHi : function() { return 'hi: ' + this.name; } }; curly.getName = moe.getName; _.bindAll(moe, 'getName', 'sayHi'); curly.sayHi = moe.sayHi; equal(curly.getName(), 'name: curly', 'unbound function is bound to current object'); equal(curly.sayHi(), 'hi: moe', 'bound function is still bound to original object'); }); `) }) } // memoize. func Test_underscore_functions_3(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("memoize", function() { var fib = function(n) { return n < 2 ? n : fib(n - 1) + fib(n - 2); }; var fastFib = _.memoize(fib); equal(fib(10), 55, 'a memoized version of fibonacci produces identical results'); equal(fastFib(10), 55, 'a memoized version of fibonacci produces identical results'); var o = function(str) { return str; }; var fastO = _.memoize(o); equal(o('toString'), 'toString', 'checks hasOwnProperty'); equal(fastO('toString'), 'toString', 'checks hasOwnProperty'); }); `) }) } // once. func Test_underscore_functions_4(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("once", function() { var num = 0; var increment = _.once(function(){ num++; }); increment(); increment(); equal(num, 1); }); `) }) } // wrap. func Test_underscore_functions_5(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("wrap", function() { var greet = function(name){ return "hi: " + name; }; var backwards = _.wrap(greet, function(func, name){ return func(name) + ' ' + name.split('').reverse().join(''); }); equal(backwards('moe'), 'hi: moe eom', 'wrapped the saluation function'); var inner = function(){ return "Hello "; }; var obj = {name : "Moe"}; obj.hi = _.wrap(inner, function(fn){ return fn() + this.name; }); equal(obj.hi(), "Hello Moe"); var noop = function(){}; var wrapped = _.wrap(noop, function(fn){ return Array.prototype.slice.call(arguments, 0); }); var ret = wrapped(['whats', 'your'], 'vector', 'victor'); deepEqual(ret, [noop, ['whats', 'your'], 'vector', 'victor']); }); `) }) } // compose. func Test_underscore_functions_6(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("compose", function() { var greet = function(name){ return "hi: " + name; }; var exclaim = function(sentence){ return sentence + '!'; }; var composed = _.compose(exclaim, greet); equal(composed('moe'), 'hi: moe!', 'can compose a function that takes another'); composed = _.compose(greet, exclaim); equal(composed('moe'), 'hi: moe!', 'in this case, the functions are also commutative'); }); `) }) } // after. func Test_underscore_functions_7(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("after", function() { var testAfter = function(afterAmount, timesCalled) { var afterCalled = 0; var after = _.after(afterAmount, function() { afterCalled++; }); while (timesCalled--) after(); return afterCalled; }; equal(testAfter(5, 5), 1, "after(N) should fire after being called N times"); equal(testAfter(5, 4), 0, "after(N) should not fire unless called N times"); equal(testAfter(0, 1), 1, "after(0) should fire immediately"); }); `) }) } ================================================ FILE: underscore_objects_test.go ================================================ package otto import ( "testing" ) // keys. func Test_underscore_objects_0(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("keys", function() { equal(_.keys({one : 1, two : 2}).join(', '), 'one, two', 'can extract the keys from an object'); // the test above is not safe because it relies on for-in enumeration order var a = []; a[1] = 0; equal(_.keys(a).join(', '), '1', 'is not fooled by sparse arrays; see issue #95'); equal(_.keys(null).join(''), '', 'empty array for values'); equal(_.keys(void 0).join(''), '', 'empty array for values'); equal(_.keys(1).join(''), '', 'empty array for number primitives'); equal(_.keys('a').join(''), '', 'empty array for string primitives'); equal(_.keys(true).join(''), '', 'empty array for boolean primitives'); }); `) }) } // values. func Test_underscore_objects_1(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("values", function() { equal(_.values({one: 1, two: 2}).join(', '), '1, 2', 'can extract the values from an object'); equal(_.values({one: 1, two: 2, length: 3}).join(', '), '1, 2, 3', '... even when one of them is "length"'); }); `) }) } // pairs. func Test_underscore_objects_2(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("pairs", function() { deepEqual(_.pairs({one: 1, two: 2}), [['one', 1], ['two', 2]], 'can convert an object into pairs'); deepEqual(_.pairs({one: 1, two: 2, length: 3}), [['one', 1], ['two', 2], ['length', 3]], '... even when one of them is "length"'); }); `) }) } // invert. func Test_underscore_objects_3(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("invert", function() { var obj = {first: 'Moe', second: 'Larry', third: 'Curly'}; equal(_.keys(_.invert(obj)).join(' '), 'Moe Larry Curly', 'can invert an object'); ok(_.isEqual(_.invert(_.invert(obj)), obj), 'two inverts gets you back where you started'); var obj = {length: 3}; ok(_.invert(obj)['3'] == 'length', 'can invert an object with "length"') }); `) }) } // functions. func Test_underscore_objects_4(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("functions", function() { var obj = {a : 'dash', b : _.map, c : (/yo/), d : _.reduce}; ok(_.isEqual(['b', 'd'], _.functions(obj)), 'can grab the function names of any passed-in object'); var Animal = function(){}; Animal.prototype.run = function(){}; equal(_.functions(new Animal).join(''), 'run', 'also looks up functions on the prototype'); }); `) }) } // extend. func Test_underscore_objects_5(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("extend", function() { var result; equal(_.extend({}, {a:'b'}).a, 'b', 'can extend an object with the attributes of another'); equal(_.extend({a:'x'}, {a:'b'}).a, 'b', 'properties in source override destination'); equal(_.extend({x:'x'}, {a:'b'}).x, 'x', 'properties not in source dont get overriden'); result = _.extend({x:'x'}, {a:'a'}, {b:'b'}); ok(_.isEqual(result, {x:'x', a:'a', b:'b'}), 'can extend from multiple source objects'); result = _.extend({x:'x'}, {a:'a', x:2}, {a:'b'}); ok(_.isEqual(result, {x:2, a:'b'}), 'extending from multiple source objects last property trumps'); result = _.extend({}, {a: void 0, b: null}); equal(_.keys(result).join(''), 'ab', 'extend does not copy undefined values'); try { result = {}; _.extend(result, null, undefined, {a:1}); } catch(ex) {} equal(result.a, 1, 'should not error on or sources'); }); `) }) } // pick. func Test_underscore_objects_6(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("pick", function() { var result; result = _.pick({a:1, b:2, c:3}, 'a', 'c'); ok(_.isEqual(result, {a:1, c:3}), 'can restrict properties to those named'); result = _.pick({a:1, b:2, c:3}, ['b', 'c']); ok(_.isEqual(result, {b:2, c:3}), 'can restrict properties to those named in an array'); result = _.pick({a:1, b:2, c:3}, ['a'], 'b'); ok(_.isEqual(result, {a:1, b:2}), 'can restrict properties to those named in mixed args'); var Obj = function(){}; Obj.prototype = {a: 1, b: 2, c: 3}; ok(_.isEqual(_.pick(new Obj, 'a', 'c'), {a:1, c: 3}), 'include prototype props'); }); `) }) } // omit. func Test_underscore_objects_7(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("omit", function() { var result; result = _.omit({a:1, b:2, c:3}, 'b'); ok(_.isEqual(result, {a:1, c:3}), 'can omit a single named property'); result = _.omit({a:1, b:2, c:3}, 'a', 'c'); ok(_.isEqual(result, {b:2}), 'can omit several named properties'); result = _.omit({a:1, b:2, c:3}, ['b', 'c']); ok(_.isEqual(result, {a:1}), 'can omit properties named in an array'); var Obj = function(){}; Obj.prototype = {a: 1, b: 2, c: 3}; ok(_.isEqual(_.omit(new Obj, 'b'), {a:1, c: 3}), 'include prototype props'); }); `) }) } // defaults. func Test_underscore_objects_8(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("defaults", function() { var result; var options = {zero: 0, one: 1, empty: "", nan: NaN, string: "string"}; _.defaults(options, {zero: 1, one: 10, twenty: 20}); equal(options.zero, 0, 'value exists'); equal(options.one, 1, 'value exists'); equal(options.twenty, 20, 'default applied'); _.defaults(options, {empty: "full"}, {nan: "nan"}, {word: "word"}, {word: "dog"}); equal(options.empty, "", 'value exists'); ok(_.isNaN(options.nan), "NaN isn't overridden"); equal(options.word, "word", 'new value is added, first one wins'); try { options = {}; _.defaults(options, null, undefined, {a:1}); } catch(ex) {} equal(options.a, 1, 'should not error on or sources'); }); `) }) } // clone. func Test_underscore_objects_9(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("clone", function() { var moe = {name : 'moe', lucky : [13, 27, 34]}; var clone = _.clone(moe); equal(clone.name, 'moe', 'the clone as the attributes of the original'); clone.name = 'curly'; ok(clone.name == 'curly' && moe.name == 'moe', 'clones can change shallow attributes without affecting the original'); clone.lucky.push(101); equal(_.last(moe.lucky), 101, 'changes to deep attributes are shared with the original'); equal(_.clone(undefined), void 0, 'non objects should not be changed by clone'); equal(_.clone(1), 1, 'non objects should not be changed by clone'); equal(_.clone(null), null, 'non objects should not be changed by clone'); }); `) }) } // isEqual. func Test_underscore_objects_10(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isEqual", function() { function First() { this.value = 1; } First.prototype.value = 1; function Second() { this.value = 1; } Second.prototype.value = 2; // Basic equality and identity comparisons. ok(_.isEqual(null, null), " is equal to "); ok(_.isEqual(), " is equal to "); ok(!_.isEqual(0, -0), "<0> is not equal to <-0>"); ok(!_.isEqual(-0, 0), "Commutative equality is implemented for <0> and <-0>"); ok(!_.isEqual(null, undefined), " is not equal to "); ok(!_.isEqual(undefined, null), "Commutative equality is implemented for and "); // String object and primitive comparisons. ok(_.isEqual("Curly", "Curly"), "Identical string primitives are equal"); ok(_.isEqual(new String("Curly"), new String("Curly")), "String objects with identical primitive values are equal"); ok(_.isEqual(new String("Curly"), "Curly"), "String primitives and their corresponding object wrappers are equal"); ok(_.isEqual("Curly", new String("Curly")), "Commutative equality is implemented for string objects and primitives"); ok(!_.isEqual("Curly", "Larry"), "String primitives with different values are not equal"); ok(!_.isEqual(new String("Curly"), new String("Larry")), "String objects with different primitive values are not equal"); ok(!_.isEqual(new String("Curly"), {toString: function(){ return "Curly"; }}), "String objects and objects with a custom method are not equal"); // Number object and primitive comparisons. ok(_.isEqual(75, 75), "Identical number primitives are equal"); ok(_.isEqual(new Number(75), new Number(75)), "Number objects with identical primitive values are equal"); ok(_.isEqual(75, new Number(75)), "Number primitives and their corresponding object wrappers are equal"); ok(_.isEqual(new Number(75), 75), "Commutative equality is implemented for number objects and primitives"); ok(!_.isEqual(new Number(0), -0), " and <-0> are not equal"); ok(!_.isEqual(0, new Number(-0)), "Commutative equality is implemented for and <-0>"); ok(!_.isEqual(new Number(75), new Number(63)), "Number objects with different primitive values are not equal"); ok(!_.isEqual(new Number(63), {valueOf: function(){ return 63; }}), "Number objects and objects with a method are not equal"); // Comparisons involving . ok(_.isEqual(NaN, NaN), " is equal to "); ok(!_.isEqual(61, NaN), "A number primitive is not equal to "); ok(!_.isEqual(new Number(79), NaN), "A number object is not equal to "); ok(!_.isEqual(Infinity, NaN), " is not equal to "); // Boolean object and primitive comparisons. ok(_.isEqual(true, true), "Identical boolean primitives are equal"); ok(_.isEqual(new Boolean, new Boolean), "Boolean objects with identical primitive values are equal"); ok(_.isEqual(true, new Boolean(true)), "Boolean primitives and their corresponding object wrappers are equal"); ok(_.isEqual(new Boolean(true), true), "Commutative equality is implemented for booleans"); ok(!_.isEqual(new Boolean(true), new Boolean), "Boolean objects with different primitive values are not equal"); // Common type coercions. ok(!_.isEqual(true, new Boolean(false)), "Boolean objects are not equal to the boolean primitive "); ok(!_.isEqual("75", 75), "String and number primitives with like values are not equal"); ok(!_.isEqual(new Number(63), new String(63)), "String and number objects with like values are not equal"); ok(!_.isEqual(75, "75"), "Commutative equality is implemented for like string and number values"); ok(!_.isEqual(0, ""), "Number and string primitives with like values are not equal"); ok(!_.isEqual(1, true), "Number and boolean primitives with like values are not equal"); ok(!_.isEqual(new Boolean(false), new Number(0)), "Boolean and number objects with like values are not equal"); ok(!_.isEqual(false, new String("")), "Boolean primitives and string objects with like values are not equal"); ok(!_.isEqual(12564504e5, new Date(2009, 9, 25)), "Dates and their corresponding numeric primitive values are not equal"); // Dates. ok(_.isEqual(new Date(2009, 9, 25), new Date(2009, 9, 25)), "Date objects referencing identical times are equal"); ok(!_.isEqual(new Date(2009, 9, 25), new Date(2009, 11, 13)), "Date objects referencing different times are not equal"); ok(!_.isEqual(new Date(2009, 11, 13), { getTime: function(){ return 12606876e5; } }), "Date objects and objects with a method are not equal"); ok(!_.isEqual(new Date("Curly"), new Date("Curly")), "Invalid dates are not equal"); // Functions. ok(!_.isEqual(First, Second), "Different functions with identical bodies and source code representations are not equal"); // RegExps. ok(_.isEqual(/(?:)/gim, /(?:)/gim), "RegExps with equivalent patterns and flags are equal"); ok(!_.isEqual(/(?:)/g, /(?:)/gi), "RegExps with equivalent patterns and different flags are not equal"); ok(!_.isEqual(/Moe/gim, /Curly/gim), "RegExps with different patterns and equivalent flags are not equal"); ok(!_.isEqual(/(?:)/gi, /(?:)/g), "Commutative equality is implemented for RegExps"); ok(!_.isEqual(/Curly/g, {source: "Larry", global: true, ignoreCase: false, multiline: false}), "RegExps and RegExp-like objects are not equal"); // Empty arrays, array-like objects, and object literals. ok(_.isEqual({}, {}), "Empty object literals are equal"); ok(_.isEqual([], []), "Empty array literals are equal"); ok(_.isEqual([{}], [{}]), "Empty nested arrays and objects are equal"); ok(!_.isEqual({length: 0}, []), "Array-like objects and arrays are not equal."); ok(!_.isEqual([], {length: 0}), "Commutative equality is implemented for array-like objects"); ok(!_.isEqual({}, []), "Object literals and array literals are not equal"); ok(!_.isEqual([], {}), "Commutative equality is implemented for objects and arrays"); // Arrays with primitive and object values. ok(_.isEqual([1, "Larry", true], [1, "Larry", true]), "Arrays containing identical primitives are equal"); ok(_.isEqual([(/Moe/g), new Date(2009, 9, 25)], [(/Moe/g), new Date(2009, 9, 25)]), "Arrays containing equivalent elements are equal"); // Multi-dimensional arrays. var a = [new Number(47), false, "Larry", /Moe/, new Date(2009, 11, 13), ['running', 'biking', new String('programming')], {a: 47}]; var b = [new Number(47), false, "Larry", /Moe/, new Date(2009, 11, 13), ['running', 'biking', new String('programming')], {a: 47}]; ok(_.isEqual(a, b), "Arrays containing nested arrays and objects are recursively compared"); // Overwrite the methods defined in ES 5.1 section 15.4.4. a.forEach = a.map = a.filter = a.every = a.indexOf = a.lastIndexOf = a.some = a.reduce = a.reduceRight = null; b.join = b.pop = b.reverse = b.shift = b.slice = b.splice = b.concat = b.sort = b.unshift = null; // Array elements and properties. ok(_.isEqual(a, b), "Arrays containing equivalent elements and different non-numeric properties are equal"); a.push("White Rocks"); ok(!_.isEqual(a, b), "Arrays of different lengths are not equal"); a.push("East Boulder"); b.push("Gunbarrel Ranch", "Teller Farm"); ok(!_.isEqual(a, b), "Arrays of identical lengths containing different elements are not equal"); // Sparse arrays. ok(_.isEqual(Array(3), Array(3)), "Sparse arrays of identical lengths are equal"); ok(!_.isEqual(Array(3), Array(6)), "Sparse arrays of different lengths are not equal when both are empty"); // Simple objects. ok(_.isEqual({a: "Curly", b: 1, c: true}, {a: "Curly", b: 1, c: true}), "Objects containing identical primitives are equal"); ok(_.isEqual({a: /Curly/g, b: new Date(2009, 11, 13)}, {a: /Curly/g, b: new Date(2009, 11, 13)}), "Objects containing equivalent members are equal"); ok(!_.isEqual({a: 63, b: 75}, {a: 61, b: 55}), "Objects of identical sizes with different values are not equal"); ok(!_.isEqual({a: 63, b: 75}, {a: 61, c: 55}), "Objects of identical sizes with different property names are not equal"); ok(!_.isEqual({a: 1, b: 2}, {a: 1}), "Objects of different sizes are not equal"); ok(!_.isEqual({a: 1}, {a: 1, b: 2}), "Commutative equality is implemented for objects"); ok(!_.isEqual({x: 1, y: undefined}, {x: 1, z: 2}), "Objects with identical keys and different values are not equivalent"); // contains nested objects and arrays. a = { name: new String("Moe Howard"), age: new Number(77), stooge: true, hobbies: ["acting"], film: { name: "Sing a Song of Six Pants", release: new Date(1947, 9, 30), stars: [new String("Larry Fine"), "Shemp Howard"], minutes: new Number(16), seconds: 54 } }; // contains equivalent nested objects and arrays. b = { name: new String("Moe Howard"), age: new Number(77), stooge: true, hobbies: ["acting"], film: { name: "Sing a Song of Six Pants", release: new Date(1947, 9, 30), stars: [new String("Larry Fine"), "Shemp Howard"], minutes: new Number(16), seconds: 54 } }; ok(_.isEqual(a, b), "Objects with nested equivalent members are recursively compared"); // Instances. ok(_.isEqual(new First, new First), "Object instances are equal"); ok(!_.isEqual(new First, new Second), "Objects with different constructors and identical own properties are not equal"); ok(!_.isEqual({value: 1}, new First), "Object instances and objects sharing equivalent properties are not equal"); ok(!_.isEqual({value: 2}, new Second), "The prototype chain of objects should not be examined"); // Circular Arrays. (a = []).push(a); (b = []).push(b); ok(_.isEqual(a, b), "Arrays containing circular references are equal"); a.push(new String("Larry")); b.push(new String("Larry")); ok(_.isEqual(a, b), "Arrays containing circular references and equivalent properties are equal"); a.push("Shemp"); b.push("Curly"); ok(!_.isEqual(a, b), "Arrays containing circular references and different properties are not equal"); // More circular arrays #767. a = ["everything is checked but", "this", "is not"]; a[1] = a; b = ["everything is checked but", ["this", "array"], "is not"]; ok(!_.isEqual(a, b), "Comparison of circular references with non-circular references are not equal"); // Circular Objects. a = {abc: null}; b = {abc: null}; a.abc = a; b.abc = b; ok(_.isEqual(a, b), "Objects containing circular references are equal"); a.def = 75; b.def = 75; ok(_.isEqual(a, b), "Objects containing circular references and equivalent properties are equal"); a.def = new Number(75); b.def = new Number(63); ok(!_.isEqual(a, b), "Objects containing circular references and different properties are not equal"); // More circular objects #767. a = {everything: "is checked", but: "this", is: "not"}; a.but = a; b = {everything: "is checked", but: {that:"object"}, is: "not"}; ok(!_.isEqual(a, b), "Comparison of circular references with non-circular object references are not equal"); // Cyclic Structures. a = [{abc: null}]; b = [{abc: null}]; (a[0].abc = a).push(a); (b[0].abc = b).push(b); ok(_.isEqual(a, b), "Cyclic structures are equal"); a[0].def = "Larry"; b[0].def = "Larry"; ok(_.isEqual(a, b), "Cyclic structures containing equivalent properties are equal"); a[0].def = new String("Larry"); b[0].def = new String("Curly"); ok(!_.isEqual(a, b), "Cyclic structures containing different properties are not equal"); // Complex Circular References. a = {foo: {b: {foo: {c: {foo: null}}}}}; b = {foo: {b: {foo: {c: {foo: null}}}}}; a.foo.b.foo.c.foo = a; b.foo.b.foo.c.foo = b; ok(_.isEqual(a, b), "Cyclic structures with nested and identically-named properties are equal"); // Chaining. ok(!_.isEqual(_({x: 1, y: undefined}).chain(), _({x: 1, z: 2}).chain()), 'Chained objects containing different values are not equal'); a = _({x: 1, y: 2}).chain(); b = _({x: 1, y: 2}).chain(); equal(_.isEqual(a.isEqual(b), _(true)), true, ' can be chained'); // TEST: ??? return; // Objects from another frame. ok(_.isEqual({}, iObject)); }); `) }) } // isEmpty. func Test_underscore_objects_11(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isEmpty", function() { ok(!_([1]).isEmpty(), '[1] is not empty'); ok(_.isEmpty([]), '[] is empty'); ok(!_.isEmpty({one : 1}), '{one : 1} is not empty'); ok(_.isEmpty({}), '{} is empty'); ok(_.isEmpty(new RegExp('')), 'objects with prototype properties are empty'); ok(_.isEmpty(null), 'null is empty'); ok(_.isEmpty(), 'undefined is empty'); ok(_.isEmpty(''), 'the empty string is empty'); ok(!_.isEmpty('moe'), 'but other strings are not'); var obj = {one : 1}; delete obj.one; ok(_.isEmpty(obj), 'deleting all the keys from an object empties it'); }); `) }) } // isElement. func Test_underscore_objects_12(t *testing.T) { // TEST: ReferenceError: $ is not defined if true { return } tt(t, func() { test := underscoreTest() test(` test("isElement", function() { ok(!_.isElement('div'), 'strings are not dom elements'); ok(_.isElement($('html')[0]), 'the html tag is a DOM element'); ok(_.isElement(iElement), 'even from another frame'); }); `) }) } // isArguments. func Test_underscore_objects_13(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isArguments", function() { var args = (function(){ return arguments; })(1, 2, 3); ok(!_.isArguments('string'), 'a string is not an arguments object'); ok(!_.isArguments(_.isArguments), 'a function is not an arguments object'); ok(_.isArguments(args), 'but the arguments object is an arguments object'); ok(!_.isArguments(_.toArray(args)), 'but not when it\'s converted into an array'); ok(!_.isArguments([1,2,3]), 'and not vanilla arrays.'); // TEST: ReferenceError: iArguments is not defined return; ok(_.isArguments(iArguments), 'even from another frame'); }); `) }) } // isObject. func Test_underscore_objects_14(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isObject", function() { ok(_.isObject(arguments), 'the arguments object is object'); ok(_.isObject([1, 2, 3]), 'and arrays'); // TEST: ReferenceError: $ is not defined return; ok(_.isObject($('html')[0]), 'and DOM element'); ok(_.isObject(iElement), 'even from another frame'); ok(_.isObject(function () {}), 'and functions'); ok(_.isObject(iFunction), 'even from another frame'); ok(!_.isObject(null), 'but not null'); ok(!_.isObject(undefined), 'and not undefined'); ok(!_.isObject('string'), 'and not string'); ok(!_.isObject(12), 'and not number'); ok(!_.isObject(true), 'and not boolean'); ok(_.isObject(new String('string')), 'but new String()'); }); `) }) } // isArray. func Test_underscore_objects_15(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isArray", function() { ok(!_.isArray(arguments), 'the arguments object is not an array'); ok(_.isArray([1, 2, 3]), 'but arrays are'); // TEST: ??? return; ok(_.isArray(iArray), 'even from another frame'); }); `) }) } // isString. func Test_underscore_objects_16(t *testing.T) { // TEST: ReferenceError: document is not defined if true { return } tt(t, func() { test := underscoreTest() test(` test("isString", function() { ok(!_.isString(document.body), 'the document body is not a string'); ok(_.isString([1, 2, 3].join(', ')), 'but strings are'); // TEST: ??? return; ok(_.isString(iString), 'even from another frame'); }); `) }) } // isNumber. func Test_underscore_objects_17(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isNumber", function() { ok(!_.isNumber('string'), 'a string is not a number'); ok(!_.isNumber(arguments), 'the arguments object is not a number'); ok(!_.isNumber(undefined), 'undefined is not a number'); ok(_.isNumber(3 * 4 - 7 / 10), 'but numbers are'); ok(_.isNumber(NaN), 'NaN *is* a number'); ok(_.isNumber(Infinity), 'Infinity is a number'); // TEST: ??? return; ok(_.isNumber(iNumber), 'even from another frame'); ok(!_.isNumber('1'), 'numeric strings are not numbers'); }); `) }) } // isBoolean. func Test_underscore_objects_18(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isBoolean", function() { ok(!_.isBoolean(2), 'a number is not a boolean'); ok(!_.isBoolean("string"), 'a string is not a boolean'); ok(!_.isBoolean("false"), 'the string "false" is not a boolean'); ok(!_.isBoolean("true"), 'the string "true" is not a boolean'); ok(!_.isBoolean(arguments), 'the arguments object is not a boolean'); ok(!_.isBoolean(undefined), 'undefined is not a boolean'); ok(!_.isBoolean(NaN), 'NaN is not a boolean'); ok(!_.isBoolean(null), 'null is not a boolean'); ok(_.isBoolean(true), 'but true is'); ok(_.isBoolean(false), 'and so is false'); // TEST: ??? return; ok(_.isBoolean(iBoolean), 'even from another frame'); }); `) }) } // isFunction. func Test_underscore_objects_19(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isFunction", function() { ok(!_.isFunction([1, 2, 3]), 'arrays are not functions'); ok(!_.isFunction('moe'), 'strings are not functions'); ok(_.isFunction(_.isFunction), 'but functions are'); // TEST: ??? return; ok(_.isFunction(iFunction), 'even from another frame'); }); `) }) } // isDate. func Test_underscore_objects_20(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isDate", function() { ok(!_.isDate(100), 'numbers are not dates'); ok(!_.isDate({}), 'objects are not dates'); ok(_.isDate(new Date()), 'but dates are'); // TEST: ??? return; ok(_.isDate(iDate), 'even from another frame'); }); `) }) } // isRegExp. func Test_underscore_objects_21(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isRegExp", function() { ok(!_.isRegExp(_.identity), 'functions are not RegExps'); ok(_.isRegExp(/identity/), 'but RegExps are'); // TEST: ??? return; ok(_.isRegExp(iRegExp), 'even from another frame'); }); `) }) } // isFinite. func Test_underscore_objects_22(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isFinite", function() { ok(!_.isFinite(undefined), 'undefined is not Finite'); ok(!_.isFinite(null), 'null is not Finite'); ok(!_.isFinite(NaN), 'NaN is not Finite'); ok(!_.isFinite(Infinity), 'Infinity is not Finite'); ok(!_.isFinite(-Infinity), '-Infinity is not Finite'); ok(_.isFinite('12'), 'Numeric strings are numbers'); ok(!_.isFinite('1a'), 'Non numeric strings are not numbers'); ok(!_.isFinite(''), 'Empty strings are not numbers'); var obj = new Number(5); ok(_.isFinite(obj), 'Number instances can be finite'); ok(_.isFinite(0), '0 is Finite'); ok(_.isFinite(123), 'Ints are Finite'); ok(_.isFinite(-12.44), 'Floats are Finite'); }); `) }) } // isNaN. func Test_underscore_objects_23(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isNaN", function() { ok(!_.isNaN(undefined), 'undefined is not NaN'); ok(!_.isNaN(null), 'null is not NaN'); ok(!_.isNaN(0), '0 is not NaN'); ok(_.isNaN(NaN), 'but NaN is'); // TEST: ??? return; ok(_.isNaN(iNaN), 'even from another frame'); ok(_.isNaN(new Number(NaN)), 'wrapped NaN is still NaN'); }); `) }) } // isNull. func Test_underscore_objects_24(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isNull", function() { ok(!_.isNull(undefined), 'undefined is not null'); ok(!_.isNull(NaN), 'NaN is not null'); ok(_.isNull(null), 'but null is'); // TEST: ??? return; ok(_.isNull(iNull), 'even from another frame'); }); `) }) } // isUndefined. func Test_underscore_objects_25(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("isUndefined", function() { ok(!_.isUndefined(1), 'numbers are defined'); ok(!_.isUndefined(null), 'null is defined'); ok(!_.isUndefined(false), 'false is defined'); ok(!_.isUndefined(NaN), 'NaN is defined'); ok(_.isUndefined(), 'nothing is undefined'); ok(_.isUndefined(undefined), 'undefined is undefined'); // TEST: ??? return; ok(_.isUndefined(iUndefined), 'even from another frame'); }); `) }) } // tap. func Test_underscore_objects_26(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("tap", function() { var intercepted = null; var interceptor = function(obj) { intercepted = obj; }; var returned = _.tap(1, interceptor); equal(intercepted, 1, "passes tapped object to interceptor"); equal(returned, 1, "returns tapped object"); returned = _([1,2,3]).chain(). map(function(n){ return n * 2; }). max(). tap(interceptor). value(); ok(returned == 6 && intercepted == 6, 'can use tapped objects in a chain'); }); `) }) } // has. func Test_underscore_objects_27(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("has", function () { var obj = {foo: "bar", func: function () {} }; ok (_.has(obj, "foo"), "has() checks that the object has a property."); ok (_.has(obj, "baz") == false, "has() returns false if the object doesn't have the property."); ok (_.has(obj, "func"), "has() works for functions too."); obj.hasOwnProperty = null; ok (_.has(obj, "foo"), "has() works even when the hasOwnProperty method is deleted."); var child = {}; child.prototype = obj; ok (_.has(child, "foo") == false, "has() does not check the prototype chain for a property.") }); `) }) } ================================================ FILE: underscore_test.go ================================================ package otto import ( "sync" "testing" "github.com/robertkrimen/otto/terst" "github.com/robertkrimen/otto/underscore" ) func init() { underscore.Disable() } var ( // A persistent handle for the underscore tester // We do not run underscore tests in parallel, so it is okay to stash globally. tester *_tester once sync.Once ) // A tester for underscore: underscoreTest => test(underscore) :). func underscoreTest() func(string, ...interface{}) Value { setTester := func() { tester = newTester() tester.underscore() // Load underscore and testing shim, etc. } once.Do(setTester) return tester.test } func (te *_tester) underscore() { vm := te.vm _, err := vm.Run(underscore.Source()) if err != nil { panic(err) } err = vm.Set("assert", func(call FunctionCall) Value { if !call.Argument(0).bool() { message := "Assertion failed" if len(call.ArgumentList) > 1 { message = call.ArgumentList[1].string() } t := terst.Caller().T() is(message, nil) t.Fail() return falseValue } return trueValue }) if err != nil { panic(err) } _, err = vm.Run(` var templateSettings; function _setup() { templateSettings = _.clone(_.templateSettings); } function _teardown() { _.templateSettings = templateSettings; } function module() { /* Nothing happens. */ } function equals(a, b, emit) { assert(a == b, emit + ", <" + a + "> != <" + b + ">"); } var equal = equals; function notStrictEqual(a, b, emit) { assert(a !== b, emit); } function strictEqual(a, b, emit) { assert(a === b, emit); } function ok(a, emit) { assert(a, emit); } function raises(fn, want, emit) { var have, _ok = false; if (typeof want === "string") { emit = want; want = null; } try { fn(); } catch(tmp) { have = tmp; } if (have) { if (!want) { _ok = true; } else if (want instanceof RegExp) { _ok = want.test(have); } else if (have instanceof want) { _ok = true } else if (want.call({}, have) === true) { _ok = true; } } ok(_ok, emit); } function test(name){ _setup() try { templateSettings = _.clone(_.templateSettings); if (arguments.length == 3) { count = 0 for (count = 0; count < arguments[1]; count++) { arguments[2]() } } else { // For now. arguments[1]() } } finally { _teardown() } } function deepEqual(a, b, emit) { // Also, for now. assert(_.isEqual(a, b), emit) } `) if err != nil { panic(err) } } func Test_underscore(t *testing.T) { tt(t, func() { test := underscoreTest() test(` _.map([1, 2, 3], function(value){ return value + 1 }) `, "2,3,4") test(` abc = _.find([1, 2, 3, -1], function(value) { return value == -1 }) `, -1) test(`_.isEqual(1, 1)`, true) test(`_.isEqual([], [])`, true) test(`_.isEqual(['b', 'd'], ['b', 'd'])`, true) test(`_.isEqual(['b', 'd', 'c'], ['b', 'd', 'e'])`, false) test(`_.isFunction(function(){})`, true) test(`_.template('

\u2028<%= "\\u2028\\u2029" %>\u2029

')()`, "

\u2028\u2028\u2029\u2029

") }) } // TODO Test: typeof An argument reference // TODO Test: abc = {}; abc == Object(abc) ================================================ FILE: underscore_utility_test.go ================================================ package otto import ( "testing" ) // #750 - Return _ instance. func Test_underscore_utility_0(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("#750 - Return _ instance.", 2, function() { var instance = _([]); ok(_(instance) === instance); ok(new _(instance) === instance); }); `) }) } // identity. func Test_underscore_utility_1(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("identity", function() { var moe = {name : 'moe'}; equal(_.identity(moe), moe, 'moe is the same as his identity'); }); `) }) } // random. func Test_underscore_utility_2(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("random", function() { var array = _.range(1000); var min = Math.pow(2, 31); var max = Math.pow(2, 62); ok(_.every(array, function() { return _.random(min, max) >= min; }), "should produce a random number greater than or equal to the minimum number"); ok(_.some(array, function() { return _.random(Number.MAX_VALUE) > 0; }), "should produce a random number when passed "); }); `) }) } // uniqueId. func Test_underscore_utility_3(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("uniqueId", function() { var ids = [], i = 0; while(i++ < 100) ids.push(_.uniqueId()); equal(_.uniq(ids).length, ids.length, 'can generate a globally-unique stream of ids'); }); `) }) } // times. func Test_underscore_utility_4(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("times", function() { var vals = []; _.times(3, function (i) { vals.push(i); }); ok(_.isEqual(vals, [0,1,2]), "is 0 indexed"); // vals = []; _(3).times(function(i) { vals.push(i); }); ok(_.isEqual(vals, [0,1,2]), "works as a wrapper"); // collects return values ok(_.isEqual([0, 1, 2], _.times(3, function(i) { return i; })), "collects return values"); }); `) }) } // mixin. func Test_underscore_utility_5(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("mixin", function() { _.mixin({ myReverse: function(string) { return string.split('').reverse().join(''); } }); equal(_.myReverse('panacea'), 'aecanap', 'mixed in a function to _'); equal(_('champ').myReverse(), 'pmahc', 'mixed in a function to the OOP wrapper'); }); `) }) } // _.escape. func Test_underscore_utility_6(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("_.escape", function() { equal(_.escape("Curly & Moe"), "Curly & Moe"); equal(_.escape("Curly & Moe"), "Curly &amp; Moe"); equal(_.escape(null), ''); }); `) }) } // _.unescape. func Test_underscore_utility_7(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("_.unescape", function() { var string = "Curly & Moe"; equal(_.unescape("Curly & Moe"), string); equal(_.unescape("Curly &amp; Moe"), "Curly & Moe"); equal(_.unescape(null), ''); equal(_.unescape(_.escape(string)), string); }); `) }) } // template. func Test_underscore_utility_8(t *testing.T) { tt(t, func() { test := underscoreTest() test(` test("template", function() { var basicTemplate = _.template("<%= thing %> is gettin' on my noives!"); var result = basicTemplate({thing : 'This'}); equal(result, "This is gettin' on my noives!", 'can do basic attribute interpolation'); var sansSemicolonTemplate = _.template("A <% this %> B"); equal(sansSemicolonTemplate(), "A B"); var backslashTemplate = _.template("<%= thing %> is \\ridanculous"); equal(backslashTemplate({thing: 'This'}), "This is \\ridanculous"); var escapeTemplate = _.template('<%= a ? "checked=\\"checked\\"" : "" %>'); equal(escapeTemplate({a: true}), 'checked="checked"', 'can handle slash escapes in interpolations.'); var fancyTemplate = _.template("
    <% \ for (var key in people) { \ %>
  • <%= people[key] %>
  • <% } %>
"); result = fancyTemplate({people : {moe : "Moe", larry : "Larry", curly : "Curly"}}); equal(result, "
  • Moe
  • Larry
  • Curly
", 'can run arbitrary javascript in templates'); var escapedCharsInJavascriptTemplate = _.template("
    <% _.each(numbers.split('\\n'), function(item) { %>
  • <%= item %>
  • <% }) %>
"); result = escapedCharsInJavascriptTemplate({numbers: "one\ntwo\nthree\nfour"}); equal(result, "
  • one
  • two
  • three
  • four
", 'Can use escaped characters (e.g. \\n) in Javascript'); var namespaceCollisionTemplate = _.template("<%= pageCount %> <%= thumbnails[pageCount] %> <% _.each(thumbnails, function(p) { %>
\">
<% }); %>"); result = namespaceCollisionTemplate({ pageCount: 3, thumbnails: { 1: "p1-thumbnail.gif", 2: "p2-thumbnail.gif", 3: "p3-thumbnail.gif" } }); equal(result, "3 p3-thumbnail.gif
"); var noInterpolateTemplate = _.template("

Just some text. Hey, I know this is silly but it aids consistency.

"); result = noInterpolateTemplate(); equal(result, "

Just some text. Hey, I know this is silly but it aids consistency.

"); var quoteTemplate = _.template("It's its, not it's"); equal(quoteTemplate({}), "It's its, not it's"); var quoteInStatementAndBody = _.template("<%\ if(foo == 'bar'){ \ %>Statement quotes and 'quotes'.<% } %>"); equal(quoteInStatementAndBody({foo: "bar"}), "Statement quotes and 'quotes'."); var withNewlinesAndTabs = _.template('This\n\t\tis: <%= x %>.\n\tok.\nend.'); equal(withNewlinesAndTabs({x: 'that'}), 'This\n\t\tis: that.\n\tok.\nend.'); var template = _.template("<%- value %>"); var result = template({value: "