├── .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.go ├── dbg └── 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.go ├── otto └── main.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 /.clog.toml: -------------------------------------------------------------------------------- 1 | [clog] 2 | repository = "https://github.com/robertkrimen/otto" 3 | subtitle = "Release Notes" 4 | 5 | [sections] 6 | "Refactors" = ["refactor"] 7 | "Chores" = ["chore"] 8 | "Continuous Integration" = ["ci"] 9 | "Improvements" = ["imp", "improvement"] 10 | "Features" = ["feat", "feature"] 11 | "Legacy" = ["legacy"] 12 | "QA" = ["qa", "test", "tests"] 13 | "Documentation" = ["doc", "docs"] 14 | -------------------------------------------------------------------------------- /.github/workflows/release-build.yml: -------------------------------------------------------------------------------- 1 | name: Build Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+*' 7 | 8 | jobs: 9 | goreleaser: 10 | name: Release Go Binary 11 | runs-on: [ubuntu-latest] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: 1.23 21 | cache: true 22 | - name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v6 24 | with: 25 | # either 'goreleaser' (default) or 'goreleaser-pro' 26 | distribution: goreleaser 27 | version: latest 28 | args: release --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution 32 | # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} 33 | -------------------------------------------------------------------------------- /.github/workflows/test-lint.yml: -------------------------------------------------------------------------------- 1 | name: Go test and lint 2 | 3 | on: 4 | pull_request: 5 | branches: 'master' 6 | 7 | jobs: 8 | go-test-lint: 9 | strategy: 10 | matrix: 11 | go: [1.22, 1.23] 12 | golangcli: [v1.61.0] 13 | os: [ubuntu-latest] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: ${{ matrix.go }} 23 | cache: true 24 | 25 | - name: Validate go mod / generate 26 | run: | 27 | go mod tidy 28 | go install golang.org/x/tools/cmd/stringer@latest 29 | go generate ./... 30 | git --no-pager diff && [[ 0 -eq $(git status --porcelain | wc -l) ]] 31 | 32 | - name: Go Lint 33 | uses: golangci/golangci-lint-action@v4 34 | with: 35 | version: ${{ matrix.golangcli }} 36 | args: --out-format=colored-line-number 37 | skip-pkg-cache: true 38 | skip-build-cache: true 39 | 40 | - name: Go Build 41 | run: go build ./... 42 | 43 | - name: Go Test 44 | run: go test -race -v ./... 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .test 2 | otto/otto 3 | otto/otto-* 4 | tools/tester/testdata/ 5 | tools/tester/tester 6 | tools/gen-jscore/gen-jscore 7 | tools/gen-tokens/gen-tokens 8 | .idea 9 | dist/ 10 | .vscode/ 11 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 6m 3 | 4 | linters-settings: 5 | govet: 6 | settings: 7 | shadow: 8 | strict: true 9 | enable-all: true 10 | goconst: 11 | min-len: 2 12 | min-occurrences: 4 13 | revive: 14 | enable-all-rules: false 15 | rules: 16 | - name: var-naming 17 | disabled: true 18 | gosec: 19 | excludes: 20 | - G115 # Too many false positives. 21 | 22 | linters: 23 | enable-all: true 24 | disable: 25 | - dupl 26 | - lll 27 | - gochecknoglobals 28 | - gochecknoinits 29 | - funlen 30 | - godox 31 | - err113 32 | - wsl 33 | - nlreturn 34 | - gomnd 35 | - mnd 36 | - paralleltest 37 | - wrapcheck 38 | - testpackage 39 | - gocognit 40 | - nestif 41 | - exhaustive 42 | - forcetypeassert 43 | - gocyclo 44 | - cyclop 45 | - varnamelen 46 | - maintidx 47 | - ireturn 48 | - exhaustruct 49 | - dupword 50 | # Just causes noise 51 | - depguard 52 | # Deprecated 53 | - execinquery 54 | # Not needed in go 1.22+ 55 | - exportloopref 56 | 57 | issues: 58 | exclude-use-default: false 59 | max-same-issues: 0 60 | exclude: 61 | - Deferring unsafe method "Close" on type "io\.ReadCloser" 62 | exclude-dirs: 63 | - terst 64 | exclude-files: 65 | - dbg/dbg.go 66 | - token/token_const.go 67 | exclude-rules: 68 | # Field alignment in tests isn't a performance issue. 69 | - text: fieldalignment 70 | path: _test\.go 71 | - text: Error return value of `fmt\.Fprint.*` is not checked 72 | path: tools/tester/main.go 73 | 74 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # When adding options check the documentation at https://goreleaser.com 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - darwin 11 | goarch: 12 | - amd64 13 | - arm64 14 | main: ./otto 15 | id: otto 16 | binary: otto 17 | universal_binaries: 18 | - replace: true 19 | id: otto 20 | checksum: 21 | name_template: 'checksums.txt' 22 | snapshot: 23 | name_template: "{{ incpatch .Version }}-next" 24 | archives: 25 | - id: otto 26 | name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" 27 | release: 28 | header: | 29 | 30 | ### {{.Tag}} Release Notes ({{.Date}}) 31 | footer: | 32 | [Full Changelog](https://{{ .ModulePath }}/compare/{{ .PreviousTag }}...{{ .Tag }}) 33 | changelog: 34 | use: github 35 | sort: asc 36 | filters: 37 | exclude: 38 | - Merge pull request 39 | - Merge remote-tracking branch 40 | - Merge branch 41 | 42 | # Group commits messages by given regex and title. 43 | # Order value defines the order of the groups. 44 | # Proving no regex means all commits will be grouped under the default group. 45 | # Groups are disabled when using github-native, as it already groups things by itself. 46 | # Matches are performed against strings of the form: " ". 47 | # Regex use RE2 syntax as defined here: https://github.com/google/re2/wiki/Syntax. 48 | # 49 | # Default is no groups. 50 | groups: 51 | - title: Features 52 | regexp: '^.*?(feat|feature)(\([[:word:]]+\))??!?:.+$' 53 | order: 0 54 | - title: 'Bug fixes' 55 | regexp: '^.*?fix(\([[:word:]]+\))??!?:.+$' 56 | order: 1 57 | - title: 'Chores' 58 | regexp: '^.*?chore(\([[:word:]]+\))??!?:.+$' 59 | order: 2 60 | - title: 'Quality' 61 | regexp: '^.*?(qa|test|tests)(\([[:word:]]+\))??!?:.+$' 62 | order: 3 63 | - title: 'Documentation' 64 | regexp: '^.*?(doc|docs)(\([[:word:]]+\))??!?:.+$' 65 | order: 4 66 | - title: 'Continuous Integration' 67 | regexp: '^.*?ci(\([[:word:]]+\))??!?:.+$' 68 | order: 5 69 | - title: Other 70 | order: 999 71 | -------------------------------------------------------------------------------- /DESIGN.markdown: -------------------------------------------------------------------------------- 1 | * Designate the filename of "anonymous" source code by the hash (md5/sha1, etc.) 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Robert Krimen 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /ast/comments_test.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/robertkrimen/otto/file" 7 | ) 8 | 9 | func TestCommentMap(t *testing.T) { 10 | statement := &EmptyStatement{file.Idx(1)} 11 | comment := &Comment{Begin: 1, Text: "test", Position: LEADING} 12 | 13 | cm := CommentMap{} 14 | cm.AddComment(statement, comment) 15 | 16 | if cm.Size() != 1 { 17 | t.Errorf("the number of comments is %v, not 1", cm.Size()) 18 | } 19 | 20 | if len(cm[statement]) != 1 { 21 | t.Errorf("the number of comments is %v, not 1", cm.Size()) 22 | } 23 | 24 | if cm[statement][0].Text != "test" { 25 | t.Errorf("the text is %v, not \"test\"", cm[statement][0].Text) 26 | } 27 | } 28 | 29 | func TestCommentMap_move(t *testing.T) { 30 | statement1 := &EmptyStatement{file.Idx(1)} 31 | statement2 := &EmptyStatement{file.Idx(2)} 32 | comment := &Comment{Begin: 1, Text: "test", Position: LEADING} 33 | 34 | cm := CommentMap{} 35 | cm.AddComment(statement1, comment) 36 | 37 | if cm.Size() != 1 { 38 | t.Errorf("the number of comments is %v, not 1", cm.Size()) 39 | } 40 | 41 | if len(cm[statement1]) != 1 { 42 | t.Errorf("the number of comments is %v, not 1", cm.Size()) 43 | } 44 | 45 | if len(cm[statement2]) != 0 { 46 | t.Errorf("the number of comments is %v, not 0", cm.Size()) 47 | } 48 | 49 | cm.MoveComments(statement1, statement2, LEADING) 50 | 51 | if cm.Size() != 1 { 52 | t.Errorf("the number of comments is %v, not 1", cm.Size()) 53 | } 54 | 55 | if len(cm[statement2]) != 1 { 56 | t.Errorf("the number of comments is %v, not 1", cm.Size()) 57 | } 58 | 59 | if len(cm[statement1]) != 0 { 60 | t.Errorf("the number of comments is %v, not 0", cm.Size()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ast/walk.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import "fmt" 4 | 5 | // Visitor Enter method is invoked for each node encountered by Walk. 6 | // If the result visitor w is not nil, Walk visits each of the children 7 | // of node with the visitor v, followed by a call of the Exit method. 8 | type Visitor interface { 9 | Enter(n Node) (v Visitor) 10 | Exit(n Node) 11 | } 12 | 13 | // Walk traverses an AST in depth-first order: It starts by calling 14 | // v.Enter(node); node must not be nil. If the visitor v returned by 15 | // v.Enter(node) is not nil, Walk is invoked recursively with visitor 16 | // v for each of the non-nil children of node, followed by a call 17 | // of v.Exit(node). 18 | func Walk(v Visitor, n Node) { 19 | if n == nil { 20 | return 21 | } 22 | if v = v.Enter(n); v == nil { 23 | return 24 | } 25 | 26 | defer v.Exit(n) 27 | 28 | switch n := n.(type) { 29 | case *ArrayLiteral: 30 | if n != nil { 31 | for _, ex := range n.Value { 32 | Walk(v, ex) 33 | } 34 | } 35 | case *AssignExpression: 36 | if n != nil { 37 | Walk(v, n.Left) 38 | Walk(v, n.Right) 39 | } 40 | case *BadExpression: 41 | case *BadStatement: 42 | case *BinaryExpression: 43 | if n != nil { 44 | Walk(v, n.Left) 45 | Walk(v, n.Right) 46 | } 47 | case *BlockStatement: 48 | if n != nil { 49 | for _, s := range n.List { 50 | Walk(v, s) 51 | } 52 | } 53 | case *BooleanLiteral: 54 | case *BracketExpression: 55 | if n != nil { 56 | Walk(v, n.Left) 57 | Walk(v, n.Member) 58 | } 59 | case *BranchStatement: 60 | if n != nil { 61 | Walk(v, n.Label) 62 | } 63 | case *CallExpression: 64 | if n != nil { 65 | Walk(v, n.Callee) 66 | for _, a := range n.ArgumentList { 67 | Walk(v, a) 68 | } 69 | } 70 | case *CaseStatement: 71 | if n != nil { 72 | Walk(v, n.Test) 73 | for _, c := range n.Consequent { 74 | Walk(v, c) 75 | } 76 | } 77 | case *CatchStatement: 78 | if n != nil { 79 | Walk(v, n.Parameter) 80 | Walk(v, n.Body) 81 | } 82 | case *ConditionalExpression: 83 | if n != nil { 84 | Walk(v, n.Test) 85 | Walk(v, n.Consequent) 86 | Walk(v, n.Alternate) 87 | } 88 | case *DebuggerStatement: 89 | case *DoWhileStatement: 90 | if n != nil { 91 | Walk(v, n.Test) 92 | Walk(v, n.Body) 93 | } 94 | case *DotExpression: 95 | if n != nil { 96 | Walk(v, n.Left) 97 | Walk(v, n.Identifier) 98 | } 99 | case *EmptyExpression: 100 | case *EmptyStatement: 101 | case *ExpressionStatement: 102 | if n != nil { 103 | Walk(v, n.Expression) 104 | } 105 | case *ForInStatement: 106 | if n != nil { 107 | Walk(v, n.Into) 108 | Walk(v, n.Source) 109 | Walk(v, n.Body) 110 | } 111 | case *ForStatement: 112 | if n != nil { 113 | Walk(v, n.Initializer) 114 | Walk(v, n.Update) 115 | Walk(v, n.Test) 116 | Walk(v, n.Body) 117 | } 118 | case *FunctionLiteral: 119 | if n != nil { 120 | Walk(v, n.Name) 121 | for _, p := range n.ParameterList.List { 122 | Walk(v, p) 123 | } 124 | Walk(v, n.Body) 125 | } 126 | case *FunctionStatement: 127 | if n != nil { 128 | Walk(v, n.Function) 129 | } 130 | case *Identifier: 131 | case *IfStatement: 132 | if n != nil { 133 | Walk(v, n.Test) 134 | Walk(v, n.Consequent) 135 | Walk(v, n.Alternate) 136 | } 137 | case *LabelledStatement: 138 | if n != nil { 139 | Walk(v, n.Label) 140 | Walk(v, n.Statement) 141 | } 142 | case *NewExpression: 143 | if n != nil { 144 | Walk(v, n.Callee) 145 | for _, a := range n.ArgumentList { 146 | Walk(v, a) 147 | } 148 | } 149 | case *NullLiteral: 150 | case *NumberLiteral: 151 | case *ObjectLiteral: 152 | if n != nil { 153 | for _, p := range n.Value { 154 | Walk(v, p.Value) 155 | } 156 | } 157 | case *Program: 158 | if n != nil { 159 | for _, b := range n.Body { 160 | Walk(v, b) 161 | } 162 | } 163 | case *RegExpLiteral: 164 | case *ReturnStatement: 165 | if n != nil { 166 | Walk(v, n.Argument) 167 | } 168 | case *SequenceExpression: 169 | if n != nil { 170 | for _, e := range n.Sequence { 171 | Walk(v, e) 172 | } 173 | } 174 | case *StringLiteral: 175 | case *SwitchStatement: 176 | if n != nil { 177 | Walk(v, n.Discriminant) 178 | for _, c := range n.Body { 179 | Walk(v, c) 180 | } 181 | } 182 | case *ThisExpression: 183 | case *ThrowStatement: 184 | if n != nil { 185 | Walk(v, n.Argument) 186 | } 187 | case *TryStatement: 188 | if n != nil { 189 | Walk(v, n.Body) 190 | Walk(v, n.Catch) 191 | Walk(v, n.Finally) 192 | } 193 | case *UnaryExpression: 194 | if n != nil { 195 | Walk(v, n.Operand) 196 | } 197 | case *VariableExpression: 198 | if n != nil { 199 | Walk(v, n.Initializer) 200 | } 201 | case *VariableStatement: 202 | if n != nil { 203 | for _, e := range n.List { 204 | Walk(v, e) 205 | } 206 | } 207 | case *WhileStatement: 208 | if n != nil { 209 | Walk(v, n.Test) 210 | Walk(v, n.Body) 211 | } 212 | case *WithStatement: 213 | if n != nil { 214 | Walk(v, n.Object) 215 | Walk(v, n.Body) 216 | } 217 | default: 218 | panic(fmt.Sprintf("Walk: unexpected node type %T", n)) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /ast/walk_example_test.go: -------------------------------------------------------------------------------- 1 | package ast_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/robertkrimen/otto/ast" 8 | "github.com/robertkrimen/otto/file" 9 | "github.com/robertkrimen/otto/parser" 10 | ) 11 | 12 | type walkExample struct { 13 | source string 14 | shift file.Idx 15 | } 16 | 17 | func (w *walkExample) Enter(n ast.Node) ast.Visitor { 18 | if id, ok := n.(*ast.Identifier); ok && id != nil { 19 | idx := n.Idx0() + w.shift - 1 20 | s := w.source[:idx] + "new_" + w.source[idx:] 21 | w.source = s 22 | w.shift += 4 23 | } 24 | if v, ok := n.(*ast.VariableExpression); ok && v != nil { 25 | idx := n.Idx0() + w.shift - 1 26 | s := w.source[:idx] + "varnew_" + w.source[idx:] 27 | w.source = s 28 | w.shift += 7 29 | } 30 | 31 | return w 32 | } 33 | 34 | func (w *walkExample) Exit(n ast.Node) { 35 | // AST node n has had all its children walked. Pop it out of your 36 | // stack, or do whatever processing you need to do, if any. 37 | } 38 | 39 | func ExampleVisitor_codeRewrite() { 40 | source := `var b = function() {test(); try {} catch(e) {} var test = "test(); var test = 1"} // test` 41 | program, err := parser.ParseFile(nil, "", source, 0) 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | w := &walkExample{source: source} 47 | 48 | ast.Walk(w, program) 49 | 50 | fmt.Println(w.source) 51 | // Output: var varnew_b = function() {new_test(); try {} catch(new_e) {} var varnew_test = "test(); var test = 1"} // test 52 | } 53 | -------------------------------------------------------------------------------- /ast/walk_test.go: -------------------------------------------------------------------------------- 1 | package ast_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/robertkrimen/otto/ast" 7 | "github.com/robertkrimen/otto/file" 8 | "github.com/robertkrimen/otto/parser" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | type walker struct { 13 | seen map[ast.Node]struct{} 14 | source string 15 | stack []ast.Node 16 | shift file.Idx 17 | duplicate int 18 | newExpressionIdx1 file.Idx 19 | } 20 | 21 | // push and pop below are to prove the symmetry of Enter/Exit calls 22 | 23 | func (w *walker) push(n ast.Node) { 24 | w.stack = append(w.stack, n) 25 | } 26 | 27 | func (w *walker) pop(n ast.Node) { 28 | size := len(w.stack) 29 | if size <= 0 { 30 | panic("pop of empty stack") 31 | } 32 | 33 | if toPop := w.stack[size-1]; toPop != n { 34 | panic("pop: nodes do not equal") 35 | } 36 | 37 | w.stack[size-1] = nil 38 | w.stack = w.stack[:size-1] 39 | } 40 | 41 | func (w *walker) Enter(n ast.Node) ast.Visitor { 42 | w.push(n) 43 | if _, ok := w.seen[n]; ok { 44 | // Skip items we've already seen which occurs due to declarations. 45 | w.duplicate++ 46 | return w 47 | } 48 | 49 | w.seen[n] = struct{}{} 50 | 51 | switch t := n.(type) { 52 | case *ast.Identifier: 53 | if t != nil { 54 | idx := n.Idx0() + w.shift - 1 55 | s := w.source[:idx] + "IDENT_" + w.source[idx:] 56 | w.source = s 57 | w.shift += 6 58 | } 59 | case *ast.VariableExpression: 60 | if t != nil { 61 | idx := n.Idx0() + w.shift - 1 62 | s := w.source[:idx] + "VAR_" + w.source[idx:] 63 | w.source = s 64 | w.shift += 4 65 | } 66 | case *ast.NewExpression: 67 | w.newExpressionIdx1 = n.Idx1() 68 | } 69 | 70 | return w 71 | } 72 | 73 | func (w *walker) Exit(n ast.Node) { 74 | w.pop(n) 75 | } 76 | 77 | func TestVisitorRewrite(t *testing.T) { 78 | source := `var b = function() { 79 | test(); 80 | try {} catch(e) {} 81 | var test = "test(); var test = 1" 82 | } // test` 83 | program, err := parser.ParseFile(nil, "", source, 0) 84 | require.NoError(t, err) 85 | 86 | w := &walker{ 87 | source: source, 88 | seen: make(map[ast.Node]struct{}), 89 | } 90 | ast.Walk(w, program) 91 | 92 | xformed := `var VAR_b = function() { 93 | IDENT_test(); 94 | try {} catch(IDENT_e) {} 95 | var VAR_test = "test(); var test = 1" 96 | } // test` 97 | 98 | require.Equal(t, xformed, w.source) 99 | require.Empty(t, w.stack) 100 | require.Zero(t, w.duplicate) 101 | } 102 | 103 | func Test_issue261(t *testing.T) { 104 | tests := map[string]struct { 105 | code string 106 | want file.Idx 107 | }{ 108 | "no-parenthesis": { 109 | code: `var i = new Image;`, 110 | want: 18, 111 | }, 112 | "no-args": { 113 | code: `var i = new Image();`, 114 | want: 20, 115 | }, 116 | "two-args": { 117 | code: `var i = new Image(1, 2);`, 118 | want: 24, 119 | }, 120 | } 121 | 122 | for name, tt := range tests { 123 | t.Run(name, func(t *testing.T) { 124 | prog, err := parser.ParseFile(nil, "", tt.code, 0) 125 | require.NoError(t, err) 126 | 127 | w := &walker{ 128 | source: tt.code, 129 | seen: make(map[ast.Node]struct{}), 130 | } 131 | ast.Walk(w, prog) 132 | 133 | require.Equal(t, tt.want, w.newExpressionIdx1) 134 | require.Empty(t, w.stack) 135 | require.Zero(t, w.duplicate) 136 | }) 137 | } 138 | } 139 | 140 | func TestBadStatement(t *testing.T) { 141 | source := ` 142 | var abc; 143 | break; do { 144 | } while(true); 145 | ` 146 | program, err := parser.ParseFile(nil, "", source, 0) 147 | require.ErrorContains(t, err, "Illegal break statement") 148 | 149 | w := &walker{ 150 | source: source, 151 | seen: make(map[ast.Node]struct{}), 152 | } 153 | 154 | require.NotPanics(t, func() { 155 | ast.Walk(w, program) 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /builtin_boolean.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | // Boolean 4 | 5 | func builtinBoolean(call FunctionCall) Value { 6 | return boolValue(call.Argument(0).bool()) 7 | } 8 | 9 | func builtinNewBoolean(obj *object, argumentList []Value) Value { 10 | return objectValue(obj.runtime.newBoolean(valueOfArrayIndex(argumentList, 0))) 11 | } 12 | 13 | func builtinBooleanToString(call FunctionCall) Value { 14 | value := call.This 15 | if !value.IsBoolean() { 16 | // Will throw a TypeError if ThisObject is not a Boolean 17 | value = call.thisClassObject(classBooleanName).primitiveValue() 18 | } 19 | return stringValue(value.string()) 20 | } 21 | 22 | func builtinBooleanValueOf(call FunctionCall) Value { 23 | value := call.This 24 | if !value.IsBoolean() { 25 | value = call.thisClassObject(classBooleanName).primitiveValue() 26 | } 27 | return value 28 | } 29 | -------------------------------------------------------------------------------- /builtin_error.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func builtinError(call FunctionCall) Value { 8 | return objectValue(call.runtime.newError(classErrorName, call.Argument(0), 1)) 9 | } 10 | 11 | func builtinNewError(obj *object, argumentList []Value) Value { 12 | return objectValue(obj.runtime.newError(classErrorName, valueOfArrayIndex(argumentList, 0), 0)) 13 | } 14 | 15 | func builtinErrorToString(call FunctionCall) Value { 16 | thisObject := call.thisObject() 17 | if thisObject == nil { 18 | panic(call.runtime.panicTypeError("Error.toString is nil")) 19 | } 20 | 21 | name := classErrorName 22 | nameValue := thisObject.get("name") 23 | if nameValue.IsDefined() { 24 | name = nameValue.string() 25 | } 26 | 27 | message := "" 28 | messageValue := thisObject.get("message") 29 | if messageValue.IsDefined() { 30 | message = messageValue.string() 31 | } 32 | 33 | if len(name) == 0 { 34 | return stringValue(message) 35 | } 36 | 37 | if len(message) == 0 { 38 | return stringValue(name) 39 | } 40 | 41 | return stringValue(fmt.Sprintf("%s: %s", name, message)) 42 | } 43 | 44 | func (rt *runtime) newEvalError(message Value) *object { 45 | o := rt.newErrorObject("EvalError", message, 0) 46 | o.prototype = rt.global.EvalErrorPrototype 47 | return o 48 | } 49 | 50 | func builtinEvalError(call FunctionCall) Value { 51 | return objectValue(call.runtime.newEvalError(call.Argument(0))) 52 | } 53 | 54 | func builtinNewEvalError(obj *object, argumentList []Value) Value { 55 | return objectValue(obj.runtime.newEvalError(valueOfArrayIndex(argumentList, 0))) 56 | } 57 | 58 | func (rt *runtime) newTypeError(message Value) *object { 59 | o := rt.newErrorObject("TypeError", message, 0) 60 | o.prototype = rt.global.TypeErrorPrototype 61 | return o 62 | } 63 | 64 | func builtinTypeError(call FunctionCall) Value { 65 | return objectValue(call.runtime.newTypeError(call.Argument(0))) 66 | } 67 | 68 | func builtinNewTypeError(obj *object, argumentList []Value) Value { 69 | return objectValue(obj.runtime.newTypeError(valueOfArrayIndex(argumentList, 0))) 70 | } 71 | 72 | func (rt *runtime) newRangeError(message Value) *object { 73 | o := rt.newErrorObject("RangeError", message, 0) 74 | o.prototype = rt.global.RangeErrorPrototype 75 | return o 76 | } 77 | 78 | func builtinRangeError(call FunctionCall) Value { 79 | return objectValue(call.runtime.newRangeError(call.Argument(0))) 80 | } 81 | 82 | func builtinNewRangeError(obj *object, argumentList []Value) Value { 83 | return objectValue(obj.runtime.newRangeError(valueOfArrayIndex(argumentList, 0))) 84 | } 85 | 86 | func (rt *runtime) newURIError(message Value) *object { 87 | o := rt.newErrorObject("URIError", message, 0) 88 | o.prototype = rt.global.URIErrorPrototype 89 | return o 90 | } 91 | 92 | func (rt *runtime) newReferenceError(message Value) *object { 93 | o := rt.newErrorObject("ReferenceError", message, 0) 94 | o.prototype = rt.global.ReferenceErrorPrototype 95 | return o 96 | } 97 | 98 | func builtinReferenceError(call FunctionCall) Value { 99 | return objectValue(call.runtime.newReferenceError(call.Argument(0))) 100 | } 101 | 102 | func builtinNewReferenceError(obj *object, argumentList []Value) Value { 103 | return objectValue(obj.runtime.newReferenceError(valueOfArrayIndex(argumentList, 0))) 104 | } 105 | 106 | func (rt *runtime) newSyntaxError(message Value) *object { 107 | o := rt.newErrorObject("SyntaxError", message, 0) 108 | o.prototype = rt.global.SyntaxErrorPrototype 109 | return o 110 | } 111 | 112 | func builtinSyntaxError(call FunctionCall) Value { 113 | return objectValue(call.runtime.newSyntaxError(call.Argument(0))) 114 | } 115 | 116 | func builtinNewSyntaxError(obj *object, argumentList []Value) Value { 117 | return objectValue(obj.runtime.newSyntaxError(valueOfArrayIndex(argumentList, 0))) 118 | } 119 | 120 | func builtinURIError(call FunctionCall) Value { 121 | return objectValue(call.runtime.newURIError(call.Argument(0))) 122 | } 123 | 124 | func builtinNewURIError(obj *object, argumentList []Value) Value { 125 | return objectValue(obj.runtime.newURIError(valueOfArrayIndex(argumentList, 0))) 126 | } 127 | -------------------------------------------------------------------------------- /builtin_function.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | 8 | "github.com/robertkrimen/otto/parser" 9 | ) 10 | 11 | // Function 12 | 13 | func builtinFunction(call FunctionCall) Value { 14 | return objectValue(builtinNewFunctionNative(call.runtime, call.ArgumentList)) 15 | } 16 | 17 | func builtinNewFunction(obj *object, argumentList []Value) Value { 18 | return objectValue(builtinNewFunctionNative(obj.runtime, argumentList)) 19 | } 20 | 21 | func argumentList2parameterList(argumentList []Value) []string { 22 | parameterList := make([]string, 0, len(argumentList)) 23 | for _, value := range argumentList { 24 | tmp := strings.FieldsFunc(value.string(), func(chr rune) bool { 25 | return chr == ',' || unicode.IsSpace(chr) 26 | }) 27 | parameterList = append(parameterList, tmp...) 28 | } 29 | return parameterList 30 | } 31 | 32 | func builtinNewFunctionNative(rt *runtime, argumentList []Value) *object { 33 | var parameterList, body string 34 | if count := len(argumentList); count > 0 { 35 | tmp := make([]string, 0, count-1) 36 | for _, value := range argumentList[0 : count-1] { 37 | tmp = append(tmp, value.string()) 38 | } 39 | parameterList = strings.Join(tmp, ",") 40 | body = argumentList[count-1].string() 41 | } 42 | 43 | // FIXME 44 | function, err := parser.ParseFunction(parameterList, body) 45 | rt.parseThrow(err) // Will panic/throw appropriately 46 | cmpl := compiler{} 47 | cmplFunction := cmpl.parseExpression(function) 48 | 49 | return rt.newNodeFunction(cmplFunction.(*nodeFunctionLiteral), rt.globalStash) 50 | } 51 | 52 | func builtinFunctionToString(call FunctionCall) Value { 53 | obj := call.thisClassObject(classFunctionName) // Should throw a TypeError unless Function 54 | switch fn := obj.value.(type) { 55 | case nativeFunctionObject: 56 | return stringValue(fmt.Sprintf("function %s() { [native code] }", fn.name)) 57 | case nodeFunctionObject: 58 | return stringValue(fn.node.source) 59 | case bindFunctionObject: 60 | return stringValue("function () { [native code] }") 61 | default: 62 | panic(call.runtime.panicTypeError("Function.toString unknown type %T", obj.value)) 63 | } 64 | } 65 | 66 | func builtinFunctionApply(call FunctionCall) Value { 67 | if !call.This.isCallable() { 68 | panic(call.runtime.panicTypeError("Function.apply %q is not callable", call.This)) 69 | } 70 | this := call.Argument(0) 71 | if this.IsUndefined() { 72 | // FIXME Not ECMA5 73 | this = objectValue(call.runtime.globalObject) 74 | } 75 | argumentList := call.Argument(1) 76 | switch argumentList.kind { 77 | case valueUndefined, valueNull: 78 | return call.thisObject().call(this, nil, false, nativeFrame) 79 | case valueObject: 80 | default: 81 | panic(call.runtime.panicTypeError("Function.apply unknown type %T for second argument")) 82 | } 83 | 84 | arrayObject := argumentList.object() 85 | thisObject := call.thisObject() 86 | length := int64(toUint32(arrayObject.get(propertyLength))) 87 | valueArray := make([]Value, length) 88 | for index := range length { 89 | valueArray[index] = arrayObject.get(arrayIndexToString(index)) 90 | } 91 | return thisObject.call(this, valueArray, false, nativeFrame) 92 | } 93 | 94 | func builtinFunctionCall(call FunctionCall) Value { 95 | if !call.This.isCallable() { 96 | panic(call.runtime.panicTypeError("Function.call %q is not callable", call.This)) 97 | } 98 | thisObject := call.thisObject() 99 | this := call.Argument(0) 100 | if this.IsUndefined() { 101 | // FIXME Not ECMA5 102 | this = objectValue(call.runtime.globalObject) 103 | } 104 | if len(call.ArgumentList) >= 1 { 105 | return thisObject.call(this, call.ArgumentList[1:], false, nativeFrame) 106 | } 107 | return thisObject.call(this, nil, false, nativeFrame) 108 | } 109 | 110 | func builtinFunctionBind(call FunctionCall) Value { 111 | target := call.This 112 | if !target.isCallable() { 113 | panic(call.runtime.panicTypeError("Function.bind %q is not callable", call.This)) 114 | } 115 | targetObject := target.object() 116 | 117 | this := call.Argument(0) 118 | argumentList := call.slice(1) 119 | if this.IsUndefined() { 120 | // FIXME Do this elsewhere? 121 | this = objectValue(call.runtime.globalObject) 122 | } 123 | 124 | return objectValue(call.runtime.newBoundFunction(targetObject, this, argumentList)) 125 | } 126 | -------------------------------------------------------------------------------- /builtin_math.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | ) 7 | 8 | // Math 9 | 10 | func builtinMathAbs(call FunctionCall) Value { 11 | number := call.Argument(0).float64() 12 | return float64Value(math.Abs(number)) 13 | } 14 | 15 | func builtinMathAcos(call FunctionCall) Value { 16 | number := call.Argument(0).float64() 17 | return float64Value(math.Acos(number)) 18 | } 19 | 20 | func builtinMathAcosh(call FunctionCall) Value { 21 | number := call.Argument(0).float64() 22 | return float64Value(math.Acosh(number)) 23 | } 24 | 25 | func builtinMathAsin(call FunctionCall) Value { 26 | number := call.Argument(0).float64() 27 | return float64Value(math.Asin(number)) 28 | } 29 | 30 | func builtinMathAsinh(call FunctionCall) Value { 31 | number := call.Argument(0).float64() 32 | return float64Value(math.Asinh(number)) 33 | } 34 | 35 | func builtinMathAtan(call FunctionCall) Value { 36 | number := call.Argument(0).float64() 37 | return float64Value(math.Atan(number)) 38 | } 39 | 40 | func builtinMathAtan2(call FunctionCall) Value { 41 | y := call.Argument(0).float64() 42 | if math.IsNaN(y) { 43 | return NaNValue() 44 | } 45 | x := call.Argument(1).float64() 46 | if math.IsNaN(x) { 47 | return NaNValue() 48 | } 49 | return float64Value(math.Atan2(y, x)) 50 | } 51 | 52 | func builtinMathAtanh(call FunctionCall) Value { 53 | number := call.Argument(0).float64() 54 | return float64Value(math.Atanh(number)) 55 | } 56 | 57 | func builtinMathCbrt(call FunctionCall) Value { 58 | number := call.Argument(0).float64() 59 | return float64Value(math.Cbrt(number)) 60 | } 61 | 62 | func builtinMathCos(call FunctionCall) Value { 63 | number := call.Argument(0).float64() 64 | return float64Value(math.Cos(number)) 65 | } 66 | 67 | func builtinMathCeil(call FunctionCall) Value { 68 | number := call.Argument(0).float64() 69 | return float64Value(math.Ceil(number)) 70 | } 71 | 72 | func builtinMathCosh(call FunctionCall) Value { 73 | number := call.Argument(0).float64() 74 | return float64Value(math.Cosh(number)) 75 | } 76 | 77 | func builtinMathExp(call FunctionCall) Value { 78 | number := call.Argument(0).float64() 79 | return float64Value(math.Exp(number)) 80 | } 81 | 82 | func builtinMathExpm1(call FunctionCall) Value { 83 | number := call.Argument(0).float64() 84 | return float64Value(math.Expm1(number)) 85 | } 86 | 87 | func builtinMathFloor(call FunctionCall) Value { 88 | number := call.Argument(0).float64() 89 | return float64Value(math.Floor(number)) 90 | } 91 | 92 | func builtinMathLog(call FunctionCall) Value { 93 | number := call.Argument(0).float64() 94 | return float64Value(math.Log(number)) 95 | } 96 | 97 | func builtinMathLog10(call FunctionCall) Value { 98 | number := call.Argument(0).float64() 99 | return float64Value(math.Log10(number)) 100 | } 101 | 102 | func builtinMathLog1p(call FunctionCall) Value { 103 | number := call.Argument(0).float64() 104 | return float64Value(math.Log1p(number)) 105 | } 106 | 107 | func builtinMathLog2(call FunctionCall) Value { 108 | number := call.Argument(0).float64() 109 | return float64Value(math.Log2(number)) 110 | } 111 | 112 | func builtinMathMax(call FunctionCall) Value { 113 | switch len(call.ArgumentList) { 114 | case 0: 115 | return negativeInfinityValue() 116 | case 1: 117 | return float64Value(call.ArgumentList[0].float64()) 118 | } 119 | result := call.ArgumentList[0].float64() 120 | if math.IsNaN(result) { 121 | return NaNValue() 122 | } 123 | for _, value := range call.ArgumentList[1:] { 124 | value := value.float64() 125 | if math.IsNaN(value) { 126 | return NaNValue() 127 | } 128 | result = math.Max(result, value) 129 | } 130 | return float64Value(result) 131 | } 132 | 133 | func builtinMathMin(call FunctionCall) Value { 134 | switch len(call.ArgumentList) { 135 | case 0: 136 | return positiveInfinityValue() 137 | case 1: 138 | return float64Value(call.ArgumentList[0].float64()) 139 | } 140 | result := call.ArgumentList[0].float64() 141 | if math.IsNaN(result) { 142 | return NaNValue() 143 | } 144 | for _, value := range call.ArgumentList[1:] { 145 | value := value.float64() 146 | if math.IsNaN(value) { 147 | return NaNValue() 148 | } 149 | result = math.Min(result, value) 150 | } 151 | return float64Value(result) 152 | } 153 | 154 | func builtinMathPow(call FunctionCall) Value { 155 | // TODO Make sure this works according to the specification (15.8.2.13) 156 | x := call.Argument(0).float64() 157 | y := call.Argument(1).float64() 158 | if math.Abs(x) == 1 && math.IsInf(y, 0) { 159 | return NaNValue() 160 | } 161 | return float64Value(math.Pow(x, y)) 162 | } 163 | 164 | func builtinMathRandom(call FunctionCall) Value { 165 | var v float64 166 | if call.runtime.random != nil { 167 | v = call.runtime.random() 168 | } else { 169 | v = rand.Float64() //nolint:gosec 170 | } 171 | return float64Value(v) 172 | } 173 | 174 | func builtinMathRound(call FunctionCall) Value { 175 | number := call.Argument(0).float64() 176 | value := math.Floor(number + 0.5) 177 | if value == 0 { 178 | value = math.Copysign(0, number) 179 | } 180 | return float64Value(value) 181 | } 182 | 183 | func builtinMathSin(call FunctionCall) Value { 184 | number := call.Argument(0).float64() 185 | return float64Value(math.Sin(number)) 186 | } 187 | 188 | func builtinMathSinh(call FunctionCall) Value { 189 | number := call.Argument(0).float64() 190 | return float64Value(math.Sinh(number)) 191 | } 192 | 193 | func builtinMathSqrt(call FunctionCall) Value { 194 | number := call.Argument(0).float64() 195 | return float64Value(math.Sqrt(number)) 196 | } 197 | 198 | func builtinMathTan(call FunctionCall) Value { 199 | number := call.Argument(0).float64() 200 | return float64Value(math.Tan(number)) 201 | } 202 | 203 | func builtinMathTanh(call FunctionCall) Value { 204 | number := call.Argument(0).float64() 205 | return float64Value(math.Tanh(number)) 206 | } 207 | 208 | func builtinMathTrunc(call FunctionCall) Value { 209 | number := call.Argument(0).float64() 210 | return float64Value(math.Trunc(number)) 211 | } 212 | -------------------------------------------------------------------------------- /builtin_number.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | 7 | "golang.org/x/text/language" 8 | "golang.org/x/text/message" 9 | "golang.org/x/text/number" 10 | ) 11 | 12 | // Number 13 | 14 | func numberValueFromNumberArgumentList(argumentList []Value) Value { 15 | if len(argumentList) > 0 { 16 | return argumentList[0].numberValue() 17 | } 18 | return intValue(0) 19 | } 20 | 21 | func builtinNumber(call FunctionCall) Value { 22 | return numberValueFromNumberArgumentList(call.ArgumentList) 23 | } 24 | 25 | func builtinNewNumber(obj *object, argumentList []Value) Value { 26 | return objectValue(obj.runtime.newNumber(numberValueFromNumberArgumentList(argumentList))) 27 | } 28 | 29 | func builtinNumberToString(call FunctionCall) Value { 30 | // Will throw a TypeError if ThisObject is not a Number 31 | value := call.thisClassObject(classNumberName).primitiveValue() 32 | radix := 10 33 | radixArgument := call.Argument(0) 34 | if radixArgument.IsDefined() { 35 | integer := toIntegerFloat(radixArgument) 36 | if integer < 2 || integer > 36 { 37 | panic(call.runtime.panicRangeError("toString() radix must be between 2 and 36")) 38 | } 39 | radix = int(integer) 40 | } 41 | if radix == 10 { 42 | return stringValue(value.string()) 43 | } 44 | return stringValue(numberToStringRadix(value, radix)) 45 | } 46 | 47 | func builtinNumberValueOf(call FunctionCall) Value { 48 | return call.thisClassObject(classNumberName).primitiveValue() 49 | } 50 | 51 | func builtinNumberToFixed(call FunctionCall) Value { 52 | precision := toIntegerFloat(call.Argument(0)) 53 | if 20 < precision || 0 > precision { 54 | panic(call.runtime.panicRangeError("toFixed() precision must be between 0 and 20")) 55 | } 56 | if call.This.IsNaN() { 57 | return stringValue("NaN") 58 | } 59 | if value := call.This.float64(); math.Abs(value) >= 1e21 { 60 | return stringValue(floatToString(value, 64)) 61 | } 62 | return stringValue(strconv.FormatFloat(call.This.float64(), 'f', int(precision), 64)) 63 | } 64 | 65 | func builtinNumberToExponential(call FunctionCall) Value { 66 | if call.This.IsNaN() { 67 | return stringValue("NaN") 68 | } 69 | precision := float64(-1) 70 | if value := call.Argument(0); value.IsDefined() { 71 | precision = toIntegerFloat(value) 72 | if 0 > precision { 73 | panic(call.runtime.panicRangeError("toString() radix must be between 2 and 36")) 74 | } 75 | } 76 | return stringValue(strconv.FormatFloat(call.This.float64(), 'e', int(precision), 64)) 77 | } 78 | 79 | func builtinNumberToPrecision(call FunctionCall) Value { 80 | if call.This.IsNaN() { 81 | return stringValue("NaN") 82 | } 83 | value := call.Argument(0) 84 | if value.IsUndefined() { 85 | return stringValue(call.This.string()) 86 | } 87 | precision := toIntegerFloat(value) 88 | if 1 > precision { 89 | panic(call.runtime.panicRangeError("toPrecision() precision must be greater than 1")) 90 | } 91 | return stringValue(strconv.FormatFloat(call.This.float64(), 'g', int(precision), 64)) 92 | } 93 | 94 | func builtinNumberIsNaN(call FunctionCall) Value { 95 | if len(call.ArgumentList) < 1 { 96 | return boolValue(false) 97 | } 98 | return boolValue(call.Argument(0).IsNaN()) 99 | } 100 | 101 | func builtinNumberToLocaleString(call FunctionCall) Value { 102 | value := call.thisClassObject(classNumberName).primitiveValue() 103 | locale := call.Argument(0) 104 | lang := defaultLanguage 105 | if locale.IsDefined() { 106 | lang = language.MustParse(locale.string()) 107 | } 108 | 109 | p := message.NewPrinter(lang) 110 | return stringValue(p.Sprintf("%v", number.Decimal(value.value))) 111 | } 112 | -------------------------------------------------------------------------------- /builtin_regexp.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // RegExp 8 | 9 | func builtinRegExp(call FunctionCall) Value { 10 | pattern := call.Argument(0) 11 | flags := call.Argument(1) 12 | if obj := pattern.object(); obj != nil { 13 | if obj.class == classRegExpName && flags.IsUndefined() { 14 | return pattern 15 | } 16 | } 17 | return objectValue(call.runtime.newRegExp(pattern, flags)) 18 | } 19 | 20 | func builtinNewRegExp(obj *object, argumentList []Value) Value { 21 | return objectValue(obj.runtime.newRegExp( 22 | valueOfArrayIndex(argumentList, 0), 23 | valueOfArrayIndex(argumentList, 1), 24 | )) 25 | } 26 | 27 | func builtinRegExpToString(call FunctionCall) Value { 28 | thisObject := call.thisObject() 29 | source := thisObject.get("source").string() 30 | flags := []byte{} 31 | if thisObject.get("global").bool() { 32 | flags = append(flags, 'g') 33 | } 34 | if thisObject.get("ignoreCase").bool() { 35 | flags = append(flags, 'i') 36 | } 37 | if thisObject.get("multiline").bool() { 38 | flags = append(flags, 'm') 39 | } 40 | return stringValue(fmt.Sprintf("/%s/%s", source, flags)) 41 | } 42 | 43 | func builtinRegExpExec(call FunctionCall) Value { 44 | thisObject := call.thisObject() 45 | target := call.Argument(0).string() 46 | match, result := execRegExp(thisObject, target) 47 | if !match { 48 | return nullValue 49 | } 50 | return objectValue(execResultToArray(call.runtime, target, result)) 51 | } 52 | 53 | func builtinRegExpTest(call FunctionCall) Value { 54 | thisObject := call.thisObject() 55 | target := call.Argument(0).string() 56 | match, result := execRegExp(thisObject, target) 57 | 58 | if !match { 59 | return boolValue(match) 60 | } 61 | 62 | // Match extract and assign input, $_ and $1 -> $9 on global RegExp. 63 | input := stringValue(target) 64 | call.runtime.global.RegExp.defineProperty("$_", input, 0o100, false) 65 | call.runtime.global.RegExp.defineProperty("input", input, 0o100, false) 66 | 67 | var start int 68 | n := 1 69 | re := call.runtime.global.RegExp 70 | empty := stringValue("") 71 | for i, v := range result[2:] { 72 | if i%2 == 0 { 73 | start = v 74 | } else { 75 | if v == -1 { 76 | // No match for this part. 77 | re.defineProperty(fmt.Sprintf("$%d", n), empty, 0o100, false) 78 | } else { 79 | re.defineProperty(fmt.Sprintf("$%d", n), stringValue(target[start:v]), 0o100, false) 80 | } 81 | n++ 82 | if n == 10 { 83 | break 84 | } 85 | } 86 | } 87 | 88 | if n <= 9 { 89 | // Erase remaining. 90 | for i := n; i <= 9; i++ { 91 | re.defineProperty(fmt.Sprintf("$%d", i), empty, 0o100, false) 92 | } 93 | } 94 | 95 | return boolValue(match) 96 | } 97 | 98 | func builtinRegExpCompile(call FunctionCall) Value { 99 | // This (useless) function is deprecated, but is here to provide some 100 | // semblance of compatibility. 101 | // Caveat emptor: it may not be around for long. 102 | return Value{} 103 | } 104 | -------------------------------------------------------------------------------- /builtin_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestString_substr(t *testing.T) { 8 | tt(t, func() { 9 | test, _ := test() 10 | 11 | test(` 12 | [ 13 | "uñiçode".substr(0,1), // "u" 14 | "uñiçode".substr(0,2), // "uñ" 15 | "uñiçode".substr(0,3), // "uñi" 16 | "uñiçode".substr(0,4), // "uñiç" 17 | "uñiçode".substr(0,9), // "uñiçode" 18 | ]; 19 | `, "u,uñ,uñi,uñiç,uñiçode") 20 | 21 | test(` 22 | [ 23 | "abc".substr(0,1), // "a" 24 | "abc".substr(0,2), // "ab" 25 | "abc".substr(0,3), // "abc" 26 | "abc".substr(0,4), // "abc" 27 | "abc".substr(0,9), // "abc" 28 | ]; 29 | `, "a,ab,abc,abc,abc") 30 | 31 | test(` 32 | [ 33 | "abc".substr(1,1), // "b" 34 | "abc".substr(1,2), // "bc" 35 | "abc".substr(1,3), // "bc" 36 | "abc".substr(1,4), // "bc" 37 | "abc".substr(1,9), // "bc" 38 | ]; 39 | `, "b,bc,bc,bc,bc") 40 | 41 | test(` 42 | [ 43 | "abc".substr(2,1), // "c" 44 | "abc".substr(2,2), // "c" 45 | "abc".substr(2,3), // "c" 46 | "abc".substr(2,4), // "c" 47 | "abc".substr(2,9), // "c" 48 | ]; 49 | `, "c,c,c,c,c") 50 | 51 | test(` 52 | [ 53 | "abc".substr(3,1), // "" 54 | "abc".substr(3,2), // "" 55 | "abc".substr(3,3), // "" 56 | "abc".substr(3,4), // "" 57 | "abc".substr(3,9), // "" 58 | ]; 59 | `, ",,,,") 60 | 61 | test(` 62 | [ 63 | "abc".substr(0), // "abc" 64 | "abc".substr(1), // "bc" 65 | "abc".substr(2), // "c" 66 | "abc".substr(3), // "" 67 | "abc".substr(9), // "" 68 | ]; 69 | `, "abc,bc,c,,") 70 | 71 | test(` 72 | [ 73 | "abc".substr(-9), // "abc" 74 | "abc".substr(-3), // "abc" 75 | "abc".substr(-2), // "bc" 76 | "abc".substr(-1), // "c" 77 | ]; 78 | `, "abc,abc,bc,c") 79 | 80 | test(` 81 | [ 82 | "abc".substr(-9, 1), // "a" 83 | "abc".substr(-3, 1), // "a" 84 | "abc".substr(-2, 1), // "b" 85 | "abc".substr(-1, 1), // "c" 86 | "abc".substr(-1, 2), // "c" 87 | ]; 88 | `, "a,a,b,c,c") 89 | 90 | test(`"abcd".substr(3, 5)`, "d") 91 | }) 92 | } 93 | 94 | func Test_builtin_escape(t *testing.T) { 95 | tt(t, func() { 96 | is(builtinEscape("abc"), "abc") 97 | 98 | is(builtinEscape("="), "%3D") 99 | 100 | is(builtinEscape("abc=%+32"), "abc%3D%25+32") 101 | 102 | is(builtinEscape("世界"), "%u4E16%u754C") 103 | }) 104 | } 105 | 106 | func Test_builtin_unescape(t *testing.T) { 107 | tt(t, func() { 108 | is(builtinUnescape("abc"), "abc") 109 | 110 | is(builtinUnescape("=%3D"), "==") 111 | 112 | is(builtinUnescape("abc%3D%25+32"), "abc=%+32") 113 | 114 | is(builtinUnescape("%u4E16%u754C"), "世界") 115 | }) 116 | } 117 | 118 | func TestGlobal_escape(t *testing.T) { 119 | tt(t, func() { 120 | test, _ := test() 121 | 122 | test(` 123 | [ 124 | escape("abc"), // "abc" 125 | escape("="), // "%3D" 126 | escape("abc=%+32"), // "abc%3D%25+32" 127 | escape("\u4e16\u754c"), // "%u4E16%u754C" 128 | ]; 129 | `, "abc,%3D,abc%3D%25+32,%u4E16%u754C") 130 | }) 131 | } 132 | 133 | func TestGlobal_unescape(t *testing.T) { 134 | tt(t, func() { 135 | test, _ := test() 136 | 137 | test(` 138 | [ 139 | unescape("abc"), // "abc" 140 | unescape("=%3D"), // "==" 141 | unescape("abc%3D%25+32"), // "abc=%+32" 142 | unescape("%u4E16%u754C"), // "世界" 143 | ]; 144 | `, "abc,==,abc=%+32,世界") 145 | }) 146 | } 147 | 148 | func TestNumber_isNaN(t *testing.T) { 149 | tt(t, func() { 150 | test, _ := test() 151 | test(`Number.isNaN(1)`, false) 152 | test(`Number.isNaN(null)`, false) 153 | test(`Number.isNaN()`, false) 154 | test(`Number.isNaN(Number.NaN)`, true) 155 | test(`Number.isNaN(0+undefined)`, true) 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /clone.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type cloner struct { 8 | runtime *runtime 9 | obj map[*object]*object 10 | objectstash map[*objectStash]*objectStash 11 | dclstash map[*dclStash]*dclStash 12 | fnstash map[*fnStash]*fnStash 13 | } 14 | 15 | func (rt *runtime) clone() *runtime { 16 | rt.lck.Lock() 17 | defer rt.lck.Unlock() 18 | 19 | out := &runtime{ 20 | debugger: rt.debugger, 21 | random: rt.random, 22 | stackLimit: rt.stackLimit, 23 | traceLimit: rt.traceLimit, 24 | } 25 | 26 | c := cloner{ 27 | runtime: out, 28 | obj: make(map[*object]*object), 29 | objectstash: make(map[*objectStash]*objectStash), 30 | dclstash: make(map[*dclStash]*dclStash), 31 | fnstash: make(map[*fnStash]*fnStash), 32 | } 33 | 34 | globalObject := c.object(rt.globalObject) 35 | out.globalStash = out.newObjectStash(globalObject, nil) 36 | out.globalObject = globalObject 37 | out.global = global{ 38 | c.object(rt.global.Object), 39 | c.object(rt.global.Function), 40 | c.object(rt.global.Array), 41 | c.object(rt.global.String), 42 | c.object(rt.global.Boolean), 43 | c.object(rt.global.Number), 44 | c.object(rt.global.Math), 45 | c.object(rt.global.Date), 46 | c.object(rt.global.RegExp), 47 | c.object(rt.global.Error), 48 | c.object(rt.global.EvalError), 49 | c.object(rt.global.TypeError), 50 | c.object(rt.global.RangeError), 51 | c.object(rt.global.ReferenceError), 52 | c.object(rt.global.SyntaxError), 53 | c.object(rt.global.URIError), 54 | c.object(rt.global.JSON), 55 | 56 | c.object(rt.global.ObjectPrototype), 57 | c.object(rt.global.FunctionPrototype), 58 | c.object(rt.global.ArrayPrototype), 59 | c.object(rt.global.StringPrototype), 60 | c.object(rt.global.BooleanPrototype), 61 | c.object(rt.global.NumberPrototype), 62 | c.object(rt.global.DatePrototype), 63 | c.object(rt.global.RegExpPrototype), 64 | c.object(rt.global.ErrorPrototype), 65 | c.object(rt.global.EvalErrorPrototype), 66 | c.object(rt.global.TypeErrorPrototype), 67 | c.object(rt.global.RangeErrorPrototype), 68 | c.object(rt.global.ReferenceErrorPrototype), 69 | c.object(rt.global.SyntaxErrorPrototype), 70 | c.object(rt.global.URIErrorPrototype), 71 | } 72 | 73 | out.eval = out.globalObject.property["eval"].value.(Value).value.(*object) 74 | out.globalObject.prototype = out.global.ObjectPrototype 75 | 76 | // Not sure if this is necessary, but give some help to the GC 77 | c.runtime = nil 78 | c.obj = nil 79 | c.objectstash = nil 80 | c.dclstash = nil 81 | c.fnstash = nil 82 | 83 | return out 84 | } 85 | 86 | func (c *cloner) object(in *object) *object { 87 | if out, exists := c.obj[in]; exists { 88 | return out 89 | } 90 | out := &object{} 91 | c.obj[in] = out 92 | return in.objectClass.clone(in, out, c) 93 | } 94 | 95 | func (c *cloner) dclStash(in *dclStash) (*dclStash, bool) { 96 | if out, exists := c.dclstash[in]; exists { 97 | return out, true 98 | } 99 | out := &dclStash{} 100 | c.dclstash[in] = out 101 | return out, false 102 | } 103 | 104 | func (c *cloner) objectStash(in *objectStash) (*objectStash, bool) { 105 | if out, exists := c.objectstash[in]; exists { 106 | return out, true 107 | } 108 | out := &objectStash{} 109 | c.objectstash[in] = out 110 | return out, false 111 | } 112 | 113 | func (c *cloner) fnStash(in *fnStash) (*fnStash, bool) { 114 | if out, exists := c.fnstash[in]; exists { 115 | return out, true 116 | } 117 | out := &fnStash{} 118 | c.fnstash[in] = out 119 | return out, false 120 | } 121 | 122 | func (c *cloner) value(in Value) Value { 123 | out := in 124 | if value, ok := in.value.(*object); ok { 125 | out.value = c.object(value) 126 | } 127 | return out 128 | } 129 | 130 | func (c *cloner) valueArray(in []Value) []Value { 131 | out := make([]Value, len(in)) 132 | for index, value := range in { 133 | out[index] = c.value(value) 134 | } 135 | return out 136 | } 137 | 138 | func (c *cloner) stash(in stasher) stasher { 139 | if in == nil { 140 | return nil 141 | } 142 | return in.clone(c) 143 | } 144 | 145 | func (c *cloner) property(in property) property { 146 | out := in 147 | 148 | switch value := in.value.(type) { 149 | case Value: 150 | out.value = c.value(value) 151 | case propertyGetSet: 152 | p := propertyGetSet{} 153 | if value[0] != nil { 154 | p[0] = c.object(value[0]) 155 | } 156 | if value[1] != nil { 157 | p[1] = c.object(value[1]) 158 | } 159 | out.value = p 160 | default: 161 | panic(fmt.Errorf("in.value.(Value) != true; in.value is %T", in.value)) 162 | } 163 | 164 | return out 165 | } 166 | 167 | func (c *cloner) dclProperty(in dclProperty) dclProperty { 168 | out := in 169 | out.value = c.value(in.value) 170 | return out 171 | } 172 | -------------------------------------------------------------------------------- /clone_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestCloneGetterSetter(t *testing.T) { 10 | vm := New() 11 | 12 | _, err := vm.Run(`var x = Object.create(null, { 13 | x: { 14 | get: function() {}, 15 | set: function() {}, 16 | }, 17 | })`) 18 | require.NoError(t, err) 19 | require.NotPanics(t, func() { 20 | vm.Copy() 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /cmpl.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "github.com/robertkrimen/otto/ast" 5 | "github.com/robertkrimen/otto/file" 6 | ) 7 | 8 | type compiler struct { 9 | file *file.File 10 | program *ast.Program 11 | } 12 | -------------------------------------------------------------------------------- /cmpl_evaluate.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | func (rt *runtime) cmplEvaluateNodeProgram(node *nodeProgram, eval bool) Value { 8 | if !eval { 9 | rt.enterGlobalScope() 10 | defer rt.leaveScope() 11 | } 12 | rt.cmplFunctionDeclaration(node.functionList) 13 | rt.cmplVariableDeclaration(node.varList) 14 | rt.scope.frame.file = node.file 15 | return rt.cmplEvaluateNodeStatementList(node.body) 16 | } 17 | 18 | func (rt *runtime) cmplCallNodeFunction(function *object, stash *fnStash, node *nodeFunctionLiteral, argumentList []Value) Value { 19 | indexOfParameterName := make([]string, len(argumentList)) 20 | // function(abc, def, ghi) 21 | // indexOfParameterName[0] = "abc" 22 | // indexOfParameterName[1] = "def" 23 | // indexOfParameterName[2] = "ghi" 24 | // ... 25 | 26 | argumentsFound := false 27 | for index, name := range node.parameterList { 28 | if name == "arguments" { 29 | argumentsFound = true 30 | } 31 | value := Value{} 32 | if index < len(argumentList) { 33 | value = argumentList[index] 34 | indexOfParameterName[index] = name 35 | } 36 | // strict = false 37 | rt.scope.lexical.setValue(name, value, false) 38 | } 39 | 40 | if !argumentsFound { 41 | arguments := rt.newArgumentsObject(indexOfParameterName, stash, len(argumentList)) 42 | arguments.defineProperty("callee", objectValue(function), 0o101, false) 43 | stash.arguments = arguments 44 | // strict = false 45 | rt.scope.lexical.setValue("arguments", objectValue(arguments), false) 46 | for index := range argumentList { 47 | if index < len(node.parameterList) { 48 | continue 49 | } 50 | indexAsString := strconv.FormatInt(int64(index), 10) 51 | arguments.defineProperty(indexAsString, argumentList[index], 0o111, false) 52 | } 53 | } 54 | 55 | rt.cmplFunctionDeclaration(node.functionList) 56 | rt.cmplVariableDeclaration(node.varList) 57 | 58 | result := rt.cmplEvaluateNodeStatement(node.body) 59 | if result.kind == valueResult { 60 | return result 61 | } 62 | 63 | return Value{} 64 | } 65 | 66 | func (rt *runtime) cmplFunctionDeclaration(list []*nodeFunctionLiteral) { 67 | executionContext := rt.scope 68 | eval := executionContext.eval 69 | stash := executionContext.variable 70 | 71 | for _, function := range list { 72 | name := function.name 73 | value := rt.cmplEvaluateNodeExpression(function) 74 | if !stash.hasBinding(name) { 75 | stash.createBinding(name, eval, value) 76 | } else { 77 | // TODO 10.5.5.e 78 | stash.setBinding(name, value, false) // TODO strict 79 | } 80 | } 81 | } 82 | 83 | func (rt *runtime) cmplVariableDeclaration(list []string) { 84 | executionContext := rt.scope 85 | eval := executionContext.eval 86 | stash := executionContext.variable 87 | 88 | for _, name := range list { 89 | if !stash.hasBinding(name) { 90 | stash.createBinding(name, eval, Value{}) // TODO strict? 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /cmpl_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/robertkrimen/otto/parser" 7 | ) 8 | 9 | func Test_cmpl(t *testing.T) { 10 | tt(t, func() { 11 | vm := New() 12 | 13 | test := func(src string, expect ...interface{}) { 14 | program, err := parser.ParseFile(nil, "", src, 0) 15 | is(err, nil) 16 | { 17 | program := cmplParse(program) 18 | value := vm.runtime.cmplEvaluateNodeProgram(program, false) 19 | if len(expect) > 0 { 20 | is(value, expect[0]) 21 | } 22 | } 23 | } 24 | 25 | test(``, Value{}) 26 | 27 | test(`var abc = 1; abc;`, 1) 28 | 29 | test(`var abc = 1 + 1; abc;`, 2) 30 | 31 | test(`1 + 2;`, 3) 32 | }) 33 | } 34 | 35 | func TestParse_cmpl(t *testing.T) { 36 | tt(t, func() { 37 | test := func(src string) { 38 | program, err := parser.ParseFile(nil, "", src, 0) 39 | is(err, nil) 40 | is(cmplParse(program), "!=", nil) 41 | } 42 | 43 | test(``) 44 | 45 | test(`var abc = 1; abc;`) 46 | 47 | test(` 48 | function abc() { 49 | return; 50 | } 51 | `) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /console.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func formatForConsole(argumentList []Value) string { 10 | output := []string{} 11 | for _, argument := range argumentList { 12 | output = append(output, fmt.Sprintf("%v", argument)) 13 | } 14 | return strings.Join(output, " ") 15 | } 16 | 17 | func builtinConsoleLog(call FunctionCall) Value { 18 | fmt.Fprintln(os.Stdout, formatForConsole(call.ArgumentList)) //nolint:errcheck // Nothing we can do if this fails. 19 | return Value{} 20 | } 21 | 22 | func builtinConsoleError(call FunctionCall) Value { 23 | fmt.Fprintln(os.Stdout, formatForConsole(call.ArgumentList)) //nolint:errcheck // Nothing we can do if this fails. 24 | return Value{} 25 | } 26 | 27 | // Nothing happens. 28 | func builtinConsoleDir(call FunctionCall) Value { 29 | return Value{} 30 | } 31 | 32 | func builtinConsoleTime(call FunctionCall) Value { 33 | return Value{} 34 | } 35 | 36 | func builtinConsoleTimeEnd(call FunctionCall) Value { 37 | return Value{} 38 | } 39 | 40 | func builtinConsoleTrace(call FunctionCall) Value { 41 | return Value{} 42 | } 43 | 44 | func builtinConsoleAssert(call FunctionCall) Value { 45 | return Value{} 46 | } 47 | -------------------------------------------------------------------------------- /consts.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | const ( 4 | // Common classes. 5 | classStringName = "String" 6 | classGoArrayName = "GoArray" 7 | classGoSliceName = "GoSlice" 8 | classNumberName = "Number" 9 | classDateName = "Date" 10 | classArrayName = "Array" 11 | classFunctionName = "Function" 12 | classObjectName = "Object" 13 | classRegExpName = "RegExp" 14 | classBooleanName = "Boolean" 15 | classMathName = "Math" 16 | classJSONName = "JSON" 17 | 18 | // Error classes. 19 | classErrorName = "Error" 20 | classEvalErrorName = "EvalError" 21 | classTypeErrorName = "TypeError" 22 | classRangeErrorName = "RangeError" 23 | classReferenceErrorName = "ReferenceError" 24 | classSyntaxErrorName = "SyntaxError" 25 | classURIErrorName = "URIError" 26 | 27 | // Common properties. 28 | propertyName = "name" 29 | propertyLength = "length" 30 | propertyPrototype = "prototype" 31 | propertyConstructor = "constructor" 32 | 33 | // Common methods. 34 | methodToString = "toString" 35 | ) 36 | -------------------------------------------------------------------------------- /dbg.go: -------------------------------------------------------------------------------- 1 | // This file was AUTOMATICALLY GENERATED by dbg-import (smuggol) for github.com/robertkrimen/dbg 2 | 3 | package otto 4 | 5 | import ( 6 | Dbg "github.com/robertkrimen/otto/dbg" 7 | ) 8 | 9 | var dbg, dbgf = Dbg.New() 10 | -------------------------------------------------------------------------------- /documentation_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func ExampleSynopsis() { //nolint:govet 9 | vm := New() 10 | _, err := vm.Run(` 11 | abc = 2 + 2; 12 | console.log("The value of abc is " + abc); // 4 13 | `) 14 | if err != nil { 15 | fmt.Fprintln(os.Stderr, err) 16 | return 17 | } 18 | 19 | value, err := vm.Get("abc") 20 | if err != nil { 21 | fmt.Fprintln(os.Stderr, err) 22 | return 23 | } 24 | 25 | iv, err := value.ToInteger() 26 | if err != nil { 27 | fmt.Fprintln(os.Stderr, err) 28 | return 29 | } 30 | fmt.Println(iv) 31 | 32 | err = vm.Set("def", 11) 33 | if err != nil { 34 | fmt.Fprintln(os.Stderr, err) 35 | return 36 | } 37 | _, err = vm.Run(` 38 | console.log("The value of def is " + def); 39 | `) 40 | if err != nil { 41 | fmt.Fprintln(os.Stderr, err) 42 | return 43 | } 44 | 45 | err = vm.Set("xyzzy", "Nothing happens.") 46 | if err != nil { 47 | fmt.Fprintln(os.Stderr, err) 48 | return 49 | } 50 | _, err = vm.Run(` 51 | console.log(xyzzy.length); 52 | `) 53 | if err != nil { 54 | fmt.Fprintln(os.Stderr, err) 55 | return 56 | } 57 | 58 | value, err = vm.Run("xyzzy.length") 59 | if err != nil { 60 | fmt.Fprintln(os.Stderr, err) 61 | return 62 | } 63 | 64 | iv, err = value.ToInteger() 65 | if err != nil { 66 | fmt.Fprintln(os.Stderr, err) 67 | return 68 | } 69 | fmt.Println(iv) 70 | 71 | value, err = vm.Run("abcdefghijlmnopqrstuvwxyz.length") 72 | fmt.Println(value) 73 | fmt.Println(err) // Expected error. 74 | 75 | err = vm.Set("sayHello", func(call FunctionCall) Value { 76 | fmt.Printf("Hello, %s.\n", call.Argument(0).String()) 77 | return UndefinedValue() 78 | }) 79 | if err != nil { 80 | fmt.Fprintln(os.Stderr, err) 81 | return 82 | } 83 | 84 | err = vm.Set("twoPlus", func(call FunctionCall) Value { 85 | right, _ := call.Argument(0).ToInteger() 86 | result, _ := vm.ToValue(2 + right) 87 | return result 88 | }) 89 | if err != nil { 90 | fmt.Fprintln(os.Stderr, err) 91 | return 92 | } 93 | 94 | value, err = vm.Run(` 95 | sayHello("Xyzzy"); 96 | sayHello(); 97 | 98 | result = twoPlus(2.0); 99 | `) 100 | if err != nil { 101 | fmt.Fprintln(os.Stderr, err) 102 | return 103 | } 104 | fmt.Println(value) 105 | 106 | // Output: 107 | // The value of abc is 4 108 | // 4 109 | // The value of def is 11 110 | // 16 111 | // 16 112 | // undefined 113 | // ReferenceError: 'abcdefghijlmnopqrstuvwxyz' is not defined 114 | // Hello, Xyzzy. 115 | // Hello, undefined. 116 | // 4 117 | } 118 | 119 | func ExampleConsole() { //nolint:govet 120 | vm := New() 121 | console := map[string]interface{}{ 122 | "log": func(call FunctionCall) Value { 123 | fmt.Println("console.log:", formatForConsole(call.ArgumentList)) 124 | return UndefinedValue() 125 | }, 126 | } 127 | 128 | err := vm.Set("console", console) 129 | if err != nil { 130 | panic(fmt.Errorf("console error: %w", err)) 131 | } 132 | 133 | value, err := vm.Run(`console.log("Hello, World.");`) 134 | fmt.Println(value) 135 | fmt.Println(err) 136 | 137 | // Output: 138 | // console.log: Hello, World. 139 | // undefined 140 | // 141 | } 142 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/robertkrimen/otto/file" 8 | ) 9 | 10 | type exception struct { 11 | value interface{} 12 | } 13 | 14 | func newException(value interface{}) *exception { 15 | return &exception{ 16 | value: value, 17 | } 18 | } 19 | 20 | func (e *exception) eject() interface{} { 21 | value := e.value 22 | e.value = nil // Prevent Go from holding on to the value, whatever it is 23 | return value 24 | } 25 | 26 | type ottoError struct { 27 | name string 28 | message string 29 | trace []frame 30 | 31 | offset int 32 | } 33 | 34 | func (e ottoError) format() string { 35 | if len(e.name) == 0 { 36 | return e.message 37 | } 38 | if len(e.message) == 0 { 39 | return e.name 40 | } 41 | return fmt.Sprintf("%s: %s", e.name, e.message) 42 | } 43 | 44 | func (e ottoError) formatWithStack() string { 45 | str := e.format() + "\n" 46 | for _, frm := range e.trace { 47 | str += " at " + frm.location() + "\n" 48 | } 49 | return str 50 | } 51 | 52 | type frame struct { 53 | fn interface{} 54 | file *file.File 55 | nativeFile string 56 | callee string 57 | nativeLine int 58 | offset int 59 | native bool 60 | } 61 | 62 | var nativeFrame = frame{} 63 | 64 | type at int 65 | 66 | func (fr frame) location() string { 67 | str := "" 68 | 69 | switch { 70 | case fr.native: 71 | str = "" 72 | if fr.nativeFile != "" && fr.nativeLine != 0 { 73 | str = fmt.Sprintf("%s:%d", fr.nativeFile, fr.nativeLine) 74 | } 75 | case fr.file != nil: 76 | if p := fr.file.Position(file.Idx(fr.offset)); p != nil { 77 | path, line, column := p.Filename, p.Line, p.Column 78 | 79 | if path == "" { 80 | path = "" 81 | } 82 | 83 | str = fmt.Sprintf("%s:%d:%d", path, line, column) 84 | } 85 | } 86 | 87 | if fr.callee != "" { 88 | str = fmt.Sprintf("%s (%s)", fr.callee, str) 89 | } 90 | 91 | return str 92 | } 93 | 94 | // An Error represents a runtime error, e.g. a TypeError, a ReferenceError, etc. 95 | type Error struct { 96 | ottoError 97 | } 98 | 99 | // Error returns a description of the error 100 | // 101 | // TypeError: 'def' is not a function 102 | func (e Error) Error() string { 103 | return e.format() 104 | } 105 | 106 | // String returns a description of the error and a trace of where the 107 | // error occurred. 108 | // 109 | // TypeError: 'def' is not a function 110 | // at xyz (:3:9) 111 | // at :7:1/ 112 | func (e Error) String() string { 113 | return e.formatWithStack() 114 | } 115 | 116 | // GoString returns a description of the error and a trace of where the 117 | // error occurred. Printing with %#v will trigger this behaviour. 118 | func (e Error) GoString() string { 119 | return e.formatWithStack() 120 | } 121 | 122 | func (e ottoError) describe(format string, in ...interface{}) string { 123 | return fmt.Sprintf(format, in...) 124 | } 125 | 126 | func (e ottoError) messageValue() Value { 127 | if e.message == "" { 128 | return Value{} 129 | } 130 | return stringValue(e.message) 131 | } 132 | 133 | func (rt *runtime) typeErrorResult(throw bool) bool { 134 | if throw { 135 | panic(rt.panicTypeError()) 136 | } 137 | return false 138 | } 139 | 140 | func newError(rt *runtime, name string, stackFramesToPop int, in ...interface{}) ottoError { 141 | err := ottoError{ 142 | name: name, 143 | offset: -1, 144 | } 145 | description := "" 146 | length := len(in) 147 | 148 | if rt != nil && rt.scope != nil { 149 | curScope := rt.scope 150 | 151 | for range stackFramesToPop { 152 | if curScope.outer != nil { 153 | curScope = curScope.outer 154 | } 155 | } 156 | 157 | frm := curScope.frame 158 | 159 | if length > 0 { 160 | if atv, ok := in[length-1].(at); ok { 161 | in = in[0 : length-1] 162 | if curScope != nil { 163 | frm.offset = int(atv) 164 | } 165 | length-- 166 | } 167 | if length > 0 { 168 | description, in = in[0].(string), in[1:] 169 | } 170 | } 171 | 172 | limit := rt.traceLimit 173 | 174 | err.trace = append(err.trace, frm) 175 | if curScope != nil { 176 | for curScope = curScope.outer; curScope != nil; curScope = curScope.outer { 177 | if limit--; limit == 0 { 178 | break 179 | } 180 | 181 | if curScope.frame.offset >= 0 { 182 | err.trace = append(err.trace, curScope.frame) 183 | } 184 | } 185 | } 186 | } else if length > 0 { 187 | description, in = in[0].(string), in[1:] 188 | } 189 | err.message = err.describe(description, in...) 190 | 191 | return err 192 | } 193 | 194 | func (rt *runtime) panicTypeError(argumentList ...interface{}) *exception { 195 | return &exception{ 196 | value: newError(rt, "TypeError", 0, argumentList...), 197 | } 198 | } 199 | 200 | func (rt *runtime) panicReferenceError(argumentList ...interface{}) *exception { 201 | return &exception{ 202 | value: newError(rt, "ReferenceError", 0, argumentList...), 203 | } 204 | } 205 | 206 | func (rt *runtime) panicURIError(argumentList ...interface{}) *exception { 207 | return &exception{ 208 | value: newError(rt, "URIError", 0, argumentList...), 209 | } 210 | } 211 | 212 | func (rt *runtime) panicSyntaxError(argumentList ...interface{}) *exception { 213 | return &exception{ 214 | value: newError(rt, "SyntaxError", 0, argumentList...), 215 | } 216 | } 217 | 218 | func (rt *runtime) panicRangeError(argumentList ...interface{}) *exception { 219 | return &exception{ 220 | value: newError(rt, "RangeError", 0, argumentList...), 221 | } 222 | } 223 | 224 | func catchPanic(function func()) (err error) { 225 | defer func() { 226 | if caught := recover(); caught != nil { 227 | if excep, ok := caught.(*exception); ok { 228 | caught = excep.eject() 229 | } 230 | switch caught := caught.(type) { 231 | case *Error: 232 | err = caught 233 | return 234 | case ottoError: 235 | err = &Error{caught} 236 | return 237 | case Value: 238 | if vl := caught.object(); vl != nil { 239 | if vl, ok := vl.value.(ottoError); ok { 240 | err = &Error{vl} 241 | return 242 | } 243 | } 244 | err = errors.New(caught.string()) 245 | return 246 | } 247 | panic(caught) 248 | } 249 | }() 250 | function() 251 | return nil 252 | } 253 | -------------------------------------------------------------------------------- /error_native_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | // this is its own file because the tests in it rely on the line numbers of 10 | // some of the functions defined here. putting it in with the rest of the 11 | // tests would probably be annoying. 12 | 13 | func TestErrorContextNative(t *testing.T) { 14 | tt(t, func() { 15 | vm := New() 16 | 17 | err := vm.Set("N", func(c FunctionCall) Value { 18 | v, err := c.Argument(0).Call(NullValue()) 19 | if err != nil { 20 | panic(err) 21 | } 22 | return v 23 | }) 24 | require.NoError(t, err) 25 | 26 | s, err := vm.Compile("test.js", ` 27 | function F() { throw new Error('wow'); } 28 | function G() { return N(F); } 29 | `) 30 | require.NoError(t, err) 31 | 32 | _, err = vm.Run(s) 33 | require.NoError(t, err) 34 | 35 | f1, err := vm.Get("G") 36 | require.NoError(t, err) 37 | _, err = f1.Call(NullValue()) 38 | require.Error(t, err) 39 | err1 := asError(t, err) 40 | is(err1.message, "wow") 41 | is(len(err1.trace), 3) 42 | is(err1.trace[0].location(), "F (test.js:2:29)") 43 | is(err1.trace[1].location(), "github.com/robertkrimen/otto.TestErrorContextNative.func1.1 (error_native_test.go:17)") 44 | is(err1.trace[2].location(), "G (test.js:3:26)") 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /file/file.go: -------------------------------------------------------------------------------- 1 | // Package file encapsulates the file abstractions used by the ast & parser. 2 | package file 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "gopkg.in/sourcemap.v1" 9 | ) 10 | 11 | // Idx is a compact encoding of a source position within a file set. 12 | // It can be converted into a Position for a more convenient, but much 13 | // larger, representation. 14 | type Idx int 15 | 16 | // Position describes an arbitrary source position 17 | // including the filename, line, and column location. 18 | type Position struct { 19 | Filename string // The filename where the error occurred, if any 20 | Offset int // The src offset 21 | Line int // The line number, starting at 1 22 | Column int // The column number, starting at 1 (The character count) 23 | } 24 | 25 | // A Position is valid if the line number is > 0. 26 | 27 | func (p *Position) isValid() bool { 28 | return p.Line > 0 29 | } 30 | 31 | // String returns a string in one of several forms: 32 | // 33 | // file:line:column A valid position with filename 34 | // line:column A valid position without filename 35 | // file An invalid position with filename 36 | // - An invalid position without filename 37 | func (p *Position) String() string { 38 | str := p.Filename 39 | if p.isValid() { 40 | if str != "" { 41 | str += ":" 42 | } 43 | str += fmt.Sprintf("%d:%d", p.Line, p.Column) 44 | } 45 | if str == "" { 46 | str = "-" 47 | } 48 | return str 49 | } 50 | 51 | // A FileSet represents a set of source files. 52 | type FileSet struct { 53 | last *File 54 | files []*File 55 | } 56 | 57 | // AddFile adds a new file with the given filename and src. 58 | // 59 | // This an internal method, but exported for cross-package use. 60 | func (fs *FileSet) AddFile(filename, src string) int { 61 | base := fs.nextBase() 62 | file := &File{ 63 | name: filename, 64 | src: src, 65 | base: base, 66 | } 67 | fs.files = append(fs.files, file) 68 | fs.last = file 69 | return base 70 | } 71 | 72 | func (fs *FileSet) nextBase() int { 73 | if fs.last == nil { 74 | return 1 75 | } 76 | return fs.last.base + len(fs.last.src) + 1 77 | } 78 | 79 | // File returns the File at idx or nil if not found. 80 | func (fs *FileSet) File(idx Idx) *File { 81 | for _, file := range fs.files { 82 | if idx <= Idx(file.base+len(file.src)) { 83 | return file 84 | } 85 | } 86 | return nil 87 | } 88 | 89 | // Position converts an Idx in the FileSet into a Position. 90 | func (fs *FileSet) Position(idx Idx) *Position { 91 | for _, file := range fs.files { 92 | if idx <= Idx(file.base+len(file.src)) { 93 | return file.Position(idx - Idx(file.base)) 94 | } 95 | } 96 | 97 | return nil 98 | } 99 | 100 | // File represents a file to parse. 101 | type File struct { 102 | sm *sourcemap.Consumer 103 | name string 104 | src string 105 | base int 106 | } 107 | 108 | // NewFile returns a new file with the given filename, src and base. 109 | func NewFile(filename, src string, base int) *File { 110 | return &File{ 111 | name: filename, 112 | src: src, 113 | base: base, 114 | } 115 | } 116 | 117 | // WithSourceMap sets the source map of fl. 118 | func (fl *File) WithSourceMap(sm *sourcemap.Consumer) *File { 119 | fl.sm = sm 120 | return fl 121 | } 122 | 123 | // Name returns the name of fl. 124 | func (fl *File) Name() string { 125 | return fl.name 126 | } 127 | 128 | // Source returns the source of fl. 129 | func (fl *File) Source() string { 130 | return fl.src 131 | } 132 | 133 | // Base returns the base of fl. 134 | func (fl *File) Base() int { 135 | return fl.base 136 | } 137 | 138 | // Position returns the position at idx or nil if not valid. 139 | func (fl *File) Position(idx Idx) *Position { 140 | position := &Position{} 141 | 142 | offset := int(idx) - fl.base 143 | 144 | if offset >= len(fl.src) || offset < 0 { 145 | return nil 146 | } 147 | 148 | src := fl.src[:offset] 149 | 150 | position.Filename = fl.name 151 | position.Offset = offset 152 | position.Line = strings.Count(src, "\n") + 1 153 | 154 | if index := strings.LastIndex(src, "\n"); index >= 0 { 155 | position.Column = offset - index 156 | } else { 157 | position.Column = len(src) + 1 158 | } 159 | 160 | if fl.sm != nil { 161 | if f, _, l, c, ok := fl.sm.Source(position.Line, position.Column); ok { 162 | position.Filename, position.Line, position.Column = f, l, c 163 | } 164 | } 165 | 166 | return position 167 | } 168 | -------------------------------------------------------------------------------- /function_stack_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | // this is its own file because the tests in it rely on the line numbers of 10 | // some of the functions defined here. putting it in with the rest of the 11 | // tests would probably be annoying. 12 | 13 | func TestFunction_stack(t *testing.T) { 14 | tt(t, func() { 15 | vm := New() 16 | 17 | s, err := vm.Compile("fake.js", `function X(fn1, fn2, fn3) { fn1(fn2, fn3); }`) 18 | require.NoError(t, err) 19 | _, err = vm.Run(s) 20 | require.NoError(t, err) 21 | 22 | expected := []frame{ 23 | {native: true, nativeFile: "function_stack_test.go", nativeLine: 36, offset: 0, callee: "github.com/robertkrimen/otto.TestFunction_stack.func1.2"}, 24 | {native: true, nativeFile: "function_stack_test.go", nativeLine: 29, offset: 0, callee: "github.com/robertkrimen/otto.TestFunction_stack.func1.1"}, 25 | {native: false, nativeFile: "", nativeLine: 0, offset: 29, callee: "X", file: s.program.file}, 26 | {native: false, nativeFile: "", nativeLine: 0, offset: 29, callee: "X", file: s.program.file}, 27 | } 28 | 29 | err = vm.Set("A", func(c FunctionCall) Value { 30 | _, err = c.Argument(0).Call(UndefinedValue()) 31 | require.NoError(t, err) 32 | return UndefinedValue() 33 | }) 34 | require.NoError(t, err) 35 | 36 | err = vm.Set("B", func(c FunctionCall) Value { 37 | depth := 0 38 | for s := c.Otto.runtime.scope; s != nil; s = s.outer { 39 | // these properties are tested explicitly so that we don't test `.fn`, 40 | // which will differ from run to run 41 | is(s.frame.native, expected[depth].native) 42 | is(s.frame.nativeFile, expected[depth].nativeFile) 43 | is(s.frame.nativeLine, expected[depth].nativeLine) 44 | is(s.frame.offset, expected[depth].offset) 45 | is(s.frame.callee, expected[depth].callee) 46 | is(s.frame.file, expected[depth].file) 47 | depth++ 48 | } 49 | 50 | is(depth, 4) 51 | 52 | return UndefinedValue() 53 | }) 54 | require.NoError(t, err) 55 | 56 | x, err := vm.Get("X") 57 | require.NoError(t, err) 58 | a, err := vm.Get("A") 59 | require.NoError(t, err) 60 | b, err := vm.Get("B") 61 | require.NoError(t, err) 62 | 63 | _, err = x.Call(UndefinedValue(), x, a, b) 64 | require.NoError(t, err) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | //go:generate go run ./tools/gen-jscore -output inline.go 4 | //go:generate stringer -type=valueKind -trimprefix=value -output=value_kind.gen.go 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/robertkrimen/otto 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/stretchr/testify v1.8.1 7 | golang.org/x/text v0.4.0 8 | gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375 9 | gopkg.in/sourcemap.v1 v1.0.5 10 | gopkg.in/yaml.v3 v3.0.1 11 | ) 12 | 13 | require ( 14 | github.com/chzyer/test v1.0.0 // indirect 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 2 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 3 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 4 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 12 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 13 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 15 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 16 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 17 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= 18 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375 h1:hPki/oSSWOLiI9Gc9jyIoj33O3j29fUc9PlLha2yDj0= 22 | gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375/go.mod h1:lNEQeAhU009zbRxng+XOj5ITVgY24WcbNnQopyfKoYQ= 23 | gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= 24 | gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= 25 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | -------------------------------------------------------------------------------- /json_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func BenchmarkJSON_parse(b *testing.B) { 10 | vm := New() 11 | for i := 0; i < b.N; i++ { 12 | _, err := vm.Run(`JSON.parse("1")`) 13 | require.NoError(b, err) 14 | _, err = vm.Run(`JSON.parse("[1,2,3]")`) 15 | require.NoError(b, err) 16 | _, err = vm.Run(`JSON.parse('{"a":{"x":100,"y":110},"b":[10,20,30],"c":"zazazaza"}')`) 17 | require.NoError(b, err) 18 | _, err = vm.Run(`JSON.parse("[1,2,3]", function(k, v) { return undefined })`) 19 | require.NoError(b, err) 20 | } 21 | } 22 | 23 | func TestJSON_parse(t *testing.T) { 24 | tt(t, func() { 25 | test, _ := test() 26 | 27 | test(` 28 | JSON.parse("1"); 29 | `, 1) 30 | 31 | test(` 32 | JSON.parse("null"); 33 | `, "null") // TODO Can we make this nil? 34 | 35 | test(` 36 | var abc = JSON.parse('"a\uFFFFbc"'); 37 | [ abc[0], abc[2], abc[3], abc.length ]; 38 | `, "a,b,c,4") 39 | 40 | test(` 41 | JSON.parse("[1, 2, 3]"); 42 | `, "1,2,3") 43 | 44 | test(` 45 | JSON.parse('{ "abc": 1, "def":2 }').abc; 46 | `, 1) 47 | 48 | test(` 49 | JSON.parse('{ "abc": { "x": 100, "y": 110 }, "def": [ 10, 20 ,30 ], "ghi": "zazazaza" }').def; 50 | `, "10,20,30") 51 | 52 | test(`raise: 53 | JSON.parse("12\t\r\n 34"); 54 | `, "SyntaxError: invalid character '3' after top-level value") 55 | 56 | test(` 57 | JSON.parse("[1, 2, 3]", function() { return undefined }); 58 | `, "undefined") 59 | 60 | test(`raise: 61 | JSON.parse(""); 62 | `, "SyntaxError: unexpected end of JSON input") 63 | 64 | test(`raise: 65 | JSON.parse("[1, 2, 3"); 66 | `, "SyntaxError: unexpected end of JSON input") 67 | 68 | test(`raise: 69 | JSON.parse("[1, 2, ; abc=10"); 70 | `, "SyntaxError: invalid character ';' looking for beginning of value") 71 | 72 | test(`raise: 73 | JSON.parse("[1, 2, function(){}]"); 74 | `, "SyntaxError: invalid character 'u' in literal false (expecting 'a')") 75 | }) 76 | } 77 | 78 | func TestJSON_stringify(t *testing.T) { 79 | tt(t, func() { 80 | test, _ := test() 81 | 82 | defer mockUTC()() 83 | 84 | test(` 85 | JSON.stringify(function(){}); 86 | `, "undefined") 87 | 88 | test(` 89 | JSON.stringify(new Boolean(false)); 90 | `, "false") 91 | 92 | test(` 93 | JSON.stringify({a1: {b1: [1,2,3,4], b2: {c1: 1, c2: 2}}, a2: 'a2'}, null, -5); 94 | `, `{"a1":{"b1":[1,2,3,4],"b2":{"c1":1,"c2":2}},"a2":"a2"}`) 95 | 96 | test(` 97 | JSON.stringify(undefined); 98 | `, "undefined") 99 | 100 | test(` 101 | JSON.stringify(1); 102 | `, "1") 103 | 104 | test(` 105 | JSON.stringify("abc def"); 106 | `, "\"abc def\"") 107 | 108 | test(` 109 | JSON.stringify(3.14159); 110 | `, "3.14159") 111 | 112 | test(` 113 | JSON.stringify([]); 114 | `, "[]") 115 | 116 | test(` 117 | JSON.stringify([1, 2, 3]); 118 | `, "[1,2,3]") 119 | 120 | test(` 121 | JSON.stringify([true, false, null]); 122 | `, "[true,false,null]") 123 | 124 | test(` 125 | JSON.stringify({ 126 | abc: { x: 100, y: 110 }, 127 | def: [ 10, 20, 30 ], 128 | ghi: "zazazaza" 129 | }); 130 | `, `{"abc":{"x":100,"y":110},"def":[10,20,30],"ghi":"zazazaza"}`) 131 | 132 | test(` 133 | JSON.stringify([ 134 | 'e', 135 | {pluribus: 'unum'} 136 | ], null, '\t'); 137 | `, "[\n\t\"e\",\n\t{\n\t\t\"pluribus\": \"unum\"\n\t}\n]") 138 | 139 | test(` 140 | JSON.stringify(new Date(0)); 141 | `, `"1970-01-01T00:00:00.000Z"`) 142 | 143 | test(` 144 | JSON.stringify([ new Date(0) ], function(key, value){ 145 | return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value 146 | }); 147 | `, `["Date(Thu, 01 Jan 1970 00:00:00 UTC)"]`) 148 | 149 | test(` 150 | JSON.stringify({ 151 | abc: 1, 152 | def: 2, 153 | ghi: 3 154 | }, ['abc','def']); 155 | `, `{"abc":1,"def":2}`) 156 | 157 | test(`raise: 158 | var abc = { 159 | def: null 160 | }; 161 | abc.def = abc; 162 | JSON.stringify(abc) 163 | `, "TypeError: Converting circular structure to JSON") 164 | 165 | test(`raise: 166 | var abc= [ null ]; 167 | abc[0] = abc; 168 | JSON.stringify(abc); 169 | `, "TypeError: Converting circular structure to JSON") 170 | 171 | test(`raise: 172 | var abc = { 173 | def: {} 174 | }; 175 | abc.def.ghi = abc; 176 | JSON.stringify(abc) 177 | `, "TypeError: Converting circular structure to JSON") 178 | 179 | test(` 180 | var ghi = { "pi": 3.14159 }; 181 | var abc = { 182 | def: {} 183 | }; 184 | abc.ghi = ghi; 185 | abc.def.ghi = ghi; 186 | JSON.stringify(abc); 187 | `, `{"def":{"ghi":{"pi":3.14159}},"ghi":{"pi":3.14159}}`) 188 | }) 189 | } 190 | -------------------------------------------------------------------------------- /locale.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import "golang.org/x/text/language" 4 | 5 | var defaultLanguage = language.MustParse("en-US") 6 | -------------------------------------------------------------------------------- /native_stack_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestNativeStackFrames(t *testing.T) { 10 | tt(t, func() { 11 | vm := New() 12 | 13 | s, err := vm.Compile("input.js", ` 14 | function A() { ext1(); } 15 | function B() { ext2(); } 16 | A(); 17 | `) 18 | require.NoError(t, err) 19 | 20 | err = vm.Set("ext1", func(c FunctionCall) Value { 21 | if _, err = c.Otto.Eval("B()"); err != nil { 22 | panic(err) 23 | } 24 | 25 | return UndefinedValue() 26 | }) 27 | require.NoError(t, err) 28 | 29 | err = vm.Set("ext2", func(c FunctionCall) Value { 30 | { 31 | // no limit, include innermost native frames 32 | ctx := c.Otto.ContextSkip(-1, false) 33 | 34 | is(ctx.Stacktrace, []string{ 35 | "github.com/robertkrimen/otto.TestNativeStackFrames.func1.2 (native_stack_test.go:29)", 36 | "B (input.js:3:19)", 37 | "github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)", 38 | "A (input.js:2:19)", "input.js:4:4", 39 | }) 40 | 41 | is(ctx.Callee, "github.com/robertkrimen/otto.TestNativeStackFrames.func1.2") 42 | is(ctx.Filename, "native_stack_test.go") 43 | is(ctx.Line, 29) 44 | is(ctx.Column, 0) 45 | } 46 | 47 | { 48 | // no limit, skip innermost native frames 49 | ctx := c.Otto.ContextSkip(-1, true) 50 | 51 | is(ctx.Stacktrace, []string{ 52 | "B (input.js:3:19)", 53 | "github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)", 54 | "A (input.js:2:19)", "input.js:4:4", 55 | }) 56 | 57 | is(ctx.Callee, "B") 58 | is(ctx.Filename, "input.js") 59 | is(ctx.Line, 3) 60 | is(ctx.Column, 19) 61 | } 62 | 63 | if _, err = c.Otto.Eval("ext3()"); err != nil { 64 | panic(err) 65 | } 66 | 67 | return UndefinedValue() 68 | }) 69 | require.NoError(t, err) 70 | 71 | err = vm.Set("ext3", func(c FunctionCall) Value { 72 | { 73 | // no limit, include innermost native frames 74 | ctx := c.Otto.ContextSkip(-1, false) 75 | 76 | is(ctx.Stacktrace, []string{ 77 | "github.com/robertkrimen/otto.TestNativeStackFrames.func1.3 (native_stack_test.go:71)", 78 | "github.com/robertkrimen/otto.TestNativeStackFrames.func1.2 (native_stack_test.go:29)", 79 | "B (input.js:3:19)", 80 | "github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)", 81 | "A (input.js:2:19)", "input.js:4:4", 82 | }) 83 | 84 | is(ctx.Callee, "github.com/robertkrimen/otto.TestNativeStackFrames.func1.3") 85 | is(ctx.Filename, "native_stack_test.go") 86 | is(ctx.Line, 71) 87 | is(ctx.Column, 0) 88 | } 89 | 90 | { 91 | // no limit, skip innermost native frames 92 | ctx := c.Otto.ContextSkip(-1, true) 93 | 94 | is(ctx.Stacktrace, []string{ 95 | "B (input.js:3:19)", 96 | "github.com/robertkrimen/otto.TestNativeStackFrames.func1.1 (native_stack_test.go:20)", 97 | "A (input.js:2:19)", "input.js:4:4", 98 | }) 99 | 100 | is(ctx.Callee, "B") 101 | is(ctx.Filename, "input.js") 102 | is(ctx.Line, 3) 103 | is(ctx.Column, 19) 104 | } 105 | 106 | return UndefinedValue() 107 | }) 108 | require.NoError(t, err) 109 | 110 | _, err = vm.Run(s) 111 | require.NoError(t, err) 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /number_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNumber(t *testing.T) { 8 | tt(t, func() { 9 | test, _ := test() 10 | 11 | test(` 12 | var abc = Object.getOwnPropertyDescriptor(Number, "prototype"); 13 | [ [ typeof Number.prototype ], 14 | [ abc.writable, abc.enumerable, abc.configurable ] ]; 15 | `, "object,false,false,false") 16 | }) 17 | } 18 | 19 | func TestNumber_toString(t *testing.T) { 20 | tt(t, func() { 21 | test, _ := test() 22 | 23 | test(` 24 | new Number(451).toString(); 25 | `, "451") 26 | 27 | test(` 28 | new Number(451).toString(10); 29 | `, "451") 30 | 31 | test(` 32 | new Number(451).toString(8); 33 | `, "703") 34 | 35 | test(`raise: 36 | new Number(451).toString(1); 37 | `, "RangeError: toString() radix must be between 2 and 36") 38 | 39 | test(`raise: 40 | new Number(451).toString(Infinity); 41 | `, "RangeError: toString() radix must be between 2 and 36") 42 | 43 | test(` 44 | new Number(NaN).toString() 45 | `, "NaN") 46 | 47 | test(` 48 | new Number(Infinity).toString() 49 | `, "Infinity") 50 | 51 | test(` 52 | new Number(Infinity).toString(16) 53 | `, "Infinity") 54 | 55 | test(` 56 | [ 57 | Number.prototype.toString(undefined), 58 | new Number().toString(undefined), 59 | new Number(0).toString(undefined), 60 | new Number(-1).toString(undefined), 61 | new Number(1).toString(undefined), 62 | new Number(Number.NaN).toString(undefined), 63 | new Number(Number.POSITIVE_INFINITY).toString(undefined), 64 | new Number(Number.NEGATIVE_INFINITY).toString(undefined) 65 | ] 66 | `, "0,0,0,-1,1,NaN,Infinity,-Infinity") 67 | }) 68 | } 69 | 70 | func TestNumber_toFixed(t *testing.T) { 71 | tt(t, func() { 72 | test, _ := test() 73 | 74 | test(`new Number(451).toFixed(2)`, "451.00") 75 | test(`12345.6789.toFixed()`, "12346") 76 | test(`12345.6789.toFixed(1)`, "12345.7") 77 | test(`12345.6789.toFixed(6)`, "12345.678900") 78 | test(`(1.23e-20).toFixed(2)`, "0.00") 79 | test(`2.34.toFixed(1)`, "2.3") // FIXME Wtf? "2.3" 80 | test(`-2.34.toFixed(1)`, -2.3) // FIXME Wtf? -2.3 81 | test(`(-2.34).toFixed(1)`, "-2.3") 82 | 83 | test(`raise: 84 | new Number("a").toFixed(Number.POSITIVE_INFINITY); 85 | `, "RangeError: toFixed() precision must be between 0 and 20") 86 | 87 | test(` 88 | [ 89 | new Number(1e21).toFixed(), 90 | new Number(1e21).toFixed(0), 91 | new Number(1e21).toFixed(1), 92 | new Number(1e21).toFixed(1.1), 93 | new Number(1e21).toFixed(0.9), 94 | new Number(1e21).toFixed("1"), 95 | new Number(1e21).toFixed("1.1"), 96 | new Number(1e21).toFixed("0.9"), 97 | new Number(1e21).toFixed(Number.NaN), 98 | new Number(1e21).toFixed("some string") 99 | ]; 100 | `, "1e+21,1e+21,1e+21,1e+21,1e+21,1e+21,1e+21,1e+21,1e+21,1e+21") 101 | 102 | test(`raise: 103 | new Number(1e21).toFixed(Number.POSITIVE_INFINITY); 104 | `, "RangeError: toFixed() precision must be between 0 and 20") 105 | }) 106 | } 107 | 108 | func TestNumber_toExponential(t *testing.T) { 109 | tt(t, func() { 110 | test, _ := test() 111 | 112 | test(`new Number(451).toExponential(2)`, "4.51e+02") 113 | test(`77.1234.toExponential()`, "7.71234e+01") 114 | test(`77.1234.toExponential(4)`, "7.7123e+01") 115 | test(`77.1234.toExponential(2)`, "7.71e+01") 116 | test(`77 .toExponential()`, "7.7e+01") 117 | }) 118 | } 119 | 120 | func TestNumber_toPrecision(t *testing.T) { 121 | tt(t, func() { 122 | test, _ := test() 123 | 124 | test(`new Number(451).toPrecision()`, "451") 125 | test(`new Number(451).toPrecision(1)`, "5e+02") 126 | test(`5.123456.toPrecision()`, "5.123456") 127 | test(`5.123456.toPrecision(5)`, "5.1235") 128 | test(`5.123456.toPrecision(2)`, "5.1") 129 | test(`5.123456.toPrecision(1)`, "5") 130 | }) 131 | } 132 | 133 | func TestNumber_toLocaleString(t *testing.T) { 134 | tt(t, func() { 135 | test, _ := test() 136 | 137 | test(` 138 | [ 139 | new Number(4510).toLocaleString(), 140 | new Number(4510).toLocaleString('en-US'), 141 | new Number(4510).toLocaleString('nl-NL') 142 | ]; 143 | `, "4,510,4,510,4.510") 144 | }) 145 | } 146 | 147 | func TestValue_number(t *testing.T) { 148 | tt(t, func() { 149 | nm := toValue(0.0).number() 150 | is(nm.kind, numberInteger) 151 | 152 | nm = toValue(3.14159).number() 153 | is(nm.kind, numberFloat) 154 | }) 155 | } 156 | 157 | func Test_NaN(t *testing.T) { 158 | tt(t, func() { 159 | test, _ := test() 160 | 161 | test(` 162 | [ NaN === NaN, NaN == NaN ]; 163 | `, "false,false") 164 | }) 165 | } 166 | -------------------------------------------------------------------------------- /object.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | type object struct { 4 | value interface{} 5 | runtime *runtime 6 | objectClass *objectClass 7 | prototype *object 8 | property map[string]property 9 | class string 10 | propertyOrder []string 11 | extensible bool 12 | } 13 | 14 | func newObject(rt *runtime, class string) *object { 15 | o := &object{ 16 | runtime: rt, 17 | class: class, 18 | objectClass: classObject, 19 | property: make(map[string]property), 20 | extensible: true, 21 | } 22 | return o 23 | } 24 | 25 | // 8.12 26 | 27 | // 8.12.1. 28 | func (o *object) getOwnProperty(name string) *property { 29 | return o.objectClass.getOwnProperty(o, name) 30 | } 31 | 32 | // 8.12.2. 33 | func (o *object) getProperty(name string) *property { 34 | return o.objectClass.getProperty(o, name) 35 | } 36 | 37 | // 8.12.3. 38 | func (o *object) get(name string) Value { 39 | return o.objectClass.get(o, name) 40 | } 41 | 42 | // 8.12.4. 43 | func (o *object) canPut(name string) bool { 44 | return o.objectClass.canPut(o, name) 45 | } 46 | 47 | // 8.12.5. 48 | func (o *object) put(name string, value Value, throw bool) { 49 | o.objectClass.put(o, name, value, throw) 50 | } 51 | 52 | // 8.12.6. 53 | func (o *object) hasProperty(name string) bool { 54 | return o.objectClass.hasProperty(o, name) 55 | } 56 | 57 | func (o *object) hasOwnProperty(name string) bool { 58 | return o.objectClass.hasOwnProperty(o, name) 59 | } 60 | 61 | type defaultValueHint int 62 | 63 | const ( 64 | defaultValueNoHint defaultValueHint = iota 65 | defaultValueHintString 66 | defaultValueHintNumber 67 | ) 68 | 69 | // 8.12.8. 70 | func (o *object) DefaultValue(hint defaultValueHint) Value { 71 | if hint == defaultValueNoHint { 72 | if o.class == classDateName { 73 | // Date exception 74 | hint = defaultValueHintString 75 | } else { 76 | hint = defaultValueHintNumber 77 | } 78 | } 79 | methodSequence := []string{"valueOf", "toString"} 80 | if hint == defaultValueHintString { 81 | methodSequence = []string{"toString", "valueOf"} 82 | } 83 | for _, methodName := range methodSequence { 84 | method := o.get(methodName) 85 | // FIXME This is redundant... 86 | if method.isCallable() { 87 | result := method.object().call(objectValue(o), nil, false, nativeFrame) 88 | if result.IsPrimitive() { 89 | return result 90 | } 91 | } 92 | } 93 | 94 | panic(o.runtime.panicTypeError("Object.DefaultValue unknown")) 95 | } 96 | 97 | func (o *object) String() string { 98 | return o.DefaultValue(defaultValueHintString).string() 99 | } 100 | 101 | func (o *object) defineProperty(name string, value Value, mode propertyMode, throw bool) bool { //nolint:unparam 102 | return o.defineOwnProperty(name, property{value, mode}, throw) 103 | } 104 | 105 | // 8.12.9. 106 | func (o *object) defineOwnProperty(name string, descriptor property, throw bool) bool { 107 | return o.objectClass.defineOwnProperty(o, name, descriptor, throw) 108 | } 109 | 110 | func (o *object) delete(name string, throw bool) bool { 111 | return o.objectClass.delete(o, name, throw) 112 | } 113 | 114 | func (o *object) enumerate(all bool, each func(string) bool) { 115 | o.objectClass.enumerate(o, all, each) 116 | } 117 | 118 | func (o *object) readProperty(name string) (property, bool) { 119 | prop, exists := o.property[name] 120 | return prop, exists 121 | } 122 | 123 | func (o *object) writeProperty(name string, value interface{}, mode propertyMode) { 124 | if value == nil { 125 | value = Value{} 126 | } 127 | if _, exists := o.property[name]; !exists { 128 | o.propertyOrder = append(o.propertyOrder, name) 129 | } 130 | o.property[name] = property{value, mode} 131 | } 132 | 133 | func (o *object) deleteProperty(name string) { 134 | if _, exists := o.property[name]; !exists { 135 | return 136 | } 137 | 138 | delete(o.property, name) 139 | for index, prop := range o.propertyOrder { 140 | if name == prop { 141 | if index == len(o.propertyOrder)-1 { 142 | o.propertyOrder = o.propertyOrder[:index] 143 | } else { 144 | o.propertyOrder = append(o.propertyOrder[:index], o.propertyOrder[index+1:]...) 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /otto/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "os" 9 | 10 | "github.com/robertkrimen/otto" 11 | "github.com/robertkrimen/otto/underscore" 12 | ) 13 | 14 | var flagUnderscore *bool = flag.Bool("underscore", true, "Load underscore into the runtime environment") 15 | 16 | func readSource(filename string) ([]byte, error) { 17 | if filename == "" || filename == "-" { 18 | return io.ReadAll(os.Stdin) 19 | } 20 | return os.ReadFile(filename) //nolint:gosec 21 | } 22 | 23 | func main() { 24 | flag.Parse() 25 | 26 | if !*flagUnderscore { 27 | underscore.Disable() 28 | } 29 | 30 | err := func() error { 31 | src, err := readSource(flag.Arg(0)) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | vm := otto.New() 37 | _, err = vm.Run(src) 38 | return err 39 | }() 40 | if err != nil { 41 | var oerr *otto.Error 42 | if errors.As(err, &oerr) { 43 | fmt.Fprint(os.Stderr, oerr.String()) 44 | } else { 45 | fmt.Fprintln(os.Stderr, err) 46 | } 47 | os.Exit(64) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /otto_.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | goruntime "runtime" 7 | "strconv" 8 | ) 9 | 10 | var isIdentifierRegexp *regexp.Regexp = regexp.MustCompile(`^[a-zA-Z\$][a-zA-Z0-9\$]*$`) 11 | 12 | func isIdentifier(value string) bool { 13 | return isIdentifierRegexp.MatchString(value) 14 | } 15 | 16 | func (rt *runtime) toValueArray(arguments ...interface{}) []Value { 17 | length := len(arguments) 18 | if length == 1 { 19 | if valueArray, ok := arguments[0].([]Value); ok { 20 | return valueArray 21 | } 22 | return []Value{rt.toValue(arguments[0])} 23 | } 24 | 25 | valueArray := make([]Value, length) 26 | for index, value := range arguments { 27 | valueArray[index] = rt.toValue(value) 28 | } 29 | 30 | return valueArray 31 | } 32 | 33 | func stringToArrayIndex(name string) int64 { 34 | index, err := strconv.ParseInt(name, 10, 64) 35 | if err != nil { 36 | return -1 37 | } 38 | if index < 0 { 39 | return -1 40 | } 41 | if index >= maxUint32 { 42 | // The value 2^32 (or above) is not a valid index because 43 | // you cannot store a uint32 length for an index of uint32 44 | return -1 45 | } 46 | return index 47 | } 48 | 49 | func isUint32(value int64) bool { 50 | return value >= 0 && value <= maxUint32 51 | } 52 | 53 | func arrayIndexToString(index int64) string { 54 | return strconv.FormatInt(index, 10) 55 | } 56 | 57 | func valueOfArrayIndex(array []Value, index int) Value { 58 | value, _ := getValueOfArrayIndex(array, index) 59 | return value 60 | } 61 | 62 | func getValueOfArrayIndex(array []Value, index int) (Value, bool) { 63 | if index >= 0 && index < len(array) { 64 | value := array[index] 65 | if !value.isEmpty() { 66 | return value, true 67 | } 68 | } 69 | return Value{}, false 70 | } 71 | 72 | // A range index can be anything from 0 up to length. It is NOT safe to use as an index 73 | // to an array, but is useful for slicing and in some ECMA algorithms. 74 | func valueToRangeIndex(indexValue Value, length int64, negativeIsZero bool) int64 { 75 | index := indexValue.number().int64 76 | if negativeIsZero { 77 | if index < 0 { 78 | index = 0 79 | } 80 | // minimum(index, length) 81 | if index >= length { 82 | index = length 83 | } 84 | return index 85 | } 86 | 87 | if index < 0 { 88 | index += length 89 | if index < 0 { 90 | index = 0 91 | } 92 | } else if index > length { 93 | index = length 94 | } 95 | return index 96 | } 97 | 98 | func rangeStartEnd(array []Value, size int64, negativeIsZero bool) (start, end int64) { //nolint:nonamedreturns 99 | start = valueToRangeIndex(valueOfArrayIndex(array, 0), size, negativeIsZero) 100 | if len(array) == 1 { 101 | // If there is only the start argument, then end = size 102 | end = size 103 | return 104 | } 105 | 106 | // Assuming the argument is undefined... 107 | end = size 108 | endValue := valueOfArrayIndex(array, 1) 109 | if !endValue.IsUndefined() { 110 | // Which it is not, so get the value as an array index 111 | end = valueToRangeIndex(endValue, size, negativeIsZero) 112 | } 113 | return 114 | } 115 | 116 | func rangeStartLength(source []Value, size int64) (start, length int64) { //nolint:nonamedreturns 117 | start = valueToRangeIndex(valueOfArrayIndex(source, 0), size, false) 118 | 119 | // Assume the second argument is missing or undefined 120 | length = size 121 | if len(source) == 1 { 122 | // If there is only the start argument, then length = size 123 | return start, length 124 | } 125 | 126 | lengthValue := valueOfArrayIndex(source, 1) 127 | if !lengthValue.IsUndefined() { 128 | // Which it is not, so get the value as an array index 129 | length = lengthValue.number().int64 130 | } 131 | return start, length 132 | } 133 | 134 | func hereBeDragons(arguments ...interface{}) string { 135 | pc, _, _, _ := goruntime.Caller(1) //nolint:dogsled 136 | name := goruntime.FuncForPC(pc).Name() 137 | message := "Here be dragons -- " + name 138 | if len(arguments) > 0 { 139 | message += ": " 140 | argument0 := fmt.Sprintf("%s", arguments[0]) 141 | if len(arguments) == 1 { 142 | message += argument0 143 | } else { 144 | message += fmt.Sprintf(argument0, arguments[1:]...) 145 | } 146 | } else { 147 | message += "." 148 | } 149 | return message 150 | } 151 | -------------------------------------------------------------------------------- /otto_error_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestOttoError(t *testing.T) { 8 | tt(t, func() { 9 | vm := New() 10 | 11 | _, err := vm.Run(`throw "Xyzzy"`) 12 | is(err, "Xyzzy") 13 | 14 | _, err = vm.Run(`throw new TypeError()`) 15 | is(err, "TypeError") 16 | 17 | _, err = vm.Run(`throw new TypeError("Nothing happens.")`) 18 | is(err, "TypeError: Nothing happens.") 19 | 20 | _, err = ToValue([]byte{}) 21 | is(err, "TypeError: invalid value (slice): missing runtime: [] ([]uint8)") 22 | 23 | _, err = vm.Run(` 24 | (function(){ 25 | return abcdef.length 26 | })() 27 | `) 28 | is(err, "ReferenceError: 'abcdef' is not defined") 29 | 30 | _, err = vm.Run(` 31 | function start() { 32 | } 33 | 34 | start() 35 | 36 | xyzzy() 37 | `) 38 | is(err, "ReferenceError: 'xyzzy' is not defined") 39 | 40 | _, err = vm.Run(` 41 | // Just a comment 42 | 43 | xyzzy 44 | `) 45 | is(err, "ReferenceError: 'xyzzy' is not defined") 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /panic_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_panic(t *testing.T) { 8 | tt(t, func() { 9 | test, _ := test() 10 | 11 | // Test that property.value is set to something if writable is set 12 | // to something 13 | test(` 14 | var abc = []; 15 | Object.defineProperty(abc, "0", { writable: false }); 16 | Object.defineProperty(abc, "0", { writable: false }); 17 | "0" in abc; 18 | `, true) 19 | 20 | test(`raise: 21 | var abc = []; 22 | Object.defineProperty(abc, "0", { writable: false }); 23 | Object.defineProperty(abc, "0", { value: false, writable: false }); 24 | `, "TypeError: Array.DefineOwnProperty Object.DefineOwnProperty failed") 25 | 26 | // Test that a regular expression can contain \c0410 (CYRILLIC CAPITAL LETTER A) 27 | // without panicking 28 | test(` 29 | var abc = 0x0410; 30 | var def = String.fromCharCode(abc); 31 | new RegExp("\\c" + def).exec(def); 32 | `, "null") 33 | 34 | // Test transforming a transformable regular expression without a panic 35 | test(` 36 | new RegExp("\\u0000"); 37 | new RegExp("\\undefined").test("undefined"); 38 | `, true) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /parser/regexp_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | ) 7 | 8 | func TestRegExp(t *testing.T) { 9 | tt(t, func() { 10 | { 11 | // err 12 | test := func(input string, expect interface{}) { 13 | _, err := TransformRegExp(input) 14 | is(err, expect) 15 | } 16 | 17 | test("[", "Unterminated character class") 18 | 19 | test("(", "Unterminated group") 20 | 21 | test("(?=)", "re2: Invalid (?=) ") 22 | 23 | test("(?=)", "re2: Invalid (?=) ") 24 | 25 | test("(?!)", "re2: Invalid (?!) ") 26 | 27 | // An error anyway 28 | test("(?=", "re2: Invalid (?=) ") 29 | 30 | test("\\1", "re2: Invalid \\1 ") 31 | 32 | test("\\90", "re2: Invalid \\90 ") 33 | 34 | test("\\9123456789", "re2: Invalid \\9123456789 ") 35 | 36 | test("\\(?=)", "Unmatched ')'") 37 | 38 | test(")", "Unmatched ')'") 39 | } 40 | 41 | { 42 | // err 43 | test := func(input, expect string, expectErr interface{}) { 44 | output, err := TransformRegExp(input) 45 | is(output, expect) 46 | is(err, expectErr) 47 | } 48 | 49 | test("(?!)", "(?!)", "re2: Invalid (?!) ") 50 | 51 | test(")", "", "Unmatched ')'") 52 | 53 | test("(?!))", "", "re2: Invalid (?!) ") 54 | 55 | test("\\0", "\\0", nil) 56 | 57 | test("\\1", "\\1", "re2: Invalid \\1 ") 58 | 59 | test("\\9123456789", "\\9123456789", "re2: Invalid \\9123456789 ") 60 | } 61 | 62 | { 63 | // err 64 | test := func(input string, expect string) { 65 | result, err := TransformRegExp(input) 66 | is(err, nil) 67 | if is(result, expect) { 68 | _, err = regexp.Compile(result) 69 | if !is(err, nil) { 70 | t.Log(result) 71 | } 72 | } 73 | } 74 | 75 | test("", "") 76 | 77 | test("abc", "abc") 78 | 79 | test(`\abc`, `abc`) 80 | 81 | test(`\a\b\c`, `a\bc`) 82 | 83 | test(`\x`, `x`) 84 | 85 | test(`\c`, `c`) 86 | 87 | test(`\cA`, `\x01`) 88 | 89 | test(`\cz`, `\x1a`) 90 | 91 | test(`\ca`, `\x01`) 92 | 93 | test(`\cj`, `\x0a`) 94 | 95 | test(`\ck`, `\x0b`) 96 | 97 | test(`\+`, `\+`) 98 | 99 | test(`[\b]`, `[\x08]`) 100 | 101 | test(`\u0z01\x\undefined`, `u0z01xundefined`) 102 | 103 | test(`\\|'|\r|\n|\t|\u2028|\u2029`, `\\|'|\r|\n|\t|\x{2028}|\x{2029}`) 104 | 105 | test("]", "]") 106 | 107 | test("}", "}") 108 | 109 | test("%", "%") 110 | 111 | test("(%)", "(%)") 112 | 113 | test("(?:[%\\s])", "(?:[%\\s])") 114 | 115 | test("[[]", "[[]") 116 | 117 | test("\\101", "\\x41") 118 | 119 | test("\\51", "\\x29") 120 | 121 | test("\\051", "\\x29") 122 | 123 | test("\\175", "\\x7d") 124 | 125 | test("\\04", "\\x04") 126 | 127 | test(`<%([\s\S]+?)%>`, `<%([\s\S]+?)%>`) 128 | 129 | test(`(.)^`, "(.)^") 130 | 131 | test(`<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$`, `<%-([\s\S]+?)%>|<%=([\s\S]+?)%>|<%([\s\S]+?)%>|$`) 132 | 133 | test(`\$`, `\$`) 134 | 135 | test(`[G-b]`, `[G-b]`) 136 | 137 | test(`[G-b\0]`, `[G-b\0]`) 138 | } 139 | }) 140 | } 141 | 142 | func TestTransformRegExp(t *testing.T) { 143 | tt(t, func() { 144 | pattern, err := TransformRegExp(`\s+abc\s+`) 145 | is(err, nil) 146 | is(pattern, `\s+abc\s+`) 147 | is(regexp.MustCompile(pattern).MatchString("\t abc def"), true) 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /parser/scope.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/robertkrimen/otto/ast" 5 | ) 6 | 7 | type scope struct { 8 | outer *scope 9 | declarationList []ast.Declaration 10 | labels []string 11 | allowIn bool 12 | inIteration bool 13 | inSwitch bool 14 | inFunction bool 15 | } 16 | 17 | func (p *parser) openScope() { 18 | p.scope = &scope{ 19 | outer: p.scope, 20 | allowIn: true, 21 | } 22 | } 23 | 24 | func (p *parser) closeScope() { 25 | p.scope = p.scope.outer 26 | } 27 | 28 | func (p *scope) declare(declaration ast.Declaration) { 29 | p.declarationList = append(p.declarationList, declaration) 30 | } 31 | 32 | func (p *scope) hasLabel(name string) bool { 33 | for _, label := range p.labels { 34 | if label == name { 35 | return true 36 | } 37 | } 38 | if p.outer != nil && !p.inFunction { 39 | // Crossing a function boundary to look for a label is verboten 40 | return p.outer.hasLabel(name) 41 | } 42 | return false 43 | } 44 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPersistence(t *testing.T) { 8 | tt(t, func() { 9 | test, _ := test() 10 | 11 | test(` 12 | function abc() { return 1; } 13 | abc.toString(); 14 | `, "function abc() { return 1; }") 15 | 16 | test(` 17 | function def() { return 3.14159; } 18 | [ abc.toString(), def.toString() ]; 19 | `, "function abc() { return 1; },function def() { return 3.14159; }") 20 | 21 | test(` 22 | eval("function ghi() { return 'ghi' }"); 23 | [ abc.toString(), def.toString(), ghi.toString() ]; 24 | `, "function abc() { return 1; },function def() { return 3.14159; },function ghi() { return 'ghi' }") 25 | 26 | test(` 27 | [ abc.toString(), def.toString(), ghi.toString() ]; 28 | `, "function abc() { return 1; },function def() { return 3.14159; },function ghi() { return 'ghi' }") 29 | 30 | test(`/* 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | */`, UndefinedValue()) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /property.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | // property 4 | 5 | type propertyMode int 6 | 7 | const ( 8 | modeWriteMask propertyMode = 0o700 9 | modeEnumerateMask propertyMode = 0o070 10 | modeConfigureMask propertyMode = 0o007 11 | modeOnMask propertyMode = 0o111 12 | modeSetMask propertyMode = 0o222 // If value is 2, then mode is neither "On" nor "Off" 13 | ) 14 | 15 | type propertyGetSet [2]*object 16 | 17 | var nilGetSetObject = object{} 18 | 19 | type property struct { 20 | value interface{} 21 | mode propertyMode 22 | } 23 | 24 | func (p property) writable() bool { 25 | return p.mode&modeWriteMask == modeWriteMask&modeOnMask 26 | } 27 | 28 | func (p *property) writeOn() { 29 | p.mode = (p.mode & ^modeWriteMask) | (modeWriteMask & modeOnMask) 30 | } 31 | 32 | func (p *property) writeOff() { 33 | p.mode &= ^modeWriteMask 34 | } 35 | 36 | func (p *property) writeClear() { 37 | p.mode = (p.mode & ^modeWriteMask) | (modeWriteMask & modeSetMask) 38 | } 39 | 40 | func (p property) writeSet() bool { 41 | return p.mode&modeWriteMask&modeSetMask == 0 42 | } 43 | 44 | func (p property) enumerable() bool { 45 | return p.mode&modeEnumerateMask == modeEnumerateMask&modeOnMask 46 | } 47 | 48 | func (p *property) enumerateOn() { 49 | p.mode = (p.mode & ^modeEnumerateMask) | (modeEnumerateMask & modeOnMask) 50 | } 51 | 52 | func (p *property) enumerateOff() { 53 | p.mode &= ^modeEnumerateMask 54 | } 55 | 56 | func (p property) enumerateSet() bool { 57 | return p.mode&modeEnumerateMask&modeSetMask == 0 58 | } 59 | 60 | func (p property) configurable() bool { 61 | return p.mode&modeConfigureMask == modeConfigureMask&modeOnMask 62 | } 63 | 64 | func (p *property) configureOn() { 65 | p.mode = (p.mode & ^modeConfigureMask) | (modeConfigureMask & modeOnMask) 66 | } 67 | 68 | func (p *property) configureOff() { 69 | p.mode &= ^modeConfigureMask 70 | } 71 | 72 | func (p property) configureSet() bool { //nolint:unused 73 | return p.mode&modeConfigureMask&modeSetMask == 0 74 | } 75 | 76 | func (p property) copy() *property { //nolint:unused 77 | cpy := p 78 | return &cpy 79 | } 80 | 81 | func (p property) get(this *object) Value { 82 | switch value := p.value.(type) { 83 | case Value: 84 | return value 85 | case propertyGetSet: 86 | if value[0] != nil { 87 | return value[0].call(toValue(this), nil, false, nativeFrame) 88 | } 89 | } 90 | return Value{} 91 | } 92 | 93 | func (p property) isAccessorDescriptor() bool { 94 | setGet, test := p.value.(propertyGetSet) 95 | return test && (setGet[0] != nil || setGet[1] != nil) 96 | } 97 | 98 | func (p property) isDataDescriptor() bool { 99 | if p.writeSet() { // Either "On" or "Off" 100 | return true 101 | } 102 | value, valid := p.value.(Value) 103 | return valid && !value.isEmpty() 104 | } 105 | 106 | func (p property) isGenericDescriptor() bool { 107 | return !(p.isDataDescriptor() || p.isAccessorDescriptor()) 108 | } 109 | 110 | func (p property) isEmpty() bool { 111 | return p.mode == 0o222 && p.isGenericDescriptor() 112 | } 113 | 114 | // _enumerableValue, _enumerableTrue, _enumerableFalse? 115 | // .enumerableValue() .enumerableExists() 116 | 117 | func toPropertyDescriptor(rt *runtime, value Value) property { 118 | objectDescriptor := value.object() 119 | if objectDescriptor == nil { 120 | panic(rt.panicTypeError("toPropertyDescriptor on nil")) 121 | } 122 | 123 | var descriptor property 124 | descriptor.mode = modeSetMask // Initially nothing is set 125 | if objectDescriptor.hasProperty("enumerable") { 126 | if objectDescriptor.get("enumerable").bool() { 127 | descriptor.enumerateOn() 128 | } else { 129 | descriptor.enumerateOff() 130 | } 131 | } 132 | 133 | if objectDescriptor.hasProperty("configurable") { 134 | if objectDescriptor.get("configurable").bool() { 135 | descriptor.configureOn() 136 | } else { 137 | descriptor.configureOff() 138 | } 139 | } 140 | 141 | if objectDescriptor.hasProperty("writable") { 142 | if objectDescriptor.get("writable").bool() { 143 | descriptor.writeOn() 144 | } else { 145 | descriptor.writeOff() 146 | } 147 | } 148 | 149 | var getter, setter *object 150 | getterSetter := false 151 | 152 | if objectDescriptor.hasProperty("get") { 153 | val := objectDescriptor.get("get") 154 | if val.IsDefined() { 155 | if !val.isCallable() { 156 | panic(rt.panicTypeError("toPropertyDescriptor get not callable")) 157 | } 158 | getter = val.object() 159 | getterSetter = true 160 | } else { 161 | getter = &nilGetSetObject 162 | getterSetter = true 163 | } 164 | } 165 | 166 | if objectDescriptor.hasProperty("set") { 167 | val := objectDescriptor.get("set") 168 | if val.IsDefined() { 169 | if !val.isCallable() { 170 | panic(rt.panicTypeError("toPropertyDescriptor set not callable")) 171 | } 172 | setter = val.object() 173 | getterSetter = true 174 | } else { 175 | setter = &nilGetSetObject 176 | getterSetter = true 177 | } 178 | } 179 | 180 | if getterSetter { 181 | if descriptor.writeSet() { 182 | panic(rt.panicTypeError("toPropertyDescriptor descriptor writeSet")) 183 | } 184 | descriptor.value = propertyGetSet{getter, setter} 185 | } 186 | 187 | if objectDescriptor.hasProperty("value") { 188 | if getterSetter { 189 | panic(rt.panicTypeError("toPropertyDescriptor value getterSetter")) 190 | } 191 | descriptor.value = objectDescriptor.get("value") 192 | } 193 | 194 | return descriptor 195 | } 196 | 197 | func (rt *runtime) fromPropertyDescriptor(descriptor property) *object { 198 | obj := rt.newObject() 199 | if descriptor.isDataDescriptor() { 200 | obj.defineProperty("value", descriptor.value.(Value), 0o111, false) 201 | obj.defineProperty("writable", boolValue(descriptor.writable()), 0o111, false) 202 | } else if descriptor.isAccessorDescriptor() { 203 | getSet := descriptor.value.(propertyGetSet) 204 | get := Value{} 205 | if getSet[0] != nil { 206 | get = objectValue(getSet[0]) 207 | } 208 | set := Value{} 209 | if getSet[1] != nil { 210 | set = objectValue(getSet[1]) 211 | } 212 | obj.defineProperty("get", get, 0o111, false) 213 | obj.defineProperty("set", set, 0o111, false) 214 | } 215 | obj.defineProperty("enumerable", boolValue(descriptor.enumerable()), 0o111, false) 216 | obj.defineProperty("configurable", boolValue(descriptor.configurable()), 0o111, false) 217 | return obj 218 | } 219 | -------------------------------------------------------------------------------- /registry/registry.go: -------------------------------------------------------------------------------- 1 | // Package registry is an experimental package to facilitate altering the otto runtime via import. 2 | // 3 | // This interface can change at any time. 4 | package registry 5 | 6 | var registry []*Entry = make([]*Entry, 0) 7 | 8 | // Entry represents a registry entry. 9 | type Entry struct { 10 | source func() string 11 | active bool 12 | } 13 | 14 | // newEntry returns a new Entry for source. 15 | func newEntry(source func() string) *Entry { 16 | return &Entry{ 17 | active: true, 18 | source: source, 19 | } 20 | } 21 | 22 | // Enable enables the entry. 23 | func (e *Entry) Enable() { 24 | e.active = true 25 | } 26 | 27 | // Disable disables the entry. 28 | func (e *Entry) Disable() { 29 | e.active = false 30 | } 31 | 32 | // Source returns the source of the entry. 33 | func (e Entry) Source() string { 34 | return e.source() 35 | } 36 | 37 | // Apply applies callback to all registry entries. 38 | func Apply(callback func(Entry)) { 39 | for _, entry := range registry { 40 | if !entry.active { 41 | continue 42 | } 43 | callback(*entry) 44 | } 45 | } 46 | 47 | // Register registers a new Entry for source. 48 | func Register(source func() string) *Entry { 49 | entry := newEntry(source) 50 | registry = append(registry, entry) 51 | return entry 52 | } 53 | -------------------------------------------------------------------------------- /repl/autocompleter.go: -------------------------------------------------------------------------------- 1 | package repl 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/robertkrimen/otto" 8 | ) 9 | 10 | type autoCompleter struct { 11 | vm *otto.Otto 12 | } 13 | 14 | var lastExpressionRegex = regexp.MustCompile(`[a-zA-Z0-9]([a-zA-Z0-9\.]*[a-zA-Z0-9])?\.?$`) 15 | 16 | func (a *autoCompleter) Do(line []rune, pos int) ([][]rune, int) { 17 | lastExpression := lastExpressionRegex.FindString(string(line)) 18 | 19 | bits := strings.Split(lastExpression, ".") 20 | 21 | var l []string 22 | if first := bits[:len(bits)-1]; len(first) == 0 { 23 | c := a.vm.Context() 24 | 25 | l = make([]string, len(c.Symbols)) 26 | 27 | i := 0 28 | for k := range c.Symbols { 29 | l[i] = k 30 | i++ 31 | } 32 | } else { 33 | r, err := a.vm.Eval(strings.Join(bits[:len(bits)-1], ".")) 34 | if err != nil { 35 | return nil, 0 36 | } 37 | 38 | if o := r.Object(); o != nil { 39 | for _, v := range o.KeysByParent() { 40 | l = append(l, v...) //nolint:makezero 41 | } 42 | } 43 | } 44 | 45 | last := bits[len(bits)-1] 46 | var r [][]rune 47 | for _, s := range l { 48 | if strings.HasPrefix(s, last) { 49 | r = append(r, []rune(strings.TrimPrefix(s, last))) 50 | } 51 | } 52 | 53 | return r, len(last) 54 | } 55 | -------------------------------------------------------------------------------- /repl/repl.go: -------------------------------------------------------------------------------- 1 | // Package repl implements a REPL (read-eval-print loop) for otto. 2 | package repl 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "sync/atomic" 10 | 11 | "github.com/robertkrimen/otto" 12 | "gopkg.in/readline.v1" 13 | ) 14 | 15 | var counter uint32 16 | 17 | // DebuggerHandler implements otto's debugger handler signature, providing a 18 | // simple drop-in debugger implementation. 19 | func DebuggerHandler(vm *otto.Otto) { 20 | i := atomic.AddUint32(&counter, 1) 21 | 22 | // purposefully ignoring the error here - we can't do anything useful with 23 | // it except panicking, and that'd be pretty rude. it'd be easy enough for a 24 | // consumer to define an equivalent function that _does_ panic if desired. 25 | _ = RunWithPrompt(vm, fmt.Sprintf("DEBUGGER[%d]> ", i)) 26 | } 27 | 28 | // Run creates a REPL with the default prompt and no prelude. 29 | func Run(vm *otto.Otto) error { 30 | return RunWithOptions(vm, Options{}) 31 | } 32 | 33 | // RunWithPrompt runs a REPL with the given prompt and no prelude. 34 | func RunWithPrompt(vm *otto.Otto, prompt string) error { 35 | return RunWithOptions(vm, Options{ 36 | Prompt: prompt, 37 | }) 38 | } 39 | 40 | // RunWithPrelude runs a REPL with the default prompt and the given prelude. 41 | func RunWithPrelude(vm *otto.Otto, prelude string) error { 42 | return RunWithOptions(vm, Options{ 43 | Prelude: prelude, 44 | }) 45 | } 46 | 47 | // RunWithPromptAndPrelude runs a REPL with the given prompt and prelude. 48 | func RunWithPromptAndPrelude(vm *otto.Otto, prompt, prelude string) error { 49 | return RunWithOptions(vm, Options{ 50 | Prompt: prompt, 51 | Prelude: prelude, 52 | }) 53 | } 54 | 55 | // Options contains parameters for configuring a REPL session. 56 | type Options struct { 57 | // Prompt controls what's shown at the beginning of the line. If not 58 | // specified, this defaults to "> " 59 | Prompt string 60 | // Prelude is some text that's printed before control is given to the user. 61 | // By default this is empty. If specified, this will be printed with a 62 | // newline following it. 63 | Prelude string 64 | // Autocomplete controls whether this REPL session has autocompletion 65 | // enabled. The way autocomplete is implemented can incur a performance 66 | // penalty, so it's turned off by default. 67 | Autocomplete bool 68 | } 69 | 70 | // RunWithOptions runs a REPL with the given options. 71 | func RunWithOptions(vm *otto.Otto, options Options) error { 72 | prompt := options.Prompt 73 | if prompt == "" { 74 | prompt = "> " 75 | } 76 | 77 | c := &readline.Config{ 78 | Prompt: prompt, 79 | } 80 | 81 | if options.Autocomplete { 82 | c.AutoComplete = &autoCompleter{vm} 83 | } 84 | 85 | rl, err := readline.NewEx(c) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | prelude := options.Prelude 91 | if prelude != "" { 92 | if _, err = io.Copy(rl.Stderr(), strings.NewReader(prelude+"\n")); err != nil { 93 | return err 94 | } 95 | 96 | rl.Refresh() 97 | } 98 | 99 | var d []string 100 | 101 | for { 102 | l, errRL := rl.Readline() 103 | if errRL != nil { 104 | if errors.Is(errRL, readline.ErrInterrupt) { 105 | if d != nil { 106 | d = nil 107 | 108 | rl.SetPrompt(prompt) 109 | rl.Refresh() 110 | 111 | continue 112 | } 113 | 114 | break 115 | } 116 | 117 | return errRL 118 | } 119 | 120 | if l == "" { 121 | continue 122 | } 123 | 124 | d = append(d, l) 125 | 126 | s, errRL := vm.Compile("repl", strings.Join(d, "\n")) 127 | if errRL != nil { 128 | rl.SetPrompt(strings.Repeat(" ", len(prompt))) 129 | } else { 130 | rl.SetPrompt(prompt) 131 | 132 | d = nil 133 | 134 | v, errEval := vm.Eval(s) 135 | if errEval != nil { 136 | var oerr *otto.Error 137 | if errors.As(errEval, &oerr) { 138 | if _, err = io.Copy(rl.Stdout(), strings.NewReader(oerr.String())); err != nil { 139 | return fmt.Errorf("write out: %w", err) 140 | } 141 | } else { 142 | if _, err = io.Copy(rl.Stdout(), strings.NewReader(errEval.Error())); err != nil { 143 | return fmt.Errorf("write out: %w", err) 144 | } 145 | } 146 | } else { 147 | if _, err = rl.Stdout().Write([]byte(v.String() + "\n")); err != nil { 148 | return fmt.Errorf("write out: %w", err) 149 | } 150 | } 151 | } 152 | 153 | rl.Refresh() 154 | } 155 | 156 | return rl.Close() 157 | } 158 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | type resultKind int 4 | 5 | const ( 6 | _ resultKind = iota 7 | resultReturn 8 | resultBreak 9 | resultContinue 10 | ) 11 | 12 | type result struct { 13 | value Value 14 | target string 15 | kind resultKind 16 | } 17 | 18 | func newReturnResult(value Value) result { 19 | return result{kind: resultReturn, value: value, target: ""} 20 | } 21 | 22 | func newContinueResult(target string) result { 23 | return result{kind: resultContinue, value: emptyValue, target: target} 24 | } 25 | 26 | func newBreakResult(target string) result { 27 | return result{kind: resultBreak, value: emptyValue, target: target} 28 | } 29 | -------------------------------------------------------------------------------- /scope.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | // An ECMA-262 ExecutionContext. 4 | type scope struct { 5 | lexical stasher 6 | variable stasher 7 | this *object 8 | outer *scope 9 | frame frame 10 | depth int 11 | eval bool 12 | } 13 | 14 | func newScope(lexical stasher, variable stasher, this *object) *scope { 15 | return &scope{ 16 | lexical: lexical, 17 | variable: variable, 18 | this: this, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /script.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "errors" 7 | ) 8 | 9 | // ErrVersion is an error which represents a version mismatch. 10 | var ErrVersion = errors.New("version mismatch") 11 | 12 | var scriptVersion = "2014-04-13/1" 13 | 14 | // Script is a handle for some (reusable) JavaScript. 15 | // Passing a Script value to a run method will evaluate the JavaScript. 16 | type Script struct { 17 | version string 18 | program *nodeProgram 19 | filename string 20 | src string 21 | } 22 | 23 | // Compile will parse the given source and return a Script value or nil and 24 | // an error if there was a problem during compilation. 25 | // 26 | // script, err := vm.Compile("", `var abc; if (!abc) abc = 0; abc += 2; abc;`) 27 | // vm.Run(script) 28 | func (o *Otto) Compile(filename string, src interface{}) (*Script, error) { 29 | return o.CompileWithSourceMap(filename, src, nil) 30 | } 31 | 32 | // CompileWithSourceMap does the same thing as Compile, but with the obvious 33 | // difference of applying a source map. 34 | func (o *Otto) CompileWithSourceMap(filename string, src, sm interface{}) (*Script, error) { 35 | program, err := o.runtime.parse(filename, src, sm) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | node := cmplParse(program) 41 | script := &Script{ 42 | version: scriptVersion, 43 | program: node, 44 | filename: filename, 45 | src: program.File.Source(), 46 | } 47 | 48 | return script, nil 49 | } 50 | 51 | func (s *Script) String() string { 52 | return "// " + s.filename + "\n" + s.src 53 | } 54 | 55 | // MarshalBinary will marshal a script into a binary form. A marshalled script 56 | // that is later unmarshalled can be executed on the same version of the otto runtime. 57 | // 58 | // The binary format can change at any time and should be considered unspecified and opaque. 59 | func (s *Script) marshalBinary() ([]byte, error) { 60 | var bfr bytes.Buffer 61 | encoder := gob.NewEncoder(&bfr) 62 | err := encoder.Encode(s.version) 63 | if err != nil { 64 | return nil, err 65 | } 66 | err = encoder.Encode(s.program) 67 | if err != nil { 68 | return nil, err 69 | } 70 | err = encoder.Encode(s.filename) 71 | if err != nil { 72 | return nil, err 73 | } 74 | err = encoder.Encode(s.src) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return bfr.Bytes(), nil 79 | } 80 | 81 | // UnmarshalBinary will vivify a marshalled script into something usable. If the script was 82 | // originally marshalled on a different version of the otto runtime, then this method 83 | // will return an error. 84 | // 85 | // The binary format can change at any time and should be considered unspecified and opaque. 86 | func (s *Script) unmarshalBinary(data []byte) (err error) { //nolint:nonamedreturns 87 | decoder := gob.NewDecoder(bytes.NewReader(data)) 88 | defer func() { 89 | if err != nil { 90 | s.version = "" 91 | s.program = nil 92 | s.filename = "" 93 | s.src = "" 94 | } 95 | }() 96 | if err = decoder.Decode(&s.version); err != nil { 97 | return err 98 | } 99 | if s.version != scriptVersion { 100 | return ErrVersion 101 | } 102 | if err = decoder.Decode(&s.program); err != nil { 103 | return err 104 | } 105 | if err = decoder.Decode(&s.filename); err != nil { 106 | return err 107 | } 108 | return decoder.Decode(&s.src) 109 | } 110 | -------------------------------------------------------------------------------- /script_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestScript(t *testing.T) { 10 | tt(t, func() { 11 | vm := New() 12 | 13 | script, err := vm.Compile("xyzzy", `var abc; if (!abc) abc = 0; abc += 2; abc;`) 14 | require.NoError(t, err) 15 | 16 | str := script.String() 17 | is(str, "// xyzzy\nvar abc; if (!abc) abc = 0; abc += 2; abc;") 18 | 19 | val, err := vm.Run(script) 20 | require.NoError(t, err) 21 | is(val, 2) 22 | 23 | // TODO(steve): Fix the underlying issues as to why this returns early. 24 | if true { 25 | return 26 | } 27 | 28 | tmp, err := script.marshalBinary() 29 | require.NoError(t, err) 30 | is(len(tmp), 1228) 31 | 32 | { 33 | script2 := &Script{} 34 | err = script2.unmarshalBinary(tmp) 35 | require.NoError(t, err) 36 | 37 | is(script2.String(), str) 38 | 39 | val, err = vm.Run(script2) 40 | require.NoError(t, err) 41 | is(val, 4) 42 | 43 | tmp, err = script2.marshalBinary() 44 | require.NoError(t, err) 45 | is(len(tmp), 1228) 46 | } 47 | 48 | { 49 | script2 := &Script{} 50 | err = script2.unmarshalBinary(tmp) 51 | require.NoError(t, err) 52 | 53 | is(script2.String(), str) 54 | 55 | val2, err2 := vm.Run(script2) 56 | require.NoError(t, err2) 57 | is(val2, 6) 58 | 59 | tmp, err2 = script2.marshalBinary() 60 | require.NoError(t, err2) 61 | is(len(tmp), 1228) 62 | } 63 | 64 | { 65 | version := scriptVersion 66 | scriptVersion = "bogus" 67 | 68 | script2 := &Script{} 69 | err = script2.unmarshalBinary(tmp) 70 | is(err, "version mismatch") 71 | 72 | is(script2.String(), "// \n") 73 | is(script2.version, "") 74 | is(script2.program == nil, true) 75 | is(script2.filename, "") 76 | is(script2.src, "") 77 | 78 | scriptVersion = version 79 | } 80 | }) 81 | } 82 | 83 | func TestFunctionCall_CallerLocation(t *testing.T) { 84 | tt(t, func() { 85 | vm := New() 86 | err := vm.Set("loc", func(call FunctionCall) Value { 87 | return toValue(call.CallerLocation()) 88 | }) 89 | require.NoError(t, err) 90 | script, err := vm.Compile("somefile.js", `var where = loc();`) 91 | require.NoError(t, err) 92 | _, err = vm.Run(script) 93 | require.NoError(t, err) 94 | where, err := vm.Get("where") 95 | require.NoError(t, err) 96 | is(where, "somefile.js:1:13") 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /sourcemap_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | const ( 10 | testSourcemapCodeOriginal = "function functionA(argA, argB) {\n functionB(argA, argB);\n}\n\nfunction functionB(argA, argB) {\n functionExternal(argA, argB);\n}" 11 | testSourcemapCodeMangled = "function functionA(argA,argB){functionB(argA,argB)}function functionB(argA,argB){functionExternal(argA,argB)}" 12 | 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"}` 13 | 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=" 14 | 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" 15 | 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" 16 | 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" 17 | ) 18 | 19 | func TestSourceMapOriginalWithNoSourcemap(t *testing.T) { 20 | tt(t, func() { 21 | vm := New() 22 | 23 | s, err := vm.Compile("hello.js", testSourcemapCodeOriginal) 24 | require.NoError(t, err) 25 | 26 | _, err = vm.Run(s) 27 | require.NoError(t, err) 28 | 29 | _, err = vm.Run(`functionA()`) 30 | require.Error(t, err) 31 | var oerr *Error 32 | require.ErrorAs(t, err, &oerr) 33 | require.Equal(t, testSourcemapOriginalStack, oerr.String()) 34 | }) 35 | } 36 | 37 | func TestSourceMapMangledWithNoSourcemap(t *testing.T) { 38 | tt(t, func() { 39 | vm := New() 40 | 41 | s, err := vm.Compile("hello.js", testSourcemapCodeMangled) 42 | require.NoError(t, err) 43 | 44 | _, err = vm.Run(s) 45 | require.NoError(t, err) 46 | 47 | _, err = vm.Run(`functionA()`) 48 | require.Error(t, err) 49 | var oerr *Error 50 | require.ErrorAs(t, err, &oerr) 51 | require.Equal(t, testSourcemapMangledStack, oerr.String()) 52 | }) 53 | } 54 | 55 | func TestSourceMapMangledWithSourcemap(t *testing.T) { 56 | tt(t, func() { 57 | vm := New() 58 | 59 | s, err := vm.CompileWithSourceMap("hello.js", testSourcemapCodeMangled, testSourcemapContent) 60 | require.NoError(t, err) 61 | 62 | _, err = vm.Run(s) 63 | require.NoError(t, err) 64 | 65 | _, err = vm.Run(`functionA()`) 66 | require.Error(t, err) 67 | var oerr *Error 68 | require.ErrorAs(t, err, &oerr) 69 | require.Equal(t, testSourcemapMappedStack, oerr.String()) 70 | }) 71 | } 72 | 73 | func TestSourceMapMangledWithInlineSourcemap(t *testing.T) { 74 | tt(t, func() { 75 | vm := New() 76 | 77 | s, err := vm.CompileWithSourceMap("hello.js", testSourcemapInline, nil) 78 | require.NoError(t, err) 79 | 80 | _, err = vm.Run(s) 81 | require.NoError(t, err) 82 | 83 | _, err = vm.Run(`functionA()`) 84 | require.Error(t, err) 85 | var oerr *Error 86 | require.ErrorAs(t, err, &oerr) 87 | require.Equal(t, testSourcemapMappedStack, oerr.String()) 88 | }) 89 | } 90 | 91 | func TestSourceMapContextPosition(t *testing.T) { 92 | tt(t, func() { 93 | vm := New() 94 | 95 | s, err := vm.CompileWithSourceMap("hello.js", testSourcemapCodeMangled, testSourcemapContent) 96 | require.NoError(t, err) 97 | 98 | _, err = vm.Run(s) 99 | require.NoError(t, err) 100 | 101 | err = vm.Set("functionExternal", func(c FunctionCall) Value { 102 | ctx := c.Otto.Context() 103 | 104 | is(ctx.Filename, "hello.js") 105 | is(ctx.Line, 6) 106 | is(ctx.Column, 2) 107 | 108 | return UndefinedValue() 109 | }) 110 | require.NoError(t, err) 111 | 112 | _, err = vm.Run(`functionA()`) 113 | require.NoError(t, err) 114 | }) 115 | } 116 | 117 | func TestSourceMapContextStacktrace(t *testing.T) { 118 | tt(t, func() { 119 | vm := New() 120 | 121 | s, err := vm.CompileWithSourceMap("hello.js", testSourcemapCodeMangled, testSourcemapContent) 122 | require.NoError(t, err) 123 | 124 | _, err = vm.Run(s) 125 | require.NoError(t, err) 126 | 127 | err = vm.Set("functionExternal", func(c FunctionCall) Value { 128 | ctx := c.Otto.Context() 129 | 130 | is(ctx.Stacktrace, []string{ 131 | "functionB (hello.js:6:2)", 132 | "functionA (hello.js:2:2)", 133 | ":1:1", 134 | }) 135 | 136 | return UndefinedValue() 137 | }) 138 | require.NoError(t, err) 139 | 140 | _, err = vm.Run(`functionA()`) 141 | require.NoError(t, err) 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /testing_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/robertkrimen/otto/terst" 10 | ) 11 | 12 | func tt(t *testing.T, arguments ...func()) { 13 | t.Helper() 14 | halt := errors.New("A test was taking too long") 15 | timer := time.AfterFunc(20*time.Second, func() { 16 | panic(halt) 17 | }) 18 | defer func() { 19 | timer.Stop() 20 | }() 21 | terst.Terst(t, arguments...) 22 | } 23 | 24 | func is(arguments ...interface{}) bool { 25 | var got, expect interface{} 26 | 27 | switch len(arguments) { 28 | case 0, 1: 29 | return terst.Is(arguments...) 30 | case 2: 31 | got, expect = arguments[0], arguments[1] 32 | default: 33 | got, expect = arguments[0], arguments[2] 34 | } 35 | 36 | switch value := got.(type) { 37 | case Value: 38 | if value.value != nil { 39 | got = value.value 40 | } 41 | case *Error: 42 | if value != nil { 43 | got = value.Error() 44 | } 45 | if expect == nil { 46 | // FIXME This is weird 47 | expect = "" 48 | } 49 | } 50 | 51 | if len(arguments) == 2 { 52 | arguments[0] = got 53 | arguments[1] = expect 54 | } else { 55 | arguments[0] = got 56 | arguments[2] = expect 57 | } 58 | 59 | return terst.Is(arguments...) 60 | } 61 | 62 | func test(arguments ...interface{}) (func(string, ...interface{}) Value, *_tester) { 63 | tester := newTester() 64 | if len(arguments) > 0 { 65 | tester.test(arguments[0].(string)) 66 | } 67 | return tester.test, tester 68 | } 69 | 70 | type _tester struct { 71 | vm *Otto 72 | } 73 | 74 | func newTester() *_tester { 75 | return &_tester{ 76 | vm: New(), 77 | } 78 | } 79 | 80 | func (te *_tester) Get(name string) (Value, error) { 81 | return te.vm.Get(name) 82 | } 83 | 84 | func (te *_tester) Set(name string, value interface{}) Value { 85 | err := te.vm.Set(name, value) 86 | is(err, nil) 87 | if err != nil { 88 | terst.Caller().T().FailNow() 89 | } 90 | return te.vm.getValue(name) 91 | } 92 | 93 | func (te *_tester) Run(src interface{}) (Value, error) { 94 | return te.vm.Run(src) 95 | } 96 | 97 | func (te *_tester) test(name string, expect ...interface{}) Value { 98 | vm := te.vm 99 | raise := false 100 | defer func() { 101 | if caught := recover(); caught != nil { 102 | if exception, ok := caught.(*exception); ok { 103 | caught = exception.eject() 104 | } 105 | if raise { 106 | if len(expect) > 0 { 107 | is(caught, expect[0]) 108 | } 109 | } else { 110 | dbg("Panic, caught:", caught) 111 | panic(caught) 112 | } 113 | } 114 | }() 115 | var value Value 116 | var err error 117 | if isIdentifier(name) { 118 | value = vm.getValue(name) 119 | } else { 120 | source := name 121 | index := strings.Index(source, "raise:") 122 | if index == 0 { 123 | raise = true 124 | source = source[6:] 125 | source = strings.TrimLeft(source, " ") 126 | } 127 | value, err = vm.runtime.cmplRun(source, nil) 128 | if err != nil { 129 | panic(err) 130 | } 131 | } 132 | value = value.resolve() 133 | if len(expect) > 0 { 134 | is(value, expect[0]) 135 | } 136 | return value 137 | } 138 | -------------------------------------------------------------------------------- /token/generate.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | //go:generate go run ../tools/gen-tokens -output token_const.go 4 | -------------------------------------------------------------------------------- /token/token.go: -------------------------------------------------------------------------------- 1 | // Package token defines constants representing the lexical tokens of JavaScript (ECMA5). 2 | package token 3 | 4 | import ( 5 | "strconv" 6 | ) 7 | 8 | // Token is the set of lexical tokens in JavaScript (ECMA5). 9 | type Token int 10 | 11 | // String returns the string corresponding to the token. 12 | // For operators, delimiters, and keywords the string is the actual 13 | // token string (e.g., for the token PLUS, the String() is 14 | // "+"). For all other tokens the string corresponds to the token 15 | // name (e.g. for the token IDENTIFIER, the string is "IDENTIFIER"). 16 | func (tkn Token) String() string { 17 | switch { 18 | case tkn == 0: 19 | return "UNKNOWN" 20 | case tkn < Token(len(token2string)): 21 | return token2string[tkn] 22 | default: 23 | return "token(" + strconv.Itoa(int(tkn)) + ")" 24 | } 25 | } 26 | 27 | type keyword struct { 28 | token Token 29 | futureKeyword bool 30 | strict bool 31 | } 32 | 33 | // IsKeyword returns the keyword token if literal is a keyword, a KEYWORD token 34 | // if the literal is a future keyword (const, let, class, super, ...), or 0 if the literal is not a keyword. 35 | // 36 | // If the literal is a keyword, IsKeyword returns a second value indicating if the literal 37 | // is considered a future keyword in strict-mode only. 38 | // 39 | // 7.6.1.2 Future Reserved Words: 40 | // 41 | // const 42 | // class 43 | // enum 44 | // export 45 | // extends 46 | // import 47 | // super 48 | // 49 | // 7.6.1.2 Future Reserved Words (strict): 50 | // 51 | // implements 52 | // interface 53 | // let 54 | // package 55 | // private 56 | // protected 57 | // public 58 | // static 59 | func IsKeyword(literal string) (Token, bool) { 60 | if kw, exists := keywordTable[literal]; exists { 61 | if kw.futureKeyword { 62 | return KEYWORD, kw.strict 63 | } 64 | return kw.token, false 65 | } 66 | return 0, false 67 | } 68 | -------------------------------------------------------------------------------- /tools/gen-jscore/helpers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "unicode" 6 | ) 7 | 8 | // ucfirst converts the first rune of val to uppercase an returns the result. 9 | func ucfirst(val string) string { 10 | r := []rune(val) 11 | r[0] = unicode.ToUpper(r[0]) 12 | 13 | return string(r) 14 | } 15 | 16 | // dict is a template helper returns a map created from alternating values. 17 | // Values must be passed as key, value pairs with key being a string. 18 | // It can be used to pass the combination of multiple values to a template. 19 | func dict(values ...interface{}) (map[string]interface{}, error) { 20 | if len(values)%2 != 0 { 21 | return nil, fmt.Errorf("map requires parameters which are multiple of 2 got %d", len(values)) 22 | } 23 | 24 | m := make(map[string]interface{}, len(values)/2) 25 | for i := 0; i < len(values); i += 2 { 26 | key, ok := values[i].(string) 27 | if !ok { 28 | return nil, fmt.Errorf("map keys must be strings got %T", values[i]) 29 | } 30 | m[key] = values[i+1] 31 | } 32 | 33 | return m, nil 34 | } 35 | -------------------------------------------------------------------------------- /tools/gen-jscore/main.go: -------------------------------------------------------------------------------- 1 | // Command gen-jscore generates go representations of JavaScript core types file. 2 | package main 3 | 4 | import ( 5 | "embed" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | "text/template" 13 | 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | //go:embed .gen-jscore.yaml 18 | var configData []byte 19 | 20 | //go:embed templates/* 21 | var templates embed.FS 22 | 23 | // jsType represents JavaScript type to generate. 24 | type jsType struct { 25 | Prototype *prototype `yaml:"prototype"` 26 | Name string `yaml:"name"` 27 | ObjectClass string `yaml:"objectClass"` 28 | ObjectPrototype string `yaml:"objectPrototype"` 29 | Class string `yaml:"class"` 30 | Value string `yaml:"value"` 31 | Properties []property `yaml:"properties"` 32 | Core bool `yaml:"core"` 33 | } 34 | 35 | // BlankConstructor is a default fallback returning false for templates. 36 | func (t jsType) BlankConstructor() bool { 37 | return false 38 | } 39 | 40 | // prototype represents a JavaScript prototype to generate. 41 | type prototype struct { 42 | Value string `yaml:"value"` 43 | ObjectClass string `yaml:"objectClass"` 44 | Prototype string `yaml:"prototype"` 45 | Properties []property `yaml:"properties"` 46 | } 47 | 48 | // Property returns the property with the given name. 49 | func (p prototype) Property(name string) (*property, error) { 50 | for _, prop := range p.Properties { 51 | if prop.Name == name { 52 | return &prop, nil 53 | } 54 | } 55 | 56 | return nil, fmt.Errorf("missing property %q", name) 57 | } 58 | 59 | // property represents a JavaScript property to generate. 60 | type property struct { 61 | Name string `yaml:"name"` 62 | Call string `yaml:"call"` 63 | Mode string `yaml:"mode"` 64 | Value string `yaml:"value"` 65 | Kind string `yaml:"kind"` 66 | Function int `yaml:"function"` 67 | } 68 | 69 | // value represents a JavaScript value to generate a Value creator for. 70 | type value struct { 71 | Name string `yaml:"name"` 72 | Type string `yaml:"type"` 73 | } 74 | 75 | // config represents our configuration. 76 | type config struct { 77 | Types []jsType `yaml:"types"` 78 | Values []value `yaml:"values"` 79 | Log jsType `yaml:"log"` 80 | } 81 | 82 | // Type returns the type for name. 83 | func (c config) Type(name string) (*jsType, error) { 84 | for _, t := range c.Types { 85 | if t.Name == name { 86 | return &t, nil 87 | } 88 | } 89 | 90 | return nil, fmt.Errorf("missing type %q", name) 91 | } 92 | 93 | // generate generates the context file writing the output to filename. 94 | func generate(filename string) (err error) { 95 | var cfg config 96 | if err = yaml.Unmarshal(configData, &cfg); err != nil { 97 | return fmt.Errorf("decode config: %w", err) 98 | } 99 | 100 | tmpl := template.New("base").Funcs(template.FuncMap{ 101 | "ucfirst": ucfirst, 102 | "dict": dict, 103 | "contains": strings.Contains, 104 | }) 105 | 106 | tmpl, err = tmpl.ParseFS(templates, "templates/*.tmpl") 107 | if err != nil { 108 | return fmt.Errorf("parse templates: %w", err) 109 | } 110 | 111 | output, err := os.Create(filename) //nolint:gosec 112 | if err != nil { 113 | return fmt.Errorf("open output: %w", err) 114 | } 115 | 116 | defer func() { 117 | if errc := output.Close(); err == nil && errc != nil { 118 | err = errc 119 | } 120 | }() 121 | 122 | if err = tmpl.ExecuteTemplate(output, "root.tmpl", cfg); err != nil { 123 | return fmt.Errorf("execute template: %w", err) 124 | } 125 | 126 | cmd := exec.Command("gofmt", "-w", filename) 127 | buf, err := cmd.CombinedOutput() 128 | if err != nil { 129 | return fmt.Errorf("format output %q: %w", string(buf), err) 130 | } 131 | 132 | return nil 133 | } 134 | 135 | func main() { 136 | var filename string 137 | flag.StringVar(&filename, "output", "inline.go", "the filename to write the generated code to") 138 | if err := generate(filename); err != nil { 139 | log.Fatal(err) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/constructor.tmpl: -------------------------------------------------------------------------------- 1 | {{- with .Prototype.Property "constructor"}} 2 | // {{$.Name}} constructor definition. 3 | rt.global.{{$.Name}}Prototype.property[{{template "name.tmpl" .Name}}] = property{{template "property-value.tmpl" dict "Name" $.Name "Core" true "Property" .}} 4 | {{- end}} 5 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/core-prototype-property.tmpl: -------------------------------------------------------------------------------- 1 | {{/* Expects .(jsType) */}} 2 | 3 | // {{.Name}} prototype property definition. 4 | rt.global.{{.Name}}Prototype.property = {{template "property.tmpl" dict "Name" .Name "BlankConstructor" true "Properties" .Prototype.Properties}} 5 | rt.global.{{.Name}}Prototype.propertyOrder = {{template "property-order.tmpl" .Prototype}}{{/* No newline. */}} 6 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/definition.tmpl: -------------------------------------------------------------------------------- 1 | &object{ 2 | runtime: rt, 3 | class: class{{or .Class "Function"}}Name, 4 | objectClass: class{{or .ObjectClass "Object"}}, 5 | prototype: rt.global.{{or .ObjectPrototype "Function"}}Prototype, 6 | extensible: true, 7 | {{- if not .Class}} 8 | value: nativeFunctionObject{ 9 | name: class{{or .Value .Name}}Name, 10 | call: builtin{{or .Value .Name}}, 11 | construct: builtinNew{{or .Value .Name}}, 12 | }, 13 | {{- end}} 14 | {{- template "property-fields.tmpl" .}} 15 | }{{/* No newline */ -}} 16 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/function.tmpl: -------------------------------------------------------------------------------- 1 | &object{ 2 | runtime: rt, 3 | class: classFunctionName, 4 | objectClass: classObject, 5 | prototype: rt.global.FunctionPrototype, 6 | extensible: true, 7 | property: map[string]property{ 8 | propertyLength: { 9 | mode: 0, 10 | value: Value{ 11 | kind: valueNumber, 12 | value: {{if eq .Property.Function -1}}0{{else}}{{.Property.Function}}{{end}}, 13 | }, 14 | }, 15 | propertyName: { 16 | mode: 0, 17 | value: Value{ 18 | kind: valueString, 19 | value: "{{.Property.Name}}", 20 | }, 21 | }, 22 | }, 23 | propertyOrder: []string{ 24 | propertyLength, 25 | propertyName, 26 | }, 27 | value: nativeFunctionObject{ 28 | name: {{template "name.tmpl" .Property.Name}}, 29 | call: builtin{{if .Property.Call}}{{.Property.Call}}{{else}}{{.Name}}{{.Property.Name | ucfirst}}{{end}}, 30 | }, 31 | }{{/* No newline. */ -}} 32 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/global.tmpl: -------------------------------------------------------------------------------- 1 | // {{.Name}} properties. 2 | rt.globalObject.property = {{template "property.tmpl" .}} 3 | 4 | // {{.Name}} property order. 5 | rt.globalObject.propertyOrder = {{template "property-order.tmpl" . -}} 6 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/name.tmpl: -------------------------------------------------------------------------------- 1 | {{- if eq . "length" "prototype" "constructor" -}} 2 | property{{ucfirst .}} 3 | {{- else if eq . "toString" -}} 4 | methodToString 5 | {{- else if eq . "Object" "Function" "Array" "String" "Boolean" "Number" "Math" "Date" "RegExp" 6 | "Error" "EvalError" "TypeError" "RangeError" "ReferenceError" "SyntaxError" "URIError" "JSON" -}} 7 | class{{.}}Name 8 | {{- else -}} 9 | "{{.}}" 10 | {{- end -}} 11 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/property-entry.tmpl: -------------------------------------------------------------------------------- 1 | {{- template "name.tmpl" .Property.Name}}: {{- template "property-value.tmpl" .}},{{/* No newline */ -}} 2 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/property-fields.tmpl: -------------------------------------------------------------------------------- 1 | {{- /* expects .Name and .Properties */ -}} 2 | {{if .Properties}} 3 | property: {{template "property.tmpl" .}}, 4 | propertyOrder: {{template "property-order.tmpl" .}}, 5 | {{end -}} 6 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/property-order.tmpl: -------------------------------------------------------------------------------- 1 | {{- /* expects .Properties */ -}} 2 | []string{ 3 | {{range .Properties -}} 4 | {{template "name.tmpl" .Name}}, 5 | {{end -}} 6 | }{{/* No newline */ -}} 7 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/property-value.tmpl: -------------------------------------------------------------------------------- 1 | {{with .Property}} { 2 | mode: {{if .Mode}}{{.Mode}}{{else if or .Function (eq .Name "constructor")}}0o101{{else}}0{{end}}, 3 | {{- if eq .Name "constructor" | and $.BlankConstructor}} 4 | value: Value{}, 5 | {{- else}} 6 | value: Value{ 7 | kind: {{if .Kind}}{{.Kind}}{{else if eq .Name "length"}}valueNumber{{else}}valueObject{{end}}, 8 | {{- if .Function}} 9 | value: {{template "function.tmpl" $}}, 10 | {{- else if .Value}} 11 | value: {{.Value}}, 12 | {{- end}} 13 | }, 14 | {{- end}} 15 | }{{end -}} 16 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/property.tmpl: -------------------------------------------------------------------------------- 1 | {{- /* Expects .Name and .Properties */ -}} 2 | map[string]property{ 3 | {{range .Properties -}} 4 | {{- /* Skip constructor which is output later. */}} 5 | {{- if eq .Name "constructor" | not -}} 6 | {{template "property-entry.tmpl" dict "Name" $.Name "BlankConstructor" $.BlankConstructor "Property" .}} 7 | {{end -}} 8 | {{end -}} 9 | }{{/* No newline */ -}} 10 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/prototype.tmpl: -------------------------------------------------------------------------------- 1 | {{/* Expects .Name.(jsType.Name), .Prototype and optional .BlankConstructor */}} 2 | {{- with .Prototype}} 3 | // {{$.Name}} prototype. 4 | rt.global.{{$.Name}}Prototype = &object{ 5 | runtime: rt, 6 | class: class{{$.Name}}Name, 7 | objectClass: class{{or .ObjectClass "Object"}}, 8 | prototype: {{if .Prototype}}rt.global.{{.Prototype}}Prototype{{else}}nil{{end}}, 9 | extensible: true, 10 | value: {{or .Value (print "prototypeValue" $.Name)}}, 11 | {{- if not $.Core}} 12 | {{- template "property-fields.tmpl" dict "Name" $.Name "BlankConstructor" $.BlankConstructor "Properties" .Properties}} 13 | {{- end}} 14 | } 15 | {{- end -}} 16 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/root.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by tools/gen-jscore. DO NOT EDIT. 2 | 3 | package otto 4 | 5 | import ( 6 | "math" 7 | ) 8 | 9 | func (rt *runtime) newContext() { 10 | // Order here is import as definitions depend on each other. 11 | {{- $object := .Type "Object"}} 12 | {{- $function := .Type "Function"}} 13 | {{template "prototype.tmpl" $object}} 14 | {{template "prototype.tmpl" $function}} 15 | 16 | {{- template "core-prototype-property.tmpl" $object}} 17 | {{- template "core-prototype-property.tmpl" $function}} 18 | 19 | {{- template "type.tmpl" $object}} 20 | {{- template "type.tmpl" $function}} 21 | 22 | {{- range .Types}} 23 | {{- if eq .Name "Global"}} 24 | {{template "global.tmpl" . }} 25 | {{- else if not .Core}} 26 | {{template "type.tmpl" .}} 27 | {{- end}} 28 | {{- end}} 29 | } 30 | 31 | func (rt *runtime) newConsole() *object { 32 | return {{template "definition.tmpl" .Log}} 33 | } 34 | 35 | {{range .Values}} 36 | {{template "value.tmpl" .}} 37 | {{- end}} 38 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/type.tmpl: -------------------------------------------------------------------------------- 1 | {{if not .Core | and .Prototype}} 2 | {{template "prototype.tmpl" dict "Name" .Name "Prototype" .Prototype}} 3 | {{- end}} 4 | 5 | // {{.Name}} definition. 6 | rt.global.{{.Name}} = {{template "definition.tmpl" .}} 7 | 8 | {{- if .Prototype}} 9 | {{template "constructor.tmpl" .}} 10 | {{- end}} 11 | -------------------------------------------------------------------------------- /tools/gen-jscore/templates/value.tmpl: -------------------------------------------------------------------------------- 1 | func {{.Name}}Value(value {{or .Type .Name}}) Value { 2 | return Value{ 3 | kind: 4 | {{- if contains .Name "string"}} 5 | valueString 6 | {{- else if contains .Name "bool"}} 7 | valueBoolean 8 | {{- else if contains .Name "object"}} 9 | valueObject 10 | {{- else}} 11 | valueNumber 12 | {{- end}}, 13 | value: value, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tools/gen-tokens/.gen-tokens.yaml: -------------------------------------------------------------------------------- 1 | tokens: 2 | - group: Control 3 | - name: ILLEGAL 4 | - name: EOF 5 | - name: COMMENT 6 | - name: KEYWORD 7 | 8 | - group: Types 9 | - name: STRING 10 | - name: BOOLEAN 11 | - name: "NULL" 12 | - name: NUMBER 13 | - name: IDENTIFIER 14 | 15 | - group: Maths 16 | - name: PLUS 17 | symbol: "+" 18 | - name: MINUS 19 | symbol: "-" 20 | - name: MULTIPLY 21 | symbol: "*" 22 | - name: SLASH 23 | symbol: "/" 24 | - name: REMAINDER 25 | symbol: "%" 26 | 27 | - group: Logical and bitwise operators 28 | - name: AND 29 | symbol: "&" 30 | - name: OR 31 | symbol: "|" 32 | - name: EXCLUSIVE_OR 33 | symbol: "^" 34 | - name: SHIFT_LEFT 35 | symbol: "<<" 36 | - name: SHIFT_RIGHT 37 | symbol: ">>" 38 | - name: UNSIGNED_SHIFT_RIGHT 39 | symbol: ">>>" 40 | - name: AND_NOT 41 | symbol: "&^" 42 | 43 | - group: Math assignments 44 | - name: ADD_ASSIGN 45 | symbol: "+=" 46 | - name: SUBTRACT_ASSIGN 47 | symbol: "-=" 48 | - name: MULTIPLY_ASSIGN 49 | symbol: "*=" 50 | - name: QUOTIENT_ASSIGN 51 | symbol: "/=" 52 | - name: REMAINDER_ASSIGN 53 | symbol: "%=" 54 | 55 | - group: Math and bitwise assignments 56 | - name: AND_ASSIGN 57 | symbol: "&=" 58 | - name: OR_ASSIGN 59 | symbol: "|=" 60 | - name: EXCLUSIVE_OR_ASSIGN 61 | symbol: "^=" 62 | - name: SHIFT_LEFT_ASSIGN 63 | symbol: "<<=" 64 | - name: SHIFT_RIGHT_ASSIGN 65 | symbol: ">>=" 66 | - name: UNSIGNED_SHIFT_RIGHT_ASSIGN 67 | symbol: ">>>=" 68 | - name: AND_NOT_ASSIGN 69 | symbol: "&^=" 70 | 71 | - group: Logical operators and decrement / increment 72 | - name: LOGICAL_AND 73 | symbol: "&&" 74 | - name: LOGICAL_OR 75 | symbol: "||" 76 | - name: INCREMENT 77 | symbol: "++" 78 | - name: DECREMENT 79 | symbol: "--" 80 | 81 | - group: Comparison operators 82 | - name: EQUAL 83 | symbol: "==" 84 | - name: STRICT_EQUAL 85 | symbol: "===" 86 | - name: LESS 87 | symbol: "<" 88 | - name: GREATER 89 | symbol: ">" 90 | - name: ASSIGN 91 | symbol: "=" 92 | - name: NOT 93 | symbol: "!" 94 | 95 | - group: Bitwise not 96 | - name: BITWISE_NOT 97 | symbol: "~" 98 | 99 | - group: Comparison operators 100 | - name: NOT_EQUAL 101 | symbol: "!=" 102 | - name: STRICT_NOT_EQUAL 103 | symbol: "!==" 104 | - name: LESS_OR_EQUAL 105 | symbol: "<=" 106 | - name: GREATER_OR_EQUAL 107 | symbol: ">=" 108 | 109 | - group: Left operators 110 | - name: LEFT_PARENTHESIS 111 | symbol: "(" 112 | - name: LEFT_BRACKET 113 | symbol: "[" 114 | - name: LEFT_BRACE 115 | symbol: "{" 116 | - name: COMMA 117 | symbol: "," 118 | - name: PERIOD 119 | symbol: "." 120 | 121 | - group: Right operators 122 | - name: RIGHT_PARENTHESIS 123 | symbol: ")" 124 | - name: RIGHT_BRACKET 125 | symbol: "]" 126 | - name: RIGHT_BRACE 127 | symbol: "}" 128 | - name: SEMICOLON 129 | symbol: ";" 130 | - name: COLON 131 | symbol: ":" 132 | - name: QUESTION_MARK 133 | symbol: "?" 134 | 135 | - group: Basic flow - keywords below here 136 | - name: _ 137 | - name: IF 138 | - name: IN 139 | - name: DO 140 | 141 | - group: Declarations 142 | - name: VAR 143 | - name: FOR 144 | - name: NEW 145 | - name: TRY 146 | 147 | - group: Advanced flow 148 | - name: THIS 149 | - name: ELSE 150 | - name: CASE 151 | - name: VOID 152 | - name: WITH 153 | 154 | - group: Loops 155 | - name: WHILE 156 | - name: BREAK 157 | - name: CATCH 158 | - name: THROW 159 | 160 | - group: Functions 161 | - name: RETURN 162 | - name: TYPEOF 163 | - name: DELETE 164 | - name: SWITCH 165 | 166 | - group: Fallback identifiers 167 | - name: DEFAULT 168 | - name: FINALLY 169 | 170 | - group: Miscellaneous 171 | - name: FUNCTION 172 | - name: CONTINUE 173 | - name: DEBUGGER 174 | 175 | - group: Instance of 176 | - name: INSTANCEOF 177 | 178 | # Future 179 | - name: const 180 | future: true 181 | - name: class 182 | future: true 183 | - name: enum 184 | future: true 185 | - name: export 186 | future: true 187 | - name: extends 188 | future: true 189 | - name: import 190 | future: true 191 | - name: super 192 | future: true 193 | 194 | # Future Strict items 195 | - name: implements 196 | future: true 197 | strict: true 198 | - name: interface 199 | future: true 200 | strict: true 201 | - name: let 202 | future: true 203 | strict: true 204 | - name: package 205 | future: true 206 | strict: true 207 | - name: private 208 | future: true 209 | strict: true 210 | - name: protected 211 | future: true 212 | strict: true 213 | - name: public 214 | future: true 215 | strict: true 216 | - name: static 217 | future: true 218 | strict: true 219 | -------------------------------------------------------------------------------- /tools/gen-tokens/main.go: -------------------------------------------------------------------------------- 1 | // Command gen-tokens generates go representations of JavaScript tokens. 2 | package main 3 | 4 | import ( 5 | "embed" 6 | "flag" 7 | "fmt" 8 | "log" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | "text/template" 13 | 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | //go:embed .gen-tokens.yaml 18 | var configData []byte 19 | 20 | //go:embed templates/* 21 | var templates embed.FS 22 | 23 | // token represents a JavaScript token. 24 | type token struct { 25 | Group string `yaml:"group"` 26 | Name string `yaml:"name"` 27 | Symbol string `yaml:"symbol"` 28 | Future bool `yaml:"future"` 29 | Strict bool `yaml:"strict"` 30 | } 31 | 32 | // config represents our configuration. 33 | type config struct { 34 | Tokens []token `yaml:"tokens"` 35 | } 36 | 37 | // generate generates the context file writing the output to filename. 38 | func generate(filename string) (err error) { 39 | var cfg config 40 | if err = yaml.Unmarshal(configData, &cfg); err != nil { 41 | return fmt.Errorf("decode config: %w", err) 42 | } 43 | 44 | tmpl := template.New("base").Funcs(template.FuncMap{ 45 | "toLower": strings.ToLower, 46 | }) 47 | 48 | tmpl, err = tmpl.ParseFS(templates, "templates/*.tmpl") 49 | if err != nil { 50 | return fmt.Errorf("parse templates: %w", err) 51 | } 52 | 53 | output, err := os.Create(filename) //nolint:gosec 54 | if err != nil { 55 | return fmt.Errorf("open output: %w", err) 56 | } 57 | 58 | defer func() { 59 | if errc := output.Close(); err == nil && errc != nil { 60 | err = errc 61 | } 62 | }() 63 | 64 | if err = tmpl.ExecuteTemplate(output, "root.tmpl", cfg); err != nil { 65 | return fmt.Errorf("execute template: %w", err) 66 | } 67 | 68 | cmd := exec.Command("gofmt", "-w", filename) 69 | buf, err := cmd.CombinedOutput() 70 | if err != nil { 71 | return fmt.Errorf("format output %q: %w", string(buf), err) 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func main() { 78 | var filename string 79 | flag.StringVar(&filename, "output", "token_const.go", "the filename to write the generated code to") 80 | if err := generate(filename); err != nil { 81 | log.Fatal(err) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tools/gen-tokens/templates/root.tmpl: -------------------------------------------------------------------------------- 1 | // Code generated by tools/gen-tokens. DO NOT EDIT. 2 | 3 | package token 4 | 5 | const ( 6 | _ Token = iota 7 | {{range .Tokens}} 8 | {{- if not .Future}} 9 | {{- if .Group}} 10 | // {{.Group}}. 11 | {{- else}} 12 | {{.Name}} {{- if .Symbol}}// {{.Symbol}}{{end}} 13 | {{- end}} 14 | {{- end}} 15 | {{- end}} 16 | ) 17 | 18 | 19 | var token2string = [...]string{ 20 | {{- $lc := false -}} 21 | {{- range .Tokens -}} 22 | {{if or .Future .Group | not -}} 23 | {{- if eq .Name "_" -}} 24 | {{$lc = true}} 25 | {{- else}} 26 | {{- $symbol := or .Symbol .Name}} 27 | {{.Name}}: "{{if $lc}}{{toLower $symbol}}{{else}}{{$symbol}}{{end}}", 28 | {{- end}} 29 | {{- end -}} 30 | {{- end}} 31 | } 32 | 33 | var keywordTable = map[string]keyword{ 34 | {{- $keyword := false -}} 35 | {{range .Tokens}} 36 | {{- /* First keyword follows _ */ -}} 37 | {{- if eq .Name "_"}}{{$keyword = true}}{{continue}}{{end}} 38 | {{- if $keyword}} 39 | {{- if or .Symbol .Group | not}} 40 | "{{toLower .Name}}": { 41 | {{- if .Future}} 42 | token: KEYWORD, 43 | futureKeyword: true, 44 | {{- else}} 45 | token: {{.Name}}, 46 | {{- end}} 47 | {{- if .Strict}} 48 | strict: true, 49 | {{- end}} 50 | }, 51 | {{- end -}} 52 | {{end -}} 53 | {{- end}} 54 | } 55 | -------------------------------------------------------------------------------- /type_arguments.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | func (rt *runtime) newArgumentsObject(indexOfParameterName []string, stash stasher, length int) *object { 8 | obj := rt.newClassObject("Arguments") 9 | 10 | for index := range indexOfParameterName { 11 | name := strconv.FormatInt(int64(index), 10) 12 | objectDefineOwnProperty(obj, name, property{Value{}, 0o111}, false) 13 | } 14 | 15 | obj.objectClass = classArguments 16 | obj.value = argumentsObject{ 17 | indexOfParameterName: indexOfParameterName, 18 | stash: stash, 19 | } 20 | 21 | obj.prototype = rt.global.ObjectPrototype 22 | 23 | obj.defineProperty(propertyLength, intValue(length), 0o101, false) 24 | 25 | return obj 26 | } 27 | 28 | type argumentsObject struct { 29 | stash stasher 30 | indexOfParameterName []string 31 | } 32 | 33 | func (o argumentsObject) clone(c *cloner) argumentsObject { 34 | indexOfParameterName := make([]string, len(o.indexOfParameterName)) 35 | copy(indexOfParameterName, o.indexOfParameterName) 36 | return argumentsObject{ 37 | indexOfParameterName: indexOfParameterName, 38 | stash: c.stash(o.stash), 39 | } 40 | } 41 | 42 | func (o argumentsObject) get(name string) (Value, bool) { 43 | index := stringToArrayIndex(name) 44 | if index >= 0 && index < int64(len(o.indexOfParameterName)) { 45 | if name = o.indexOfParameterName[index]; name == "" { 46 | return Value{}, false 47 | } 48 | return o.stash.getBinding(name, false), true 49 | } 50 | return Value{}, false 51 | } 52 | 53 | func (o argumentsObject) put(name string, value Value) { 54 | index := stringToArrayIndex(name) 55 | name = o.indexOfParameterName[index] 56 | o.stash.setBinding(name, value, false) 57 | } 58 | 59 | func (o argumentsObject) delete(name string) { 60 | index := stringToArrayIndex(name) 61 | o.indexOfParameterName[index] = "" 62 | } 63 | 64 | func argumentsGet(obj *object, name string) Value { 65 | if value, exists := obj.value.(argumentsObject).get(name); exists { 66 | return value 67 | } 68 | return objectGet(obj, name) 69 | } 70 | 71 | func argumentsGetOwnProperty(obj *object, name string) *property { 72 | prop := objectGetOwnProperty(obj, name) 73 | if value, exists := obj.value.(argumentsObject).get(name); exists { 74 | prop.value = value 75 | } 76 | return prop 77 | } 78 | 79 | func argumentsDefineOwnProperty(obj *object, name string, descriptor property, throw bool) bool { 80 | if _, exists := obj.value.(argumentsObject).get(name); exists { 81 | if !objectDefineOwnProperty(obj, name, descriptor, false) { 82 | return obj.runtime.typeErrorResult(throw) 83 | } 84 | if value, valid := descriptor.value.(Value); valid { 85 | obj.value.(argumentsObject).put(name, value) 86 | } 87 | return true 88 | } 89 | return objectDefineOwnProperty(obj, name, descriptor, throw) 90 | } 91 | 92 | func argumentsDelete(obj *object, name string, throw bool) bool { 93 | if !objectDelete(obj, name, throw) { 94 | return false 95 | } 96 | if _, exists := obj.value.(argumentsObject).get(name); exists { 97 | obj.value.(argumentsObject).delete(name) 98 | } 99 | return true 100 | } 101 | -------------------------------------------------------------------------------- /type_array.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | func (rt *runtime) newArrayObject(length uint32) *object { 8 | obj := rt.newObject() 9 | obj.class = classArrayName 10 | obj.defineProperty(propertyLength, uint32Value(length), 0o100, false) 11 | obj.objectClass = classArray 12 | return obj 13 | } 14 | 15 | func isArray(obj *object) bool { 16 | if obj == nil { 17 | return false 18 | } 19 | 20 | switch obj.class { 21 | case classArrayName, classGoArrayName, classGoSliceName: 22 | return true 23 | default: 24 | return false 25 | } 26 | } 27 | 28 | func objectLength(obj *object) uint32 { 29 | if obj == nil { 30 | return 0 31 | } 32 | switch obj.class { 33 | case classArrayName: 34 | return obj.get(propertyLength).value.(uint32) 35 | case classStringName: 36 | return uint32(obj.get(propertyLength).value.(int)) 37 | case classGoArrayName, classGoSliceName: 38 | return uint32(obj.get(propertyLength).value.(int)) 39 | } 40 | return 0 41 | } 42 | 43 | func arrayUint32(rt *runtime, value Value) uint32 { 44 | nm := value.number() 45 | if nm.kind != numberInteger || !isUint32(nm.int64) { 46 | // FIXME 47 | panic(rt.panicRangeError()) 48 | } 49 | return uint32(nm.int64) 50 | } 51 | 52 | func arrayDefineOwnProperty(obj *object, name string, descriptor property, throw bool) bool { 53 | lengthProperty := obj.getOwnProperty(propertyLength) 54 | lengthValue, valid := lengthProperty.value.(Value) 55 | if !valid { 56 | panic("Array.length != Value{}") 57 | } 58 | 59 | reject := func(reason string) bool { 60 | if throw { 61 | panic(obj.runtime.panicTypeError("Array.DefineOwnProperty %s", reason)) 62 | } 63 | return false 64 | } 65 | length := lengthValue.value.(uint32) 66 | if name == propertyLength { 67 | if descriptor.value == nil { 68 | return objectDefineOwnProperty(obj, name, descriptor, throw) 69 | } 70 | newLengthValue, isValue := descriptor.value.(Value) 71 | if !isValue { 72 | panic(obj.runtime.panicTypeError("Array.DefineOwnProperty %q is not a value", descriptor.value)) 73 | } 74 | newLength := arrayUint32(obj.runtime, newLengthValue) 75 | descriptor.value = uint32Value(newLength) 76 | if newLength > length { 77 | return objectDefineOwnProperty(obj, name, descriptor, throw) 78 | } 79 | if !lengthProperty.writable() { 80 | return reject("property length for not writable") 81 | } 82 | newWritable := true 83 | if descriptor.mode&0o700 == 0 { 84 | // If writable is off 85 | newWritable = false 86 | descriptor.mode |= 0o100 87 | } 88 | if !objectDefineOwnProperty(obj, name, descriptor, throw) { 89 | return false 90 | } 91 | for newLength < length { 92 | length-- 93 | if !obj.delete(strconv.FormatInt(int64(length), 10), false) { 94 | descriptor.value = uint32Value(length + 1) 95 | if !newWritable { 96 | descriptor.mode &= 0o077 97 | } 98 | objectDefineOwnProperty(obj, name, descriptor, false) 99 | return reject("delete failed") 100 | } 101 | } 102 | if !newWritable { 103 | descriptor.mode &= 0o077 104 | objectDefineOwnProperty(obj, name, descriptor, false) 105 | } 106 | } else if index := stringToArrayIndex(name); index >= 0 { 107 | if index >= int64(length) && !lengthProperty.writable() { 108 | return reject("property length not writable") 109 | } 110 | if !objectDefineOwnProperty(obj, strconv.FormatInt(index, 10), descriptor, false) { 111 | return reject("Object.DefineOwnProperty failed") 112 | } 113 | if index >= int64(length) { 114 | lengthProperty.value = uint32Value(uint32(index + 1)) 115 | objectDefineOwnProperty(obj, propertyLength, *lengthProperty, false) 116 | return true 117 | } 118 | } 119 | return objectDefineOwnProperty(obj, name, descriptor, throw) 120 | } 121 | -------------------------------------------------------------------------------- /type_boolean.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | func (rt *runtime) newBooleanObject(value Value) *object { 4 | return rt.newPrimitiveObject(classBooleanName, boolValue(value.bool())) 5 | } 6 | -------------------------------------------------------------------------------- /type_error.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | func (rt *runtime) newErrorObject(name string, message Value, stackFramesToPop int) *object { 4 | obj := rt.newClassObject(classErrorName) 5 | if message.IsDefined() { 6 | err := newError(rt, name, stackFramesToPop, "%s", message.string()) 7 | obj.defineProperty("message", err.messageValue(), 0o111, false) 8 | obj.value = err 9 | } else { 10 | obj.value = newError(rt, name, stackFramesToPop) 11 | } 12 | 13 | obj.defineOwnProperty("stack", property{ 14 | value: propertyGetSet{ 15 | rt.newNativeFunction("get", "internal", 0, func(FunctionCall) Value { 16 | return stringValue(obj.value.(ottoError).formatWithStack()) 17 | }), 18 | &nilGetSetObject, 19 | }, 20 | mode: modeConfigureMask & modeOnMask, 21 | }, false) 22 | 23 | return obj 24 | } 25 | 26 | func (rt *runtime) newErrorObjectError(err ottoError) *object { 27 | obj := rt.newClassObject(classErrorName) 28 | obj.defineProperty("message", err.messageValue(), 0o111, false) 29 | obj.value = err 30 | switch err.name { 31 | case "EvalError": 32 | obj.prototype = rt.global.EvalErrorPrototype 33 | case "TypeError": 34 | obj.prototype = rt.global.TypeErrorPrototype 35 | case "RangeError": 36 | obj.prototype = rt.global.RangeErrorPrototype 37 | case "ReferenceError": 38 | obj.prototype = rt.global.ReferenceErrorPrototype 39 | case "SyntaxError": 40 | obj.prototype = rt.global.SyntaxErrorPrototype 41 | case "URIError": 42 | obj.prototype = rt.global.URIErrorPrototype 43 | default: 44 | obj.prototype = rt.global.ErrorPrototype 45 | } 46 | 47 | obj.defineOwnProperty("stack", property{ 48 | value: propertyGetSet{ 49 | rt.newNativeFunction("get", "internal", 0, func(FunctionCall) Value { 50 | return stringValue(obj.value.(ottoError).formatWithStack()) 51 | }), 52 | &nilGetSetObject, 53 | }, 54 | mode: modeConfigureMask & modeOnMask, 55 | }, false) 56 | 57 | return obj 58 | } 59 | -------------------------------------------------------------------------------- /type_go_array.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | ) 7 | 8 | func (rt *runtime) newGoArrayObject(value reflect.Value) *object { 9 | o := rt.newObject() 10 | o.class = classGoArrayName 11 | o.objectClass = classGoArray 12 | o.value = newGoArrayObject(value) 13 | return o 14 | } 15 | 16 | type goArrayObject struct { 17 | value reflect.Value 18 | writable bool 19 | propertyMode propertyMode 20 | } 21 | 22 | func newGoArrayObject(value reflect.Value) *goArrayObject { 23 | writable := value.Kind() == reflect.Ptr || value.CanSet() // The Array is addressable (like a Slice) 24 | mode := propertyMode(0o010) 25 | if writable { 26 | mode = 0o110 27 | } 28 | 29 | return &goArrayObject{ 30 | value: value, 31 | writable: writable, 32 | propertyMode: mode, 33 | } 34 | } 35 | 36 | func (o goArrayObject) getValue(name string) (reflect.Value, bool) { //nolint:unused 37 | if index, err := strconv.ParseInt(name, 10, 64); err != nil { 38 | v, ok := o.getValueIndex(index) 39 | if ok { 40 | return v, ok 41 | } 42 | } 43 | 44 | if m := o.value.MethodByName(name); m.IsValid() { 45 | return m, true 46 | } 47 | 48 | return reflect.Value{}, false 49 | } 50 | 51 | func (o goArrayObject) getValueIndex(index int64) (reflect.Value, bool) { 52 | value := reflect.Indirect(o.value) 53 | if index < int64(value.Len()) { 54 | return value.Index(int(index)), true 55 | } 56 | 57 | return reflect.Value{}, false 58 | } 59 | 60 | func (o goArrayObject) setValue(index int64, value Value) bool { 61 | indexValue, exists := o.getValueIndex(index) 62 | if !exists { 63 | return false 64 | } 65 | reflectValue, err := value.toReflectValue(reflect.Indirect(o.value).Type().Elem()) 66 | if err != nil { 67 | panic(err) 68 | } 69 | indexValue.Set(reflectValue) 70 | return true 71 | } 72 | 73 | func goArrayGetOwnProperty(obj *object, name string) *property { 74 | // length 75 | if name == propertyLength { 76 | return &property{ 77 | value: toValue(reflect.Indirect(obj.value.(*goArrayObject).value).Len()), 78 | mode: 0, 79 | } 80 | } 81 | 82 | // .0, .1, .2, ... 83 | if index := stringToArrayIndex(name); index >= 0 { 84 | goObj := obj.value.(*goArrayObject) 85 | value := Value{} 86 | reflectValue, exists := goObj.getValueIndex(index) 87 | if exists { 88 | value = obj.runtime.toValue(reflectValue.Interface()) 89 | } 90 | return &property{ 91 | value: value, 92 | mode: goObj.propertyMode, 93 | } 94 | } 95 | 96 | if method := obj.value.(*goArrayObject).value.MethodByName(name); method.IsValid() { 97 | return &property{ 98 | obj.runtime.toValue(method.Interface()), 99 | 0o110, 100 | } 101 | } 102 | 103 | return objectGetOwnProperty(obj, name) 104 | } 105 | 106 | func goArrayEnumerate(obj *object, all bool, each func(string) bool) { 107 | goObj := obj.value.(*goArrayObject) 108 | // .0, .1, .2, ... 109 | 110 | for index, length := 0, goObj.value.Len(); index < length; index++ { 111 | name := strconv.FormatInt(int64(index), 10) 112 | if !each(name) { 113 | return 114 | } 115 | } 116 | 117 | objectEnumerate(obj, all, each) 118 | } 119 | 120 | func goArrayDefineOwnProperty(obj *object, name string, descriptor property, throw bool) bool { 121 | if name == propertyLength { 122 | return obj.runtime.typeErrorResult(throw) 123 | } else if index := stringToArrayIndex(name); index >= 0 { 124 | goObj := obj.value.(*goArrayObject) 125 | if goObj.writable { 126 | if obj.value.(*goArrayObject).setValue(index, descriptor.value.(Value)) { 127 | return true 128 | } 129 | } 130 | return obj.runtime.typeErrorResult(throw) 131 | } 132 | return objectDefineOwnProperty(obj, name, descriptor, throw) 133 | } 134 | 135 | func goArrayDelete(obj *object, name string, throw bool) bool { 136 | // length 137 | if name == propertyLength { 138 | return obj.runtime.typeErrorResult(throw) 139 | } 140 | 141 | // .0, .1, .2, ... 142 | index := stringToArrayIndex(name) 143 | if index >= 0 { 144 | goObj := obj.value.(*goArrayObject) 145 | if goObj.writable { 146 | indexValue, exists := goObj.getValueIndex(index) 147 | if exists { 148 | indexValue.Set(reflect.Zero(reflect.Indirect(goObj.value).Type().Elem())) 149 | return true 150 | } 151 | } 152 | return obj.runtime.typeErrorResult(throw) 153 | } 154 | 155 | return obj.delete(name, throw) 156 | } 157 | -------------------------------------------------------------------------------- /type_go_map.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func (rt *runtime) newGoMapObject(value reflect.Value) *object { 8 | obj := rt.newObject() 9 | obj.class = classObjectName // TODO Should this be something else? 10 | obj.objectClass = classGoMap 11 | obj.value = newGoMapObject(value) 12 | return obj 13 | } 14 | 15 | type goMapObject struct { 16 | keyType reflect.Type 17 | valueType reflect.Type 18 | value reflect.Value 19 | } 20 | 21 | func newGoMapObject(value reflect.Value) *goMapObject { 22 | if value.Kind() != reflect.Map { 23 | dbgf("%/panic//%@: %v != reflect.Map", value.Kind()) 24 | } 25 | return &goMapObject{ 26 | value: value, 27 | keyType: value.Type().Key(), 28 | valueType: value.Type().Elem(), 29 | } 30 | } 31 | 32 | func (o goMapObject) toKey(name string) reflect.Value { 33 | reflectValue, err := stringToReflectValue(name, o.keyType.Kind()) 34 | if err != nil { 35 | panic(err) 36 | } 37 | return reflectValue 38 | } 39 | 40 | func (o goMapObject) toValue(value Value) reflect.Value { 41 | reflectValue, err := value.toReflectValue(o.valueType) 42 | if err != nil { 43 | panic(err) 44 | } 45 | return reflectValue 46 | } 47 | 48 | func goMapGetOwnProperty(obj *object, name string) *property { 49 | goObj := obj.value.(*goMapObject) 50 | 51 | // an error here means that the key referenced by `name` could not possibly 52 | // be a property of this object, so it should be safe to ignore this error 53 | // 54 | // TODO: figure out if any cases from 55 | // https://go.dev/ref/spec#Comparison_operators meet the criteria of 1) 56 | // being possible to represent as a string, 2) being possible to reconstruct 57 | // from a string, and 3) having a meaningful failure case in this context 58 | // other than "key does not exist" 59 | key, err := stringToReflectValue(name, goObj.keyType.Kind()) 60 | if err != nil { 61 | return nil 62 | } 63 | 64 | value := goObj.value.MapIndex(key) 65 | if value.IsValid() { 66 | return &property{obj.runtime.toValue(value.Interface()), 0o111} 67 | } 68 | 69 | // Other methods 70 | if method := obj.value.(*goMapObject).value.MethodByName(name); method.IsValid() { 71 | return &property{ 72 | value: obj.runtime.toValue(method.Interface()), 73 | mode: 0o110, 74 | } 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func goMapEnumerate(obj *object, all bool, each func(string) bool) { 81 | goObj := obj.value.(*goMapObject) 82 | keys := goObj.value.MapKeys() 83 | for _, key := range keys { 84 | if !each(toValue(key).String()) { 85 | return 86 | } 87 | } 88 | } 89 | 90 | func goMapDefineOwnProperty(obj *object, name string, descriptor property, throw bool) bool { 91 | goObj := obj.value.(*goMapObject) 92 | // TODO ...or 0222 93 | if descriptor.mode != 0o111 { 94 | return obj.runtime.typeErrorResult(throw) 95 | } 96 | if !descriptor.isDataDescriptor() { 97 | return obj.runtime.typeErrorResult(throw) 98 | } 99 | goObj.value.SetMapIndex(goObj.toKey(name), goObj.toValue(descriptor.value.(Value))) 100 | return true 101 | } 102 | 103 | func goMapDelete(obj *object, name string, throw bool) bool { 104 | goObj := obj.value.(*goMapObject) 105 | goObj.value.SetMapIndex(goObj.toKey(name), reflect.Value{}) 106 | // FIXME 107 | return true 108 | } 109 | -------------------------------------------------------------------------------- /type_go_map_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "sort" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | type GoMapTest map[string]int 10 | 11 | func (s GoMapTest) Join() string { 12 | joinedStr := "" 13 | 14 | // Ordering the map takes some effort 15 | // because map iterators in golang are unordered by definition. 16 | // So we need to extract keys, sort them, and then generate K/V pairs 17 | // All of this is meant to ensure that the test is predictable. 18 | keys := make([]string, len(s)) 19 | i := 0 20 | for key := range s { 21 | keys[i] = key 22 | i++ 23 | } 24 | 25 | sort.Strings(keys) 26 | 27 | for _, key := range keys { 28 | joinedStr += key + ": " + strconv.Itoa(s[key]) + " " 29 | } 30 | return joinedStr 31 | } 32 | 33 | func TestGoMap(t *testing.T) { 34 | tt(t, func() { 35 | test, vm := test() 36 | vm.Set("TestMap", GoMapTest{"one": 1, "two": 2, "three": 3}) 37 | is(test(`TestMap["one"]`).export(), 1) 38 | is(test(`TestMap.Join()`).export(), "one: 1 three: 3 two: 2 ") 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /type_go_slice.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | ) 7 | 8 | func (rt *runtime) newGoSliceObject(value reflect.Value) *object { 9 | o := rt.newObject() 10 | o.class = classGoSliceName 11 | o.objectClass = classGoSlice 12 | o.value = newGoSliceObject(value) 13 | return o 14 | } 15 | 16 | type goSliceObject struct { 17 | value reflect.Value 18 | } 19 | 20 | func newGoSliceObject(value reflect.Value) *goSliceObject { 21 | return &goSliceObject{ 22 | value: value, 23 | } 24 | } 25 | 26 | func (o goSliceObject) getValue(index int64) (reflect.Value, bool) { 27 | if index < int64(o.value.Len()) { 28 | return o.value.Index(int(index)), true 29 | } 30 | return reflect.Value{}, false 31 | } 32 | 33 | func (o *goSliceObject) setLength(value Value) { 34 | want, err := value.ToInteger() 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | wantInt := int(want) 40 | switch { 41 | case wantInt == o.value.Len(): 42 | // No change needed. 43 | case wantInt < o.value.Cap(): 44 | // Fits in current capacity. 45 | o.value.SetLen(wantInt) 46 | default: 47 | // Needs expanding. 48 | newSlice := reflect.MakeSlice(o.value.Type(), wantInt, wantInt) 49 | reflect.Copy(newSlice, o.value) 50 | o.value = newSlice 51 | } 52 | } 53 | 54 | func (o *goSliceObject) setValue(index int64, value Value) bool { 55 | reflectValue, err := value.toReflectValue(o.value.Type().Elem()) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | indexValue, exists := o.getValue(index) 61 | if !exists { 62 | if int64(o.value.Len()) == index { 63 | // Trying to append e.g. slice.push(...), allow it. 64 | o.value = reflect.Append(o.value, reflectValue) 65 | return true 66 | } 67 | return false 68 | } 69 | 70 | indexValue.Set(reflectValue) 71 | return true 72 | } 73 | 74 | func goSliceGetOwnProperty(obj *object, name string) *property { 75 | // length 76 | if name == propertyLength { 77 | return &property{ 78 | value: toValue(obj.value.(*goSliceObject).value.Len()), 79 | mode: 0o110, 80 | } 81 | } 82 | 83 | // .0, .1, .2, ... 84 | if index := stringToArrayIndex(name); index >= 0 { 85 | value := Value{} 86 | reflectValue, exists := obj.value.(*goSliceObject).getValue(index) 87 | if exists { 88 | value = obj.runtime.toValue(reflectValue.Interface()) 89 | } 90 | return &property{ 91 | value: value, 92 | mode: 0o110, 93 | } 94 | } 95 | 96 | // Other methods 97 | if method := obj.value.(*goSliceObject).value.MethodByName(name); method.IsValid() { 98 | return &property{ 99 | value: obj.runtime.toValue(method.Interface()), 100 | mode: 0o110, 101 | } 102 | } 103 | 104 | return objectGetOwnProperty(obj, name) 105 | } 106 | 107 | func goSliceEnumerate(obj *object, all bool, each func(string) bool) { 108 | goObj := obj.value.(*goSliceObject) 109 | // .0, .1, .2, ... 110 | 111 | for index, length := 0, goObj.value.Len(); index < length; index++ { 112 | name := strconv.FormatInt(int64(index), 10) 113 | if !each(name) { 114 | return 115 | } 116 | } 117 | 118 | objectEnumerate(obj, all, each) 119 | } 120 | 121 | func goSliceDefineOwnProperty(obj *object, name string, descriptor property, throw bool) bool { 122 | if name == propertyLength { 123 | obj.value.(*goSliceObject).setLength(descriptor.value.(Value)) 124 | return true 125 | } else if index := stringToArrayIndex(name); index >= 0 { 126 | if obj.value.(*goSliceObject).setValue(index, descriptor.value.(Value)) { 127 | return true 128 | } 129 | return obj.runtime.typeErrorResult(throw) 130 | } 131 | return objectDefineOwnProperty(obj, name, descriptor, throw) 132 | } 133 | 134 | func goSliceDelete(obj *object, name string, throw bool) bool { 135 | // length 136 | if name == propertyLength { 137 | return obj.runtime.typeErrorResult(throw) 138 | } 139 | 140 | // .0, .1, .2, ... 141 | index := stringToArrayIndex(name) 142 | if index >= 0 { 143 | goObj := obj.value.(*goSliceObject) 144 | indexValue, exists := goObj.getValue(index) 145 | if exists { 146 | indexValue.Set(reflect.Zero(goObj.value.Type().Elem())) 147 | return true 148 | } 149 | return obj.runtime.typeErrorResult(throw) 150 | } 151 | 152 | return obj.delete(name, throw) 153 | } 154 | -------------------------------------------------------------------------------- /type_go_slice_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import "testing" 4 | 5 | type GoSliceTest []int 6 | 7 | func (s GoSliceTest) Sum() int { 8 | sum := 0 9 | for _, v := range s { 10 | sum += v 11 | } 12 | return sum 13 | } 14 | 15 | func TestGoSlice(t *testing.T) { 16 | tt(t, func() { 17 | test, vm := test() 18 | vm.Set("TestSlice", GoSliceTest{1, 2, 3}) 19 | is(test(`TestSlice.length`).export(), 3) 20 | is(test(`TestSlice[1]`).export(), 2) 21 | is(test(`TestSlice.Sum()`).export(), 6) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /type_go_struct.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | ) 7 | 8 | // FIXME Make a note about not being able to modify a struct unless it was 9 | // passed as a pointer-to: &struct{ ... } 10 | // This seems to be a limitation of the reflect package. 11 | // This goes for the other Go constructs too. 12 | // I guess we could get around it by either: 13 | // 1. Creating a new struct every time 14 | // 2. Creating an addressable? struct in the constructor 15 | 16 | func (rt *runtime) newGoStructObject(value reflect.Value) *object { 17 | o := rt.newObject() 18 | o.class = classObjectName // TODO Should this be something else? 19 | o.objectClass = classGoStruct 20 | o.value = newGoStructObject(value) 21 | return o 22 | } 23 | 24 | type goStructObject struct { 25 | value reflect.Value 26 | } 27 | 28 | func newGoStructObject(value reflect.Value) *goStructObject { 29 | if reflect.Indirect(value).Kind() != reflect.Struct { 30 | dbgf("%/panic//%@: %v != reflect.Struct", value.Kind()) 31 | } 32 | return &goStructObject{ 33 | value: value, 34 | } 35 | } 36 | 37 | func (o goStructObject) getValue(name string) reflect.Value { 38 | if idx := fieldIndexByName(reflect.Indirect(o.value).Type(), name); len(idx) > 0 { 39 | return reflect.Indirect(o.value).FieldByIndex(idx) 40 | } 41 | 42 | if validGoStructName(name) { 43 | // Do not reveal hidden or unexported fields. 44 | if field := reflect.Indirect(o.value).FieldByName(name); field.IsValid() { 45 | return field 46 | } 47 | 48 | if method := o.value.MethodByName(name); method.IsValid() { 49 | return method 50 | } 51 | } 52 | 53 | return reflect.Value{} 54 | } 55 | 56 | func (o goStructObject) fieldIndex(name string) []int { //nolint:unused 57 | return fieldIndexByName(reflect.Indirect(o.value).Type(), name) 58 | } 59 | 60 | func (o goStructObject) method(name string) (reflect.Method, bool) { //nolint:unused 61 | return reflect.Indirect(o.value).Type().MethodByName(name) 62 | } 63 | 64 | func (o goStructObject) setValue(rt *runtime, name string, value Value) bool { 65 | if idx := fieldIndexByName(reflect.Indirect(o.value).Type(), name); len(idx) == 0 { 66 | return false 67 | } 68 | 69 | fieldValue := o.getValue(name) 70 | converted, err := rt.convertCallParameter(value, fieldValue.Type()) 71 | if err != nil { 72 | panic(rt.panicTypeError("Object.setValue convertCallParameter: %s", err)) 73 | } 74 | fieldValue.Set(converted) 75 | 76 | return true 77 | } 78 | 79 | func goStructGetOwnProperty(obj *object, name string) *property { 80 | goObj := obj.value.(*goStructObject) 81 | value := goObj.getValue(name) 82 | if value.IsValid() { 83 | return &property{obj.runtime.toValue(value), 0o110} 84 | } 85 | 86 | return objectGetOwnProperty(obj, name) 87 | } 88 | 89 | func validGoStructName(name string) bool { 90 | if name == "" { 91 | return false 92 | } 93 | return 'A' <= name[0] && name[0] <= 'Z' // TODO What about Unicode? 94 | } 95 | 96 | func goStructEnumerate(obj *object, all bool, each func(string) bool) { 97 | goObj := obj.value.(*goStructObject) 98 | 99 | // Enumerate fields 100 | for index := range reflect.Indirect(goObj.value).NumField() { 101 | name := reflect.Indirect(goObj.value).Type().Field(index).Name 102 | if validGoStructName(name) { 103 | if !each(name) { 104 | return 105 | } 106 | } 107 | } 108 | 109 | // Enumerate methods 110 | for index := range goObj.value.NumMethod() { 111 | name := goObj.value.Type().Method(index).Name 112 | if validGoStructName(name) { 113 | if !each(name) { 114 | return 115 | } 116 | } 117 | } 118 | 119 | objectEnumerate(obj, all, each) 120 | } 121 | 122 | func goStructCanPut(obj *object, name string) bool { 123 | goObj := obj.value.(*goStructObject) 124 | value := goObj.getValue(name) 125 | if value.IsValid() { 126 | return true 127 | } 128 | 129 | return objectCanPut(obj, name) 130 | } 131 | 132 | func goStructPut(obj *object, name string, value Value, throw bool) { 133 | goObj := obj.value.(*goStructObject) 134 | if goObj.setValue(obj.runtime, name, value) { 135 | return 136 | } 137 | 138 | objectPut(obj, name, value, throw) 139 | } 140 | 141 | func goStructMarshalJSON(obj *object) json.Marshaler { 142 | goObj := obj.value.(*goStructObject) 143 | goValue := reflect.Indirect(goObj.value).Interface() 144 | marshaler, _ := goValue.(json.Marshaler) 145 | return marshaler 146 | } 147 | -------------------------------------------------------------------------------- /type_go_struct_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGoStructEmbeddedFields(t *testing.T) { 8 | type A struct { 9 | A1 string `json:"a1"` 10 | A2 string `json:"a2"` 11 | A3 string `json:"a3"` 12 | } 13 | 14 | type B struct { 15 | A 16 | B1 string `json:"b1"` 17 | } 18 | 19 | tt(t, func() { 20 | test, vm := test() 21 | 22 | vm.Set("v", B{A{"a1", "a2", "a3"}, "b1"}) 23 | 24 | test(`[v.a1,v.a2,v.a3,v.b1]`, "a1,a2,a3,b1") 25 | }) 26 | } 27 | 28 | func TestGoStructNilBoolPointerField(t *testing.T) { 29 | type S struct { 30 | C interface{} `json:"c"` 31 | B *bool `json:"b"` 32 | A int `json:"a"` 33 | } 34 | 35 | tt(t, func() { 36 | test, vm := test() 37 | vm.Set("s", S{A: 1, B: nil, C: nil}) 38 | test(`'a' in s`, true) 39 | test(`typeof s.a`, "number") 40 | test(`'b' in s`, true) 41 | test(`typeof s.b`, "undefined") 42 | test(`'c' in s`, true) 43 | test(`typeof s.c`, "undefined") 44 | }) 45 | } 46 | 47 | func TestGoStructError(t *testing.T) { 48 | type S1 struct { 49 | A string `json:"a"` 50 | B string `json:"b"` 51 | } 52 | 53 | type S2 struct { 54 | B S1 `json:"b"` 55 | A []S1 `json:"a"` 56 | } 57 | 58 | type S3 struct { 59 | B S2 `json:"b"` 60 | A []S2 `json:"a"` 61 | } 62 | 63 | tt(t, func() { 64 | test, vm := test() 65 | vm.Set("fn", func(s *S3) string { return "cool" }) 66 | test( 67 | `(function() { try { fn({a:[{a:[{c:"x"}]}]}) } catch (ex) { return ex } })()`, 68 | `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`, 69 | ) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /type_number.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | func (rt *runtime) newNumberObject(value Value) *object { 4 | return rt.newPrimitiveObject(classNumberName, value.numberValue()) 5 | } 6 | -------------------------------------------------------------------------------- /type_reference.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | type referencer interface { 4 | invalid() bool // IsUnresolvableReference 5 | getValue() Value // getValue 6 | putValue(value Value) string // PutValue 7 | delete() bool 8 | } 9 | 10 | // PropertyReference 11 | 12 | type propertyReference struct { 13 | base *object 14 | runtime *runtime 15 | name string 16 | at at 17 | strict bool 18 | } 19 | 20 | func newPropertyReference(rt *runtime, base *object, name string, strict bool, atv at) *propertyReference { 21 | return &propertyReference{ 22 | runtime: rt, 23 | name: name, 24 | strict: strict, 25 | base: base, 26 | at: atv, 27 | } 28 | } 29 | 30 | func (pr *propertyReference) invalid() bool { 31 | return pr.base == nil 32 | } 33 | 34 | func (pr *propertyReference) getValue() Value { 35 | if pr.base == nil { 36 | panic(pr.runtime.panicReferenceError("'%s' is not defined", pr.name, pr.at)) 37 | } 38 | return pr.base.get(pr.name) 39 | } 40 | 41 | func (pr *propertyReference) putValue(value Value) string { 42 | if pr.base == nil { 43 | return pr.name 44 | } 45 | pr.base.put(pr.name, value, pr.strict) 46 | return "" 47 | } 48 | 49 | func (pr *propertyReference) delete() bool { 50 | if pr.base == nil { 51 | // TODO Throw an error if strict 52 | return true 53 | } 54 | return pr.base.delete(pr.name, pr.strict) 55 | } 56 | 57 | type stashReference struct { 58 | base stasher 59 | name string 60 | strict bool 61 | } 62 | 63 | func (sr *stashReference) invalid() bool { 64 | return false // The base (an environment) will never be nil 65 | } 66 | 67 | func (sr *stashReference) getValue() Value { 68 | return sr.base.getBinding(sr.name, sr.strict) 69 | } 70 | 71 | func (sr *stashReference) putValue(value Value) string { 72 | sr.base.setValue(sr.name, value, sr.strict) 73 | return "" 74 | } 75 | 76 | func (sr *stashReference) delete() bool { 77 | if sr.base == nil { 78 | // This should never be reached, but just in case 79 | return false 80 | } 81 | return sr.base.deleteBinding(sr.name) 82 | } 83 | 84 | // getIdentifierReference. 85 | func getIdentifierReference(rt *runtime, stash stasher, name string, strict bool, atv at) referencer { 86 | if stash == nil { 87 | return newPropertyReference(rt, nil, name, strict, atv) 88 | } 89 | if stash.hasBinding(name) { 90 | return stash.newReference(name, strict, atv) 91 | } 92 | return getIdentifierReference(rt, stash.outer(), name, strict, atv) 93 | } 94 | -------------------------------------------------------------------------------- /type_regexp.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/robertkrimen/otto/parser" 8 | ) 9 | 10 | type regExpObject struct { 11 | regularExpression *regexp.Regexp 12 | source string 13 | flags string 14 | global bool 15 | ignoreCase bool 16 | multiline bool 17 | } 18 | 19 | func (rt *runtime) newRegExpObject(pattern string, flags string) *object { 20 | o := rt.newObject() 21 | o.class = classRegExpName 22 | 23 | global := false 24 | ignoreCase := false 25 | multiline := false 26 | re2flags := "" 27 | 28 | // TODO Maybe clean up the panicking here... TypeError, SyntaxError, ? 29 | 30 | for _, chr := range flags { 31 | switch chr { 32 | case 'g': 33 | if global { 34 | panic(rt.panicSyntaxError("newRegExpObject: %s %s", pattern, flags)) 35 | } 36 | global = true 37 | case 'm': 38 | if multiline { 39 | panic(rt.panicSyntaxError("newRegExpObject: %s %s", pattern, flags)) 40 | } 41 | multiline = true 42 | re2flags += "m" 43 | case 'i': 44 | if ignoreCase { 45 | panic(rt.panicSyntaxError("newRegExpObject: %s %s", pattern, flags)) 46 | } 47 | ignoreCase = true 48 | re2flags += "i" 49 | } 50 | } 51 | 52 | re2pattern, err := parser.TransformRegExp(pattern) 53 | if err != nil { 54 | panic(rt.panicTypeError("Invalid regular expression: %s", err.Error())) 55 | } 56 | if len(re2flags) > 0 { 57 | re2pattern = fmt.Sprintf("(?%s:%s)", re2flags, re2pattern) 58 | } 59 | 60 | regularExpression, err := regexp.Compile(re2pattern) 61 | if err != nil { 62 | panic(rt.panicSyntaxError("Invalid regular expression: %s", err.Error()[22:])) 63 | } 64 | 65 | o.value = regExpObject{ 66 | regularExpression: regularExpression, 67 | global: global, 68 | ignoreCase: ignoreCase, 69 | multiline: multiline, 70 | source: pattern, 71 | flags: flags, 72 | } 73 | o.defineProperty("global", boolValue(global), 0, false) 74 | o.defineProperty("ignoreCase", boolValue(ignoreCase), 0, false) 75 | o.defineProperty("multiline", boolValue(multiline), 0, false) 76 | o.defineProperty("lastIndex", intValue(0), 0o100, false) 77 | o.defineProperty("source", stringValue(pattern), 0, false) 78 | return o 79 | } 80 | 81 | func (o *object) regExpValue() regExpObject { 82 | value, _ := o.value.(regExpObject) 83 | return value 84 | } 85 | 86 | func execRegExp(this *object, target string) (bool, []int) { 87 | if this.class != classRegExpName { 88 | panic(this.runtime.panicTypeError("Calling RegExp.exec on a non-RegExp object")) 89 | } 90 | lastIndex := this.get("lastIndex").number().int64 91 | index := lastIndex 92 | global := this.get("global").bool() 93 | if !global { 94 | index = 0 95 | } 96 | 97 | var result []int 98 | if 0 > index || index > int64(len(target)) { 99 | } else { 100 | result = this.regExpValue().regularExpression.FindStringSubmatchIndex(target[index:]) 101 | } 102 | 103 | if result == nil { 104 | this.put("lastIndex", intValue(0), true) 105 | return false, nil 106 | } 107 | 108 | startIndex := index 109 | endIndex := int(lastIndex) + result[1] 110 | // We do this shift here because the .FindStringSubmatchIndex above 111 | // was done on a local subordinate slice of the string, not the whole string 112 | for index, offset := range result { 113 | if offset != -1 { 114 | result[index] += int(startIndex) 115 | } 116 | } 117 | if global { 118 | this.put("lastIndex", intValue(endIndex), true) 119 | } 120 | 121 | return true, result 122 | } 123 | 124 | func execResultToArray(rt *runtime, target string, result []int) *object { 125 | captureCount := len(result) / 2 126 | valueArray := make([]Value, captureCount) 127 | for index := range captureCount { 128 | offset := 2 * index 129 | if result[offset] != -1 { 130 | valueArray[index] = stringValue(target[result[offset]:result[offset+1]]) 131 | } else { 132 | valueArray[index] = Value{} 133 | } 134 | } 135 | matchIndex := result[0] 136 | if matchIndex != 0 { 137 | // Find the utf16 index in the string, not the byte index. 138 | matchIndex = utf16Length(target[:matchIndex]) 139 | } 140 | match := rt.newArrayOf(valueArray) 141 | match.defineProperty("input", stringValue(target), 0o111, false) 142 | match.defineProperty("index", intValue(matchIndex), 0o111, false) 143 | return match 144 | } 145 | -------------------------------------------------------------------------------- /type_string.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "strconv" 5 | "unicode/utf16" 6 | "unicode/utf8" 7 | ) 8 | 9 | type stringObjecter interface { 10 | Length() int 11 | At(at int) rune 12 | String() string 13 | } 14 | 15 | type stringASCII string 16 | 17 | func (str stringASCII) Length() int { 18 | return len(str) 19 | } 20 | 21 | func (str stringASCII) At(at int) rune { 22 | return rune(str[at]) 23 | } 24 | 25 | func (str stringASCII) String() string { 26 | return string(str) 27 | } 28 | 29 | type stringWide struct { 30 | string string 31 | value16 []uint16 32 | } 33 | 34 | func (str stringWide) Length() int { 35 | if str.value16 == nil { 36 | str.value16 = utf16.Encode([]rune(str.string)) 37 | } 38 | return len(str.value16) 39 | } 40 | 41 | func (str stringWide) At(at int) rune { 42 | if str.value16 == nil { 43 | str.value16 = utf16.Encode([]rune(str.string)) 44 | } 45 | return rune(str.value16[at]) 46 | } 47 | 48 | func (str stringWide) String() string { 49 | return str.string 50 | } 51 | 52 | func newStringObject(str string) stringObjecter { 53 | for i := range len(str) { 54 | if str[i] >= utf8.RuneSelf { 55 | goto wide 56 | } 57 | } 58 | 59 | return stringASCII(str) 60 | 61 | wide: 62 | return &stringWide{ 63 | string: str, 64 | } 65 | } 66 | 67 | func stringAt(str stringObjecter, index int) rune { 68 | if 0 <= index && index < str.Length() { 69 | return str.At(index) 70 | } 71 | return utf8.RuneError 72 | } 73 | 74 | func (rt *runtime) newStringObject(value Value) *object { 75 | str := newStringObject(value.string()) 76 | 77 | obj := rt.newClassObject(classStringName) 78 | obj.defineProperty(propertyLength, intValue(str.Length()), 0, false) 79 | obj.objectClass = classString 80 | obj.value = str 81 | return obj 82 | } 83 | 84 | func (o *object) stringValue() stringObjecter { 85 | if str, ok := o.value.(stringObjecter); ok { 86 | return str 87 | } 88 | return nil 89 | } 90 | 91 | func stringEnumerate(obj *object, all bool, each func(string) bool) { 92 | if str := obj.stringValue(); str != nil { 93 | length := str.Length() 94 | for index := range length { 95 | if !each(strconv.FormatInt(int64(index), 10)) { 96 | return 97 | } 98 | } 99 | } 100 | objectEnumerate(obj, all, each) 101 | } 102 | 103 | func stringGetOwnProperty(obj *object, name string) *property { 104 | if prop := objectGetOwnProperty(obj, name); prop != nil { 105 | return prop 106 | } 107 | // TODO Test a string of length >= +int32 + 1? 108 | if index := stringToArrayIndex(name); index >= 0 { 109 | if chr := stringAt(obj.stringValue(), int(index)); chr != utf8.RuneError { 110 | return &property{stringValue(string(chr)), 0} 111 | } 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /underscore/LICENSE.underscorejs: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2022 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /underscore/README.md: -------------------------------------------------------------------------------- 1 | # underscore 2 | 3 | [![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) 4 | 5 | To update the version of underscore run: 6 | 7 | ```shell 8 | go generate 9 | ``` 10 | -------------------------------------------------------------------------------- /underscore/download.go: -------------------------------------------------------------------------------- 1 | //go:build generate 2 | 3 | package main 4 | 5 | import ( 6 | "context" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net/http" 12 | "os" 13 | "time" 14 | ) 15 | 16 | var ( 17 | url = flag.String("url", "", "url to read from") 18 | output = flag.String("output", "", "output file to write the result too") 19 | ) 20 | 21 | func download(url, output string) (err error) { 22 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 23 | defer cancel() 24 | 25 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 26 | if err != nil { 27 | return fmt.Errorf("new request failed: %w", err) 28 | } 29 | 30 | resp, err := http.DefaultClient.Do(req) 31 | if err != nil { 32 | return fmt.Errorf("request failed: %w", err) 33 | } 34 | defer resp.Body.Close() 35 | 36 | var f *os.File 37 | if output != "" { 38 | if f, err = os.Create(output); err != nil { 39 | return fmt.Errorf("create file %q failed: %w", output, err) 40 | } 41 | 42 | defer f.Close() 43 | } else { 44 | f = os.Stdout 45 | } 46 | 47 | if _, err := io.Copy(f, resp.Body); err != nil { 48 | return fmt.Errorf("body save: %w", err) 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func main() { 55 | flag.Parse() 56 | 57 | switch { 58 | case len(*url) == 0: 59 | log.Fatal("missing required --url parameter") 60 | } 61 | 62 | if err := download(*url, *output); err != nil { 63 | log.Fatal(err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /underscore/generate.go: -------------------------------------------------------------------------------- 1 | package underscore 2 | 3 | //go:generate go run download.go --url https://underscorejs.org/underscore-min.js --output underscore-min.js 4 | //go:generate go run download.go --url https://raw.githubusercontent.com/jashkenas/underscore/master/LICENSE --output LICENSE.underscorejs 5 | -------------------------------------------------------------------------------- /underscore/testify: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | my $underscore_test = shift @ARGV || ""; 7 | if (!-d $underscore_test) { 8 | print <<_END_; 9 | Usage: 10 | 11 | testify ./underscore/test 12 | 13 | # Should look something like: 14 | arrays.js 15 | chaining.js 16 | collections.js 17 | functions.js 18 | index.html 19 | objects.js 20 | speed.js 21 | utility.js 22 | vendor 23 | 24 | _END_ 25 | if ($underscore_test) { 26 | die "!: Not a directory: $underscore_test\n" 27 | } 28 | exit; 29 | } 30 | 31 | chdir $underscore_test or die "!: $!"; 32 | 33 | my @js = <*.js>; 34 | 35 | for my $file (@js) { 36 | open my $fh, '<', $file or die "!: $!"; 37 | my $tests = join "", <$fh>; 38 | my @tests = $tests =~ m/ 39 | ^(\s{2}test\(.*? 40 | ^\s{2}}\);)$ 41 | /mgxs; 42 | close $fh; 43 | next unless @tests; 44 | print "$file: ", scalar(@tests), "\n"; 45 | my $underscore_name = "underscore_$file"; 46 | $underscore_name =~ s/.js$//; 47 | my $go_file = "${underscore_name}_test.go"; 48 | $go_file =~ s/.js$/.go/; 49 | open $fh, '>', $go_file or die "!: $!"; 50 | 51 | $fh->print(<<_END_); 52 | package otto 53 | 54 | import ( 55 | "testing" 56 | ) 57 | 58 | _END_ 59 | 60 | my $count = 0; 61 | for my $test (@tests) { 62 | $test =~ s/`([^`]+)`/<$1>/g; 63 | my ($name) = $test =~ m/^\s*test\(['"]([^'"]+)['"]/; 64 | $fh->print(<<_END_); 65 | // $name 66 | func Test_${underscore_name}_$count(t *testing.T) { 67 | tt(t, func(){ 68 | test := underscoreTest() 69 | 70 | test(` 71 | $test 72 | `) 73 | }) 74 | } 75 | 76 | _END_ 77 | $count++; 78 | } 79 | } 80 | 81 | # test('#779 - delimeters are applied to unescaped text.', 1, function() { 82 | # var template = _.template('<<\nx\n>>', null, {evaluate: /<<(.*?)>>/g}); 83 | # strictEqual(template(), '<<\nx\n>>'); 84 | # }); 85 | -------------------------------------------------------------------------------- /underscore/underscore.go: -------------------------------------------------------------------------------- 1 | // Package underscore contains the source for the JavaScript utility-belt library. 2 | // 3 | // import ( 4 | // _ "github.com/robertkrimen/otto/underscore" 5 | // ) 6 | // 7 | // Every Otto runtime will now include [underscore] for more information see the [underscore docs] 8 | // 9 | // By importing this package, you'll automatically load underscore every time you create a new Otto runtime. 10 | // 11 | // To prevent this behavior, you can do the following: 12 | // 13 | // import ( 14 | // "github.com/robertkrimen/otto/underscore" 15 | // ) 16 | // 17 | // func init() { 18 | // underscore.Disable() 19 | // } 20 | // 21 | // [underscore]: http://underscorejs.org 22 | // [underscore docs]: https://github.com/documentcloud/underscore 23 | package underscore 24 | 25 | import ( 26 | _ "embed" // Embed underscore. 27 | 28 | "github.com/robertkrimen/otto/registry" 29 | ) 30 | 31 | //go:embed underscore-min.js 32 | var underscore string 33 | var entry *registry.Entry = registry.Register(Source) 34 | 35 | // Enable underscore runtime inclusion. 36 | func Enable() { 37 | entry.Enable() 38 | } 39 | 40 | // Disable underscore runtime inclusion. 41 | func Disable() { 42 | entry.Disable() 43 | } 44 | 45 | // Source returns the underscore source. 46 | func Source() string { 47 | return underscore 48 | } 49 | -------------------------------------------------------------------------------- /underscore_chaining_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // map/flatten/reduce. 8 | func Test_underscore_chaining_0(t *testing.T) { 9 | tt(t, func() { 10 | test := underscoreTest() 11 | 12 | test(` 13 | test("map/flatten/reduce", function() { 14 | var lyrics = [ 15 | "I'm a lumberjack and I'm okay", 16 | "I sleep all night and I work all day", 17 | "He's a lumberjack and he's okay", 18 | "He sleeps all night and he works all day" 19 | ]; 20 | var counts = _(lyrics).chain() 21 | .map(function(line) { return line.split(''); }) 22 | .flatten() 23 | .reduce(function(hash, l) { 24 | hash[l] = hash[l] || 0; 25 | hash[l]++; 26 | return hash; 27 | }, {}).value(); 28 | ok(counts['a'] == 16 && counts['e'] == 10, 'counted all the letters in the song'); 29 | }); 30 | `) 31 | }) 32 | } 33 | 34 | // select/reject/sortBy. 35 | func Test_underscore_chaining_1(t *testing.T) { 36 | tt(t, func() { 37 | test := underscoreTest() 38 | 39 | test(` 40 | test("select/reject/sortBy", function() { 41 | var numbers = [1,2,3,4,5,6,7,8,9,10]; 42 | numbers = _(numbers).chain().select(function(n) { 43 | return n % 2 == 0; 44 | }).reject(function(n) { 45 | return n % 4 == 0; 46 | }).sortBy(function(n) { 47 | return -n; 48 | }).value(); 49 | equal(numbers.join(', '), "10, 6, 2", "filtered and reversed the numbers"); 50 | }); 51 | `) 52 | }) 53 | } 54 | 55 | // select/reject/sortBy in functional style. 56 | func Test_underscore_chaining_2(t *testing.T) { 57 | tt(t, func() { 58 | test := underscoreTest() 59 | 60 | test(` 61 | test("select/reject/sortBy in functional style", function() { 62 | var numbers = [1,2,3,4,5,6,7,8,9,10]; 63 | numbers = _.chain(numbers).select(function(n) { 64 | return n % 2 == 0; 65 | }).reject(function(n) { 66 | return n % 4 == 0; 67 | }).sortBy(function(n) { 68 | return -n; 69 | }).value(); 70 | equal(numbers.join(', '), "10, 6, 2", "filtered and reversed the numbers"); 71 | }); 72 | `) 73 | }) 74 | } 75 | 76 | // reverse/concat/unshift/pop/map. 77 | func Test_underscore_chaining_3(t *testing.T) { 78 | tt(t, func() { 79 | test := underscoreTest() 80 | 81 | test(` 82 | test("reverse/concat/unshift/pop/map", function() { 83 | var numbers = [1,2,3,4,5]; 84 | numbers = _(numbers).chain() 85 | .reverse() 86 | .concat([5, 5, 5]) 87 | .unshift(17) 88 | .pop() 89 | .map(function(n){ return n * 2; }) 90 | .value(); 91 | equal(numbers.join(', '), "34, 10, 8, 6, 4, 2, 10, 10", 'can chain together array functions.'); 92 | }); 93 | `) 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /underscore_test.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/robertkrimen/otto/terst" 8 | "github.com/robertkrimen/otto/underscore" 9 | ) 10 | 11 | func init() { 12 | underscore.Disable() 13 | } 14 | 15 | var ( 16 | // A persistent handle for the underscore tester 17 | // We do not run underscore tests in parallel, so it is okay to stash globally. 18 | tester *_tester 19 | once sync.Once 20 | ) 21 | 22 | // A tester for underscore: underscoreTest => test(underscore) :). 23 | func underscoreTest() func(string, ...interface{}) Value { 24 | setTester := func() { 25 | tester = newTester() 26 | tester.underscore() // Load underscore and testing shim, etc. 27 | } 28 | once.Do(setTester) 29 | 30 | return tester.test 31 | } 32 | 33 | func (te *_tester) underscore() { 34 | vm := te.vm 35 | _, err := vm.Run(underscore.Source()) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | err = vm.Set("assert", func(call FunctionCall) Value { 41 | if !call.Argument(0).bool() { 42 | message := "Assertion failed" 43 | if len(call.ArgumentList) > 1 { 44 | message = call.ArgumentList[1].string() 45 | } 46 | t := terst.Caller().T() 47 | is(message, nil) 48 | t.Fail() 49 | return falseValue 50 | } 51 | return trueValue 52 | }) 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | _, err = vm.Run(` 58 | var templateSettings; 59 | 60 | function _setup() { 61 | templateSettings = _.clone(_.templateSettings); 62 | } 63 | 64 | function _teardown() { 65 | _.templateSettings = templateSettings; 66 | } 67 | 68 | function module() { 69 | /* Nothing happens. */ 70 | } 71 | 72 | function equals(a, b, emit) { 73 | assert(a == b, emit + ", <" + a + "> != <" + b + ">"); 74 | } 75 | var equal = equals; 76 | 77 | function notStrictEqual(a, b, emit) { 78 | assert(a !== b, emit); 79 | } 80 | 81 | function strictEqual(a, b, emit) { 82 | assert(a === b, emit); 83 | } 84 | 85 | function ok(a, emit) { 86 | assert(a, emit); 87 | } 88 | 89 | function raises(fn, want, emit) { 90 | var have, _ok = false; 91 | if (typeof want === "string") { 92 | emit = want; 93 | want = null; 94 | } 95 | 96 | try { 97 | fn(); 98 | } catch(tmp) { 99 | have = tmp; 100 | } 101 | 102 | if (have) { 103 | if (!want) { 104 | _ok = true; 105 | } 106 | else if (want instanceof RegExp) { 107 | _ok = want.test(have); 108 | } 109 | else if (have instanceof want) { 110 | _ok = true 111 | } 112 | else if (want.call({}, have) === true) { 113 | _ok = true; 114 | } 115 | } 116 | 117 | ok(_ok, emit); 118 | } 119 | 120 | function test(name){ 121 | _setup() 122 | try { 123 | templateSettings = _.clone(_.templateSettings); 124 | if (arguments.length == 3) { 125 | count = 0 126 | for (count = 0; count < arguments[1]; count++) { 127 | arguments[2]() 128 | } 129 | } else { 130 | // For now. 131 | arguments[1]() 132 | } 133 | } 134 | finally { 135 | _teardown() 136 | } 137 | } 138 | 139 | function deepEqual(a, b, emit) { 140 | // Also, for now. 141 | assert(_.isEqual(a, b), emit) 142 | } 143 | `) 144 | if err != nil { 145 | panic(err) 146 | } 147 | } 148 | 149 | func Test_underscore(t *testing.T) { 150 | tt(t, func() { 151 | test := underscoreTest() 152 | 153 | test(` 154 | _.map([1, 2, 3], function(value){ 155 | return value + 1 156 | }) 157 | `, "2,3,4") 158 | 159 | test(` 160 | abc = _.find([1, 2, 3, -1], function(value) { return value == -1 }) 161 | `, -1) 162 | 163 | test(`_.isEqual(1, 1)`, true) 164 | test(`_.isEqual([], [])`, true) 165 | test(`_.isEqual(['b', 'd'], ['b', 'd'])`, true) 166 | test(`_.isEqual(['b', 'd', 'c'], ['b', 'd', 'e'])`, false) 167 | test(`_.isFunction(function(){})`, true) 168 | test(`_.template('

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

')()`, "

\u2028\u2028\u2029\u2029

") 169 | }) 170 | } 171 | 172 | // TODO Test: typeof An argument reference 173 | // TODO Test: abc = {}; abc == Object(abc) 174 | -------------------------------------------------------------------------------- /value_boolean.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "reflect" 7 | "unicode/utf16" 8 | ) 9 | 10 | func (v Value) bool() bool { 11 | if v.kind == valueBoolean { 12 | return v.value.(bool) 13 | } 14 | if v.IsUndefined() || v.IsNull() { 15 | return false 16 | } 17 | switch value := v.value.(type) { 18 | case bool: 19 | return value 20 | case int, int8, int16, int32, int64: 21 | return reflect.ValueOf(value).Int() != 0 22 | case uint, uint8, uint16, uint32, uint64: 23 | return reflect.ValueOf(value).Uint() != 0 24 | case float32: 25 | return value != 0 26 | case float64: 27 | if math.IsNaN(value) || value == 0 { 28 | return false 29 | } 30 | return true 31 | case string: 32 | return len(value) != 0 33 | case []uint16: 34 | return len(utf16.Decode(value)) != 0 35 | } 36 | if v.IsObject() { 37 | return true 38 | } 39 | panic(fmt.Sprintf("unexpected boolean type %T", v.value)) 40 | } 41 | -------------------------------------------------------------------------------- /value_kind.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=valueKind -trimprefix=value -output=value_kind.gen.go"; DO NOT EDIT. 2 | 3 | package otto 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[valueUndefined-0] 12 | _ = x[valueNull-1] 13 | _ = x[valueNumber-2] 14 | _ = x[valueString-3] 15 | _ = x[valueBoolean-4] 16 | _ = x[valueObject-5] 17 | _ = x[valueEmpty-6] 18 | _ = x[valueResult-7] 19 | _ = x[valueReference-8] 20 | } 21 | 22 | const _valueKind_name = "UndefinedNullNumberStringBooleanObjectEmptyResultReference" 23 | 24 | var _valueKind_index = [...]uint8{0, 9, 13, 19, 25, 32, 38, 43, 49, 58} 25 | 26 | func (i valueKind) String() string { 27 | if i < 0 || i >= valueKind(len(_valueKind_index)-1) { 28 | return "valueKind(" + strconv.FormatInt(int64(i), 10) + ")" 29 | } 30 | return _valueKind_name[_valueKind_index[i]:_valueKind_index[i+1]] 31 | } 32 | -------------------------------------------------------------------------------- /value_primitive.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | func toNumberPrimitive(value Value) Value { 4 | return toPrimitive(value, defaultValueHintNumber) 5 | } 6 | 7 | func toPrimitiveValue(value Value) Value { 8 | return toPrimitive(value, defaultValueNoHint) 9 | } 10 | 11 | func toPrimitive(value Value, hint defaultValueHint) Value { 12 | switch value.kind { 13 | case valueNull, valueUndefined, valueNumber, valueString, valueBoolean: 14 | return value 15 | case valueObject: 16 | return value.object().DefaultValue(hint) 17 | default: 18 | panic(hereBeDragons(value.kind, value)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /value_string.go: -------------------------------------------------------------------------------- 1 | package otto 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "regexp" 7 | "strconv" 8 | "unicode/utf16" 9 | ) 10 | 11 | var matchLeading0Exponent = regexp.MustCompile(`([eE][\+\-])0+([1-9])`) // 1e-07 => 1e-7 12 | 13 | // FIXME 14 | // https://code.google.com/p/v8/source/browse/branches/bleeding_edge/src/conversions.cc?spec=svn18082&r=18082 15 | func floatToString(value float64, bitsize int) string { 16 | // TODO Fit to ECMA-262 9.8.1 specification 17 | if math.IsNaN(value) { 18 | return "NaN" 19 | } else if math.IsInf(value, 0) { 20 | if math.Signbit(value) { 21 | return "-Infinity" 22 | } 23 | return "Infinity" 24 | } 25 | exponent := math.Log10(math.Abs(value)) 26 | if exponent >= 21 || exponent < -6 { 27 | return matchLeading0Exponent.ReplaceAllString(strconv.FormatFloat(value, 'g', -1, bitsize), "$1$2") 28 | } 29 | return strconv.FormatFloat(value, 'f', -1, bitsize) 30 | } 31 | 32 | func numberToStringRadix(value Value, radix int) string { 33 | float := value.float64() 34 | switch { 35 | case math.IsNaN(float): 36 | return "NaN" 37 | case math.IsInf(float, 1): 38 | return "Infinity" 39 | case math.IsInf(float, -1): 40 | return "-Infinity" 41 | case float == 0: 42 | return "0" 43 | } 44 | // FIXME This is very broken 45 | // Need to do proper radix conversion for floats, ... 46 | // This truncates large floats (so bad). 47 | return strconv.FormatInt(int64(float), radix) 48 | } 49 | 50 | func (v Value) string() string { 51 | if v.kind == valueString { 52 | switch value := v.value.(type) { 53 | case string: 54 | return value 55 | case []uint16: 56 | return string(utf16.Decode(value)) 57 | } 58 | } 59 | if v.IsUndefined() { 60 | return "undefined" 61 | } 62 | if v.IsNull() { 63 | return "null" 64 | } 65 | switch value := v.value.(type) { 66 | case bool: 67 | return strconv.FormatBool(value) 68 | case int: 69 | return strconv.FormatInt(int64(value), 10) 70 | case int8: 71 | return strconv.FormatInt(int64(value), 10) 72 | case int16: 73 | return strconv.FormatInt(int64(value), 10) 74 | case int32: 75 | return strconv.FormatInt(int64(value), 10) 76 | case int64: 77 | return strconv.FormatInt(value, 10) 78 | case uint: 79 | return strconv.FormatUint(uint64(value), 10) 80 | case uint8: 81 | return strconv.FormatUint(uint64(value), 10) 82 | case uint16: 83 | return strconv.FormatUint(uint64(value), 10) 84 | case uint32: 85 | return strconv.FormatUint(uint64(value), 10) 86 | case uint64: 87 | return strconv.FormatUint(value, 10) 88 | case float32: 89 | if value == 0 { 90 | return "0" // Take care not to return -0 91 | } 92 | return floatToString(float64(value), 32) 93 | case float64: 94 | if value == 0 { 95 | return "0" // Take care not to return -0 96 | } 97 | return floatToString(value, 64) 98 | case []uint16: 99 | return string(utf16.Decode(value)) 100 | case string: 101 | return value 102 | case *object: 103 | return value.DefaultValue(defaultValueHintString).string() 104 | } 105 | panic(fmt.Errorf("%v.string( %T)", v.value, v.value)) 106 | } 107 | --------------------------------------------------------------------------------